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/x-script.python • 7.38 kiB
Python script, ASCII text executable
        
            
1
import dataclasses
2
import os
3
from pathlib import Path
4
from pywayland.client import Display
5
from pywayland.protocol.wayland import WlRegistry
6
from pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1 import (
7
ZwlrForeignToplevelManagerV1,
8
ZwlrForeignToplevelHandleV1
9
)
10
import panorama_panel
11
12
import gi
13
14
gi.require_version("Gtk", "4.0")
15
16
from gi.repository import Gtk, GLib, Gio, Gdk
17
18
19
module_directory = Path(__file__).resolve().parent
20
21
22
def split_bytes_into_ints(array: bytes, size: int = 4) -> list[int]:
23
if len(array) % size:
24
raise ValueError(f"The byte string's length must be a multiple of {size}")
25
26
values: list[int] = []
27
for i in range(0, len(array), size):
28
values.append(int.from_bytes(array[i : i+size], byteorder="little"))
29
30
return values
31
32
33
@dataclasses.dataclass
34
class WindowState:
35
minimised: bool
36
maximised: bool
37
fullscreen: bool
38
focused: bool
39
40
@classmethod
41
def from_state_array(cls, array: bytes):
42
values = split_bytes_into_ints(array)
43
instance = cls(False, False, False, False)
44
for value in values:
45
match value:
46
case 0:
47
instance.maximised = True
48
case 1:
49
instance.minimised = True
50
case 2:
51
instance.focused = True
52
case 3:
53
instance.fullscreen = True
54
55
return instance
56
57
58
class WindowButton(Gtk.ToggleButton):
59
def __init__(self, window_id, window_title, **kwargs):
60
super().__init__(**kwargs)
61
62
self.window_id = window_id
63
self.set_has_frame(False)
64
self.label = Gtk.Label()
65
self.icon = Gtk.Image.new_from_icon_name("application-x-executable")
66
box = Gtk.Box()
67
box.append(self.icon)
68
box.append(self.label)
69
self.set_child(box)
70
71
self.window_title = window_title
72
73
self.last_state = False
74
75
@property
76
def window_title(self):
77
return self.label.get_text()
78
79
@window_title.setter
80
def window_title(self, value):
81
self.label.set_text(value)
82
83
def set_icon_from_app_id(self, app_id):
84
# Try getting an icon from the correct theme
85
app_ids = app_id.split()
86
icon_theme = Gtk.IconTheme.get_for_display(self.get_display())
87
88
for app_id in app_ids:
89
if icon_theme.has_icon(app_id):
90
self.icon.set_from_icon_name(app_id)
91
return
92
93
# If that doesn't work, try getting one from .desktop files
94
for app_id in app_ids:
95
try:
96
desktop_file = Gio.DesktopAppInfo.new(app_id + ".desktop")
97
if desktop_file:
98
self.icon.set_from_gicon(desktop_file.get_icon())
99
return
100
except TypeError:
101
# Due to a bug, the constructor may sometimes return C NULL
102
pass
103
104
105
class WFWindowList(panorama_panel.Applet):
106
name = "Wayfire window list"
107
description = "Traditional window list (for Wayfire)"
108
109
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
110
super().__init__(orientation=orientation, config=config)
111
if config is None:
112
config = {}
113
114
self.toplevel_buttons: dict[ZwlrForeignToplevelHandleV1, WindowButton] = {}
115
# This button doesn't belong to any window but is used for the button group and to be
116
# selected when no window is focused
117
self.initial_button = Gtk.ToggleButton()
118
119
self.display = Display()
120
self.display.connect()
121
self.registry = self.display.get_registry()
122
self.registry.dispatcher["global"] = self.on_global
123
self.display.roundtrip()
124
fd = self.display.get_fd()
125
GLib.io_add_watch(fd, GLib.IO_IN, self.on_display_event)
126
127
self.context_menu = self.make_context_menu()
128
panorama_panel.track_popover(self.context_menu)
129
130
right_click_controller = Gtk.GestureClick()
131
right_click_controller.set_button(3)
132
right_click_controller.connect("pressed", self.show_context_menu)
133
134
self.add_controller(right_click_controller)
135
136
action_group = Gio.SimpleActionGroup()
137
options_action = Gio.SimpleAction.new("options", None)
138
options_action.connect("activate", self.show_options)
139
action_group.add_action(options_action)
140
self.insert_action_group("applet", action_group)
141
142
self.options_window = None
143
144
def on_display_event(self, source, condition):
145
if condition == GLib.IO_IN:
146
self.display.dispatch(block=True)
147
return True
148
149
def on_global(self, registry, name, interface, version):
150
if interface == "zwlr_foreign_toplevel_manager_v1":
151
self.print_log("Interface registered")
152
self.manager = registry.bind(name, ZwlrForeignToplevelManagerV1, version)
153
self.manager.dispatcher["toplevel"] = self.on_new_toplevel
154
self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished")
155
self.display.roundtrip()
156
self.display.flush()
157
158
def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1,
159
handle: ZwlrForeignToplevelHandleV1):
160
handle.dispatcher["title"] = lambda h, title: self.on_title_changed(h, title)
161
handle.dispatcher["app_id"] = lambda h, app_id: self.on_app_id_changed(h, app_id)
162
handle.dispatcher["state"] = lambda h, states: self.on_state_changed(h, states)
163
handle.dispatcher["closed"] = lambda h: self.on_closed(h)
164
165
def on_title_changed(self, handle, title):
166
if handle not in self.toplevel_buttons:
167
button = WindowButton(handle, title)
168
button.set_group(self.initial_button)
169
button.connect("clicked", lambda *a: self.on_button_click(handle))
170
self.toplevel_buttons[handle] = button
171
self.append(button)
172
else:
173
button = self.toplevel_buttons[handle]
174
button.window_title = title
175
176
def on_state_changed(self, handle, states):
177
if handle in self.toplevel_buttons:
178
state_info = WindowState.from_state_array(states)
179
button = self.toplevel_buttons[handle]
180
if state_info.focused:
181
button.set_active(True)
182
else:
183
self.initial_button.set_active(True)
184
185
def on_app_id_changed(self, handle, app_id):
186
if handle in self.toplevel_buttons:
187
button = self.toplevel_buttons[handle]
188
button.set_icon_from_app_id(app_id)
189
190
def on_closed(self, handle):
191
if handle in self.toplevel_buttons:
192
self.remove(self.toplevel_buttons[handle])
193
self.toplevel_buttons.pop(handle)
194
195
def make_context_menu(self):
196
menu = Gio.Menu()
197
menu.append("Window list _options", "applet.options")
198
context_menu = Gtk.PopoverMenu.new_from_model(menu)
199
context_menu.set_has_arrow(False)
200
context_menu.set_parent(self)
201
context_menu.set_halign(Gtk.Align.START)
202
context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
203
return context_menu
204
205
def show_context_menu(self, gesture, n_presses, x, y):
206
rect = Gdk.Rectangle()
207
rect.x = int(x)
208
rect.y = int(y)
209
rect.width = 1
210
rect.height = 1
211
212
self.context_menu.set_pointing_to(rect)
213
self.context_menu.popup()
214
215
def show_options(self, _0=None, _1=None):
216
pass
217
218
def get_config(self):
219
return {}
220