summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRené Stadler <mail@renestadler.de>2008-11-11 23:52:57 +0200
committerRené Stadler <mail@renestadler.de>2008-11-15 16:23:51 +0200
commit4e8178dce43fc3627e3729c4de89a22010c83121 (patch)
treec7e558826dca62b297fbbc564bdf0e5f3a45e6d3
parent36c555e6cc497fd85bd8577741d7af5ecdc14f56 (diff)
Split giant GUI module into submodules
-rw-r--r--GstInspector/GUI.py3486
-rw-r--r--GstInspector/GUI/__init__.py92
-rw-r--r--GstInspector/GUI/actions.py136
-rw-r--r--GstInspector/GUI/app.py165
-rw-r--r--GstInspector/GUI/columns.py421
-rw-r--r--GstInspector/GUI/filters.py740
-rw-r--r--GstInspector/GUI/models.py232
-rw-r--r--GstInspector/GUI/pages.py905
-rw-r--r--GstInspector/GUI/state.py291
-rw-r--r--GstInspector/GUI/utils.py168
-rw-r--r--GstInspector/GUI/window.py607
-rw-r--r--po/POTFILES.in11
-rwxr-xr-xsetup.py3
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
diff --git a/setup.py b/setup.py
index f6e4da3..606edb4 100755
--- a/setup.py
+++ b/setup.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,