# Orca
#
# Copyright 2010 Joanmarie Diggs.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
# pylint: disable=too-many-branches
"""Custom script utilities for LibreOffice"""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2010 Joanmarie Diggs."
__license__ = "LGPL"
from orca import debug
from orca import focus_manager
from orca import input_event_manager
from orca import messages
from orca import script_utilities
from orca.ax_object import AXObject
from orca.ax_selection import AXSelection
from orca.ax_table import AXTable
from orca.ax_text import AXText
from orca.ax_utilities import AXUtilities
class Utilities(script_utilities.Utilities):
"""Custom script utilities for LibreOffice"""
def __init__(self, script):
super().__init__(script)
self._calc_selected_cells = []
self._calc_selected_rows = []
self._calc_selected_columns = []
def is_cell_being_edited(self, obj):
"""Returns True if obj is a cell being edited."""
parent = AXObject.get_parent(obj)
if AXUtilities.is_panel(parent) or AXUtilities.is_extended(parent):
return self.spreadsheet_cell_name(parent)
return False
def spreadsheet_cell_name(self, cell):
"""Returns the accessible name of the cell."""
# TODO - JD: Is this still needed? See also _get_cell_name_for_coordinates.
name_list = AXObject.get_name(cell).split()
for name in name_list:
name = name.replace('.', '')
if not name.isalpha() and name.isalnum():
return name
return ''
def _get_cell_name_for_coordinates(self, obj, row, col, include_contents=False):
# https://bugs.documentfoundation.org/show_bug.cgi?id=158030
cell = AXTable.get_cell_at(obj, row, col)
name = self.spreadsheet_cell_name(cell)
if include_contents:
text = AXText.get_all_text(cell)
name = f"{text} {name}"
return name.strip()
@staticmethod
def flows_from_or_to_selection(obj):
"""Returns True if obj flows to or from another object with selected text."""
# TODO - JD: 1) Is this still needed? 2) If so, move it to AXUtilities.
targets = AXUtilities.get_flows_from(obj)
targets.extend(AXUtilities.get_flows_to(obj))
for target in targets:
if AXText.has_selected_text(target):
return True
return False
def getWordAtOffsetAdjustedForNavigation(self, obj, offset=None):
"""Returns the word in obj at the specified or current offset."""
return AXText.get_word_at_offset(obj, offset)
def shouldReadFullRow(self, obj, prevObj=None):
"""Returns True if the full row in obj should be read."""
if self._script.get_table_navigator().last_input_event_was_navigation_command():
return False
if input_event_manager.get_manager().last_event_was_tab_navigation():
return False
return super().shouldReadFullRow(obj, prevObj)
def _isTopLevelObject(self, obj):
# https://bugs.documentfoundation.org/show_bug.cgi?id=160806
if AXObject.get_parent(obj) is None and AXObject.get_role(obj) in self._topLevelRoles():
tokens = ["SOFFICE:", obj, "has no parent. Treating as top-level."]
debug.print_tokens(debug.LEVEL_INFO, tokens, True, True)
return True
return super()._isTopLevelObject(obj)
def columnConvert(self, column):
"""Convert a spreadsheet column into its column label."""
base26 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
if column <= len(base26):
return base26[column-1]
res = ""
while column > 0:
digit = column % len(base26)
res = " " + base26[digit-1] + res
column = int(column / len(base26))
return res
def _get_coordinates_for_selected_range(self, obj):
if not (AXObject.supports_table(obj) and AXObject.supports_selection(obj)):
tokens = ["SOFFICE:", obj, "does not implement both selection and table"]
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
return (-1, -1), (-1, -1)
first = AXSelection.get_selected_child(obj, 0)
last = AXSelection.get_selected_child(obj, -1)
return AXTable.get_cell_coordinates(first), AXTable.get_cell_coordinates(last)
def getSelectionContainer(self, obj):
"""Returns the selection container of obj."""
# Writer implements the selection interface on the document and all its
# children. The former is interesting, but interferes with our presentation
# of selected text. The latter is just weird.
if AXUtilities.is_document_text(obj):
return None
if AXObject.find_ancestor(obj, AXUtilities.is_document_text):
return None
return super().getSelectionContainer(obj)
def speakSelectedCellRange(self, obj):
"""Speaks the selected cell range."""
first_coords, last_coords = self._get_coordinates_for_selected_range(obj)
if first_coords == (-1, -1) or last_coords == (-1, -1):
return False
self._script.presentationInterrupt()
if first_coords == last_coords:
cell = self._get_cell_name_for_coordinates(obj, *first_coords, True)
self._script.speakMessage(messages.CELL_SELECTED % cell)
return True
cell1 = self._get_cell_name_for_coordinates(obj, *first_coords, True)
cell2 = self._get_cell_name_for_coordinates(obj, *last_coords, True)
self._script.speakMessage(messages.CELL_RANGE_SELECTED % (cell1, cell2))
return True
def handle_cell_selection_change(self, obj):
"""Presents the selection change for obj."""
first_coords, last_coords = self._get_coordinates_for_selected_range(obj)
if first_coords == (-1, -1) or last_coords == (-1, -1):
return True
current = []
for r in range(first_coords[0], last_coords[0]+1):
current.extend((r, c) for c in range(first_coords[1], last_coords[1]+1))
current = set(current)
previous = set(self._calc_selected_cells)
current.discard((-1, -1))
previous.discard((-1, -1))
unselected = sorted(previous.difference(current))
selected = sorted(current.difference(previous))
focus_coords = AXTable.get_cell_coordinates(
focus_manager.get_manager().get_locus_of_focus())
if focus_coords in selected:
selected.remove(focus_coords)
self._calc_selected_cells = sorted(current)
msgs = []
if len(unselected) == 1:
cell = self._get_cell_name_for_coordinates(obj, *unselected[0], True)
msgs.append(messages.CELL_UNSELECTED % cell)
elif len(unselected) > 1:
cell1 = self._get_cell_name_for_coordinates(obj, *unselected[0], True)
cell2 = self._get_cell_name_for_coordinates(obj, *unselected[-1], True)
msgs.append(messages.CELL_RANGE_UNSELECTED % (cell1, cell2))
if len(selected) == 1:
cell = self._get_cell_name_for_coordinates(obj, *selected[0], True)
msgs.append(messages.CELL_SELECTED % cell)
elif len(selected) > 1:
cell1 = self._get_cell_name_for_coordinates(obj, *selected[0], True)
cell2 = self._get_cell_name_for_coordinates(obj, *selected[-1], True)
msgs.append(messages.CELL_RANGE_SELECTED % (cell1, cell2))
if msgs:
self._script.presentationInterrupt()
for msg in msgs:
self._script.speakMessage(msg, interrupt=False)
return bool(msgs)
def handle_row_and_column_selection_change(self, obj):
"""Presents the selection change for obj."""
if not (AXObject.supports_table(obj) and AXObject.supports_selection(obj)):
return True
cols = set(AXTable.get_selected_columns(obj))
rows = set(AXTable.get_selected_rows(obj))
selected_cols = sorted(cols.difference(set(self._calc_selected_columns)))
unselected_cols = sorted(set(self._calc_selected_columns).difference(cols))
def convert_column(x):
return self.columnConvert(x+1)
def convert_row(x):
return x + 1
selected_cols = list(map(convert_column, selected_cols))
unselected_cols = list(map(convert_column, unselected_cols))
selected_rows = sorted(rows.difference(set(self._calc_selected_rows)))
unselected_rows = sorted(set(self._calc_selected_rows).difference(rows))
selected_rows = list(map(convert_row, selected_rows))
unselected_rows = list(map(convert_row, unselected_rows))
self._calc_selected_columns = list(cols)
self._calc_selected_rows = list(rows)
column_count = AXTable.get_column_count(obj)
if len(cols) == column_count:
self._script.speakMessage(messages.DOCUMENT_SELECTED_ALL)
return True
if not cols and len(unselected_cols) == column_count:
self._script.speakMessage(messages.DOCUMENT_UNSELECTED_ALL)
return True
msgs = []
if len(unselected_cols) == 1:
msgs.append(messages.TABLE_COLUMN_UNSELECTED % unselected_cols[0])
elif len(unselected_cols) > 1:
msgs.append(messages.TABLE_COLUMN_RANGE_UNSELECTED % \
(unselected_cols[0], unselected_cols[-1]))
if len(unselected_rows) == 1:
msgs.append(messages.TABLE_ROW_UNSELECTED % unselected_rows[0])
elif len(unselected_rows) > 1:
msgs.append(messages.TABLE_ROW_RANGE_UNSELECTED % \
(unselected_rows[0], unselected_rows[-1]))
if len(selected_cols) == 1:
msgs.append(messages.TABLE_COLUMN_SELECTED % selected_cols[0])
elif len(selected_cols) > 1:
msgs.append(
messages.TABLE_COLUMN_RANGE_SELECTED % (selected_cols[0], selected_cols[-1]))
if len(selected_rows) == 1:
msgs.append(messages.TABLE_ROW_SELECTED % selected_rows[0])
elif len(selected_rows) > 1:
msgs.append(messages.TABLE_ROW_RANGE_SELECTED % (selected_rows[0], selected_rows[-1]))
if msgs:
self._script.presentationInterrupt()
for msg in msgs:
self._script.speakMessage(msg, interrupt=False)
return bool(msgs)