def unmap_cb(self, *junk): GLib.source_remove(self._timer_id) self.app.doc.tdw.disconnect_by_func(self.event_cb) self.app.drawWindow.disconnect_by_func(self.event_cb) logger.info('Event statistics disabled.')
def _build_ui(self): """Builds the UI from ``brusheditor.glade``""" ui_dir = os.path.dirname(os.path.abspath(__file__)) ui_path = os.path.join(ui_dir, self._UI_DEFINITION_FILE) with open(ui_path, 'r') as ui_fp: ui_xml = ui_fp.read() self._builder.add_from_string(ui_xml) self._populate_inputs(ui_xml) self._populate_settings_treestore() self._builder.connect_signals(self) for inp in brushsettings.inputs: grid = self._builder.get_object("by%s_curve_grid" % inp.name) GLib.idle_add(grid.hide) curve = self._builder.get_object("by%s_curve" % inp.name) def _curve_changed_cb(curve, i=inp): self._update_brush_from_input_widgets(i) curve.changed_cb = _curve_changed_cb btn = self._builder.get_object("by%s_reset_button" % inp.name) btn.connect("clicked", self.input_adj_reset_button_clicked_cb, inp) # Certain actions must be coordinated via a real app instance if not self.app: action_buttons = [ "clone_button", "rename_button", "edit_icon_button", "delete_button", "live_update_checkbutton", "save_button" ] for b_name in action_buttons: w = self._builder.get_object(b_name) w.set_sensitive(False)
def _delete_cb(self, widget): """Properties dialog delete callback""" self._dialog.hide() name = brushmanager.translate_group_name(self._group) msg = C_( "brush group delete", u"Really delete group \u201C{group_name}\u201D?", ).format(group_name=name, ) bm = self._app.brushmanager if not dialogs.confirm(self, msg): return bm.delete_group(self._group) if self._group not in bm.groups: GLib.idle_add( self._remove_panel_idle_cb, self.__gtype_name__, (self._group, ), ) return # Special groups like "Deleted" cannot be deleted, # but the error message is very confusing in that case... msg = C_( "brush group delete", u"Could not delete group \u201C{group_name}\u201D.\n" u"Some special groups cannot be deleted.", ).format(group_name=name, ) dialogs.error(self, msg)
def _cancel_autoreveal_timeout(self): """Cancels any pending auto-reveal""" if not self._autoreveal_timeout: return for timer in self._autoreveal_timeout: GLib.source_remove(timer) self._autoreveal_timeout = []
def _complete_initial_layout(self): """Finish initial layout; called after toplevel win is positioned""" # Init tool group sizes by setting vpaned positions for paned in self._get_paneds(): if paned._initial_divider_position: pos = paned._initial_divider_position GLib.idle_add(paned.set_position, pos)
def activate_from_button_event(self, event): """Activate during handling of a GdkEventButton (press/release) If the event is a button press, then the grab will start immediately, begin updating immediately, and will terminate by the release of the initiating button. If the event is a button release, then the grab start will be deferred to start in an idle handler. When the grab starts, it won't begin updating until the user clicks button 1 (and only button 1), and it will only be terminated with a button1 release. This covers the case of events delivered to "clicked" signal handlers """ if event.type == Gdk.EventType.BUTTON_PRESS: logger.debug("Starting picking grab") has_button_info, button_num = event.get_button() if not has_button_info: return self._start_grab(event.device, event.time, button_num) elif event.type == Gdk.EventType.BUTTON_RELEASE: logger.debug("Queueing picking grab") GLib.idle_add( self._start_grab, event.device, event.time, None, )
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 target(self, targ): inkmode, cn_idx = targ inkmode_ref = None if inkmode: inkmode_ref = weakref.ref(inkmode) self._target = (inkmode_ref, cn_idx) GLib.idle_add(self._update_ui_for_current_target)
def stop(self): """Immediately stop processing and clear the queue.""" if self._idle_id: GLib.source_remove(self._idle_id) self._idle_id = None self._queue.clear() assert self._idle_id is None assert len(self._queue) == 0
def _post_show_cb(self, widget): # Ensure the tree selection matches the root stack's current layer. self._update_selection() # Match the flag column widths to the name column's height. # This only makes sense after the 1st text layout, sadly. GLib.idle_add(self._sizeify_flag_columns) return False
def _stop_task_queue_runner(self, complete=True): """Halts processing of the task queue, and clears it""" if self._task_queue_runner_id is None: return if complete: for (callback, args, kwargs) in self._task_queue: callback(*args, **kwargs) self._task_queue.clear() GLib.source_remove(self._task_queue_runner_id) self._task_queue_runner_id = None
def _sidebar_swap_button_clicked_cb(self, button): """Switch the current page's sidebar ("clicked" event handler) Ultimately fires the tool_widget_removed() and tool_widget_added() @events of the owning workspace. """ page_num = self.get_current_page() page = self.get_nth_page(page_num) if page is not None: GLib.idle_add(self._deferred_swap_tool_widget_sidebar, page)
def _popup_leave_notify_cb(self, widget, event): if not self.active: return # allow to leave the window for a short time if self._outside_popup_timeout_id: GLib.source_remove(self._outside_popup_timeout_id) self._outside_popup_timeout_id = None self._outside_popup_timeout_id = GLib.timeout_add( int(1000 * self.outside_popup_timeout), self._outside_popup_timeout_cb, )
def _close_button_clicked_cb(self, button): """Remove the current page (close button "clicked" event callback) Ultimately fires the ``tool_widget_removed()`` @event of the owning workspace. """ page_num = self.get_current_page() page = self.get_nth_page(page_num) if page is not None: GLib.idle_add(self._deferred_remove_tool_widget, page)
def _set_input_expanded(self, inp, expand, scroll=True): getobj = self._builder.get_object arrow = getobj("by%s_expander_arrow" % (inp.name, )) grid = getobj("by%s_curve_grid" % (inp.name, )) if expand: arrow.set_property("arrow-type", Gtk.ArrowType.DOWN) grid.show_all() if scroll: GLib.idle_add(self._scroll_setting_editor, grid) else: arrow.set_property("arrow-type", Gtk.ArrowType.RIGHT) grid.hide()
def show_transient_message(self, text, seconds=5): """Display a brief, impermanent status message""" context_id = self._transient_msg_context_id self.statusbar.remove_all(context_id) self.statusbar.push(context_id, unicode(text)) timeout_id = self._transient_msg_remove_timeout_id if timeout_id is not None: GLib.source_remove(timeout_id) timeout_id = GLib.timeout_add_seconds( interval=seconds, function=self._transient_msg_remove_timer_cb, ) self._transient_msg_remove_timeout_id = timeout_id
def run(): logger.debug('user_datapath: %r', userdatapath) logger.debug('user_confpath: %r', userconfpath) # User-configured locale (if enabled by user) set_user_configured_locale(userconfpath) # Locale setting from lib.gettext_setup import init_gettext init_gettext(localepath) # mypaintlib import is performed first in gui.application now. from gui import application app_state_dirs = application.StateDirs( app_data=datapath, app_icons=iconspath, user_data=userdatapath, user_config=userconfpath, ) app = application.Application( filenames=args, state_dirs=app_state_dirs, version=version, fullscreen=options.fullscreen, ) # Gtk must not be imported before init_gettext # has been run - else locales will not be set # up properly (e.g: left-to-right interfaces for right-to-left scripts) # Note that this is not the first import of Gtk in the __program__; # it is imported indirectly via the import of gui.application from lib.gibindings import Gtk settings = Gtk.Settings.get_default() dark = app.preferences.get("ui.dark_theme_variant", True) settings.set_property("gtk-application-prefer-dark-theme", dark) if debug and options.run_and_quit: from lib.gibindings import GLib GLib.timeout_add(1000, lambda *a: Gtk.main_quit()) else: from gui import gtkexcepthook func = app.filehandler.confirm_destructive_action gtkexcepthook.quit_confirmation_func = func # temporary workaround for gtk3 Ctrl-C bug: # https://bugzilla.gnome.org/show_bug.cgi?id=622084 import signal signal.signal(signal.SIGINT, signal.SIG_DFL) Gtk.main()
def add_tool_widget(self, widget, maxnotebooks=None, maxpages=3): """Tries to find space for, then add and show a tool widget Finding space is based on constraints, adjustable via the parameters. The process is driven by `Workspace.add_tool_widget()`. :param widget: the widget that needs adoption. :type widget: Gtk.Widget created by the Workspace's factory. :param maxnotebooks: never make more than this many groups in the stack :type maxnotebooks: int :param maxpages: never make more than this many pages in a group :type maxpages: int :return: whether space was found for the widget :rtype: bool The idea is to try repeatedly with gradually relaxing constraint parameters across all stacks in the system until space is found somewhere. """ # Try to find a notebook with few enough pages. # It might be the zero-pages placeholder on the end. target_notebook = None notebooks = self._get_notebooks() assert len(notebooks) > 0, ( "There should always be at least one Notebook widget " "in any ToolStack.") for nb in notebooks: if nb.get_n_pages() < maxpages: target_notebook = nb break # The placeholder tends to be recreated in idle routines, # so it may not be present on the end of the stack just yet. if target_notebook is None: assert nb.get_n_pages() > 0 new_placeholder = nb.split_former_placeholder() target_notebook = new_placeholder # Adding a page to the placeholder would result in a split # in the idle routine later. Check constraint now. if maxnotebooks and (target_notebook.get_n_pages() == 0): n_populated_notebooks = len( [n for n in notebooks if n.get_n_pages() > 0]) if n_populated_notebooks >= maxnotebooks: return False # We're good to go. target_notebook.append_tool_widget_page(widget) if self.workspace: GLib.idle_add(self.workspace.tool_widget_added, widget) return True
def init_user_dir_caches(): """Caches the GLib user directories >>> init_user_dir_caches() The first time this module is imported is from a particular point in the launch script, after all the i18n setup is done and before lib.mypaintlib is imported. If they're not cached up-front in this manner, get_user_config_dir() & friends may return literal "?"s in place of non-ASCII characters (Windows systems with non-ASCII user profile dirs are known to trigger this). The debugging prints may be useful too. """ logger.debug("Init g_get_user_config_dir(): %r", get_user_config_dir()) logger.debug("Init g_get_user_data_dir(): %r", get_user_data_dir()) logger.debug("Init g_get_user_cache_dir(): %r", get_user_cache_dir()) # It doesn't matter if some of these are None for i in range(GLib.UserDirectory.N_DIRECTORIES): k = GLib.UserDirectory(i) logger.debug( "Init g_get_user_special_dir(%s): %r", k.value_name, get_user_special_dir(k), )
def _queue_movement(self, zone, args): self._move_item = (zone, args) if not self._move_timeout_id: self._move_timeout_id = GLib.timeout_add( interval=16.66, # 60 fps cap function=self._do_move, )
def _value_changed_cb(self, adj, prefs_key): # Direct GtkAdjustment callback for a single adjustment being changed. value = float(adj.get_value()) self.app.preferences[prefs_key] = value self._changed_settings.add(prefs_key) if self._idle_srcid is None: self._idle_srcid = GLib.idle_add(self._values_changed_idle_cb)
def drag_update_cb(self, tdw, event, ev_x, ev_y, dx, dy): if self._line_possible: self.update_position(ev_x, ev_y) if self.idle_srcid is None: self.idle_srcid = GLib.idle_add(self._drag_idle_cb) return super(LineModeBase, self).drag_update_cb(tdw, event, ev_x, ev_y, dx, dy)
def startfile(filepath, operation="open"): """os.startfile / g_app_info_launch_default_for_uri compat This has the similar semantics to os.startfile, where it's supported: it launches the given file or folder path with the default app. On Windows, operation can be set to "edit" to use the default editor for a file. The operation parameter is ignored on other systems, and GIO's equivalent routine is used. The relevant app is started in the background, and there are no means for getting its pid. """ try: if os.name == 'nt': os.startfile(filepath, operation) # raises: WindowsError else: uri = GLib.filename_to_uri(filepath) Gio.app_info_launch_default_for_uri(uri, None) # raises: GError return True except Exception: logger.exception( "Failed to launch the default application for %r (op=%r)", filepath, operation, ) return False
def _queue_color_change(self, new_col, cm): self._queued_data = new_col, cm if not self._timeout_id: self._timeout_id = GLib.timeout_add( interval=16.66, # 60 fps cap function=self._change_color, )
def _values_changed_idle_cb(self): # Aggregate, idle-state callback for multiple adjustments being changed # in a single event. Queues redraws, and runs observers. The curve sets # multiple settings at once, and we might as well not queue too many # redraws. if self._idle_srcid is not None: current_mode = self.app.doc.modes.top if isinstance(current_mode, LineModeBase): # Redraw last_line when settings are adjusted # in the adjustment Curve GLib.idle_add(current_mode.redraw_line_cb) for func in self.observers: func(self._changed_settings) self._changed_settings = set() self._idle_srcid = None return False
def _realize_cb(self, widget): """Kick off the deferred layout code when the widget is realized""" # Set up monitoring of the toplevel's size changes. toplevel = self.get_toplevel() toplevel.connect("configure-event", self._toplevel_configure_cb) # Do the initial layout layout = self._initial_layout if layout is None: return llayout = layout.get("left_sidebar", {}) if llayout: logger.debug("Left sidebar: building from saved layout...") num_added_l = self._lstack.build_from_layout(llayout) logger.debug("Left sidebar: added %d group(s)", num_added_l) rlayout = layout.get("right_sidebar", {}) if rlayout: logger.debug("Right sidebar: building from saved layout...") num_added_r = self._rstack.build_from_layout(rlayout) logger.debug("Right sidebar: added %d group(s)", num_added_r) # Floating windows for fi, flayout in enumerate(layout.get("floating", [])): logger.debug( "Floating window %d: building from saved layout...", fi, ) win = ToolStackWindow(self) self.floating_window_created(win) num_added_f = win.build_from_layout(flayout) logger.debug( "Floating window %d: added %d group(s)", fi, num_added_f, ) # The populated ones are only revealed after their # floating_window_created have had a chance to run. if num_added_f > 0: self._floating.add(win) GLib.idle_add(win.show_all) else: logger.warning( "Floating window %d is initially unpopulated. " "Destroying it.", fi, ) GLib.idle_add(win.destroy)
def _toplevel_configure_cb(self, toplevel, event): """Record the toplevel window's position ("configure-event" callback) """ # Avoid saving fullscreen positions. The timeout is a bit of hack, but # it's necessary because the state change event and the configure event # when fullscreening don't have a sensible order. w, h = event.width, event.height srcid = self._save_toplevel_pos_timeout if srcid: GLib.source_remove(srcid) srcid = GLib.timeout_add( 250, self._save_toplevel_pos_timeout_cb, w, h, ) self._save_toplevel_pos_timeout = srcid
def _restart_autoleave_timeout(self): if not self.autoleave_timeout: return self._stop_autoleave_timeout() self._autoleave_timeout_id = GLib.timeout_add( int(1000 * self.autoleave_timeout), self._autoleave_timeout_cb, )
def _restart_outside_popup_timeout(self): if not self.outside_popup_timeout: return self._stop_outside_popup_timeout() self._outside_popup_timeout_id = GLib.timeout_add( int(1000 * self.outside_popup_timeout), self._outside_popup_timeout_cb, )
def _first_alloc_cb(self, widget, alloc): """Try to allocate child widgets their natural size when alloced. """ # Normally, if child widgets declare a real minimum size then in a # structure like this they'll be allocated their minimum size even # when there's enough space to give them their natural size. As a # workaround, set the bar position on the very first size-allocate # event to the best compromise we can calculate. # Child natural and minimum heights. c1 = self.get_child1() c2 = self.get_child2() if not (c1 and c2): return c1min, c1nat = c1.get_preferred_height_for_width(alloc.width) c2min, c2nat = c2.get_preferred_height_for_width(alloc.width) # Disconnect the handler; only run the 1st time. self.disconnect(self._first_alloc_id) self._first_alloc_id = None # If ToolStack.build_from_layout set an initial position, # then code elsewhere will be allocating a size. if self._initial_divider_position is not None: return # Get handle size handle_size = GObject.Value() handle_size.init(int) handle_size.set_int(12) # conservative initial guess self.style_get_property("handle-size", handle_size) bar_height = handle_size.get_int() # Strategy here is to try and give one child widget its natural # size first, slightly favouring the first (top) child. We # could be more egalitarian by inspecting the deep structure. pos = -1 if c1nat + c2min <= alloc.height - bar_height: pos = c1nat elif c1min + c2nat <= alloc.height - bar_height: pos = alloc.height - c2nat - bar_height elif c1min + c2min <= alloc.height - bar_height: pos = alloc.height - c2min - bar_height # The position setting must be done outside this handler # or it'll look weird. GLib.idle_add(self.set_position, pos)