By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 __init__.py

View raw Download
text/plain • 9.76 kiB
Python script, ASCII text executable
        
            
1
"""
2
MATE-like app menu applet for the Panorama panel.
3
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
4
5
This program is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public Licence as published by
7
the Free Software Foundation, either version 3 of the Licence, or
8
(at your option) any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
GNU General Public Licence for more details.
14
15
You should have received a copy of the GNU General Public Licence
16
along with this program. If not, see <https://www.gnu.org/licenses/>.
17
"""
18
19
import os
20
from pathlib import Path
21
import locale
22
import panorama_panel
23
24
import gi
25
gi.require_version("Gtk", "4.0")
26
27
from gi.repository import Gtk, GLib, Gio, Gdk
28
29
30
module_directory = Path(__file__).resolve().parent
31
32
locale.bindtextdomain("panorama-app-menu", module_directory / "locale")
33
_ = lambda x: locale.dgettext("panorama-app-menu", x)
34
35
36
CATEGORY_MAPPINGS = {
37
"Utility": {"menu_name": _("Accessories"), "icon": "applications-accessories"},
38
"Development": {"menu_name": _("Programming"), "icon": "applications-development"},
39
"Game": {"menu_name": _("Games"), "icon": "applications-games"},
40
"Graphics": {"menu_name": _("Graphics"), "icon": "applications-graphics"},
41
"Network": {"menu_name": _("Network"), "icon": "applications-internet"},
42
"AudioVideo": {"menu_name": _("Multimedia"), "icon": "applications-multimedia"},
43
"Office": {"menu_name": _("Office"), "icon": "applications-office"},
44
"Science": {"menu_name": _("Science"), "icon": "applications-science"},
45
"Education": {"menu_name": _("Education"), "icon": "applications-education"},
46
"System": {"menu_name": _("System"), "icon": "applications-system"},
47
"Settings": {"menu_name": _("Settings"), "icon": "preferences-desktop"},
48
"Other": {"menu_name": _("Other"), "icon": "applications-other"},
49
}
50
51
custom_css = """
52
.no-menu-item-padding:dir(ltr) {
53
padding-left: 0;
54
}
55
56
.no-menu-item-padding:dir(rtl) {
57
padding-right: 0;
58
}
59
"""
60
61
css_provider = Gtk.CssProvider()
62
css_provider.load_from_data(custom_css)
63
Gtk.StyleContext.add_provider_for_display(
64
Gdk.Display.get_default(),
65
css_provider,
66
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
67
)
68
69
70
def force_visible_on_visible_notify(widget, *args):
71
if not widget.get_visible():
72
widget.set_visible(True)
73
74
75
def add_icons_to_menu(popover: Gtk.PopoverMenu):
76
section = popover.get_child().get_first_child().get_first_child().get_first_child()
77
while section is not None:
78
child = section.get_first_child().get_first_child()
79
80
while child is not None:
81
gutter_box: Gtk.Box = child.get_first_child()
82
if isinstance(gutter_box.get_next_sibling(), Gtk.Image):
83
# For some reason GTK creates images but they're hidden?
84
image: Gtk.Image = gutter_box.get_next_sibling()
85
label: Gtk.Label = image.get_next_sibling()
86
87
image.unparent()
88
gutter_box.unparent()
89
gutter_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
90
gutter_box.insert_before(child, label)
91
gutter_box.append(image)
92
image.set_icon_size(Gtk.IconSize.LARGE)
93
image.set_margin_start(8)
94
image.set_margin_end(8)
95
96
child.add_css_class("no-menu-item-padding")
97
98
# Push the arrow to the left
99
label.set_halign(Gtk.Align.FILL)
100
label.set_hexpand(True)
101
label.set_xalign(0)
102
103
# GTK pushes its stance on icons so hard it makes them invisible multiple times;
104
# force it visible
105
image.set_visible(True)
106
image.connect("notify::visible", force_visible_on_visible_notify)
107
108
# Find the submenu if there is one
109
subchild = child.get_first_child()
110
submenu = None
111
while subchild is not None:
112
if isinstance(subchild, Gtk.PopoverMenu):
113
submenu = subchild
114
subchild = subchild.get_next_sibling()
115
116
# Recursive
117
if submenu is not None:
118
add_icons_to_menu(submenu)
119
120
child = child.get_next_sibling()
121
122
section = section.get_next_sibling()
123
124
125
class AppMenu(panorama_panel.Applet):
126
name = _("App menu")
127
description = _("Show apps installed on your system, grouped by category")
128
129
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
130
super().__init__(orientation=orientation, config=config)
131
if config is None:
132
config = {}
133
self.category_mappings = config.get("category_mappings", CATEGORY_MAPPINGS)
134
self.trigger_name = config.get("trigger_name", "app-menu")
135
self.button = Gtk.MenuButton()
136
self.button.set_has_frame(False) # flat look
137
self.icon_name = config.get("icon_name", "start-here-symbolic")
138
self.icon = Gtk.Image.new_from_icon_name(self.icon_name)
139
self.button.set_child(self.icon)
140
self.apps_by_id: dict[int, Gio.AppInfo] = {}
141
# Wait for the widget to be in a layer-shell window before doing this
142
self.connect("realize", lambda *args: self.add_trigger_to_app())
143
144
self.menu = Gio.Menu()
145
self.popover = Gtk.PopoverMenu.new_from_model_full(self.menu, Gtk.PopoverMenuFlags.NESTED)
146
self.popover.set_has_arrow(False)
147
panorama_panel.track_popover(self.popover)
148
self.button.set_popover(self.popover)
149
150
self.generate_app_menu()
151
152
self.append(self.button)
153
154
self.context_menu = self.make_context_menu()
155
panorama_panel.track_popover(self.context_menu)
156
157
right_click_controller = Gtk.GestureClick()
158
right_click_controller.set_button(3)
159
right_click_controller.connect("pressed", self.show_context_menu)
160
161
self.add_controller(right_click_controller)
162
163
action_group = Gio.SimpleActionGroup()
164
options_action = Gio.SimpleAction.new("options", None)
165
options_action.connect("activate", self.show_options)
166
action_group.add_action(options_action)
167
options_action = Gio.SimpleAction.new("launch-app", GLib.VariantType.new("s"))
168
options_action.connect("activate", self.launch_app)
169
action_group.add_action(options_action)
170
self.insert_action_group("applet", action_group)
171
172
self.options_window = None
173
174
def add_trigger_to_app(self):
175
app: Gtk.Application = self.get_root().get_application()
176
action = Gio.SimpleAction.new(self.trigger_name, None)
177
action.connect("activate", lambda *args: self.button.popup())
178
app.add_action(action)
179
180
def launch_app(self, action, id: GLib.Variant):
181
app = self.apps_by_id[int(id.get_string())]
182
app.launch()
183
184
def make_context_menu(self):
185
menu = Gio.Menu()
186
menu.append(_("Menu _options"), "applet.options")
187
context_menu = Gtk.PopoverMenu.new_from_model(menu)
188
context_menu.set_has_arrow(False)
189
context_menu.set_parent(self)
190
context_menu.set_halign(Gtk.Align.START)
191
context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
192
return context_menu
193
194
def show_context_menu(self, gesture, n_presses, x, y):
195
rect = Gdk.Rectangle()
196
rect.x = int(x)
197
rect.y = int(y)
198
rect.width = 1
199
rect.height = 1
200
201
self.context_menu.set_pointing_to(rect)
202
self.context_menu.popup()
203
204
def show_options(self, _0=None, _1=None):
205
...
206
207
def shutdown(self):
208
app: Gtk.Application = self.get_root().get_application()
209
app.remove_action(self.trigger_name)
210
211
def get_config(self):
212
return {"category_mappings": self.category_mappings, "trigger_name": self.trigger_name, "icon_name": self.icon_name}
213
214
def set_panel_position(self, position):
215
self.popover.set_position(panorama_panel.OPPOSITE_POSITION[position])
216
self.button.set_direction(panorama_panel.POSITION_TO_ARROW[panorama_panel.OPPOSITE_POSITION[position]])
217
218
def generate_app_menu(self):
219
self.menu.remove_all()
220
self.apps_by_id = {}
221
222
all_apps = Gio.AppInfo.get_all()
223
apps_by_category: dict[str, list[Gio.AppInfo]] = {}
224
for category, info in self.category_mappings.items():
225
apps_by_category[category] = []
226
227
for app in all_apps:
228
category_found = False
229
if isinstance(app, Gio.DesktopAppInfo):
230
if app.get_categories() is not None:
231
categories = app.get_categories().split(";")
232
for category in categories:
233
if category in apps_by_category:
234
apps_by_category[category].append(app)
235
category_found = True
236
237
if not category_found:
238
apps_by_category["Other"].append(app)
239
240
for category, info in self.category_mappings.items():
241
if apps_by_category[category]:
242
item = Gio.MenuItem.new(info["menu_name"])
243
item.set_icon(Gio.ThemedIcon.new(info["icon"]))
244
submenu = Gio.Menu()
245
246
for app in apps_by_category[category]:
247
if isinstance(app, Gio.DesktopAppInfo) and (app.get_is_hidden() or app.get_nodisplay()):
248
continue
249
250
subitem = Gio.MenuItem.new(app.get_display_name(), f"applet.launch-app::{id(app)}")
251
subitem.set_icon(app.get_icon() or Gio.ThemedIcon.new("image-missing"))
252
self.apps_by_id[id(app)] = app
253
submenu.append_item(subitem)
254
255
item.set_submenu(submenu)
256
self.menu.append_item(item)
257
258
add_icons_to_menu(self.popover)
259