/* Copyright 2016 Software Freedom Conservancy Inc. * * This software is licensed under the GNU LGPL (version 2.1 or later). * See the COPYING file in this distribution. */ public class InjectionGroup { public class Element { public enum ItemType { MENUITEM, MENU, SEPARATOR } public string name; public string action; public string? accelerator; public ItemType kind; public Element(string name, string? action, string? accelerator, ItemType kind) { this.name = name; this.action = action != null ? action : name; this.accelerator = accelerator; this.kind = kind; } } private string path; private Gee.ArrayList elements = new Gee.ArrayList(); private int separator_id = 0; public InjectionGroup(string path) { this.path = path; } public string get_path() { return path; } public Gee.List get_elements() { return elements; } public void add_menu_item(string name, string? action = null, string? accelerator = null) { elements.add(new Element(name, action, accelerator, Element.ItemType.MENUITEM)); } public void add_menu(string name, string? action = null) { elements.add(new Element(name, action, null, Element.ItemType.MENU)); } public void add_separator() { elements.add(new Element("%d-separator".printf(separator_id++), null, null, Element.ItemType.SEPARATOR)); } } public abstract class Page : Gtk.ScrolledWindow { private const int CONSIDER_CONFIGURE_HALTED_MSEC = 400; protected Gtk.Builder builder = new Gtk.Builder (); protected Gtk.Toolbar toolbar; protected bool in_view = false; private string page_name; private ViewCollection view = null; private Gtk.Window container = null; private string toolbar_path; private Gdk.Rectangle last_position = Gdk.Rectangle(); private Gtk.Widget event_source = null; private ulong last_configure_ms = 0; private bool report_move_finished = false; private bool report_resize_finished = false; private Gdk.Point last_down = Gdk.Point(); private bool is_destroyed = false; private bool ctrl_pressed = false; private bool alt_pressed = false; private bool shift_pressed = false; private bool super_pressed = false; private Gdk.CursorType last_cursor = Gdk.CursorType.LEFT_PTR; private bool cursor_hidden = false; private int cursor_hide_msec = 0; private uint last_timeout_id = 0; private int cursor_hide_time_cached = 0; private bool are_actions_attached = false; private OneShotScheduler? update_actions_scheduler = null; protected double wheel_factor = 0.0; protected double modified_wheel_factor = 1.0; protected Page(string page_name) { this.page_name = page_name; view = new ViewCollection("ViewCollection for Page %s".printf(page_name)); last_down = { -1, -1 }; set_can_focus(true); popup_menu.connect(on_context_keypress); realize.connect(attach_view_signals); } ~Page() { #if TRACE_DTORS debug("DTOR: Page %s", page_name); #endif } // This is called by the page controller when it has removed this page ... pages should override // this (or the signal) to clean up public override void destroy() { if (is_destroyed) return; // untie signals detach_event_source(); detach_view_signals(); view.close(); // remove refs to external objects which may be pointing to the Page clear_container(); if (toolbar != null) toolbar.destroy(); // halt any pending callbacks if (update_actions_scheduler != null) update_actions_scheduler.cancel(); is_destroyed = true; base.destroy(); debug("Page %s Destroyed", get_page_name()); } public string get_page_name() { return page_name; } public virtual void set_page_name(string page_name) { this.page_name = page_name; } public string to_string() { return page_name; } public ViewCollection get_view() { return view; } public Gtk.Window? get_container() { return container; } public virtual void set_container(Gtk.Window container) { assert(this.container == null); this.container = container; } public virtual void clear_container() { container = null; } public void set_event_source(Gtk.Widget event_source) { assert(this.event_source == null); this.event_source = event_source; event_source.set_can_focus(true); // interested in mouse button and motion events on the event source event_source.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK | Gdk.EventMask.BUTTON_MOTION_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK | Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK); event_source.button_press_event.connect(on_button_pressed_internal); event_source.button_release_event.connect(on_button_released_internal); event_source.motion_notify_event.connect(on_motion_internal); event_source.leave_notify_event.connect(on_leave_notify_event); event_source.scroll_event.connect(on_mousewheel_internal); event_source.realize.connect(on_event_source_realize); } private void detach_event_source() { if (event_source == null) return; event_source.button_press_event.disconnect(on_button_pressed_internal); event_source.button_release_event.disconnect(on_button_released_internal); event_source.motion_notify_event.disconnect(on_motion_internal); event_source.leave_notify_event.disconnect(on_leave_notify_event); event_source.scroll_event.disconnect(on_mousewheel_internal); event_source = null; } public Gtk.Widget? get_event_source() { return event_source; } private bool menubar_injected = false; public GLib.MenuModel get_menubar() { var model = builder.get_object ("MenuBar") as GLib.Menu; if (model == null) { return new GLib.Menu(); } if (!menubar_injected) { // Collect injected UI elements and add them to the UI manager InjectionGroup[] injection_groups = init_collect_injection_groups(); foreach (InjectionGroup group in injection_groups) { var items = model.get_n_items (); for (int i = 0; i < items; i++) { var submenu = model.get_item_link (i, GLib.Menu.LINK_SUBMENU); var section = this.find_extension_point (submenu, group.get_path ()); if (section == null) { continue; } foreach (var element in group.get_elements ()) { var menu = section as GLib.Menu; switch (element.kind) { case InjectionGroup.Element.ItemType.MENUITEM: var item = new GLib.MenuItem (element.name, "win." + element.action); if (element.accelerator != null) { item.set_attribute ("accel", "s", element.accelerator); } menu.append_item (item); break; default: break; } } } } this.menubar_injected = true; } return model; } public virtual Gtk.Toolbar get_toolbar() { if (toolbar == null) { toolbar = toolbar_path == null ? new Gtk.Toolbar() : builder.get_object (toolbar_path) as Gtk.Toolbar; toolbar.get_style_context().add_class("bottom-toolbar"); // for elementary theme toolbar.set_icon_size(Gtk.IconSize.SMALL_TOOLBAR); } return toolbar; } public virtual Gtk.Menu? get_page_context_menu() { return null; } public virtual void switching_from() { in_view = false; //remove_actions(AppWindow.get_instance()); var map = get_container() as GLib.ActionMap; if (map != null) { remove_actions(map); } if (toolbar_path != null) toolbar = null; } public virtual void switched_to() { in_view = true; add_ui(); var map = get_container() as GLib.ActionMap; if (map != null) { add_actions(map); } int selected_count = get_view().get_selected_count(); int count = get_view().get_count(); init_actions(selected_count, count); update_actions(selected_count, count); update_modifiers(); } public virtual void ready() { } public bool is_in_view() { return in_view; } public virtual void switching_to_fullscreen(FullscreenWindow fsw) { add_actions(fsw); } public virtual void returning_from_fullscreen(FullscreenWindow fsw) { remove_actions(fsw); switched_to(); } public GLib.Action? get_action (string name) { GLib.ActionMap? map = null; if (container is FullscreenWindow) { map = container as GLib.ActionMap; } else { map = AppWindow.get_instance () as GLib.ActionMap; } if (map != null) { return map.lookup_action(name); } return null; } public void set_action_sensitive(string name, bool sensitive) { GLib.SimpleAction? action = get_action(name) as GLib.SimpleAction; if (action != null) action.set_enabled (sensitive); } public void set_action_details(string name, string? label, string? tooltip, bool sensitive) { GLib.SimpleAction? action = get_action(name) as GLib.SimpleAction; if (action == null) return; if (label != null) this.update_menu_item_label (name, label); action.set_enabled (sensitive); } public void activate_action(string name) { var action = get_action(name); if (action != null) action.activate (null); } public GLib.Action? get_common_action(string name, bool log_warning = true) { var action = get_action (name); if (action != null) return action; if (log_warning) warning("Page %s: Unable to locate common action %s", get_page_name(), name); return null; } public void set_common_action_sensitive(string name, bool sensitive) { var action = get_common_action(name) as GLib.SimpleAction; if (action != null) action.set_enabled (sensitive); } public void set_common_action_label(string name, string label) { debug ("Trying to set common action label for %s", name); } public void set_common_action_important(string name, bool important) { debug ("Setting action to important: %s", name); } public void activate_common_action(string name) { var action = get_common_action(name) as GLib.SimpleAction; if (action != null) action.activate(null); } public bool get_ctrl_pressed() { return ctrl_pressed; } public bool get_alt_pressed() { return alt_pressed; } public bool get_shift_pressed() { return shift_pressed; } public bool get_super_pressed() { return super_pressed; } protected void set_action_active (string name, bool active) { var action = get_action (name) as GLib.SimpleAction; if (action != null) { action.set_state (active); } } private bool get_modifiers(out bool ctrl, out bool alt, out bool shift, out bool super) { if (AppWindow.get_instance().get_window() == null) { ctrl = false; alt = false; shift = false; super = false; return false; } int x, y; Gdk.ModifierType mask; var seat = Gdk.Display.get_default().get_default_seat(); AppWindow.get_instance().get_window().get_device_position(seat.get_pointer(), out x, out y, out mask); ctrl = (mask & Gdk.ModifierType.CONTROL_MASK) != 0; alt = (mask & Gdk.ModifierType.MOD1_MASK) != 0; shift = (mask & Gdk.ModifierType.SHIFT_MASK) != 0; super = (mask & Gdk.ModifierType.MOD4_MASK) != 0; // not SUPER_MASK return true; } private void update_modifiers() { bool ctrl_currently_pressed, alt_currently_pressed, shift_currently_pressed, super_currently_pressed; if (!get_modifiers(out ctrl_currently_pressed, out alt_currently_pressed, out shift_currently_pressed, out super_currently_pressed)) { return; } if (ctrl_pressed && !ctrl_currently_pressed) on_ctrl_released(null); else if (!ctrl_pressed && ctrl_currently_pressed) on_ctrl_pressed(null); if (alt_pressed && !alt_currently_pressed) on_alt_released(null); else if (!alt_pressed && alt_currently_pressed) on_alt_pressed(null); if (shift_pressed && !shift_currently_pressed) on_shift_released(null); else if (!shift_pressed && shift_currently_pressed) on_shift_pressed(null); if(super_pressed && !super_currently_pressed) on_super_released(null); else if (!super_pressed && super_currently_pressed) on_super_pressed(null); ctrl_pressed = ctrl_currently_pressed; alt_pressed = alt_currently_pressed; shift_pressed = shift_currently_pressed; super_pressed = super_currently_pressed; } public PageWindow? get_page_window() { Gtk.Widget p = parent; while (p != null) { if (p is PageWindow) return (PageWindow) p; p = p.parent; } return null; } public CommandManager get_command_manager() { return AppWindow.get_command_manager(); } protected virtual void add_actions (GLib.ActionMap map) { } protected virtual void remove_actions (GLib.ActionMap map) { } protected void on_action_toggle (GLib.Action action, Variant? value) { Variant new_state = ! (bool) action.get_state (); action.change_state (new_state); } protected void on_action_radio (GLib.Action action, Variant? value) { action.change_state (value); } private void add_ui() { // Collect all UI filenames and load them into the UI manager Gee.List ui_filenames = new Gee.ArrayList(); init_collect_ui_filenames(ui_filenames); if (ui_filenames.size == 0) message("No UI file specified for %s", get_page_name()); foreach (string ui_filename in ui_filenames) init_load_ui(ui_filename); //ui.insert_action_group(action_group, 0); } public void init_toolbar(string path) { toolbar_path = path; } // Called from "realize" private void attach_view_signals() { if (are_actions_attached) return; // initialize the Gtk.Actions according to current state int selected_count = get_view().get_selected_count(); int count = get_view().get_count(); init_actions(selected_count, count); update_actions(selected_count, count); // monitor state changes to update actions get_view().items_state_changed.connect(on_update_actions); get_view().selection_group_altered.connect(on_update_actions); get_view().items_visibility_changed.connect(on_update_actions); get_view().contents_altered.connect(on_update_actions); are_actions_attached = true; } // Called from destroy() private void detach_view_signals() { if (!are_actions_attached) return; get_view().items_state_changed.disconnect(on_update_actions); get_view().selection_group_altered.disconnect(on_update_actions); get_view().items_visibility_changed.disconnect(on_update_actions); get_view().contents_altered.disconnect(on_update_actions); are_actions_attached = false; } private void on_update_actions() { if (update_actions_scheduler == null) { update_actions_scheduler = new OneShotScheduler( "Update actions scheduler for %s".printf(get_page_name()), on_update_actions_on_idle); } update_actions_scheduler.at_priority_idle(Priority.LOW); } private void on_update_actions_on_idle() { if (is_destroyed) return; if (!this.in_view) return; update_actions(get_view().get_selected_count(), get_view().get_count()); } private void init_load_ui(string ui_filename) { var ui_resource = Resources.get_ui(ui_filename); try { builder.add_from_resource(ui_resource); this.menubar_injected = false; } catch (Error err) { AppWindow.error_message("Error loading UI resource %s: %s".printf( ui_resource, err.message)); Application.get_instance().panic(); } } // This is called during add_ui() to collect all the UI files to be loaded into the UI // manager. Because order is important here, call the base method *first*, then add the // classes' filename. protected virtual void init_collect_ui_filenames(Gee.List ui_filenames) { } // This is called during add_ui() to collect all Page.InjectedUIElements for the page. They // should be added to the MultiSet using the injection path as the key. protected virtual InjectionGroup[] init_collect_injection_groups() { return new InjectionGroup[0]; } // This is called during "map" allowing for Gtk.Actions to be updated at // initialization time. protected virtual void init_actions(int selected_count, int count) { } // This is called during "map" and during ViewCollection selection, visibility, // and collection content altered events. This can be used to both initialize Gtk.Actions and // update them when selection or visibility has been altered. protected virtual void update_actions(int selected_count, int count) { } // Use this function rather than GDK or GTK's get_pointer, especially if called during a // button-down mouse drag (i.e. a window grab). // // For more information, see: https://bugzilla.gnome.org/show_bug.cgi?id=599937 public bool get_event_source_pointer(out int x, out int y, out Gdk.ModifierType mask) { if (event_source == null) { x = 0; y = 0; mask = 0; return false; } var seat = Gdk.Display.get_default().get_default_seat(); event_source.get_window().get_device_position(seat.get_pointer(), out x, out y, out mask); if (last_down.x < 0 || last_down.y < 0) return true; // check for bogus values inside a drag which goes outside the window // caused by (most likely) X windows signed 16-bit int overflow and fixup // (https://bugzilla.gnome.org/show_bug.cgi?id=599937) if ((x - last_down.x).abs() >= 0x7FFF) x += 0xFFFF; if ((y - last_down.y).abs() >= 0x7FFF) y += 0xFFFF; return true; } protected virtual bool on_left_click(Gdk.EventButton event) { return false; } protected virtual bool on_middle_click(Gdk.EventButton event) { return false; } protected virtual bool on_right_click(Gdk.EventButton event) { return false; } protected virtual bool on_left_released(Gdk.EventButton event) { return false; } protected virtual bool on_middle_released(Gdk.EventButton event) { return false; } protected virtual bool on_right_released(Gdk.EventButton event) { return false; } private bool on_button_pressed_internal(Gdk.EventButton event) { switch (event.button) { case 1: if (event_source != null) event_source.grab_focus(); // stash location of mouse down for drag fixups last_down.x = (int) event.x; last_down.y = (int) event.y; return on_left_click(event); case 2: return on_middle_click(event); case 3: return on_right_click(event); default: return false; } } private bool on_button_released_internal(Gdk.EventButton event) { switch (event.button) { case 1: // clear when button released, only for drag fixups last_down = { -1, -1 }; return on_left_released(event); case 2: return on_middle_released(event); case 3: return on_right_released(event); default: return false; } } protected virtual bool on_ctrl_pressed(Gdk.EventKey? event) { return false; } protected virtual bool on_ctrl_released(Gdk.EventKey? event) { return false; } protected virtual bool on_alt_pressed(Gdk.EventKey? event) { return false; } protected virtual bool on_alt_released(Gdk.EventKey? event) { return false; } protected virtual bool on_shift_pressed(Gdk.EventKey? event) { return false; } protected virtual bool on_shift_released(Gdk.EventKey? event) { return false; } protected virtual bool on_super_pressed(Gdk.EventKey? event) { return false; } protected virtual bool on_super_released(Gdk.EventKey? event) { return false; } protected virtual bool on_app_key_pressed(Gdk.EventKey event) { return false; } protected virtual bool on_app_key_released(Gdk.EventKey event) { return false; } public bool notify_app_key_pressed(Gdk.EventKey event) { bool ctrl_currently_pressed, alt_currently_pressed, shift_currently_pressed, super_currently_pressed; get_modifiers(out ctrl_currently_pressed, out alt_currently_pressed, out shift_currently_pressed, out super_currently_pressed); switch (Gdk.keyval_name(event.keyval)) { case "Control_L": case "Control_R": if (!ctrl_currently_pressed || ctrl_pressed) return false; ctrl_pressed = true; return on_ctrl_pressed(event); case "Meta_L": case "Meta_R": case "Alt_L": case "Alt_R": if (!alt_currently_pressed || alt_pressed) return false; alt_pressed = true; return on_alt_pressed(event); case "Shift_L": case "Shift_R": if (!shift_currently_pressed || shift_pressed) return false; shift_pressed = true; return on_shift_pressed(event); case "Super_L": case "Super_R": if (!super_currently_pressed || super_pressed) return false; super_pressed = true; return on_super_pressed(event); } return on_app_key_pressed(event); } public bool notify_app_key_released(Gdk.EventKey event) { bool ctrl_currently_pressed, alt_currently_pressed, shift_currently_pressed, super_currently_pressed; get_modifiers(out ctrl_currently_pressed, out alt_currently_pressed, out shift_currently_pressed, out super_currently_pressed); switch (Gdk.keyval_name(event.keyval)) { case "Control_L": case "Control_R": if (ctrl_currently_pressed || !ctrl_pressed) return false; ctrl_pressed = false; return on_ctrl_released(event); case "Meta_L": case "Meta_R": case "Alt_L": case "Alt_R": if (alt_currently_pressed || !alt_pressed) return false; alt_pressed = false; return on_alt_released(event); case "Shift_L": case "Shift_R": if (shift_currently_pressed || !shift_pressed) return false; shift_pressed = false; return on_shift_released(event); case "Super_L": case "Super_R": if (super_currently_pressed || !super_pressed) return false; super_pressed = false; return on_super_released(event); } return on_app_key_released(event); } public bool notify_app_focus_in(Gdk.EventFocus event) { update_modifiers(); return false; } public bool notify_app_focus_out(Gdk.EventFocus event) { return false; } protected virtual void on_move(Gdk.Rectangle rect) { } protected virtual void on_move_start(Gdk.Rectangle rect) { } protected virtual void on_move_finished(Gdk.Rectangle rect) { } protected virtual void on_resize(Gdk.Rectangle rect) { } protected virtual void on_resize_start(Gdk.Rectangle rect) { } protected virtual void on_resize_finished(Gdk.Rectangle rect) { } protected virtual bool on_configure(Gdk.EventConfigure event, Gdk.Rectangle rect) { return false; } public bool notify_configure_event(Gdk.EventConfigure event) { Gdk.Rectangle rect = Gdk.Rectangle(); rect.x = event.x; rect.y = event.y; rect.width = event.width; rect.height = event.height; // special case events, to report when a configure first starts (and appears to end) if (last_configure_ms == 0) { if (last_position.x != rect.x || last_position.y != rect.y) { on_move_start(rect); report_move_finished = true; } if (last_position.width != rect.width || last_position.height != rect.height) { on_resize_start(rect); report_resize_finished = true; } // need to check more often then the timeout, otherwise it could be up to twice the // wait time before it's noticed Timeout.add(CONSIDER_CONFIGURE_HALTED_MSEC / 8, check_configure_halted); } if (last_position.x != rect.x || last_position.y != rect.y) on_move(rect); if (last_position.width != rect.width || last_position.height != rect.height) on_resize(rect); last_position = rect; last_configure_ms = now_ms(); return on_configure(event, rect); } private bool check_configure_halted() { if (is_destroyed) return false; if ((now_ms() - last_configure_ms) < CONSIDER_CONFIGURE_HALTED_MSEC) return true; Gtk.Allocation allocation; get_allocation(out allocation); if (report_move_finished) on_move_finished((Gdk.Rectangle) allocation); if (report_resize_finished) on_resize_finished((Gdk.Rectangle) allocation); last_configure_ms = 0; report_move_finished = false; report_resize_finished = false; return false; } protected virtual bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) { check_cursor_hiding(); return false; } protected virtual bool on_leave_notify_event() { return false; } private bool on_motion_internal(Gdk.EventMotion event) { int x, y; Gdk.ModifierType mask; if (event.is_hint == 1) { get_event_source_pointer(out x, out y, out mask); } else { x = (int) event.x; y = (int) event.y; mask = event.state; } return on_motion(event, x, y, mask); } private bool on_mousewheel_internal(Gdk.EventScroll event) { switch (event.direction) { case Gdk.ScrollDirection.UP: return on_mousewheel_up(event); case Gdk.ScrollDirection.DOWN: return on_mousewheel_down(event); case Gdk.ScrollDirection.LEFT: return on_mousewheel_left(event); case Gdk.ScrollDirection.RIGHT: return on_mousewheel_right(event); case Gdk.ScrollDirection.SMOOTH: { double dx, dy; event.get_scroll_deltas(out dx, out dy); if (dy < -1.0 * this.wheel_factor) return on_mousewheel_up(event); else if (dy > this.wheel_factor) return on_mousewheel_down(event); else if (dx < -1.0 * this.wheel_factor) return on_mousewheel_left(event); else if (dx > this.wheel_factor) return on_mousewheel_right(event); else return false; } default: return false; } } protected virtual bool on_mousewheel_up(Gdk.EventScroll event) { return false; } protected virtual bool on_mousewheel_down(Gdk.EventScroll event) { return false; } protected virtual bool on_mousewheel_left(Gdk.EventScroll event) { return false; } protected virtual bool on_mousewheel_right(Gdk.EventScroll event) { return false; } protected virtual bool on_context_keypress() { return false; } protected virtual bool on_context_buttonpress(Gdk.EventButton event) { return false; } protected virtual bool on_context_invoked() { return true; } protected bool popup_context_menu(Gtk.Menu? context_menu, Gdk.EventButton? event = null) { if (context_menu == null || !on_context_invoked()) return false; context_menu.popup_at_pointer(event); return true; } protected void on_event_source_realize() { assert(event_source.get_window() != null); // the realize event means the Widget has a window if (event_source.get_window().get_cursor() != null) { last_cursor = event_source.get_window().get_cursor().get_cursor_type(); return; } // no custom cursor defined, check parents Gdk.Window? parent_window = event_source.get_window(); do { parent_window = parent_window.get_parent(); } while (parent_window != null && parent_window.get_cursor() == null); if (parent_window != null) last_cursor = parent_window.get_cursor().get_cursor_type(); } public void set_cursor_hide_time(int hide_time) { cursor_hide_msec = hide_time; } public void start_cursor_hiding() { check_cursor_hiding(); } public void stop_cursor_hiding() { if (last_timeout_id != 0) { Source.remove(last_timeout_id); last_timeout_id = 0; } } public void suspend_cursor_hiding() { cursor_hide_time_cached = cursor_hide_msec; if (last_timeout_id != 0) { Source.remove(last_timeout_id); last_timeout_id = 0; } cursor_hide_msec = 0; } public void restore_cursor_hiding() { cursor_hide_msec = cursor_hide_time_cached; check_cursor_hiding(); } // Use this method to set the cursor for a page, NOT window.set_cursor(...) protected virtual void set_page_cursor(Gdk.CursorType cursor_type) { last_cursor = cursor_type; if (!cursor_hidden && event_source != null) { var display = event_source.get_window ().get_display (); event_source.get_window().set_cursor(new Gdk.Cursor.for_display(display, cursor_type)); } } private void check_cursor_hiding() { if (cursor_hidden) { cursor_hidden = false; set_page_cursor(last_cursor); } if (cursor_hide_msec != 0) { if (last_timeout_id != 0) Source.remove(last_timeout_id); last_timeout_id = Timeout.add(cursor_hide_msec, on_hide_cursor); } } private bool on_hide_cursor() { cursor_hidden = true; if (event_source != null) { var display = event_source.get_window().get_display (); event_source.get_window().set_cursor(new Gdk.Cursor.for_display(display, Gdk.CursorType.BLANK_CURSOR)); } // We remove the timeout so reset the id last_timeout_id = 0; return false; } protected void update_menu_item_label (string id, string new_label) { AppWindow.get_instance().update_menu_item_label (id, new_label); } protected GLib.MenuModel? find_extension_point (GLib.MenuModel model, string extension_point) { var items = model.get_n_items (); GLib.MenuModel? section = null; for (int i = 0; i < items && section == null; i++) { string? name = null; model.get_item_attribute (i, "id", "s", out name); if (name == extension_point) { section = model.get_item_link (i, GLib.Menu.LINK_SECTION); } else { var subsection = model.get_item_link (i, GLib.Menu.LINK_SECTION); if (subsection == null) continue; // Recurse into submenus var sub_items = subsection.get_n_items (); for (int j = 0; j < sub_items && section == null; j++) { var submenu = subsection.get_item_link (j, GLib.Menu.LINK_SUBMENU); if (submenu != null) { section = this.find_extension_point (submenu, extension_point); } } } } return section; } }