diff options
author | René Stadler <mail@renestadler.de> | 2008-11-11 23:52:57 +0200 |
---|---|---|
committer | René Stadler <mail@renestadler.de> | 2008-11-15 16:23:51 +0200 |
commit | 4e8178dce43fc3627e3729c4de89a22010c83121 (patch) | |
tree | c7e558826dca62b297fbbc564bdf0e5f3a45e6d3 | |
parent | 36c555e6cc497fd85bd8577741d7af5ecdc14f56 (diff) |
Split giant GUI module into submodules
-rw-r--r-- | GstInspector/GUI.py | 3486 | ||||
-rw-r--r-- | GstInspector/GUI/__init__.py | 92 | ||||
-rw-r--r-- | GstInspector/GUI/actions.py | 136 | ||||
-rw-r--r-- | GstInspector/GUI/app.py | 165 | ||||
-rw-r--r-- | GstInspector/GUI/columns.py | 421 | ||||
-rw-r--r-- | GstInspector/GUI/filters.py | 740 | ||||
-rw-r--r-- | GstInspector/GUI/models.py | 232 | ||||
-rw-r--r-- | GstInspector/GUI/pages.py | 905 | ||||
-rw-r--r-- | GstInspector/GUI/state.py | 291 | ||||
-rw-r--r-- | GstInspector/GUI/utils.py | 168 | ||||
-rw-r--r-- | GstInspector/GUI/window.py | 607 | ||||
-rw-r--r-- | po/POTFILES.in | 11 | ||||
-rwxr-xr-x | setup.py | 3 |
13 files changed, 3769 insertions, 3488 deletions
diff --git a/GstInspector/GUI.py b/GstInspector/GUI.py deleted file mode 100644 index a3757ee..0000000 --- a/GstInspector/GUI.py +++ /dev/null @@ -1,3486 +0,0 @@ -# -*- coding: utf-8; mode: python; -*- -# -# GStreamer Inspector - Multimedia system plugin introspection -# -# Copyright (C) 2007 René Stadler <mail@renestadler.de> -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation; either version 3 of the License, or (at your option) -# any later version. -# -# This program 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 General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# this program. If not, see <http://www.gnu.org/licenses/>. - -"""GStreamer Inspector GUI module.""" - -import os -import logging -import locale -from gettext import gettext, ngettext - -import pygtk -pygtk.require ("2.0") -del pygtk - -import gobject -import pango -import gtk -import gtk.glade - -from GstInspector import Data, main, utils - -# Pre 2.10 GTK+ compatibility. -_gtk_has_link_buttons = hasattr (gtk, "LinkButton") - -# This allows for having translated strings as class attributes the easy way. -# At the end of the file, _ is replaced with the regular gettext function. -def _ (s): - def get (obj): - return gettext (s) - return utils.ClassProperty (get) - -# Workaround for a pygtk deficiency. -TREE_SORTABLE_UNSORTED_COLUMN_ID = -1 -def tree_sortable_get_sort_column_id (model): - col, order = model.get_sort_column_id () - if col is None: - return (TREE_SORTABLE_UNSORTED_COLUMN_ID, - gtk.SORT_ASCENDING,) - else: - return (col, order,) - -def walk_tree_model (model): - - tree_iter = model.get_iter_root () - - while tree_iter: - yield tree_iter - if model.iter_has_child (tree_iter): - tree_iter = model.iter_children (tree_iter) - else: - next = model.iter_next (tree_iter) - if next: - tree_iter = next - else: - tree_iter = model.iter_parent (tree_iter) - while tree_iter: - next = model.iter_next (tree_iter) - if next: - tree_iter = next - break - else: - tree_iter = model.iter_parent (tree_iter) - -def iter_container (container, **property_matches): - - for child in container.get_children (): - for name, value in property_matches.iteritems (): - if container.child_get_property (child, name) != value: - break - else: - yield child - -def size_group_for_tables (tables, mode, **property_matches): - - """Example: size_group_for_tables ([table1, table2], - gtk.SIZE_GROUP_HORIZONTAL, - left_attach = 0, - right_attach = 1) - """ - - size_group = gtk.SizeGroup (mode) - for table in tables: - for child in iter_container (table, **property_matches): - size_group.add_widget (child) - -class StateString (object): - - """Descriptor for binding to AppState classes.""" - - def __init__ (self, option, section = None): - - self.option = option - self.section = section - - def get_section (self, state): - - if self.section is None: - return state._default_section - else: - return self.section - - def get_getter (self, state): - - return state._parser.get - - def get_default (self, state): - - return None - - def __get__ (self, state, state_class = None): - - import ConfigParser - - if state is None: - return self - - getter = self.get_getter (state) - section = self.get_section (state) - - try: - return getter (section, self.option) - except (ConfigParser.NoSectionError, - ConfigParser.NoOptionError,): - return self.get_default (state) - - def __set__ (self, state, value): - - import ConfigParser - - if value is None: - value = "" - - section = self.get_section (state) - option = self.option - option_value = str (value) - - try: - state._parser.set (section, option, option_value) - except ConfigParser.NoSectionError: - state._parser.add_section (section) - state._parser.set (section, option, option_value) - -class StateBool (StateString): - - """Descriptor for binding to AppState classes.""" - - def get_getter (self, state): - - return state._parser.getboolean - -class StateInt (StateString): - - """Descriptor for binding to AppState classes.""" - - def get_getter (self, state): - - return state._parser.getint - -class StateInt4 (StateString): - - """Descriptor for binding to AppState classes. This implements storing a - tuple of 4 integers.""" - - def __get__ (self, state, state_class = None): - - if state is None: - return self - - value = StateString.__get__ (self, state) - - try: - l = value.split (",") - if len (l) != 4: - return None - else: - return tuple ((int (v) for v in l)) - except (AttributeError, TypeError, ValueError,): - return None - - def __set__ (self, state, value): - - if value is None: - svalue = "" - elif len (value) != 4: - raise ValueError ("value needs to be a 4-sequence, or None") - else: - svalue = ", ".join ((str (v) for v in value)) - - return StateString.__set__ (self, state, svalue) - -class StateItem (StateString): - - """Descriptor for binding to AppState classes. This implements storing a - class controlled by a Manager class.""" - - def __init__ (self, option, manager_class, section = None): - - StateString.__init__ (self, option, section = section) - - self.manager = manager_class - - def __get__ (self, state, state_class = None): - - if state is None: - return self - - value = StateString.__get__ (self, state) - - if not value: - return None - - return self.parse_item (value) - - def __set__ (self, state, value): - - if value is None: - svalue = "" - else: - svalue = value.name - - StateString.__set__ (self, state, svalue) - - def parse_item (self, value): - - name = value.strip () - - try: - return self.manager.find_item_class (name = name) - except KeyError: - return None - -class StateItemList (StateItem): - - """Descriptor for binding to AppState classes. This implements storing an - ordered set of Manager items.""" - - def __get__ (self, state, state_class = None): - - if state is None: - return self - - value = StateString.__get__ (self, state) - - if not value: - return [] - - classes = [] - for name in value.split (","): - item_class = self.parse_item (name) - if item_class is None: - continue - if not item_class in classes: - classes.append (item_class) - - return classes - - def __set__ (self, state, value): - - if value is None: - svalue = "" - else: - svalue = ", ".join ((v.name for v in value)) - - StateString.__set__ (self, state, svalue) - -class AppState (object): - - _default_section = "state" - - def __init__ (self, filename, old_filenames = ()): - - import ConfigParser - - self._filename = filename - self._parser = ConfigParser.RawConfigParser () - success = self._parser.read ([filename]) - if not success: - for old_filename in old_filenames: - success = self._parser.read ([old_filename]) - if success: - break - - def save (self): - - # TODO Py2.5: Use 'with' statement. - fp = utils.SaveWriteFile (self._filename, "wt") - try: - self._parser.write (fp) - except: - fp.discard () - else: - fp.close () - -def widget_add_popup_menu (widget, menu, button = 3): - - def popup_callback (widget, event): - - if event.button == button: - menu.popup (None, None, None, event.button, event.get_time ()) - return False - - widget.connect ("button-press-event", popup_callback) - -class Actions (dict): - - def __init__ (self): - - dict.__init__ (self) - - self.groups = () - - def __getattr__ (self, name): - - try: - return self[name] - except KeyError: - if "_" in name: - try: - return self[name.replace ("_", "-")] - except KeyError: - pass - - raise AttributeError ("no action with name %r" % (name,)) - - def add_group (self, group): - - self.groups += (group,) - for action in group.list_actions (): - self[action.props.name] = action - -class Widgets (dict): - - def __init__ (self, glade_tree): - - widgets = glade_tree.get_widget_prefix ("") - dict.__init__ (self, ((w.name, w,) for w in widgets)) - - def __getattr__ (self, name): - - try: - return self[name] - except KeyError: - if "_" in name: - try: - return self[name.replace ("_", "-")] - except KeyError: - pass - - raise AttributeError ("no widget with name %r" % (name,)) - -class WidgetFactory (object): - - def __init__ (self, glade_filename): - - self.filename = glade_filename - - def make (self, widget_name, autoconnect = None): - - glade_tree = gtk.glade.XML (self.filename, widget_name) - - if autoconnect is not None: - glade_tree.signal_autoconnect (autoconnect) - - return Widgets (glade_tree) - - def make_one (self, widget_name): - - glade_tree = gtk.glade.XML (self.filename, widget_name) - - return glade_tree.get_widget (widget_name) - -class UIFactory (object): - - def __init__ (self, ui_filename, actions = None): - - self.filename = ui_filename - if actions: - self.action_groups = actions.groups - else: - self.action_groups = () - - def make (self, extra_actions = None): - - ui_manager = gtk.UIManager () - for action_group in self.action_groups: - ui_manager.insert_action_group (action_group, 0) - if extra_actions: - for action_group in extra_actions.groups: - ui_manager.insert_action_group (action_group, 0) - ui_manager.add_ui_from_file (self.filename) - ui_manager.ensure_update () - - return ui_manager - -class MetaModel (gobject.GObjectMeta): - - """Meta class for easy setup of gtk tree models. - - Looks for a class attribute named `columns' which must be set to a - sequence of the form name1, type1, name2, type2, ..., where the - names are strings. This metaclass adds the following attributes - to created classes: - - cls.column_types = (type1, type2, ...) - cls.name1 = 0 - cls.name2 = 1 - ... - - Example: A gtk.ListStore derived model can use - - columns = ("COL_NAME", str, "COL_VALUE", str) - - and use this in __init__: - - gtk.ListStore.__init__ (self, *self.column_types) - - Then insert data like this: - - self.set (self.append (), - self.COL_NAME, "spam", - self.COL_VALUE, "ham") - """ - - def __init__ (cls, name, bases, dict): - - super (MetaModel, cls).__init__ (name, bases, dict) - - spec = tuple (cls.columns) - - column_names = spec[::2] - column_types = spec[1::2] - column_indices = range (len (column_names)) - - for col_index, col_name, in zip (column_indices, column_names): - setattr (cls, col_name, col_index) - - cls.column_types = column_types - -class ElementModel (gtk.ListStore, Data.Consumer): - - __metaclass__ = MetaModel - - columns = ("COL_ELEMENT", object, - "COL_FACTORY_NAME", str, - "COL_FACTORY_LONGNAME", str, - "COL_RANK", object, - "COL_KLASS", object, - "COL_PLUGIN_NAME", str, - "COL_PACKAGE", str, - "COL_SOURCE", str,) - - def __init__ (self, finish = None): - - gtk.ListStore.__init__ (self, *self.column_types) - Data.Consumer.__init__ (self) - - def handle_load_started_after (self): - - self.clear () - - def handle_data_added (self, plugin): - - package = plugin.package - source = plugin.source - - for element in plugin.elements: - - self.set (self.append (), - self.COL_ELEMENT, element, - self.COL_FACTORY_LONGNAME, element.longname, - self.COL_FACTORY_NAME, element.name, - self.COL_RANK, element.rank, - self.COL_KLASS, element.klasses, - self.COL_PLUGIN_NAME, plugin.name, - self.COL_PACKAGE, package, - self.COL_SOURCE, source) - -class Manager (object): - - """GUI Manager base class.""" - - @classmethod - def iter_item_classes (cls): - - msg = "%s class does not support manager item class access" - raise NotImplementedError (msg % (cls.__name__,)) - - @classmethod - def find_item_class (self, **kw): - - return self.__find_by_attrs (self.iter_item_classes (), kw) - - def iter_items (self): - - msg = "%s object does not support manager item access" - raise NotImplementedError (msg % (type (self).__name__,)) - - def find_item (self, **kw): - - return self.__find_by_attrs (self.iter_items (), kw) - - @staticmethod - def __find_by_attrs (i, kw): - - from operator import attrgetter - - if len (kw) != 1: - raise ValueError ("need exactly one keyword argument") - - attr, value = kw.items ()[0] - getter = attrgetter (attr) - - for item in i: - if getter (item) == value: - return item - else: - raise KeyError ("no item such that item.%s == %r" % (attr, value,)) - -class FilterModelFactory (Data.ConsumerProxy): - - __metaclass__ = utils.SingletonMeta - - def __init__ (self, data_spec): - - Data.ConsumerProxy.__init__ (self) - - self.model = None - self.collector = Data.Collector (data_spec) - self.consumers.append (self.collector) - - def clear (self): - - if self.model is not None: - self.model.clear () - - def handle_load_started_after (self): - - Data.ConsumerProxy.handle_load_started_after (self) - - self.clear () - - def handle_load_finished (self): - - Data.ConsumerProxy.handle_load_finished (self) - - self.fill () - - self.collector.clear () - -class FilterModelFactoryFlat (FilterModelFactory): - - def __init__ (self, data_spec): - - FilterModelFactory.__init__ (self, data_spec) - - self.model = gtk.ListStore (str) - - def fill (self): - - model = self.model - collector = self.collector - - for sort_name, item in sorted (((locale.strxfrm (unicode (item, "UTF-8")), item,) - for item in collector.items)): - model.set (model.append (), 0, item) - -class FilterModelFactoryNestedAlpha (FilterModelFactory): - - def __init__ (self, data_spec): - - FilterModelFactory.__init__ (self, data_spec) - - self.model = gtk.TreeStore (str) - - def fill (self): - - model = self.model - - # TODO: This would ideally use a dynamic number of partitions. - - iter1 = model.append (None) - iter2 = model.append (None) - iter3 = model.append (None) - iter4 = model.append (None) - - model.set (iter1, 0, "A-G") - model.set (iter2, 0, "H-M") - model.set (iter3, 0, "N-S") - model.set (iter4, 0, "T-Z") - - for sort_name, item in sorted (((locale.strxfrm (unicode (item, "UTF-8")), item,) - for item in self.collector.items)): - if item.upper () < "H": - use_iter = iter1 - elif item.upper () < "N": - use_iter = iter2 - elif item.upper () < "T": - use_iter = iter3 - else: - use_iter = iter4 - model.set (model.append (use_iter), 0, item) - - remove = [row for row in model - if model.iter_n_children (row.iter) == 0] - for row in remove: - model.remove (row.iter) - -class FilterModelFactoryCapsNames (FilterModelFactory): - - def __init__ (self, data_spec): - - FilterModelFactory.__init__ (self, data_spec) - - self.model = gtk.TreeStore (str) - self.flat_model = gtk.ListStore (str) - - def clear (self): - - FilterModelFactory.clear (self) - - self.flat_model.clear () - - def fill (self): - - from sets import Set - - model = self.model - items = self.collector.items - - raw_names = [n for n in items if "-raw-" in n] - non_raw_names = [n for n in items if not "-raw-" in n] - prefixes = Set ((name.split ("/")[0] for name in non_raw_names)) - - for name in sorted (raw_names): - model.set (model.append (None), 0, name) - - if raw_names and non_raw_names: - # Add separator. - model.set (model.append (None), 0, "") - - for prefix in sorted (prefixes): - prefix_iter = model.append (None) - names = [name for name in non_raw_names - if name.startswith (prefix)] - if len (names) == 1: - model.set (prefix_iter, 0, names[0]) - continue - model.set (prefix_iter, 0, "%s/" % (prefix,)) - for name in sorted (names): - model.set (model.append (prefix_iter), 0, name) - - # Fill the flat model (used for completion). Since the popup doesn't - # work, don't even bother sorting the list. - model = self.flat_model - for item in items: - model.set (model.append (), 0, item) - -class UIFilter (object): - - name = None - label = None - param = None - sensitive = True - widget_name = None - data_spec = None - model_factory_class = None - filter_class = None - - @classmethod - def get_model_factory (cls): - - if cls.data_spec and cls.model_factory_class: - return cls.model_factory_class (cls.data_spec) - else: - return None - - def __init__ (self, manager, widgets): - - self.logger = logging.getLogger ("ui.filter.%s" % (self.name,)) - self.manager = manager - - if self.widget_name: - self.widget = getattr (widgets, self.widget_name) - else: - self.widget = None - self.model_factory = self.get_model_factory () - if self.model_factory: - self.model = self.model_factory.model - else: - self.model = None - - self.default_param = None - - def param_changed (self): - - self.manager.handle_ui_filter_param_changed (self) - - def push_default_param (self): - - # Don't overwrite defaults if the filter is inactive: - if self.param and not self.default_param: - self.default_param = self.param - self.logger.debug ("pushed default param %r", - self.default_param) - else: - self.logger.debug ("default param not pushed") - - def pop_default_param (self): - - if self.default_param is not None: - self.param = self.default_param - self.logger.debug ("popped default param %r", - self.default_param) - else: - self.logger.debug ("no default param to pop") - - def get_filter_func (self): - - spec = Data.accessor_transform (self.data_spec, "element") - return self.filter_class (spec, self.param) - -class UIFilterNone (UIFilter): - - name = "show-all" - label = _("Show all elements") - - @staticmethod - def filter_func (element): - return True - - def get_filter_func (self): - - return self.filter_func - -class UIFilterCombo (UIFilter): - - def _get_param (self): - - active_iter = self.widget.get_active_iter () - if active_iter is None: - # Our model is empty. - return None - model = self.widget.props.model - param = model.get (active_iter, 0)[0] - - return param - - def _set_param (self, param): - - model = self.widget.props.model - - for row in model: - if row[0] == param: - # This emits the "changed" signal. - self.widget.set_active_iter (row.iter) - break - else: - raise KeyError ("no such entry for param %r" % (param,)) - - def _get_sensitive (self): - - return len (self.model) > 0 - - param = property (_get_param, _set_param) - sensitive = property (_get_sensitive) - model_factory_class = FilterModelFactoryFlat - - def __init__ (self, manager, widgets, widget_prefix = None): - - if widget_prefix is None: - widget_prefix = self.name - if self.widget_name is None: - self.widget_name = "%s_filter_param_combo" % (widget_prefix,) - - UIFilter.__init__ (self, manager, widgets) - - cell = gtk.CellRendererText () - # We limit the width to 35 characters. The automatic size is far too - # large because of some semi-bogus entries we get (like URLs and almost - # whole sentences for element.author.) - cell.props.width_chars = 35 - cell.props.ellipsize = pango.ELLIPSIZE_END - - widget = self.widget - widget.clear () - widget.pack_start (cell, True) - widget.set_cell_data_func (cell, self.combo_cell_data_func) - widget.props.model = self.model - - self.default_param = self.get_initial_default () - if self.default_param is not None: - self.param = self.default_param - self.default_param = None - - widget.connect ("changed", self.handle_combo_box_changed) - - def get_initial_default (self): - - try: - tree_iter = self.model.get_iter ((0,)) - except ValueError: - self.logger.debug ("no initial default param: model is empty") - return None - else: - param = self.model.get (tree_iter, 0)[0] - self.logger.debug ("initial default param is %r", param) - return param - - def handle_combo_box_changed (self, combo_box): - - self.param_changed () - - @staticmethod - def combo_cell_data_func (layout, cell, model, tree_iter): - - cell.props.text = model.get (tree_iter, 0)[0] - - def pop_default_param (self): - - widget = self.widget - model = widget.props.model - - if self.default_param is None: - self.param = self.get_initial_default () - else: - try: - self.param = self.default_param - except KeyError: - # The param does not exist in the new data anymore, fall back: - self.param = self.get_initial_default () - self.default_param = None - -class UIFilterComboNested (UIFilterCombo): - - def _set_param (self, param): - - model = self.widget.props.model - - for row in model: - for childrow in row.iterchildren (): - if childrow[0] == param: - # This emits the "changed" signal. - self.widget.set_active_iter (childrow.iter) - return - else: - raise KeyError ("no such entry for param %r" % (param,)) - - param = property (UIFilterCombo._get_param, _set_param) - - @staticmethod - def combo_cell_data_func (layout, cell, model, tree_iter): - - props = cell.props - - props.text = model.get (tree_iter, 0)[0] - props.sensitive = not model.iter_has_child (tree_iter) - -class UIFilterAuthor (UIFilterComboNested): - - name = "author" - data_spec = "element.authors.name" - model_factory_class = FilterModelFactoryNestedAlpha - filter_class = Data.FilterIn - label = _("Author") - - def get_initial_default (self): - - try: - tree_iter = self.model.get_iter ((0, 0,)) - except ValueError: - self.logger.debug ("no initial default param: model is empty") - return None - else: - param = self.model.get (tree_iter, 0)[0] - self.logger.debug ("initial default param is %r", param) - return param - -class UIFilterCapsName (UIFilterComboNested): - - def _get_param (self): - - return self.widget.get_active_text () - - def _set_param (self, param): - - entry = self.widget.get_child () - # This emits the "changed" signal. - entry.props.text = param - - name = "caps_name" - widget_name = "caps_filter_param_combo_entry" - data_spec = "element.pads.caps.name" - model_factory_class = FilterModelFactoryCapsNames - filter_class = Data.FilterInIn - # TRANSLATORS: The term "Caps" refers to a GStreamer data structure. - label = _("Caps name") - param = property (_get_param, _set_param) - - def __init__ (self, manager, widgets): - - UIFilterComboNested.__init__ (self, manager, widgets) - - self.id = None - - self.widget.set_row_separator_func (self.row_separator_func) - - flat_model = self.model_factory.flat_model - entry = self.widget.get_child () - comp = gtk.EntryCompletion () - comp.props.model = flat_model - comp.props.text_column = 0 - comp.props.inline_completion = True - # TODO: Would be nice to have the popup, but it doesn't work correctly. - # It gets the wrong size and apparently does not render any text. - comp.props.popup_completion = False - entry.set_completion (comp) - - def get_initial_default (self): - - return "" - - def handle_combo_box_changed (self, widget): - - # We idly defer signalling the change, which will trigger refiltering. - # This helps against sluggishness during typing, which occurs because - # of TreeView slowness (the filtering itself is fast). - - if self.id is not None: - # Already queued up. - return - - def emit_change_deferred (): - - self.param_changed () - self.id = None - return False - - self.id = gobject.idle_add (emit_change_deferred, priority = gobject.PRIORITY_LOW) - - def pop_default_param (self): - - if self.default_param is not None: - # TODO: Ensure that we also set the active iter somehow, to make - # the scroll wheel work as expected. - self.param = self.default_param - - @staticmethod - def row_separator_func (model, tree_iter): - - return model.get (tree_iter, 0)[0] == "" - -class UIFilterKlass (UIFilterCombo): - - name = "klass" - data_spec = "element.klasses" - filter_class = Data.FilterIn - label = _("Classification") - -class UIFilterInterface (UIFilterCombo): - - name = "interface" - data_spec = "element.interfaces" - filter_class = Data.FilterIn - label = _("Interface") - -class UIFilterLicense (UIFilterCombo): - - name = "license" - data_spec = "plugin.license" - filter_class = Data.FilterEq - label = _("License") - -class UIFilterSource (UIFilterCombo): - - name = "source" - data_spec = "plugin.source" - filter_class = Data.FilterEq - label = _("Source module") - -class UIFilterPackage (UIFilterCombo): - - name = "package" - data_spec = "plugin.package" - filter_class = Data.FilterEq - label = _("Binary package") - -class UIFilterParentClass (UIFilterCombo): - - name = "parent" - data_spec = "element.parent_classes" - filter_class = Data.FilterIn - label = _("Parent class") - -class UIFilterURIProtocols (UIFilterCombo): - - name = "uri_protocol" - data_spec = "element.uri_protocols" - filter_class = Data.FilterIn - label = _("URI protocol") - - @staticmethod - def combo_cell_data_func (layout, cell, model, tree_iter): - - cell.props.text = "%s://" % (model.get (tree_iter, 0)[0],) - -class FilterManager (Manager): - - """GUI manager that handles the filter selection combo box and the notebook - containing the parameter selectors for the available filters. The - individual parameter selector pages are handled by specific objects derived - from the UIFilter class.""" - - filter_classes = (UIFilterNone, UIFilterAuthor, UIFilterCapsName, - UIFilterInterface, UIFilterKlass, UIFilterLicense, - UIFilterPackage, UIFilterParentClass, UIFilterSource, - UIFilterURIProtocols,) - - @classmethod - def iter_item_classes (cls): - - return iter (cls.filter_classes) - - def _get_active (self): - - combo = self.combo - model = combo.props.model - active_iter = combo.get_active_iter () - - if active_iter is None: - return None - else: - return model.get (active_iter, 1)[0] - - def _set_active (self, ui_filter): - - for row in self.combo.props.model: - - label, other_ui_filter = row - - if not label: - # Separator. - continue - - if ui_filter == other_ui_filter: - self.combo.set_active_iter (row.iter) - break - - else: - raise KeyError ("no such UIFilter object %r" % (ui_filter,)) - - active = property (_get_active, _set_active) - - def __init__ (self): - - self.logger = logging.getLogger ("ui.filtermanager") - - self.disabled = False - self.default_active = None - - def attach (self, inspector_window): - - self.app = inspector_window.app - widgets = inspector_window.widgets - - self.set_filter_func = inspector_window.set_filter_func - - self.combo = widgets.filter_combo - self.book = widgets.filter_params_book - self.book.hide () - - model = gtk.ListStore (str, object) - self.combo.set_row_separator_func (self.row_separator_func) - self.combo.props.model = model - - # The first filter is special. - ui_filter = UIFilterNone (self, widgets) - model.set (model.append (), - 0, ui_filter.label, - 1, ui_filter) - model.set (model.append (), - 0, "", - 1, None) - - # Now add the rest. - classes = self.filter_classes[1:] - ui_filters = tuple ((cls (self, widgets) for cls in classes)) - labels = [ui_filter.label for ui_filter in ui_filters] - sort_labels = [locale.strxfrm (label) for label in labels] - for sort_label, label, ui_filter in sorted (zip (sort_labels, - labels, - ui_filters)): - model.set (model.append (), - 0, label, - 1, ui_filter) - - self.combo.props.active = 0 - self.combo.connect ("changed", self.handle_filter_combo_changed) - self.combo.set_row_separator_func (self.row_separator_func) - - filter_class = self.app.state.filter - default_param = self.app.state.filter_param - - if not filter_class: - filter_class = UIFilterNone - self.logger.debug ("no default filter in saved state, using %r", - filter_class.name) - else: - self.logger.debug ("restored default filter %r with param %r from state", - filter_class.name, default_param) - - ui_filter = self.find_item (name = filter_class.name) - ui_filter.default_param = default_param - self.active = ui_filter - self.default_active = ui_filter - - def detach (self): - - active = self.active - state = self.app.state - - state.filter = active - state.filter_param = active.param - - def handle_filter_combo_changed (self, combo_box): - - ui_filter = self.active - - if ui_filter.widget: - self.book.show () - child = ui_filter.widget - pos = self.book.child_get (child, "position")[0] - self.book.props.page = pos - else: - self.book.hide () - - if not ui_filter.sensitive: - return - - if self.disabled: - return - - func = ui_filter.get_filter_func () - self.set_filter_func (func) - - def handle_ui_filter_param_changed (self, ui_filter): - - # Called when a UIFilter instance has changed its parameter. - - active_ui_filter = self.active - if ui_filter != active_ui_filter: - return - - if self.disabled: - return - - func = ui_filter.get_filter_func () - self.set_filter_func (func) - - @staticmethod - def row_separator_func (model, tree_iter): - - return model.get (tree_iter, 0)[0] == "" - - def iter_items (self): - - for label, ui_filter in self.combo.props.model: - if not ui_filter: - # Either "show-all" or separator. - continue - yield ui_filter - - def handle_load_started (self): - - self.disable () - - for ui_filter in self.iter_items (): - ui_filter.push_default_param () - - def handle_load_finished (self): - - for ui_filter in self.iter_items (): - ui_filter.pop_default_param () - - self.enable () - - def disable (self): - - if self.disabled: - return - - self.disabled = True - - show_all = self.find_item (name = "show-all") - self.set_filter_func (show_all.get_filter_func ()) - - def enable (self): - - if not self.disabled: - return - - self.disabled = False - - ui_filter = self.active - if not ui_filter.sensitive: - self.active = self.find_item (name = "show-all") - self.logger.debug ("filter %s lost sensitivity, switched to show-all", - ui_filter.name) - elif ui_filter.name != "show-all": - self.set_filter_func (ui_filter.get_filter_func ()) - - def push_default_active (self): - - self.default_active = self.active - self.active = self.find_item (name = "show-all") - - def pop_default_active (self): - - if self.default_active is None: - raise ValueError ("not attached") - elif self.default_active == self.active: - return - - self.active = self.default_active - -class Page (object): - - name = None - widget_name = None - - sensitive = True - - def __init__ (self, window): - - self.logger = logging.getLogger ("ui.page.%s" % (self.name,)) - self.widget = getattr (window.widgets, self.widget_name) - self.element = None - - def update (self, element): - - self.element = element - - self.handle_update (element) - - def handle_update (self, element): - - pass - -class DetailsPage (Page): - - name = "details" - widget_name = "details_scrolled" - - def __init__ (self, window, *a, **kw): - - Page.__init__ (self, window, *a, **kw) - - widgets = window.widgets - - self.widgets = widgets - - size_group_for_tables ((widgets.plugin_details_table, - widgets.element_details_table,), - gtk.SIZE_GROUP_HORIZONTAL, - left_attach = 0, right_attach = 1) - - if _gtk_has_link_buttons: - self._prepare_origin () - - def _prepare_origin (self): - - box = self.widgets.origin_hbox - label = self.widgets.origin_label - label.hide () - self.link_button = gtk.LinkButton ("") - self.link_button.props.xalign = 0.0 - self.link_button.connect ("clicked", self.handle_link_button_clicked) - self.link_button.show () - - # Little hack to make the label inside the LinkButton align with all - # the others. The LinkButton has a margin around the label. A shadow - # (border) is drawn there on mouse over only, so it looks totally odd - # without this little adjustment. - button_req_width = self.link_button.size_request ()[0] - child_req_width = self.link_button.child.size_request ()[0] - extra_pad = (button_req_width - child_req_width) / 2 - children = [label] - children += iter_container (self.widgets.element_details_table, - left_attach = 1, right_attach = 2) - children += iter_container (self.widgets.plugin_details_table, - left_attach = 1, right_attach = 2) - for child in children: - if child != box: - child.props.xpad = extra_pad - - box.pack_start (self.link_button, False, False) - - def _update_origin (self, origin): - - if "://" in origin: - is_uri = True - else: - is_uri = False - - if is_uri: - self.widgets.origin_label.hide () - button = self.link_button - button.props.label = origin - button.props.uri = origin - button.show () - else: - self.link_button.hide () - label = self.widgets.origin_label - label.props.label = origin - label.show () - - def _fix_text (self, text): - - if text is None: - return "" - text = text.replace ("\n", " ").replace (" ", " ") - return text - - def handle_link_button_clicked (self, button): - - show_uri (button.props.uri, widget = button) - - def handle_update (self, element): - - widgets = self.widgets - plugin = element.plugin - - # Element details. - - description = self._fix_text (element.description) - authors_h = ngettext ("Author:", "Authors:", - len (element.authors or ())) - authors = "\n".join ((str (a) for a in element.authors)) - uri_protocols = ", ".join (("%s://" % (s,) - for s in element.uri_protocols)) - widgets.uri_protocols_label.props.sensitive = uri_protocols and True - if not uri_protocols: - uri_protocols = _("Handles no URIs") - interfaces = "\n".join (element.interfaces) - widgets.interfaces_label.props.sensitive = interfaces and True - if not interfaces: - interfaces = _("Implements no interfaces") - hierarchy = "\n".join (" " * i + h - for i, h in zip (range (len (element.hierarchy)), - element.hierarchy)) - - widgets.element_name_label.props.label = element.name - widgets.element_longname_label.props.label = element.longname - widgets.element_description_label.props.label = description - widgets.authors_h_label.props.label = authors_h - widgets.authors_label.props.label = authors - widgets.rank_label.props.label = str (element.rank) - widgets.klasses_label.props.label = "/".join (element.klasses) - widgets.uri_protocols_label.props.label = uri_protocols - widgets.interfaces_label.props.label = interfaces - widgets.hierarchy_label.props.label = hierarchy - - # Plugin details. - - description = self._fix_text (plugin.description) - filename = plugin.filename - if not filename: - filename = "(static plugin)" - - widgets.plugin_name_label.props.label = plugin.name - widgets.plugin_description_label.props.label = description - widgets.filename_label.props.label = filename - widgets.version_label.props.label = plugin.version - widgets.license_label.props.label = plugin.license - widgets.source_label.props.label = plugin.source - widgets.package_label.props.label = plugin.package - if not _gtk_has_link_buttons: - widgets.origin_label.props.label = plugin.origin - else: - self._update_origin (plugin.origin) - -class SpanningCellRenderer (gtk.GenericCellRenderer): - - DEBUG_DISABLE_DRAW_SPACING = False - - def __init__ (self, *a, **kw): - - gtk.CellRenderer.__init__ (self, *a, **kw) - - self.child_cells = [] - self.spacing = 0 - self.connect ("notify::is-expander", self.handle_notify_is_expander) - self.connect ("notify::is-expanded", self.handle_notify_is_expanded) - - def add_cell (self, cell): - - if len (self.child_cells) == 2: - raise NotImplementedError ("can only handle two child cells for now") - - self.child_cells.append (cell) - - def handle_notify_is_expander (self, prop, value): - - for child in self.child_cells: - child.props.is_expander = value - - def handle_notify_is_expanded (self, prop, value): - - for child in self.child_cells: - child.props.is_expanded = value - - def on_get_size (self, widget, area = None): - - visible_cells = [c for c in self.child_cells if c.props.visible] - width, height = 0, 0 - - for child in visible_cells: - cx, cy, cw, ch = child.get_size (widget) - width += cw - height = max (height, ch) - - if visible_cells: - width += self.spacing * (len (visible_cells) - 1) - - return (0, 0, width, height,) - - @staticmethod - def get_child_width (cell, widget): - - if cell.props.width > 0: - return cell.props.width - return cell.get_size (widget)[2] - - def on_render (self, window, widget, background_area, cell_area, expose_area, flags): - - if not self.child_cells: - return - - if len (self.child_cells) == 2: - child1, child2 = self.child_cells - else: - child1 = self.child_cells[0] - child2 = None - if not child1.props.visible and (not child2 or not child2.props.visible): - return - if not child2 or not child2.props.visible: - child1.render (window, widget, background_area, cell_area, expose_area, flags) - elif not child1.props.visible: - child2.render (window, widget, background_area, cell_area, expose_area, flags) - else: - child1_width = self.get_child_width (child1, widget) - - cell_area1 = cell_area.copy () - cell_area2 = cell_area.copy () - - bg_area1 = background_area.copy () - bg_area2 = background_area.copy () - - cell_area1.width = child1_width - cell_area2.x += child1_width + self.spacing - cell_area2.width -= child1_width - - bg_area1.width = cell_area1.x + cell_area1.width - bg_area1.x - if not self.DEBUG_DISABLE_DRAW_SPACING: - bg_area1.width += self.spacing // 2 - - bg_area2.x = bg_area1.x + bg_area1.width - bg_area2.width -= bg_area1.width - if self.DEBUG_DISABLE_DRAW_SPACING: - bg_area2.x += self.spacing - bg_area2.width -= self.spacing - - child1.render (window, widget, bg_area1, cell_area1, expose_area, flags) - child2.render (window, widget, bg_area2, cell_area2, expose_area, flags) - - def on_activate (event, widget, path, background_area, cell_area, flags): - - raise NotImplementedError ("activation is not implemented") - - def on_start_editing (event, widget, path, background_area, cell_area, flags): - - raise NotImplementedError ("editing is not implemented") - -class NameValueModel (gtk.TreeStore): - - __metaclass__ = MetaModel - - columns = ("COL_NAME", str, - "COL_VALUE", str,) - - def __init__ (self): - - gtk.TreeStore.__init__ (self, *self.column_types) - - def set_name_value (self, tree_iter, name, value): - - self.set (tree_iter, self.COL_NAME, name, self.COL_VALUE, value) - -class NameValuePageBase (Page): - - """Base class for pages that display a tree view populated with name/value - pairs.""" - - DEBUG_DRAW_COLORED_AREAS = False - - CELL_SPACING = 12 - - view_name = None - - def __init__ (self, window): - - Page.__init__ (self, window) - - self.view = view = getattr (window.widgets, self.view_name) - - self.old_size = None - - view.connect ("row-collapsed", self.handle_view_row_collapsed) - view.connect ("row-expanded", self.handle_view_row_expanded) - view.connect ("row-activated", self.handle_view_row_activated) - view.connect ("notify::style", self.handle_view_notify_style) - view.connect ("notify::model", self.handle_view_notify_model) - - self.name_cell_width = 0 - self.value_cell_width = 0 - self.cached_name_widths = {} - self.cached_name_width = None - - self.tree_view_states = {} - self.collapsed_rows = [] - self.expanded_rows = [] - - cell = SpanningCellRenderer () - self.cell = cell - cell.spacing = self.CELL_SPACING - - sub_cell = gtk.CellRendererText () - # Storing xpad in instance to speed up the cell data function: - self.name_xpad = sub_cell.props.xpad - cell.add_cell (sub_cell) - - sub_cell = gtk.CellRendererText () - cell.add_cell (sub_cell) - - self.set_default_cell_data () - - column = gtk.TreeViewColumn ("") - self.column = column - column.connect ("notify::width", self.handle_column_notify_width) - column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED - column.pack_start (cell, True) - column.set_cell_data_func (cell, self.cell_data_function) - view.append_column (column) - - self.update_style_values () - - widget_add_popup_menu (self.view, window.name_value_popup) - - def update_style_values (self): - - self.expander_size = self.view.style_get_property ("expander-size") - self.h_separator = self.view.style_get_property ("horizontal-separator") - self.focus_line_width = self.view.style_get_property ("focus-line-width") - try: - self.level_indentation = self.view.props.level_indentation - self.show_expanders = self.view.props.show_expanders - except AttributeError: - # These properties were added during the 2.10 series. - self.level_indentation = 0 - self.show_expanders = True - - EXPANDER_EXTRA_PADDING = 4 - self.expander_size += EXPANDER_EXTRA_PADDING - - def get_indentation (self, depth): - - if self.show_expanders: - expander = self.expander_size * (depth + 1) - elif self.show_expanders: - expander = self.expander_size - else: - expander = 0 - - if depth > 0: - level = self.level_indentation * (depth - 1) - else: - level = 0 - - return expander + level - - def set_default_cell_data (self, intrinsic = False): - - name_cell, value_cell = self.cell.child_cells - name_props = name_cell.props - value_props = value_cell.props - - name_props.ellipsize = pango.ELLIPSIZE_END - if intrinsic: - name_props.ellipsize = pango.ELLIPSIZE_NONE - name_props.wrap_width = -1 - name_props.width = -1 - else: - name_props.ellipsize = pango.ELLIPSIZE_END - if self.DEBUG_DRAW_COLORED_AREAS: - name_props.cell_background = "blue" - name_props.background = "yellow" - - value_props.wrap_mode = pango.WRAP_WORD_CHAR - if intrinsic: - value_props.wrap_width = -1 - value_props.width = -1 - else: - xpad = value_props.xpad - value_props.width = self.value_cell_width - value_props.wrap_width = max (0, self.value_cell_width - xpad * 2) - - if self.DEBUG_DRAW_COLORED_AREAS: - value_props.cell_background = "red" - value_props.background = "green" - - def cell_data_function (self, column, cell, model, tree_iter, intrinsic = False): - - name, value = model.get (tree_iter, - NameValueModel.COL_NAME, - NameValueModel.COL_VALUE) - depth = model.iter_depth (tree_iter) - - name_cell, value_cell = cell.child_cells - name_props = name_cell.props - value_props = value_cell.props - - name_props.text = name - if model.iter_has_child (tree_iter): - # Line up with expander. - name_props.yalign = 0.5 - else: - name_props.yalign = 0.0 - - value_props.text = value - - if depth != 0 and value is not None: - name_props.weight = pango.WEIGHT_NORMAL - else: - # depth == 0 or value is None - name_props.weight = pango.WEIGHT_BOLD - - if value: - value_props.visible = True - if not intrinsic: - # The intrinsic width includes the indentation, now we subtract - # it again: - indentation = self.get_indentation (depth) - name_width = max (0, self.name_cell_width - indentation) - else: - value_props.visible = False - if not intrinsic: - name_width = self.name_cell_width + self.value_cell_width - - if not intrinsic: - name_props.width = name_width - name_props.wrap_width = max (-1, name_width - self.name_xpad * 2) - - def compute_intrinsic_name_width (self): - - model = self.view.props.model - name_cell, value_cell = self.cell.child_cells - - if not model: - return 0 - - self.set_default_cell_data (intrinsic = True) - - name_width = 0 - for tree_iter in walk_tree_model (model): - - name, value = model.get (tree_iter, - NameValueModel.COL_NAME, - NameValueModel.COL_VALUE) - if value: - # The name cell does not span. - depth = model.iter_depth (tree_iter) - # The computed width depends on the values of just two renderer - # properties: 'text' and whether 'weight' is set to bold. We - # replicate the logic from the data function here to avoid - # calling it (pushing property values into the cell renderer is - # surprisingly slow). - vars = (name, depth == 0 or value is None,) - if vars in self.cached_name_widths: - w = self.cached_name_widths[vars] - else: - self.cell_data_function (self.column, self.cell, model, tree_iter, - intrinsic = True) - w = name_cell.get_size (self.view, None)[2] - self.cached_name_widths[vars] = w - name_width = max (name_width, w + self.get_indentation (depth)) - - # Restore constant renderer properties for regular rendering. - self.set_default_cell_data (intrinsic = False) - - return name_width - - def value_width_if_less_than (self, max_width): - - model = self.view.props.model - value_cell = self.cell.child_cells[1] - - if not model: - return 0 - - self.set_default_cell_data (intrinsic = True) - try: - value_width = 0 - for tree_iter in walk_tree_model (model): - - self.cell_data_function (self.column, self.cell, model, tree_iter, - intrinsic = True) - width = value_cell.get_size (self.view, None)[2] - if width > max_width: - return width - value_width = max (width, value_width) - finally: - self.set_default_cell_data (intrinsic = False) - - return value_width - - def handle_view_row_activated (self, view, tree_path, column): - - expanded = view.row_expanded (tree_path) - - if expanded: - view.collapse_row (tree_path) - else: - view.expand_row (tree_path, True) - - def handle_column_notify_width (self, column, gparam): - - self.update_style_values () - self.recompute_sizes (resize = True) - - def handle_view_notify_style (self, view, gparam): - - self.update_style_values () - self.cached_name_widths.clear () - self.recompute_sizes (force = True) - - def handle_view_notify_model (self, view, gparam): - - model = view.props.model - - if not model: - return - - self.recompute_sizes (force = True) - - if len (model): - self.sensitive = True - view.props.sensitive = True - else: - self.sensitive = False - view.props.sensitive = False - - view.expand_all () - - def recompute_sizes (self, force = False, resize = False): - - width = self.column.props.width - if not force and self.old_size == width: - return - self.old_size = width - - spacing = self.CELL_SPACING - - if force or self.cached_name_width is None: - self.cached_name_width = self.compute_intrinsic_name_width () - name_intrinsic = self.cached_name_width - if name_intrinsic: - width -= spacing + self.h_separator + self.focus_line_width - if name_intrinsic < width // 2: - self.name_cell_width = name_intrinsic - self.value_cell_width = width - name_intrinsic - else: - limit = width - name_intrinsic - value_width = self.value_width_if_less_than (limit) - if value_width < limit: - self.name_cell_width = name_intrinsic - self.value_cell_width = width - name_intrinsic - else: - limit = width // 2 - value_width = self.value_width_if_less_than (limit) - if value_width < limit: - self.name_cell_width = width - value_width - self.value_cell_width = value_width - else: - self.name_cell_width = width // 2 - self.value_cell_width = width - self.name_cell_width - - if self.name_cell_width < 0: - self.name_cell_width = 0 - if self.value_cell_width < 0: - self.value_cell_width = 0 - - self.set_default_cell_data () - - if resize: - # Ensure that the rows receive correct heights (needed since we - # implement wrap widths depending on the column width): - self.column.queue_resize () - - def expand_all (self): - - self.view.expand_all () - - def collapse_all (self): - - self.view.collapse_all () - - def handle_view_row_collapsed (self, view, tree_iter, path): - - try: - self.expanded_rows.remove (path) - except ValueError: - self.collapsed_rows.append (path) - - def handle_view_row_expanded (self, view, tree_iter, path): - - try: - self.collapsed_rows.remove (path) - except ValueError: - self.expanded_rows.append (path) - - def save_tree_view_state (self): - - model = self.view.props.model - if model is None: - return - - selection = self.view.get_selection () - selected_rows = selection.get_selected_rows ()[1] - - top_row = None - if self.view.flags () & gtk.MAPPED: - vis_range = self.view.get_visible_range () - if vis_range: - top_row = vis_range[0] - if top_row == (0,): - top_row = None - - state = {"selected-rows" : selected_rows, - "top-row" : top_row, - "expanded-rows" : tuple (self.expanded_rows), - "collapsed-rows" : tuple (self.collapsed_rows)} - self.tree_view_states[self.element.name] = state - - del self.expanded_rows[:] - del self.collapsed_rows[:] - - def restore_tree_view_state (self): - - try: - state = self.tree_view_states[self.element.name] - except KeyError: - selected_rows = [] - top_row = None - else: - selected_rows = state["selected-rows"] - top_row = state["top-row"] - self.collapsed_rows[:] = state["collapsed-rows"] - self.expanded_rows[:] = state["expanded-rows"] - - for path in self.collapsed_rows: - self.view.collapse_row (path) - - for path in self.expanded_rows: - self.view.expand_row (path, False) - - selection = self.view.get_selection () - selection.unselect_all () - try: - for path in selected_rows: - selection.select_path (path) - except ValueError: - pass - - if top_row is None: - top_row = (0,) - - model = self.view.props.model - if model and len (model): - self.view.scroll_to_cell (top_row, self.column, True, 0., 0.) - - def clear (self): - - if self.element: - self.save_tree_view_state () - - Page.clear (self) - - def update (self, element): - - self.view.handler_block_by_func (self.handle_view_row_collapsed) - self.view.handler_block_by_func (self.handle_view_row_expanded) - - try: - if self.element: - self.save_tree_view_state () - - Page.update (self, element) - - self.restore_tree_view_state () - - finally: - self.view.handler_unblock_by_func (self.handle_view_row_collapsed) - self.view.handler_unblock_by_func (self.handle_view_row_expanded) - -class PadsModel (NameValueModel): - - def __init__ (self, element): - - NameValueModel.__init__ (self) - - templates = element.pads - append = self.append - set = self.set_name_value - - # Move sink pad templates to the beginning of the list. - sorted_templates = ([t for t in templates if t.direction == "sink"] + - [t for t in templates if t.direction != "sink"]) - - _ = utils.gettext_cache () - - for template in sorted_templates: - - caps = template.caps - - template_tree_iter = append (None) - set (template_tree_iter, template.name, None) - set (append (template_tree_iter), _("Direction"), - template.direction) - set (append (template_tree_iter), _("Presence"), - template.presence) - caps_tree_iter = append (template_tree_iter) - # TRANSLATORS: The term "Caps" refers to a GStreamer data - # structure. - set (caps_tree_iter, _("Caps"), None) - if caps.any: - self.set (caps_tree_iter, self.COL_VALUE, "ANY") - continue - elif len (caps) == 0: - self.set (caps_tree_iter, self.COL_VALUE, "EMPTY") - continue - for structure in caps: - struct_tree_iter = append (caps_tree_iter) - set (struct_tree_iter, structure.name, None) - for name, value in structure.fields: - set (append (struct_tree_iter), name, value) - -class PadsPage (NameValuePageBase): - - name = "pads" - widget_name = "pads_scrolled" - view_name = "pads_view" - - def handle_update (self, element): - - view = self.view - model = PadsModel (element) - - view.props.model = model - -class PropertiesModel (NameValueModel): - - def __init__ (self, element): - - NameValueModel.__init__ (self) - - set = self.set_name_value - _ = utils.gettext_cache () - - for prop in element.properties: - - tree_iter = self.append (None) - if prop.owner_name == element.type_name: - set (tree_iter, prop.name, None) - else: - # The property is inherited from a parent class. - set (tree_iter, prop.name, _("(from %s)") % (prop.owner_name,)) - - if prop.description and prop.description != prop.name: - set (self.append (tree_iter), _("Description"), prop.description) - set (self.append (tree_iter), _("Flags"), ", ".join (prop.flags)) - set (self.append (tree_iter), _("Data type"), prop.data_type) - - if not prop.default in (None, "", (),): - if prop.default in (True, False,): - str_default = str (prop.default).lower () - else: - str_default = str (prop.default) - set (self.append (tree_iter), _("Default"), str_default) - - if prop.minimum is not None and prop.maximum is not None: - set (self.append (tree_iter), _("Minimum"), str (prop.minimum)) - set (self.append (tree_iter), _("Maximum"), str (prop.maximum)) - - if prop.values: - e_tree_iter = self.append (tree_iter) - set (e_tree_iter, _("Values"), None) - for data in prop.values: - set (self.append (e_tree_iter), data.nickname, data.description) - -class PropertiesPage (NameValuePageBase): - - name = "properties" - widget_name = "props_scrolled" - view_name = "props_view" - - def handle_update (self, element): - - view = self.view - model = PropertiesModel (element) - view.props.model = model - - for row in model: - value = row[model.COL_VALUE] - if value is not None: - # Property is inherited from a parent class. - view.collapse_row (row.path) - -class SignalsModel (NameValueModel): - - def __init__ (self, element): - - NameValueModel.__init__ (self) - - set = self.set_name_value - _ = utils.gettext_cache () - - for signal in element.signals: - - flags = ", ".join (signal.flags) - arguments = ",\n".join (signal.parameters) - - tree_iter = self.append (None) - if signal.owner_name == element.type_name: - set (tree_iter, signal.name, None) - else: - # The signal is inherited from a parent class. - set (tree_iter, signal.name, _("(from %s)") % (signal.owner_name,)) - if flags: - set (self.append (tree_iter), _("Flags"), flags) - if arguments: - set (self.append (tree_iter), _("Parameters"), arguments) - set (self.append (tree_iter), _("Returns"), signal.return_type) - -class SignalsPage (NameValuePageBase): - - name = "signals" - widget_name = "signals_scrolled" - view_name = "signals_view" - - def handle_update (self, element): - - view = self.view - model = SignalsModel (element) - - view.props.model = model - -class PageManager (Manager): - - """GUI manager that handles the element info notebook. The individual - notebook pages are handled by specific objects derived from the Page - class.""" - - def _get_active (self): - - notebook = self.notebook - - page_index = notebook.props.page - widget = notebook.get_nth_page (page_index) - page = self.pages[widget] - - return page - - def _set_active (self, page): - - notebook = self.notebook - - for i in range (notebook.get_n_pages ()): - widget = notebook.get_nth_page (i) - if page.widget == widget: - self.notebook.props.page = i - return - else: - raise KeyError ("no such page %r" % (page,)) - - page_classes = (DetailsPage, PadsPage, PropertiesPage, SignalsPage,) - active = property (_get_active, _set_active) - - @classmethod - def iter_item_classes (cls): - - return iter (cls.page_classes) - - def __init__ (self, *a, **kw): - - Manager.__init__ (self, *a, **kw) - - self.action_group = gtk.ActionGroup ("NameValueActions") - self.action_group.add_actions ([("expand-all", gtk.STOCK_ZOOM_IN, _("_Expand all")), - ("collapse-all", gtk.STOCK_ZOOM_OUT, _("_Collapse all"))]) - - def attach (self, inspector_window): - - self.app = inspector_window.app - widgets = inspector_window.widgets - actions = inspector_window.actions - - self.notebook = widgets.element_info_book - pages = [cls (inspector_window) for cls in self.page_classes] - - notebook_widgets = [self.notebook.get_nth_page (i) - for i in range (self.notebook.get_n_pages ())] - for page in pages: - assert page.widget in notebook_widgets - - self.pages = dict (((page.widget, page,) for page in pages)) - - last_page_class = self.app.state.page - if not last_page_class: - last_page_class = DetailsPage - - self.active = self.find_item (name = last_page_class.name) - self.element = None - self.update_source = None - - actions.expand_all.connect ("activate", self.handle_expand_all_action_activate) - actions.collapse_all.connect ("activate", self.handle_collapse_all_action_activate) - - def detach (self): - - if self.update_source is not None: - gobject.source_remove (self.update_source) - self.update_source = None - - self.app.state.page = self.active - - def iter_items (self): - - return self.pages.itervalues () - - def update_iter (self): - - while True: - for page in self.pages.values (): - if page.element != self.element: - page.update (self.element) - self.update_label_sensitivity (page) - assert page.element == self.element - yield True - break - else: - self.update_source = None - yield False - return - - def update (self, element): - - self.element = element - - self.active.update (element) - self.update_label_sensitivity (self.active) - - # Defer updating the other pages, which are invisible: - if self.update_source is None: - callback = self.update_iter ().next - self.update_source = gobject.idle_add (callback) - - def update_label_sensitivity (self, page): - - tab_label = self.notebook.get_tab_label (page.widget) - if tab_label: - tab_label.props.sensitive = page.sensitive - - def handle_expand_all_action_activate (self, action): - - try: - self.active.expand_all () - except AttributeError: - pass - - def handle_collapse_all_action_activate (self, action): - - try: - self.active.collapse_all () - except AttributeError: - pass - -class Column (object): - - """A single list view column, managed by a ColumnManager instance.""" - - name = None - id = None - label_header = None - get_modify_func = None - get_sort_func = None - - def __init__ (self): - - view_column = gtk.TreeViewColumn (self.label_header) - view_column.props.reorderable = True - - self.view_column = view_column - -class TextColumn (Column): - - def __init__ (self): - - Column.__init__ (self) - - column = self.view_column - cell = gtk.CellRendererText () - column.pack_start (cell) - - if not self.get_modify_func: - column.add_attribute (cell, "text", self.id) - else: - modify_func = self.get_modify_func () - id_ = self.id - def cell_data_func (column, cell, model, tree_iter): - cell.props.text = modify_func (model.get (tree_iter, id_)[0]) - column.set_cell_data_func (cell, cell_data_func) - - column.props.resizable = True - column.set_sort_column_id (self.id) - -class NameColumn (TextColumn): - - name = "name" - id = ElementModel.COL_FACTORY_NAME - label_header = _("Name") - -class LongNameColumn (TextColumn): - - name = "longname" - id = ElementModel.COL_FACTORY_LONGNAME - label_header = _("Long Name") - -class PluginColumn (TextColumn): - - name = "plugin" - id = ElementModel.COL_PLUGIN_NAME - label_header = _("Plugin") - -class SourceColumn (TextColumn): - - name = "source" - id = ElementModel.COL_SOURCE - label_header = _("Source Module") - -class PackageColumn (TextColumn): - - name = "package" - id = ElementModel.COL_PACKAGE - label_header = _("Binary Package") - -class KlassColumn (TextColumn): - - name = "klass" - id = ElementModel.COL_KLASS - label_header = _("Classification") - - @classmethod - def get_sort_func (cls): - - col = cls.id - def column_sort_func (model, tree_iter_a, tree_iter_b): - obj_a = model.get (tree_iter_a, col)[0] - obj_b = model.get (tree_iter_b, col)[0] - return cmp (sorted (obj_a), sorted (obj_b)) - return column_sort_func - - def get_modify_func (self): - - def modify_func (klasses): - return "/".join (sorted (klasses)) - return modify_func - -class RankColumn (TextColumn): - - name = "rank" - id = ElementModel.COL_RANK - label_header = _("Rank") - - # It is possible to have a column set as sort column without having it shown - # (i.e. having an instance), which is why the custom sort function is - # available at the class level. - - @classmethod - def get_sort_func (cls): - - col = cls.id - def column_sort_func (model, tree_iter_a, tree_iter_b): - obj_a = model.get (tree_iter_a, col)[0] - obj_b = model.get (tree_iter_b, col)[0] - # cmp objects in different order to make higher ranks appear on top - # for ascended order. - return cmp (obj_b, obj_a) - return column_sort_func - - def get_modify_func (self): - - return str - -class ColumnManager (Manager): - - """GUI manager that handles the columns of a list view and the toggle - actions that control their visibility.""" - - column_classes = () - - @classmethod - def iter_item_classes (cls): - - return iter (cls.column_classes) - - def __init__ (self): - - self.view = None - self.actions = None - self.__columns_changed_id = None - self.columns = [] - self.column_order = list (self.column_classes) - - self.action_group = gtk.ActionGroup ("ColumnActions") - - def make_entry (col_class): - return ("show-%s-column" % (col_class.name,), - None, - col_class.label_header, - None, - None, - None, - True,) - - entries = [make_entry (cls) for cls in self.column_classes] - self.action_group.add_toggle_actions (entries) - - def iter_items (self): - - return iter (self.columns) - - def attach (self): - - for col_class in self.column_classes: - action = self.get_toggle_action (col_class) - if action.props.active: - self.__add_column (col_class ()) - action.connect ("toggled", - self.__handle_show_column_action_toggled, - col_class.name) - - self.__columns_changed_id = self.view.connect ("columns-changed", - self.__handle_view_columns_changed) - - def detach (self): - - if self.__columns_changed_id is not None: - self.view.disconnect (self.__columns_changed_id) - self.__columns_changed_id = None - - def attach_sort (self): - - sort_model = self.view.props.model - - # Inform the sorted tree model of any custom sorting functions. - for col_class in self.column_classes: - if col_class.get_sort_func: - sort_func = col_class.get_sort_func () - sort_model.set_sort_func (col_class.id, sort_func) - - def enable_sort (self): - - sort_model = self.view.props.model - - if sort_model: - self.logger.debug ("activating sort") - sort_model.set_sort_column_id (*self.default_sort) - self.default_sort = None - else: - self.logger.debug ("not activating sort (no model set)") - - def disable_sort (self): - - self.logger.debug ("deactivating sort") - - sort_model = self.view.props.model - - self.default_sort = tree_sortable_get_sort_column_id (sort_model) - - sort_model.set_sort_column_id (TREE_SORTABLE_UNSORTED_COLUMN_ID, - gtk.SORT_ASCENDING) - - def get_toggle_action (self, column_class): - - action_name = "show-%s-column" % (column_class.name,) - return self.action_group.get_action (action_name) - - def get_initial_column_order (self): - - return tuple (self.column_classes) - - def __add_column (self, column): - - name = column.name - pos = self.__get_column_insert_position (column) - self.columns.insert (pos, column) - self.view.insert_column (column.view_column, pos) - - def __remove_column (self, column): - - self.columns.remove (column) - self.view.remove_column (column.view_column) - - def __get_column_insert_position (self, column): - - col_class = self.find_item_class (name = column.name) - pos = self.column_order.index (col_class) - before = self.column_order[:pos] - shown_names = [col.name for col in self.columns] - for col_class in before: - if not col_class.name in shown_names: - pos -= 1 - return pos - - def __iter_next_hidden (self, column_class): - - pos = self.column_order.index (column_class) - rest = self.column_order[pos + 1:] - for next_class in rest: - try: - self.find_item (name = next_class.name) - except KeyError: - # No instance -- the column is hidden. - yield next_class - else: - break - - def __handle_show_column_action_toggled (self, toggle_action, name): - - if toggle_action.props.active: - try: - # This should fail. - column = self.find_item (name = name) - except KeyError: - col_class = self.find_item_class (name = name) - self.__add_column (col_class ()) - else: - # Out of sync for some reason. - return - else: - try: - column = self.find_item (name = name) - except KeyError: - # Out of sync for some reason. - return - else: - self.__remove_column (column) - - def __handle_view_columns_changed (self, element_view): - - view_columns = element_view.get_columns () - new_visible = [self.find_item (view_column = column) - for column in view_columns] - - # We only care about reordering here. - if len (new_visible) != len (self.columns): - return - - if new_visible != self.columns: - - new_order = [] - for column in new_visible: - col_class = self.find_item_class (name = column.name) - new_order.append (col_class) - new_order.extend (self.__iter_next_hidden (col_class)) - - names = (column.name for column in new_visible) - self.logger.debug ("visible columns reordered: %s", - ", ".join (names)) - - self.columns[:] = new_visible - self.column_order[:] = new_order - -class InspectorColumnManager (ColumnManager): - - # Order of column classes represents the default column order in the view - # and matches the order of the check menu items in the ui file. - column_classes = (NameColumn, LongNameColumn, KlassColumn, RankColumn, - PluginColumn, SourceColumn, PackageColumn,) - default_columns = column_classes[:2] - - def __init__ (self): - - ColumnManager.__init__ (self) - - self.logger = logging.getLogger ("ui.columns") - - def attach (self, inspector_window): - - self.view = inspector_window.widgets.main_view - model = inspector_window.app.element_model - - self.app = inspector_window.app - - sort_model = self.view.props.model - if sort_model: - self.attach_sort () - self.view.connect ("notify::model", self.__handle_view_notify_model) - - order = self.app.state.column_order - if len (order) == len (self.column_classes): - self.column_order[:] = order - - visible = self.app.state.columns_visible - if not visible: - visible = (NameColumn, LongNameColumn,) - for col_class in self.column_classes: - action = self.get_toggle_action (col_class) - action.props.active = (col_class in visible) - - ColumnManager.attach (self) - - self.default_sort = (model.COL_FACTORY_NAME, gtk.SORT_ASCENDING,) - - state_sort_column = self.app.state.sort_column - state_sort_order = self.app.state.sort_order - if state_sort_order == "unsorted": - self.default_sort = (TREE_SORTABLE_UNSORTED_COLUMN_ID, - gtk.SORT_ASCENDING,) - elif state_sort_column: - sort_id = state_sort_column.id - if state_sort_order == "ascending": - self.default_sort = (sort_id, gtk.SORT_ASCENDING,) - elif state_sort_order == "descending": - self.default_sort = (sort_id, gtk.SORT_DESCENDING,) - - self.enable_sort () - - def detach (self): - - self.app.state.column_order = self.column_order - self.app.state.columns_visible = self.columns - - if self.default_sort is None: - sort_model = self.view.props.model - sort_id, sort_order = tree_sortable_get_sort_column_id (sort_model) - else: - sort_id, sort_order = self.default_sort - - if sort_id >= 0: - self.app.state.sort_column = self.find_item_class (id = sort_id) - if sort_order == gtk.SORT_ASCENDING: - self.app.state.sort_order = "ascending" - else: - self.app.state.sort_order = "descending" - else: - # Most probably TREE_SORTABLE_UNSORTED_COLUMN_ID. - self.app.state.sort_column = None - self.app.state.sort_order = "unsorted" - - ColumnManager.detach (self) - - def __handle_view_notify_model (self, view, gparam): - - model = view.props.model - if model: - self.attach_sort () - -class WindowState (object): - - def __init__ (self): - - self.logger = logging.getLogger ("ui.window-state") - - self.notebook_size_allocate_id = None - - self.paned_size = 0 - self.target_child_size = None - self.is_maximized = False - - def attach (self, inspector_window): - - widgets = inspector_window.widgets - - self.app = inspector_window.app - self.paned = widgets.main_hpaned - self.notebook = widgets.element_info_book - self.window = widgets.inspector_window - - if not self.app.state.info_pane_size: - label = gtk.Label ("Description: This string should have a good size") - size = label.size_request ()[0] - self.app.state.info_pane_size = size - - self.window.connect ("window-state-event", - self.handle_window_state_event) - - self.paned.connect ("size-allocate", - self.handle_paned_size_allocate) - - geometry = self.app.state.geometry - if geometry: - self.window.move (*geometry[:2]) - self.window.set_default_size (*geometry[2:]) - - if self.app.state.maximized: - self.logger.debug ("initially maximized") - self.window.maximize () - - def detach (self): - - window = self.window - - self.app.state.maximized = self.is_maximized - if not self.is_maximized: - position = tuple (window.get_position ()) - size = tuple (window.get_size ()) - self.app.state.geometry = position + size - - if self.notebook_size_allocate_id is not None: - self.notebook.disconnect (self.notebook_size_allocate_id) - self.notebook_size_allocate_id = None - self.notebook = None - - self.paned.disconnect_by_func (self.handle_paned_size_allocate) - self.paned = None - - self.window.disconnect_by_func (self.handle_window_state_event) - self.window = None - - def handle_notebook_size_allocate (self, widget, allocation): - - if self.app.state.info_pane_size != allocation.width: - self.logger.debug ("notebook size changed to %i", allocation.width) - self.app.state.info_pane_size = allocation.width - - def handle_paned_size_allocate (self, widget, allocation): - - if self.paned_size == allocation.width: - return - - self.paned_size = allocation.width - - if self.notebook_size_allocate_id is None: - child_size = self.app.state.info_pane_size - if child_size: - self.set_info_pane_size (child_size) - - handler_id = self.notebook.connect ("size-allocate", - self.handle_notebook_size_allocate) - self.notebook_size_allocate_id = handler_id - self.logger.debug ("now listening for notebook size allocation changes") - - if self.target_child_size is not None: - self.set_info_pane_size (self.target_child_size) - - def handle_window_state_event (self, window, event): - - if not event.changed_mask & gtk.gdk.WINDOW_STATE_MAXIMIZED: - return - - if event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED: - self.logger.debug ("maximized") - self.is_maximized = True - else: - self.logger.debug ("unmaximized") - self.is_maximized = False - - def set_info_pane_size (self, size): - - # For saving and restoring the info notebook size (and therefore the - # paned's handle position), we cannot just save and restore - # paned.props.position. Doing this breaks for maximized windows as - # they seem to undergo a maximized-unmaximized-maximized cycle on - # restore, making the pane handle position increase by the difference - # between the maximized width and the default width. - - paned_children = self.paned.get_children () - if not self.notebook in paned_children: - return - # The 'position' property of gtk.HPaned equals the width of the first - # child. - if self.paned.get_child1 () == self.notebook: - # Notebook on the left. - target_position = size - else: - # Notebook on the right. - border = self.paned.props.border_width - handle = self.paned.style_get_property ("handle-size") - target_position = self.paned_size - 2 * border - handle - size - - if (target_position < self.paned.props.min_position or - target_position > self.paned.props.max_position): - - self.target_child_size = size - self.logger.debug ("target position %i out of bounds, postponing", - target_position) - else: - self.target_child_size = None - self.paned.props.position = target_position - self.logger.debug ("set paned position to %i (for size %i)", - target_position, size) - -class DocumentationAction (gtk.Action): - - def __get_entry (self): - - return self.__entry - - def __set_entry (self, value): - - if value != self.__entry: - self.__entry = value - self.handle_entry_changed () - - entry = property (__get_entry, __set_entry) - - icon_name = None - - def __init__ (self, name): - - gtk.Action.__init__ (self, name, - _("Show _Documentation"), - _("Display documentation about the selected item"), - "") - self.__entry = "" - self.connect ("activate", self.__handle_activate) - self.logger = logging.getLogger ("ui.doc-action") - - if self.icon_name is not None: - icon_theme = gtk.icon_theme_get_default () - if icon_theme and icon_theme.has_icon (self.icon_name): - self.props.icon_name = self.icon_name - - def __handle_activate (self, action): - - if self.entry: - self.handle_activate () - - def handle_entry_changed (self): - - pass - - def handle_activate (self): - - pass - -class DevhelpAction (DocumentationAction): - - icon_name = "devhelp" - - def __init__ (self, name, devhelp_client = None): - - DocumentationAction.__init__ (self, name) - - if devhelp_client is None: - devhelp_client = utils.DevhelpClient () - - self.client = devhelp_client - - if not self.client.available (): - self.props.visible = False - - def handle_entry_changed (self): - - # TODO: This would be the place to filter out undocumented elements, - # but there is no obvious way to do it. - - if self.entry: - self.props.sensitive = True - else: - self.props.sensitive = False - - def handle_activate (self): - - entry = self.entry - - self.logger.info ("invoking devhelp to search for %s", entry) - - try: - self.client.search (entry = entry) - except (utils.DevhelpError, EnvironmentError,), exc: - self.logger.error ("error invoking devhelp: %s", exc) - -class InspectorAppState (AppState): - - element = StateString ("element") - page = StateItem ("page", PageManager) - info_pane_size = StateInt ("info-pane-size") - geometry = StateInt4 ("geometry") - maximized = StateBool ("maximized") - - filter = StateItem ("filter", FilterManager) - filter_param = StateString ("filter-param") - - sort_order = StateString ("sort-order") - sort_column = StateItem ("sort-column", InspectorColumnManager) - column_order = StateItemList ("column-order", InspectorColumnManager) - columns_visible = StateItemList ("columns-visible", InspectorColumnManager) - - def __init__ (self): - - path = os.path.join (utils.XDG.CONFIG_HOME, "gst-inspector", "state") - - # Version 0.1 location: - old_path = os.path.join ("~", ".gstreamer-0.10", "inspector.state") - old_path = os.path.expanduser (old_path) - - AppState.__init__ (self, path, [old_path]) - - self.logger = logging.getLogger ("state") - self.timeout_save_id = None - - def save (self, now = False): - - if now: - - if self.timeout_save_id is not None: - gobject.source_remove (self.timeout_save_id) - self.timeout_save_id = None - - AppState.save (self) - self.logger.debug ("state saved") - - elif self.timeout_save_id is None: - - def state_save_later (): - - self.logger.debug ("auto save state") - self.save (now = True) - return False - - self.logger.debug ("auto save state in 5 seconds") - self.timeout_save_id = gobject.timeout_add (5000, state_save_later) - -class InspectorWindow (Data.Consumer): - - def _get_active (self): - - selection = self.element_view.get_selection () - model, tree_iter = selection.get_selected () - if tree_iter is None: - return None - else: - element = model.get (tree_iter, self.element_model.COL_ELEMENT)[0] - return element - - def _set_active (self, element): - - model = self.element_view.props.model - col = self.element_model.COL_ELEMENT - selection = self.element_view.get_selection () - - if model is None: - raise KeyError ("view model is None, cannot set active element") - - for row in model: - if row[col] == element: - selection.select_iter (row.iter) - return - else: - raise KeyError ("no such row with element %r" % (element,)) - - active = property (_get_active, _set_active) - count = -1 - - def __init__ (self, app): - - self.logger = logging.getLogger ("ui.window") - - InspectorWindow.count += 1 - self.count = InspectorWindow.count - - self.app = app - - self.idle_update_source = None - - self.page_manager = PageManager () - self.filter_manager = FilterManager () - self.column_manager = InspectorColumnManager () - self.window_state = WindowState () - - # These would rather belong in the App object such that one instance of - # each action could be shared between all windows. However, - # GtkUIManager breaks the accelerators then (which could be a bug in - # GTK+). That or I'm getting the usage pattern for gtk actions wrong. - # It seems sharing this way should be just fine since all of the - # action's state is common between all windows at any time (at least - # sharing the menu actions works). - app_group = gtk.ActionGroup ("AppActions") - app_group.add_actions ([("new-window", gtk.STOCK_NEW, _("_New Window")), - ("reload-data", gtk.STOCK_REFRESH, _("_Refresh Data"), "<Ctrl>R")]) - - group = gtk.ActionGroup ("WindowActions") - group.add_actions ([("show-about", gtk.STOCK_ABOUT), - ("close-window", gtk.STOCK_CLOSE, None)]) - group.add_action_with_accel (DevhelpAction ("show-documentation"), "<Ctrl>D") - group.add_toggle_actions ([("show-filter", None, _("Filter"), "<Ctrl>F")]) - - actions = Actions () - actions.add_group (group) - actions.add_group (app_group) - actions.add_group (self.page_manager.action_group) - actions.add_group (self.column_manager.action_group) - - widgets = self.app.widget_factory.make ("inspector_window", - autoconnect = self) - - self.actions = actions - self.widgets = widgets - - self.gtk_window = widgets.inspector_window - self.element_view = widgets.main_view - self.element_count_label = widgets.row_count_label - - ui = self.app.ui_factory.make (self.actions) - self.gtk_window.add_accel_group (ui.get_accel_group ()) - menu_bar = ui.get_widget ("/ui/menubar") - box = widgets.main_vbox - box.pack_start (menu_bar, False, False, 0) - menu_bar.show () - - # We need to keep a reference to the context menubar around, otherwise - # the child menus will have invalid windows: - self.context_menubar = ui.get_widget ("/ui/context") - self.name_value_popup = ui.get_widget ("/ui/context/NameValueContextMenu").get_submenu () - self.columns_popup = ui.get_widget ("/ui/menubar/ViewMenu/ViewColumnsMenu").get_submenu () - - model = self.app.element_model - self.element_model = model - self.element_filter = model.filter_new () - self.filter_func = UIFilterNone.filter_func - model_get = model.get - element_col = model.COL_ELEMENT - def filter_func (model, tree_iter): - element = model_get (tree_iter, element_col)[0] - if element is None: - # This happens during (re)load. The model filter reacts to - # row-inserted signals and lets us evaluate these new rows. - # However, our ElementModel is based on gtk.ListStore. For - # these, adding a row and setting its values are separated - # operations. Therefore, all added rows are initially empty. - return False - else: - return self.filter_func (element) - self.element_filter.set_visible_func (filter_func) - - self.default_focus_widget = None - self.default_active_name = self.app.state.element - - self.attach () - - def __repr__ (self): - - return "<%s object (index %i) at 0x%x>" % (type (self).__name__, - self.count, - id (self),) - - def attach (self): - - self.window_state.attach (self) - - self.actions.close_window.connect ("activate", self.handle_close_window_action_activate) - self.actions.show_filter.connect ("toggled", self.handle_show_filter_action_toggled) - self.actions.show_about.connect ("activate", self.handle_show_about_action_activate) - - window = self.gtk_window - window.connect ("delete-event", self.handle_window_delete_event) - window.connect ("realize", self.handle_window_realize) - - model = self.app.element_model - view = self.element_view - view.drag_dest_unset () - view.unset_rows_drag_source () - view.get_selection ().connect ("changed", self.handle_element_view_selection_changed) - widget_add_popup_menu (view, self.columns_popup) - - self.page_manager.attach (self) - self.filter_manager.attach (self) - self.column_manager.attach (self) - - default_filter = self.app.state.filter - if default_filter and default_filter != UIFilterNone: - self.actions.show_filter.props.active = True - - if len (model): - # Model is already filled, so we are a new window created after - # data has been loaded. - self.post_attach () - - window.show () - - def post_attach (self): - - view = self.element_view - view.props.model = gtk.TreeModelSort (self.element_filter) - view.set_search_column (self.element_model.COL_FACTORY_NAME) - - self.filter_manager.handle_load_finished () - - self.update_element_count () - - # Sorting was postponed until now, which is much faster: - self.column_manager.enable_sort () - - if self.default_active_name is None: - self.select_first_row () - else: - try: - self.active = self.find_element (name = self.default_active_name) - except KeyError: - self.select_first_row () - - self.scroll_to_active () - - if self.default_focus_widget is None: - # The element view isn't set as initial default right away because - # the tree view sets the first column header button as focus widget - # if the model contains no rows (like before load). - self.default_focus_widget = self.element_view - self.default_focus_widget.grab_focus () - - def detach (self): - - state = self.app.state - - if self.active is not None: - state.element = self.active.name - - self.page_manager.detach () - self.filter_manager.detach () - self.column_manager.detach () - self.window_state.detach () - - self.gtk_window.hide () - - state.save () - - self.gtk_window.destroy () - - def find_element (self, name): - - col = self.element_model.COL_ELEMENT - - for row in self.element_model: - element = row[col] - if element.name == name: - return element - else: - raise KeyError ("no such element with name %r" % (name,)) - - def set_filter_func (self, func): - - if func == self.filter_func: - self.logger.debug ("ignoring attempt to set same filter func again") - return - - if self.active is not None: - self.default_active_name = self.active.name - - self.logger.debug ("changing filter func, refiltering element model") - self.filter_func = func - self.element_filter.refilter () - self.update_element_count () - - if self.active is None and self.default_active_name is not None: - try: - self.active = self.find_element (name = self.default_active_name) - except KeyError: - pass - else: - self.scroll_to_active () - - def set_busy_cursor (self, setting): - - window = self.gtk_window - - if not window.window: - return - - if setting: - window.window.set_cursor (gtk.gdk.Cursor (gtk.gdk.WATCH)) - else: - window.window.set_cursor (None) - - def show_filter (self): - - self.logger.debug ("showing filter") - self.widgets.filter_vbox.show () - self.filter_manager.pop_default_active () - - def hide_filter (self): - - self.logger.debug ("hiding filter") - self.widgets.filter_vbox.hide () - self.filter_manager.push_default_active () - - def handle_window_realize (self, window): - - self.set_busy_cursor (not window.props.sensitive) - - def handle_show_filter_action_toggled (self, toggle_action): - - visible = toggle_action.props.active - - if visible: - self.show_filter () - else: - self.hide_filter () - - def handle_filter_close_button_clicked (self, button): - - self.actions.show_filter.props.active = False - - def handle_show_about_action_activate (self, action): - - import GstInspector - - dialog = self.app.widget_factory.make_one ("about_dialog") - - # This workaround for the issue in GTK+ bug #345822 ensures that the - # dialog displays the program name as set in the glade file when we run - # against GTK+ 2.12. - - # TODO: Once we depend on a recent enough pygobject, use - # gobject.set_application_name in main instead. The about dialog will - # pick it up from there. - try: - dialog.props.program_name = _("GStreamer Inspector") - except AttributeError: - pass - - dialog.props.version = GstInspector.version - - dialog.run () - - dialog.destroy () - - def handle_window_delete_event (self, window, event): - - self.app.close_window (self) - - def handle_close_window_action_activate (self, action): - - self.app.close_window (self) - - def handle_load_started (self): - - """Data.Consumer method.""" - - if self.active is not None: - self.default_active_name = self.active.name - - self.filtered_row_insertions = 0 - self.element_filter.connect ("row-inserted", self.handle_filtered_row_inserted) - - if self.default_focus_widget is not None: - focus_widget = self.gtk_window.get_focus () - if focus_widget: - self.default_focus_widget = focus_widget - - self.gtk_window.props.sensitive = False - self.set_busy_cursor (True) - self.update_element_count () - - self.filter_manager.handle_load_started () - - if self.element_view.props.model: - self.column_manager.disable_sort () - - def handle_filtered_row_inserted (self, model, tree_path, tree_iter): - - self.filtered_row_insertions += 1 - if self.filtered_row_insertions % 11 == 0: - self.update_element_count () - - def handle_load_finished (self): - - """Data.Consumer method.""" - - self.element_filter.disconnect_by_func (self.handle_filtered_row_inserted) - del self.filtered_row_insertions - - self.set_busy_cursor (False) - self.gtk_window.props.sensitive = True - - self.post_attach () - - def update (self, element): - - if self.idle_update_source is not None: - # Already queued up. - return - - # The real update is deferred, we retrieve the selected row - # when it is really executed. - - def real_update (): - - element = self.active - if element is not None: - self.page_manager.update (element) - self.actions.show_documentation.entry = element.hierarchy[-1] - self.app.state.element = element.name - self.app.state.save () - - self.idle_update_source = None - return False - - self.idle_update_source = gobject.timeout_add (50, real_update) - - def update_element_count (self): - - """Update the label text to reflect the number of currently displayed - elements in the list view.""" - - model = self.element_filter - count = model.iter_n_children (None) - text = ngettext ("%i Element shown", "%i Elements shown", count) - self.element_count_label.props.label = text % (count,) - - def handle_element_view_selection_changed (self, tree_selection): - - element = self.active - if element is None: - # Unselected. The filter has changed and the element got filtered - # out. We do not update in this case; doing so would be too odd to - # the user. Things reach a consistent state again if the user - # picks another element or changes the filter again to make the - # previously selected one reappear (of which we stored the name in - # self.default_active_name). - return - else: - self.update (element) - - def select_first_row (self): - - view = self.element_view - # Get the derived model, which accounts for filtering and sorting: - model = view.props.model - if model is None: - return - tree_iter = model.get_iter_first () - if tree_iter is None: - return - view.scroll_to_cell (model.get_path (tree_iter), view.get_column (0)) - selection = view.get_selection () - selection.select_iter (tree_iter) - # Above call does not emit the signal, need to call handler manually: - self.handle_element_view_selection_changed (selection) - - def scroll_to_active (self): - - view = self.element_view - model = view.props.model - col = self.element_model.COL_ELEMENT - - if model is None: - return - - element = self.active - if element is None: - return - - for row in model: - if row[col] == element: - tree_iter = model.get_iter (row.path) - view.scroll_to_cell (model.get_path (tree_iter), - view.get_column (0), - True, 0.5, 0.0) - -class InspectorApp (Data.Consumer): - - def __init__ (self): - - from sets import Set - - self.logger = logging.getLogger ("app") - self.windows = [] - - self.state = InspectorAppState () - self.element_model = ElementModel () - - ui_filter_classes = FilterManager.iter_item_classes () - model_factories = Set ((cls.get_model_factory () - for cls in ui_filter_classes)) - if None in model_factories: - model_factories.remove (None) - - self.data_producer = Data.Producer (Data.GSourceDispatcher (), - Data.Policy (long_running = True)) - self.data_producer.consumers += model_factories - self.data_producer.consumers += [self.element_model, self] - - gtk.window_set_default_icon_name ("gst-inspector") - - # Keep startup notification spinning until we have filled the view. - gtk.window_set_auto_startup_notification (False) - - menu_group = gtk.ActionGroup ("MenuActions") - menu_group.add_actions ([("FileMenuAction", None, _("_File")), - ("ViewMenuAction", None, _("_View")), - ("ViewColumnsMenuAction", None, _("_Columns")), - ("HelpMenuAction", None, _("_Help")), - ("NameValueContextMenuAction", None, "")]) - - self.actions = Actions () - self.actions.add_group (menu_group) - - glade_filename = os.path.join (main.Paths.data_dir, "gst-inspector.glade") - self.widget_factory = WidgetFactory (glade_filename) - - ui_filename = os.path.join (main.Paths.data_dir, "gst-inspector.ui") - self.ui_factory = UIFactory (ui_filename, self.actions) - - self.new_window () - - def handle_load_started_after (self): - - self.logger.info ("data load has started") - - def handle_data_error (self, error): - - """Data.Consumer method.""" - - if error.exc_type_name == ImportError.__name__: - raise ImportError (error.error) - else: - import sys - print >> sys.stderr, "Exception in child process:" - print >> sys.stderr, error.traceback, - raise RuntimeError ("error from child process: %s" - % (error.error,)) - - def handle_load_finished (self): - - """Data.Consumer method.""" - - gtk.gdk.notify_startup_complete () - - self.logger.info ("data load has finished") - - def new_window (self): - - window = InspectorWindow (self) - self.logger.info ("created new window (%i)", window.count) - self.data_producer.consumers.append (window) - self.windows.append (window) - self.logger.debug ("window list is now %r", self.windows) - window.actions.new_window.connect ("activate", self.handle_new_window_action_activate) - window.actions.reload_data.connect ("activate", self.handle_reload_data_action_activate) - - def close_window (self, window): - - try: - window.detach () - finally: - self.state.save () - self.logger.info ("window (%i) closed", window.count) - self.data_producer.consumers.remove (window) - self.windows.remove (window) - - if self.windows: - self.logger.debug ("window list is now %r", self.windows) - - if not self.windows: - self.logger.info ("last window closed, exiting main loop") - self.state.save (now = True) - - gtk.main_quit () - - def update_data (self): - - """Trigger a refresh of all introspection data.""" - - self.logger.info ("reloading data") - - policy = Data.Policy (update = True) - self.data_producer.policy.modify (policy) - self.data_producer.start () - - def handle_new_window_action_activate (self, action): - - self.new_window () - - def handle_reload_data_action_activate (self, action): - - self.update_data () - - def run (self): - - try: - self.data_producer.start () - - main_loop_wrapper = main.MainLoopWrapper (enter = gtk.main, - exit = gtk.main_quit) - main_loop_wrapper.run () - finally: - gtk.gdk.notify_startup_complete () - -def show_uri (location, widget = None): - - import webbrowser - - try: - webbrowser.open_new (location) - except webbrowser.Error, exc: - if not widget: - raise - while widget.get_parent (): - widget = widget.get_parent () - msg1 = _("Could not open browser window") - msg2 = str (exc) - dialog = gtk.MessageDialog (widget, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, - gtk.BUTTONS_OK, msg1) - # The property for secondary text is new in 2.10, so we use this clunky - # method instead. - dialog.format_secondary_text (msg2) - dialog.set_default_response (0) - dialog.run () - dialog.destroy () - -def _global_about_dialog_url_hook (about_dialog, uri): - - show_uri (uri, widget = about_dialog) - -def main_gui (*a, **kw): - - from main import Paths, GETTEXT_DOMAIN - - logger = logging.getLogger ("main") - - try: - gtk_version = gtk.ver - except AttributeError: - pass - else: - logger.info ("using GTK+ %i.%i.%i", *gtk_version) - - gtk.about_dialog_set_url_hook (_global_about_dialog_url_hook) - - Paths.ensure_setup () - - if Paths.locale_dir: - gtk.glade.bindtextdomain (GETTEXT_DOMAIN, Paths.locale_dir) - gtk.glade.textdomain (GETTEXT_DOMAIN) - - # Ensure that we can load icons even if installed outside of the regular - # icon search paths (e.g. under a user's home directory). - if Paths.icon_dir: - theme = gtk.icon_theme_get_default () - if theme: - theme.append_search_path (Paths.icon_dir) - del theme - - del Paths, GETTEXT_DOMAIN, logger - - InspectorApp ().run () - -del _ -from gettext import gettext as _ diff --git a/GstInspector/GUI/__init__.py b/GstInspector/GUI/__init__.py new file mode 100644 index 0000000..7ca4636 --- /dev/null +++ b/GstInspector/GUI/__init__.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8; mode: python; -*- +# +# GStreamer Inspector - Multimedia system plugin introspection +# +# Copyright (C) 2007 René Stadler <mail@renestadler.de> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see <http://www.gnu.org/licenses/>. + +"""GStreamer Inspector GUI module.""" + +import logging +import locale +from gettext import gettext as _ + +import pygtk +pygtk.require ("2.0") +del pygtk + +import gtk +import gtk.glade + +from GstInspector.GUI.app import InspectorApp + +def show_uri (location, widget = None): + + import webbrowser + + try: + webbrowser.open_new (location) + except webbrowser.Error, exc: + if not widget: + raise + while widget.get_parent (): + widget = widget.get_parent () + msg1 = _("Could not open browser window") + msg2 = str (exc) + dialog = gtk.MessageDialog (widget, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, + gtk.BUTTONS_OK, msg1) + # The property for secondary text is new in 2.10, so we use this clunky + # method instead. + dialog.format_secondary_text (msg2) + dialog.set_default_response (0) + dialog.run () + dialog.destroy () + +def _global_about_dialog_url_hook (about_dialog, uri): + + show_uri (uri, widget = about_dialog) + +def main_gui (*a, **kw): + + from GstInspector.main import Paths, GETTEXT_DOMAIN + + logger = logging.getLogger ("main") + + try: + gtk_version = gtk.ver + except AttributeError: + pass + else: + logger.info ("using GTK+ %i.%i.%i", *gtk_version) + + gtk.about_dialog_set_url_hook (_global_about_dialog_url_hook) + + Paths.ensure_setup () + + if Paths.locale_dir: + gtk.glade.bindtextdomain (GETTEXT_DOMAIN, Paths.locale_dir) + gtk.glade.textdomain (GETTEXT_DOMAIN) + + # Ensure that we can load icons even if installed outside of the regular + # icon search paths (e.g. under a user's home directory). + if Paths.icon_dir: + theme = gtk.icon_theme_get_default () + if theme: + theme.append_search_path (Paths.icon_dir) + del theme + + del Paths, GETTEXT_DOMAIN, logger + + InspectorApp ().run () diff --git a/GstInspector/GUI/actions.py b/GstInspector/GUI/actions.py new file mode 100644 index 0000000..4858651 --- /dev/null +++ b/GstInspector/GUI/actions.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8; mode: python; -*- +# +# GStreamer Inspector - Multimedia system plugin introspection +# +# Copyright (C) 2007 René Stadler <mail@renestadler.de> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see <http://www.gnu.org/licenses/>. + +"""GStreamer Inspector GUI.actions module.""" + +import logging + +import gtk + +from GstInspector import utils + +class Actions (dict): + + def __init__ (self): + + dict.__init__ (self) + + self.groups = () + + def __getattr__ (self, name): + + try: + return self[name] + except KeyError: + if "_" in name: + try: + return self[name.replace ("_", "-")] + except KeyError: + pass + + raise AttributeError ("no action with name %r" % (name,)) + + def add_group (self, group): + + self.groups += (group,) + for action in group.list_actions (): + self[action.props.name] = action + +class DocumentationAction (gtk.Action): + + def __get_entry (self): + + return self.__entry + + def __set_entry (self, value): + + if value != self.__entry: + self.__entry = value + self.handle_entry_changed () + + entry = property (__get_entry, __set_entry) + + icon_name = None + + def __init__ (self, name): + + gtk.Action.__init__ (self, name, + _("Show _Documentation"), + _("Display documentation about the selected item"), + "") + self.__entry = "" + self.connect ("activate", self.__handle_activate) + self.logger = logging.getLogger ("ui.doc-action") + + if self.icon_name is not None: + icon_theme = gtk.icon_theme_get_default () + if icon_theme and icon_theme.has_icon (self.icon_name): + self.props.icon_name = self.icon_name + + def __handle_activate (self, action): + + if self.entry: + self.handle_activate () + + def handle_entry_changed (self): + + pass + + def handle_activate (self): + + pass + +class DevhelpAction (DocumentationAction): + + icon_name = "devhelp" + + def __init__ (self, name, devhelp_client = None): + + DocumentationAction.__init__ (self, name) + + if devhelp_client is None: + devhelp_client = utils.DevhelpClient () + + self.client = devhelp_client + + if not self.client.available (): + self.props.visible = False + + def handle_entry_changed (self): + + # TODO: This would be the place to filter out undocumented elements, + # but there is no obvious way to do it. + + if self.entry: + self.props.sensitive = True + else: + self.props.sensitive = False + + def handle_activate (self): + + entry = self.entry + + self.logger.info ("invoking devhelp to search for %s", entry) + + try: + self.client.search (entry = entry) + except (utils.DevhelpError, EnvironmentError,), exc: + self.logger.error ("error invoking devhelp: %s", exc) + +from gettext import gettext as _ diff --git a/GstInspector/GUI/app.py b/GstInspector/GUI/app.py new file mode 100644 index 0000000..262dd6e --- /dev/null +++ b/GstInspector/GUI/app.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8; mode: python; -*- +# +# GStreamer Inspector - Multimedia system plugin introspection +# +# Copyright (C) 2007 René Stadler <mail@renestadler.de> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see <http://www.gnu.org/licenses/>. + +"""GStreamer Inspector GUI.app module.""" + +import os +import logging + +import gtk + +from GstInspector import Data, main + +from GstInspector.GUI.actions import Actions +from GstInspector.GUI.filters import FilterManager +from GstInspector.GUI.models import ElementModel +from GstInspector.GUI.state import InspectorAppState +from GstInspector.GUI.utils import UIFactory, WidgetFactory +from GstInspector.GUI.window import InspectorWindow + +class InspectorApp (Data.Consumer): + + def __init__ (self): + + from sets import Set + + self.logger = logging.getLogger ("app") + self.windows = [] + + self.state = InspectorAppState () + self.element_model = ElementModel () + + ui_filter_classes = FilterManager.iter_item_classes () + model_factories = Set ((cls.get_model_factory () + for cls in ui_filter_classes)) + if None in model_factories: + model_factories.remove (None) + + self.data_producer = Data.Producer (Data.GSourceDispatcher (), + Data.Policy (long_running = True)) + self.data_producer.consumers += model_factories + self.data_producer.consumers += [self.element_model, self] + + gtk.window_set_default_icon_name ("gst-inspector") + + # Keep startup notification spinning until we have filled the view. + gtk.window_set_auto_startup_notification (False) + + menu_group = gtk.ActionGroup ("MenuActions") + menu_group.add_actions ([("FileMenuAction", None, _("_File")), + ("ViewMenuAction", None, _("_View")), + ("ViewColumnsMenuAction", None, _("_Columns")), + ("HelpMenuAction", None, _("_Help")), + ("NameValueContextMenuAction", None, "")]) + + self.actions = Actions () + self.actions.add_group (menu_group) + + glade_filename = os.path.join (main.Paths.data_dir, "gst-inspector.glade") + self.widget_factory = WidgetFactory (glade_filename) + + ui_filename = os.path.join (main.Paths.data_dir, "gst-inspector.ui") + self.ui_factory = UIFactory (ui_filename, self.actions) + + self.new_window () + + def handle_load_started_after (self): + + self.logger.info ("data load has started") + + def handle_data_error (self, error): + + """Data.Consumer method.""" + + if error.exc_type_name == ImportError.__name__: + raise ImportError (error.error) + else: + import sys + print >> sys.stderr, "Exception in child process:" + print >> sys.stderr, error.traceback, + raise RuntimeError ("error from child process: %s" + % (error.error,)) + + def handle_load_finished (self): + + """Data.Consumer method.""" + + gtk.gdk.notify_startup_complete () + + self.logger.info ("data load has finished") + + def new_window (self): + + window = InspectorWindow (self) + self.logger.info ("created new window (%i)", window.count) + self.data_producer.consumers.append (window) + self.windows.append (window) + self.logger.debug ("window list is now %r", self.windows) + window.actions.new_window.connect ("activate", self.handle_new_window_action_activate) + window.actions.reload_data.connect ("activate", self.handle_reload_data_action_activate) + + def close_window (self, window): + + try: + window.detach () + finally: + self.state.save () + self.logger.info ("window (%i) closed", window.count) + self.data_producer.consumers.remove (window) + self.windows.remove (window) + + if self.windows: + self.logger.debug ("window list is now %r", self.windows) + + if not self.windows: + self.logger.info ("last window closed, exiting main loop") + self.state.save (now = True) + + gtk.main_quit () + + def update_data (self): + + """Trigger a refresh of all introspection data.""" + + self.logger.info ("reloading data") + + policy = Data.Policy (update = True) + self.data_producer.policy.modify (policy) + self.data_producer.start () + + def handle_new_window_action_activate (self, action): + + self.new_window () + + def handle_reload_data_action_activate (self, action): + + self.update_data () + + def run (self): + + try: + self.data_producer.start () + + main_loop_wrapper = main.MainLoopWrapper (enter = gtk.main, + exit = gtk.main_quit) + main_loop_wrapper.run () + finally: + gtk.gdk.notify_startup_complete () + +from gettext import gettext as _ diff --git a/GstInspector/GUI/columns.py b/GstInspector/GUI/columns.py new file mode 100644 index 0000000..5835d37 --- /dev/null +++ b/GstInspector/GUI/columns.py @@ -0,0 +1,421 @@ +# -*- coding: utf-8; mode: python; -*- +# +# GStreamer Inspector - Multimedia system plugin introspection +# +# Copyright (C) 2007 René Stadler <mail@renestadler.de> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see <http://www.gnu.org/licenses/>. + +"""GStreamer Inspector GUI.columns module.""" + +import logging + +import gtk + +from GstInspector.GUI.models import ElementModel +from GstInspector.GUI.utils import Manager, _ + +# Workaround for a pygtk deficiency. +TREE_SORTABLE_UNSORTED_COLUMN_ID = -1 +def tree_sortable_get_sort_column_id (model): + col, order = model.get_sort_column_id () + if col is None: + return (TREE_SORTABLE_UNSORTED_COLUMN_ID, + gtk.SORT_ASCENDING,) + else: + return (col, order,) + +class Column (object): + + """A single list view column, managed by a ColumnManager instance.""" + + name = None + id = None + label_header = None + get_modify_func = None + get_sort_func = None + + def __init__ (self): + + view_column = gtk.TreeViewColumn (self.label_header) + view_column.props.reorderable = True + + self.view_column = view_column + +class TextColumn (Column): + + def __init__ (self): + + Column.__init__ (self) + + column = self.view_column + cell = gtk.CellRendererText () + column.pack_start (cell) + + if not self.get_modify_func: + column.add_attribute (cell, "text", self.id) + else: + modify_func = self.get_modify_func () + id_ = self.id + def cell_data_func (column, cell, model, tree_iter): + cell.props.text = modify_func (model.get (tree_iter, id_)[0]) + column.set_cell_data_func (cell, cell_data_func) + + column.props.resizable = True + column.set_sort_column_id (self.id) + +class NameColumn (TextColumn): + + name = "name" + id = ElementModel.COL_FACTORY_NAME + label_header = _("Name") + +class LongNameColumn (TextColumn): + + name = "longname" + id = ElementModel.COL_FACTORY_LONGNAME + label_header = _("Long Name") + +class PluginColumn (TextColumn): + + name = "plugin" + id = ElementModel.COL_PLUGIN_NAME + label_header = _("Plugin") + +class SourceColumn (TextColumn): + + name = "source" + id = ElementModel.COL_SOURCE + label_header = _("Source Module") + +class PackageColumn (TextColumn): + + name = "package" + id = ElementModel.COL_PACKAGE + label_header = _("Binary Package") + +class KlassColumn (TextColumn): + + name = "klass" + id = ElementModel.COL_KLASS + label_header = _("Classification") + + @classmethod + def get_sort_func (cls): + + col = cls.id + def column_sort_func (model, tree_iter_a, tree_iter_b): + obj_a = model.get (tree_iter_a, col)[0] + obj_b = model.get (tree_iter_b, col)[0] + return cmp (sorted (obj_a), sorted (obj_b)) + return column_sort_func + + def get_modify_func (self): + + def modify_func (klasses): + return "/".join (sorted (klasses)) + return modify_func + +class RankColumn (TextColumn): + + name = "rank" + id = ElementModel.COL_RANK + label_header = _("Rank") + + # It is possible to have a column set as sort column without having it shown + # (i.e. having an instance), which is why the custom sort function is + # available at the class level. + + @classmethod + def get_sort_func (cls): + + col = cls.id + def column_sort_func (model, tree_iter_a, tree_iter_b): + obj_a = model.get (tree_iter_a, col)[0] + obj_b = model.get (tree_iter_b, col)[0] + # cmp objects in different order to make higher ranks appear on top + # for ascended order. + return cmp (obj_b, obj_a) + return column_sort_func + + def get_modify_func (self): + + return str + +class ColumnManager (Manager): + + """GUI manager that handles the columns of a list view and the toggle + actions that control their visibility.""" + + column_classes = () + + @classmethod + def iter_item_classes (cls): + + return iter (cls.column_classes) + + def __init__ (self): + + self.view = None + self.actions = None + self.__columns_changed_id = None + self.columns = [] + self.column_order = list (self.column_classes) + + self.action_group = gtk.ActionGroup ("ColumnActions") + + def make_entry (col_class): + return ("show-%s-column" % (col_class.name,), + None, + col_class.label_header, + None, + None, + None, + True,) + + entries = [make_entry (cls) for cls in self.column_classes] + self.action_group.add_toggle_actions (entries) + + def iter_items (self): + + return iter (self.columns) + + def attach (self): + + for col_class in self.column_classes: + action = self.get_toggle_action (col_class) + if action.props.active: + self.__add_column (col_class ()) + action.connect ("toggled", + self.__handle_show_column_action_toggled, + col_class.name) + + self.__columns_changed_id = self.view.connect ("columns-changed", + self.__handle_view_columns_changed) + + def detach (self): + + if self.__columns_changed_id is not None: + self.view.disconnect (self.__columns_changed_id) + self.__columns_changed_id = None + + def attach_sort (self): + + sort_model = self.view.props.model + + # Inform the sorted tree model of any custom sorting functions. + for col_class in self.column_classes: + if col_class.get_sort_func: + sort_func = col_class.get_sort_func () + sort_model.set_sort_func (col_class.id, sort_func) + + def enable_sort (self): + + sort_model = self.view.props.model + + if sort_model: + self.logger.debug ("activating sort") + sort_model.set_sort_column_id (*self.default_sort) + self.default_sort = None + else: + self.logger.debug ("not activating sort (no model set)") + + def disable_sort (self): + + self.logger.debug ("deactivating sort") + + sort_model = self.view.props.model + + self.default_sort = tree_sortable_get_sort_column_id (sort_model) + + sort_model.set_sort_column_id (TREE_SORTABLE_UNSORTED_COLUMN_ID, + gtk.SORT_ASCENDING) + + def get_toggle_action (self, column_class): + + action_name = "show-%s-column" % (column_class.name,) + return self.action_group.get_action (action_name) + + def get_initial_column_order (self): + + return tuple (self.column_classes) + + def __add_column (self, column): + + name = column.name + pos = self.__get_column_insert_position (column) + self.columns.insert (pos, column) + self.view.insert_column (column.view_column, pos) + + def __remove_column (self, column): + + self.columns.remove (column) + self.view.remove_column (column.view_column) + + def __get_column_insert_position (self, column): + + col_class = self.find_item_class (name = column.name) + pos = self.column_order.index (col_class) + before = self.column_order[:pos] + shown_names = [col.name for col in self.columns] + for col_class in before: + if not col_class.name in shown_names: + pos -= 1 + return pos + + def __iter_next_hidden (self, column_class): + + pos = self.column_order.index (column_class) + rest = self.column_order[pos + 1:] + for next_class in rest: + try: + self.find_item (name = next_class.name) + except KeyError: + # No instance -- the column is hidden. + yield next_class + else: + break + + def __handle_show_column_action_toggled (self, toggle_action, name): + + if toggle_action.props.active: + try: + # This should fail. + column = self.find_item (name = name) + except KeyError: + col_class = self.find_item_class (name = name) + self.__add_column (col_class ()) + else: + # Out of sync for some reason. + return + else: + try: + column = self.find_item (name = name) + except KeyError: + # Out of sync for some reason. + return + else: + self.__remove_column (column) + + def __handle_view_columns_changed (self, element_view): + + view_columns = element_view.get_columns () + new_visible = [self.find_item (view_column = column) + for column in view_columns] + + # We only care about reordering here. + if len (new_visible) != len (self.columns): + return + + if new_visible != self.columns: + + new_order = [] + for column in new_visible: + col_class = self.find_item_class (name = column.name) + new_order.append (col_class) + new_order.extend (self.__iter_next_hidden (col_class)) + + names = (column.name for column in new_visible) + self.logger.debug ("visible columns reordered: %s", + ", ".join (names)) + + self.columns[:] = new_visible + self.column_order[:] = new_order + +class InspectorColumnManager (ColumnManager): + + # Order of column classes represents the default column order in the view + # and matches the order of the check menu items in the ui file. + column_classes = (NameColumn, LongNameColumn, KlassColumn, RankColumn, + PluginColumn, SourceColumn, PackageColumn,) + default_columns = column_classes[:2] + + def __init__ (self): + + ColumnManager.__init__ (self) + + self.logger = logging.getLogger ("ui.columns") + + def attach (self, inspector_window): + + self.view = inspector_window.widgets.main_view + model = inspector_window.app.element_model + + self.app = inspector_window.app + + sort_model = self.view.props.model + if sort_model: + self.attach_sort () + self.view.connect ("notify::model", self.__handle_view_notify_model) + + order = self.app.state.column_order + if len (order) == len (self.column_classes): + self.column_order[:] = order + + visible = self.app.state.columns_visible + if not visible: + visible = (NameColumn, LongNameColumn,) + for col_class in self.column_classes: + action = self.get_toggle_action (col_class) + action.props.active = (col_class in visible) + + ColumnManager.attach (self) + + self.default_sort = (model.COL_FACTORY_NAME, gtk.SORT_ASCENDING,) + + state_sort_column = self.app.state.sort_column + state_sort_order = self.app.state.sort_order + if state_sort_order == "unsorted": + self.default_sort = (TREE_SORTABLE_UNSORTED_COLUMN_ID, + gtk.SORT_ASCENDING,) + elif state_sort_column: + sort_id = state_sort_column.id + if state_sort_order == "ascending": + self.default_sort = (sort_id, gtk.SORT_ASCENDING,) + elif state_sort_order == "descending": + self.default_sort = (sort_id, gtk.SORT_DESCENDING,) + + self.enable_sort () + + def detach (self): + + self.app.state.column_order = self.column_order + self.app.state.columns_visible = self.columns + + if self.default_sort is None: + sort_model = self.view.props.model + sort_id, sort_order = tree_sortable_get_sort_column_id (sort_model) + else: + sort_id, sort_order = self.default_sort + + if sort_id >= 0: + self.app.state.sort_column = self.find_item_class (id = sort_id) + if sort_order == gtk.SORT_ASCENDING: + self.app.state.sort_order = "ascending" + else: + self.app.state.sort_order = "descending" + else: + # Most probably TREE_SORTABLE_UNSORTED_COLUMN_ID. + self.app.state.sort_column = None + self.app.state.sort_order = "unsorted" + + ColumnManager.detach (self) + + def __handle_view_notify_model (self, view, gparam): + + model = view.props.model + if model: + self.attach_sort () + +del _ diff --git a/GstInspector/GUI/filters.py b/GstInspector/GUI/filters.py new file mode 100644 index 0000000..b9c5d5e --- /dev/null +++ b/GstInspector/GUI/filters.py @@ -0,0 +1,740 @@ +# -*- coding: utf-8; mode: python; -*- +# +# GStreamer Inspector - Multimedia system plugin introspection +# +# Copyright (C) 2007 René Stadler <mail@renestadler.de> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see <http://www.gnu.org/licenses/>. + +"""GStreamer Inspector GUI.filters module.""" + +import locale +import logging + +import pango +import gobject +import gtk + +from GstInspector import Data, utils + +from GstInspector.GUI.utils import Manager, _ + +class FilterModelFactory (Data.ConsumerProxy): + + __metaclass__ = utils.SingletonMeta + + def __init__ (self, data_spec): + + Data.ConsumerProxy.__init__ (self) + + self.model = None + self.collector = Data.Collector (data_spec) + self.consumers.append (self.collector) + + def clear (self): + + if self.model is not None: + self.model.clear () + + def handle_load_started_after (self): + + Data.ConsumerProxy.handle_load_started_after (self) + + self.clear () + + def handle_load_finished (self): + + Data.ConsumerProxy.handle_load_finished (self) + + self.fill () + + self.collector.clear () + +class FilterModelFactoryFlat (FilterModelFactory): + + def __init__ (self, data_spec): + + FilterModelFactory.__init__ (self, data_spec) + + self.model = gtk.ListStore (str) + + def fill (self): + + model = self.model + collector = self.collector + + for sort_name, item in sorted (((locale.strxfrm (unicode (item, "UTF-8")), item,) + for item in collector.items)): + model.set (model.append (), 0, item) + +class FilterModelFactoryNestedAlpha (FilterModelFactory): + + def __init__ (self, data_spec): + + FilterModelFactory.__init__ (self, data_spec) + + self.model = gtk.TreeStore (str) + + def fill (self): + + model = self.model + + # TODO: This would ideally use a dynamic number of partitions. + + iter1 = model.append (None) + iter2 = model.append (None) + iter3 = model.append (None) + iter4 = model.append (None) + + model.set (iter1, 0, "A-G") + model.set (iter2, 0, "H-M") + model.set (iter3, 0, "N-S") + model.set (iter4, 0, "T-Z") + + for sort_name, item in sorted (((locale.strxfrm (unicode (item, "UTF-8")), item,) + for item in self.collector.items)): + if item.upper () < "H": + use_iter = iter1 + elif item.upper () < "N": + use_iter = iter2 + elif item.upper () < "T": + use_iter = iter3 + else: + use_iter = iter4 + model.set (model.append (use_iter), 0, item) + + remove = [row for row in model + if model.iter_n_children (row.iter) == 0] + for row in remove: + model.remove (row.iter) + +class FilterModelFactoryCapsNames (FilterModelFactory): + + def __init__ (self, data_spec): + + FilterModelFactory.__init__ (self, data_spec) + + self.model = gtk.TreeStore (str) + self.flat_model = gtk.ListStore (str) + + def clear (self): + + FilterModelFactory.clear (self) + + self.flat_model.clear () + + def fill (self): + + from sets import Set + + model = self.model + items = self.collector.items + + raw_names = [n for n in items if "-raw-" in n] + non_raw_names = [n for n in items if not "-raw-" in n] + prefixes = Set ((name.split ("/")[0] for name in non_raw_names)) + + for name in sorted (raw_names): + model.set (model.append (None), 0, name) + + if raw_names and non_raw_names: + # Add separator. + model.set (model.append (None), 0, "") + + for prefix in sorted (prefixes): + prefix_iter = model.append (None) + names = [name for name in non_raw_names + if name.startswith (prefix)] + if len (names) == 1: + model.set (prefix_iter, 0, names[0]) + continue + model.set (prefix_iter, 0, "%s/" % (prefix,)) + for name in sorted (names): + model.set (model.append (prefix_iter), 0, name) + + # Fill the flat model (used for completion). Since the popup doesn't + # work, don't even bother sorting the list. + model = self.flat_model + for item in items: + model.set (model.append (), 0, item) + +class UIFilter (object): + + name = None + label = None + param = None + sensitive = True + widget_name = None + data_spec = None + model_factory_class = None + filter_class = None + + @classmethod + def get_model_factory (cls): + + if cls.data_spec and cls.model_factory_class: + return cls.model_factory_class (cls.data_spec) + else: + return None + + def __init__ (self, manager, widgets): + + self.logger = logging.getLogger ("ui.filter.%s" % (self.name,)) + self.manager = manager + + if self.widget_name: + self.widget = getattr (widgets, self.widget_name) + else: + self.widget = None + self.model_factory = self.get_model_factory () + if self.model_factory: + self.model = self.model_factory.model + else: + self.model = None + + self.default_param = None + + def param_changed (self): + + self.manager.handle_ui_filter_param_changed (self) + + def push_default_param (self): + + # Don't overwrite defaults if the filter is inactive: + if self.param and not self.default_param: + self.default_param = self.param + self.logger.debug ("pushed default param %r", + self.default_param) + else: + self.logger.debug ("default param not pushed") + + def pop_default_param (self): + + if self.default_param is not None: + self.param = self.default_param + self.logger.debug ("popped default param %r", + self.default_param) + else: + self.logger.debug ("no default param to pop") + + def get_filter_func (self): + + spec = Data.accessor_transform (self.data_spec, "element") + return self.filter_class (spec, self.param) + +class UIFilterNone (UIFilter): + + name = "show-all" + label = _("Show all elements") + + @staticmethod + def filter_func (element): + return True + + def get_filter_func (self): + + return self.filter_func + +class UIFilterCombo (UIFilter): + + def _get_param (self): + + active_iter = self.widget.get_active_iter () + if active_iter is None: + # Our model is empty. + return None + model = self.widget.props.model + param = model.get (active_iter, 0)[0] + + return param + + def _set_param (self, param): + + model = self.widget.props.model + + for row in model: + if row[0] == param: + # This emits the "changed" signal. + self.widget.set_active_iter (row.iter) + break + else: + raise KeyError ("no such entry for param %r" % (param,)) + + def _get_sensitive (self): + + return len (self.model) > 0 + + param = property (_get_param, _set_param) + sensitive = property (_get_sensitive) + model_factory_class = FilterModelFactoryFlat + + def __init__ (self, manager, widgets, widget_prefix = None): + + if widget_prefix is None: + widget_prefix = self.name + if self.widget_name is None: + self.widget_name = "%s_filter_param_combo" % (widget_prefix,) + + UIFilter.__init__ (self, manager, widgets) + + cell = gtk.CellRendererText () + # We limit the width to 35 characters. The automatic size is far too + # large because of some semi-bogus entries we get (like URLs and almost + # whole sentences for element.author.) + cell.props.width_chars = 35 + cell.props.ellipsize = pango.ELLIPSIZE_END + + widget = self.widget + widget.clear () + widget.pack_start (cell, True) + widget.set_cell_data_func (cell, self.combo_cell_data_func) + widget.props.model = self.model + + self.default_param = self.get_initial_default () + if self.default_param is not None: + self.param = self.default_param + self.default_param = None + + widget.connect ("changed", self.handle_combo_box_changed) + + def get_initial_default (self): + + try: + tree_iter = self.model.get_iter ((0,)) + except ValueError: + self.logger.debug ("no initial default param: model is empty") + return None + else: + param = self.model.get (tree_iter, 0)[0] + self.logger.debug ("initial default param is %r", param) + return param + + def handle_combo_box_changed (self, combo_box): + + self.param_changed () + + @staticmethod + def combo_cell_data_func (layout, cell, model, tree_iter): + + cell.props.text = model.get (tree_iter, 0)[0] + + def pop_default_param (self): + + widget = self.widget + model = widget.props.model + + if self.default_param is None: + self.param = self.get_initial_default () + else: + try: + self.param = self.default_param + except KeyError: + # The param does not exist in the new data anymore, fall back: + self.param = self.get_initial_default () + self.default_param = None + +class UIFilterComboNested (UIFilterCombo): + + def _set_param (self, param): + + model = self.widget.props.model + + for row in model: + for childrow in row.iterchildren (): + if childrow[0] == param: + # This emits the "changed" signal. + self.widget.set_active_iter (childrow.iter) + return + else: + raise KeyError ("no such entry for param %r" % (param,)) + + param = property (UIFilterCombo._get_param, _set_param) + + @staticmethod + def combo_cell_data_func (layout, cell, model, tree_iter): + + props = cell.props + + props.text = model.get (tree_iter, 0)[0] + props.sensitive = not model.iter_has_child (tree_iter) + +class UIFilterAuthor (UIFilterComboNested): + + name = "author" + data_spec = "element.authors.name" + model_factory_class = FilterModelFactoryNestedAlpha + filter_class = Data.FilterIn + label = _("Author") + + def get_initial_default (self): + + try: + tree_iter = self.model.get_iter ((0, 0,)) + except ValueError: + self.logger.debug ("no initial default param: model is empty") + return None + else: + param = self.model.get (tree_iter, 0)[0] + self.logger.debug ("initial default param is %r", param) + return param + +class UIFilterCapsName (UIFilterComboNested): + + def _get_param (self): + + return self.widget.get_active_text () + + def _set_param (self, param): + + entry = self.widget.get_child () + # This emits the "changed" signal. + entry.props.text = param + + name = "caps_name" + widget_name = "caps_filter_param_combo_entry" + data_spec = "element.pads.caps.name" + model_factory_class = FilterModelFactoryCapsNames + filter_class = Data.FilterInIn + # TRANSLATORS: The term "Caps" refers to a GStreamer data structure. + label = _("Caps name") + param = property (_get_param, _set_param) + + def __init__ (self, manager, widgets): + + UIFilterComboNested.__init__ (self, manager, widgets) + + self.id = None + + self.widget.set_row_separator_func (self.row_separator_func) + + flat_model = self.model_factory.flat_model + entry = self.widget.get_child () + comp = gtk.EntryCompletion () + comp.props.model = flat_model + comp.props.text_column = 0 + comp.props.inline_completion = True + # TODO: Would be nice to have the popup, but it doesn't work correctly. + # It gets the wrong size and apparently does not render any text. + comp.props.popup_completion = False + entry.set_completion (comp) + + def get_initial_default (self): + + return "" + + def handle_combo_box_changed (self, widget): + + # We idly defer signalling the change, which will trigger refiltering. + # This helps against sluggishness during typing, which occurs because + # of TreeView slowness (the filtering itself is fast). + + if self.id is not None: + # Already queued up. + return + + def emit_change_deferred (): + + self.param_changed () + self.id = None + return False + + self.id = gobject.idle_add (emit_change_deferred, priority = gobject.PRIORITY_LOW) + + def pop_default_param (self): + + if self.default_param is not None: + # TODO: Ensure that we also set the active iter somehow, to make + # the scroll wheel work as expected. + self.param = self.default_param + + @staticmethod + def row_separator_func (model, tree_iter): + + return model.get (tree_iter, 0)[0] == "" + +class UIFilterKlass (UIFilterCombo): + + name = "klass" + data_spec = "element.klasses" + filter_class = Data.FilterIn + label = _("Classification") + +class UIFilterInterface (UIFilterCombo): + + name = "interface" + data_spec = "element.interfaces" + filter_class = Data.FilterIn + label = _("Interface") + +class UIFilterLicense (UIFilterCombo): + + name = "license" + data_spec = "plugin.license" + filter_class = Data.FilterEq + label = _("License") + +class UIFilterSource (UIFilterCombo): + + name = "source" + data_spec = "plugin.source" + filter_class = Data.FilterEq + label = _("Source module") + +class UIFilterPackage (UIFilterCombo): + + name = "package" + data_spec = "plugin.package" + filter_class = Data.FilterEq + label = _("Binary package") + +class UIFilterParentClass (UIFilterCombo): + + name = "parent" + data_spec = "element.parent_classes" + filter_class = Data.FilterIn + label = _("Parent class") + +class UIFilterURIProtocols (UIFilterCombo): + + name = "uri_protocol" + data_spec = "element.uri_protocols" + filter_class = Data.FilterIn + label = _("URI protocol") + + @staticmethod + def combo_cell_data_func (layout, cell, model, tree_iter): + + cell.props.text = "%s://" % (model.get (tree_iter, 0)[0],) + +class FilterManager (Manager): + + """GUI manager that handles the filter selection combo box and the notebook + containing the parameter selectors for the available filters. The + individual parameter selector pages are handled by specific objects derived + from the UIFilter class.""" + + filter_classes = (UIFilterNone, UIFilterAuthor, UIFilterCapsName, + UIFilterInterface, UIFilterKlass, UIFilterLicense, + UIFilterPackage, UIFilterParentClass, UIFilterSource, + UIFilterURIProtocols,) + + @classmethod + def iter_item_classes (cls): + + return iter (cls.filter_classes) + + def _get_active (self): + + combo = self.combo + model = combo.props.model + active_iter = combo.get_active_iter () + + if active_iter is None: + return None + else: + return model.get (active_iter, 1)[0] + + def _set_active (self, ui_filter): + + for row in self.combo.props.model: + + label, other_ui_filter = row + + if not label: + # Separator. + continue + + if ui_filter == other_ui_filter: + self.combo.set_active_iter (row.iter) + break + + else: + raise KeyError ("no such UIFilter object %r" % (ui_filter,)) + + active = property (_get_active, _set_active) + + def __init__ (self): + + self.logger = logging.getLogger ("ui.filtermanager") + + self.disabled = False + self.default_active = None + + def attach (self, inspector_window): + + self.app = inspector_window.app + widgets = inspector_window.widgets + + self.set_filter_func = inspector_window.set_filter_func + + self.combo = widgets.filter_combo + self.book = widgets.filter_params_book + self.book.hide () + + model = gtk.ListStore (str, object) + self.combo.set_row_separator_func (self.row_separator_func) + self.combo.props.model = model + + # The first filter is special. + ui_filter = UIFilterNone (self, widgets) + model.set (model.append (), + 0, ui_filter.label, + 1, ui_filter) + model.set (model.append (), + 0, "", + 1, None) + + # Now add the rest. + classes = self.filter_classes[1:] + ui_filters = tuple ((cls (self, widgets) for cls in classes)) + labels = [ui_filter.label for ui_filter in ui_filters] + sort_labels = [locale.strxfrm (label) for label in labels] + for sort_label, label, ui_filter in sorted (zip (sort_labels, + labels, + ui_filters)): + model.set (model.append (), + 0, label, + 1, ui_filter) + + self.combo.props.active = 0 + self.combo.connect ("changed", self.handle_filter_combo_changed) + self.combo.set_row_separator_func (self.row_separator_func) + + filter_class = self.app.state.filter + default_param = self.app.state.filter_param + + if not filter_class: + filter_class = UIFilterNone + self.logger.debug ("no default filter in saved state, using %r", + filter_class.name) + else: + self.logger.debug ("restored default filter %r with param %r from state", + filter_class.name, default_param) + + ui_filter = self.find_item (name = filter_class.name) + ui_filter.default_param = default_param + self.active = ui_filter + self.default_active = ui_filter + + def detach (self): + + active = self.active + state = self.app.state + + state.filter = active + state.filter_param = active.param + + def handle_filter_combo_changed (self, combo_box): + + ui_filter = self.active + + if ui_filter.widget: + self.book.show () + child = ui_filter.widget + pos = self.book.child_get (child, "position")[0] + self.book.props.page = pos + else: + self.book.hide () + + if not ui_filter.sensitive: + return + + if self.disabled: + return + + func = ui_filter.get_filter_func () + self.set_filter_func (func) + + def handle_ui_filter_param_changed (self, ui_filter): + + # Called when a UIFilter instance has changed its parameter. + + active_ui_filter = self.active + if ui_filter != active_ui_filter: + return + + if self.disabled: + return + + func = ui_filter.get_filter_func () + self.set_filter_func (func) + + @staticmethod + def row_separator_func (model, tree_iter): + + return model.get (tree_iter, 0)[0] == "" + + def iter_items (self): + + for label, ui_filter in self.combo.props.model: + if not ui_filter: + # Either "show-all" or separator. + continue + yield ui_filter + + def handle_load_started (self): + + self.disable () + + for ui_filter in self.iter_items (): + ui_filter.push_default_param () + + def handle_load_finished (self): + + for ui_filter in self.iter_items (): + ui_filter.pop_default_param () + + self.enable () + + def disable (self): + + if self.disabled: + return + + self.disabled = True + + show_all = self.find_item (name = "show-all") + self.set_filter_func (show_all.get_filter_func ()) + + def enable (self): + + if not self.disabled: + return + + self.disabled = False + + ui_filter = self.active + if not ui_filter.sensitive: + self.active = self.find_item (name = "show-all") + self.logger.debug ("filter %s lost sensitivity, switched to show-all", + ui_filter.name) + elif ui_filter.name != "show-all": + self.set_filter_func (ui_filter.get_filter_func ()) + + def push_default_active (self): + + self.default_active = self.active + self.active = self.find_item (name = "show-all") + + def pop_default_active (self): + + if self.default_active is None: + raise ValueError ("not attached") + elif self.default_active == self.active: + return + + self.active = self.default_active + +del _ +from gettext import gettext as _ diff --git a/GstInspector/GUI/models.py b/GstInspector/GUI/models.py new file mode 100644 index 0000000..cfa545f --- /dev/null +++ b/GstInspector/GUI/models.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8; mode: python; -*- +# +# GStreamer Inspector - Multimedia system plugin introspection +# +# Copyright (C) 2007 René Stadler <mail@renestadler.de> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see <http://www.gnu.org/licenses/>. + +"""GStreamer Inspector GUI.models module.""" + +import gobject +import gtk + +from GstInspector import Data, utils + +class MetaModel (gobject.GObjectMeta): + + """Meta class for easy setup of gtk tree models. + + Looks for a class attribute named `columns' which must be set to a + sequence of the form name1, type1, name2, type2, ..., where the + names are strings. This metaclass adds the following attributes + to created classes: + + cls.column_types = (type1, type2, ...) + cls.name1 = 0 + cls.name2 = 1 + ... + + Example: A gtk.ListStore derived model can use + + columns = ("COL_NAME", str, "COL_VALUE", str) + + and use this in __init__: + + gtk.ListStore.__init__ (self, *self.column_types) + + Then insert data like this: + + self.set (self.append (), + self.COL_NAME, "spam", + self.COL_VALUE, "ham") + """ + + def __init__ (cls, name, bases, dict): + + super (MetaModel, cls).__init__ (name, bases, dict) + + spec = tuple (cls.columns) + + column_names = spec[::2] + column_types = spec[1::2] + column_indices = range (len (column_names)) + + for col_index, col_name, in zip (column_indices, column_names): + setattr (cls, col_name, col_index) + + cls.column_types = column_types + +class ElementModel (gtk.ListStore, Data.Consumer): + + __metaclass__ = MetaModel + + columns = ("COL_ELEMENT", object, + "COL_FACTORY_NAME", str, + "COL_FACTORY_LONGNAME", str, + "COL_RANK", object, + "COL_KLASS", object, + "COL_PLUGIN_NAME", str, + "COL_PACKAGE", str, + "COL_SOURCE", str,) + + def __init__ (self, finish = None): + + gtk.ListStore.__init__ (self, *self.column_types) + Data.Consumer.__init__ (self) + + def handle_load_started_after (self): + + self.clear () + + def handle_data_added (self, plugin): + + package = plugin.package + source = plugin.source + + for element in plugin.elements: + + self.set (self.append (), + self.COL_ELEMENT, element, + self.COL_FACTORY_LONGNAME, element.longname, + self.COL_FACTORY_NAME, element.name, + self.COL_RANK, element.rank, + self.COL_KLASS, element.klasses, + self.COL_PLUGIN_NAME, plugin.name, + self.COL_PACKAGE, package, + self.COL_SOURCE, source) + +class NameValueModel (gtk.TreeStore): + + __metaclass__ = MetaModel + + columns = ("COL_NAME", str, + "COL_VALUE", str,) + + def __init__ (self): + + gtk.TreeStore.__init__ (self, *self.column_types) + + def set_name_value (self, tree_iter, name, value): + + self.set (tree_iter, self.COL_NAME, name, self.COL_VALUE, value) + +class PadsModel (NameValueModel): + + def __init__ (self, element): + + NameValueModel.__init__ (self) + + templates = element.pads + append = self.append + set = self.set_name_value + + # Move sink pad templates to the beginning of the list. + sorted_templates = ([t for t in templates if t.direction == "sink"] + + [t for t in templates if t.direction != "sink"]) + + _ = utils.gettext_cache () + + for template in sorted_templates: + + caps = template.caps + + template_tree_iter = append (None) + set (template_tree_iter, template.name, None) + set (append (template_tree_iter), _("Direction"), + template.direction) + set (append (template_tree_iter), _("Presence"), + template.presence) + caps_tree_iter = append (template_tree_iter) + # TRANSLATORS: The term "Caps" refers to a GStreamer data + # structure. + set (caps_tree_iter, _("Caps"), None) + if caps.any: + self.set (caps_tree_iter, self.COL_VALUE, "ANY") + continue + elif len (caps) == 0: + self.set (caps_tree_iter, self.COL_VALUE, "EMPTY") + continue + for structure in caps: + struct_tree_iter = append (caps_tree_iter) + set (struct_tree_iter, structure.name, None) + for name, value in structure.fields: + set (append (struct_tree_iter), name, value) + +class PropertiesModel (NameValueModel): + + def __init__ (self, element): + + NameValueModel.__init__ (self) + + set = self.set_name_value + _ = utils.gettext_cache () + + for prop in element.properties: + + tree_iter = self.append (None) + if prop.owner_name == element.type_name: + set (tree_iter, prop.name, None) + else: + # The property is inherited from a parent class. + set (tree_iter, prop.name, _("(from %s)") % (prop.owner_name,)) + + if prop.description and prop.description != prop.name: + set (self.append (tree_iter), _("Description"), prop.description) + set (self.append (tree_iter), _("Flags"), ", ".join (prop.flags)) + set (self.append (tree_iter), _("Data type"), prop.data_type) + + if not prop.default in (None, "", (),): + if prop.default in (True, False,): + str_default = str (prop.default).lower () + else: + str_default = str (prop.default) + set (self.append (tree_iter), _("Default"), str_default) + + if prop.minimum is not None and prop.maximum is not None: + set (self.append (tree_iter), _("Minimum"), str (prop.minimum)) + set (self.append (tree_iter), _("Maximum"), str (prop.maximum)) + + if prop.values: + e_tree_iter = self.append (tree_iter) + set (e_tree_iter, _("Values"), None) + for data in prop.values: + set (self.append (e_tree_iter), data.nickname, data.description) + +class SignalsModel (NameValueModel): + + def __init__ (self, element): + + NameValueModel.__init__ (self) + + set = self.set_name_value + _ = utils.gettext_cache () + + for signal in element.signals: + + flags = ", ".join (signal.flags) + arguments = ",\n".join (signal.parameters) + + tree_iter = self.append (None) + if signal.owner_name == element.type_name: + set (tree_iter, signal.name, None) + else: + # The signal is inherited from a parent class. + set (tree_iter, signal.name, _("(from %s)") % (signal.owner_name,)) + if flags: + set (self.append (tree_iter), _("Flags"), flags) + if arguments: + set (self.append (tree_iter), _("Parameters"), arguments) + set (self.append (tree_iter), _("Returns"), signal.return_type) + diff --git a/GstInspector/GUI/pages.py b/GstInspector/GUI/pages.py new file mode 100644 index 0000000..5388c9d --- /dev/null +++ b/GstInspector/GUI/pages.py @@ -0,0 +1,905 @@ +# -*- coding: utf-8; mode: python; -*- +# +# GStreamer Inspector - Multimedia system plugin introspection +# +# Copyright (C) 2007 René Stadler <mail@renestadler.de> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see <http://www.gnu.org/licenses/>. + +"""GStreamer Inspector GUI.pages module.""" + +import logging +from gettext import ngettext + +import pango +import gobject +import gtk + +from GstInspector.GUI.models import NameValueModel, PadsModel, PropertiesModel, SignalsModel +from GstInspector.GUI.utils import Manager, iter_container, size_group_for_tables, widget_add_popup_menu + +# Pre 2.10 GTK+ compatibility. +_gtk_has_link_buttons = hasattr (gtk, "LinkButton") + +def walk_tree_model (model): + + tree_iter = model.get_iter_root () + + while tree_iter: + yield tree_iter + if model.iter_has_child (tree_iter): + tree_iter = model.iter_children (tree_iter) + else: + next = model.iter_next (tree_iter) + if next: + tree_iter = next + else: + tree_iter = model.iter_parent (tree_iter) + while tree_iter: + next = model.iter_next (tree_iter) + if next: + tree_iter = next + break + else: + tree_iter = model.iter_parent (tree_iter) + +class Page (object): + + name = None + widget_name = None + + sensitive = True + + def __init__ (self, window): + + self.logger = logging.getLogger ("ui.page.%s" % (self.name,)) + self.widget = getattr (window.widgets, self.widget_name) + self.element = None + + def update (self, element): + + self.element = element + + self.handle_update (element) + + def handle_update (self, element): + + pass + +class DetailsPage (Page): + + name = "details" + widget_name = "details_scrolled" + + def __init__ (self, window, *a, **kw): + + Page.__init__ (self, window, *a, **kw) + + widgets = window.widgets + + self.widgets = widgets + + size_group_for_tables ((widgets.plugin_details_table, + widgets.element_details_table,), + gtk.SIZE_GROUP_HORIZONTAL, + left_attach = 0, right_attach = 1) + + if _gtk_has_link_buttons: + self._prepare_origin () + + def _prepare_origin (self): + + box = self.widgets.origin_hbox + label = self.widgets.origin_label + label.hide () + self.link_button = gtk.LinkButton ("") + self.link_button.props.xalign = 0.0 + self.link_button.connect ("clicked", self.handle_link_button_clicked) + self.link_button.show () + + # Little hack to make the label inside the LinkButton align with all + # the others. The LinkButton has a margin around the label. A shadow + # (border) is drawn there on mouse over only, so it looks totally odd + # without this little adjustment. + button_req_width = self.link_button.size_request ()[0] + child_req_width = self.link_button.child.size_request ()[0] + extra_pad = (button_req_width - child_req_width) / 2 + children = [label] + children += iter_container (self.widgets.element_details_table, + left_attach = 1, right_attach = 2) + children += iter_container (self.widgets.plugin_details_table, + left_attach = 1, right_attach = 2) + for child in children: + if child != box: + child.props.xpad = extra_pad + + box.pack_start (self.link_button, False, False) + + def _update_origin (self, origin): + + if "://" in origin: + is_uri = True + else: + is_uri = False + + if is_uri: + self.widgets.origin_label.hide () + button = self.link_button + button.props.label = origin + button.props.uri = origin + button.show () + else: + self.link_button.hide () + label = self.widgets.origin_label + label.props.label = origin + label.show () + + def _fix_text (self, text): + + if text is None: + return "" + text = text.replace ("\n", " ").replace (" ", " ") + return text + + def handle_link_button_clicked (self, button): + + show_uri (button.props.uri, widget = button) + + def handle_update (self, element): + + widgets = self.widgets + plugin = element.plugin + + # Element details. + + description = self._fix_text (element.description) + authors_h = ngettext ("Author:", "Authors:", + len (element.authors or ())) + authors = "\n".join ((str (a) for a in element.authors)) + uri_protocols = ", ".join (("%s://" % (s,) + for s in element.uri_protocols)) + widgets.uri_protocols_label.props.sensitive = uri_protocols and True + if not uri_protocols: + uri_protocols = _("Handles no URIs") + interfaces = "\n".join (element.interfaces) + widgets.interfaces_label.props.sensitive = interfaces and True + if not interfaces: + interfaces = _("Implements no interfaces") + hierarchy = "\n".join (" " * i + h + for i, h in zip (range (len (element.hierarchy)), + element.hierarchy)) + + widgets.element_name_label.props.label = element.name + widgets.element_longname_label.props.label = element.longname + widgets.element_description_label.props.label = description + widgets.authors_h_label.props.label = authors_h + widgets.authors_label.props.label = authors + widgets.rank_label.props.label = str (element.rank) + widgets.klasses_label.props.label = "/".join (element.klasses) + widgets.uri_protocols_label.props.label = uri_protocols + widgets.interfaces_label.props.label = interfaces + widgets.hierarchy_label.props.label = hierarchy + + # Plugin details. + + description = self._fix_text (plugin.description) + filename = plugin.filename + if not filename: + filename = "(static plugin)" + + widgets.plugin_name_label.props.label = plugin.name + widgets.plugin_description_label.props.label = description + widgets.filename_label.props.label = filename + widgets.version_label.props.label = plugin.version + widgets.license_label.props.label = plugin.license + widgets.source_label.props.label = plugin.source + widgets.package_label.props.label = plugin.package + if not _gtk_has_link_buttons: + widgets.origin_label.props.label = plugin.origin + else: + self._update_origin (plugin.origin) + +class SpanningCellRenderer (gtk.GenericCellRenderer): + + DEBUG_DISABLE_DRAW_SPACING = False + + def __init__ (self, *a, **kw): + + gtk.CellRenderer.__init__ (self, *a, **kw) + + self.child_cells = [] + self.spacing = 0 + self.connect ("notify::is-expander", self.handle_notify_is_expander) + self.connect ("notify::is-expanded", self.handle_notify_is_expanded) + + def add_cell (self, cell): + + if len (self.child_cells) == 2: + raise NotImplementedError ("can only handle two child cells for now") + + self.child_cells.append (cell) + + def handle_notify_is_expander (self, prop, value): + + for child in self.child_cells: + child.props.is_expander = value + + def handle_notify_is_expanded (self, prop, value): + + for child in self.child_cells: + child.props.is_expanded = value + + def on_get_size (self, widget, area = None): + + visible_cells = [c for c in self.child_cells if c.props.visible] + width, height = 0, 0 + + for child in visible_cells: + cx, cy, cw, ch = child.get_size (widget) + width += cw + height = max (height, ch) + + if visible_cells: + width += self.spacing * (len (visible_cells) - 1) + + return (0, 0, width, height,) + + @staticmethod + def get_child_width (cell, widget): + + if cell.props.width > 0: + return cell.props.width + return cell.get_size (widget)[2] + + def on_render (self, window, widget, background_area, cell_area, expose_area, flags): + + if not self.child_cells: + return + + if len (self.child_cells) == 2: + child1, child2 = self.child_cells + else: + child1 = self.child_cells[0] + child2 = None + if not child1.props.visible and (not child2 or not child2.props.visible): + return + if not child2 or not child2.props.visible: + child1.render (window, widget, background_area, cell_area, expose_area, flags) + elif not child1.props.visible: + child2.render (window, widget, background_area, cell_area, expose_area, flags) + else: + child1_width = self.get_child_width (child1, widget) + + cell_area1 = cell_area.copy () + cell_area2 = cell_area.copy () + + bg_area1 = background_area.copy () + bg_area2 = background_area.copy () + + cell_area1.width = child1_width + cell_area2.x += child1_width + self.spacing + cell_area2.width -= child1_width + + bg_area1.width = cell_area1.x + cell_area1.width - bg_area1.x + if not self.DEBUG_DISABLE_DRAW_SPACING: + bg_area1.width += self.spacing // 2 + + bg_area2.x = bg_area1.x + bg_area1.width + bg_area2.width -= bg_area1.width + if self.DEBUG_DISABLE_DRAW_SPACING: + bg_area2.x += self.spacing + bg_area2.width -= self.spacing + + child1.render (window, widget, bg_area1, cell_area1, expose_area, flags) + child2.render (window, widget, bg_area2, cell_area2, expose_area, flags) + + def on_activate (event, widget, path, background_area, cell_area, flags): + + raise NotImplementedError ("activation is not implemented") + + def on_start_editing (event, widget, path, background_area, cell_area, flags): + + raise NotImplementedError ("editing is not implemented") + +class NameValuePageBase (Page): + + """Base class for pages that display a tree view populated with name/value + pairs.""" + + DEBUG_DRAW_COLORED_AREAS = False + + CELL_SPACING = 12 + + view_name = None + + def __init__ (self, window): + + Page.__init__ (self, window) + + self.view = view = getattr (window.widgets, self.view_name) + + self.old_size = None + + view.connect ("row-collapsed", self.handle_view_row_collapsed) + view.connect ("row-expanded", self.handle_view_row_expanded) + view.connect ("row-activated", self.handle_view_row_activated) + view.connect ("notify::style", self.handle_view_notify_style) + view.connect ("notify::model", self.handle_view_notify_model) + + self.name_cell_width = 0 + self.value_cell_width = 0 + self.cached_name_widths = {} + self.cached_name_width = None + + self.tree_view_states = {} + self.collapsed_rows = [] + self.expanded_rows = [] + + cell = SpanningCellRenderer () + self.cell = cell + cell.spacing = self.CELL_SPACING + + sub_cell = gtk.CellRendererText () + # Storing xpad in instance to speed up the cell data function: + self.name_xpad = sub_cell.props.xpad + cell.add_cell (sub_cell) + + sub_cell = gtk.CellRendererText () + cell.add_cell (sub_cell) + + self.set_default_cell_data () + + column = gtk.TreeViewColumn ("") + self.column = column + column.connect ("notify::width", self.handle_column_notify_width) + column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + column.pack_start (cell, True) + column.set_cell_data_func (cell, self.cell_data_function) + view.append_column (column) + + self.update_style_values () + + widget_add_popup_menu (self.view, window.name_value_popup) + + def update_style_values (self): + + self.expander_size = self.view.style_get_property ("expander-size") + self.h_separator = self.view.style_get_property ("horizontal-separator") + self.focus_line_width = self.view.style_get_property ("focus-line-width") + try: + self.level_indentation = self.view.props.level_indentation + self.show_expanders = self.view.props.show_expanders + except AttributeError: + # These properties were added during the 2.10 series. + self.level_indentation = 0 + self.show_expanders = True + + EXPANDER_EXTRA_PADDING = 4 + self.expander_size += EXPANDER_EXTRA_PADDING + + def get_indentation (self, depth): + + if self.show_expanders: + expander = self.expander_size * (depth + 1) + elif self.show_expanders: + expander = self.expander_size + else: + expander = 0 + + if depth > 0: + level = self.level_indentation * (depth - 1) + else: + level = 0 + + return expander + level + + def set_default_cell_data (self, intrinsic = False): + + name_cell, value_cell = self.cell.child_cells + name_props = name_cell.props + value_props = value_cell.props + + name_props.ellipsize = pango.ELLIPSIZE_END + if intrinsic: + name_props.ellipsize = pango.ELLIPSIZE_NONE + name_props.wrap_width = -1 + name_props.width = -1 + else: + name_props.ellipsize = pango.ELLIPSIZE_END + if self.DEBUG_DRAW_COLORED_AREAS: + name_props.cell_background = "blue" + name_props.background = "yellow" + + value_props.wrap_mode = pango.WRAP_WORD_CHAR + if intrinsic: + value_props.wrap_width = -1 + value_props.width = -1 + else: + xpad = value_props.xpad + value_props.width = self.value_cell_width + value_props.wrap_width = max (0, self.value_cell_width - xpad * 2) + + if self.DEBUG_DRAW_COLORED_AREAS: + value_props.cell_background = "red" + value_props.background = "green" + + def cell_data_function (self, column, cell, model, tree_iter, intrinsic = False): + + name, value = model.get (tree_iter, + NameValueModel.COL_NAME, + NameValueModel.COL_VALUE) + depth = model.iter_depth (tree_iter) + + name_cell, value_cell = cell.child_cells + name_props = name_cell.props + value_props = value_cell.props + + name_props.text = name + if model.iter_has_child (tree_iter): + # Line up with expander. + name_props.yalign = 0.5 + else: + name_props.yalign = 0.0 + + value_props.text = value + + if depth != 0 and value is not None: + name_props.weight = pango.WEIGHT_NORMAL + else: + # depth == 0 or value is None + name_props.weight = pango.WEIGHT_BOLD + + if value: + value_props.visible = True + if not intrinsic: + # The intrinsic width includes the indentation, now we subtract + # it again: + indentation = self.get_indentation (depth) + name_width = max (0, self.name_cell_width - indentation) + else: + value_props.visible = False + if not intrinsic: + name_width = self.name_cell_width + self.value_cell_width + + if not intrinsic: + name_props.width = name_width + name_props.wrap_width = max (-1, name_width - self.name_xpad * 2) + + def compute_intrinsic_name_width (self): + + model = self.view.props.model + name_cell, value_cell = self.cell.child_cells + + if not model: + return 0 + + self.set_default_cell_data (intrinsic = True) + + name_width = 0 + for tree_iter in walk_tree_model (model): + + name, value = model.get (tree_iter, + NameValueModel.COL_NAME, + NameValueModel.COL_VALUE) + if value: + # The name cell does not span. + depth = model.iter_depth (tree_iter) + # The computed width depends on the values of just two renderer + # properties: 'text' and whether 'weight' is set to bold. We + # replicate the logic from the data function here to avoid + # calling it (pushing property values into the cell renderer is + # surprisingly slow). + vars = (name, depth == 0 or value is None,) + if vars in self.cached_name_widths: + w = self.cached_name_widths[vars] + else: + self.cell_data_function (self.column, self.cell, model, tree_iter, + intrinsic = True) + w = name_cell.get_size (self.view, None)[2] + self.cached_name_widths[vars] = w + name_width = max (name_width, w + self.get_indentation (depth)) + + # Restore constant renderer properties for regular rendering. + self.set_default_cell_data (intrinsic = False) + + return name_width + + def value_width_if_less_than (self, max_width): + + model = self.view.props.model + value_cell = self.cell.child_cells[1] + + if not model: + return 0 + + self.set_default_cell_data (intrinsic = True) + try: + value_width = 0 + for tree_iter in walk_tree_model (model): + + self.cell_data_function (self.column, self.cell, model, tree_iter, + intrinsic = True) + width = value_cell.get_size (self.view, None)[2] + if width > max_width: + return width + value_width = max (width, value_width) + finally: + self.set_default_cell_data (intrinsic = False) + + return value_width + + def handle_view_row_activated (self, view, tree_path, column): + + expanded = view.row_expanded (tree_path) + + if expanded: + view.collapse_row (tree_path) + else: + view.expand_row (tree_path, True) + + def handle_column_notify_width (self, column, gparam): + + self.update_style_values () + self.recompute_sizes (resize = True) + + def handle_view_notify_style (self, view, gparam): + + self.update_style_values () + self.cached_name_widths.clear () + self.recompute_sizes (force = True) + + def handle_view_notify_model (self, view, gparam): + + model = view.props.model + + if not model: + return + + self.recompute_sizes (force = True) + + if len (model): + self.sensitive = True + view.props.sensitive = True + else: + self.sensitive = False + view.props.sensitive = False + + view.expand_all () + + def recompute_sizes (self, force = False, resize = False): + + width = self.column.props.width + if not force and self.old_size == width: + return + self.old_size = width + + spacing = self.CELL_SPACING + + if force or self.cached_name_width is None: + self.cached_name_width = self.compute_intrinsic_name_width () + name_intrinsic = self.cached_name_width + if name_intrinsic: + width -= spacing + self.h_separator + self.focus_line_width + if name_intrinsic < width // 2: + self.name_cell_width = name_intrinsic + self.value_cell_width = width - name_intrinsic + else: + limit = width - name_intrinsic + value_width = self.value_width_if_less_than (limit) + if value_width < limit: + self.name_cell_width = name_intrinsic + self.value_cell_width = width - name_intrinsic + else: + limit = width // 2 + value_width = self.value_width_if_less_than (limit) + if value_width < limit: + self.name_cell_width = width - value_width + self.value_cell_width = value_width + else: + self.name_cell_width = width // 2 + self.value_cell_width = width - self.name_cell_width + + if self.name_cell_width < 0: + self.name_cell_width = 0 + if self.value_cell_width < 0: + self.value_cell_width = 0 + + self.set_default_cell_data () + + if resize: + # Ensure that the rows receive correct heights (needed since we + # implement wrap widths depending on the column width): + self.column.queue_resize () + + def expand_all (self): + + self.view.expand_all () + + def collapse_all (self): + + self.view.collapse_all () + + def handle_view_row_collapsed (self, view, tree_iter, path): + + try: + self.expanded_rows.remove (path) + except ValueError: + self.collapsed_rows.append (path) + + def handle_view_row_expanded (self, view, tree_iter, path): + + try: + self.collapsed_rows.remove (path) + except ValueError: + self.expanded_rows.append (path) + + def save_tree_view_state (self): + + model = self.view.props.model + if model is None: + return + + selection = self.view.get_selection () + selected_rows = selection.get_selected_rows ()[1] + + top_row = None + if self.view.flags () & gtk.MAPPED: + vis_range = self.view.get_visible_range () + if vis_range: + top_row = vis_range[0] + if top_row == (0,): + top_row = None + + state = {"selected-rows" : selected_rows, + "top-row" : top_row, + "expanded-rows" : tuple (self.expanded_rows), + "collapsed-rows" : tuple (self.collapsed_rows)} + self.tree_view_states[self.element.name] = state + + del self.expanded_rows[:] + del self.collapsed_rows[:] + + def restore_tree_view_state (self): + + try: + state = self.tree_view_states[self.element.name] + except KeyError: + selected_rows = [] + top_row = None + else: + selected_rows = state["selected-rows"] + top_row = state["top-row"] + self.collapsed_rows[:] = state["collapsed-rows"] + self.expanded_rows[:] = state["expanded-rows"] + + for path in self.collapsed_rows: + self.view.collapse_row (path) + + for path in self.expanded_rows: + self.view.expand_row (path, False) + + selection = self.view.get_selection () + selection.unselect_all () + try: + for path in selected_rows: + selection.select_path (path) + except ValueError: + pass + + if top_row is None: + top_row = (0,) + + model = self.view.props.model + if model and len (model): + self.view.scroll_to_cell (top_row, self.column, True, 0., 0.) + + def clear (self): + + if self.element: + self.save_tree_view_state () + + Page.clear (self) + + def update (self, element): + + self.view.handler_block_by_func (self.handle_view_row_collapsed) + self.view.handler_block_by_func (self.handle_view_row_expanded) + + try: + if self.element: + self.save_tree_view_state () + + Page.update (self, element) + + self.restore_tree_view_state () + + finally: + self.view.handler_unblock_by_func (self.handle_view_row_collapsed) + self.view.handler_unblock_by_func (self.handle_view_row_expanded) + +class PadsPage (NameValuePageBase): + + name = "pads" + widget_name = "pads_scrolled" + view_name = "pads_view" + + def handle_update (self, element): + + view = self.view + model = PadsModel (element) + + view.props.model = model + +class PropertiesPage (NameValuePageBase): + + name = "properties" + widget_name = "props_scrolled" + view_name = "props_view" + + def handle_update (self, element): + + view = self.view + model = PropertiesModel (element) + view.props.model = model + + for row in model: + value = row[model.COL_VALUE] + if value is not None: + # Property is inherited from a parent class. + view.collapse_row (row.path) + +class SignalsPage (NameValuePageBase): + + name = "signals" + widget_name = "signals_scrolled" + view_name = "signals_view" + + def handle_update (self, element): + + view = self.view + model = SignalsModel (element) + + view.props.model = model + +class PageManager (Manager): + + """GUI manager that handles the element info notebook. The individual + notebook pages are handled by specific objects derived from the Page + class.""" + + def _get_active (self): + + notebook = self.notebook + + page_index = notebook.props.page + widget = notebook.get_nth_page (page_index) + page = self.pages[widget] + + return page + + def _set_active (self, page): + + notebook = self.notebook + + for i in range (notebook.get_n_pages ()): + widget = notebook.get_nth_page (i) + if page.widget == widget: + self.notebook.props.page = i + return + else: + raise KeyError ("no such page %r" % (page,)) + + page_classes = (DetailsPage, PadsPage, PropertiesPage, SignalsPage,) + active = property (_get_active, _set_active) + + @classmethod + def iter_item_classes (cls): + + return iter (cls.page_classes) + + def __init__ (self, *a, **kw): + + Manager.__init__ (self, *a, **kw) + + self.action_group = gtk.ActionGroup ("NameValueActions") + self.action_group.add_actions ([("expand-all", gtk.STOCK_ZOOM_IN, _("_Expand all")), + ("collapse-all", gtk.STOCK_ZOOM_OUT, _("_Collapse all"))]) + + def attach (self, inspector_window): + + self.app = inspector_window.app + widgets = inspector_window.widgets + actions = inspector_window.actions + + self.notebook = widgets.element_info_book + pages = [cls (inspector_window) for cls in self.page_classes] + + notebook_widgets = [self.notebook.get_nth_page (i) + for i in range (self.notebook.get_n_pages ())] + for page in pages: + assert page.widget in notebook_widgets + + self.pages = dict (((page.widget, page,) for page in pages)) + + last_page_class = self.app.state.page + if not last_page_class: + last_page_class = DetailsPage + + self.active = self.find_item (name = last_page_class.name) + self.element = None + self.update_source = None + + actions.expand_all.connect ("activate", self.handle_expand_all_action_activate) + actions.collapse_all.connect ("activate", self.handle_collapse_all_action_activate) + + def detach (self): + + if self.update_source is not None: + gobject.source_remove (self.update_source) + self.update_source = None + + self.app.state.page = self.active + + def iter_items (self): + + return self.pages.itervalues () + + def update_iter (self): + + while True: + for page in self.pages.values (): + if page.element != self.element: + page.update (self.element) + self.update_label_sensitivity (page) + assert page.element == self.element + yield True + break + else: + self.update_source = None + yield False + return + + def update (self, element): + + self.element = element + + self.active.update (element) + self.update_label_sensitivity (self.active) + + # Defer updating the other pages, which are invisible: + if self.update_source is None: + callback = self.update_iter ().next + self.update_source = gobject.idle_add (callback) + + def update_label_sensitivity (self, page): + + tab_label = self.notebook.get_tab_label (page.widget) + if tab_label: + tab_label.props.sensitive = page.sensitive + + def handle_expand_all_action_activate (self, action): + + try: + self.active.expand_all () + except AttributeError: + pass + + def handle_collapse_all_action_activate (self, action): + + try: + self.active.collapse_all () + except AttributeError: + pass + +from gettext import gettext as _ diff --git a/GstInspector/GUI/state.py b/GstInspector/GUI/state.py new file mode 100644 index 0000000..d75ee6e --- /dev/null +++ b/GstInspector/GUI/state.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8; mode: python; -*- +# +# GStreamer Inspector - Multimedia system plugin introspection +# +# Copyright (C) 2007 René Stadler <mail@renestadler.de> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see <http://www.gnu.org/licenses/>. + +"""GStreamer Inspector GUI.state module.""" + +import os +import logging + +import gobject +import gtk + +from GstInspector import utils + +from GstInspector.GUI.columns import InspectorColumnManager +from GstInspector.GUI.filters import FilterManager +from GstInspector.GUI.pages import PageManager + +class StateString (object): + + """Descriptor for binding to AppState classes.""" + + def __init__ (self, option, section = None): + + self.option = option + self.section = section + + def get_section (self, state): + + if self.section is None: + return state._default_section + else: + return self.section + + def get_getter (self, state): + + return state._parser.get + + def get_default (self, state): + + return None + + def __get__ (self, state, state_class = None): + + import ConfigParser + + if state is None: + return self + + getter = self.get_getter (state) + section = self.get_section (state) + + try: + return getter (section, self.option) + except (ConfigParser.NoSectionError, + ConfigParser.NoOptionError,): + return self.get_default (state) + + def __set__ (self, state, value): + + import ConfigParser + + if value is None: + value = "" + + section = self.get_section (state) + option = self.option + option_value = str (value) + + try: + state._parser.set (section, option, option_value) + except ConfigParser.NoSectionError: + state._parser.add_section (section) + state._parser.set (section, option, option_value) + +class StateBool (StateString): + + """Descriptor for binding to AppState classes.""" + + def get_getter (self, state): + + return state._parser.getboolean + +class StateInt (StateString): + + """Descriptor for binding to AppState classes.""" + + def get_getter (self, state): + + return state._parser.getint + +class StateInt4 (StateString): + + """Descriptor for binding to AppState classes. This implements storing a + tuple of 4 integers.""" + + def __get__ (self, state, state_class = None): + + if state is None: + return self + + value = StateString.__get__ (self, state) + + try: + l = value.split (",") + if len (l) != 4: + return None + else: + return tuple ((int (v) for v in l)) + except (AttributeError, TypeError, ValueError,): + return None + + def __set__ (self, state, value): + + if value is None: + svalue = "" + elif len (value) != 4: + raise ValueError ("value needs to be a 4-sequence, or None") + else: + svalue = ", ".join ((str (v) for v in value)) + + return StateString.__set__ (self, state, svalue) + +class StateItem (StateString): + + """Descriptor for binding to AppState classes. This implements storing a + class controlled by a Manager class.""" + + def __init__ (self, option, manager_class, section = None): + + StateString.__init__ (self, option, section = section) + + self.manager = manager_class + + def __get__ (self, state, state_class = None): + + if state is None: + return self + + value = StateString.__get__ (self, state) + + if not value: + return None + + return self.parse_item (value) + + def __set__ (self, state, value): + + if value is None: + svalue = "" + else: + svalue = value.name + + StateString.__set__ (self, state, svalue) + + def parse_item (self, value): + + name = value.strip () + + try: + return self.manager.find_item_class (name = name) + except KeyError: + return None + +class StateItemList (StateItem): + + """Descriptor for binding to AppState classes. This implements storing an + ordered set of Manager items.""" + + def __get__ (self, state, state_class = None): + + if state is None: + return self + + value = StateString.__get__ (self, state) + + if not value: + return [] + + classes = [] + for name in value.split (","): + item_class = self.parse_item (name) + if item_class is None: + continue + if not item_class in classes: + classes.append (item_class) + + return classes + + def __set__ (self, state, value): + + if value is None: + svalue = "" + else: + svalue = ", ".join ((v.name for v in value)) + + StateString.__set__ (self, state, svalue) + +class AppState (object): + + _default_section = "state" + + def __init__ (self, filename, old_filenames = ()): + + import ConfigParser + + self._filename = filename + self._parser = ConfigParser.RawConfigParser () + success = self._parser.read ([filename]) + if not success: + for old_filename in old_filenames: + success = self._parser.read ([old_filename]) + if success: + break + + def save (self): + + # TODO Py2.5: Use 'with' statement. + fp = utils.SaveWriteFile (self._filename, "wt") + try: + self._parser.write (fp) + except: + fp.discard () + else: + fp.close () + +class InspectorAppState (AppState): + + element = StateString ("element") + page = StateItem ("page", PageManager) + info_pane_size = StateInt ("info-pane-size") + geometry = StateInt4 ("geometry") + maximized = StateBool ("maximized") + + filter = StateItem ("filter", FilterManager) + filter_param = StateString ("filter-param") + + sort_order = StateString ("sort-order") + sort_column = StateItem ("sort-column", InspectorColumnManager) + column_order = StateItemList ("column-order", InspectorColumnManager) + columns_visible = StateItemList ("columns-visible", InspectorColumnManager) + + def __init__ (self): + + path = os.path.join (utils.XDG.CONFIG_HOME, "gst-inspector", "state") + + # Version 0.1 location: + old_path = os.path.join ("~", ".gstreamer-0.10", "inspector.state") + old_path = os.path.expanduser (old_path) + + AppState.__init__ (self, path, [old_path]) + + self.logger = logging.getLogger ("state") + self.timeout_save_id = None + + def save (self, now = False): + + if now: + + if self.timeout_save_id is not None: + gobject.source_remove (self.timeout_save_id) + self.timeout_save_id = None + + AppState.save (self) + self.logger.debug ("state saved") + + elif self.timeout_save_id is None: + + def state_save_later (): + + self.logger.debug ("auto save state") + self.save (now = True) + return False + + self.logger.debug ("auto save state in 5 seconds") + self.timeout_save_id = gobject.timeout_add (5000, state_save_later) diff --git a/GstInspector/GUI/utils.py b/GstInspector/GUI/utils.py new file mode 100644 index 0000000..bd0934b --- /dev/null +++ b/GstInspector/GUI/utils.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8; mode: python; -*- +# +# GStreamer Inspector - Multimedia system plugin introspection +# +# Copyright (C) 2007 René Stadler <mail@renestadler.de> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see <http://www.gnu.org/licenses/>. + +"""GStreamer Inspector GUI.utils module.""" + +from gettext import gettext + +import gtk + +from GstInspector import utils + +def _ (s): + def get (obj): + return gettext (s) + return utils.ClassProperty (get) + +def widget_add_popup_menu (widget, menu, button = 3): + + def popup_callback (widget, event): + + if event.button == button: + menu.popup (None, None, None, event.button, event.get_time ()) + return False + + widget.connect ("button-press-event", popup_callback) + +def iter_container (container, **property_matches): + + for child in container.get_children (): + for name, value in property_matches.iteritems (): + if container.child_get_property (child, name) != value: + break + else: + yield child + +def size_group_for_tables (tables, mode, **property_matches): + + """Example: size_group_for_tables ([table1, table2], + gtk.SIZE_GROUP_HORIZONTAL, + left_attach = 0, + right_attach = 1) + """ + + size_group = gtk.SizeGroup (mode) + for table in tables: + for child in iter_container (table, **property_matches): + size_group.add_widget (child) + +class Manager (object): + + """GUI Manager base class.""" + + @classmethod + def iter_item_classes (cls): + + msg = "%s class does not support manager item class access" + raise NotImplementedError (msg % (cls.__name__,)) + + @classmethod + def find_item_class (self, **kw): + + return self.__find_by_attrs (self.iter_item_classes (), kw) + + def iter_items (self): + + msg = "%s object does not support manager item access" + raise NotImplementedError (msg % (type (self).__name__,)) + + def find_item (self, **kw): + + return self.__find_by_attrs (self.iter_items (), kw) + + @staticmethod + def __find_by_attrs (i, kw): + + from operator import attrgetter + + if len (kw) != 1: + raise ValueError ("need exactly one keyword argument") + + attr, value = kw.items ()[0] + getter = attrgetter (attr) + + for item in i: + if getter (item) == value: + return item + else: + raise KeyError ("no item such that item.%s == %r" % (attr, value,)) + +class Widgets (dict): + + def __init__ (self, glade_tree): + + widgets = glade_tree.get_widget_prefix ("") + dict.__init__ (self, ((w.name, w,) for w in widgets)) + + def __getattr__ (self, name): + + try: + return self[name] + except KeyError: + if "_" in name: + try: + return self[name.replace ("_", "-")] + except KeyError: + pass + + raise AttributeError ("no widget with name %r" % (name,)) + +class WidgetFactory (object): + + def __init__ (self, glade_filename): + + self.filename = glade_filename + + def make (self, widget_name, autoconnect = None): + + glade_tree = gtk.glade.XML (self.filename, widget_name) + + if autoconnect is not None: + glade_tree.signal_autoconnect (autoconnect) + + return Widgets (glade_tree) + + def make_one (self, widget_name): + + glade_tree = gtk.glade.XML (self.filename, widget_name) + + return glade_tree.get_widget (widget_name) + +class UIFactory (object): + + def __init__ (self, ui_filename, actions = None): + + self.filename = ui_filename + if actions: + self.action_groups = actions.groups + else: + self.action_groups = () + + def make (self, extra_actions = None): + + ui_manager = gtk.UIManager () + for action_group in self.action_groups: + ui_manager.insert_action_group (action_group, 0) + if extra_actions: + for action_group in extra_actions.groups: + ui_manager.insert_action_group (action_group, 0) + ui_manager.add_ui_from_file (self.filename) + ui_manager.ensure_update () + + return ui_manager diff --git a/GstInspector/GUI/window.py b/GstInspector/GUI/window.py new file mode 100644 index 0000000..9b1954b --- /dev/null +++ b/GstInspector/GUI/window.py @@ -0,0 +1,607 @@ +# -*- coding: utf-8; mode: python; -*- +# +# GStreamer Inspector - Multimedia system plugin introspection +# +# Copyright (C) 2007 René Stadler <mail@renestadler.de> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see <http://www.gnu.org/licenses/>. + +"""GStreamer Inspector GUI.window module.""" + +import logging +from gettext import ngettext + +import gobject +import gtk + +from GstInspector import Data + +from GstInspector.GUI.actions import Actions, DevhelpAction +from GstInspector.GUI.columns import InspectorColumnManager +from GstInspector.GUI.filters import FilterManager, UIFilterNone +from GstInspector.GUI.pages import PageManager +from GstInspector.GUI.utils import widget_add_popup_menu + +class WindowState (object): + + def __init__ (self): + + self.logger = logging.getLogger ("ui.window-state") + + self.notebook_size_allocate_id = None + + self.paned_size = 0 + self.target_child_size = None + self.is_maximized = False + + def attach (self, inspector_window): + + widgets = inspector_window.widgets + + self.app = inspector_window.app + self.paned = widgets.main_hpaned + self.notebook = widgets.element_info_book + self.window = widgets.inspector_window + + if not self.app.state.info_pane_size: + label = gtk.Label ("Description: This string should have a good size") + size = label.size_request ()[0] + self.app.state.info_pane_size = size + + self.window.connect ("window-state-event", + self.handle_window_state_event) + + self.paned.connect ("size-allocate", + self.handle_paned_size_allocate) + + geometry = self.app.state.geometry + if geometry: + self.window.move (*geometry[:2]) + self.window.set_default_size (*geometry[2:]) + + if self.app.state.maximized: + self.logger.debug ("initially maximized") + self.window.maximize () + + def detach (self): + + window = self.window + + self.app.state.maximized = self.is_maximized + if not self.is_maximized: + position = tuple (window.get_position ()) + size = tuple (window.get_size ()) + self.app.state.geometry = position + size + + if self.notebook_size_allocate_id is not None: + self.notebook.disconnect (self.notebook_size_allocate_id) + self.notebook_size_allocate_id = None + self.notebook = None + + self.paned.disconnect_by_func (self.handle_paned_size_allocate) + self.paned = None + + self.window.disconnect_by_func (self.handle_window_state_event) + self.window = None + + def handle_notebook_size_allocate (self, widget, allocation): + + if self.app.state.info_pane_size != allocation.width: + self.logger.debug ("notebook size changed to %i", allocation.width) + self.app.state.info_pane_size = allocation.width + + def handle_paned_size_allocate (self, widget, allocation): + + if self.paned_size == allocation.width: + return + + self.paned_size = allocation.width + + if self.notebook_size_allocate_id is None: + child_size = self.app.state.info_pane_size + if child_size: + self.set_info_pane_size (child_size) + + handler_id = self.notebook.connect ("size-allocate", + self.handle_notebook_size_allocate) + self.notebook_size_allocate_id = handler_id + self.logger.debug ("now listening for notebook size allocation changes") + + if self.target_child_size is not None: + self.set_info_pane_size (self.target_child_size) + + def handle_window_state_event (self, window, event): + + if not event.changed_mask & gtk.gdk.WINDOW_STATE_MAXIMIZED: + return + + if event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED: + self.logger.debug ("maximized") + self.is_maximized = True + else: + self.logger.debug ("unmaximized") + self.is_maximized = False + + def set_info_pane_size (self, size): + + # For saving and restoring the info notebook size (and therefore the + # paned's handle position), we cannot just save and restore + # paned.props.position. Doing this breaks for maximized windows as + # they seem to undergo a maximized-unmaximized-maximized cycle on + # restore, making the pane handle position increase by the difference + # between the maximized width and the default width. + + paned_children = self.paned.get_children () + if not self.notebook in paned_children: + return + # The 'position' property of gtk.HPaned equals the width of the first + # child. + if self.paned.get_child1 () == self.notebook: + # Notebook on the left. + target_position = size + else: + # Notebook on the right. + border = self.paned.props.border_width + handle = self.paned.style_get_property ("handle-size") + target_position = self.paned_size - 2 * border - handle - size + + if (target_position < self.paned.props.min_position or + target_position > self.paned.props.max_position): + + self.target_child_size = size + self.logger.debug ("target position %i out of bounds, postponing", + target_position) + else: + self.target_child_size = None + self.paned.props.position = target_position + self.logger.debug ("set paned position to %i (for size %i)", + target_position, size) + +class InspectorWindow (Data.Consumer): + + def _get_active (self): + + selection = self.element_view.get_selection () + model, tree_iter = selection.get_selected () + if tree_iter is None: + return None + else: + element = model.get (tree_iter, self.element_model.COL_ELEMENT)[0] + return element + + def _set_active (self, element): + + model = self.element_view.props.model + col = self.element_model.COL_ELEMENT + selection = self.element_view.get_selection () + + if model is None: + raise KeyError ("view model is None, cannot set active element") + + for row in model: + if row[col] == element: + selection.select_iter (row.iter) + return + else: + raise KeyError ("no such row with element %r" % (element,)) + + active = property (_get_active, _set_active) + count = -1 + + def __init__ (self, app): + + self.logger = logging.getLogger ("ui.window") + + InspectorWindow.count += 1 + self.count = InspectorWindow.count + + self.app = app + + self.idle_update_source = None + + self.page_manager = PageManager () + self.filter_manager = FilterManager () + self.column_manager = InspectorColumnManager () + self.window_state = WindowState () + + # These would rather belong in the App object such that one instance of + # each action could be shared between all windows. However, + # GtkUIManager breaks the accelerators then (which could be a bug in + # GTK+). That or I'm getting the usage pattern for gtk actions wrong. + # It seems sharing this way should be just fine since all of the + # action's state is common between all windows at any time (at least + # sharing the menu actions works). + app_group = gtk.ActionGroup ("AppActions") + app_group.add_actions ([("new-window", gtk.STOCK_NEW, _("_New Window")), + ("reload-data", gtk.STOCK_REFRESH, _("_Refresh Data"), "<Ctrl>R")]) + + group = gtk.ActionGroup ("WindowActions") + group.add_actions ([("show-about", gtk.STOCK_ABOUT), + ("close-window", gtk.STOCK_CLOSE, None)]) + group.add_action_with_accel (DevhelpAction ("show-documentation"), "<Ctrl>D") + group.add_toggle_actions ([("show-filter", None, _("Filter"), "<Ctrl>F")]) + + actions = Actions () + actions.add_group (group) + actions.add_group (app_group) + actions.add_group (self.page_manager.action_group) + actions.add_group (self.column_manager.action_group) + + widgets = self.app.widget_factory.make ("inspector_window", + autoconnect = self) + + self.actions = actions + self.widgets = widgets + + self.gtk_window = widgets.inspector_window + self.element_view = widgets.main_view + self.element_count_label = widgets.row_count_label + + ui = self.app.ui_factory.make (self.actions) + self.gtk_window.add_accel_group (ui.get_accel_group ()) + menu_bar = ui.get_widget ("/ui/menubar") + box = widgets.main_vbox + box.pack_start (menu_bar, False, False, 0) + menu_bar.show () + + # We need to keep a reference to the context menubar around, otherwise + # the child menus will have invalid windows: + self.context_menubar = ui.get_widget ("/ui/context") + self.name_value_popup = ui.get_widget ("/ui/context/NameValueContextMenu").get_submenu () + self.columns_popup = ui.get_widget ("/ui/menubar/ViewMenu/ViewColumnsMenu").get_submenu () + + model = self.app.element_model + self.element_model = model + self.element_filter = model.filter_new () + self.filter_func = UIFilterNone.filter_func + model_get = model.get + element_col = model.COL_ELEMENT + def filter_func (model, tree_iter): + element = model_get (tree_iter, element_col)[0] + if element is None: + # This happens during (re)load. The model filter reacts to + # row-inserted signals and lets us evaluate these new rows. + # However, our ElementModel is based on gtk.ListStore. For + # these, adding a row and setting its values are separated + # operations. Therefore, all added rows are initially empty. + return False + else: + return self.filter_func (element) + self.element_filter.set_visible_func (filter_func) + + self.default_focus_widget = None + self.default_active_name = self.app.state.element + + self.attach () + + def __repr__ (self): + + return "<%s object (index %i) at 0x%x>" % (type (self).__name__, + self.count, + id (self),) + + def attach (self): + + self.window_state.attach (self) + + self.actions.close_window.connect ("activate", self.handle_close_window_action_activate) + self.actions.show_filter.connect ("toggled", self.handle_show_filter_action_toggled) + self.actions.show_about.connect ("activate", self.handle_show_about_action_activate) + + window = self.gtk_window + window.connect ("delete-event", self.handle_window_delete_event) + window.connect ("realize", self.handle_window_realize) + + model = self.app.element_model + view = self.element_view + view.drag_dest_unset () + view.unset_rows_drag_source () + view.get_selection ().connect ("changed", self.handle_element_view_selection_changed) + widget_add_popup_menu (view, self.columns_popup) + + self.page_manager.attach (self) + self.filter_manager.attach (self) + self.column_manager.attach (self) + + default_filter = self.app.state.filter + if default_filter and default_filter != UIFilterNone: + self.actions.show_filter.props.active = True + + if len (model): + # Model is already filled, so we are a new window created after + # data has been loaded. + self.post_attach () + + window.show () + + def post_attach (self): + + view = self.element_view + view.props.model = gtk.TreeModelSort (self.element_filter) + view.set_search_column (self.element_model.COL_FACTORY_NAME) + + self.filter_manager.handle_load_finished () + + self.update_element_count () + + # Sorting was postponed until now, which is much faster: + self.column_manager.enable_sort () + + if self.default_active_name is None: + self.select_first_row () + else: + try: + self.active = self.find_element (name = self.default_active_name) + except KeyError: + self.select_first_row () + + self.scroll_to_active () + + if self.default_focus_widget is None: + # The element view isn't set as initial default right away because + # the tree view sets the first column header button as focus widget + # if the model contains no rows (like before load). + self.default_focus_widget = self.element_view + self.default_focus_widget.grab_focus () + + def detach (self): + + state = self.app.state + + if self.active is not None: + state.element = self.active.name + + self.page_manager.detach () + self.filter_manager.detach () + self.column_manager.detach () + self.window_state.detach () + + self.gtk_window.hide () + + state.save () + + self.gtk_window.destroy () + + def find_element (self, name): + + col = self.element_model.COL_ELEMENT + + for row in self.element_model: + element = row[col] + if element.name == name: + return element + else: + raise KeyError ("no such element with name %r" % (name,)) + + def set_filter_func (self, func): + + if func == self.filter_func: + self.logger.debug ("ignoring attempt to set same filter func again") + return + + if self.active is not None: + self.default_active_name = self.active.name + + self.logger.debug ("changing filter func, refiltering element model") + self.filter_func = func + self.element_filter.refilter () + self.update_element_count () + + if self.active is None and self.default_active_name is not None: + try: + self.active = self.find_element (name = self.default_active_name) + except KeyError: + pass + else: + self.scroll_to_active () + + def set_busy_cursor (self, setting): + + window = self.gtk_window + + if not window.window: + return + + if setting: + window.window.set_cursor (gtk.gdk.Cursor (gtk.gdk.WATCH)) + else: + window.window.set_cursor (None) + + def show_filter (self): + + self.logger.debug ("showing filter") + self.widgets.filter_vbox.show () + self.filter_manager.pop_default_active () + + def hide_filter (self): + + self.logger.debug ("hiding filter") + self.widgets.filter_vbox.hide () + self.filter_manager.push_default_active () + + def handle_window_realize (self, window): + + self.set_busy_cursor (not window.props.sensitive) + + def handle_show_filter_action_toggled (self, toggle_action): + + visible = toggle_action.props.active + + if visible: + self.show_filter () + else: + self.hide_filter () + + def handle_filter_close_button_clicked (self, button): + + self.actions.show_filter.props.active = False + + def handle_show_about_action_activate (self, action): + + import GstInspector + + dialog = self.app.widget_factory.make_one ("about_dialog") + + # This workaround for the issue in GTK+ bug #345822 ensures that the + # dialog displays the program name as set in the glade file when we run + # against GTK+ 2.12. + + # TODO: Once we depend on a recent enough pygobject, use + # gobject.set_application_name in main instead. The about dialog will + # pick it up from there. + try: + dialog.props.program_name = _("GStreamer Inspector") + except AttributeError: + pass + + dialog.props.version = GstInspector.version + + dialog.run () + + dialog.destroy () + + def handle_window_delete_event (self, window, event): + + self.app.close_window (self) + + def handle_close_window_action_activate (self, action): + + self.app.close_window (self) + + def handle_load_started (self): + + """Data.Consumer method.""" + + if self.active is not None: + self.default_active_name = self.active.name + + self.filtered_row_insertions = 0 + self.element_filter.connect ("row-inserted", self.handle_filtered_row_inserted) + + if self.default_focus_widget is not None: + focus_widget = self.gtk_window.get_focus () + if focus_widget: + self.default_focus_widget = focus_widget + + self.gtk_window.props.sensitive = False + self.set_busy_cursor (True) + self.update_element_count () + + self.filter_manager.handle_load_started () + + if self.element_view.props.model: + self.column_manager.disable_sort () + + def handle_filtered_row_inserted (self, model, tree_path, tree_iter): + + self.filtered_row_insertions += 1 + if self.filtered_row_insertions % 11 == 0: + self.update_element_count () + + def handle_load_finished (self): + + """Data.Consumer method.""" + + self.element_filter.disconnect_by_func (self.handle_filtered_row_inserted) + del self.filtered_row_insertions + + self.set_busy_cursor (False) + self.gtk_window.props.sensitive = True + + self.post_attach () + + def update (self, element): + + if self.idle_update_source is not None: + # Already queued up. + return + + # The real update is deferred, we retrieve the selected row + # when it is really executed. + + def real_update (): + + element = self.active + if element is not None: + self.page_manager.update (element) + self.actions.show_documentation.entry = element.hierarchy[-1] + self.app.state.element = element.name + self.app.state.save () + + self.idle_update_source = None + return False + + self.idle_update_source = gobject.timeout_add (50, real_update) + + def update_element_count (self): + + """Update the label text to reflect the number of currently displayed + elements in the list view.""" + + model = self.element_filter + count = model.iter_n_children (None) + text = ngettext ("%i Element shown", "%i Elements shown", count) + self.element_count_label.props.label = text % (count,) + + def handle_element_view_selection_changed (self, tree_selection): + + element = self.active + if element is None: + # Unselected. The filter has changed and the element got filtered + # out. We do not update in this case; doing so would be too odd to + # the user. Things reach a consistent state again if the user + # picks another element or changes the filter again to make the + # previously selected one reappear (of which we stored the name in + # self.default_active_name). + return + else: + self.update (element) + + def select_first_row (self): + + view = self.element_view + # Get the derived model, which accounts for filtering and sorting: + model = view.props.model + if model is None: + return + tree_iter = model.get_iter_first () + if tree_iter is None: + return + view.scroll_to_cell (model.get_path (tree_iter), view.get_column (0)) + selection = view.get_selection () + selection.select_iter (tree_iter) + # Above call does not emit the signal, need to call handler manually: + self.handle_element_view_selection_changed (selection) + + def scroll_to_active (self): + + view = self.element_view + model = view.props.model + col = self.element_model.COL_ELEMENT + + if model is None: + return + + element = self.active + if element is None: + return + + for row in model: + if row[col] == element: + tree_iter = model.get_iter (row.path) + view.scroll_to_cell (model.get_path (tree_iter), + view.get_column (0), + True, 0.5, 0.0) + +from gettext import gettext as _ diff --git a/po/POTFILES.in b/po/POTFILES.in index a367305..31f76c7 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,6 +1,15 @@ [encoding: UTF-8] GstInspector/Data.py -GstInspector/GUI.py +GstInspector/GUI/__init__.py +GstInspector/GUI/actions.py +GstInspector/GUI/app.py +GstInspector/GUI/columns.py +GstInspector/GUI/filters.py +GstInspector/GUI/models.py +GstInspector/GUI/pages.py +GstInspector/GUI/state.py +GstInspector/GUI/utils.py +GstInspector/GUI/window.py GstInspector/__init__.py GstInspector/main.py GstInspector/utils.py @@ -337,7 +337,8 @@ for size in ("16x16", "22x22", "24x24", "32x32", "48x48", "scalable",): setup (cmdclass = cmdclass, - packages = ["GstInspector"], + packages = ["GstInspector", + "GstInspector.GUI"], scripts = ["gst-inspector"], data_files = [("share/gst-inspector", ["data/gst-inspector.glade", "data/gst-inspector.ui"],)] + icons, |