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 • 10.5 kiB
Python script, ASCII text executable
        
            
1
import os
2
import sys
3
import importlib
4
from itertools import accumulate, chain
5
from pathlib import Path
6
import ruamel.yaml as yaml
7
8
os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0"
9
10
from ctypes import CDLL
11
CDLL('libgtk4-layer-shell.so')
12
13
import gi
14
gi.require_version("Gtk", "4.0")
15
gi.require_version("Gtk4LayerShell", "1.0")
16
17
from gi.repository import Gtk, GLib, Gtk4LayerShell, Gdk, Gio
18
19
sys.path.insert(0, str((Path(__file__).parent / "shared").resolve()))
20
21
import panorama_panel
22
23
24
def get_applet_directories():
25
data_home = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share"))
26
data_dirs = [Path(d) for d in os.getenv("XDG_DATA_DIRS", "/usr/local/share:/usr/share").split(":")]
27
28
all_paths = [data_home / "panorama-panel" / "applets"] + [d / "panorama-panel" / "applets" for d in data_dirs]
29
return [d for d in all_paths if d.is_dir()]
30
31
32
def get_config_file():
33
config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
34
35
return config_home / "panorama-panel" / "config.yaml"
36
37
38
class ManagerWindow(Gtk.Window):
39
def __init__(self):
40
super().__init__()
41
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
42
self.set_child(box)
43
switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
44
self.edit_mode_switch = Gtk.Switch()
45
switch_box.append(self.edit_mode_switch)
46
edit_mode_label = Gtk.Label()
47
edit_mode_label.set_text("Panel editing")
48
edit_mode_label.set_mnemonic_widget(self.edit_mode_switch)
49
switch_box.append(edit_mode_label)
50
box.append(switch_box)
51
52
self.edit_mode_switch.connect("state-set", self.set_edit_mode)
53
54
self.set_title("Panel configuration")
55
56
self.connect("close-request", self.on_destroy)
57
58
def on_destroy(self, widget):
59
self.destroy()
60
if self.get_application():
61
self.get_application().manager_window = None
62
63
def set_edit_mode(self, switch, value):
64
print(f"Editing is {value}")
65
66
if value:
67
for panel in panels:
68
panel.set_edit_mode(True)
69
else:
70
for panel in panels:
71
panel.set_edit_mode(False)
72
73
74
class AppletArea(Gtk.Box):
75
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL):
76
super().__init__()
77
78
def set_edit_mode(self, value):
79
child = self.get_first_child()
80
while child is not None:
81
child.set_sensitive(not value)
82
child.set_opacity(0.75 if value else 1)
83
child = child.get_next_sibling()
84
85
86
class Panel(Gtk.Window):
87
def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor, position: Gtk.PositionType = Gtk.PositionType.TOP, size: int = 40, monitor_index: int = 0):
88
super().__init__(application=application)
89
self.set_default_size(800, size)
90
self.set_decorated(False)
91
self.position = position
92
self.monitor_index = monitor_index
93
self.size = size
94
95
Gtk4LayerShell.init_for_window(self)
96
97
Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP)
98
99
Gtk4LayerShell.auto_exclusive_zone_enable(self)
100
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
101
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
102
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
103
104
box = Gtk.CenterBox()
105
106
match position:
107
case Gtk.PositionType.TOP | Gtk.PositionType.BOTTOM:
108
box.set_orientation(Gtk.Orientation.HORIZONTAL)
109
case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT:
110
box.set_orientation(Gtk.Orientation.VERTICAL)
111
112
self.set_child(box)
113
114
self.left_area = AppletArea(orientation=box.get_orientation())
115
self.centre_area = AppletArea(orientation=box.get_orientation())
116
self.right_area = AppletArea(orientation=box.get_orientation())
117
118
box.set_start_widget(self.left_area)
119
box.set_center_widget(self.centre_area)
120
box.set_end_widget(self.right_area)
121
122
# Add a context menu
123
menu = Gio.Menu()
124
125
menu.append("Open _manager", "panel.manager")
126
127
self.context_menu = Gtk.PopoverMenu.new_from_model(menu)
128
self.context_menu.set_has_arrow(False)
129
self.context_menu.set_parent(self)
130
self.context_menu.set_halign(Gtk.Align.START)
131
self.context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
132
133
right_click_controller = Gtk.GestureClick()
134
right_click_controller.set_button(3)
135
right_click_controller.connect("pressed", self.show_context_menu)
136
137
self.add_controller(right_click_controller)
138
139
action_group = Gio.SimpleActionGroup()
140
manager_action = Gio.SimpleAction.new("manager", None)
141
manager_action.connect("activate", self.show_manager)
142
action_group.add_action(manager_action)
143
self.insert_action_group("panel", action_group)
144
145
def set_edit_mode(self, value):
146
for area in (self.left_area, self.centre_area, self.right_area):
147
area.set_edit_mode(value)
148
149
def show_context_menu(self, gesture, n_presses, x, y):
150
rect = Gdk.Rectangle()
151
rect.x = int(x)
152
rect.y = int(y)
153
rect.width = 1
154
rect.height = 1
155
156
self.context_menu.set_pointing_to(rect)
157
self.context_menu.popup()
158
159
def show_manager(self, _0=None, _1=None):
160
print("Showing manager")
161
if self.get_application():
162
if not self.get_application().manager_window:
163
self.get_application().manager_window = ManagerWindow()
164
self.get_application().manager_window.set_application(self.get_application())
165
self.get_application().manager_window.present()
166
167
def get_orientation(self):
168
box = self.get_first_child()
169
return box.get_orientation()
170
171
172
def get_all_subclasses(klass: type) -> list[type]:
173
subclasses = []
174
for subclass in klass.__subclasses__():
175
subclasses.append(subclass)
176
subclasses += get_all_subclasses(subclass)
177
178
return subclasses
179
180
def load_packages_from_dir(dir_path: Path):
181
loaded_modules = []
182
183
for path in dir_path.iterdir():
184
if path.name.startswith("_"):
185
continue
186
187
if path.is_dir() and (path / "__init__.py").exists():
188
module_name = path.name
189
spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py")
190
module = importlib.util.module_from_spec(spec)
191
spec.loader.exec_module(module)
192
loaded_modules.append(module)
193
else:
194
continue
195
196
return loaded_modules
197
198
199
PANEL_POSITIONS = {
200
"top": Gtk.PositionType.TOP,
201
"bottom": Gtk.PositionType.BOTTOM,
202
"left": Gtk.PositionType.LEFT,
203
"right": Gtk.PositionType.RIGHT,
204
}
205
206
207
PANEL_POSITIONS_REVERSE = {
208
Gtk.PositionType.TOP: "top",
209
Gtk.PositionType.BOTTOM: "bottom",
210
Gtk.PositionType.LEFT: "left",
211
Gtk.PositionType.RIGHT: "right",
212
}
213
214
215
class PanoramaPanel(Gtk.Application):
216
def __init__(self):
217
super().__init__(application_id="com.roundabout_host.panorama.panel")
218
self.display = Gdk.Display.get_default()
219
self.monitors = self.display.get_monitors()
220
self.applets_by_name = {}
221
self.panels = []
222
self.manager_window = None
223
224
def do_startup(self):
225
Gtk.Application.do_startup(self)
226
for i, monitor in enumerate(self.monitors):
227
geometry = monitor.get_geometry()
228
print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}")
229
230
all_applets = list(chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories()))
231
print("Applets:")
232
subclasses = get_all_subclasses(panorama_panel.Applet)
233
for subclass in subclasses:
234
if subclass.__name__ in self.applets_by_name:
235
print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.", file=sys.stderr)
236
self.applets_by_name[subclass.__name__] = subclass
237
238
with open(get_config_file(), "r") as config_file:
239
yaml_loader = yaml.YAML(typ="rt")
240
yaml_file = yaml_loader.load(config_file)
241
for panel_data in yaml_file["panels"]:
242
position = PANEL_POSITIONS[panel_data["position"]]
243
monitor_index = panel_data["monitor"]
244
monitor = self.monitors[monitor_index]
245
size = panel_data["size"]
246
247
panel = Panel(self, monitor, position, size, monitor_index)
248
self.panels.append(panel)
249
panel.show()
250
251
print(f"{size}px panel on {position} edge of monitor {monitor_index}")
252
253
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
254
applet_list = panel_data["applets"].get(area_name)
255
if applet_list is None:
256
continue
257
258
for applet in applet_list:
259
item = list(applet.items())[0]
260
AppletClass = self.applets_by_name[item[0]]
261
options = item[1]
262
applet_widget = AppletClass(orientation=panel.get_orientation(), config=options)
263
264
area.append(applet_widget)
265
266
def do_activate(self):
267
Gio.Application.do_activate(self)
268
269
def save_config(self):
270
with open(get_config_file(), "w") as config_file:
271
yaml_writer = yaml.YAML(typ="rt")
272
data = {"panels": []}
273
for panel in self.panels:
274
panel_data = {
275
"position": PANEL_POSITIONS_REVERSE[panel.position],
276
"monitor": panel.monitor_index,
277
"size": panel.size,
278
"applets": {}
279
}
280
281
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
282
panel_data["applets"][area_name] = []
283
applet = area.get_first_child()
284
while applet is not None:
285
panel_data["applets"][area_name].append({
286
applet.__class__.__name__: applet.get_config(),
287
})
288
289
applet = applet.get_next_sibling()
290
291
data["panels"].append(panel_data)
292
293
yaml_writer.dump(data, config_file)
294
295
def do_shutdown(self):
296
print("Shutting down")
297
Gtk.Application.do_shutdown(self)
298
self.save_config()
299
300
301
if __name__ == "__main__":
302
app = PanoramaPanel()
303
app.run(sys.argv)
304