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.45 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
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, **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.enabled = False
72
73
def do_is_mutable(self):
74
return True
75
76
def do_get_n_items(self):
77
if not self.enabled:
78
return 0
79
self.files = list(self.root.iterdir())
80
return len(self.files)
81
82
def do_get_item_attribute_value(self, item_index, attribute, expected_type = None):
83
file = self.files[item_index]
84
match attribute:
85
case Gio.MENU_ATTRIBUTE_ACTION:
86
return GLib.Variant("s", "applet.open")
87
case Gio.MENU_ATTRIBUTE_TARGET:
88
return GLib.Variant("s", str(file.resolve()))
89
case Gio.MENU_ATTRIBUTE_LABEL:
90
return GLib.Variant("s", file.name.replace("_", "_"))
91
case "submenu-action":
92
return GLib.Variant("s", f"applet.submenu-status-{hashlib.md5(str(file).encode()).hexdigest()}")
93
case _:
94
return None
95
96
def make_submenu(self, name):
97
action_id = hashlib.md5(str(self.root / name).encode())
98
if name not in self.links:
99
menu = FileMenu(self.root / name, self.storage, self.callback, self.callback_prune)
100
container = Gio.Menu()
101
options_section = Gio.Menu()
102
open_directory = Gio.MenuItem()
103
open_directory.set_label(_("Open directory"))
104
open_directory.set_action_and_target_value("applet.open", GLib.Variant("s", str((self.root / name).resolve())))
105
options_section.append_item(open_directory)
106
container.append_section(None, options_section)
107
container.append_section(None, menu)
108
container.file_list = menu
109
self.links[name] = container
110
self.storage[action_id.hexdigest()] = menu
111
self.callback(action_id)
112
113
def do_get_item_link(self, item_index, link):
114
file = self.files[item_index]
115
if file.is_dir() and link == Gio.MENU_LINK_SUBMENU:
116
self.make_submenu(file.name)
117
return self.links[file.name]
118
return None
119
120
def do_iterate_item_attributes(self, item_index):
121
file = self.files[item_index]
122
return FileMenuAttributeIter(
123
{
124
Gio.MENU_ATTRIBUTE_ACTION: GLib.Variant("s", "applet.open"),
125
Gio.MENU_ATTRIBUTE_TARGET: GLib.Variant("s", str(file.resolve())),
126
Gio.MENU_ATTRIBUTE_LABEL: GLib.Variant("s", file.name.replace("_", "_")),
127
"submenu-action": GLib.Variant("s", f"applet.submenu-status-{hashlib.md5(str(file).encode()).hexdigest()}"),
128
}
129
)
130
131
def do_iterate_item_links(self, item_index):
132
file = self.files[item_index]
133
if file.is_dir():
134
self.make_submenu(file.name)
135
return FileMenuLinkIter({Gio.MENU_LINK_SUBMENU: self.links[file.name]})
136
return FileMenuLinkIter({})
137
138
def do_get_item_attributes(self, item_index):
139
file = self.files[item_index]
140
return {
141
Gio.MENU_ATTRIBUTE_ACTION: GLib.Variant("s", "applet.open"),
142
Gio.MENU_ATTRIBUTE_TARGET: GLib.Variant("s", str(file.resolve())),
143
Gio.MENU_ATTRIBUTE_LABEL: GLib.Variant("s", file.name.replace("_", "_")),
144
"submenu-action": GLib.Variant("s", f"applet.submenu-status-{hashlib.md5(str(file).encode()).hexdigest()}"),
145
}
146
147
def do_get_item_links(self, item_index):
148
file = self.files[item_index]
149
if file.is_dir():
150
self.make_submenu(file.name)
151
return {Gio.MENU_LINK_SUBMENU: self.links[file.name]}
152
return {}
153
154
def generate(self):
155
self.enabled = True
156
self.items_changed(0, 0, self.get_n_items())
157
158
def prune(self):
159
self.enabled = False
160
for file in self.files:
161
if file.is_dir():
162
action_id = hashlib.md5(str(self.root / file.name).encode())
163
self.links[file.name].file_list.prune()
164
del self.storage[action_id.hexdigest()]
165
self.callback_prune(action_id)
166
self.items_changed(0, len(self.files), 0)
167
self.files.clear()
168
self.links.clear()
169
170
171
class FileListingApplet(panorama_panel.Applet):
172
name = _("File listing")
173
description = _("View a nesting menu of your files")
174
175
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
176
super().__init__(orientation=orientation, config=config)
177
if config is None:
178
config = {}
179
self.action_group = Gio.SimpleActionGroup()
180
self.directory = Path(config.get("directory", "~")).expanduser().resolve()
181
self.button = Gtk.MenuButton()
182
self.button.set_has_frame(False)
183
self.append(self.button)
184
self.button.set_always_show_arrow(True)
185
self.popover = Gtk.PopoverMenu(flags=Gtk.PopoverMenuFlags.NESTED, has_arrow=False)
186
self.actions = {}
187
self.container_menu = Gio.Menu()
188
self.options_section = Gio.Menu()
189
open_directory = Gio.MenuItem()
190
open_directory.set_label(_("Open directory"))
191
open_directory.set_action_and_target_value("applet.open", GLib.Variant("s", str(self.directory)))
192
self.options_section.append_item(open_directory)
193
self.container_menu.append_section(None, self.options_section)
194
195
self.menu = FileMenu(self.directory, self.actions, self.new_action, self.delete_action)
196
self.container_menu.append_section(None, self.menu)
197
self.container_menu.file_list = self.menu
198
199
self.popover.set_menu_model(self.menu)
200
self.button.set_popover(self.popover)
201
202
self.open_action = Gio.SimpleAction(name="open", parameter_type=GLib.VariantType("s"))
203
self.open_action.connect("activate", self.open_file)
204
205
self.submenu_action = Gio.SimpleAction(name="submenu-status", state=GLib.Variant("b", False))
206
self.submenu_action.connect("notify", self.submenu_changed)
207
208
self.action_group.add_action(self.open_action)
209
self.action_group.add_action(self.submenu_action)
210
self.insert_action_group("applet", self.action_group)
211
212
self.popover.connect("realize", lambda *args: self.menu.generate())
213
self.popover.connect("unrealize", lambda *args: self.menu.prune())
214
215
def new_action(self, action_id):
216
action = Gio.SimpleAction(name=f"submenu-status-{action_id.hexdigest()}", state=GLib.Variant("b", False))
217
action.submenu = self.actions[action_id.hexdigest()]
218
action.connect("notify", self.submenu_changed)
219
self.action_group.add_action(action)
220
221
def delete_action(self, action_id):
222
self.action_group.remove(f"submenu-status-{action_id.hexdigest()}")
223
224
def open_file(self, action, data: GLib.Variant):
225
Gio.AppInfo.launch_default_for_uri(GLib.filename_to_uri(data.get_string()))
226
227
def submenu_changed(self, action, parameter):
228
if action.get_state():
229
action.submenu.generate()
230
else:
231
action.submenu.prune()
232
233
def get_config(self):
234
return {"directory": str(self.directory)}
235
236
def set_panel_position(self, position):
237
self.button.set_direction(panorama_panel.POSITION_TO_ARROW[panorama_panel.OPPOSITE_POSITION[position]])
238