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 • 8.47 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, GioUnix, Gdk
28
29
30
module_directory = Path(__file__).resolve().parent
31
32
33
CATEGORY_MAPPINGS = {
34
"Utility": {"menu_name": "Accessories", "icon": "applications-accessories"},
35
"Development": {"menu_name": "Programming", "icon": "applications-development"},
36
"Game": {"menu_name": "Games", "icon": "applications-games"},
37
"Graphics": {"menu_name": "Graphics", "icon": "applications-graphics"},
38
"Network": {"menu_name": "Network", "icon": "applications-internet"},
39
"AudioVideo": {"menu_name": "Multimedia", "icon": "applications-multimedia"},
40
"Office": {"menu_name": "Office", "icon": "applications-office"},
41
"Science": {"menu_name": "Science", "icon": "applications-science"},
42
"Education": {"menu_name": "Education", "icon": "applications-education"},
43
"System": {"menu_name": "System", "icon": "applications-system"},
44
"Settings": {"menu_name": "Settings", "icon": "preferences-desktop"},
45
"Other": {"menu_name": "Other", "icon": "applications-other"},
46
}
47
48
custom_css = """
49
.no-menu-item-padding:dir(ltr) {
50
padding-left: 0;
51
}
52
53
.no-menu-item-padding:dir(rtl) {
54
padding-right: 0;
55
}
56
"""
57
58
css_provider = Gtk.CssProvider()
59
css_provider.load_from_data(custom_css)
60
Gtk.StyleContext.add_provider_for_display(
61
Gdk.Display.get_default(),
62
css_provider,
63
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
64
)
65
66
67
locale.bindtextdomain("panorama-app-menu", module_directory / "locale")
68
_ = lambda x: locale.dgettext("panorama-app-menu", x)
69
70
71
class AppMenu(panorama_panel.Applet):
72
name = _("App menu")
73
description = _("Show apps installed on your system, grouped by category")
74
icon = Gio.ThemedIcon.new("start-here")
75
76
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None, **kwargs):
77
super().__init__(orientation=orientation, config=config, **kwargs)
78
locale.bindtextdomain("panorama-app-menu", module_directory / "locale")
79
_ = lambda x: locale.dgettext("panorama-app-menu", x)
80
if config is None:
81
config = {}
82
83
self.auto_refresh = config.get("auto_refresh", True)
84
self.category_mappings = config.get("category_mappings", CATEGORY_MAPPINGS)
85
self.trigger_name = config.get("trigger_name", "app-menu")
86
self.icon_name = config.get("icon_name", "start-here-symbolic")
87
88
self.button = Gtk.MenuButton()
89
self.button.set_has_frame(False) # flat look
90
self.icon = Gtk.Image.new_from_icon_name(self.icon_name)
91
self.icon.set_pixel_size(config.get("icon_size", 24))
92
self.button.set_child(self.icon)
93
self.apps_by_id: dict[int, Gio.AppInfo] = {}
94
# Wait for the widget to be in a layer-shell window before doing this
95
self.connect("realize", lambda *args: self.add_trigger_to_app())
96
97
self.menu = Gio.Menu()
98
self.popover = Gtk.PopoverMenu.new_from_model_full(self.menu, Gtk.PopoverMenuFlags.NESTED)
99
self.popover.set_halign(Gtk.Align.START)
100
self.popover.set_has_arrow(False)
101
panorama_panel.track_popover(self.popover)
102
self.button.set_popover(self.popover)
103
104
self.generate_app_menu()
105
106
self.append(self.button)
107
108
self.context_menu = self.make_context_menu()
109
panorama_panel.track_popover(self.context_menu)
110
111
right_click_controller = Gtk.GestureClick()
112
right_click_controller.set_button(3)
113
right_click_controller.connect("pressed", self.show_context_menu)
114
115
self.add_controller(right_click_controller)
116
117
action_group = Gio.SimpleActionGroup()
118
options_action = Gio.SimpleAction.new("options", None)
119
options_action.connect("activate", self.show_options)
120
action_group.add_action(options_action)
121
app_action = Gio.SimpleAction.new("launch-app", GLib.VariantType.new("s"))
122
app_action.connect("activate", self.launch_app)
123
action_group.add_action(app_action)
124
self.insert_action_group("applet", action_group)
125
126
if self.auto_refresh:
127
self.app_info_monitor = Gio.AppInfoMonitor.get()
128
self.app_info_monitor.connect("changed", self.generate_app_menu)
129
130
self.options_window = None
131
132
def add_trigger_to_app(self):
133
if self.trigger_name:
134
app: Gtk.Application = self.get_root().get_application()
135
action = Gio.SimpleAction.new(self.trigger_name, None)
136
action.connect("activate", lambda *args: self.button.popup())
137
app.add_action(action)
138
139
def launch_app(self, action, id: GLib.Variant):
140
app = self.apps_by_id[int(id.get_string())]
141
app.launch()
142
143
def make_context_menu(self):
144
menu = Gio.Menu()
145
menu.append(_("Menu _options"), "applet.options")
146
context_menu = Gtk.PopoverMenu.new_from_model(menu)
147
context_menu.set_has_arrow(False)
148
context_menu.set_parent(self)
149
context_menu.set_halign(Gtk.Align.START)
150
context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
151
return context_menu
152
153
def show_context_menu(self, gesture, n_presses, x, y):
154
rect = Gdk.Rectangle()
155
rect.x = int(x)
156
rect.y = int(y)
157
rect.width = 1
158
rect.height = 1
159
160
self.context_menu.set_pointing_to(rect)
161
self.context_menu.popup()
162
163
def show_options(self, _0=None, _1=None):
164
...
165
166
def shutdown(self, app: Gtk.Application):
167
app.remove_action(self.trigger_name)
168
169
def get_config(self):
170
return {
171
"category_mappings": self.category_mappings,
172
"trigger_name": self.trigger_name,
173
"icon_name": self.icon_name,
174
"icon_size": self.icon.get_pixel_size(),
175
}
176
177
def set_panel_position(self, position):
178
self.popover.set_position(panorama_panel.OPPOSITE_POSITION[position])
179
self.button.set_direction(panorama_panel.POSITION_TO_ARROW[panorama_panel.OPPOSITE_POSITION[position]])
180
181
def generate_app_menu(self, app_info_monitor=None):
182
self.menu.remove_all()
183
self.apps_by_id = {}
184
185
all_apps = Gio.AppInfo.get_all()
186
apps_by_category: dict[str, list[Gio.AppInfo]] = {}
187
for category, info in self.category_mappings.items():
188
apps_by_category[category] = []
189
190
for app in all_apps:
191
category_found = False
192
if isinstance(app, GioUnix.DesktopAppInfo):
193
if app.get_categories() is not None:
194
categories = app.get_categories().split(";")
195
for category in categories:
196
if category in apps_by_category:
197
apps_by_category[category].append(app)
198
category_found = True
199
200
if not category_found:
201
apps_by_category["Other"].append(app)
202
203
for apps in apps_by_category.values():
204
apps.sort(key=lambda app: app.get_display_name())
205
206
for category, info in self.category_mappings.items():
207
if apps_by_category[category]:
208
item = Gio.MenuItem.new(_(info["menu_name"]))
209
item.set_icon(Gio.ThemedIcon.new(info["icon"]))
210
submenu = Gio.Menu()
211
212
for app in apps_by_category[category]:
213
if isinstance(app, GioUnix.DesktopAppInfo) and (app.get_is_hidden() or app.get_nodisplay()):
214
continue
215
216
subitem = Gio.MenuItem.new(app.get_display_name(), f"applet.launch-app::{id(app)}")
217
subitem.set_icon(app.get_icon() or Gio.ThemedIcon.new("image-missing"))
218
self.apps_by_id[id(app)] = app
219
submenu.append_item(subitem)
220
221
item.set_submenu(submenu)
222
self.menu.append_item(item)
223
224
panorama_panel.add_icons_to_menu(self.popover)
225