>From 5f645b0e3a8db8c8d16b3373a6c368c576f3616d Mon Sep 17 00:00:00 2001 From: Marcus Habermehl Date: Wed, 26 Jan 2011 01:44:12 +0100 Subject: [PATCH] Patch for testing purpose only - one step to solve #620331; which variant is more comfortable? --- po/POTFILES.in | 2 + src/orca/Makefile.am | 2 + src/orca/object_navigator.py | 549 +++++++++++++++++++++++++++++++++++++ src/orca/orca-object-navigator.ui | 56 ++++ src/orca/structural_navigation.py | 37 +++ 5 files changed, 646 insertions(+), 0 deletions(-) create mode 100644 src/orca/object_navigator.py create mode 100644 src/orca/orca-object-navigator.ui diff --git a/po/POTFILES.in b/po/POTFILES.in index a96dd12..6de4f8b 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -20,6 +20,7 @@ src/orca/keynames.py src/orca/liveregions.py src/orca/mag.py src/orca/notification_messages.py +src/orca/object_navigator.py [type: gettext/glade]src/orca/orca-advanced-magnification.ui src/orca/orca_console_prefs.py [type: gettext/glade]src/orca/orca-find.ui @@ -29,6 +30,7 @@ src/orca/orca_gui_prefs.py src/orca/orca.in [type: gettext/glade]src/orca/orca-mainwin.ui src/orca/orca.py +[type: gettext/glade]src/orca/orca-object-navigator.ui [type: gettext/glade]src/orca/orca-preferences-warning.ui [type: gettext/glade]src/orca/orca-profile.ui [type: gettext/glade]src/orca/orca-quit.ui diff --git a/src/orca/Makefile.am b/src/orca/Makefile.am index 891ecac..2ec9aaf 100644 --- a/src/orca/Makefile.am +++ b/src/orca/Makefile.am @@ -41,6 +41,7 @@ orca_python_PYTHON = \ mag.py \ mouse_review.py \ notification_messages.py \ + object_navigator.py \ orca.py \ orca_console_prefs.py \ orca_gtkbuilder.py \ @@ -85,6 +86,7 @@ ui_DATA = \ orca-advanced-magnification.ui \ orca-find.ui \ orca-mainwin.ui \ + orca-object-navigator.ui \ orca-preferences-warning.ui \ orca-quit.ui \ orca-setup.ui \ diff --git a/src/orca/object_navigator.py b/src/orca/object_navigator.py new file mode 100644 index 0000000..01b80ea --- /dev/null +++ b/src/orca/object_navigator.py @@ -0,0 +1,549 @@ +# Orca +# +# Copyright 2010 Marcus Habermehl +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Library General Public +# License as published by the Free Software Foundation; either +# version 2 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 +# Library General Public License for more details. +# +# You should have received a copy of the GNU Library 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. + +"""Displays a list of objects for structural navigation.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2011 Marcus Habermehl" +__license__ = "LGPL" + +import gtk +import os +import pyatspi + +import debug +import orca +import orca_gtkbuilder +import orca_platform +import orca_state +import rolenames + +from orca_i18n import _ # for gettext support + +FORMATTING = { + pyatspi.ROLE_CHECK_BOX: "%(name)s %(role)s %(state)s %(required)s", + pyatspi.ROLE_COMBO_BOX: "%(name)s %(content)s %(role)s %(required)s", + pyatspi.ROLE_ENTRY: "%(name)s %(role)s %(content)s %(required)s", + pyatspi.ROLE_FRAME: "%(name)s %(role)s", + pyatspi.ROLE_HEADING: "%(name)s %(role)s", + pyatspi.ROLE_IMAGE: "%(name)s %(role)s", + pyatspi.ROLE_INTERNAL_FRAME: "%(name)s %(role)s", + pyatspi.ROLE_LABEL: "%(name)s %(role)s", + pyatspi.ROLE_LINK: "%(name)s %(role)s", + pyatspi.ROLE_LIST: "%(name)s %(content)s %(role)s %(required)s", + pyatspi.ROLE_PARAGRAPH: "%(name)s %(role)s", + pyatspi.ROLE_PASSWORD_TEXT: "%(name)s %(role)s %(content)s %(required)s", + pyatspi.ROLE_PUSH_BUTTON: "%(name)s %(role)s", + pyatspi.ROLE_RADIO_BUTTON: "%(name)s %(state)s %(role)s %(required)s", + pyatspi.ROLE_SECTION: "%(name)s %(role)s", + pyatspi.ROLE_TEXT: "%(name)s %(role)s %(content)s %(required)s", + "fallback": "%(name)s %(role)s" +} + +OBJECTS = { + "anchor": _("_Anchors"), + "blockquote": _("Block _quotes"), + "button": _("_Buttons"), + "checkBox": _("Check bo_xes"), + "comboBox": _("_Combo boxes"), + "entry": _("_Entries"), + "formField": _("_Form fields"), + "heading": _("_Headings"), + "landmark": _("Land_marks"), + "link": _("Li_nks"), + "list": _("_Lists"), + "listItem": _("List _items"), + "liveRegion": _("Live re_gions"), + "paragraph": _("_Paragraphs"), + "radioButton": _("_Radio buttons"), + "separator": _("_Separators"), + "table": _("_Tables"), + "unvisitedLink": _("_Unvisited links"), + "visitedLink": _("_Visited links") +} + +class ObjectNavigatorDialog(orca_gtkbuilder.GtkBuilderWrapper): + def __init__(self, fileName, script): + orca_gtkbuilder.GtkBuilderWrapper.__init__( + self, fileName, "objectNavigator") + + self._script = script + + self._objectStore = gtk.TreeStore(str, object, object) + + self.get_widget("objectView").set_model(self._objectStore) + + entryRenderer = gtk.CellRendererText() + + entryColumn = gtk.TreeViewColumn("", entryRenderer) + entryColumn.add_attribute(entryRenderer, "text", 0) + self.get_widget("objectView").append_column(entryColumn) + + for i in range(2): + objectRenderer = gtk.CellRendererText() + + objectColumn = gtk.TreeViewColumn("", objectRenderer) + objectColumn.set_visible(False) + self.get_widget("objectView").append_column(objectColumn) + + def buildTheLabel(self, obj): + """Return a string that represents the label for an entry of + the HTML object list. + + Arguments: + - obj: the accessible object. + """ + + labelVars = {} + + labelVars["content"] = getContent(obj) + + # Don't overload the labels + if len(labelVars["content"]) > 50: + labelVars["content"] = labelVars["content"][:50] + "..." + + labelVars["name"] = obj.name + + if not labelVars["name"]: + if self._script.getStructuralNavigation()._formFieldPredicate(obj): + labelVars["name"] = self._script.guessTheLabel(obj, False) + + if not labelVars["name"]: + try: + labelVars["name"] = removeSpam(obj.queryText().getText(0, -1)) + except NotImplementedError: + pass + + if not labelVars["name"]: + # Translators: this is the label/name for unlabeled/unnamed + # HTML objects. + # + labelVars["name"] = _("Unnamed") + + if len(labelVars["name"]) > 50: + labelVars["name"] = labelVars["name"][:50] + "..." + + labelVars["required"] = getRequired(obj) + + labelVars["role"] = getRole(obj) + + labelVars["state"] = getState(obj) + + if obj.getRole() in FORMATTING: + label = FORMATTING[obj.getRole()] % labelVars + else: + label = FORMATTING["fallback"] % labelVars + + while label.count(" ") > 0: + label = label.replace(" ", " ") + + return label + + def getObjects(self, document, enabledObjects): + matches = {} + + for i in enabledObjects: + matches[enabledObjects[i].objType] = [] + + keys = matches.keys() + + if "unvisitedLink" in keys or "visitedLink" in keys: + keys.append("link") + + keys.sort() + + for key in keys: + matches[key] = [] + + objects = pyatspi.findAllDescendants(document, bool) + + for obj in objects: + for key in enabledObjects: + if not enabledObjects[key].objType in OBJECTS: + continue + + if enabledObjects[key].predicate(obj): + matches[key].append([obj, enabledObjects[key].present]) + + if "link" in keys: + if enabledObjects["unvisitedLink"].predicate(obj): + matches["link"].append( + [obj, enabledObjects["unvisitedLink"].present]) + elif enabledObjects["visitedLink"].predicate(obj): + matches["link"].append( + [obj, enabledObjects["visitedLink"].present]) + + return keys, matches + + def init(self, document, enabledObjects): + found = False + keys, matches = self.getObjects(document, enabledObjects) + + for key in keys: + if len(matches[key]) > 0: + found = True + parentIter = self._objectStore.append(None, + [OBJECTS[key].replace("_", ""), None, None]) + for (obj, present) in matches[key]: + self._objectStore.append(parentIter, + [self.buildTheLabel(obj), present, obj]) + + return found + + def onFocusOutEvent(self, widget, event): + self.get_widget("objectNavigator").destroy() + + def onRowActivated(self, treeview, path, viewColumn): + objectStore, treeIter = treeview.get_selection().get_selected() + + present, obj = objectStore.get(treeIter, 1, 2) + + if not present or not obj: + return + + self.get_widget("objectNavigator").destroy() + + while gtk.events_pending(): + gtk.main_iteration() + + present(obj) + + def showGUI(self, timestamp): + """Show the HTML object navigator dialog. This assumes that the GUI has + alreaddy been created. + """ + + # Set the current time on the object navigator GUI dialog so that it'll + # get focus. set_user_time is a new call in pygtk 2.9.2 or later. + # It's surronded by a try/except block here so that if it's not found, + # then we can fail gracefully. + # + try: + self.get_widget("objectNavigator").realize() + ts = orca_state.lastInputEventTimestamp + if ts == 0: + ts = gtk.get_current_event_time() + self.get_widget("objectNavigator").window.set_user_time(ts) + except AttributeError: + debug.printException(debug.LEVEL_FINEST) + + self.get_widget("objectNavigator").show() + +class ObjectNavigatorMenu(gtk.Menu): + def __init__(self, script): + gtk.Menu.__init__(self) + + self._script = script + + def buildTheLabel(self, obj): + """Return a string that represents the label for an entry of + the HTML object list. + + Arguments: + - obj: the accessible object. + """ + + labelVars = {} + + labelVars["content"] = getContent(obj) + + # Don't overload the labels + if len(labelVars["content"]) > 50: + labelVars["content"] = labelVars["content"][:50] + "..." + + labelVars["name"] = obj.name + + if not labelVars["name"]: + if self._script.getStructuralNavigation()._formFieldPredicate(obj): + labelVars["name"] = self._script.guessTheLabel(obj, False) + + if not labelVars["name"]: + try: + labelVars["name"] = removeSpam(obj.queryText().getText(0, -1)) + except NotImplementedError: + pass + + if not labelVars["name"]: + # Translators: this is the label/name for unlabeled/unnamed + # HTML objects. + # + labelVars["name"] = _("Unnamed") + + if len(labelVars["name"]) > 50: + labelVars["name"] = labelVars["name"][:50] + "..." + + labelVars["required"] = getRequired(obj) + + labelVars["role"] = getRole(obj) + + labelVars["state"] = getState(obj) + + if obj.getRole() in FORMATTING: + label = FORMATTING[obj.getRole()] % labelVars + else: + label = FORMATTING["fallback"] % labelVars + + while label.count(" ") > 0: + label = label.replace(" ", " ") + + return label + + def getObjects(self, document, enabledObjects): + matches = {} + + for i in enabledObjects: + matches[enabledObjects[i].objType] = [] + + keys = matches.keys() + + if "unvisitedLink" in keys or "visitedLink" in keys: + keys.append("link") + + keys.sort() + + for key in keys: + matches[key] = [] + + objects = pyatspi.findAllDescendants(document, bool) + + for obj in objects: + for key in enabledObjects: + if not enabledObjects[key].objType in OBJECTS: + continue + + if enabledObjects[key].predicate(obj): + matches[key].append([obj, enabledObjects[key].present]) + + if "link" in keys: + if enabledObjects["unvisitedLink"].predicate(obj): + matches["link"].append( + [obj, enabledObjects["unvisitedLink"].present]) + elif enabledObjects["visitedLink"].predicate(obj): + matches["link"].append( + [obj, enabledObjects["visitedLink"].present]) + + return keys, matches + + def init(self, document, enabledObjects): + found = False + keys, matches = self.getObjects(document, enabledObjects) + + for key in keys: + if len(matches[key]) > 0: + found = True + subMenu = gtk.Menu() + menuItem = gtk.MenuItem(OBJECTS[key]) + menuItem.set_submenu(subMenu) + self.append(menuItem) + for (obj, present) in matches[key]: + subItem = gtk.MenuItem("_%s" % self.buildTheLabel(obj)) + subItem.connect("activate", self.onActivation, obj, present) + subMenu.append(subItem) + + return found + + def onActivation(self, menuItem, obj, present, arg = None): + while gtk.events_pending(): + gtk.main_iteration() + + present(obj, arg) + + def showGUI(self, timestamp): + self.show_all() + self.select_first(True) + self.popup(None, None, None, 0, timestamp) + +def getContent(obj): + """Return the text contained in an accessible HTML document. + + Arguments: + - obj: the accessible object + """ + + content = "" + + if obj.getRole() == pyatspi.ROLE_COMBO_BOX: + if obj.getChildAtIndex(0).getRole() == pyatspi.ROLE_MENU: + selection = obj.getChildAtIndex(0).querySelection() + for i in range(selection.nSelectedChildren): + content = "%s %s" % (content, + selection.getSelectedChild(i).name) + elif obj.getRole() == pyatspi.ROLE_LIST and \ + obj.getState().contains(pyatspi.STATE_FOCUSABLE): + selection = obj.querySelection() + for i in range(selection.nSelectedChildren): + content = "%s %s" % (content, selection.getSelectedChild(i).name) + elif obj.getRole() in [pyatspi.ROLE_ENTRY, + pyatspi.ROLE_PASSWORD_TEXT, + pyatspi.ROLE_SPIN_BUTTON]: + content = obj.queryEditableText().getText(0, -1) + + return removeSpam(content) + +def getRequired(obj): + """Return a string that indicates a 'required' form field of + a HTML form. + + Arguments: + - obj: the accessible object + """ + + if obj.getState().contains(pyatspi.STATE_REQUIRED): + # Translators: this indicates a 'required' form field of + # a HTML form. + # + return _("required") + + return "" + +def getRole(obj): + """Return a string that represents the role of an accessible + HTML object. + + Arguments: + - obj: the accessible object + """ + + labelRole = rolenames.rolenames[obj.getRole()].speech + + if obj.getRole() == pyatspi.ROLE_HEADING: + level = "" + + for i in obj.getAttributes(): + if i.startswith("level:"): + level = int(i[6:]) + break + + # Translators: this is the role of a heading, extended with the + # heading level. + # + labelRole = _("%(role)s level %(level)d") % {"role": labelRole, + "level": level} + elif obj.getRole() == pyatspi.ROLE_LINK: + if obj.getState().contains(pyatspi.STATE_VISITED): + # Translators: this is the role of a visited link. + # + labelRole = _("visited %s") % labelRole + else: + # Translators: this is the role of an unvisited link. + # + labelRole = _("unvisited %s") % labelRole + elif obj.getRole() == pyatspi.ROLE_LIST: + if obj.getState().contains(pyatspi.STATE_MULTISELECTABLE): + # Translators: this is the role of a multiselectable list. + # + labelRole = _("multi-select %s") % labelRole + + if obj.childCount == 1: + # Translators: this is the role of a list with a single item. + # + labelRole = _("%s with one item") % labelRole + elif obj.childCount > 1: + # Translators: this is the role of a list with more than one item. + # + labelRole = _("%(role)s with %(items)d items") % { + "role": labelRole, + "items": obj.childCount} + + return labelRole + +def getState(obj): + """Return a string that represents the state of form fields that + can have different states, like check boxes. + + Arguments: + - obj: the accessible object + """ + + if obj.getRole() == pyatspi.ROLE_CHECK_BOX: + if obj.getState().contains(pyatspi.STATE_CHECKED): + # Translators: this indicates that a check box is checked. + # + return _("checked") + elif obj.getState().contains(pyatspi.STATE_INDETERMINATE): + # Translators: this indicates that a check box is partially checked. + # + return _("partially checked") + else: + # Translators: this indicates that a check box isn't checked. + # + return _("not checked") + elif obj.getRole() == pyatspi.ROLE_RADIO_BUTTON: + if obj.getState().contains(pyatspi.STATE_CHECKED): + # Translators: this indicates that a radio button is selected. + # + return _("selected") + else: + # Translators: this indicates that a radio button isn't selected. + # + return _("not selected") + elif obj.getRole() == pyatspi.ROLE_TOGGLE_BUTTON: + if obj.getState().contains(pyatspi.STATE_CHECKED): + # Translators: this indicates that a toggle button is pressed. + # + return _("pressed") + else: + # Translators: this indicates that a toggle button isn't pressed. + # + return _("not pressed") + + return "" + +def removeSpam(string): + string = string.replace("\xef\xbf\xbc", " ") + string = string.replace("\xc2\xa0", " ") + string = string.strip() + string = string.rstrip() + return string + +def showObjectNavigator(widgetType, script, document, enabledObjects, timestamp): + import time + start = time.localtime() + + orca.speech.speak( + _("Please wait while objects are collected."), + orca.settings.voices.get(orca.settings.SYSTEM_VOICE)) + + if widgetType == "dialog": + uiFile = os.path.join(orca_platform.prefix, + orca_platform.datadirname, + orca_platform.package, + "ui", + "orca-object-navigator.ui") + objectNavigator = ObjectNavigatorDialog(uiFile, script) + else: + objectNavigator = ObjectNavigatorMenu(script) + + if objectNavigator.init(document, enabledObjects): + objectNavigator.showGUI(timestamp) + else: + orca.speech.speak( + # Translators: this message is spoken if no structural navigation + # objects are found. + # + _("No objects to be displayed for the structural navigation."), + orca.settings.voices.get(orca.settings.SYSTEM_VOICE)) + + end = time.localtime() + required = (((end[4] * 60) + end[5]) - ((start[4] * 60) + start[5])) + print "We needed %d seconds to perform showObjectNavigator:%s." % \ + (required, widgetType) + diff --git a/src/orca/orca-object-navigator.ui b/src/orca/orca-object-navigator.ui new file mode 100644 index 0000000..8212c0d --- /dev/null +++ b/src/orca/orca-object-navigator.ui @@ -0,0 +1,56 @@ + + + + + + 5 + Object Navigator + normal + + + + True + 2 + + + True + True + automatic + automatic + + + 400 + 200 + True + True + False + True + + + + + + 1 + + + + + True + end + + + + + + + + + False + end + 0 + + + + + + diff --git a/src/orca/structural_navigation.py b/src/orca/structural_navigation.py index 15f2a85..d7dff84 100644 --- a/src/orca/structural_navigation.py +++ b/src/orca/structural_navigation.py @@ -32,6 +32,7 @@ import pyatspi import debug import input_event import keybindings +import object_navigator import orca import orca_state import settings @@ -595,6 +596,18 @@ class StructuralNavigation: structuralNavigationObject.inputEventHandlers) self.functions.extend(structuralNavigationObject.functions) + self.inputEventHandlers["showObjectNavigatorDialog"] = \ + input_event.InputEventHandler( + self.showObjectNavigatorDialog, + _("Show the over-all list of structural " + "navigation objects as dialog.")) + + self.inputEventHandlers["showObjectNavigatorMenu"] = \ + input_event.InputEventHandler( + self.showObjectNavigatorMenu, + _("Show the over-all list of structural " + "navigation objects as menu.")) + def getKeyBindings(self): """Defines the structural navigation key bindings for a script. @@ -618,6 +631,20 @@ class StructuralNavigation: for keybinding in bindings: keyBindings.add(keybinding) + keyBindings.add( + keybindings.KeyBinding( + "F8", + settings.defaultModifierMask, + settings.ORCA_CTRL_MODIFIER_MASK, + self.inputEventHandlers["showObjectNavigatorDialog"])) + + keyBindings.add( + keybindings.KeyBinding( + "F9", + settings.defaultModifierMask, + settings.ORCA_CTRL_MODIFIER_MASK, + self.inputEventHandlers["showObjectNavigatorMenu"])) + return keyBindings ######################################################################### @@ -659,6 +686,16 @@ class StructuralNavigation: debug.println(debug.LEVEL_CONFIGURATION, string) self._script.presentMessage(string) + def showObjectNavigatorDialog(self, script, inputEvent): + object_navigator.showObjectNavigator( + "dialog", script, self._getDocument(), + self.enabledObjects, inputEvent.timestamp) + + def showObjectNavigatorMenu(self, script, inputEvent): + object_navigator.showObjectNavigator( + "menu", script, self._getDocument(), + self.enabledObjects, inputEvent.timestamp) + ######################################################################### # # # Methods for Moving to Objects # -- 1.7.1