%PDF- %PDF-
| Direktori : /lib/python3/dist-packages/orca/scripts/apps/soffice/ |
| Current File : //lib/python3/dist-packages/orca/scripts/apps/soffice/script_utilities.py |
# 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.
"""Commonly-required utility methods needed by -- and potentially
customized by -- application and toolkit scripts. They have
been pulled out from the scripts because certain scripts had
gotten way too large as a result of including these methods."""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2010 Joanmarie Diggs."
__license__ = "LGPL"
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
import orca.debug as debug
import orca.focus_manager as focus_manager
import orca.keybindings as keybindings
import orca.messages as messages
import orca.script_utilities as 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
#############################################################################
# #
# Utilities #
# #
#############################################################################
class Utilities(script_utilities.Utilities):
def __init__(self, script):
"""Creates an instance of the Utilities class.
Arguments:
- script: the script with which this instance is associated.
"""
script_utilities.Utilities.__init__(self, script)
self._calcSelectedCells = []
self._calcSelectedRows = []
self._calcSelectedColumns = []
#########################################################################
# #
# Utilities for finding, identifying, and comparing accessibles #
# #
#########################################################################
def displayedText(self, obj):
"""Returns the text being displayed for an object. Overridden here
because OpenOffice uses symbols (e.g. ">>" for buttons but exposes
more useful information via the accessible's name.
Arguments:
- obj: the object
Returns the text being displayed for an object or None if there isn't
any text being shown.
"""
name = AXObject.get_name(obj)
if name and AXUtilities.is_push_button(obj):
return name
if AXUtilities.is_table_cell(obj):
strings = list(map(self.displayedText, [x for x in AXObject.iter_children(obj)]))
text = "\n".join(strings)
if text.strip():
return text
try:
text = super().displayedText(obj)
except Exception:
return ""
# TODO - JD: This is needed because the default behavior is to fall
# back on the name, which is bogus. Once that has been fixed, this
# hack can go.
# https://bugs.documentfoundation.org/show_bug.cgi?id=158030
if AXUtilities.is_table_cell(obj) and text == name \
and (self.isSpreadSheetCell(obj) or self.isTextDocumentCell(obj)):
return ""
# More bogusness from (at least) Calc combined with the aforementioned
# fallback-to-name behavior....
# https://bugs.documentfoundation.org/show_bug.cgi?id=158029
if self.isDocument(obj) and text == name and text.startswith("file:///"):
return ""
return text
def isCellBeingEdited(self, obj):
parent = AXObject.get_parent(obj)
if AXUtilities.is_panel(parent) or AXUtilities.is_extended(parent):
return self.spreadSheetCellName(parent)
return False
def spreadSheetCellName(self, cell):
nameList = AXObject.get_name(cell).split()
for name in nameList:
name = name.replace('.', '')
if not name.isalpha() and name.isalnum():
return name
return ''
def isSameObject(self, obj1, obj2, comparePaths=False, ignoreNames=False,
ignoreDescriptions=True):
if obj1 == obj2:
return True
if not AXUtilities.have_same_role(obj1, obj2):
return False
if AXUtilities.is_paragraph(obj1):
return False
name = AXObject.get_name(obj1)
if name == AXObject.get_name(obj2) and AXUtilities.is_frame(obj1):
return True
return super().isSameObject(obj1, obj2, comparePaths, ignoreNames)
def isLayoutOnly(self, obj):
"""Returns True if the given object is a container which has
no presentable information (label, name, displayed text, etc.)."""
if AXUtilities.is_list(obj):
if AXUtilities.is_combo_box(AXObject.get_parent(obj)):
return True
return super().isLayoutOnly(obj)
name = AXObject.get_name(obj)
if not name:
return super().isLayoutOnly(obj)
if AXUtilities.is_frame(obj):
return name == AXObject.get_name(focus_manager.getManager().get_active_window())
if AXUtilities.is_panel(obj) and AXObject.get_child_count(obj):
if AXObject.get_name(AXObject.get_child(obj, 0)) == name:
return True
return super().isLayoutOnly(obj)
def frameAndDialog(self, obj):
"""Returns the frame and (possibly) the dialog containing
the object. Overridden here for presentation of the title
bar information: If the locusOfFocus is a spreadsheet cell,
1) we are not in a dialog and 2) we need to present both the
frame name and the sheet name. So we might as well return the
sheet in place of the dialog so that the default code can do
its thing.
"""
if not self.isSpreadSheetCell(obj):
return script_utilities.Utilities.frameAndDialog(self, obj)
results = [None, None]
parent = AXObject.get_parent_checked(obj)
while parent:
if AXObject.get_role(parent) == Atspi.Role.FRAME:
results[0] = parent
if AXObject.get_role(parent) == Atspi.Role.TABLE:
results[1] = parent
parent = AXObject.get_parent_checked(parent)
return results
@staticmethod
def _flowsFromOrToSelection(obj):
relationSet = AXObject.get_relations(obj)
flows = [Atspi.RelationType.FLOWS_FROM, Atspi.RelationType.FLOWS_TO]
relations = filter(lambda r: r.get_relation_type() in flows, relationSet)
targets = [r.get_target(0) for r in relations]
for target in targets:
if AXText.has_selected_text(target):
return True
return False
def objectContentsAreInClipboard(self, obj=None):
obj = obj or focus_manager.getManager().get_locus_of_focus()
if not obj:
return False
if self.isSpreadSheetCell(obj):
contents = self.getClipboardContents()
string = self.displayedText(obj) or "\n"
return string in contents
return super().objectContentsAreInClipboard(obj)
#########################################################################
# #
# Miscellaneous Utilities #
# #
#########################################################################
def isAutoTextEvent(self, event):
"""Returns True if event is associated with text being autocompleted
or autoinserted or autocorrected or autosomethingelsed.
Arguments:
- event: the accessible event being examined
"""
if AXObject.get_role(event.source) != Atspi.Role.PARAGRAPH:
return False
lastKey, mods = self.lastKeyAndModifiers()
if event.type.startswith("object:text-changed:insert"):
if not event.any_data:
return False
if lastKey == "Tab" and event.any_data != "\t":
return True
if lastKey in ["BackSpace", "ISO_Left_Tab"]:
return True
if event.type.startswith("focus:") and lastKey == "Return":
return AXText.get_character_count(event.source) > 0
return False
def containingComboBox(self, obj):
if AXUtilities.is_combo_box(obj):
comboBox = obj
else:
comboBox = AXObject.find_ancestor(obj, AXUtilities.is_combo_box)
if not comboBox:
return None
if AXObject.is_valid(comboBox):
return comboBox
parent = AXObject.get_parent(comboBox)
if not parent:
return comboBox
replicant = self.findReplicant(parent, comboBox)
if replicant and AXObject.is_valid(replicant):
comboBox = replicant
return comboBox
def isComboBoxSelectionChange(self, event):
comboBox = self.containingComboBox(event.source)
if not comboBox:
return False
lastKey, mods = self.lastKeyAndModifiers()
if lastKey not in ["Down", "Up"]:
return False
return True
def isComboBoxNoise(self, event):
role = AXObject.get_role(event.source)
if role == Atspi.Role.TEXT and event.type.startswith("object:text-"):
return self.isComboBoxSelectionChange(event)
return False
def isPresentableTextChangedEventForLocusOfFocus(self, event):
if self.isComboBoxNoise(event):
msg = "SOFFICE: Event is believed to be combo box noise"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
return super().isPresentableTextChangedEventForLocusOfFocus(event)
def isReadOnlyTextArea(self, obj):
if not super().isReadOnlyTextArea(obj):
return False
return not self.inDocumentContent(obj)
def isSelectedTextDeletionEvent(self, event):
if event.type.startswith("object:state-changed:selected") and not event.detail1:
return self.lastInputEventWasDelete() and focus_manager.getManager().focus_is_dead()
return super().isSelectedTextDeletionEvent(event)
def lastInputEventWasRedo(self):
if super().lastInputEventWasRedo():
return True
keyString, mods = self.lastKeyAndModifiers()
if mods & keybindings.COMMAND_MODIFIER_MASK and keyString.lower() == 'y':
return not (mods & keybindings.SHIFT_MODIFIER_MASK)
return False
def selectedChildren(self, obj):
# TODO - JD: Are these overrides still needed? They appear to be
# quite old.
if obj is None:
return []
if not AXObject.supports_selection(obj) and AXUtilities.is_combo_box(obj):
child = AXObject.find_descendant(obj, AXObject.supports_selection)
if child:
return super().selectedChildren(child)
# Things only seem broken for certain tables, e.g. the Paths table.
# TODO - JD: File the LibreOffice bugs and reference them here.
if not AXUtilities.is_table(obj):
return super().selectedChildren(obj)
# We will need to special case this due to the possibility of there
# being lots of children (which may also prove to be invalid objects).
# This is why we can't have nice things.
if self.isSpreadSheetTable(obj):
return []
return AXSelection.get_selected_children(obj)
def getWordAtOffsetAdjustedForNavigation(self, obj, offset=None):
return AXText.get_word_at_offset(obj, offset)
def shouldReadFullRow(self, obj, prevObj=None):
if self._script.getTableNavigator().last_input_event_was_navigation_command():
return False
lastKey, mods = self.lastKeyAndModifiers()
if lastKey in ["Tab", "ISO_Left_Tab"]:
return False
return super().shouldReadFullRow(obj, prevObj)
def presentEventFromNonShowingObject(self, event):
return self.inDocumentContent(event.source)
def columnConvert(self, column):
""" Convert a spreadsheet column into it's 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 _getCellNameForCoordinates(self, obj, row, col, includeContents=False):
# https://bugs.documentfoundation.org/show_bug.cgi?id=158030
cell = AXTable.get_cell_at(obj, row, col)
name = self.spreadSheetCellName(cell)
if includeContents:
text = self.displayedText(cell)
name = f"{text} {name}"
return name.strip()
def _getCoordinatesForSelectedRange(self, obj):
if not (AXObject.supports_table(obj) and AXObject.supports_selection(obj)):
tokens = ["SOFFICE:", obj, "does not implement both selection and table"]
debug.printTokens(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):
# 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):
firstCoords, lastCoords = self._getCoordinatesForSelectedRange(obj)
if firstCoords == (-1, -1) or lastCoords == (-1, -1):
return False
self._script.presentationInterrupt()
if firstCoords == lastCoords:
cell = self._getCellNameForCoordinates(obj, *firstCoords, True)
self._script.speakMessage(messages.CELL_SELECTED % cell)
return True
cell1 = self._getCellNameForCoordinates(obj, *firstCoords, True)
cell2 = self._getCellNameForCoordinates(obj, *lastCoords, True)
self._script.speakMessage(messages.CELL_RANGE_SELECTED % (cell1, cell2))
return True
def handleCellSelectionChange(self, obj):
firstCoords, lastCoords = self._getCoordinatesForSelectedRange(obj)
if firstCoords == (-1, -1) or lastCoords == (-1, -1):
return True
current = []
for r in range(firstCoords[0], lastCoords[0]+1):
current.extend((r, c) for c in range(firstCoords[1], lastCoords[1]+1))
current = set(current)
previous = set(self._calcSelectedCells)
current.discard((-1, -1))
previous.discard((-1, -1))
unselected = sorted(previous.difference(current))
selected = sorted(current.difference(previous))
focusCoords = AXTable.get_cell_coordinates(focus_manager.getManager().get_locus_of_focus())
if focusCoords in selected:
selected.remove(focusCoords)
self._calcSelectedCells = sorted(current)
msgs = []
if len(unselected) == 1:
cell = self._getCellNameForCoordinates(obj, *unselected[0], True)
msgs.append(messages.CELL_UNSELECTED % cell)
elif len(unselected) > 1:
cell1 = self._getCellNameForCoordinates(obj, *unselected[0], True)
cell2 = self._getCellNameForCoordinates(obj, *unselected[-1], True)
msgs.append(messages.CELL_RANGE_UNSELECTED % (cell1, cell2))
if len(selected) == 1:
cell = self._getCellNameForCoordinates(obj, *selected[0], True)
msgs.append(messages.CELL_SELECTED % cell)
elif len(selected) > 1:
cell1 = self._getCellNameForCoordinates(obj, *selected[0], True)
cell2 = self._getCellNameForCoordinates(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(len(msgs))
def handleRowAndColumnSelectionChange(self, 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))
selectedCols = sorted(cols.difference(set(self._calcSelectedColumns)))
unselectedCols = sorted(set(self._calcSelectedColumns).difference(cols))
def convertColumn(x):
return self.columnConvert(x+1)
def convertRow(x):
return x + 1
selectedCols = list(map(convertColumn, selectedCols))
unselectedCols = list(map(convertColumn, unselectedCols))
selectedRows = sorted(rows.difference(set(self._calcSelectedRows)))
unselectedRows = sorted(set(self._calcSelectedRows).difference(rows))
selectedRows = list(map(convertRow, selectedRows))
unselectedRows = list(map(convertRow, unselectedRows))
self._calcSelectedColumns = list(cols)
self._calcSelectedRows = list(rows)
columnCount = AXTable.get_column_count(obj)
if len(cols) == columnCount:
self._script.speakMessage(messages.DOCUMENT_SELECTED_ALL)
return True
if not cols and len(unselectedCols) == columnCount:
self._script.speakMessage(messages.DOCUMENT_UNSELECTED_ALL)
return True
msgs = []
if len(unselectedCols) == 1:
msgs.append(messages.TABLE_COLUMN_UNSELECTED % unselectedCols[0])
elif len(unselectedCols) > 1:
msgs.append(messages.TABLE_COLUMN_RANGE_UNSELECTED % \
(unselectedCols[0], unselectedCols[-1]))
if len(unselectedRows) == 1:
msgs.append(messages.TABLE_ROW_UNSELECTED % unselectedRows[0])
elif len(unselectedRows) > 1:
msgs.append(messages.TABLE_ROW_RANGE_UNSELECTED % \
(unselectedRows[0], unselectedRows[-1]))
if len(selectedCols) == 1:
msgs.append(messages.TABLE_COLUMN_SELECTED % selectedCols[0])
elif len(selectedCols) > 1:
msgs.append(messages.TABLE_COLUMN_RANGE_SELECTED % (selectedCols[0], selectedCols[-1]))
if len(selectedRows) == 1:
msgs.append(messages.TABLE_ROW_SELECTED % selectedRows[0])
elif len(selectedRows) > 1:
msgs.append(messages.TABLE_ROW_RANGE_SELECTED % (selectedRows[0], selectedRows[-1]))
if msgs:
self._script.presentationInterrupt()
for msg in msgs:
self._script.speakMessage(msg, interrupt=False)
return bool(len(msgs))