From dee6412f6beae82952ed0a07fc2bfdfb0cbb3e79 Mon Sep 17 00:00:00 2001 From: Vincent Vinet Date: Thu, 19 Nov 2009 16:33:26 +0000 Subject: refac property editing in the creator --- (limited to 'tutorius/propwidgets.py') diff --git a/tutorius/propwidgets.py b/tutorius/propwidgets.py new file mode 100644 index 0000000..7e78ba4 --- /dev/null +++ b/tutorius/propwidgets.py @@ -0,0 +1,489 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" +Property Widgets. + +Allows displaying properties cleanly. +""" +import gtk +import gobject + +from . import gtkutils, overlayer +########################################################################### +# Dialog classes +########################################################################### +class TextInputDialog(gtk.MessageDialog): + def __init__(self, parent, text, field): + gtk.MessageDialog.__init__(self, parent, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, + gtk.BUTTONS_OK, + None) + self.set_markup(text) + self.entry = gtk.Entry() + self.entry.connect("activate", self._dialog_done_cb, gtk.RESPONSE_OK) + hbox = gtk.HBox() + lbl = gtk.Label(field) + hbox.pack_start(lbl, False) + hbox.pack_end(self.entry) + self.vbox.pack_end(hbox, True, True) + self.show_all() + + def pop(self): + self.run() + self.hide() + text = self.entry.get_text() + return text + + def _dialog_done_cb(self, entry, response): + self.response(response) + +class SignalInputDialog(gtk.MessageDialog): + def __init__(self, parent, text, field, addr): + """ + Create a gtk signal selection dialog. + + @param parent: the parent window this dialog should stay over. + @param text: the title of the dialog. + @param field: the field description of the dialog. + @param addr: the widget address from which to fetch signal list. + """ + gtk.MessageDialog.__init__(self, parent, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, + gtk.BUTTONS_OK, + None) + self.set_markup(text) + self.model = gtk.ListStore(str) + widget = gtkutils.find_widget(parent, addr) + for signal_name in gobject.signal_list_names(widget): + self.model.append(row=(signal_name,)) + self.entry = gtk.ComboBox(self.model) + cell = gtk.CellRendererText() + self.entry.pack_start(cell) + self.entry.add_attribute(cell, 'text', 0) + hbox = gtk.HBox() + lbl = gtk.Label(field) + hbox.pack_start(lbl, False) + hbox.pack_end(self.entry) + self.vbox.pack_end(hbox, True, True) + self.show_all() + + def pop(self): + """ + Show the dialog. It will run in it's own loop and return control + to the caller when a signal has been selected. + + @returns: a signal name or None if no signal was selected + """ + self.run() + self.hide() + iter = self.entry.get_active_iter() + if iter: + text = self.model.get_value(iter, 0) + return text + return None + + def _dialog_done_cb(self, entry, response): + self.response(response) + +class WidgetSelector(object): + """ + Allow selecting a widget from within a window without interrupting the + flow of the current call. + + The selector will run on the specified window until either a widget + is selected or abort() gets called. + """ + def __init__(self, window): + super(WidgetSelector, self).__init__() + self.window = window + self._intro_mask = None + self._intro_handle = None + self._select_handle = None + self._prelight = None + + def select(self): + """ + Starts selecting a widget, by grabbing control of the mouse and + highlighting hovered widgets until one is clicked. + @returns: a widget address or None + """ + if not self._intro_mask: + self._prelight = None + self._intro_mask = overlayer.Mask(catch_events=True) + self._select_handle = self._intro_mask.connect_after( + "button-press-event", self._end_introspect) + self._intro_handle = self._intro_mask.connect_after( + "motion-notify-event", self._intro_cb) + self.window._overlayer.put(self._intro_mask, 0, 0) + self.window._overlayer.queue_draw() + + while bool(self._intro_mask) and not gtk.main_iteration(): + pass + + return gtkutils.raddr_lookup(self._prelight) + + def _end_introspect(self, widget, evt): + if evt.type == gtk.gdk.BUTTON_PRESS and self._prelight: + self._intro_mask.catch_events = False + self._intro_mask.disconnect(self._intro_handle) + self._intro_handle = None + self._intro_mask.disconnect(self._select_handle) + self._select_handle = None + self.window._overlayer.remove(self._intro_mask) + self._intro_mask = None + # for some reason, gtk may not redraw after this unless told to. + self.window.queue_draw() + + def _intro_cb(self, widget, evt): + """ + Callback for capture of widget events, when in introspect mode. + """ + # widget has focus, let's hilight it + win = gtk.gdk.display_get_default().get_window_at_pointer() + if not win: + return + click_wdg = win[0].get_user_data() + if not click_wdg.is_ancestor(self.window._overlayer): + # as popups are not (yet) supported, it would break + # badly if we were to play with a widget not in the + # hierarchy. + return + for hole in self._intro_mask.pass_thru: + self._intro_mask.mask(hole) + self._intro_mask.unmask(click_wdg) + self._prelight = click_wdg + + self.window.queue_draw() + + def abort(self): + """ + Ends the selection. The control will return to the select() caller + with a return value of None, as selection was aborted. + """ + self._intro_mask.catch_events = False + self._intro_mask.disconnect(self._intro_handle) + self._intro_handle = None + self._intro_mask.disconnect(self._select_handle) + self._select_handle = None + self.window._overlayer.remove(self._intro_mask) + self._intro_mask = None + self._prelight = None + +########################################################################### +# Property Widget Classes +########################################################################### +class PropWidget(object): + """ + Base Class for property editing widgets. + Subclasses should implement create_widget, run_dialog and refresh_widget + """ + def __init__(self, parent, edit_object, prop_name, changed_callback=None): + """Constructor + @param parent parent widget + @param edit_object TPropContainer being edited + @param prop_name name of property being edited + @param changed_callback optional callable to call on value changes + """ + self._parent = parent + self._edit_object = edit_object + self._propname = prop_name + self._widget = None + self._changed_cb = changed_callback + + ############################################################ + # Begin Properties + ############################################################ + def set_objprop(self, value): + """Setter for object property value""" + setattr(self._edit_object, self._propname, value) + def get_objprop(self): + """Getter for object property value""" + return getattr(self._edit_object, self._propname) + def _get_widget(self): + """Getter for widget. Creates the widget if necessary""" + if self._widget is None: + self._widget = self.create_widget(self.obj_prop) + return self._widget + def _get_prop_class(self): + """Getter for property type""" + return getattr(type(self._edit_object), self._propname) + def _get_parent(self): + """Getter for parent""" + return self._parent + + obj_prop = property(get_objprop, set_objprop) + widget = property(_get_widget) + prop_class = property(_get_prop_class) + parent = property(_get_parent) + + ############################################################ + # End Properties + ############################################################ + + def notify(self): + """Notify a calling object that the property was changed""" + if self._changed_cb: + self._changed_cb() + + ############################################################ + # Public Interface -- Redefine those function in subclasses + ############################################################ + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + widget = gtk.Entry() + widget.set_text(str(init_value or "")) + return widget + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + raise NotImplementedError() + + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + pass + +class StringPropWidget(PropWidget): + """ + Allows editing a str property + """ + @classmethod + def _extract_value(cls, widget): + """ + Class Method + extracts the value from the widget + """ + buf = widget.get_buffer() + return cls._from_text( + buf.get_text(buf.get_start_iter(), buf.get_end_iter()) + ) + + @classmethod + def _from_text(cls, text): + """ + Class Method + transforms the text value into the correct type if required + """ + return text + + def _text_changed(self, widget, evt): + """callback for text change event in the edit box""" + self.obj_prop = self._extract_value(widget) + self.notify() + + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + propwdg = gtk.TextView() + propwdg.get_buffer().set_text(init_value or "") + propwdg.connect_after("focus-out-event", \ + self._text_changed) + + return propwdg + + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + self.widget.get_buffer().set_text(str(self.obj_prop)) #unicode() ? + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + dlg = TextInputDialog(parent, + text="Mandatory property", + field=propname) + setattr(obj_prop, propname, cls._from_text(dlg.pop())) + +class IntPropWidget(StringPropWidget): + """ + Allows editing an int property with boundaries + """ + @classmethod + def _extract_value(cls, widget): + """ + Class Method + extracts the value from the widget + """ + return widget.get_value_as_int() + + @classmethod + def _from_text(cls, text): + """ + Class Method + transforms the text value into the correct type if required + """ + return int(text) + + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + prop = self.prop_class + adjustment = gtk.Adjustment(value=self.obj_prop, + lower=prop.lower_limit.limit, + upper=prop.upper_limit.limit, + step_incr=1) + propwdg = gtk.SpinButton(adjustment=adjustment) + propwdg.connect_after("focus-out-event", \ + self._text_changed) + +class FloatPropWidget(StringPropWidget): + """Allows editing a float property""" + @classmethod + def _from_text(cls, text): + """ + Class Method + transforms the text value into the correct type if required + """ + return float(text) + +class UAMPropWidget(PropWidget): + """Allows editing an UAM property with a widget chooser""" + def _show_uam_chooser(self, widget): + """show the UAM chooser""" + selector = WidgetSelector(self.parent) + self.obj_prop = selector.select() + self.notify() + + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + propwdg = gtk.Button(self.obj_prop) + propwdg.connect_after("clicked", self._show_uam_chooser) + return propwdg + + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + self.widget.set_label(self.obj_prop) + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + selector = WidgetSelector(parent) + value = selector.select() + setattr(obj_prop, propname, selector.select()) + +class EventTypePropWidget(PropWidget): + """Allows editing an EventType property""" + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + self.widget.set_text(str(self.obj_prop)) + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + try: + dlg = SignalInputDialog(parent, + text="Mandatory property", + field=propname, + addr=obj_prop.object_id) + setattr(obj_prop, propname, dlg.pop()) + except AttributeError: + return + +class IntArrayPropWidget(PropWidget): + """Allows editing an array of ints property""" + def _item_changed(self, widget, evt, idx): + """callback for text changed in one of the entries""" + try: + #Save props as tuples so that they can be hashed + attr = list(self.obj_prop) + attr[idx] = int(widget.get_text()) + self.obj_prop = tuple(attr) + except ValueError: + widget.set_text(str(self.obj_prop[idx])) + self.notify() + + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + value = self.obj_prop + propwdg = gtk.HBox() + for i in xrange(len(value)): + entry = gtk.Entry() + entry.set_text(str(value[i])) + propwdg.pack_start(entry) + entry.connect_after("focus-out-event", \ + self._item_changed, i) + return propwdg + + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + children = self.widget.get_children() + value = self.obj_prop + for i in xrange(len(value)): + children[i].set_text(str(value[i])) + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + pass -- cgit v0.9.1