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