roundabout,
created on Thursday, 14 August 2025, 13:11:32 (1755177092),
received on Thursday, 14 August 2025, 13:11:35 (1755177095)
Author identity: Vlad <vlad.muntoiu@gmail.com>
11b989bd7696b1e1b3e70aca01f45d89ea130d48
applets/app-menu/__init__.py
@@ -0,0 +1,243 @@
""" MATE-like app menu applet for the Panorama panel. Copyright 2025, roundabout-host.com <vlad@roundabout-host.com> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public Licence as published by the Free Software Foundation, either version 3 of the Licence, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licence for more details. You should have received a copy of the GNU General Public Licence along with this program. If not, see <https://www.gnu.org/licenses/>. """ import os from pathlib import Path import locale import panorama_panel import gi gi.require_version("Gtk", "4.0") from gi.repository import Gtk, GLib, Gio, Gdk module_directory = Path(__file__).resolve().parent locale.bindtextdomain("panorama-app-menu", module_directory / "locale") _ = lambda x: locale.dgettext("panorama-app-menu", x) CATEGORY_MAPPINGS = { "Utility": (_("Accessories"), "applications-accessories"), "Development": (_("Programming"), "applications-development"), "Game": (_("Games"), "applications-games"), "Graphics": (_("Graphics"), "applications-graphics"), "Network": (_("Network"), "applications-internet"), "AudioVideo": (_("Multimedia"), "applications-multimedia"), "Office": (_("Office"), "applications-office"), "Science": (_("Science"), "applications-science"), "Education": (_("Education"), "applications-education"), "System": (_("System"), "applications-system"), "Settings": (_("Settings"), "preferences-desktop"), "Other": (_("Other"), "applications-other"), } custom_css = """ .no-menu-item-padding:dir(ltr) { padding-left: 0; } .no-menu-item-padding:dir(rtl) { padding-right: 0; } """ css_provider = Gtk.CssProvider() css_provider.load_from_data(custom_css) Gtk.StyleContext.add_provider_for_display( Gdk.Display.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) def force_visible_on_visible_notify(widget, *args): if not widget.get_visible(): widget.set_visible(True) def add_icons_to_menu(popover: Gtk.PopoverMenu): section = popover.get_child().get_first_child().get_first_child().get_first_child() while section is not None: child = section.get_first_child().get_first_child() while child is not None: gutter_box: Gtk.Box = child.get_first_child() if isinstance(gutter_box.get_next_sibling(), Gtk.Image): # For some reason GTK creates images but they're hidden? image: Gtk.Image = gutter_box.get_next_sibling() label: Gtk.Label = image.get_next_sibling() image.unparent() gutter_box.unparent() gutter_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) gutter_box.insert_before(child, label) gutter_box.append(image) image.set_icon_size(Gtk.IconSize.LARGE) image.set_margin_start(8) image.set_margin_end(8) child.add_css_class("no-menu-item-padding") # Push the arrow to the left label.set_halign(Gtk.Align.FILL) label.set_hexpand(True) label.set_xalign(0) # GTK pushes its stance on icons so hard it makes them invisible multiple times; # force it visible image.set_visible(True) image.connect("notify::visible", force_visible_on_visible_notify) # Find the submenu if there is one subchild = child.get_first_child() submenu = None while subchild is not None: if isinstance(subchild, Gtk.PopoverMenu): submenu = subchild subchild = subchild.get_next_sibling() # Recursive if submenu is not None: add_icons_to_menu(submenu) child = child.get_next_sibling() section = section.get_next_sibling() class AppMenu(panorama_panel.Applet): name = _("App menu") description = _("Show apps installed on your system, grouped by category") def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None): super().__init__(orientation=orientation, config=config) if config is None: config = {} self.button = Gtk.MenuButton() self.button.set_has_frame(False) # flat look self.icon = Gtk.Image.new_from_icon_name("start-here") self.button.set_child(self.icon) self.apps_by_id: dict[int, Gio.AppInfo] = {} self.menu = Gio.Menu() self.popover = Gtk.PopoverMenu.new_from_model_full(self.menu, Gtk.PopoverMenuFlags.NESTED) self.popover.set_has_arrow(False) panorama_panel.track_popover(self.popover) self.button.set_popover(self.popover) self.generate_app_menu() self.append(self.button) self.context_menu = self.make_context_menu() panorama_panel.track_popover(self.context_menu) right_click_controller = Gtk.GestureClick() right_click_controller.set_button(3) right_click_controller.connect("pressed", self.show_context_menu) self.add_controller(right_click_controller) action_group = Gio.SimpleActionGroup() options_action = Gio.SimpleAction.new("options", None) options_action.connect("activate", self.show_options) action_group.add_action(options_action) options_action = Gio.SimpleAction.new("launch-app", GLib.VariantType.new("s")) options_action.connect("activate", self.launch_app) action_group.add_action(options_action) self.insert_action_group("applet", action_group) self.options_window = None def launch_app(self, action, id: GLib.Variant): app = self.apps_by_id[int(id.get_string())] app.launch() def make_context_menu(self): menu = Gio.Menu() menu.append(_("Menu _options"), "applet.options") context_menu = Gtk.PopoverMenu.new_from_model(menu) context_menu.set_has_arrow(False) context_menu.set_parent(self) context_menu.set_halign(Gtk.Align.START) context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) return context_menu def show_context_menu(self, gesture, n_presses, x, y): rect = Gdk.Rectangle() rect.x = int(x) rect.y = int(y) rect.width = 1 rect.height = 1 self.context_menu.set_pointing_to(rect) self.context_menu.popup() def show_options(self, _0=None, _1=None): ... def get_config(self): return {} def set_panel_position(self, position): self.popover.set_position(panorama_panel.OPPOSITE_POSITION[position]) self.button.set_direction(panorama_panel.POSITION_TO_ARROW[panorama_panel.OPPOSITE_POSITION[position]]) def generate_app_menu(self): self.menu.remove_all() self.apps_by_id = {} all_apps = Gio.AppInfo.get_all() apps_by_category: dict[str, list[Gio.AppInfo]] = {} for category, info in CATEGORY_MAPPINGS.items(): apps_by_category[category] = [] for app in all_apps: category_found = False if isinstance(app, Gio.DesktopAppInfo): if app.get_categories() is not None: categories = app.get_categories().split(";") for category in categories: if category in apps_by_category: apps_by_category[category].append(app) category_found = True if not category_found: apps_by_category["Other"].append(app) for category, info in CATEGORY_MAPPINGS.items(): if apps_by_category[category]: item = Gio.MenuItem.new(info[0]) item.set_icon(Gio.ThemedIcon.new(info[1])) submenu = Gio.Menu() for app in apps_by_category[category]: if isinstance(app, Gio.DesktopAppInfo) and (app.get_is_hidden() or app.get_nodisplay()): continue subitem = Gio.MenuItem.new(app.get_display_name(), f"applet.launch-app::{id(app)}") subitem.set_icon(app.get_icon() or Gio.ThemedIcon.new("image-missing")) self.apps_by_id[id(app)] = app submenu.append_item(subitem) item.set_submenu(submenu) self.menu.append_item(item) add_icons_to_menu(self.popover)
config.yaml
@@ -7,6 +7,7 @@ panels:
can_capture_keyboard: false applets: left: - AppMenu: {}- WFWindowList: max_button_width: 256 centre: []
docs/accessibility.md
@@ -11,8 +11,8 @@ Get a plugin like
[wf-panel-focus by Scott Moreau (for Wayfire)](https://github.com/soreau/wf-panel-focus) which will add a keybinding to cycle between displayed panels (default: Control-Alt-Tab) and one to return to the working area (Control-Alt-Esc). Enable `can_capture_keyboard` on your panels so it will be able to be focused.Then you can use normal GTK widget navigation in the panel.`can_capture_keyboard` on your panels, so they will be able to be focused. Then you can use normal GTK widget navigation in the panels.If you enable that option without a suitable plugin, panels can be focused in Wayfire by clicking them, which is not the expected UX, so don't enable it