# 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 jarabe.journal.objectchooser import ObjectChooser from sugar.datastore.datastore import DSObject from sugar import mime import uuid import tempfile import os import logging LOGGER = logging.getLogger("sugar.tutorius.propwidgets") 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) LOGGER.debug("SignalInputDialog :: Got signal name %s", text) 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, probe_mgr=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 @type probe_mgr: ProbeMgr instance or None @param probe_mgr: the probe manager to use to inspect activities """ self._parent = parent self._edit_object = edit_object self._propname = prop_name self._widget = None self._changed_cb = changed_callback self._probe_mgr = probe_mgr ############################################################ # 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""" evt = self._probe_mgr.create_event('FetchWidget') self.obj_prop = evt.widaddr 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 """ if self.obj_prop: self.widget.set_label(self.obj_prop) else: self.widget.set_label("") @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 class ResourcePropWidget(PropWidget): """Allows adding and changing tutorial resources.""" def _chooser_response_cb(self, chooser, response_id, chooser_id, widget): """ Callback for receiving file choices. """ if response_id == gtk.RESPONSE_ACCEPT: object_id = chooser.get_selected_object_id() jobject = DSObject(object_id=object_id) from . import creator res_path = str(jobject.file_path) creator_obj = creator.default_creator() resource_id = creator_obj.set_resource(self.obj_prop, res_path) self.widget.set_label(self.obj_prop) jobject.destroy() self.obj_prop = resource_id self.notify() chooser.destroy() del chooser def _show_file_chooser(self, widget): """ Select a resource and add it through the creator. This is expected to run in the same process, alongside the creator. """ chooser_id = uuid.uuid4().hex chooser = ObjectChooser(self._parent, what_filter=mime.GENERIC_TYPE_IMAGE) chooser.connect('response', self._chooser_response_cb, chooser_id, widget) chooser.show() 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(init_value) propwdg.connect_after("clicked", self._show_file_chooser) return propwdg def refresh_widget(self): """ Force the widget to update it's value in case the property has changed """ if self.obj_prop: self.widget.set_label(self.obj_prop) else: self.widget.set_label("") @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 RuntimeError('Cannot select a default resource') class ScreenClipPropWidget(PropWidget): """Allows adding and changing tutorial resources.""" def _on_drag_end(self, widget, event, pixbuf): from . import creator widget.destroy() end_x, end_y = event.get_coords() width = abs(end_x - self.start_x) height = abs(end_y - self.start_y) x_off = min(self.start_x, end_x) y_off = min(self.start_y, end_y) cropped = pixbuf.subpixbuf(x_off, y_off, width, height) tmp_name = tempfile.mktemp(suffix='.png') try: cropped.save(tmp_name, 'png') creator_obj = creator.default_creator() resource_id = creator_obj.set_resource(self.obj_prop, tmp_name) self.obj_prop = resource_id finally: os.unlink(tmp_name) self.notify() def _on_drag_start(self, widget, event, pixbuf): widget.connect('button-release-event', self._on_drag_end, pixbuf) widget.connect('motion-notify-event', self._on_drag_move, pixbuf) self.start_x, self.start_y = event.get_coords() def _on_drag_move(self, widget, event, pixbuf): if gtk.gdk.events_pending(): return end_x, end_y = event.get_coords() width = abs(end_x - self.start_x) height = abs(end_y - self.start_y) x_off = min(self.start_x, end_x) y_off = min(self.start_y, end_y) ctx = widget.window.cairo_create() ctx.set_source_pixbuf(pixbuf, 0, 0) ctx.paint() ctx.set_source_rgb(0, 0, 0) ctx.rectangle(x_off, y_off, width, height) ctx.stroke() def _get_capture(self, widget): """ Select a resource and add it through the creator. This is expected to run in the same process, alongside the creator. """ # take screen capture root = gtk.gdk.get_default_root_window() width, height = root.get_size() pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, width, height) pixbuf.get_from_drawable(src=root, cmap=gtk.gdk.colormap_get_system(), src_x=0, src_y=0, dest_x=0, dest_y=0, width=width, height=height) win = gtk.Window() image = gtk.Image() image.set_from_pixbuf(pixbuf) win.add(image) win.show_all() win.set_app_paintable(True) win.fullscreen() win.present() win.add_events(gtk.gdk.BUTTON_PRESS_MASK | \ gtk.gdk.BUTTON_RELEASE_MASK | \ gtk.gdk.POINTER_MOTION_MASK) win.connect('button-press-event', self._on_drag_start, pixbuf) 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("Clip Screen") propwdg.connect_after("clicked", self._get_capture) return propwdg def refresh_widget(self): """ Force the widget to update it's value in case the property has changed """ # Nothing to refresh pass @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 """ # TODO We're assuming all reasource creation is done from the creator # and not from the probe since there is a requirement to know the guid # to add resources. But for this resource type, this could technically # be done in the probe. raise RuntimeError('Cannot select a default resource')