def _draw_cb(self, da, cr): """Paint a preview of the current brush to the view.""" if not self._brush_preview: cr.set_source_rgb(1, 0, 1) cr.paint() return aw = da.get_allocated_width() ah = da.get_allocated_height() # Work in a temporary group so that # the result can be masked with a gradient later. cr.push_group() # Paint a shadow line around the edge of # where the the brush preview will go. # There's an additional top border of one pixel # for alignment with the color preview widget # in the other corner. cr.rectangle(1.5, 2.5, aw - 3, ah) cr.set_line_join(cairo.LINE_JOIN_ROUND) cr.set_source_rgba(*self._OUTLINE_RGBA) cr.set_line_width(3) cr.stroke() # Scale and align the brush preview in its own saved context cr.save() # Clip rectangle for the bit in the middle of the shadow. # Note that the bottom edge isn't shadowed. cr.rectangle(1, 2, aw - 2, ah) cr.clip() # Scale and align the preview to the top of that clip rect. preview = self._brush_preview pw = preview.get_width() ph = preview.get_height() area_size = float(max(aw, ah)) - 2 preview_size = float(max(pw, ph)) x = math.floor(-pw / 2.0) y = 0 cr.translate(aw / 2.0, 2) scale = area_size / preview_size cr.scale(scale, scale) Gdk.cairo_set_source_pixbuf(cr, preview, x, y) cr.paint() cr.restore() # Finally a highlight around the edge in the house style # Note that the bottom edge isn't highlighted. cr.rectangle(1.5, 2.5, aw - 3, ah) cr.set_line_width(1) cr.set_source_rgba(*self._EDGE_HIGHLIGHT_RGBA) cr.stroke() # Paint the group within a gradient mask cr.pop_group_to_source() mask = cairo.LinearGradient(0, 0, 0, ah) mask.add_color_stop_rgba(0.0, 1, 1, 1, 1.0) mask.add_color_stop_rgba(0.8, 1, 1, 1, 1.0) mask.add_color_stop_rgba(0.95, 1, 1, 1, 0.5) mask.add_color_stop_rgba(1.0, 1, 1, 1, 0.1) cr.mask(mask)
def render_round_floating_button(cr, x, y, color, pixbuf, z=2, radius=gui.style.FLOATING_BUTTON_RADIUS): """Draw a round floating button with a standard size. :param cairo.Context cr: Context in which to draw. :param float x: X coordinate of the center pixel. :param float y: Y coordinate of the center pixel. :param lib.color.UIColor color: Color for the button base. :param GdkPixbuf.Pixbuf pixbuf: Icon to render. :param int z: Simulated height of the button above the canvas. :param float radius: Button radius, in pixels. These are used within certain overlays tightly associated with particular interaction modes for manipulating things on the canvas. """ x = round(float(x)) y = round(float(y)) render_round_floating_color_chip(cr, x, y, color, radius=radius, z=z) cr.save() w = pixbuf.get_width() h = pixbuf.get_height() x -= w / 2 y -= h / 2 Gdk.cairo_set_source_pixbuf(cr, pixbuf, x, y) cr.rectangle(x, y, w, h) cr.clip() cr.paint() cr.restore()
def _key_release_cb(self, widget, event): """Application-wide key release handler.""" consumed = widget.propagate_key_event(event) if consumed: return True if not self.enabled: return def released(hardware_keycode): action = self.pressed[hardware_keycode] del self.pressed[hardware_keycode] if action.keyup_callback: action.keyup_callback(widget, event) action.keyup_callback = None if event.keyval == Gdk.KEY_Escape: # emergency exit in case of bugs for hardware_keycode in list(self.pressed.keys()): released(hardware_keycode) # Pop all stacked modes; they should release grabs self.app.doc.modes.reset() # Just in case... Gdk.pointer_ungrab(event.time) else: # note: event.keyval would not be suited for this because # it can be different from the one we have seen in # key_press_cb if the user has released a modifier first if event.hardware_keycode in self.pressed: released(event.hardware_keycode) return True # Fallthru handler: dispatch doc-specific stuff. return self._dispatch_fallthru_key_release_event(widget, event)
def translate(hardware_keycode, state, group): # We may need to retry several times to deal with garbled text. keymap = Gdk.Keymap.get_default() # distinct it = list(set([group, 0, 1, 2])) ok_to_return = False keyval = None keyval_lower = None for g in it: res = keymap.translate_keyboard_state(hardware_keycode, Gdk.ModifierType(0), g) if not res: # PyGTK returns None when gdk_keymap_translate_keyboard_state() # returns false. Not sure if this is a bug or a feature - the only # time I have seen this happen is when I put my laptop into sleep # mode. continue keyval = res[1] # consumed_modifiers = res[4] lbl = Gtk.accelerator_get_label(keyval, state) if is_ascii(lbl): ok_to_return = True break if not ok_to_return: logger.warning( 'translate_keyboard_state() returned no valid response. ' 'Strange key pressed?') return None, None, None, None # We want to ignore irrelevant modifiers like ScrollLock. The stored # key binding does not include modifiers that affected its keyval. mods = Gdk.ModifierType(state & Gtk.accelerator_get_default_mod_mask()) keyval_lower = Gdk.keyval_to_lower(keyval) # If lowercasing affects the keysym, then we need to include # SHIFT in the modifiers. We re-upper case when we match against # the keyval, but display and save in caseless form. if keyval != keyval_lower: mods |= Gdk.ModifierType.SHIFT_MASK # So we get (<Shift>j, Shift+J) but just (plus, +). As I # understand it. accel_label = Gtk.accelerator_get_label(keyval_lower, mods) return keyval, keyval_lower, accel_label, mods
def _key_press_cb(self, widget, event): """App-wide keypress handler for toplevel windows.""" # If an input widget has focus - their key handling is prioritized. consumed = widget.propagate_key_event(event) if consumed: return True if not self.enabled: return # See gtk sourcecode in gtkmenu.c function gtk_menu_key_press, # which uses the same code as below when changing an accelerator. keymap = Gdk.Keymap.get_default() # Instead of using event.keyval, we do it the lowlevel way. # Reason: ignoring CAPSLOCK and checking if SHIFT was pressed state = Gdk.ModifierType(event.state & ~Gdk.ModifierType.LOCK_MASK) res = keymap.translate_keyboard_state(event.hardware_keycode, state, event.group) if not res: # PyGTK returns None when gdk_keymap_translate_keyboard_state() # returns false. Not sure if this is a bug or a feature - the only # time I have seen this happen is when I put my laptop into sleep # mode. logger.warning('translate_keyboard_state() returned None. ' 'Strange key pressed?') return keyval = res[1] consumed_modifiers = res[4] # We want to ignore irrelevant modifiers like ScrollLock. The stored # key binding does not include modifiers that affected its keyval. modifiers = (event.state & Gtk.accelerator_get_default_mod_mask() & ~consumed_modifiers) # Except that key bindings are always stored in lowercase. keyval_lower = Gdk.keyval_to_lower(keyval) if keyval_lower != keyval: modifiers |= Gdk.ModifierType.SHIFT_MASK action = self.keymap.get((keyval_lower, modifiers)) if not action and not self.fallbacks_disabled(): # try hardcoded keys action = self.keymap2.get((keyval_lower, modifiers)) # Don't dispatch if the window is only sensitive to a subset of # actions, and the action is not in that set. if action is not None and isinstance(action, Gtk.Action): win_actions = self.window_actions.get(widget, None) if win_actions is not None: if action.get_name() not in win_actions: return False # If the lookup succeeded, activate the corresponding action. if action: return self.activate_keydown_event(action, event) # Otherwise, dispatch the event to the active doc. return self._dispatch_fallthru_key_press_event(widget, event)
def draw_cb(self, widget, cr): # Paint the base color, and the list's pixbuf. state_flags = widget.get_state_flags() style_context = widget.get_style_context() style_context.save() bg_color_gdk = style_context.get_background_color(state_flags) bg_color = uicolor.from_gdk_rgba(bg_color_gdk) cr.set_source_rgb(*bg_color.get_rgb()) cr.paint() Gdk.cairo_set_source_pixbuf(cr, self.pixbuf, 0, 0) cr.paint() # border colors gdkrgba = style_context.get_background_color(state_flags | Gtk.StateFlags.SELECTED) selected_color = uicolor.from_gdk_rgba(gdkrgba) gdkrgba = style_context.get_background_color(state_flags | Gtk.StateFlags.NORMAL) insertion_color = uicolor.from_gdk_rgba(gdkrgba) style_context.restore() # Draw borders last_i = len(self.itemlist) - 1 for i, b in enumerate(self.itemlist): rect_color = None if b is self.selected: rect_color = selected_color elif self.drag_insertion_index is not None: if i == self.drag_insertion_index \ or (i == last_i and self.drag_insertion_index > i): rect_color = insertion_color if rect_color is None: continue x = (i % self.tiles_w) * self.total_w y = (i // self.tiles_w) * self.total_h w = self.total_w h = self.total_h def shrink(pixels, x, y, w, h): x += pixels y += pixels w -= 2 * pixels h -= 2 * pixels return (x, y, w, h) x, y, w, h = shrink(self.spacing_outside, x, y, w, h) for j in xrange(self.border_visible_outside_cell): x, y, w, h = shrink(-1, x, y, w, h) max_j = self.border_visible + self.border_visible_outside_cell for j in xrange(max_j): cr.set_source_rgb(*rect_color.get_rgb()) cr.rectangle(x, y, w - 1, h - 1) # FIXME: check pixel alignment cr.stroke() x, y, w, h = shrink(1, x, y, w, h) return True
def button_press_displayname(button, mods, shorten=False): """Converts a button number & modifier mask to a localized unicode string. """ button = int(button) mods = int(mods) if button <= 0: return None mods = Gdk.ModifierType(mods) modif_label = Gtk.accelerator_get_label(0, mods) modif_label = unicode(modif_label) separator = "" if modif_label: separator = u"+" # TRANSLATORS: "Button" refers to a mouse button # TRANSLATORS: It is part of a button map label. mouse_button_label = _("Button") if shorten: # TRANSLATORS: abbreviated "Button <number>" for forms like "Alt+Btn1" mouse_button_label = _("Btn") return "{modifiers}{plus}{btn}{button_number}".format( modifiers=modif_label, plus=separator, btn=mouse_button_label, button_number=button, )
def _grab_pointer_outside(self, device, time): if self._outside_grab_active: logger.warning("grab: outside-popup grab already active: " "regrabbing") self._ungrab_pointer_outside(device, time) event_mask = (Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK) cursor = self._outside_cursor grab_status = device.grab( window=self.get_window(), grab_ownership=Gdk.GrabOwnership.APPLICATION, owner_events=False, event_mask=Gdk.EventMask(event_mask), cursor=cursor, time_=time, ) if grab_status == Gdk.GrabStatus.SUCCESS: logger.debug("grab: acquired grab on %r successfully", device) self._outside_grab_active = True else: logger.warning("grab: failed to acquire grab on %r (status=%s)", device, grab_status.value_nick)
def render_background_cb(self, cr, wd, ht, icon_border=None): self._backend.set_brush_color(*self._hsv) size = self._backend.get_size() pixbuf = GdkPixbuf.Pixbuf.new( GdkPixbuf.Colorspace.RGB, True, 8, size, size, ) arr = gdkpixbuf2numpy(pixbuf) self._backend.render(arr) self._dx = (wd - size) // 2 self._dy = (ht - size) // 2 cr.translate(self._dx, self._dy) Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0) cr.paint()
def to_gdk_color(color): """Convert a UIColor to a Gdk.Color. >>> gcol = to_gdk_color(RGBColor(1,1,1)) >>> gcol.to_string() '#ffffffffffff' """ return Gdk.Color(*[int(c*65535) for c in color.get_rgb()])
def drag_motion_cb(self, widget, context, x, y, time): if not self.dragging_allowed: return False action = None source_widget = Gtk.drag_get_source_widget(context) if self is source_widget: # Only moves are possible action = Gdk.DragAction.MOVE else: # Dragging from another widget, is always a copy action = Gdk.DragAction.COPY Gdk.drag_status(context, action, time) if not self.drag_highlighted: self.drag_highlighted = True self.queue_draw() if self.drag_highlighted: i = self.index(x, y) if i != self.drag_insertion_index: self.queue_draw() self.drag_insertion_index = i
def to_gdk_rgba(color): """Convert to a `GdkRGBA` (with alpha=1.0). >>> col = RGBColor(1,1,1) >>> rgba = to_gdk_rgba(col) >>> rgba.to_string() 'rgb(255,255,255)' """ rgba = list(color.get_rgb()) rgba.append(1.0) return Gdk.RGBA(*rgba)
def local_mouse_state(self, last_update=False): tdw_win = self.tdw.get_window() display = self.tdw.get_display() devmgr = display and display.get_device_manager() or None coredev = devmgr and devmgr.get_client_pointer() or None if coredev and tdw_win: win_, x, y, kbmods = tdw_win.get_device_position_double(coredev) else: x, y, kbmods = (0., 0., Gdk.ModifierType(0)) if last_update: return self.lx, self.ly, kbmods x, y = self.tdw.display_to_model(x, y) return x, y, kbmods
def cairo_request(self): """Access via a temporary Cairo context. Modifications are copied back into the backing pixbuf when the context manager finishes. """ # Make a Cairo surface copy of the subpixbuf surf = cairo.ImageSurface( cairo.FORMAT_ARGB32, self.pixbuf.get_width(), self.pixbuf.get_height(), ) cr = cairo.Context(surf) Gdk.cairo_set_source_pixbuf(cr, self.pixbuf, 0, 0) cr.paint() # User can modify its content with Cairo operations yield cr # Put the modified data back into the tile-aligned pixbuf. dx = self.x - self.ex dy = self.y - self.ey pixbuf = Gdk.pixbuf_get_from_surface(surf, 0, 0, self.w, self.h) pixbuf.copy_area(0, 0, self.w, self.h, self.epixbuf, dx, dy)
def drag_data_get_cb(self, widget, context, selection, target_type, time): """Gets the current color when a drop happens somewhere""" if "application/x-color" not in map(str, context.list_targets()): return False color = self.get_managed_color() data = gui.uicolor.to_drag_data(color) selection.set(Gdk.atom_intern("application/x-color", False), 16, data) logger.debug( "drag-data-get: sending type=%r", selection.get_data_type(), ) logger.debug("drag-data-get: sending fmt=%r", selection.get_format()) logger.debug("drag-data-get: sending data=%r len=%r", selection.get_data(), len(selection.get_data())) return True
def button_press_name(button, mods): """Converts button number & modifier mask to a prefs-storable string. Analogous to `Gtk.accelerator_name()`. Buttonpress names look similar to GDK accelerator names, for example ``<Control><Shift>Button2`` or ``<Primary><Alt>Button4`` for newer versions of GTK. If the button is equal to zero (see `button_press_parse()`), `None` is returned. """ button = int(button) mods = int(mods) if button <= 0: return None mods = Gdk.ModifierType(mods) modif_name = Gtk.accelerator_name(0, mods) return modif_name + "Button%d" % (button, )
def _key_press_cb(self, widget, event): """App-wide keypress handler for toplevel windows.""" # If an input widget has focus - their key handling is prioritized. consumed = widget.propagate_key_event(event) if consumed: return True if not self.enabled: return # Instead of using event.keyval, we do it the lowlevel way. # Reason: ignoring CAPSLOCK and checking if SHIFT was pressed keyval, keyval_lower, accel_label, modifiers = kb.translate( event.hardware_keycode, event.state, event.group) if not keyval: return # Except that key bindings are always stored in lowercase. keyval_lower = Gdk.keyval_to_lower(keyval) if keyval_lower != keyval: modifiers |= Gdk.ModifierType.SHIFT_MASK action = self.keymap.get((keyval_lower, modifiers)) if not action and not self.fallbacks_disabled(): # try hardcoded keys action = self.keymap2.get((keyval_lower, modifiers)) # Don't dispatch if the window is only sensitive to a subset of # actions, and the action is not in that set. if action is not None and isinstance(action, Gtk.Action): win_actions = self.window_actions.get(widget, None) if win_actions is not None: if action.get_name() not in win_actions: return False # If the lookup succeeded, activate the corresponding action. if action: return self.activate_keydown_event(action, event) # Otherwise, dispatch the event to the active doc. return self._dispatch_fallthru_key_press_event(widget, event)
def button_press_parse(name): """Converts button press names to a button number & modifier mask. Analogous to `Gtk.accelerator_parse()`. This function parses the strings created by `button_press_name()`, and returns a 2-tuple containing the button number and modifier mask corresponding to `name`. If the parse fails, both values will be 0 (zero). """ if name is None: return (0, 0) name = str(name) try: mods_s, button_s = name.split("Button", 1) if button_s == '': button = 0 else: button = int(button_s) except ValueError: button = 0 mods = Gdk.ModifierType(0) else: keyval_ignored, mods = Gtk.accelerator_parse(mods_s) return button, mods
def __init__(self, filenames, state_dirs, version, fullscreen=False): """Construct, but do not run. :param list filenames: The list of files to load (unicode required) :param StateDirs state_dirs: static special paths. :param unicode version: Version string for the about dialog. :param bool fullscreen: Go fullscreen after starting. Only the first filename listed will be loaded. If no files are listed, the autosave recovery dialog may be shown when the application starts up. """ assert Application._INSTANCE is None super(Application, self).__init__() Application._INSTANCE = self self.state_dirs = state_dirs #: Static special paths: see StateDirs self.version = version #: version string for the app. # Create the user's config directory and any needed R/W data # storage areas. for basedir in [state_dirs.user_config, state_dirs.user_data]: if not os.path.isdir(basedir): os.makedirs(basedir) logger.info('Created basedir %r', basedir) for datasubdir in [u'backgrounds', u'brushes', u'scratchpads']: datadir = os.path.join(state_dirs.user_data, datasubdir) if not os.path.isdir(datadir): os.mkdir(datadir) logger.info('Created data subdir %r', datadir) _init_icons(state_dirs.app_icons) # Core actions and menu structure ui_dir = os.path.dirname(os.path.abspath(__file__)) resources_xml = join(ui_dir, "resources.xml") self.builder = Gtk.Builder() self.builder.set_translation_domain("mypaint") self.builder.add_from_file(resources_xml) self.ui_manager = self.builder.get_object("app_ui_manager") signal_callback_objs = [self] Gdk.set_program_class('MyPaint') self.pixmaps = PixbufDirectory(join(state_dirs.app_data, u'pixmaps')) self.cursor_color_picker = Gdk.Cursor.new_from_pixbuf( Gdk.Display.get_default(), self.pixmaps.cursor_color_picker, 3, 15, ) self.cursor_color_picker_h = Gdk.Cursor.new_from_pixbuf( Gdk.Display.get_default(), self.pixmaps.cursor_color_picker_h, 3, 15, ) self.cursor_color_picker_c = Gdk.Cursor.new_from_pixbuf( Gdk.Display.get_default(), self.pixmaps.cursor_color_picker_c, 3, 15, ) self.cursor_color_picker_y = Gdk.Cursor.new_from_pixbuf( Gdk.Display.get_default(), self.pixmaps.cursor_color_picker_y, 3, 15, ) self.cursors = gui.cursor.CustomCursorMaker(self) # App-level settings self._preferences = lib.observable.ObservableDict() self.load_settings() # Set up compatibility mode (most of it) self.compat_mode = None self.reset_compat_mode(update=False) compat.update_default_layer_type(self) # Unmanaged main brush. # Always the same instance (we can attach settings_observers). # This brush is where temporary changes (color, size...) happen. self.brush = brush.BrushInfo() self.brush.load_defaults() # Global pressure mapping function, ignored unless set self.pressure_mapping = None # Fake inputs to send when using a mouse. Adjustable # via slider and/or hotkeys self.fakepressure = 0.5 self.fakerotation = 0.5 # Keyboard manager self.kbm = keyboard.KeyboardManager(self) # File I/O self.filehandler = filehandling.FileHandler(self) # Picking grabs self.context_grab = gui.picker.ContextPickingGrabPresenter() self.context_grab.app = self self.color_grab = gui.picker.ColorPickingGrabPresenter() self.color_grab.app = self # Load the main interface mypaint_main_xml = join(ui_dir, "mypaint.glade") self.builder.add_from_file(mypaint_main_xml) # Main drawing window self.drawWindow = self.builder.get_object("drawwindow") signal_callback_objs.append(self.drawWindow) # Workspace widget. Manages layout of toolwindows, and autohide in # fullscreen. wkspace = self.builder.get_object("app_workspace") wkspace.build_from_layout(self.preferences["workspace.layout"]) wkspace.floating_window_created += self._floating_window_created_cb fs_autohide_action = self.builder.get_object("FullscreenAutohide") fs_autohide_action.set_active(wkspace.autohide_enabled) self.workspace = wkspace # Working document: viewer widget app_canvas = self.builder.get_object("app_canvas") # Working document: model and controller cache_size = self.preferences.get('ui.rendered_tile_cache_size', lib.cache.DEFAULT_CACHE_SIZE) default_stack_size = lib.document.DEFAULT_UNDO_STACK_SIZE undo_stack_size = self.preferences.setdefault( 'command.max_undo_stack_size', default_stack_size) undo_stack_size = validation.validate( undo_stack_size, default_stack_size, int, lambda a: a > 0, "The undo stack size ({value}) must be a positive integer!") model = lib.document.Document(self.brush, cache_size=cache_size, max_undo_stack_size=undo_stack_size) self.doc = document.Document(self, app_canvas, model) app_canvas.set_model(model) signal_callback_objs.append(self.doc) signal_callback_objs.append(self.doc.modes) self.scratchpad_filename = "" scratchpad_model = lib.document.Document( self.brush, painting_only=True, cache_size=lib.cache.DEFAULT_CACHE_SIZE / 4) scratchpad_tdw = tileddrawwidget.TiledDrawWidget() scratchpad_tdw.scroll_on_allocate = False scratchpad_tdw.set_model(scratchpad_model) self.scratchpad_doc = document.Document(self, scratchpad_tdw, scratchpad_model) self.brushmanager = brushmanager.BrushManager( lib.config.mypaint_brushdir, join(self.state_dirs.user_data, 'brushes'), self, ) signal_callback_objs.append(self.filehandler) self.brushmodifier = brushmodifier.BrushModifier(self) signal_callback_objs.append(self.brushmodifier) self.blendmodemanager = blendmodehandler.BlendModeManager(self) signal_callback_objs.append(self.blendmodemanager) self.blendmodemanager.register(self.brushmodifier.bm) self.line_mode_settings = linemode.LineModeSettings(self) # Button press mapping self.button_mapping = ButtonMapping() # Monitors pluggings and uses of input device, configures them, # and switches between device-specific brushes. self.device_monitor = gui.device.Monitor(self) if not self.preferences.get("scratchpad.last_opened_scratchpad", None): self.preferences["scratchpad.last_opened_scratchpad"] \ = self.filehandler.get_scratchpad_autosave() self.scratchpad_filename \ = self.preferences["scratchpad.last_opened_scratchpad"] self.brush_color_manager = BrushColorManager(self) self.brush_color_manager.set_picker_cursor(self.cursor_color_picker) self.brush_color_manager.set_data_path(self.datapath) #: Mapping of setting cname to a GtkAdjustment which controls the base #: value of that setting for the app's current brush. self.brush_adjustment = {} self.init_brush_adjustments() # Extend with some fake inputs that act kind of like brush settings self.fake_adjustment = {} # Connect signals defined in resources.xml callback_finder = CallbackFinder(signal_callback_objs) self.builder.connect_signals(callback_finder) self.kbm.start_listening() self.filehandler.doc = self.doc self.filehandler.filename = None Gtk.AccelMap.load(join(self.user_confpath, 'accelmap.conf')) # Load the default background image self.doc.reset_background() # Non-dockable subwindows # Loading is deferred as late as possible self._subwindow_classes = { # action-name: action-class "BackgroundWindow": backgroundwindow.BackgroundWindow, "BrushEditorWindow": brusheditor.BrushEditorWindow, "PreferencesWindow": preferenceswindow.PreferencesWindow, "InputTestWindow": inputtestwindow.InputTestWindow, "BrushIconEditorWindow": brushiconeditor.BrushIconEditorWindow, } self._subwindows = {} # Statusbar init statusbar = self.builder.get_object("app_statusbar") self.statusbar = statusbar context_id = statusbar.get_context_id("transient-message") self._transient_msg_context_id = context_id self._transient_msg_remove_timeout_id = None # Profiling & debug stuff self.profiler = gui.profiling.Profiler() # Show main UI. self.drawWindow.show_all() GLib.idle_add(self._at_application_start, filenames, fullscreen)
def _info(exctyp, value, tb): global exception_dialog_active if exctyp is KeyboardInterrupt: return original_excepthook(exctyp, value, tb) sys.stderr.write(analyse_simple(exctyp, value, tb).getvalue()) if exception_dialog_active: return Gdk.pointer_ungrab(Gdk.CURRENT_TIME) Gdk.keyboard_ungrab(Gdk.CURRENT_TIME) exception_dialog_active = True # Create the dialog dialog = Gtk.MessageDialog(message_type=Gtk.MessageType.WARNING) dialog.set_title(_("Bug Detected")) primary = _( "<big><b>A programming error has been detected.</b></big>" ) secondary = _( "You may be able to ignore this error and carry on working, " "but you should probably save your work soon.\n\n" "Please tell the developers about this using the issue tracker " "if no-one else has reported it yet." ) dialog.set_markup(primary) dialog.format_secondary_text(secondary) dialog.add_button(_(u"Search Tracker…"), RESPONSE_SEARCH) if "-" in lib.meta.MYPAINT_VERSION: # only development and prereleases dialog.add_button(_("Report…"), RESPONSE_REPORT) dialog.set_response_sensitive(RESPONSE_REPORT, False) dialog.add_button(_("Ignore Error"), Gtk.ResponseType.CLOSE) dialog.add_button(_("Quit MyPaint"), RESPONSE_QUIT) # Add an expander with details of the problem to the dialog def expander_cb(expander, *ignore): # Ensures that on deactivating the expander, the dialog is resized down if expander.get_expanded(): dialog.set_resizable(True) else: dialog.set_resizable(False) details_expander = Gtk.Expander() details_expander.set_label(_(u"Details…")) details_expander.connect("notify::expanded", expander_cb) textview = Gtk.TextView() textview.show() textview.set_editable(False) textview.modify_font(Pango.FontDescription("Monospace normal")) sw = Gtk.ScrolledWindow() sw.show() sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(textview) # Set window sizing so that it's always at least 600 pixels wide, and # increases by 300 pixels in height once the details panel is open sw.set_size_request(0, 300) dialog.set_size_request(600, 0) details_expander.add(sw) details_expander.show_all() dialog.get_content_area().pack_start(details_expander, True, True, 0) # Get the traceback and set contents of the details try: trace = analyse(exctyp, value, tb).getvalue() except: try: trace = _("Exception while analyzing the exception.") + "\n" trace += analyse_simple(exctyp, value, tb).getvalue() except: trace = _("Exception while analyzing the exception.") buf = textview.get_buffer() trace = "\n".join(["```python", trace, "```"]) buf.set_text(trace) ## Would be nice to scroll to the bottom automatically, but @#&%*@ #first, last = buf.get_bounds() #buf.place_cursor(last) #mark = buf.get_insert() ##buf.scroll_mark_onscreen() ##textview.scroll_mark_onscreen(buf.get_insert(), 0) #textview.scroll_to_mark(mark, 0.0) # Connect callback and present the dialog dialog.connect('response', _dialog_response_cb, trace, exctyp, value) #dialog.set_modal(True) # this might actually be contra-productive... dialog.show()
def make_preview(thumb, preview_size): """Convert a layer's thumbnail into a nice preview image.""" # Check size check_size = 2 while check_size < (preview_size / 6) and check_size < 16: check_size *= 2 blank = GdkPixbuf.Pixbuf.new( GdkPixbuf.Colorspace.RGB, True, 8, preview_size, preview_size, ) blank.fill(0x00000000) if thumb is None: thumb = blank # Make a square of chex preview = blank.composite_color_simple( dest_width=preview_size, dest_height=preview_size, interp_type=GdkPixbuf.InterpType.NEAREST, overall_alpha=255, check_size=check_size, color1=0xff707070, color2=0xff808080, ) w = thumb.get_width() h = thumb.get_height() scale = preview_size / max(w, h) w *= scale h *= scale x = (preview_size - w) // 2 y = (preview_size - h) // 2 thumb.composite( dest=preview, dest_x=x, dest_y=y, dest_width=w, dest_height=h, offset_x=x, offset_y=y, scale_x=scale, scale_y=scale, interp_type=GdkPixbuf.InterpType.BILINEAR, overall_alpha=255, ) # Add some very minor decorations.. surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, preview_size, preview_size) cr = cairo.Context(surf) Gdk.cairo_set_source_pixbuf(cr, preview, 0, 0) cr.paint() cr.set_source_rgba(1, 1, 1, 0.1) cr.rectangle(0.5, 0.5, preview_size - 1, preview_size - 1) cr.set_line_width(1.0) cr.stroke() surf.flush() preview = Gdk.pixbuf_get_from_surface( surf, 0, 0, preview_size, preview_size, ) return preview
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.items(): 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 _edit_dialog_key_press_cb(self, dialog, event, editable): if event.type != Gdk.EventType.KEY_PRESS: return False if event.is_modifier: return False if self._USE_NORMAL_DIALOG_KEYS: if event.keyval == Gdk.KEY_Return: dialog.response(Gtk.ResponseType.OK) return True elif event.keyval == Gdk.KEY_Escape: dialog.response(Gtk.ResponseType.CANCEL) return True elif event.keyval == Gdk.KEY_BackSpace: dialog.response(Gtk.ResponseType.REJECT) return True # Stolen from GTK 2.24's gtk/gtkmenu.c (gtk_menu_key_press()) # Figure out what modifiers went into determining the key symbol keymap = Gdk.Keymap.get_default() bound, keyval, effective_group, level, consumed_modifiers = ( keymap.translate_keyboard_state( event.hardware_keycode, event.state, # https://github.com/mypaint/mypaint/issues/974 # event.group 1)) keyval = Gdk.keyval_to_lower(keyval) mods = Gdk.ModifierType(event.state & Gtk.accelerator_get_default_mod_mask() & ~consumed_modifiers) # If lowercasing affects the keysym, then we need to include # SHIFT in the modifiers. We re-upper case when we match against # the keyval, but display and save in caseless form. if keyval != event.keyval: mods |= Gdk.ModifierType.SHIFT_MASK accel_label = Gtk.accelerator_get_label(keyval, mods) # So we get (<Shift>j, Shift+J) but just (plus, +). As I # understand it. # This is rejecting some legit key combinations such as the # arrowkeys, so I had to remove it... # if not Gtk.accelerator_valid(keyval, mods) # return True clash_accel_path = None clash_action_label = None for path, kv, m, changed in self._get_accel_map_entries(): if (kv, m) == (keyval, mods): clash_accel_path = path clash_action_label = _udecode( self._action_labels.get( clash_accel_path, # TRANSLATORS: Part of the keybinding dialog, refers # TRANSLATORS: to an action bound to a key combination. _(u"Unknown Action"), )) break if clash_accel_path == dialog.accel_path: # no change self._edit_dialog_set_standard_hint(dialog) label = str(accel_label) dialog.accel_label_widget.set_text(label) elif clash_accel_path: markup_tmpl = _( # TRANSLATORS: Warning message when attempting to assign a # TRANSLATORS: keyboard combination that is already used. u"<b>{accel} is already in use for '{action}'. " u"The existing assignment will be replaced.</b>") markup = markup_tmpl.format( accel=lib.xml.escape(accel_label), action=lib.xml.escape(clash_action_label), ) self._edit_dialog_set_hint(dialog, markup) label = u"%s (replace)" % (accel_label, ) dialog.accel_label_widget.set_text(str(label)) else: self._edit_dialog_set_standard_hint(dialog) label = u"%s (changed)" % (accel_label, ) dialog.accel_label_widget.set_text(label) dialog.result_mods = mods dialog.result_keyval = keyval return True
def rgba_or_none(tup): return (tup is not None) and Gdk.RGBA(*tup) or None
def load_symbolic_icon(icon_name, size, fg=None, success=None, warning=None, error=None, outline=None): """More Pythonic wrapper for gtk_icon_info_load_symbolic() etc. :param str icon_name: Name of the symbolic icon to render :param int size: Pixel size to render at :param tuple fg: foreground color (rgba tuple, values in [0..1]) :param tuple success: success color (rgba tuple, values in [0..1]) :param tuple warning: warning color (rgba tuple, values in [0..1]) :param tuple error: error color (rgba tuple, values in [0..1]) :param tuple outline: outline color (rgba tuple, values in [0..1]) :returns: The rendered symbolic icon :rtype: GdkPixbuf.Pixbuf If the outline color is specified, a single-pixel outline is faked for the icon. Outlined renderings require a size 2 pixels larger than non-outlined if the central icon is to be of the same size. The returned value should be cached somewhere. """ theme = Gtk.IconTheme.get_default() if outline is not None: size -= 2 info = theme.lookup_icon(icon_name, size, Gtk.IconLookupFlags(0)) def rgba_or_none(tup): return (tup is not None) and Gdk.RGBA(*tup) or None icon_pixbuf, was_symbolic = info.load_symbolic( fg=rgba_or_none(fg), success_color=rgba_or_none(success), warning_color=rgba_or_none(warning), error_color=rgba_or_none(error), ) assert was_symbolic if outline is None: return icon_pixbuf result = GdkPixbuf.Pixbuf.new( GdkPixbuf.Colorspace.RGB, True, 8, size + 2, size + 2, ) result.fill(0x00000000) outline_rgba = list(outline) outline_rgba[3] /= 3.0 outline_rgba = Gdk.RGBA(*outline_rgba) outline_stamp, was_symbolic = info.load_symbolic( fg=outline_rgba, success_color=outline_rgba, warning_color=outline_rgba, error_color=outline_rgba, ) w = outline_stamp.get_width() h = outline_stamp.get_height() assert was_symbolic offsets = [ (-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), # noqa: E241 (it's clearer) (-1, 1), (0, 1), (1, 1), ] for dx, dy in offsets: outline_stamp.composite( result, dx + 1, dy + 1, w, h, dx + 1, dy + 1, 1, 1, GdkPixbuf.InterpType.NEAREST, 255, ) icon_pixbuf.composite( result, 1, 1, w, h, 1, 1, 1, 1, GdkPixbuf.InterpType.NEAREST, 255, ) return result
def render_brush_preview_pixbuf(brushinfo, max_edge_tiles=4): """Renders brush preview images :param BrushInfo brushinfo: settings to render :param int max_edge_tiles: Use at most this many tiles along an edge :returns: Preview image, at 128x128 pixels :rtype: GdkPixbuf This generates the preview image (128px icon) used for brushes which don't have saved ones. These include brushes picked from .ORA files where the parent_brush_name doesn't correspond to a brush in the user's MyPaint brushes - they're used as the default, and for the Auto button in the Brush Icon editor. Brushstrokes are inherently unpredictable in size, so the allowable area is grown until the brush fits or until the rendering becomes too big. `max_edge_tiles` limits this growth. """ assert max_edge_tiles >= 1 brushinfo = brushinfo.clone() # avoid capturing a ref brush = Brush(brushinfo) surface = lib.tiledsurface.Surface() n = lib.tiledsurface.N for size_in_tiles in range(1, max_edge_tiles): width = n * size_in_tiles height = n * size_in_tiles surface.clear() fg, spiral = _brush_preview_bg_fg(surface, size_in_tiles, brushinfo) brushinfo.set_color_rgb(fg) brush.reset() # Curve shape = _variable_pressure_scribble(width, height, size_in_tiles) surface.begin_atomic() for dt, x, y, p, xt, yt in shape: brush.stroke_to(surface.backend, x, y, p, xt, yt, dt, 1.0, 0.0, 0.0) surface.end_atomic() # Check rendered size tposs = surface.tiledict.keys() outside = min({tx for tx, ty in tposs}) < 0 outside = outside or (min({ty for tx, ty in tposs}) < 0) outside = outside or (max({tx for tx, ty in tposs}) >= size_in_tiles) outside = outside or (max({ty for tx, ty in tposs}) >= size_in_tiles) if not outside: break # Convert to pixbuf at the right scale rect = (0, 0, width, height) pixbuf = render_as_pixbuf(surface, *rect, alpha=True) if max(width, height) != 128: interp = (GdkPixbuf.InterpType.NEAREST if max(width, height) < 128 else GdkPixbuf.InterpType.BILINEAR) pixbuf = pixbuf.scale_simple(128, 128, interp) # Composite over a checquered bg via Cairo: shows erases size = gui.style.ALPHA_CHECK_SIZE nchecks = int(128 // size) cairo_surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, 128, 128) cr = cairo.Context(cairo_surf) render_checks(cr, size, nchecks) Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0) cr.paint() cairo_surf.flush() return Gdk.pixbuf_get_from_surface(cairo_surf, 0, 0, 128, 128)
def _image_surface_to_pixbuf(surf): """Convert a Cairo surface to a GdkPixbuf""" w = surf.get_width() h = surf.get_height() return Gdk.pixbuf_get_from_surface(surf, 0, 0, w, h)