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 • 10.89 kiB
Python script, Unicode text, UTF-8 text executable
        
            
1
"""
2
Nesting file 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
import hashlib
19
import os
20
import locale
21
from pathlib import Path
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, GObject, GioUnix
28
29
module_directory = Path(__file__).resolve().parent
30
locale.bindtextdomain("panorama-panel-file-listing", module_directory / "locale")
31
_ = lambda x: locale.dgettext("panorama-panel-file-listing", x)
32
33
34
class FileMenuAttributeIter(Gio.MenuAttributeIter, GObject.Object):
35
def __init__(self, attributes: dict, **kwargs):
36
GObject.Object.__init__(self, **kwargs)
37
self._attributes: dict = attributes
38
self._values = list(attributes.items())
39
self._i = 0
40
41
def do_get_next(self):
42
if self._i >= len(self._values):
43
return False, None, None
44
45
return True, *self._values[self._i]
46
47
48
class FileMenuLinkIter(Gio.MenuLinkIter, GObject.Object):
49
def __init__(self, attributes: dict, **kwargs):
50
GObject.Object.__init__(self, **kwargs)
51
self._attributes: dict = attributes
52
self._values = list(attributes.items())
53
self._i = 0
54
55
def do_get_next(self):
56
if self._i >= len(self._values):
57
return False, None, None
58
59
return True, *self._values[self._i]
60
61
62
class FileMenu(Gio.MenuModel, GObject.Object):
63
def __init__(self, root: Path, storage: dict[str, GObject.Object], new_action_callback, prune_action_callback, show_hidden=False, **kwargs):
64
GObject.Object.__init__(self, **kwargs)
65
self.root: Path = root
66
self.links: dict[str, Gio.MenuModel] = {}
67
self.files: list[Path] = []
68
self.storage = storage
69
self.callback = new_action_callback
70
self.callback_prune = prune_action_callback
71
self.show_hidden = show_hidden
72
self.enabled = False
73
74
def do_is_mutable(self):
75
return True
76
77
def do_get_n_items(self):
78
if not self.enabled:
79
return 0
80
if self.show_hidden:
81
self.files = sorted(
82
self.root.iterdir(),
83
key=lambda p: GLib.utf8_collate_key_for_filename(p.name, len(p.name))
84
)
85
else:
86
self.files = sorted(
87
(p for p in self.root.iterdir() if not p.name.startswith(".")),
88
key=lambda p: GLib.utf8_collate_key_for_filename(p.name, len(p.name))
89
)
90
return len(self.files)
91
92
def do_get_item_attribute_value(self, item_index, attribute, expected_type = None):
93
file = self.files[item_index]
94
match attribute:
95
case Gio.MENU_ATTRIBUTE_ACTION:
96
return GLib.Variant("s", "applet.open")
97
case Gio.MENU_ATTRIBUTE_TARGET:
98
return GLib.Variant("s", str(file.resolve()))
99
case Gio.MENU_ATTRIBUTE_LABEL:
100
return GLib.Variant("s", file.name.replace("_", "_"))
101
case "submenu-action":
102
return GLib.Variant("s", f"applet.submenu-status-{hashlib.md5(str(file).encode()).hexdigest()}")
103
case _:
104
return None
105
106
def make_submenu(self, name):
107
action_id = hashlib.md5(str(self.root / name).encode())
108
if name not in self.links:
109
menu = FileMenu(self.root / name, self.storage, self.callback, self.callback_prune, show_hidden=self.show_hidden)
110
container = Gio.Menu()
111
options_section = Gio.Menu()
112
open_directory = Gio.MenuItem()
113
open_directory.set_label(_("Open directory"))
114
open_directory.set_action_and_target_value("applet.open", GLib.Variant("s", str((self.root / name).resolve())))
115
options_section.append_item(open_directory)
116
container.append_section(None, options_section)
117
container.append_section(None, menu)
118
container.file_list = menu
119
self.links[name] = container
120
self.storage[action_id.hexdigest()] = menu
121
self.callback(action_id)
122
123
def do_get_item_link(self, item_index, link):
124
file = self.files[item_index]
125
if file.is_dir() and link == Gio.MENU_LINK_SUBMENU:
126
self.make_submenu(file.name)
127
return self.links[file.name]
128
return None
129
130
def do_iterate_item_attributes(self, item_index):
131
file = self.files[item_index]
132
return FileMenuAttributeIter(
133
{
134
Gio.MENU_ATTRIBUTE_ACTION: GLib.Variant("s", "applet.open"),
135
Gio.MENU_ATTRIBUTE_TARGET: GLib.Variant("s", str(file.resolve())),
136
Gio.MENU_ATTRIBUTE_LABEL: GLib.Variant("s", file.name.replace("_", "_")),
137
"submenu-action": GLib.Variant("s", f"applet.submenu-status-{hashlib.md5(str(file).encode()).hexdigest()}"),
138
}
139
)
140
141
def do_iterate_item_links(self, item_index):
142
file = self.files[item_index]
143
if file.is_dir():
144
self.make_submenu(file.name)
145
return FileMenuLinkIter({Gio.MENU_LINK_SUBMENU: self.links[file.name]})
146
return FileMenuLinkIter({})
147
148
def do_get_item_attributes(self, item_index):
149
file = self.files[item_index]
150
return {
151
Gio.MENU_ATTRIBUTE_ACTION: GLib.Variant("s", "applet.open"),
152
Gio.MENU_ATTRIBUTE_TARGET: GLib.Variant("s", str(file.resolve())),
153
Gio.MENU_ATTRIBUTE_LABEL: GLib.Variant("s", file.name.replace("_", "_")),
154
"submenu-action": GLib.Variant("s", f"applet.submenu-status-{hashlib.md5(str(file).encode()).hexdigest()}"),
155
}
156
157
def do_get_item_links(self, item_index):
158
file = self.files[item_index]
159
if file.is_dir():
160
self.make_submenu(file.name)
161
return {Gio.MENU_LINK_SUBMENU: self.links[file.name]}
162
return {}
163
164
def generate(self):
165
self.enabled = True
166
self.items_changed(0, 0, self.get_n_items())
167
168
def prune(self):
169
self.enabled = False
170
for file in self.files:
171
if file.is_dir():
172
action_id = hashlib.md5(str(self.root / file.name).encode())
173
self.links[file.name].file_list.prune()
174
del self.storage[action_id.hexdigest()]
175
self.callback_prune(action_id)
176
self.items_changed(0, len(self.files), 0)
177
self.files.clear()
178
self.links.clear()
179
180
181
class FileListingApplet(panorama_panel.Applet):
182
name = _("File listing")
183
description = _("View a nesting menu of your files")
184
icon = Gio.ThemedIcon.new("system-file-manager")
185
186
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
187
super().__init__(orientation=orientation, config=config)
188
if config is None:
189
config = {}
190
self.action_group = Gio.SimpleActionGroup()
191
self.directory = Path(config.get("directory", "~")).expanduser().resolve()
192
self.button = Gtk.MenuButton()
193
self.button.set_has_frame(False)
194
self.append(self.button)
195
196
self.inner_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
197
self.button.set_child(self.inner_box)
198
self.icon_name = config.get("icon_name")
199
self.show_hidden = config.get("show_hidden", False)
200
self.icon_size = config.get("icon_size", 24)
201
self.label = config.get("label")
202
if config.get("icon_name"):
203
self.inner_box.append(Gtk.Image(icon_name=config["icon_name"], pixel_size=config.get("icon_size", 24)))
204
if config.get("label"):
205
self.inner_box.append(Gtk.Label.new(config["label"]))
206
207
self.button.set_always_show_arrow(True)
208
self.popover = Gtk.PopoverMenu(flags=Gtk.PopoverMenuFlags.NESTED, has_arrow=False)
209
self.actions = {}
210
self.container_menu = Gio.Menu()
211
self.options_section = Gio.Menu()
212
open_directory = Gio.MenuItem()
213
open_directory.set_label(_("Open directory"))
214
open_directory.set_action_and_target_value("applet.open", GLib.Variant("s", str(self.directory)))
215
self.options_section.append_item(open_directory)
216
self.container_menu.append_section(None, self.options_section)
217
218
self.menu = FileMenu(self.directory, self.actions, self.new_action, self.delete_action, show_hidden=self.show_hidden)
219
self.container_menu.append_section(None, self.menu)
220
self.container_menu.file_list = self.menu
221
222
self.popover.set_menu_model(self.menu)
223
self.button.set_popover(self.popover)
224
225
self.open_action = Gio.SimpleAction(name="open", parameter_type=GLib.VariantType("s"))
226
self.open_action.connect("activate", self.open_file)
227
228
self.submenu_action = Gio.SimpleAction(name="submenu-status", state=GLib.Variant("b", False))
229
self.submenu_action.connect("notify", self.submenu_changed)
230
231
self.action_group.add_action(self.open_action)
232
self.action_group.add_action(self.submenu_action)
233
self.insert_action_group("applet", self.action_group)
234
235
self.popover.connect("realize", lambda *args: self.menu.generate())
236
self.popover.connect("unrealize", lambda *args: self.menu.prune())
237
238
def new_action(self, action_id):
239
action = Gio.SimpleAction(name=f"submenu-status-{action_id.hexdigest()}", state=GLib.Variant("b", False))
240
action.submenu = self.actions[action_id.hexdigest()]
241
action.connect("notify", self.submenu_changed)
242
self.action_group.add_action(action)
243
244
def delete_action(self, action_id):
245
self.action_group.remove(f"submenu-status-{action_id.hexdigest()}")
246
247
def open_file(self, action, data: GLib.Variant):
248
uri = GLib.filename_to_uri(data.get_string())
249
try:
250
GioUnix.DesktopAppInfo.new_from_filename(data.get_string()).launch()
251
except TypeError:
252
Gio.AppInfo.launch_default_for_uri(uri)
253
254
def submenu_changed(self, action, parameter):
255
if action.get_state():
256
action.submenu.generate()
257
else:
258
action.submenu.prune()
259
260
def get_config(self):
261
return {
262
"directory": str(self.directory),
263
"icon_name": self.icon_name,
264
"icon_size": self.icon_size,
265
"label": self.label,
266
"show_hidden": self.show_hidden,
267
}
268
269
def set_panel_position(self, position):
270
self.button.set_direction(panorama_panel.POSITION_TO_ARROW[panorama_panel.OPPOSITE_POSITION[position]])
271