diff options
author | Simon Poirier <simpoir@gmail.com> | 2009-12-16 19:23:03 (GMT) |
---|---|---|
committer | Simon Poirier <simpoir@gmail.com> | 2009-12-16 19:23:03 (GMT) |
commit | 6d1cf01b2b20a8deabb6bde44777e9823b0201ce (patch) | |
tree | d076d5f5001504c7a42b4ce37cfb6f545afd552c | |
parent | 3e2e2d774322511d4d18457299ac8205bd9aed87 (diff) |
screen clipping tool
-rw-r--r-- | addons/screenclipper.py | 124 | ||||
-rw-r--r-- | data/icons/screenclip.svg | 26 | ||||
-rw-r--r-- | tutorius/addon.py | 13 | ||||
-rw-r--r-- | tutorius/properties.py | 23 | ||||
-rw-r--r-- | tutorius/propwidgets.py | 110 |
5 files changed, 292 insertions, 4 deletions
diff --git a/addons/screenclipper.py b/addons/screenclipper.py new file mode 100644 index 0000000..7ad6808 --- /dev/null +++ b/addons/screenclipper.py @@ -0,0 +1,124 @@ +# 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 +from sugar.tutorius.actions import Action, DragWrapper +from sugar.tutorius.properties import TScreenClipProperty, \ + TArrayProperty +import gtk + +class ScreenClip(Action): + # Create the position as an array of fixed-size 2 + position = TArrayProperty((0,0), 2, 2) + # Do the same for the tail position + clip_image = TScreenClipProperty("") + + def __init__(self, position=None, + clip_image=None, + **kwargs): + """ + Shows a dialog with a given text, at the given position on the screen. + + @param position A list of the form [x, y] + @param clip_image the screen clip image path + @param clip_rect rectangle of the clip image + """ + Action.__init__(self, **kwargs) + + if position: + self.position = position + if clip_image: + self.clip_image = clip_image + + self.overlay = None + self._bubble = None + + def do(self, overlayer=None, **kwargs): + """ + Show the dialog + """ + if overlayer is None: + raise TypeError("Missing overlayer argument") + + self.overlay = overlayer + + if not self._bubble: + # Normal gtk widgets use the parent window to draw themselves. + # For normal layouts, this is good, but as we are using the layout + # for stacking widgets, the rendering order is sometimes wrong. + # Thus, by adding the Image widget in a visible EventBox, we ensure + # the Image is drawn in its own window, and stacking is correct. + x, y = self.position + self._bubble = gtk.EventBox() + self._bubble.set_visible_window(True) + image = gtk.Image() + image.set_from_file(self.clip_image) + self._bubble.add(image) + self._bubble.show_all() + self.overlay.put(self._bubble, x, y) + self.overlay.queue_draw() + + def undo(self): + """ + Destroy the dialog + """ + if self._bubble: + self.overlay.remove(self._bubble) + self._bubble.destroy() + self._bubble = None + + def enter_editmode(self, overlayer=None, *args, **kwargs): + """ + Enters edit mode. The action should display itself in some way, + without affecting the currently running application. + """ + if overlayer is None: + raise TypeError("Missing overlayer argument") + + self.overlay = overlayer + assert not self._drag, "bubble action set to editmode twice" + x, y = self.position + if self.clip_image: + self._bubble = gtk.EventBox() + self._bubble.set_visible_window(True) + image = gtk.Image() + image.set_from_file(self.clip_image) + self._bubble.add(image) + self._bubble.show_all() + self.overlay.put(self._bubble, x, y) + + self._drag = DragWrapper(self._bubble, self.position, + update_action_cb=self.update_property, + draggable=True) + + def exit_editmode(self, *args): + if self._drag: + x, y = self._drag.position + self.position = (int(x), int(y)) + + self._drag.draggable = False + self._drag = None + if self._bubble: + self.overlay.remove(self._bubble) + self._bubble = None + self.overlay = None + +__action__ = { + "name" : ScreenClip.__name__, + "display_name" : "Screen Capture Clip", + "icon" : "screenclip", + "class" : ScreenClip, + "mandatory_props" : [] +} + diff --git a/data/icons/screenclip.svg b/data/icons/screenclip.svg new file mode 100644 index 0000000..67e6680 --- /dev/null +++ b/data/icons/screenclip.svg @@ -0,0 +1,26 @@ +<?xml version="1.0" ?><!-- Created with Inkscape (http://www.inkscape.org/) --><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [ + <!ENTITY stroke_color "#000000"> + <!ENTITY fill_color "#ffffff"> +]><svg height="51" id="svg2" inkscape:version="0.47 r22583" sodipodi:docname="New document 1" version="1.1" width="51" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"> + <defs id="defs4"> + <inkscape:perspective id="perspective10" inkscape:persp3d-origin="372.04724 : 350.78739 : 1" inkscape:vp_x="0 : 526.18109 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="744.09448 : 526.18109 : 1" sodipodi:type="inkscape:persp3d"/> + </defs> + <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="213.0625" inkscape:cy="36.59674" inkscape:document-units="px" inkscape:guide-bbox="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="628" inkscape:window-maximized="0" inkscape:window-width="1093" inkscape:window-x="20" inkscape:window-y="20" inkscape:zoom="0.96" pagecolor="#ffffff" showgrid="false" showguides="true"/> + <metadata id="metadata7"> + <rdf:RDF> + <cc:Work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + <dc:title/> + </cc:Work> + </rdf:RDF> + </metadata> + <g id="layer1" inkscape:groupmode="layer" inkscape:label="Layer 1" transform="translate(0,-1001.3622)"> + <rect height="37.728832" id="rect2816" style="color:#000000;fill:none;stroke:&stroke_color;;stroke-width:3.72792578;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:7.45585251, 7.45585251;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" width="32.628826" x="12.63153" y="1006.964"/> + <g id="g3609" transform="matrix(0,-1.2919257,-1.2002032,0,1263.209,1094.1684)"> + <path d="m 38.46875,29.0625 2.125,3.625 c 0,0 -4.09196,3.6458 -4,6.4375 0.09196,2.7917 2.387558,7.1156 5.53125,6.9375 3.640064,-1.181 0.537287,-9.13245 1.0625,-12.40625 1.990315,3.0125 5.834605,10.3756 9.75,9.5 1.098405,-0.9127 2.29467,-4.6001 0.34375,-7.6875 C 51.255431,32.26285 45.84375,32 45.84375,32 L 46.5625,13.90625 43.34375,11.5 l -1.1875,19.4375 -3.6875,-8.1875 0,6.3125 z" id="path3607" style="color:#000000;fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.9485935;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" transform="translate(0,1001.3622)"/> + <path d="m 49.54564,1037.9255 c 0.369232,0.5291 0.738464,1.0581 1.107696,1.5872" id="path3592" style="color:#000000;fill:&fill_color;;fill-opacity:1;fill-rule:nonzero;stroke:&stroke_color;;stroke-width:2.288059;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> + <path d="m 40.618302,1040.3023 c 0.01806,0.1307 -0.09311,0.2591 -0.07504,0.3898 0.02955,0.2138 0.214643,0.01 0.04284,0.3101 -0.02316,0.041 -0.10904,0.034 -0.117883,0.08 -0.01769,0.092 0.178412,0.1384 0.160722,0.2303 -0.0088,0.046 -0.09472,0.039 -0.117884,0.08 -0.03276,0.058 -0.02501,0.1299 -0.03752,0.1949 -0.01251,0.065 -0.04655,0.1295 -0.03752,0.1949 0.0064,0.046 0.05357,0.077 0.08036,0.1151 0.01428,0.1034 0.04174,0.2057 0.04284,0.3101 0.004,0.3766 -0.266515,0.1154 -0.07505,0.3898" id="path3594" style="color:#000000;fill:&fill_color;;fill-opacity:1;fill-rule:nonzero;stroke:&stroke_color;;stroke-width:1.9485935;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/tutorius/addon.py b/tutorius/addon.py index ca729ae..1fd5143 100644 --- a/tutorius/addon.py +++ b/tutorius/addon.py @@ -78,9 +78,16 @@ def create(name, *args, **kwargs): comp_metadata = _cache[name] try: return comp_metadata['class'](*args, **kwargs) - except: - logging.debug("Could not instantiate %s with parameters %s, %s"%(comp_metadata['name'],str(args), str(kwargs))) - return None + except Exception, e: + LOGGER.error("Could not instantiate %s with parameters %s, %s"%\ + (comp_metadata['name'],str(args), str(kwargs))) + + # fetch frame information to complement exception with traceback + type, value, tb = sys.exc_info() + formatted_tb = traceback.format_tb(tb) + LOGGER.error('Error loadin tutorius add-on named [%s]:\n%s\n%s' % \ + (name, '\n'.join(formatted_tb), str(e))) + return None except KeyError: logging.debug("Addon not found for class '%s'", name) return None diff --git a/tutorius/properties.py b/tutorius/properties.py index b363a2a..1905117 100644 --- a/tutorius/properties.py +++ b/tutorius/properties.py @@ -37,7 +37,8 @@ from .propwidgets import PropWidget, \ IntPropWidget, \ FloatPropWidget, \ IntArrayPropWidget, \ - ResourcePropWidget + ResourcePropWidget, \ + ScreenClipPropWidget import logging LOGGER = logging.getLogger("properties") @@ -390,6 +391,26 @@ class TResourceProperty(TutoriusProperty): self.default = self.validate("") +class TScreenClipProperty(TResourceProperty): + """ + Represents an image resource from a screen capture + + When the system encounters a resource, it knows that it refers to a file in + the resource folder and that it should translate this resource name to an + absolute file name before it is executed. + """ + + widget_class = ScreenClipPropWidget + + def __init__(self, *args, **kwargs): + """ + Creates a new resource pointing to an existing resource. + + @param resource_name The file name of the resource (should be only the + file name itself, no directory information) + """ + super(TScreenClipProperty, self).__init__(*args, **kwargs) + class TEnumProperty(TutoriusProperty): """ Represents a value in a given enumeration. This means that the value will diff --git a/tutorius/propwidgets.py b/tutorius/propwidgets.py index 3debf98..0e6c200 100644 --- a/tutorius/propwidgets.py +++ b/tutorius/propwidgets.py @@ -24,6 +24,8 @@ 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") @@ -570,3 +572,111 @@ class ResourcePropWidget(PropWidget): """ 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') |