2828
2929import gi
3030gi .require_version ('Gtk' , '3.0' )
31- from gi .repository import Gtk , Gdk , GLib , Gio
31+ gi .require_version ('GtkLayerShell' , '0.1' )
32+ from gi .repository import Gtk , Gdk , GLib , Gio , GtkLayerShell
3233
3334import gbulb
3435gbulb .install ()
@@ -93,10 +94,12 @@ def __init__(self, qapp, dispatcher):
9394 self .initial_page = "app_page"
9495 self .sort_running = False
9596 self .start_in_background = False
97+ self .kde = "KDE" in os .getenv ("XDG_CURRENT_DESKTOP" , "" ).split (":" )
9698
9799 self ._add_cli_options ()
98100
99101 self .builder : Optional [Gtk .Builder ] = None
102+ self .layer_shell : bool = False
100103 self .main_window : Optional [Gtk .Window ] = None
101104 self .main_notebook : Optional [Gtk .Notebook ] = None
102105
@@ -144,6 +147,15 @@ def _add_cli_options(self):
144147 None ,
145148 )
146149
150+ self .add_main_option (
151+ "position" ,
152+ 0 ,
153+ GLib .OptionFlags .NONE ,
154+ GLib .OptionArg .STRING ,
155+ "Position the window in the selected corner of the screen" ,
156+ None ,
157+ )
158+
147159 def do_command_line (self , command_line ):
148160 """
149161 Handle CLI arguments. This method overrides default do_command_line
@@ -167,17 +179,20 @@ def parse_options(self, options: Dict[str, Any]):
167179 self .initial_page = PAGE_LIST [int (options ["page" ])]
168180 if "background" in options :
169181 self .start_in_background = True
170-
171- @staticmethod
172- def _do_power_button (_widget ):
182+ if "position" in options :
183+ position = options ["position" ]
184+ if position == "mouse" or position not in POSITION_LIST :
185+ print (f"Invalid value for \" position\" : { position !r} " ,
186+ file = sys .stderr )
187+ sys .exit (1 )
188+
189+ def _do_power_button (self , _widget ):
173190 """
174191 Run xfce4's default logout button. Possible enhancement would be
175192 providing our own tiny program.
176193 """
177194 # pylint: disable=consider-using-with
178- current_environs = os .environ .get ('XDG_CURRENT_DESKTOP' , '' ).split (':' )
179-
180- if 'KDE' in current_environs :
195+ if self .kde :
181196 dbus = Gio .bus_get_sync (Gio .BusType .SESSION , None )
182197 proxy = Gio .DBusProxy .new_sync (
183198 dbus , # dbus
@@ -203,21 +218,67 @@ def reposition(self):
203218 assert self .main_window
204219 match self .appmenu_position :
205220 case 'top-left' :
206- self .main_window .move (0 , 0 )
221+ if self .layer_shell :
222+ GtkLayerShell .set_anchor (self .main_window ,
223+ GtkLayerShell .Edge .LEFT , True )
224+ GtkLayerShell .set_anchor (self .main_window ,
225+ GtkLayerShell .Edge .TOP , True )
226+ else :
227+ self .main_window .move (0 , 0 )
207228 case 'top-right' :
208- self .main_window .move (
209- self .main_window .get_screen ().get_width () - \
210- self .main_window .get_size ().width , 0 )
229+ if self .layer_shell :
230+ GtkLayerShell .set_anchor (self .main_window ,
231+ GtkLayerShell .Edge .RIGHT , True )
232+ GtkLayerShell .set_anchor (self .main_window ,
233+ GtkLayerShell .Edge .TOP , True )
234+ else :
235+ self .main_window .move (
236+ self .main_window .get_screen ().get_width () -
237+ self .main_window .get_size ().width , 0 )
211238 case 'bottom-left' :
212- self .main_window .move (0 ,
213- self .main_window .get_screen ().get_height () - \
214- self .main_window .get_size ().height )
239+ if self .layer_shell :
240+ GtkLayerShell .set_anchor (self .main_window ,
241+ GtkLayerShell .Edge .LEFT , True )
242+ GtkLayerShell .set_anchor (self .main_window ,
243+ GtkLayerShell .Edge .BOTTOM , True )
244+ else :
245+ self .main_window .move (0 ,
246+ self .main_window .get_screen ().get_height () -
247+ self .main_window .get_size ().height )
215248 case 'bottom-right' :
216- self .main_window .move (
217- self .main_window .get_screen ().get_width () - \
218- self .main_window .get_size ().width ,
219- self .main_window .get_screen ().get_height () - \
220- self .main_window .get_size ().height )
249+ if self .layer_shell :
250+ GtkLayerShell .set_anchor (self .main_window ,
251+ GtkLayerShell .Edge .RIGHT , True )
252+ GtkLayerShell .set_anchor (self .main_window ,
253+ GtkLayerShell .Edge .BOTTOM , True )
254+ else :
255+ self .main_window .move (
256+ self .main_window .get_screen ().get_width () -
257+ self .main_window .get_size ().width ,
258+ self .main_window .get_screen ().get_height () -
259+ self .main_window .get_size ().height )
260+
261+ def __present (self ) -> None :
262+ assert self .main_window is not None
263+ self .reposition ()
264+ self .main_window .present ()
265+ if not self .layer_shell :
266+ return
267+ # Under Wayland, the window size must be re-requested
268+ # every time the window is shown.
269+ current_width = self .main_window .get_allocated_width ()
270+ current_height = self .main_window .get_allocated_height ()
271+ # set size if too big
272+ max_height = int (self .main_window .get_screen ().get_height () * 0.9 )
273+ assert max_height > 0
274+ # The default for layer shell is no keyboard input.
275+ # Explicitly request exclusive access to the keyboard.
276+ GtkLayerShell .set_keyboard_mode (self .main_window ,
277+ GtkLayerShell .KeyboardMode .EXCLUSIVE )
278+ # Work around https://github.com/wmww/gtk-layer-shell/issues/167
279+ # by explicitly setting the window size.
280+ self .main_window .set_size_request (current_width ,
281+ min (current_height , max_height ))
221282
222283 def do_activate (self , * args , ** kwargs ):
223284 """
@@ -234,12 +295,24 @@ def do_activate(self, *args, **kwargs):
234295 self .reposition ()
235296 self .main_window .show_all ()
236297 self .initialize_state ()
237- # set size if too big
298+ current_width = self . main_window . get_allocated_width ()
238299 current_height = self .main_window .get_allocated_height ()
239- max_height = self .main_window .get_screen ().get_height () * 0.9
240- if current_height > max_height :
241- self .main_window .resize (self .main_window .get_allocated_width (),
242- int (max_height ))
300+ # set size if too big
301+ max_height = int (self .main_window .get_screen ().get_height () * 0.9 )
302+ assert max_height > 0
303+ if self .layer_shell :
304+ if not self .start_in_background :
305+ # The default for layer shell is no keyboard input.
306+ # Explicitly request exclusive access to the keyboard.
307+ GtkLayerShell .set_keyboard_mode (self .main_window ,
308+ GtkLayerShell .KeyboardMode .EXCLUSIVE )
309+ # Work around https://github.com/wmww/gtk-layer-shell/issues/167
310+ # by explicitly setting the window size.
311+ self .main_window .set_size_request (
312+ current_width ,
313+ min (current_height , max_height ))
314+ elif current_height > max_height :
315+ self .main_window .resize (current_height , max_height )
243316
244317 # grab a focus on the initially selected page so that keyboard
245318 # navigation works
@@ -261,8 +334,7 @@ def do_activate(self, *args, **kwargs):
261334 if self .main_window .is_visible () and not self .keep_visible :
262335 self .main_window .hide ()
263336 else :
264- self .reposition ()
265- self .main_window .present ()
337+ self .__present ()
266338
267339 def hide_menu (self ):
268340 """
@@ -331,6 +403,7 @@ def perform_setup(self):
331403 self .builder .add_from_file (str (path ))
332404
333405 self .main_window = self .builder .get_object ('main_window' )
406+ self .layer_shell = GtkLayerShell .is_supported ()
334407 self .main_notebook = self .builder .get_object ('main_notebook' )
335408
336409 self .main_window .set_events (Gdk .EventMask .FOCUS_CHANGE_MASK )
@@ -375,6 +448,10 @@ def perform_setup(self):
375448 'domain-feature-delete:' + feature ,
376449 self ._update_settings )
377450
451+ if self .layer_shell :
452+ GtkLayerShell .init_for_window (self .main_window )
453+ GtkLayerShell .set_exclusive_zone (self .main_window , 0 )
454+
378455 def load_style (self , * _args ):
379456 """Load appropriate CSS stylesheet and associated properties."""
380457 light_ref = (importlib .resources .files ('qubes_menu' ) /
@@ -415,6 +492,9 @@ def load_settings(self):
415492 position = local_vm .features .get (POSITION_FEATURE , "mouse" )
416493 if position not in POSITION_LIST :
417494 position = "mouse"
495+ if position == "mouse" and self .layer_shell :
496+ # "mouse" unsupported under Wayland
497+ position = "bottom-left" if self .kde else "top-left"
418498 self .appmenu_position = position
419499
420500 for handler in self .handlers .values ():
0 commit comments