From 44ce7d95c33fc76078d88fc43e9c4453ca6522e9 Mon Sep 17 00:00:00 2001 From: Gonzalo Odiard Date: Tue, 03 May 2011 18:29:08 +0000 Subject: Implementation of Stamp functionality The user can select a part of the image to use it like a stamp. When the stamp is selected, can change the size, and use it to draw. Signed-of: manuel quiƱones Reviewed-by: Gonzalo Odiard --- diff --git a/Area.py b/Area.py index 2e979a8..90bec38 100644 --- a/Area.py +++ b/Area.py @@ -131,6 +131,7 @@ class Area(gtk.DrawingArea): ## with the following keys: ## - 'name' : a string ## - 'line size' : a integer + ## - 'stamp size' : a integer ## - 'fill color' : a gtk.gdk.Color object ## - 'stroke color' : a gtk.gdk.Color object ## - 'line shape' : a string - 'circle' or 'square', for now @@ -140,6 +141,7 @@ class Area(gtk.DrawingArea): self.tool = { 'name': 'pencil', 'line size': 4, + 'stamp size': 20, 'fill color': None, 'stroke color': None, 'line shape': 'circle', @@ -286,14 +288,24 @@ class Area(gtk.DrawingArea): Show the shape of the tool selected for pencil, brush, rainbow and eraser """ - if self.tool['name'] in ['pencil', 'eraser', 'brush', 'rainbow']: + if self.tool['name'] in ['pencil', 'eraser', 'brush', 'rainbow', + 'stamp']: if not self.drawing: - size = self.tool['line size'] - if self.tool['line shape'] == 'circle': + # draw stamp border in widget.window + if self.tool['name'] == 'stamp': + wr, hr = self.stamp_dimentions + widget.window.draw_rectangle(self.gc_brush, False, + self.x_cursor - wr / 2, self.y_cursor - hr / 2, + wr, hr) + + # draw shape of the brush, square or circle + elif self.tool['line shape'] == 'circle': + size = self.tool['line size'] widget.window.draw_arc(self.gc_brush, False, self.x_cursor - size / 2, self.y_cursor - size / 2, size, size, 0, 360 * 64) else: + size = self.tool['line size'] widget.window.draw_rectangle(self.gc_brush, False, self.x_cursor - size / 2, self.y_cursor - size / 2, size, size) @@ -355,6 +367,12 @@ class Area(gtk.DrawingArea): self.tool['line shape']) self.last = coords self.drawing = True + elif self.tool['name'] == 'stamp': + self.last = [] + self.d.stamp(widget, coords, self.last, + self.tool['stamp size']) + self.last = coords + self.drawing = True elif self.tool['name'] == 'rainbow': self.last = [] self.d.rainbow(widget, coords, self.last, @@ -412,7 +430,7 @@ class Area(gtk.DrawingArea): if state & gtk.gdk.BUTTON1_MASK and self.pixmap != None: if self.tool['name'] == 'pencil': self.d.brush(widget, coords, self.last, - self.tool['line size'], 'circle') + self.tool['line size'], self.tool['line shape']) self.last = coords elif self.tool['name'] == 'eraser': @@ -425,6 +443,11 @@ class Area(gtk.DrawingArea): self.tool['line size'], self.tool['line shape']) self.last = coords + elif self.tool['name'] == 'stamp': + self.d.stamp(widget, coords, self.last, + self.tool['stamp size']) + self.last = coords + elif self.tool['name'] == 'rainbow': self.d.rainbow(widget, coords, self.last, self.rainbow_counter, self.tool['line size'], @@ -496,7 +519,8 @@ class Area(gtk.DrawingArea): self.configure_line(self.tool['line size']) self.d.heart(widget, coords, True, self.tool['fill']) else: - if self.tool['name'] in ['brush', 'eraser', 'rainbow', 'pencil']: + if self.tool['name'] in ['brush', 'eraser', 'rainbow', 'pencil', + 'stamp']: widget.queue_draw() if self.tool['name'] == 'marquee-rectangular' and self.selmove: size = self.pixmap_sel.get_size() @@ -616,7 +640,8 @@ class Area(gtk.DrawingArea): self.d.heart(widget, coords, False, self.tool['fill']) self.enableUndo(widget) - if self.tool['name'] in ['brush', 'eraser', 'rainbow', 'pencil']: + if self.tool['name'] in ['brush', 'eraser', 'rainbow', 'pencil', + 'stamp']: self.last = [] widget.queue_draw() self.enableUndo(widget) @@ -624,19 +649,65 @@ class Area(gtk.DrawingArea): self.desenha = False def mouseleave(self, widget, event): - if self.tool['name'] in ['pencil', 'eraser', 'brush', 'rainbow']: + if self.tool['name'] in ['pencil', 'eraser', 'brush', 'rainbow', + 'stamp']: self.drawing = True size = self.tool['line size'] widget.queue_draw_area(self.x_cursor - size, self.y_cursor - size, size * 2, size * 2) def mouseenter(self, widget, event): - if self.tool['name'] in ['pencil', 'eraser', 'brush', 'rainbow']: + if self.tool['name'] in ['pencil', 'eraser', 'brush', 'rainbow', + 'stamp']: self.drawing = False size = self.tool['line size'] widget.queue_draw_area(self.x_cursor - size, self.y_cursor - size, size * 2, size * 2) + def setup_stamp(self): + """Prepare for stamping from the selected area. + + @param self -- the Area object (GtkDrawingArea) + """ + logging.debug('Area.setup_stamp(self)') + + if self.is_selected(): + # Change stamp, get it from selection: + width, height = self.pixmap_sel.get_size() + self.pixbuf_stamp = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, + 8, width, height) + self.pixbuf_stamp.get_from_drawable(self.pixmap_sel, + gtk.gdk.colormap_get_system(), 0, 0, 0, 0, width, height) + self.stamp_size = 0 + # Set white color as transparent: + stamp_alpha = self.pixbuf_stamp.add_alpha(True, 255, 255, 255) + self.pixbuf_stamp = stamp_alpha + + return self.resize_stamp(self.tool['stamp size']) + + def resize_stamp(self, stamp_size): + """Change stamping pixbuffer from the given size. + + @param self -- the Area object (GtkDrawingArea) + @param stamp_size -- the stamp will be inscripted in this size + """ + + # Area.setup_stamp needs to be called first: + assert self.pixbuf_stamp + + self.stamp_size = stamp_size + w = self.pixbuf_stamp.get_width() + h = self.pixbuf_stamp.get_height() + if w >= h: + wr, hr = stamp_size, int(stamp_size * h * 1.0 / w) + else: + wr, hr = int(stamp_size * w * 1.0 / h), stamp_size + self.stamp_dimentions = wr, hr + self.resized_stamp = self.pixbuf_stamp.scale_simple(wr, hr, + gtk.gdk.INTERP_HYPER) + + return self.resized_stamp + def undo(self): """Undo the last drawing change. @@ -1242,7 +1313,8 @@ class Area(gtk.DrawingArea): cursors = {'pencil': 'pencil', 'brush': 'paintbrush', 'eraser': 'eraser', - 'bucket': 'paint-bucket'} + 'bucket': 'paint-bucket', + 'stamp': 'pencil'} display = gtk.gdk.display_get_default() if self.tool['name'] in cursors: @@ -1329,8 +1401,10 @@ class Area(gtk.DrawingArea): self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.CROSS)) widget.queue_draw() + # TODO: unused method? def change_line_size(self, delta): - if self.tool['name'] in ['pencil', 'eraser', 'brush', 'rainbow']: + if self.tool['name'] in ['pencil', 'eraser', 'brush', 'rainbow', + 'stamp']: size = self.tool['line size'] + delta if size < 1: size = 1 diff --git a/Desenho.py b/Desenho.py index 4f8f196..1d46d4a 100644 --- a/Desenho.py +++ b/Desenho.py @@ -128,6 +128,28 @@ class Desenho: widget.desenha = False self._trace(widget, widget.gc_brush, coords, last, size, shape) + def stamp(self, widget, coords, last, stamp_size=20): + """Paint with stamp. + + @param self -- Desenho.Desenho instance + @param last -- last of oldx + @param widget -- Area object (GtkDrawingArea) + @param coords -- Two value tuple + @param stamp_size -- integer (default 20) + + """ + widget.desenha = False + gc = widget.gc_brush + + width = widget.resized_stamp.get_width() + height = widget.resized_stamp.get_height() + dx = coords[0] - width / 2 + dy = coords[1] - height / 2 + widget.pixmap.draw_pixbuf(gc, widget.resized_stamp, + 0, 0, dx, dy, width, height) + + widget.queue_draw() + def rainbow(self, widget, coords, last, color, size=5, shape='circle'): """Paint with rainbow. @@ -161,7 +183,7 @@ class Desenho: self._trace(widget, widget.gc_rainbow, coords, last, size, shape) def _trace(self, widget, gc, coords, last, size, shape): - if(shape == 'circle'): + if shape == 'circle': widget.pixmap.draw_arc(gc, True, coords[0] - size / 2, coords[1] - size / 2, size, size, 0, 360 * 64) @@ -172,7 +194,7 @@ class Desenho: last[0], last[1], coords[0], coords[1]) gc.set_line_attributes(0, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND) - if(shape == 'square'): + elif shape == 'square': widget.pixmap.draw_rectangle(gc, True, coords[0] - size / 2, coords[1] - size / 2, size, size) if last: @@ -186,7 +208,6 @@ class Desenho: (coords[0] - size / 2, coords[1] + size / 2), (last[0] - size / 2, last[1] + size / 2)] widget.pixmap.draw_polygon(gc, True, points) - if last: x = min(coords[0], last[0]) width = max(coords[0], last[0]) - x diff --git a/icons/tool-stamp.svg b/icons/tool-stamp.svg new file mode 100644 index 0000000..2a3ace3 --- /dev/null +++ b/icons/tool-stamp.svg @@ -0,0 +1,122 @@ + +image/svg+xml + + + + + + \ No newline at end of file diff --git a/toolbox.py b/toolbox.py index dafb4e7..00d98b1 100644 --- a/toolbox.py +++ b/toolbox.py @@ -137,6 +137,7 @@ class DrawToolbarBox(ToolbarBox): brush_button = tools_builder._stroke_color.color_button brush_button.set_brush_shape(self._activity.area.tool['line shape']) brush_button.set_brush_size(self._activity.area.tool['line size']) + brush_button.set_stamp_size(self._activity.area.tool['stamp size']) if self._activity.area.tool['stroke color'] is not None: brush_button.set_color(self._activity.area.tool['stroke color']) @@ -228,6 +229,7 @@ class ToolsToolbarBuilder(): _TOOL_BRUSH_NAME = 'brush' _TOOL_ERASER_NAME = 'eraser' _TOOL_BUCKET_NAME = 'bucket' + _TOOL_STAMP_NAME = 'stamp' _TOOL_MARQUEE_RECT_NAME = 'marquee-rectangular' ##The Constructor @@ -267,6 +269,14 @@ class ToolsToolbarBuilder(): activity.tool_group, _('Bucket')) toolbar.insert(self._tool_bucket, -1) + self._tool_stamp = DrawToolButton('tool-stamp', + activity.tool_group, _('Stamp')) + toolbar.insert(self._tool_stamp, -1) + + is_selected = self._activity.area.is_selected() + self._tool_stamp.set_sensitive(is_selected) + self._activity.area.connect('select', self._on_signal_select_cb) + self._tool_marquee_rectangular = \ DrawToolButton('tool-marquee-rectangular', activity.tool_group, _('Select Area')) @@ -286,6 +296,8 @@ class ToolsToolbarBuilder(): self._TOOL_ERASER_NAME) self._tool_bucket.connect('clicked', self.set_tool, self._TOOL_BUCKET_NAME) + self._tool_stamp.connect('clicked', self.set_tool, + self._TOOL_STAMP_NAME) self._tool_marquee_rectangular.connect('clicked', self.set_tool, self._TOOL_MARQUEE_RECT_NAME) @@ -298,6 +310,12 @@ class ToolsToolbarBuilder(): necessary in case this method is used in a connect() @param tool_name --The name of the selected tool """ + if tool_name == 'stamp': + resized_stamp = self._activity.area.setup_stamp() + self._stroke_color.color_button.set_resized_stamp(resized_stamp) + else: + self._stroke_color.color_button.stop_stamping() + self._stroke_color.update_stamping() self.properties['name'] = tool_name self._activity.area.set_tool(self.properties) @@ -308,6 +326,10 @@ class ToolsToolbarBuilder(): self._activity.area.set_stroke_color(new_color) self.properties['stroke color'] = new_color + def _on_signal_select_cb(self, widget, data=None): + is_selected = self._activity.area.is_selected() + self._tool_stamp.set_sensitive(is_selected) + class ButtonFillColor(ColorToolButton): """Class to manage the Fill Color of a Button""" diff --git a/widgets.py b/widgets.py index 832649b..7c23683 100644 --- a/widgets.py +++ b/widgets.py @@ -28,7 +28,9 @@ class BrushButton(_ColorButton): self._palette = None self._accept_drag = True self._brush_size = 2 + self._stamp_size = 20 self._brush_shape = 'circle' + self._resized_stamp = None self._preview = gtk.DrawingArea() self._preview.set_size_request(style.STANDARD_ICON_SIZE, style.STANDARD_ICON_SIZE) @@ -83,6 +85,26 @@ class BrushButton(_ColorButton): self._color = color self._preview.queue_draw() + def get_stamp_size(self): + return self._stamp_size + + def set_stamp_size(self, stamp_size): + self._stamp_size = stamp_size + self._preview.queue_draw() + + stamp_size = gobject.property(type=int, getter=get_stamp_size, + setter=set_stamp_size) + + def set_resized_stamp(self, resized_stamp): + self._resized_stamp = resized_stamp + + def stop_stamping(self): + self._resized_stamp = None + self._preview.queue_draw() + + def is_stamping(self): + return self._resized_stamp != None + def expose(self, widget, event): if self._gc is None: self._setup() @@ -93,16 +115,26 @@ class BrushButton(_ColorButton): True, 0, 0, style.STANDARD_ICON_SIZE, style.STANDARD_ICON_SIZE) self._gc.set_foreground(self._color) - if(self._brush_shape == 'circle'): - self.pixmap.draw_arc(self._gc, True, - center - self._brush_size / 2, - center - self._brush_size / 2, - self._brush_size, self._brush_size, 0, 360 * 64) - if(self._brush_shape == 'square'): - self.pixmap.draw_rectangle(self._gc, True, - center - self._brush_size / 2, - center - self._brush_size / 2, - self._brush_size, self._brush_size) + if self.is_stamping(): + width = self._resized_stamp.get_width() + height = self._resized_stamp.get_height() + dx = center - width / 2 + dy = center - height / 2 + self.pixmap.draw_pixbuf(self._gc, self._resized_stamp, + 0, 0, dx, dy, width, height) + + else: + if self._brush_shape == 'circle': + self.pixmap.draw_arc(self._gc, True, + center - self._brush_size / 2, + center - self._brush_size / 2, + self._brush_size, self._brush_size, 0, 360 * 64) + + elif self._brush_shape == 'square': + self.pixmap.draw_rectangle(self._gc, True, + center - self._brush_size / 2, + center - self._brush_size / 2, + self._brush_size, self._brush_size) area = event.area widget.window.draw_drawable(self._gc, self.pixmap, @@ -157,6 +189,7 @@ class ButtonStrokeColor(gtk.ToolItem): self.add(self.color_button) self.color_button.set_brush_size(2) self.color_button.set_brush_shape('circle') + self.color_button.set_stamp_size(20) # The following is so that the behaviour on the toolbar is correct. self.color_button.set_relief(gtk.RELIEF_NONE) @@ -171,6 +204,8 @@ class ButtonStrokeColor(gtk.ToolItem): self.color_button.connect('can-activate-accel', self.__button_can_activate_accel_cb) + self.create_palette() + def __button_can_activate_accel_cb(self, button, signal_id): # Accept activation via accelerators regardless of this widget's state return True @@ -193,20 +228,22 @@ class ButtonStrokeColor(gtk.ToolItem): color_palette_hbox = self._palette._picker_hbox content_box = gtk.VBox() - size_spinbutton = gtk.SpinButton() + self.size_spinbutton = gtk.SpinButton() # This is where we set restrictions for size: # Initial value, minimum value, maximum value, step adj = gtk.Adjustment(self.properties['line size'], 1.0, 100.0, 1.0) - size_spinbutton.set_adjustment(adj) - size_spinbutton.set_numeric(True) + self.size_spinbutton.set_adjustment(adj) + self.size_spinbutton.set_numeric(True) label = gtk.Label(_('Size: ')) - hbox = gtk.HBox() - content_box.pack_start(hbox) + hbox_size = gtk.HBox() + self.vbox_brush_options = gtk.VBox() + content_box.pack_start(hbox_size) + content_box.pack_start(self.vbox_brush_options) - hbox.pack_start(label) - hbox.pack_start(size_spinbutton) - size_spinbutton.connect('value-changed', self._on_value_changed) + hbox_size.pack_start(label) + hbox_size.pack_start(self.size_spinbutton) + self.size_spinbutton.connect('value-changed', self._on_value_changed) # User is able to choose Shapes for 'Brush' and 'Eraser' item1 = gtk.RadioButton(None, _('Circle')) @@ -233,37 +270,73 @@ class ButtonStrokeColor(gtk.ToolItem): label = gtk.Label(_('Shape')) - content_box.pack_start(label) - content_box.pack_start(item1) - content_box.pack_start(item2) + self.vbox_brush_options.pack_start(label) + self.vbox_brush_options.pack_start(item1) + self.vbox_brush_options.pack_start(item2) keep_aspect_checkbutton = gtk.CheckButton(_('Keep aspect')) ratio = self._activity.area.keep_aspect_ratio keep_aspect_checkbutton.set_active(ratio) keep_aspect_checkbutton.connect('toggled', self._keep_aspect_checkbutton_toggled) - content_box.pack_start(keep_aspect_checkbutton) + self.vbox_brush_options.pack_start(keep_aspect_checkbutton) color_palette_hbox.pack_start(gtk.VSeparator(), padding=style.DEFAULT_SPACING) color_palette_hbox.pack_start(content_box) color_palette_hbox.show_all() + self._update_palette() return self._palette def _keep_aspect_checkbutton_toggled(self, checkbutton): self._activity.area.keep_aspect_ratio = checkbutton.get_active() + def _update_palette(self): + palette_children = self._palette._picker_hbox.get_children() + if self.color_button.is_stamping(): + # Hide palette color widgets: + for ch in palette_children[:4]: + ch.hide_all() + # Hide brush options: + self.vbox_brush_options.hide_all() + # Change title: + self.set_title(_('Stamp properties')) + else: + # Show palette color widgets: + for ch in palette_children[:4]: + ch.show_all() + # Show brush options: + self.vbox_brush_options.show_all() + # Change title: + self.set_title(_('Brush properties')) + + self._palette._picker_hbox.resize_children() + self._palette._picker_hbox.queue_draw() + + def update_stamping(self): + if self.color_button.is_stamping(): + self.size_spinbutton.set_value(self.color_button.stamp_size) + else: + self.size_spinbutton.set_value(self.color_button.brush_size) + self._update_palette() + def _on_value_changed(self, spinbutton): size = spinbutton.get_value_as_int() - self.properties['line size'] = size + if self.color_button.is_stamping(): + self.properties['stamp size'] = size + resized_stamp = self._activity.area.resize_stamp(size) + self.color_button.set_resized_stamp(resized_stamp) + self.color_button.set_stamp_size(self.properties['stamp size']) + else: + self.properties['line size'] = size + self.color_button.set_brush_size(self.properties['line size']) self._activity.area.set_tool(self.properties) - self.color_button.set_brush_size(self.properties['line size']) def _on_toggled(self, radiobutton, tool, shape): if radiobutton.get_active(): self.properties['line shape'] = shape self.color_button.set_brush_shape(shape) - self.color_button.set_brush_size(self.properties['line size']) + self.color_button.set_brush_size(self.properties['line size']) def get_palette_invoker(self): return self._palette_invoker -- cgit v0.9.1