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

 main.py

View raw Download
text/x-script.python • 18.02 kiB
Python script, ASCII text executable
        
            
1
from __future__ import annotations
2
3
import os
4
import sys
5
import importlib
6
from itertools import accumulate, chain
7
from pathlib import Path
8
import ruamel.yaml as yaml
9
10
os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0"
11
12
from ctypes import CDLL
13
CDLL('libgtk4-layer-shell.so')
14
15
import gi
16
gi.require_version("Gtk", "4.0")
17
gi.require_version("Gtk4LayerShell", "1.0")
18
19
from gi.repository import Gtk, GLib, Gtk4LayerShell, Gdk, Gio, GObject
20
21
sys.path.insert(0, str((Path(__file__).parent / "shared").resolve()))
22
23
import panorama_panel
24
25
26
@Gtk.Template(filename="panel-configurator.ui")
27
class PanelConfigurator(Gtk.Frame):
28
__gtype_name__ = "PanelConfigurator"
29
30
panel_size_adjustment: Gtk.Adjustment = Gtk.Template.Child()
31
monitor_number_adjustment: Gtk.Adjustment = Gtk.Template.Child()
32
top_position_radio: Gtk.CheckButton = Gtk.Template.Child()
33
bottom_position_radio: Gtk.CheckButton = Gtk.Template.Child()
34
left_position_radio: Gtk.CheckButton = Gtk.Template.Child()
35
right_position_radio: Gtk.CheckButton = Gtk.Template.Child()
36
37
def __init__(self, panel: Panel, **kwargs):
38
super().__init__(**kwargs)
39
self.panel = panel
40
self.panel_size_adjustment.set_value(panel.size)
41
42
match self.panel.position:
43
case Gtk.PositionType.TOP:
44
self.top_position_radio.set_active(True)
45
case Gtk.PositionType.BOTTOM:
46
self.bottom_position_radio.set_active(True)
47
case Gtk.PositionType.LEFT:
48
self.left_position_radio.set_active(True)
49
case Gtk.PositionType.RIGHT:
50
self.right_position_radio.set_active(True)
51
52
self.top_position_radio.panel_position_target = Gtk.PositionType.TOP
53
self.bottom_position_radio.panel_position_target = Gtk.PositionType.BOTTOM
54
self.left_position_radio.panel_position_target = Gtk.PositionType.LEFT
55
self.right_position_radio.panel_position_target = Gtk.PositionType.RIGHT
56
57
@Gtk.Template.Callback()
58
def update_panel_size(self, adjustment: Gtk.Adjustment):
59
if not self.get_root():
60
return
61
self.panel.set_size(int(adjustment.get_value()))
62
63
@Gtk.Template.Callback()
64
def move_panel(self, button: Gtk.CheckButton):
65
if not self.get_root():
66
return
67
if not button.get_active():
68
return
69
print("Moving panel")
70
self.panel.set_position(button.panel_position_target)
71
self.update_panel_size(self.panel_size_adjustment)
72
73
# Make the applets aware of the changed orientation
74
for area in (self.panel.left_area, self.panel.centre_area, self.panel.right_area):
75
applet = area.get_first_child()
76
while applet:
77
applet.set_orientation(self.panel.get_orientation())
78
applet.set_panel_position(self.panel.position)
79
applet.queue_resize()
80
applet = applet.get_next_sibling()
81
82
@Gtk.Template.Callback()
83
def move_to_monitor(self, adjustment: Gtk.Adjustment):
84
if not self.get_root():
85
return
86
app: PanoramaPanel = self.get_root().get_application()
87
monitor = app.monitors[int(self.monitor_number_adjustment.get_value())]
88
self.panel.unmap()
89
Gtk4LayerShell.set_monitor(self.panel, monitor)
90
self.panel.show()
91
92
93
PANEL_POSITIONS_HUMAN = {
94
Gtk.PositionType.TOP: "top",
95
Gtk.PositionType.BOTTOM: "bottom",
96
Gtk.PositionType.LEFT: "left",
97
Gtk.PositionType.RIGHT: "right",
98
}
99
100
101
@Gtk.Template(filename="panel-manager.ui")
102
class PanelManager(Gtk.Window):
103
__gtype_name__ = "PanelManager"
104
105
panel_editing_switch: Gtk.Switch = Gtk.Template.Child()
106
panel_stack: Gtk.Stack = Gtk.Template.Child()
107
108
def __init__(self, application: Gtk.Application, **kwargs):
109
super().__init__(application=application, **kwargs)
110
111
self.connect("close-request", lambda *args: self.destroy())
112
113
action_group = Gio.SimpleActionGroup()
114
115
self.next_panel_action = Gio.SimpleAction(name="next-panel")
116
action_group.add_action(self.next_panel_action)
117
self.next_panel_action.connect("activate", lambda *args: self.panel_stack.set_visible_child(self.panel_stack.get_visible_child().get_next_sibling()))
118
119
self.previous_panel_action = Gio.SimpleAction(name="previous-panel")
120
action_group.add_action(self.previous_panel_action)
121
self.previous_panel_action.connect("activate", lambda *args: self.panel_stack.set_visible_child(self.panel_stack.get_visible_child().get_prev_sibling()))
122
123
self.insert_action_group("win", action_group)
124
if isinstance(self.get_application(), PanoramaPanel):
125
self.panel_editing_switch.set_active(application.edit_mode)
126
self.panel_editing_switch.connect("state-set", self.set_edit_mode)
127
128
self.connect("close-request", lambda *args: self.destroy())
129
130
if isinstance(self.get_application(), PanoramaPanel):
131
app: PanoramaPanel = self.get_application()
132
for panel in app.panels:
133
configurator = PanelConfigurator(panel)
134
self.panel_stack.add_child(configurator)
135
configurator.monitor_number_adjustment.set_upper(len(app.monitors))
136
137
self.panel_stack.set_visible_child(self.panel_stack.get_first_child())
138
self.panel_stack.connect("notify::visible-child", self.set_visible_panel)
139
self.panel_stack.notify("visible-child")
140
141
def set_visible_panel(self, stack: Gtk.Stack, pspec: GObject.ParamSpec):
142
panel: Panel = stack.get_visible_child().panel
143
144
self.next_panel_action.set_enabled(stack.get_visible_child().get_next_sibling() is not None)
145
self.previous_panel_action.set_enabled(stack.get_visible_child().get_prev_sibling() is not None)
146
147
def set_edit_mode(self, switch, value):
148
if isinstance(self.get_application(), PanoramaPanel):
149
self.get_application().set_edit_mode(value)
150
151
@Gtk.Template.Callback()
152
def save_settings(self, *args):
153
if isinstance(self.get_application(), PanoramaPanel):
154
print("Saving settings as user requested")
155
self.get_application().save_config()
156
157
158
def get_applet_directories():
159
data_home = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share"))
160
data_dirs = [Path(d) for d in os.getenv("XDG_DATA_DIRS", "/usr/local/share:/usr/share").split(":")]
161
162
all_paths = [data_home / "panorama-panel" / "applets"] + [d / "panorama-panel" / "applets" for d in data_dirs]
163
return [d for d in all_paths if d.is_dir()]
164
165
166
def get_config_file():
167
config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
168
169
return config_home / "panorama-panel" / "config.yaml"
170
171
172
class AppletArea(Gtk.Box):
173
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL):
174
super().__init__()
175
176
self.drop_target = Gtk.DropTarget.new(GObject.TYPE_UINT64, Gdk.DragAction.MOVE)
177
self.drop_target.set_gtypes([GObject.TYPE_UINT64])
178
self.drop_target.connect("drop", self.drop_applet)
179
180
def drop_applet(self, drop_target: Gtk.DropTarget, value: int, x: float, y: float):
181
print(f"Dropping applet: {value}")
182
applet: panorama_panel.Applet = self.get_root().get_application().drags.pop(value)
183
print(f"Type: {applet.__class__.__name__}")
184
old_area: AppletArea = applet.get_parent()
185
old_area.remove(applet)
186
self.append(applet)
187
return True
188
189
def set_edit_mode(self, value):
190
child = self.get_first_child()
191
while child is not None:
192
if value:
193
child.make_draggable()
194
else:
195
child.restore_drag()
196
child.set_opacity(0.75 if value else 1)
197
child = child.get_next_sibling()
198
199
if value:
200
self.add_controller(self.drop_target)
201
else:
202
self.remove_controller(self.drop_target)
203
204
205
class Panel(Gtk.Window):
206
def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor, position: Gtk.PositionType = Gtk.PositionType.TOP, size: int = 40, monitor_index: int = 0):
207
super().__init__(application=application)
208
self.set_decorated(False)
209
self.position = None
210
self.monitor_index = monitor_index
211
212
Gtk4LayerShell.init_for_window(self)
213
Gtk4LayerShell.set_monitor(self, monitor)
214
215
Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP)
216
217
Gtk4LayerShell.auto_exclusive_zone_enable(self)
218
219
box = Gtk.CenterBox()
220
221
self.set_child(box)
222
self.set_position(position)
223
self.set_size(size)
224
225
self.left_area = AppletArea(orientation=box.get_orientation())
226
self.centre_area = AppletArea(orientation=box.get_orientation())
227
self.right_area = AppletArea(orientation=box.get_orientation())
228
229
box.set_start_widget(self.left_area)
230
box.set_center_widget(self.centre_area)
231
box.set_end_widget(self.right_area)
232
233
# Add a context menu
234
menu = Gio.Menu()
235
236
menu.append("Open _manager", "panel.manager")
237
238
self.context_menu = Gtk.PopoverMenu.new_from_model(menu)
239
self.context_menu.set_has_arrow(False)
240
self.context_menu.set_parent(self)
241
self.context_menu.set_halign(Gtk.Align.START)
242
self.context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
243
244
right_click_controller = Gtk.GestureClick()
245
right_click_controller.set_button(3)
246
right_click_controller.connect("pressed", self.show_context_menu)
247
248
self.add_controller(right_click_controller)
249
250
action_group = Gio.SimpleActionGroup()
251
manager_action = Gio.SimpleAction.new("manager", None)
252
manager_action.connect("activate", self.show_manager)
253
action_group.add_action(manager_action)
254
self.insert_action_group("panel", action_group)
255
256
def set_edit_mode(self, value):
257
for area in (self.left_area, self.centre_area, self.right_area):
258
area.set_edit_mode(value)
259
260
def show_context_menu(self, gesture, n_presses, x, y):
261
rect = Gdk.Rectangle()
262
rect.x = int(x)
263
rect.y = int(y)
264
rect.width = 1
265
rect.height = 1
266
267
self.context_menu.set_pointing_to(rect)
268
self.context_menu.popup()
269
270
def show_manager(self, _0=None, _1=None):
271
print("Showing manager")
272
if self.get_application():
273
if not self.get_application().manager_window:
274
self.get_application().manager_window = PanelManager(self.get_application())
275
self.get_application().manager_window.connect("close-request", self.get_application().reset_manager_window)
276
self.get_application().manager_window.present()
277
278
def get_orientation(self):
279
box = self.get_first_child()
280
return box.get_orientation()
281
282
def set_size(self, value: int):
283
self.size = int(value)
284
if self.get_orientation() == Gtk.Orientation.HORIZONTAL:
285
self.set_size_request(800, self.size)
286
self.set_default_size(800, self.size)
287
else:
288
self.set_size_request(self.size, 600)
289
self.set_default_size(self.size, 600)
290
291
def set_position(self, position: Gtk.PositionType):
292
self.position = position
293
match self.position:
294
case Gtk.PositionType.TOP:
295
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, False)
296
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
297
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
298
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
299
case Gtk.PositionType.BOTTOM:
300
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, False)
301
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
302
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
303
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
304
case Gtk.PositionType.LEFT:
305
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, False)
306
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
307
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
308
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
309
case Gtk.PositionType.RIGHT:
310
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, False)
311
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
312
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
313
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
314
box = self.get_first_child()
315
match self.position:
316
case Gtk.PositionType.TOP | Gtk.PositionType.BOTTOM:
317
box.set_orientation(Gtk.Orientation.HORIZONTAL)
318
case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT:
319
box.set_orientation(Gtk.Orientation.VERTICAL)
320
321
def get_all_subclasses(klass: type) -> list[type]:
322
subclasses = []
323
for subclass in klass.__subclasses__():
324
subclasses.append(subclass)
325
subclasses += get_all_subclasses(subclass)
326
327
return subclasses
328
329
def load_packages_from_dir(dir_path: Path):
330
loaded_modules = []
331
332
for path in dir_path.iterdir():
333
if path.name.startswith("_"):
334
continue
335
336
if path.is_dir() and (path / "__init__.py").exists():
337
module_name = path.name
338
spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py")
339
module = importlib.util.module_from_spec(spec)
340
spec.loader.exec_module(module)
341
loaded_modules.append(module)
342
else:
343
continue
344
345
return loaded_modules
346
347
348
PANEL_POSITIONS = {
349
"top": Gtk.PositionType.TOP,
350
"bottom": Gtk.PositionType.BOTTOM,
351
"left": Gtk.PositionType.LEFT,
352
"right": Gtk.PositionType.RIGHT,
353
}
354
355
356
PANEL_POSITIONS_REVERSE = {
357
Gtk.PositionType.TOP: "top",
358
Gtk.PositionType.BOTTOM: "bottom",
359
Gtk.PositionType.LEFT: "left",
360
Gtk.PositionType.RIGHT: "right",
361
}
362
363
364
class PanoramaPanel(Gtk.Application):
365
def __init__(self):
366
super().__init__(application_id="com.roundabout_host.panorama.panel")
367
self.display = Gdk.Display.get_default()
368
self.monitors = self.display.get_monitors()
369
self.applets_by_name: dict[str, panorama_panel.Applet] = {}
370
self.panels: list[Panel] = []
371
self.manager_window = None
372
self.edit_mode = False
373
self.drags = {}
374
375
def do_startup(self):
376
Gtk.Application.do_startup(self)
377
for i, monitor in enumerate(self.monitors):
378
geometry = monitor.get_geometry()
379
print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}")
380
381
all_applets = list(chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories()))
382
print("Applets:")
383
subclasses = get_all_subclasses(panorama_panel.Applet)
384
for subclass in subclasses:
385
if subclass.__name__ in self.applets_by_name:
386
print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.", file=sys.stderr)
387
self.applets_by_name[subclass.__name__] = subclass
388
389
with open(get_config_file(), "r") as config_file:
390
yaml_loader = yaml.YAML(typ="rt")
391
yaml_file = yaml_loader.load(config_file)
392
for panel_data in yaml_file["panels"]:
393
position = PANEL_POSITIONS[panel_data["position"]]
394
monitor_index = panel_data["monitor"]
395
if monitor_index >= len(self.monitors):
396
continue
397
monitor = self.monitors[monitor_index]
398
size = panel_data["size"]
399
400
panel = Panel(self, monitor, position, size, monitor_index)
401
self.panels.append(panel)
402
panel.show()
403
404
print(f"{size}px panel on {position} edge of monitor {monitor_index}")
405
406
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
407
applet_list = panel_data["applets"].get(area_name)
408
if applet_list is None:
409
continue
410
411
for applet in applet_list:
412
item = list(applet.items())[0]
413
AppletClass = self.applets_by_name[item[0]]
414
options = item[1]
415
applet_widget = AppletClass(orientation=panel.get_orientation(), config=options)
416
applet_widget.set_panel_position(panel.position)
417
418
area.append(applet_widget)
419
420
def do_activate(self):
421
Gio.Application.do_activate(self)
422
423
def save_config(self):
424
with open(get_config_file(), "w") as config_file:
425
yaml_writer = yaml.YAML(typ="rt")
426
data = {"panels": []}
427
for panel in self.panels:
428
panel_data = {
429
"position": PANEL_POSITIONS_REVERSE[panel.position],
430
"monitor": panel.monitor_index,
431
"size": panel.size,
432
"applets": {}
433
}
434
435
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
436
panel_data["applets"][area_name] = []
437
applet = area.get_first_child()
438
while applet is not None:
439
panel_data["applets"][area_name].append({
440
applet.__class__.__name__: applet.get_config(),
441
})
442
443
applet = applet.get_next_sibling()
444
445
data["panels"].append(panel_data)
446
447
yaml_writer.dump(data, config_file)
448
449
def do_shutdown(self):
450
print("Shutting down")
451
Gtk.Application.do_shutdown(self)
452
self.save_config()
453
454
def set_edit_mode(self, value):
455
self.edit_mode = value
456
for panel in self.panels:
457
panel.set_edit_mode(value)
458
459
def reset_manager_window(self, *args):
460
self.manager_window = None
461
462
463
if __name__ == "__main__":
464
app = PanoramaPanel()
465
app.run(sys.argv)
466