def _configure_cb(self, widget, event): """Internal: Update size and prefs when window is adjusted""" # Constrain window to fit on its current monitor, if possible. screen = event.get_screen() mon = screen.get_monitor_at_point(event.x, event.y) mon_geom = screen.get_monitor_geometry(mon) # Constrain width and height w = clamp(int(event.width), self.MIN_WIDTH, self.MAX_WIDTH) h = clamp(int(event.height), self.MIN_HEIGHT, self.MAX_HEIGHT) # Constrain position x, y = event.x, event.y if y + h > mon_geom.y + mon_geom.height: y = mon_geom.y + mon_geom.height - h if x + w > mon_geom.x + mon_geom.width: x = mon_geom.x + mon_geom.width - w if x < mon_geom.x: x = mon_geom.x if y < mon_geom.y: y = mon_geom.y event_size = (event.x, event.y, event.width, event.height) ex, ey, ew, eh = [int(c) for c in event_size] x, y, w, h = [int(c) for c in (x, y, w, h)] if not self._corrected_pos: if (x, y) != (ex, ey): GObject.idle_add(self.move, x, y) if (w, h) != (ew, eh): GObject.idle_add(self.resize, w, h) self._corrected_pos = True # Record size self._size = (x, y, w, h) self.app.preferences[self._prefs_size_key] = (w, h)
def _configure_cb(self, widget, event): """Internal: Update size and prefs when window is adjusted""" # Constrain window to fit on its current monitor, if possible. screen = event.get_screen() mon = screen.get_monitor_at_point(event.x, event.y) mon_geom = screen.get_monitor_geometry(mon) # Constrain width and height w = clamp(int(event.width), self.MIN_WIDTH, self.MAX_WIDTH) h = clamp(int(event.height), self.MIN_HEIGHT, self.MAX_HEIGHT) # Constrain position x, y = event.x, event.y if y+h > mon_geom.y + mon_geom.height: y = mon_geom.y + mon_geom.height - h if x+w > mon_geom.x + mon_geom.width: x = mon_geom.x + mon_geom.width - w if x < mon_geom.x: x = mon_geom.x if y < mon_geom.y: y = mon_geom.y event_size = (event.x, event.y, event.width, event.height) ex, ey, ew, eh = [int(c) for c in event_size] x, y, w, h = [int(c) for c in (x, y, w, h)] if not self._corrected_pos: if (x, y) != (ex, ey): GLib.idle_add(self.move, x, y) if (w, h) != (ew, eh): GLib.idle_add(self.resize, w, h) self._corrected_pos = True # Record size self._size = (x, y, w, h) self.app.preferences[self._prefs_size_key] = (w, h)
def _interpolate_p0_p1(self): """Interpolate between p0 and p1, but do not step or clear""" pt0p, pt0 = self._pt0_prev, self._pt0 pt1, pt1n = self._pt1, self._pt1_next can_interp = ( pt0 is not None and pt1 is not None and len(self._np) > 0 ) if can_interp: if pt0p is None: pt0p = pt0 if pt1n is None: pt1n = pt1 t0 = pt0[0] t1 = pt1[0] dt = t1 - t0 can_interp = dt > 0 if can_interp: for np in self._np: t, x, y = np[0:3] p, xt, yt = spline_4p( float(t - t0) / dt, array(pt0p[3:]), array(pt0[3:]), array(pt1[3:]), array(pt1n[3:]) ) p = clamp(p, 0.0, 1.0) xt = clamp(xt, -1.0, 1.0) yt = clamp(yt, -1.0, 1.0) yield (t, x, y, p, xt, yt) if pt1 is not None: yield pt1
def _sizeify_flag_columns(self): """Sneakily scale the fixed size of the flag icons to match texts. This can only be called after the list has rendered once, because GTK doesn't know how tall the treeview's rows will be till then. Therefore it's called in an idle callback after the first show. """ # Get the maximum height for all columns. s = 0 for col in self._columns: ox, oy, w, h = col.cell_get_size(None) if h > s: s = h if not s: return # Set that as the fixed size of the flag icon columns, # within reason, and force a re-layout. h = helpers.clamp(s, 24, 48) w = helpers.clamp(s, 24, 48) for col in [self._flags1_col, self._flags2_col]: for cell in col.get_cells(): cell.set_fixed_size(w, h) col.set_min_width(w) for col in self._columns: col.queue_resize()
def _interpolate_p0_p1(self): """Interpolate between p0 and p1, but do not step or clear""" pt0p, pt0 = self._pt0_prev, self._pt0 pt1, pt1n = self._pt1, self._pt1_next can_interp = (pt0 is not None and pt1 is not None and len(self._np) > 0) if can_interp: if pt0p is None: pt0p = pt0 if pt1n is None: pt1n = pt1 t0 = pt0[0] t1 = pt1[0] dt = t1 - t0 can_interp = dt > 0 if can_interp: for np in self._np: t, x, y = np[0:3] p, xt, yt = spline_4p( float(t - t0) / dt, array(pt0p[3:]), array(pt0[3:]), array(pt1[3:]), array(pt1n[3:])) p = clamp(p, 0.0, 1.0) xt = clamp(xt, -1.0, 1.0) yt = clamp(yt, -1.0, 1.0) yield (t, x, y, p, xt, yt) if pt1 is not None: yield pt1
def _get_paint_chip_highlight(color): """Paint chip highlight edge color""" highlight = HCYColor(color=color) ky = gui.style.PAINT_CHIP_HIGHLIGHT_HCY_Y_MULT kc = gui.style.PAINT_CHIP_HIGHLIGHT_HCY_C_MULT highlight.y = clamp(highlight.y * ky, 0, 1) highlight.c = clamp(highlight.c * kc, 0, 1) return highlight
def _get_paint_chip_shadow(color): """Paint chip shadow edge color""" shadow = HCYColor(color=color) ky = gui.style.PAINT_CHIP_SHADOW_HCY_Y_MULT kc = gui.style.PAINT_CHIP_SHADOW_HCY_C_MULT shadow.y = clamp(shadow.y * ky, 0, 1) shadow.c = clamp(shadow.c * kc, 0, 1) return shadow
def set_color_hsv(self, hsv): h, s, v = hsv while h > 1.0: h -= 1.0 while h < 0.0: h += 1.0 s = clamp(s, 0.0, 1.0) v = clamp(v, 0.0, 1.0) if self.hsv_widget is not None: self.hsv_widget.set_color(h, s, v) else: color = gdk.color_from_hsv(h, s, v) self.color_sel.set_current_color(color)
def _process_queued_event(self, tdw, event_data): """Process one motion event from the motion queue""" drawstate = self._get_drawing_state(tdw) (time, x, y, pressure, xtilt, ytilt, viewzoom, viewrotation, barrel_rotation) = event_data model = tdw.doc # Calculate time delta for the brush engine last_event_time = drawstate.last_handled_event_time drawstate.last_handled_event_time = time if not last_event_time: return dtime = (time - last_event_time) / 1000.0 if self._debug: cavg = drawstate.avgtime if cavg is not None: tavg, nevents = cavg nevents += 1 tavg += (dtime - tavg) / nevents else: tavg = dtime nevents = 1 if ((nevents * tavg) > 1.0) and nevents > 20: logger.debug("Processing at %d events/s (t_avg=%0.3fs)", nevents, tavg) drawstate.avgtime = None else: drawstate.avgtime = (tavg, nevents) current_layer = model._layers.current if not current_layer.get_paintable(): return # Feed data to the brush engine. Pressure and tilt cleanup # needs to be done here to catch all forwarded data after the # earlier interpolations. The interpolation method used for # filling in missing axis data is known to generate # OverflowErrors for legitimate but pathological input streams. # https://github.com/mypaint/mypaint/issues/344 pressure = clamp(pressure, 0.0, 1.0) xtilt = clamp(xtilt, -1.0, 1.0) ytilt = clamp(ytilt, -1.0, 1.0) self.stroke_to(model, dtime, x, y, pressure, xtilt, ytilt, viewzoom, viewrotation, barrel_rotation) # Update the TDW's idea of where we last painted # FIXME: this should live in the model, not the view if pressure: tdw.set_last_painting_pos((x, y))
def __unicode__(self): """Py2-era serialization as a Unicode string. Used by the Py3 __str__() while we are in transition. """ result = u"GIMP Palette\n" if self._name is not None: result += u"Name: %s\n" % self._name if self._columns > 0: result += u"Columns: %d\n" % self._columns result += u"#\n" for col in self._colors: if col is self._EMPTY_SLOT_ITEM: col_name = self._EMPTY_SLOT_NAME r = g = b = 0 else: col_name = col.__name r, g, b = [ clamp(int(c * 0xff), 0, 0xff) for c in col.get_rgb() ] if col_name is None: result += u"%d %d %d\n" % (r, g, b) else: result += u"%d %d %d %s\n" % (r, g, b, col_name) return result
def _datafunc_get_pixbuf_height(initial, column, multiple=8, maximum=256): """Nearest multiple-of-n height for a pixbuf data cell.""" ox, oy, w, h = column.cell_get_size(None) s = initial if h is not None: s = helpers.clamp((int(h // 8) * 8), s, maximum) return s
def _fixed_center(self, center=None, ongoing=True): """Keep a fixed center when zoom or rotation changes :param tuple center: Center of the rotation, display (X, Y) :param bool ongoing: Hint that this is in an ongoing change This context manager's cleanup phase applies a corrective transform which keeps the specified center in the same position on the screen. If the center isn't specified, the center pixel of the widget itself is used. It also queues a redraw. """ # Determine the requested (or default) center in model space cx = None cy = None if center is not None: cx, cy = center if cx is None or cy is None: cx, cy = self.renderer.get_center() cx_model, cy_model = self.renderer.display_to_model(cx, cy) # Execute the changes in the body of the with statement yield # Corrective transform (& limits) self.renderer.scale = helpers.clamp(self.renderer.scale, self.renderer.zoom_min, self.renderer.zoom_max) cx_new, cy_new = self.renderer.model_to_display(cx_model, cy_model) self.renderer.translation_x += cx - cx_new self.renderer.translation_y += cy - cy_new # Redraw handling if ongoing: self.renderer.defer_hq_rendering() self.renderer.queue_draw()
def from_gdk_rgba(gdk_rgba): """Construct a new UIColor from a `Gdk.RGBA` (omitting alpha) >>> from_gdk_rgba(Gdk.RGBA(0.5, 0.8, 0.2, 1)) <RGBColor r=0.5000, g=0.8000, b=0.2000> """ rgbflt = (gdk_rgba.red, gdk_rgba.green, gdk_rgba.blue) return RGBColor(*[clamp(c, 0., 1.) for c in rgbflt])
def motion_notify_cb(self, widget, event): if self.grabbed is None: return x, y = self.eventpoint(event.x, event.y) i = self.grabbed points = self.points # XXX this may fail for non contiguous groups. if i in self._ylock: i_candidate = None if x > points[max(self._ylock[i])][0]: i_candidate = max((i, ) + self._ylock[i]) elif x < points[min(self._ylock[i])][0]: i_candidate = min((i, ) + self._ylock[i]) if (i_candidate is not None and abs(points[i][0] - points[i_candidate][0]) < 0.001): i = i_candidate out = False # by default, points cannot be removed if i == len(points) - 1: # last point stays right left_bound = right_bound = 1.0 elif i == 0: # first point stays left left_bound = right_bound = 0.0 else: # other points can be dragged out left_bound = points[i - 1][0] right_bound = points[i + 1][0] margin = 0.02 inside_x_bounds = left_bound - margin < x < right_bound + margin inside_y_bounds = -0.1 <= y <= 1.1 out = not (self.npoints or (inside_x_bounds and inside_y_bounds)) if out: points[i] = None else: y = clamp(y, 0.0, 1.0) if self.magnetic: x_diff = [abs(x - v) for v in self._SNAP_TO] y_diff = [abs(y - v) for v in self._SNAP_TO] if min(x_diff) < 0.015 and min(y_diff) < 0.015: y = self._SNAP_TO[y_diff.index(min(y_diff))] x = self._SNAP_TO[x_diff.index(min(x_diff))] x = clamp(x, left_bound, right_bound) self.set_point(i, (x, y)) self.queue_draw()
def _load_common_flags_from_ora_elem(self, elem): attrs = elem.attrib self.name = unicode(attrs.get("name", "")) compop = str(attrs.get("composite-op", "")) self.mode = ORA_MODES_BY_OPNAME.get(compop, DEFAULT_MODE) self.opacity = helpers.clamp(float(attrs.get("opacity", "1.0")), 0.0, 1.0) visible = attrs.get("visibility", "visible").lower() self.visible = visible != "hidden" locked = attrs.get("edit-locked", "false").lower() self.locked = helpers.xsd2bool(locked) selected = attrs.get("selected", "false").lower() self.initially_selected = helpers.xsd2bool(selected)
def _load_common_flags_from_ora_elem(self, elem): attrs = elem.attrib self.name = unicode(attrs.get('name', '')) compop = str(attrs.get('composite-op', '')) self.mode = ORA_MODES_BY_OPNAME.get(compop, DEFAULT_MODE) self.opacity = helpers.clamp(float(attrs.get('opacity', '1.0')), 0.0, 1.0) visible = attrs.get('visibility', 'visible').lower() self.visible = (visible != "hidden") locked = attrs.get("edit-locked", 'false').lower() self.locked = lib.xml.xsd2bool(locked) selected = attrs.get("selected", 'false').lower() self.initially_selected = lib.xml.xsd2bool(selected)
def opacity(self, opacity): opacity = helpers.clamp(float(opacity), 0.0, 1.0) if opacity == self._opacity: return if self.mode == PASS_THROUGH_MODE: warn("Cannot change the change the opacity of a layer " "group using PASS_THROUGH_MODE", RuntimeWarning, stacklevel=2) return self._opacity = opacity self._properties_changed(["opacity"]) bbox = tuple(self.get_full_redraw_bbox()) self._content_changed(*bbox)
def rotozoom_with_center(self, function, at_pointer=False): if at_pointer and self.has_pointer and self.last_event_x is not None: cx, cy = self.last_event_x, self.last_event_y else: w, h = self.window.get_size() cx, cy = self.get_center() cx_model, cy_model = self.display_to_model(cx, cy) function() self.scale = helpers.clamp(self.scale, self.zoom_min, self.zoom_max) cx_new, cy_new = self.model_to_display(cx_model, cy_model) self.translation_x += cx - cx_new self.translation_y += cy - cy_new self.queue_draw()
def rotozoom_with_center(self, function, center=None): cx = None cy = None if center is not None: cx, cy = center if cx is None or cy is None: cx, cy = self.renderer.get_center() cx_model, cy_model = self.renderer.display_to_model(cx, cy) function() self.renderer.scale = helpers.clamp(self.renderer.scale, self.renderer.zoom_min, self.renderer.zoom_max) cx_new, cy_new = self.renderer.model_to_display(cx_model, cy_model) self.renderer.translation_x += cx - cx_new self.renderer.translation_y += cy - cy_new self.renderer.queue_draw()
def rotozoom_with_center(self, function, at_pointer=False): if at_pointer and self.has_pointer and self.event_box.last_event_x is not None: cx, cy = self.event_box.last_event_x, self.event_box.last_event_y else: allocation = self.get_allocation() w, h = allocation.width, allocation.height cx, cy = self.renderer.get_center() cx_model, cy_model = self.renderer.display_to_model(cx, cy) function() self.renderer.scale = helpers.clamp(self.renderer.scale, self.renderer.zoom_min, self.renderer.zoom_max) cx_new, cy_new = self.renderer.model_to_display(cx_model, cy_model) self.renderer.translation_x += cx - cx_new self.renderer.translation_y += cy - cy_new self.renderer.queue_draw()
def opacity(self, opacity): opacity = helpers.clamp(float(opacity), 0.0, 1.0) if opacity == self._opacity: return if self.mode == PASS_THROUGH_MODE: warn( "Cannot change the change the opacity of a layer " "group using PASS_THROUGH_MODE", RuntimeWarning, stacklevel=2) return self._opacity = opacity self._properties_changed(["opacity"]) bbox = tuple(self.get_full_redraw_bbox()) self._content_changed(*bbox)
def nearest_move_target(self, x,y): """ Returns the nearest (x, y) the selection indicator can move to during a move with the button held down. """ area_source = self.area_at(self.press_x, self.press_y) area = self.area_at(x,y) if area == area_source and area in [AREA_CIRCLE, AREA_SQUARE]: return x,y dx = x-self.x0 dy = y-self.y0 d = sqrt(dx*dx+dy*dy) x1 = dx/d y1 = dy/d if area_source == AREA_CIRCLE: rx = self.x0 + (self.r2+3.0)*x1 ry = self.y0 + (self.r2+3.0)*y1 else: m = self.m dx = clamp(dx, -m, m) dy = clamp(dy, -m, m) rx = self.x0 + dx ry = self.y0 + dy return rx,ry
def anim_cb(self): """Animation callback. Each step fades the alpha multiplier slightly and invalidates the area last painted. """ self.alpha -= 1 / (self.fade_fps * self.fade_duration) self.alpha = clamp(self.alpha, 0.0, 1.0) if self.__area: self.tdw.queue_draw_area(*self.__area) if self.alpha <= 0.0: self.__anim_srcid = None return False else: return True
def rotozoom_with_center(self, function, at_pointer=False): if at_pointer and self.has_pointer and self.last_event_x is not None: cx, cy = self.last_event_x, self.last_event_y else: w, h = self.window.get_size() cx, cy = self.get_center() cr = self.get_model_coordinates_cairo_context() cx_device, cy_device = cr.device_to_user(cx, cy) function() self.scale = helpers.clamp(self.scale, self.zoom_min, self.zoom_max) cr = self.get_model_coordinates_cairo_context() cx_new, cy_new = cr.user_to_device(cx_device, cy_device) self.translation_x += cx - cx_new self.translation_y += cy - cy_new self.queue_draw()
def anim_cb(self): """Animation callback. Each step fades the alpha multiplier slightly and invalidates the area last painted. """ self.alpha -= 1.0 / (float(self.fade_fps) * self.fade_duration) self.alpha = clamp(self.alpha, 0.0, 1.0) if self.__area: self.tdw.queue_draw_area(*self.__area) if self.alpha <= 0.0: self.__anim_srcid = None return False else: return True
def anim_cb(self): """Animation callback. Each step fades the alpha multiplier slightly and invalidates the area last painted. """ self.alpha -= 1.0 / (float(self.fade_fps) * self.fade_duration) self.alpha = clamp(self.alpha, 0.0, 1.0) win = self.tdw.get_window() if win is not None: if self.__area: win.invalidate_rect(gdk.Rectangle(*self.__area), True) if self.alpha <= 0.0: self.__anim_srcid = None return False else: return True
def new_blank_pixbuf(rgb, w, h): """Create a blank pixbuf with all pixels set to a color :param tuple rgb: Color to blank the pixbuf to (``R,G,B``, floats) :param int w: Width for the new pixbuf :param int h: Width for the new pixbuf The returned pixbuf has no alpha channel. """ pixbuf = GdkPixbuf.Pixbuf.new( GdkPixbuf.Colorspace.RGB, False, 8, w, h, ) r, g, b = (helpers.clamp(int(round(0xff*x)), 0, 0xff) for x in rgb) rgba_pixel = (r<<24) + (g<<16) + (b<<8) + 0xff pixbuf.fill(rgba_pixel) return pixbuf
def opacity(self, opacity): opacity = helpers.clamp(float(opacity), 0.0, 1.0) if opacity == self._opacity: return if self.mode == PASS_THROUGH_MODE: warn("Cannot change the change the opacity multiplier " "of a layer group in PASS_THROUGH_MODE", RuntimeWarning, stacklevel=2) return self._opacity = opacity self._properties_changed(["opacity"]) # Note: not the full_redraw_bbox here. # Changing a layer's opacity multiplier alone cannot change the # calculated alpha of an outlying empty tile in the layer. # Those are always zero. Even if the layer has a fancy masking # mode, that won't affect redraws arising from mere opacity # multiplier updates. bbox = tuple(self.get_bbox()) self._content_changed(*bbox)
def new_blank_pixbuf(rgb, w, h): """Create a blank pixbuf with all pixels set to a color :param tuple rgb: Color to blank the pixbuf to (``R,G,B``, floats) :param int w: Width for the new pixbuf :param int h: Width for the new pixbuf The returned pixbuf has no alpha channel. """ pixbuf = GdkPixbuf.Pixbuf.new( GdkPixbuf.Colorspace.RGB, False, 8, w, h, ) r, g, b = (helpers.clamp(int(round(0xff * x)), 0, 0xff) for x in rgb) rgba_pixel = (r << 24) + (g << 16) + (b << 8) + 0xff pixbuf.fill(rgba_pixel) return pixbuf
def __unicode__(self): result = u"GIMP Palette\n" if self._name is not None: result += u"Name: %s\n" % self._name if self._columns > 0: result += u"Columns: %d\n" % self._columns result += u"#\n" for col in self._colors: if col is self._EMPTY_SLOT_ITEM: col_name = self._EMPTY_SLOT_NAME r = g = b = 0 else: col_name = col.__name r, g, b = [ clamp(int(c * 0xff), 0, 0xff) for c in col.get_rgb() ] if col_name is None: result += u"%d %d %d\n" % (r, g, b) else: result += u"%d %d %d %s\n" % (r, g, b, col_name) return result
def button_press_cb(self, widget, event): if not (self.points or event.button == 1): return x, y = self.eventpoint(event.x, event.y) # Note: Squared distance used for comparisons def dist_squared(p): return abs(x - p[0])**2 + abs(y - p[1])**2 points = self.points dsq, pos = min((dist_squared(p), i) for i, p in enumerate(points)) # Unless the number of points are fixed, maxed out, or the intent # was to move an existing point, insert a new curve point. if not (self.npoints or dsq <= 0.003 or len(points) >= self.maxpoints): candidates = [i+1 for i, (px, _) in enumerate(points) if px < x] insert_pos = candidates and candidates[-1] if insert_pos and insert_pos < len(points): points.insert(insert_pos, (x, clamp(y, 0.0, 1.0))) pos = insert_pos self.queue_draw() self.grabbed = pos
def __unicode__(self): """Py2-era serialization as a Unicode string. Used by the Py3 __str__() while we are in transition. """ result = u"GIMP Palette\n" if self._name is not None: result += u"Name: %s\n" % self._name if self._columns > 0: result += u"Columns: %d\n" % self._columns result += u"#\n" for col in self._colors: if col is self._EMPTY_SLOT_ITEM: col_name = self._EMPTY_SLOT_NAME r = g = b = 0 else: col_name = col.__name r, g, b = [clamp(int(c*0xff), 0, 0xff) for c in col.get_rgb()] if col_name is None: result += u"%d %d %d\n" % (r, g, b) else: result += u"%d %d %d %s\n" % (r, g, b, col_name) return result
def motion_notify_cb(self, tdw, event, fakepressure=None): """Motion event handler: queues raw input and returns :param tdw: The TiledDrawWidget receiving the event :param event: the MotionNotify event being handled :param fakepressure: fake pressure to use if no real pressure Fake pressure is passed with faked motion events, e.g. button-press and button-release handlers for mouse events. """ # Do nothing if painting is inactivated current_layer = tdw.doc._layers.current if not (tdw.is_sensitive and current_layer.get_paintable()): return False # If the device has changed and the last pressure value from the # previous device is not equal to 0.0, this can leave a visible # stroke on the layer even if the 'new' device is not pressed on # the tablet and has a pressure axis == 0.0. Resetting the brush # when the device changes fixes this issue, but there may be a # much more elegant solution that only resets the brush on this # edge-case. same_device = True if tdw.app is not None: device = event.get_source_device() same_device = tdw.app.device_monitor.device_used(device) if not same_device: tdw.doc.brush.reset() # Extract the raw readings for this event x = event.x y = event.y time = event.time pressure = event.get_axis(Gdk.AxisUse.PRESSURE) xtilt = event.get_axis(Gdk.AxisUse.XTILT) ytilt = event.get_axis(Gdk.AxisUse.YTILT) viewzoom = tdw.scale viewrotation = tdw.rotation barrel_rotation = event.get_axis(Gdk.AxisUse.WHEEL) state = event.state # Workaround for buggy evdev behaviour. # Events sometimes get a zero raw pressure reading when the # pressure reading has not changed. This results in broken # lines. As a workaround, forbid zero pressures if there is a # button pressed down, and substitute the last-known good value. # Detail: https://github.com/mypaint/mypaint/issues/29 drawstate = self._get_drawing_state(tdw) if drawstate.button_down is not None: if pressure == 0.0: pressure = drawstate.last_good_raw_pressure elif pressure is not None and np.isfinite(pressure): drawstate.last_good_raw_pressure = pressure # Ensure each event has a defined pressure if pressure is not None: # Using the reported pressure. Apply some sanity checks if not np.isfinite(pressure): # infinity/nan: use button state (instead of clamping in # brush.hpp) https://gna.org/bugs/?14709 pressure = None else: pressure = clamp(pressure, 0.0, 1.0) drawstate.last_event_had_pressure = True # Fake the pressure if we have none, or if infinity was reported if pressure is None: if fakepressure is not None: pressure = clamp(fakepressure, 0.0, 1.0) else: pressure = ((state & Gdk.ModifierType.BUTTON1_MASK) and tdw.app.fakepressure or 0.0) drawstate.last_event_had_pressure = False # Check whether tilt is present. For some tablets without # tilt support GTK reports a tilt axis with value nan, instead # of None. https://gna.org/bugs/?17084 if xtilt is None or ytilt is None or not np.isfinite(xtilt + ytilt): xtilt = 0.0 ytilt = 0.0 # Switching from a non-tilt device to a device which reports # tilt can cause GDK to return out-of-range tilt values, on X11. xtilt = clamp(xtilt, -1.0, 1.0) ytilt = clamp(ytilt, -1.0, 1.0) tilt_ascension = 0.5 * math.atan2(-xtilt, ytilt) / math.pi # Offset barrel rotation if wanted # This could be used to correct for different devices, # Left vs Right handed, etc. b_offset = tdw.app.preferences.get("input.barrel_rotation_offset") if (barrel_rotation is not None): barrel_rotation = (barrel_rotation + b_offset) % 1.0 # barrel_rotation is likely affected by ascension (a bug?) # lets compensate but allow disabling if (barrel_rotation is not None and tdw.app.preferences.get( "input.barrel_rotation_subtract_ascension")): barrel_rotation = (barrel_rotation - tilt_ascension) % 1.0 # If WHEEL is missing (barrel_rotation) # Use the fakerotation controller to allow keyboard control # We can't trust None so also look at preference if (barrel_rotation is None or not tdw.app.preferences.get("input.use_barrel_rotation")): barrel_rotation = (tdw.app.fakerotation + b_offset) % 1.0 # Evdev workaround. X and Y tilts suffer from the same # problem as pressure for fancier devices. if drawstate.button_down is not None: if xtilt == 0.0: xtilt = drawstate.last_good_raw_xtilt else: drawstate.last_good_raw_xtilt = xtilt if ytilt == 0.0: ytilt = drawstate.last_good_raw_ytilt else: drawstate.last_good_raw_ytilt = ytilt if tdw.mirrored: xtilt *= -1.0 barrel_rotation *= -1.0 # Apply pressure mapping if we're running as part of a full # MyPaint application (and if there's one defined). if tdw.app is not None and tdw.app.pressure_mapping: pressure = tdw.app.pressure_mapping(pressure) # Apply any configured while-drawing cursor if pressure > 0: self._hide_drawing_cursor(tdw) else: self._reinstate_drawing_cursor(tdw) # Queue this event x, y = tdw.display_to_model(x, y) event_data = (time, x, y, pressure, xtilt, ytilt, viewzoom, viewrotation, barrel_rotation) drawstate.queue_motion(event_data) # Start the motion event processor, if it isn't already running if not drawstate.motion_processing_cbid: cbid = GLib.idle_add( self._motion_queue_idle_cb, tdw, priority=self.MOTION_QUEUE_PRIORITY, ) drawstate.motion_processing_cbid = cbid
def layer_decrease_opacity(self, action): opa = helpers.clamp(self.model.layer.opacity - 0.08, 0.0, 1.0) self.model.set_layer_opacity(opa)
def __init__(self, app, actions, config_name): """Initialize. :param app: the main Application object. :param iterable actions: keyboard action names to pass through. :param str config_name: config prefix for saving window size. Use a simple "lowercase_with_underscores" name for the configuration key prefix. See also: `gui.keyboard.KeyboardManager.add_window()`. """ # Superclass Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) self.set_modal(True) # Internal state self.app = app self._size = None # last recorded size from any show() self._motion_handler_id = None self._prefs_size_key = "%s.window_size" % (config_name, ) self._resize_info = None # state during an edge resize self._outside_grab_active = False self._outside_cursor = Gdk.Cursor(Gdk.CursorType.LEFT_PTR) self._popup_info = None # Initial positioning self._initial_move_pos = None # used when forcing a specific position self._corrected_pos = None # used when keeping the widget on-screen # Resize cursors self._edge_cursors = {} for edge, cursor in self.EDGE_CURSORS.iteritems(): if cursor is not None: cursor = Gdk.Cursor(cursor) self._edge_cursors[edge] = cursor # Default size self.set_gravity(Gdk.Gravity.NORTH_WEST) default_size = (self.MIN_WIDTH, self.MIN_HEIGHT) w, h = app.preferences.get(self._prefs_size_key, default_size) w = clamp(int(w), self.MIN_WIDTH, self.MAX_WIDTH) h = clamp(int(h), self.MIN_HEIGHT, self.MAX_HEIGHT) default_size = (w, h) self.set_transient_for(app.drawWindow) self.set_default_size(*default_size) self.set_position(Gtk.WindowPosition.MOUSE) # Register with the keyboard manager, but only let certain actions be # driven from the keyboard. app.kbm.add_window(self, actions) # Event handlers self.connect("realize", self._realize_cb) self.connect("configure-event", self._configure_cb) self.connect("enter-notify-event", self._crossing_cb) self.connect("leave-notify-event", self._crossing_cb) self.connect("show", self._show_cb) self.connect("hide", self._hide_cb) self.connect("button-press-event", self._button_press_cb) self.connect("button-release-event", self._button_release_cb) self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK) # Appearance self._frame = Gtk.Frame() self._frame.set_shadow_type(Gtk.ShadowType.OUT) self._align = Gtk.Alignment(0.5, 0.5, 1.0, 1.0) self._align.set_padding(self.EDGE_SIZE, self.EDGE_SIZE, self.EDGE_SIZE, self.EDGE_SIZE) self._frame.add(self._align) Gtk.Window.add(self, self._frame)
def __init__(self, app, actions, config_name): """Initialize. :param app: the main Application object. :param iterable actions: keyboard action names to pass through. :param str config_name: config prefix for saving window size. Use a simple "lowercase_with_underscores" name for the configuration key prefix. See also: `gui.keyboard.KeyboardManager.add_window()`. """ # Superclass Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) self.set_modal(True) # Internal state self.app = app self._size = None # last recorded size from any show() self._motion_handler_id = None self._prefs_size_key = "%s.window_size" % (config_name,) self._resize_info = None # state during an edge resize self._outside_grab_active = False self._outside_cursor = Gdk.Cursor(Gdk.CursorType.LEFT_PTR) self._popup_info = None # Initial positioning self._initial_move_pos = None # used when forcing a specific position self._corrected_pos = None # used when keeping the widget on-screen # Resize cursors self._edge_cursors = {} for edge, cursor in self.EDGE_CURSORS.iteritems(): if cursor is not None: cursor = Gdk.Cursor(cursor) self._edge_cursors[edge] = cursor # Default size self.set_gravity(Gdk.Gravity.NORTH_WEST) default_size = (self.MIN_WIDTH, self.MIN_HEIGHT) w, h = app.preferences.get(self._prefs_size_key, default_size) w = clamp(int(w), self.MIN_WIDTH, self.MAX_WIDTH) h = clamp(int(h), self.MIN_HEIGHT, self.MAX_HEIGHT) default_size = (w, h) self.set_transient_for(app.drawWindow) self.set_default_size(*default_size) self.set_position(Gtk.WindowPosition.MOUSE) # Register with the keyboard manager, but only let certain actions be # driven from the keyboard. app.kbm.add_window(self, actions) # Event handlers self.connect("realize", self._realize_cb) self.connect("configure-event", self._configure_cb) self.connect("enter-notify-event", self._crossing_cb) self.connect("leave-notify-event", self._crossing_cb) self.connect("show", self._show_cb) self.connect("hide", self._hide_cb) self.connect("button-press-event", self._button_press_cb) self.connect("button-release-event", self._button_release_cb) self.add_events( Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK ) # Appearance self._frame = Gtk.Frame() self._frame.set_shadow_type(Gtk.ShadowType.OUT) self._align = Gtk.Alignment.new(0.5, 0.5, 1.0, 1.0) self._align.set_padding( self.EDGE_SIZE, self.EDGE_SIZE, self.EDGE_SIZE, self.EDGE_SIZE ) self._frame.add(self._align) Gtk.Window.add(self, self._frame)
def render_execute(self, cr, surface, sparse, mipmap_level, clip_region): translation_only = self.is_translation_only() model_bbox = surface.x, surface.y, surface.w, surface.h #print 'model bbox', model_bbox # not sure if it is a good idea to clip so tightly # has no effect right now because device_bbox is always smaller cr.rectangle(*model_bbox) cr.clip() layers = self.get_visible_layers() if self.visualize_rendering: surface.pixbuf.fill((int(random.random()*0xff)<<16)+0x00000000) background = None if self.current_layer_solo: background = self.neutral_background_pixbuf layers = [self.doc.layer] # this is for hiding instead #layers.pop(self.doc.layer_idx) if self.overlay_layer: idx = layers.index(self.doc.layer) layers.insert(idx+1, self.overlay_layer) # Composite tiles = [(tx, ty) for tx, ty in surface.get_tiles() if tile_is_visible(cr, tx, ty, clip_region, sparse, translation_only)] self.doc.render_into(surface, tiles, mipmap_level, layers, background) if translation_only and not pygtkcompat.USE_GTK3: # not sure why, but using gdk directly is notably faster than the same via cairo x, y = cr.user_to_device(surface.x, surface.y) self.window.draw_pixbuf(None, surface.pixbuf, 0, 0, int(x), int(y), dither=gdk.RGB_DITHER_MAX) else: #print 'Position (screen coordinates):', cr.user_to_device(surface.x, surface.y) if pygtkcompat.USE_GTK3: gdk.cairo_set_source_pixbuf(cr, surface.pixbuf, round(surface.x), round(surface.y)) else: cr.set_source_pixbuf(surface.pixbuf, round(surface.x), round(surface.y)) pattern = cr.get_source() # We could set interpolation mode here (eg nearest neighbour) #pattern.set_filter(cairo.FILTER_NEAREST) # 1.6s #pattern.set_filter(cairo.FILTER_FAST) # 2.0s #pattern.set_filter(cairo.FILTER_GOOD) # 3.1s #pattern.set_filter(cairo.FILTER_BEST) # 3.1s #pattern.set_filter(cairo.FILTER_BILINEAR) # 3.1s if self.scale > 2.8: # pixelize at high zoom-in levels pattern.set_filter(cairo.FILTER_NEAREST) cr.paint() if self.doc.frame_enabled: # Draw a overlay for all the area outside the "document area" cr.save() frame_rgba = self.app.preferences["frame.color_rgba"] frame_rgba = [helpers.clamp(c, 0, 1) for c in frame_rgba] cr.set_source_rgba(*frame_rgba) cr.set_operator(cairo.OPERATOR_OVER) mipmap_factor = 2**mipmap_level frame = self.doc.get_frame() cr.rectangle(frame[0]/mipmap_factor, frame[1]/mipmap_factor, frame[2]/mipmap_factor, frame[3]/mipmap_factor) cr.rectangle(*model_bbox) cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) cr.fill() cr.restore() if self.visualize_rendering: # visualize painted bboxes (blue) cr.set_source_rgba(0, 0, random.random(), 0.4) cr.paint()
def motion_notify_cb(self, tdw, event, fakepressure=None): """Motion event handler: queues raw input and returns :param tdw: The TiledDrawWidget receiving the event :param event: the MotionNotify event being handled :param fakepressure: fake pressure to use if no real pressure Fake pressure is passed with faked motion events, e.g. button-press and button-release handlers for mouse events. GTK 3.8 and above does motion compression, forcing our use of event filter hackery to obtain the high-resolution event positions required for making brushstrokes. This handler is still called for the events the GDK compression code lets through, and it is the only source of pressure and tilt info available when motion compression is active. """ # Do nothing if painting is inactivated current_layer = tdw.doc._layers.current if not ( tdw.is_sensitive and current_layer.get_paintable() ): return False # Try and initialize an event filter, used to circumvent the # unhelpful motion event compression of newer GDKs. This filter # passes through all events, but motion events are translated # and passed to queue_motion_event separately. drawstate = self._get_drawing_state(tdw) if drawstate.evhack_data is None: self._add_evhack(tdw) # If the device has changed and the last pressure value from the # previous device is not equal to 0.0, this can leave a visible # stroke on the layer even if the 'new' device is not pressed on # the tablet and has a pressure axis == 0.0. Reseting the brush # when the device changes fixes this issue, but there may be a # much more elegant solution that only resets the brush on this # edge-case. same_device = True if tdw.app is not None: device = event.get_source_device() same_device = tdw.app.device_monitor.device_used(device) if not same_device: tdw.doc.brush.reset() # Extract the raw readings for this event x = event.x y = event.y time = event.time pressure = event.get_axis(gdk.AXIS_PRESSURE) xtilt = event.get_axis(gdk.AXIS_XTILT) ytilt = event.get_axis(gdk.AXIS_YTILT) state = event.state # Ensure each non-evhack event has a defined pressure if pressure is not None: # Using the reported pressure. Apply some sanity checks if not isfinite(pressure): # infinity/nan: use button state (instead of clamping in # brush.hpp) https://gna.org/bugs/?14709 pressure = None else: pressure = clamp(pressure, 0.0, 1.0) drawstate.last_event_had_pressure = True # Fake the pressure if we have none, or if infinity was reported if pressure is None: if fakepressure is not None: pressure = clamp(fakepressure, 0.0, 1.0) else: pressure = (state & gdk.BUTTON1_MASK) and 0.5 or 0.0 drawstate.last_event_had_pressure = False # Check whether tilt is present. For some tablets without # tilt support GTK reports a tilt axis with value nan, instead # of None. https://gna.org/bugs/?17084 if xtilt is None or ytilt is None or not isfinite(xtilt+ytilt): xtilt = 0.0 ytilt = 0.0 else: # Tilt inputs are assumed to be relative to the viewport, # but the canvas may be rotated or mirrored, or both. # Compensate before passing them to the brush engine. # https://gna.org/bugs/?19988 if tdw.mirrored: xtilt *= -1.0 if tdw.rotation != 0: tilt_angle = math.atan2(ytilt, xtilt) - tdw.rotation tilt_magnitude = math.sqrt((xtilt**2) + (ytilt**2)) xtilt = tilt_magnitude * math.cos(tilt_angle) ytilt = tilt_magnitude * math.sin(tilt_angle) # HACK: color picking, do not paint # TEST: Does this ever happen now? if state & gdk.CONTROL_MASK or state & gdk.MOD1_MASK: # Don't simply return; this is a workaround for unwanted # lines in https://gna.org/bugs/?16169 pressure = 0.0 # Apply pressure mapping if we're running as part of a full # MyPaint application (and if there's one defined). if tdw.app is not None and tdw.app.pressure_mapping: pressure = tdw.app.pressure_mapping(pressure) # HACK: straight line mode? # TEST: Does this ever happen? if state & gdk.SHIFT_MASK: pressure = 0.0 # If the eventhack filter caught more than one event, push them # onto the motion event queue. Pressures and tilts will be # interpolated from surrounding motion-notify events. if len(drawstate.evhack_positions) > 1: # Remove the last item: it should be the one corresponding # to the current motion-notify-event. hx0, hy0, ht0 = drawstate.evhack_positions.pop(-1) # Check that we can use the eventhack data uncorrected if (hx0, hy0, ht0) == (x, y, time): for hx, hy, ht in drawstate.evhack_positions: hx, hy = tdw.display_to_model(hx, hy) event_data = (ht, hx, hy, None, None, None) drawstate.queue_motion(event_data) else: logger.warning( "Final evhack event (%0.2f, %0.2f, %d) doesn't match its " "corresponding motion-notify-event (%0.2f, %0.2f, %d). " "This can be ignored if it's just a one-off occurrence.", hx0, hy0, ht0, x, y, time) # Reset the eventhack queue if len(drawstate.evhack_positions) > 0: drawstate.evhack_positions = [] # Queue this event x, y = tdw.display_to_model(x, y) event_data = (time, x, y, pressure, xtilt, ytilt) drawstate.queue_motion(event_data) # Start the motion event processor, if it isn't already running if not drawstate.motion_processing_cbid: cbid = gobject.idle_add(self._motion_queue_idle_cb, tdw, priority=self.MOTION_QUEUE_PRIORITY) drawstate.motion_processing_cbid = cbid
def motion_notify_cb(self, tdw, event, fakepressure=None): """Motion event handler: queues raw input and returns :param tdw: The TiledDrawWidget receiving the event :param event: the MotionNotify event being handled :param fakepressure: fake pressure to use if no real pressure Fake pressure is passed with faked motion events, e.g. button-press and button-release handlers for mouse events. GTK 3.8 and above does motion compression, forcing our use of event filter hackery to obtain the high-resolution event positions required for making brushstrokes. This handler is still called for the events the GDK compression code lets through, and it is the only source of pressure and tilt info available when motion compression is active. """ # Do nothing if painting is inactivated current_layer = tdw.doc._layers.current if not (tdw.is_sensitive and current_layer.get_paintable()): return False # Disable or work around GDK's motion event compression if self._event_compression_supported is None: win = tdw.get_window() mc_supported = hasattr(win, "set_event_compression") self._event_compression_supported = mc_supported drawstate = self._get_drawing_state(tdw) if drawstate.event_compression_workaround is None: self._add_event_compression_workaround(tdw) # If the device has changed and the last pressure value from the # previous device is not equal to 0.0, this can leave a visible # stroke on the layer even if the 'new' device is not pressed on # the tablet and has a pressure axis == 0.0. Reseting the brush # when the device changes fixes this issue, but there may be a # much more elegant solution that only resets the brush on this # edge-case. same_device = True if tdw.app is not None: device = event.get_source_device() same_device = tdw.app.device_monitor.device_used(device) if not same_device: tdw.doc.brush.reset() # Extract the raw readings for this event x = event.x y = event.y time = event.time pressure = event.get_axis(Gdk.AxisUse.PRESSURE) xtilt = event.get_axis(Gdk.AxisUse.XTILT) ytilt = event.get_axis(Gdk.AxisUse.YTILT) state = event.state # Workaround for buggy evdev behaviour. # Events sometimes get a zero raw pressure reading when the # pressure reading has not changed. This results in broken # lines. As a workaround, forbid zero pressures if there is a # button pressed down, and substitute the last-known good value. # Detail: https://github.com/mypaint/mypaint/issues/29 if drawstate.button_down is not None: if pressure == 0.0: pressure = drawstate.last_good_raw_pressure elif pressure is not None and np.isfinite(pressure): drawstate.last_good_raw_pressure = pressure # Ensure each non-evhack event has a defined pressure if pressure is not None: # Using the reported pressure. Apply some sanity checks if not np.isfinite(pressure): # infinity/nan: use button state (instead of clamping in # brush.hpp) https://gna.org/bugs/?14709 pressure = None else: pressure = clamp(pressure, 0.0, 1.0) drawstate.last_event_had_pressure = True # Fake the pressure if we have none, or if infinity was reported if pressure is None: if fakepressure is not None: pressure = clamp(fakepressure, 0.0, 1.0) else: pressure = ( (state & Gdk.ModifierType.BUTTON1_MASK) and 0.5 or 0.0) drawstate.last_event_had_pressure = False # Check whether tilt is present. For some tablets without # tilt support GTK reports a tilt axis with value nan, instead # of None. https://gna.org/bugs/?17084 if xtilt is None or ytilt is None or not np.isfinite(xtilt + ytilt): xtilt = 0.0 ytilt = 0.0 # Switching from a non-tilt device to a device which reports # tilt can cause GDK to return out-of-range tilt values, on X11. xtilt = clamp(xtilt, -1.0, 1.0) ytilt = clamp(ytilt, -1.0, 1.0) # Evdev workaround. X and Y tilts suffer from the same # problem as pressure for fancier devices. if drawstate.button_down is not None: if xtilt == 0.0: xtilt = drawstate.last_good_raw_xtilt else: drawstate.last_good_raw_xtilt = xtilt if ytilt == 0.0: ytilt = drawstate.last_good_raw_ytilt else: drawstate.last_good_raw_ytilt = ytilt # Tilt inputs are assumed to be relative to the viewport, # but the canvas may be rotated or mirrored, or both. # Compensate before passing them to the brush engine. # https://gna.org/bugs/?19988 if tdw.mirrored: xtilt *= -1.0 if tdw.rotation != 0: tilt_angle = math.atan2(ytilt, xtilt) - tdw.rotation tilt_magnitude = math.sqrt((xtilt**2) + (ytilt**2)) xtilt = tilt_magnitude * math.cos(tilt_angle) ytilt = tilt_magnitude * math.sin(tilt_angle) # HACK: color picking, do not paint # TEST: Does this ever happen now? if (state & Gdk.ModifierType.CONTROL_MASK or state & Gdk.ModifierType.MOD1_MASK): # Don't simply return; this is a workaround for unwanted # lines in https://gna.org/bugs/?16169 pressure = 0.0 # Apply pressure mapping if we're running as part of a full # MyPaint application (and if there's one defined). if tdw.app is not None and tdw.app.pressure_mapping: pressure = tdw.app.pressure_mapping(pressure) # Apply any configured while-drawing cursor if pressure > 0: self._hide_drawing_cursor(tdw) else: self._reinstate_drawing_cursor(tdw) # HACK: straight line mode? # TEST: Does this ever happen? if state & Gdk.ModifierType.SHIFT_MASK: pressure = 0.0 # If the eventhack filter caught more than one event, push them # onto the motion event queue. Pressures and tilts will be # interpolated from surrounding motion-notify events. if len(drawstate.evhack_positions) > 1: # Remove the last item: it should be the one corresponding # to the current motion-notify-event. hx0, hy0, ht0 = drawstate.evhack_positions.pop(-1) # Check that we can use the eventhack data uncorrected if (hx0, hy0, ht0) == (x, y, time): for hx, hy, ht in drawstate.evhack_positions: hx, hy = tdw.display_to_model(hx, hy) event_data = (ht, hx, hy, None, None, None) drawstate.queue_motion(event_data) else: logger.warning( "Final evhack event (%0.2f, %0.2f, %d) doesn't match its " "corresponding motion-notify-event (%0.2f, %0.2f, %d). " "This can be ignored if it's just a one-off occurrence.", hx0, hy0, ht0, x, y, time) # Reset the eventhack queue if len(drawstate.evhack_positions) > 0: drawstate.evhack_positions = [] # Queue this event x, y = tdw.display_to_model(x, y) event_data = (time, x, y, pressure, xtilt, ytilt) drawstate.queue_motion(event_data) # Start the motion event processor, if it isn't already running if not drawstate.motion_processing_cbid: cbid = GLib.idle_add( self._motion_queue_idle_cb, tdw, priority = self.MOTION_QUEUE_PRIORITY, ) drawstate.motion_processing_cbid = cbid
def paint(self, cr): """Paints the frame, and edit boxes if the app is in FrameEditMode""" if not self.doc.model.frame_enabled: return tdw = self.doc.tdw allocation = tdw.get_allocation() w, h = allocation.width, allocation.height canvas_bbox = (-1, -1, w + 2, h + 2) cr.rectangle(*canvas_bbox) corners = self._frame_corners() p1, p2, p3, p4 = [(int(x) + 0.5, int(y) + 0.5) for x, y in corners] cr.move_to(*p1) cr.line_to(*p2) cr.line_to(*p3) cr.line_to(*p4) cr.close_path() cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) frame_rgba = self.app.preferences["frame.color_rgba"] frame_rgba = [helpers.clamp(c, 0, 1) for c in frame_rgba] cr.set_source_rgba(*frame_rgba) editmode = None for m in self.doc.modes: if isinstance(m, FrameEditMode): editmode = m break if not editmode: # Frame mask cr.fill_preserve() # Doublestrike the edge for emphasis, assuming an alpha frame cr.set_line_width(self.OUTLINE_WIDTH) cr.stroke() else: # Frame mask cr.fill() # Editable frame outline cr.set_line_cap(cairo.LINE_CAP_ROUND) cr.set_line_width(self.EDIT_BOX_WIDTH + 2) r, g, b = self.EDIT_BOX_OUTLINE_RGB cr.set_source_rgba(r, g, b, 0.666) cr.move_to(*p1) cr.line_to(*p2) cr.line_to(*p3) cr.line_to(*p4) cr.close_path() cr.stroke() # Line corresponding to editable zones zonelines = [(FrameEditMode.TOP, p1, p2), (FrameEditMode.RIGHT, p2, p3), (FrameEditMode.BOTTOM, p3, p4), (FrameEditMode.LEFT, p4, p1)] cr.set_line_width(self.EDIT_BOX_WIDTH) for zone, p, q in zonelines: if editmode._zone and (editmode._zone == zone): r, g, b = self.EDIT_BOX_PRELIGHT_RGB else: r, g, b = self.EDIT_BOX_RGB cr.set_source_rgba(r, g, b, 1) cr.move_to(*p) cr.line_to(*q) cr.stroke() # Corner dot outline radius = self.EDIT_CORNER_SIZE + 1 r, g, b = self.EDIT_BOX_OUTLINE_RGB cr.set_source_rgba(r, g, b, 0.666) for p in [p1, p2, p3, p4]: x, y = p cr.arc(x, y, radius, 0, 2 * math.pi) cr.fill() cr.stroke() # Dots corresponding to editable corners zonecorners = [ (p1, FrameEditMode.TOP | FrameEditMode.LEFT), (p2, FrameEditMode.TOP | FrameEditMode.RIGHT), (p3, FrameEditMode.BOTTOM | FrameEditMode.RIGHT), (p4, FrameEditMode.BOTTOM | FrameEditMode.LEFT), ] radius = self.EDIT_CORNER_SIZE for p, zonemask in zonecorners: x, y = p if editmode._zone and (editmode._zone == zonemask): r, g, b = self.EDIT_BOX_PRELIGHT_RGB else: r, g, b = self.EDIT_BOX_RGB cr.set_source_rgba(r, g, b, 1) cr.arc(x, y, radius, 0, 2 * math.pi) cr.fill()
def paint(self, cr): """Paints the frame, and edit boxes if the app is in FrameEditMode""" if not self.doc.model.frame_enabled: return tdw = self.doc.tdw allocation = tdw.get_allocation() w, h = allocation.width, allocation.height canvas_bbox = (-1, -1, w+2, h+2) cr.rectangle(*canvas_bbox) corners = self._frame_corners() p1, p2, p3, p4 = [(int(x)+0.5, int(y)+0.5) for x, y in corners] cr.move_to(*p1) cr.line_to(*p2) cr.line_to(*p3) cr.line_to(*p4) cr.close_path() cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) frame_rgba = self.app.preferences["frame.color_rgba"] frame_rgba = [helpers.clamp(c, 0, 1) for c in frame_rgba] cr.set_source_rgba(*frame_rgba) editmode = None for m in self.doc.modes: if isinstance(m, FrameEditMode): editmode = m break if not editmode: # Frame mask cr.fill_preserve() # Doublestrike the edge for emphasis, assuming an alpha frame cr.set_line_width(self.OUTLINE_WIDTH) cr.stroke() else: # Frame mask cr.fill() # Editable/active frame outline cr.set_line_cap(cairo.LINE_CAP_ROUND) zonelines = [(FrameEditMode.TOP, p1, p2), (FrameEditMode.RIGHT, p2, p3), (FrameEditMode.BOTTOM, p3, p4), (FrameEditMode.LEFT, p4, p1)] edge_width = gui.style.DRAGGABLE_EDGE_WIDTH for zone, p, q in zonelines: gui.drawutils.draw_draggable_edge_drop_shadow( cr=cr, p0=p, p1=q, width=edge_width, ) cr.set_line_width(edge_width) for zone, p, q in zonelines: if editmode._zone and (editmode._zone == zone): rgb = gui.style.ACTIVE_ITEM_COLOR.get_rgb() else: rgb = gui.style.EDITABLE_ITEM_COLOR.get_rgb() cr.set_source_rgb(*rgb) cr.move_to(*p) cr.line_to(*q) cr.stroke() # Dots corresponding to editable corners zonecorners = [ (p1, FrameEditMode.TOP | FrameEditMode.LEFT), (p2, FrameEditMode.TOP | FrameEditMode.RIGHT), (p3, FrameEditMode.BOTTOM | FrameEditMode.RIGHT), (p4, FrameEditMode.BOTTOM | FrameEditMode.LEFT) ] radius = gui.style.DRAGGABLE_POINT_HANDLE_SIZE for p, zonemask in zonecorners: x, y = p if editmode._zone and (editmode._zone == zonemask): col = gui.style.ACTIVE_ITEM_COLOR else: col = gui.style.EDITABLE_ITEM_COLOR gui.drawutils.render_round_floating_color_chip( cr=cr, x=x, y=y, color=col, radius=radius, )
def load(self, filehandle, silent=False): """Load contents from a file handle containing a GIMP palette. :param filehandle: File-like object (.readline, line iteration) :param bool silent: If true, don't emit any events. >>> pal = Palette() >>> with open("palettes/MyPaint_Default.gpl", "r") as fp: ... pal.load(fp) >>> len(pal) > 1 True If the file format is incorrect, a RuntimeError will be raised. """ comment_line_re = re.compile(r'^#') field_line_re = re.compile(r'^(\w+)\s*:\s*(.*)$') color_line_re = re.compile(r'^(\d+)\s+(\d+)\s+(\d+)\s*(?:\b(.*))$') fp = filehandle self.clear(silent=True) # method fires events itself line = fp.readline() if line.strip() != "GIMP Palette": raise RuntimeError("Not a valid GIMP Palette") header_done = False line_num = 0 for line in fp: line = line.strip() line_num += 1 if line == '': continue if comment_line_re.match(line): continue if not header_done: match = field_line_re.match(line) if match: key, value = match.groups() key = key.lower() if key == 'name': self._name = value.strip() elif key == 'columns': self._columns = int(value) else: logger.warning("Unknown 'key:value' pair %r", line) continue else: header_done = True match = color_line_re.match(line) if not match: logger.warning("Expected 'R G B [Name]', not %r", line) continue r, g, b, col_name = match.groups() col_name = col_name.strip() r = clamp(int(r), 0, 0xff) / 0xff g = clamp(int(g), 0, 0xff) / 0xff b = clamp(int(b), 0, 0xff) / 0xff if r == g == b == 0 and col_name == self._EMPTY_SLOT_NAME: self.append(None) else: col = RGBColor(r, g, b) col.__name = col_name self._colors.append(col) if not silent: self.info_changed() self.sequence_changed() self.match_changed()
def motion_notify_cb(self, tdw, event, fakepressure=None): """Motion event handler: queues raw input and returns :param tdw: The TiledDrawWidget receiving the event :param event: the MotionNotify event being handled :param fakepressure: fake pressure to use if no real pressure Fake pressure is passed with faked motion events, e.g. button-press and button-release handlers for mouse events. """ # Do nothing if painting is inactivated current_layer = tdw.doc._layers.current if not (tdw.is_sensitive and current_layer.get_paintable()): return False # If the device has changed and the last pressure value from the # previous device is not equal to 0.0, this can leave a visible # stroke on the layer even if the 'new' device is not pressed on # the tablet and has a pressure axis == 0.0. Reseting the brush # when the device changes fixes this issue, but there may be a # much more elegant solution that only resets the brush on this # edge-case. same_device = True if tdw.app is not None: device = event.get_source_device() same_device = tdw.app.device_monitor.device_used(device) if not same_device: tdw.doc.brush.reset() # Extract the raw readings for this event x = event.x y = event.y time = event.time pressure = event.get_axis(Gdk.AxisUse.PRESSURE) xtilt = event.get_axis(Gdk.AxisUse.XTILT) ytilt = event.get_axis(Gdk.AxisUse.YTILT) viewzoom = tdw.scale viewrotation = tdw.rotation state = event.state # Workaround for buggy evdev behaviour. # Events sometimes get a zero raw pressure reading when the # pressure reading has not changed. This results in broken # lines. As a workaround, forbid zero pressures if there is a # button pressed down, and substitute the last-known good value. # Detail: https://github.com/mypaint/mypaint/issues/29 drawstate = self._get_drawing_state(tdw) if drawstate.button_down is not None: if pressure == 0.0: pressure = drawstate.last_good_raw_pressure elif pressure is not None and np.isfinite(pressure): drawstate.last_good_raw_pressure = pressure # Ensure each event has a defined pressure if pressure is not None: # Using the reported pressure. Apply some sanity checks if not np.isfinite(pressure): # infinity/nan: use button state (instead of clamping in # brush.hpp) https://gna.org/bugs/?14709 pressure = None else: pressure = clamp(pressure, 0.0, 1.0) drawstate.last_event_had_pressure = True # Fake the pressure if we have none, or if infinity was reported if pressure is None: if fakepressure is not None: pressure = clamp(fakepressure, 0.0, 1.0) else: pressure = ((state & Gdk.ModifierType.BUTTON1_MASK) and 0.5 or 0.0) drawstate.last_event_had_pressure = False # Check whether tilt is present. For some tablets without # tilt support GTK reports a tilt axis with value nan, instead # of None. https://gna.org/bugs/?17084 if xtilt is None or ytilt is None or not np.isfinite(xtilt + ytilt): xtilt = 0.0 ytilt = 0.0 # Switching from a non-tilt device to a device which reports # tilt can cause GDK to return out-of-range tilt values, on X11. xtilt = clamp(xtilt, -1.0, 1.0) ytilt = clamp(ytilt, -1.0, 1.0) # Evdev workaround. X and Y tilts suffer from the same # problem as pressure for fancier devices. if drawstate.button_down is not None: if xtilt == 0.0: xtilt = drawstate.last_good_raw_xtilt else: drawstate.last_good_raw_xtilt = xtilt if ytilt == 0.0: ytilt = drawstate.last_good_raw_ytilt else: drawstate.last_good_raw_ytilt = ytilt # Tilt inputs are assumed to be relative to the viewport, # but the canvas may be rotated or mirrored, or both. # Compensate before passing them to the brush engine. # https://gna.org/bugs/?19988 if tdw.mirrored: xtilt *= -1.0 if tdw.rotation != 0: tilt_angle = math.atan2(ytilt, xtilt) - tdw.rotation tilt_magnitude = math.sqrt((xtilt**2) + (ytilt**2)) xtilt = tilt_magnitude * math.cos(tilt_angle) ytilt = tilt_magnitude * math.sin(tilt_angle) # HACK: color picking, do not paint # TEST: Does this ever happen now? if (state & Gdk.ModifierType.CONTROL_MASK or state & Gdk.ModifierType.MOD1_MASK): # Don't simply return; this is a workaround for unwanted # lines in https://gna.org/bugs/?16169 pressure = 0.0 # Apply pressure mapping if we're running as part of a full # MyPaint application (and if there's one defined). if tdw.app is not None and tdw.app.pressure_mapping: pressure = tdw.app.pressure_mapping(pressure) # Apply any configured while-drawing cursor if pressure > 0: self._hide_drawing_cursor(tdw) else: self._reinstate_drawing_cursor(tdw) # HACK: straight line mode? # TEST: Does this ever happen? if state & Gdk.ModifierType.SHIFT_MASK: pressure = 0.0 # Queue this event x, y = tdw.display_to_model(x, y) event_data = (time, x, y, pressure, xtilt, ytilt, viewzoom, viewrotation) drawstate.queue_motion(event_data) # Start the motion event processor, if it isn't already running if not drawstate.motion_processing_cbid: cbid = GLib.idle_add( self._motion_queue_idle_cb, tdw, priority=self.MOTION_QUEUE_PRIORITY, ) drawstate.motion_processing_cbid = cbid
def paint(self, cr): """Paints the frame, and edit boxes if the app is in FrameEditMode""" if not self.doc.model.frame_enabled: return tdw = self.doc.tdw allocation = tdw.get_allocation() w, h = allocation.width, allocation.height canvas_bbox = (-1, -1, w+2, h+2) cr.rectangle(*canvas_bbox) corners = self._frame_corners() p1, p2, p3, p4 = [(int(x)+0.5, int(y)+0.5) for x, y in corners] cr.move_to(*p1) cr.line_to(*p2) cr.line_to(*p3) cr.line_to(*p4) cr.close_path() cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) frame_rgba = self.app.preferences["frame.color_rgba"] frame_rgba = [helpers.clamp(c, 0, 1) for c in frame_rgba] cr.set_source_rgba(*frame_rgba) editmode = None for m in self.doc.modes: if isinstance(m, FrameEditMode): editmode = m break if not editmode: # Frame mask cr.fill_preserve() # Doublestrike the edge for emphasis, assuming an alpha frame cr.set_line_width(self.OUTLINE_WIDTH) cr.stroke() else: # Frame mask cr.fill() # Editable frame outline cr.set_line_cap(cairo.LINE_CAP_ROUND) cr.set_line_width(self.EDIT_BOX_WIDTH + 2) r, g, b = self.EDIT_BOX_OUTLINE_RGB cr.set_source_rgba(r, g, b, 0.666) cr.move_to(*p1) cr.line_to(*p2) cr.line_to(*p3) cr.line_to(*p4) cr.close_path() cr.stroke() # Line corresponding to editable zones zonelines = [(FrameEditMode.TOP, p1, p2), (FrameEditMode.RIGHT, p2, p3), (FrameEditMode.BOTTOM, p3, p4), (FrameEditMode.LEFT, p4, p1)] cr.set_line_width(self.EDIT_BOX_WIDTH) for zone, p, q in zonelines: if editmode._zone and (editmode._zone == zone): r, g, b = self.EDIT_BOX_PRELIGHT_RGB else: r, g, b = self.EDIT_BOX_RGB cr.set_source_rgba(r, g, b, 1) cr.move_to(*p) cr.line_to(*q) cr.stroke() # Corner dot outline radius = self.EDIT_CORNER_SIZE + 1 r, g, b = self.EDIT_BOX_OUTLINE_RGB cr.set_source_rgba(r, g, b, 0.666) for p in [p1, p2, p3, p4]: x, y = p cr.arc(x, y, radius, 0, 2*math.pi) cr.fill() cr.stroke() # Dots corresponding to editable corners zonecorners = [ (p1, FrameEditMode.TOP | FrameEditMode.LEFT), (p2, FrameEditMode.TOP | FrameEditMode.RIGHT), (p3, FrameEditMode.BOTTOM | FrameEditMode.RIGHT), (p4, FrameEditMode.BOTTOM | FrameEditMode.LEFT) ] radius = self.EDIT_CORNER_SIZE for p, zonemask in zonecorners: x, y = p if editmode._zone and (editmode._zone == zonemask): r, g, b = self.EDIT_BOX_PRELIGHT_RGB else: r, g, b = self.EDIT_BOX_RGB cr.set_source_rgba(r, g, b, 1) cr.arc(x, y, radius, 0, 2*math.pi) cr.fill()
def motion_notify_cb(self, tdw, event, fakepressure=None): """Motion event handler: queues raw input and returns :param tdw: The TiledDrawWidget receiving the event :param event: the MotionNotify event being handled :param fakepressure: fake pressure to use if no real pressure Fake pressure is passed with faked motion events, e.g. button-press and button-release handlers for mouse events. GTK 3.8 and above does motion compression, forcing our use of event filter hackery to obtain the high-resolution event positions required for making brushstrokes. This handler is still called for the events the GDK compression code lets through, and it is the only source of pressure and tilt info available when motion compression is active. """ # Do nothing if painting is inactivated current_layer = tdw.doc._layers.current if not (tdw.is_sensitive and current_layer.get_paintable()): return False # Disable or work around GDK's motion event compression if self._event_compression_supported is None: win = tdw.get_window() mc_supported = hasattr(win, "set_event_compression") self._event_compression_supported = mc_supported drawstate = self._get_drawing_state(tdw) if drawstate.event_compression_workaround is None: self._add_event_compression_workaround(tdw) # If the device has changed and the last pressure value from the # previous device is not equal to 0.0, this can leave a visible # stroke on the layer even if the 'new' device is not pressed on # the tablet and has a pressure axis == 0.0. Reseting the brush # when the device changes fixes this issue, but there may be a # much more elegant solution that only resets the brush on this # edge-case. same_device = True if tdw.app is not None: device = event.get_source_device() same_device = tdw.app.device_monitor.device_used(device) if not same_device: tdw.doc.brush.reset() # Extract the raw readings for this event x = event.x y = event.y time = event.time pressure = event.get_axis(gdk.AXIS_PRESSURE) xtilt = event.get_axis(gdk.AXIS_XTILT) ytilt = event.get_axis(gdk.AXIS_YTILT) state = event.state # Workaround for buggy evdev behaviour. # Events sometimes get a zero raw pressure reading when the # pressure reading has not changed. This results in broken # lines. As a workaround, forbid zero pressures if there is a # button pressed down, and substitute the last-known good value. # Detail: https://github.com/mypaint/mypaint/issues/29 if drawstate.button_down is not None: if pressure == 0.0: pressure = drawstate.last_good_raw_pressure elif pressure is not None and isfinite(pressure): drawstate.last_good_raw_pressure = pressure # Ensure each non-evhack event has a defined pressure if pressure is not None: # Using the reported pressure. Apply some sanity checks if not isfinite(pressure): # infinity/nan: use button state (instead of clamping in # brush.hpp) https://gna.org/bugs/?14709 pressure = None else: pressure = clamp(pressure, 0.0, 1.0) drawstate.last_event_had_pressure = True # Fake the pressure if we have none, or if infinity was reported if pressure is None: if fakepressure is not None: pressure = clamp(fakepressure, 0.0, 1.0) else: pressure = (state & gdk.BUTTON1_MASK) and 0.5 or 0.0 drawstate.last_event_had_pressure = False # Check whether tilt is present. For some tablets without # tilt support GTK reports a tilt axis with value nan, instead # of None. https://gna.org/bugs/?17084 if xtilt is None or ytilt is None or not isfinite(xtilt + ytilt): xtilt = 0.0 ytilt = 0.0 else: # Evdev workaround. X and Y tilts suffer from the same # problem as pressure for fancier devices. if drawstate.button_down is not None: if xtilt == 0.0: xtilt = drawstate.last_good_raw_xtilt else: drawstate.last_good_raw_xtilt = xtilt if ytilt == 0.0: ytilt = drawstate.last_good_raw_ytilt else: drawstate.last_good_raw_ytilt = ytilt # Tilt inputs are assumed to be relative to the viewport, # but the canvas may be rotated or mirrored, or both. # Compensate before passing them to the brush engine. # https://gna.org/bugs/?19988 if tdw.mirrored: xtilt *= -1.0 if tdw.rotation != 0: tilt_angle = math.atan2(ytilt, xtilt) - tdw.rotation tilt_magnitude = math.sqrt((xtilt**2) + (ytilt**2)) xtilt = tilt_magnitude * math.cos(tilt_angle) ytilt = tilt_magnitude * math.sin(tilt_angle) # HACK: color picking, do not paint # TEST: Does this ever happen now? if state & gdk.CONTROL_MASK or state & gdk.MOD1_MASK: # Don't simply return; this is a workaround for unwanted # lines in https://gna.org/bugs/?16169 pressure = 0.0 # Apply pressure mapping if we're running as part of a full # MyPaint application (and if there's one defined). if tdw.app is not None and tdw.app.pressure_mapping: pressure = tdw.app.pressure_mapping(pressure) # Apply any configured while-drawing cursor if pressure > 0: self._hide_drawing_cursor(tdw) else: self._reinstate_drawing_cursor(tdw) # HACK: straight line mode? # TEST: Does this ever happen? if state & gdk.SHIFT_MASK: pressure = 0.0 # If the eventhack filter caught more than one event, push them # onto the motion event queue. Pressures and tilts will be # interpolated from surrounding motion-notify events. if len(drawstate.evhack_positions) > 1: # Remove the last item: it should be the one corresponding # to the current motion-notify-event. hx0, hy0, ht0 = drawstate.evhack_positions.pop(-1) # Check that we can use the eventhack data uncorrected if (hx0, hy0, ht0) == (x, y, time): for hx, hy, ht in drawstate.evhack_positions: hx, hy = tdw.display_to_model(hx, hy) event_data = (ht, hx, hy, None, None, None) drawstate.queue_motion(event_data) else: logger.warning( "Final evhack event (%0.2f, %0.2f, %d) doesn't match its " "corresponding motion-notify-event (%0.2f, %0.2f, %d). " "This can be ignored if it's just a one-off occurrence.", hx0, hy0, ht0, x, y, time) # Reset the eventhack queue if len(drawstate.evhack_positions) > 0: drawstate.evhack_positions = [] # Queue this event x, y = tdw.display_to_model(x, y) event_data = (time, x, y, pressure, xtilt, ytilt) drawstate.queue_motion(event_data) # Start the motion event processor, if it isn't already running if not drawstate.motion_processing_cbid: cbid = gobject.idle_add(self._motion_queue_idle_cb, tdw, priority=self.MOTION_QUEUE_PRIORITY) drawstate.motion_processing_cbid = cbid