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 _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 _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 _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 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 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 _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 _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 _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 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 _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 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 _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 _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 _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)
def _page_added_cb(self, notebook, child, page_num): stack = self._toolstack GLib.idle_add(stack._update_structure) # Reinstate the previous size on the divider # if this is the result of dragging a tab out # into a new window. try: size = child.__prev_size except AttributeError: return if self is not stack._get_first_notebook(): return if self.get_n_pages() != 1: return # The size-setting must be done outside the event handler # for it to take effect. w, h = size GLib.idle_add(stack._set_first_paned_position, h)
def build_from_layout(self, layout): """Builds the workspace from a definition dict. :param layout: a layout definition :type layout: dict In order to have any effect, this must be called before the workspace widget is realized, but after it has been packed into its toplevel window. Keys and values in the dict are as follows: * position: an initial window position dict for the toplevel window. See `set_initial_window_position()`. * left_sidebar, right_sidebar: `ToolStack` definition lists. See `Toolstack.build_from_layout()`. * floating: a list of floating window definitions. Each element is a dict with the following keys: - contents: a `ToolStack` definition dict: see above. - position: an initial window position dict: see above. * autohide: whether autohide is enabled when fullscreening. * fullsceen: whether to start in fullscreen mode. * maximized: whether to start maximized. See also `get_layout()`. """ toplevel_win = self.get_toplevel() assert toplevel_win is not None assert toplevel_win is not self assert not toplevel_win.get_visible() # Set initial position and fullscreen state toplevel_pos = layout.get("position", None) if toplevel_pos: set_initial_window_position(toplevel_win, toplevel_pos) if layout.get("fullscreen", False): toplevel_win.fullscreen() GLib.idle_add(lambda *a: toplevel_win.fullscreen()) elif layout.get("maximized", False): toplevel_win.maximize() GLib.idle_add(lambda *a: toplevel_win.maximize()) toplevel_win.connect("window-state-event", self._toplevel_window_state_event_cb) self.autohide_enabled = layout.get("autohide", True) self._initial_layout = layout
def drag_update_cb(self, tdw, event, dx, dy): """UI and model updates during a drag""" if self._cmd: assert tdw is self._drag_active_tdw x, y = tdw.display_to_model(event.x, event.y) self._cmd.move_to(x, y) if self._drag_update_idler_srcid is None: idler = self._drag_update_idler self._drag_update_idler_srcid = GLib.idle_add(idler) return super(LayerMoveMode, self).drag_update_cb(tdw, event, dx, dy)
def update(self, width=None, height=None): """ Redraws the widget from scratch. """ self.total_border = self.border_visible \ + self.spacing_inside + self.spacing_outside self.total_w = self.item_w + 2 * self.total_border self.total_h = self.item_h + 2 * self.total_border if width is None: if not self.pixbuf: return width = self.pixbuf.get_width() height = self.pixbuf.get_height() width = max(width, self.total_w) self.tiles_w = max(1, int(width // self.total_w)) self.tiles_h = max(1, int(ceil(len(self.itemlist) / self.tiles_w))) height = self.tiles_h * self.total_h # self.set_size_request(-1, -1) GLib.idle_add(self.set_size_request, self.total_w, height) self.pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, width, height) self.pixbuf.fill(0xffffff00) # transparent for i, item in enumerate(self.itemlist): x = (i % self.tiles_w) * self.total_w y = (i // self.tiles_w) * self.total_h x += self.total_border y += self.total_border pixbuf = self.pixbuffunc(item) if pixbuf not in self.thumbnails: self.thumbnails[pixbuf] = helpers.pixbuf_thumbnail( pixbuf, self.item_w, self.item_h, ) pixbuf = self.thumbnails[pixbuf] pixbuf.composite(self.pixbuf, x, y, self.item_w, self.item_h, x, y, 1, 1, GdkPixbuf.InterpType.BILINEAR, 255) self.queue_draw()
def drag_stop_cb(self, tdw): """UI and model updates at the end of a drag""" # Stop the update idler running on its next scheduling self._drag_update_idler_srcid = None # This will leave a non-cleaned-up move if one is still active, # so finalize it in its own idle routine. if self._cmd is not None: assert tdw is self._drag_active_tdw # Arrange for the background work to be done, and look busy tdw.set_sensitive(False) window = tdw.get_window() cursor = Gdk.Cursor.new_for_display(window.get_display(), Gdk.CursorType.WATCH) tdw.set_override_cursor(cursor) self.final_modifiers = self.current_modifiers() GLib.idle_add(self._finalize_move_idler) else: # Still need cleanup for tracking state, cursors etc. self._drag_cleanup() return super(LayerMoveMode, self).drag_stop_cb(tdw)
def _map_cb(self, widget): """Window map event actions""" toplevel = None workspace = self.stack.workspace if workspace: toplevel = workspace.get_toplevel() # Things we only need to do on the first window map if not self._mapped_once: self._mapped_once = True if toplevel: self.set_transient_for(toplevel) if workspace: workspace._floating.add(self) win = widget.get_window() decor = (Gdk.WMDecoration.TITLE | Gdk.WMDecoration.BORDER | Gdk.WMDecoration.RESIZEH) win.set_decorations(decor) wmfuncs = Gdk.WMFunction.RESIZE | Gdk.WMFunction.MOVE win.set_functions(wmfuncs) # Hack to force an initial x,y position to be what was saved, used # as a workaround for WM bugs and misfeatures. # Forcing the position up front rather than in an idle handler # avoids flickering in Xfce 4.8, when this is necessary. # Xfce 4.8 requires position forcing for second and subsequent # map events too, if a window has been resized due its content growing. # Hopefully we never have to do this twice. Once is too much really. if self._onmap_position is not None: if self._AGGRESSIVE_POSITIONING_HACK: self._set_onmap_position(False) GLib.idle_add(self._set_onmap_position, True) else: self._set_onmap_position(True) # Prevent subwindows from taking keyboard focus from the main window # in Metacity by presenting it again. https://gna.org/bugs/?17899 # Still affects GNOME 3.14. # https://github.com/mypaint/mypaint/issues/247 if toplevel: GLib.idle_add(lambda *a: toplevel.present())
def _queue_live_update(self): """Queues a single live update of the most recent brushstroke""" # Not if already queued, or if disabled if self._live_update_idle_cb_id: return if not self._live_update_enabled: return # Only in live-updatable modes. # Currently: only FreehandMode supports this. It could potentially # work with other modes: please test! if not getattr(self.app.doc.modes.top, "IS_LIVE_UPDATEABLE", False): return cbid = GLib.idle_add(self._live_update_idle_cb) self._live_update_idle_cb_id = cbid
def add_work(self, func, *args, **kwargs): """Adds work :param func: a task callable. :param *args: passed to func :param **kwargs: passed to func This starts the queue running if it isn't already. Each callable will be called with the given parameters until it returns false, at which point it's discarded. """ if not self._idle_id: self._idle_id = GLib.idle_add( self._process, priority=self._priority, ) self._queue.append((func, args, kwargs))
def _in_grab_motion_cb(self, widget, event): assert self._grabbed_pointer_dev is not None if not self._check_event_devices_still_grabbed(event): return True if event.device is not self._grabbed_pointer_dev: return False if not self._grab_button_num: return False # Due to a performance issue, picking can take more time # than we have between two motion events (about 8ms). if self._delayed_picking_update_id: GLib.source_remove(self._delayed_picking_update_id) self._delayed_picking_update_id = GLib.idle_add( self._delayed_picking_update_cb, event.device, event.x_root, event.y_root, ) return True
def composite(handler, fill_args, trim_result, filled, tiles_bbox, dst): """Composite the filled tiles into the destination surface""" handler.set_stage(handler.COMPOSITE, len(filled)) fill_col = fill_args.color # Prepare opaque color rgba tile for copying full_rgba = myplib.rgba_tile_from_alpha_tile( _FULL_TILE, *(fill_col + (0, 0, N - 1, N - 1))) # Bounding box of tiles that need updating dst_changed_bbox = None dst_tiles = dst.get_tiles() skip_empty_dst = fill_args.skip_empty_dst() mode = fill_args.mode lock_alpha = fill_args.lock_alpha opacity = fill_args.opacity tile_combine = myplib.tile_combine # Composite filled tiles into the destination surface for tile_coord, src_tile in iteritems(filled): if not handler.run: break handler.inc_processed() # Omit tiles outside of the bounding box _if_ the frame is enabled # Note:filled tiles outside bbox only originates from dilation/blur if trim_result and tiles_bbox.outside(tile_coord): continue # Skip empty destination tiles for erasing and alpha locking # Avoids completely unnecessary tile allocation and copying if skip_empty_dst and tile_coord not in dst_tiles: continue with dst.tile_request(*tile_coord, readonly=False) as dst_tile: # Only at this point might the bounding box need to be updated dst_changed_bbox = update_bbox(dst_changed_bbox, *tile_coord) # Under certain conditions, direct copies and dict manipulation # can be used instead of compositing operations. cut_off = trim_result and tiles_bbox.crossing(tile_coord) full_inner = src_tile is _FULL_TILE and not cut_off if full_inner: if mode == myplib.CombineNormal and opacity == 1.0: myplib.tile_copy_rgba16_into_rgba16(full_rgba, dst_tile) continue elif mode == myplib.CombineDestinationOut and opacity == 1.0: dst_tiles.pop(tile_coord) continue elif mode == myplib.CombineDestinationIn and opacity == 1.0: continue # Even if opacity != 1.0, we can reuse the full rgba tile src_tile_rgba = full_rgba else: if trim_result: tile_bounds = tiles_bbox.tile_bounds(tile_coord) else: tile_bounds = (0, 0, N - 1, N - 1) src_tile_rgba = myplib.rgba_tile_from_alpha_tile( src_tile, *(fill_col + tile_bounds)) # If alpha locking is enabled in combination with a mode other than # CombineNormal, we need to copy the dst tile to mask the result if lock_alpha and mode != myplib.CombineSourceAtop: mask = np.copy(dst_tile) mask_mode = myplib.CombineDestinationAtop tile_combine(mode, src_tile_rgba, dst_tile, True, opacity) tile_combine(mask_mode, mask, dst_tile, True, 1.0) else: tile_combine(mode, src_tile_rgba, dst_tile, True, opacity) # Handle dst-out and dst-atop: clear untouched tiles if mode in [myplib.CombineDestinationIn, myplib.CombineDestinationAtop]: for tile_coord in list(dst_tiles.keys()): if not handler.run: break if tile_coord not in filled: dst_changed_bbox = update_bbox(dst_changed_bbox, *tile_coord) with dst.tile_request(*tile_coord, readonly=False): dst_tiles.pop(tile_coord) if dst_changed_bbox and handler.run: min_tx, min_ty, max_tx, max_ty = dst_changed_bbox bbox = ( min_tx * N, min_ty * N, (1 + max_tx - min_tx) * N, (1 + max_ty - min_ty) * N, ) # Even for large fills on slow machines, this stage # will almost always be too short to even notice. # It is not cancellable once entered. handler.set_stage(FillHandler.FINISHING) # The observers may directly or indirectly use the # Gtk API, so the call is scheduled on the gui thread. GLib.idle_add(dst.notify_observers, *bbox)
def _start_task_queue_runner(self): """Begin processing the task queue, if not already going""" if self._task_queue_runner_id is not None: return idler_id = GLib.idle_add(self._task_queue_runner_cb) self._task_queue_runner_id = idler_id
def _preview_area_modified_cb(self, preview_model, x, y, w, h): """Handles changes made to the preview canvas""" self._preview_modified = True GLib.idle_add(self._update_widgets)
def build_from_layout(self, desc, init_sizes_state=None): """Loads groups and pages from a layout description :param desc: stack definition :type desc: dict :param init_sizes_state: toplevel window state transition on which to set the group dividers' initial positions. If left unset, set the sizes immediately. :type init_sizes_state: Gdk.WindowState :rtype: int :returns: the number of groups added The `desc` parameter has the following keys and values: * w: integer width (ignored here) * h: integer height (ignored here) * groups: list of group definitions - see below Width and height may be of relevance to the parent widget, but are not consumed by this method. `get_layout()` writes them, however. Each group definition is a dict with the following keys and values. * tools: a list of tool definitions - see below * h: integer height: used here to set the height of the group * w: integer width (ignored here) Each tool definition is a tuple of the form (GTYPENAME,*CONSTRUCTARGS). GTYPENAME is a string containing a GType name which is used for finding and constructing the tool instance. CONSTRUCTARGS is currently ignored. """ next_nb = self._get_first_notebook() factory = self.workspace._tool_widgets num_groups_added = 0 for group_desc in desc.get("groups", []): assert next_nb.get_n_pages() == 0 # Only add unique tool widgets. Assume this is being called on # startup, with an initially empty factory cache. tool_widgets = [] for tool_desc in group_desc.get("tools", []): if factory.cache_has(*tool_desc): logger.warning("Duplicate entry %r ignored", tool_desc) continue logger.debug("build_from_layout: building tool %r", tool_desc) try: tool_widget = factory.get(*tool_desc) tool_widgets.append(tool_widget) except objfactory.ConstructError as ex: logger.error("build_from_layout: %s", ex.message) # Group might be empty if construction fails or if everything's a # duplicate. if not tool_widgets: logger.debug("Empty tab group in workspace, not added") continue # We have something to add, so create a new Notebook with the # pages, and move the insert ref nb = next_nb next_nb = self._append_new_placeholder(nb) for tool_widget in tool_widgets: nb.append_tool_widget_page(tool_widget) if self.workspace: GLib.idle_add( self.workspace.tool_widget_added, tool_widget, ) active_page = group_desc.get("active_page", -1) nb.set_current_page(active_page) num_groups_added += 1 # Position the divider between the new notebook and the next. group_min_h = 1 group_h = int(group_desc.get("h", group_min_h)) group_h = max(group_min_h, group_h) nb_parent = nb.get_parent() assert isinstance(nb_parent, ToolStack._Paned) nb_parent._initial_divider_position = group_h return num_groups_added
def _page_removed_cb(self, notebook, child, page_num): GLib.idle_add(self._toolstack._update_structure)