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
@@ -168,16 +171,13 @@ def parse_options(self, options: Dict[str, Any]):
168171 if "background" in options :
169172 self .start_in_background = True
170173
171- @staticmethod
172- def _do_power_button (_widget ):
174+ def _do_power_button (self , _widget ):
173175 """
174176 Run xfce4's default logout button. Possible enhancement would be
175177 providing our own tiny program.
176178 """
177179 # pylint: disable=consider-using-with
178- current_environs = os .environ .get ('XDG_CURRENT_DESKTOP' , '' ).split (':' )
179-
180- if 'KDE' in current_environs :
180+ if self .kde :
181181 dbus = Gio .bus_get_sync (Gio .BusType .SESSION , None )
182182 proxy = Gio .DBusProxy .new_sync (
183183 dbus , # dbus
@@ -203,21 +203,67 @@ def reposition(self):
203203 assert self .main_window
204204 match self .appmenu_position :
205205 case 'top-left' :
206- self .main_window .move (0 , 0 )
206+ if self .layer_shell :
207+ GtkLayerShell .set_anchor (self .main_window ,
208+ GtkLayerShell .Edge .LEFT , True )
209+ GtkLayerShell .set_anchor (self .main_window ,
210+ GtkLayerShell .Edge .TOP , True )
211+ else :
212+ self .main_window .move (0 , 0 )
207213 case 'top-right' :
208- self .main_window .move (
209- self .main_window .get_screen ().get_width () - \
210- self .main_window .get_size ().width , 0 )
214+ if self .layer_shell :
215+ GtkLayerShell .set_anchor (self .main_window ,
216+ GtkLayerShell .Edge .RIGHT , True )
217+ GtkLayerShell .set_anchor (self .main_window ,
218+ GtkLayerShell .Edge .TOP , True )
219+ else :
220+ self .main_window .move (
221+ self .main_window .get_screen ().get_width () -
222+ self .main_window .get_size ().width , 0 )
211223 case 'bottom-left' :
212- self .main_window .move (0 ,
213- self .main_window .get_screen ().get_height () - \
214- self .main_window .get_size ().height )
224+ if self .layer_shell :
225+ GtkLayerShell .set_anchor (self .main_window ,
226+ GtkLayerShell .Edge .LEFT , True )
227+ GtkLayerShell .set_anchor (self .main_window ,
228+ GtkLayerShell .Edge .BOTTOM , True )
229+ else :
230+ self .main_window .move (0 ,
231+ self .main_window .get_screen ().get_height () -
232+ self .main_window .get_size ().height )
215233 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 )
234+ if self .layer_shell :
235+ GtkLayerShell .set_anchor (self .main_window ,
236+ GtkLayerShell .Edge .RIGHT , True )
237+ GtkLayerShell .set_anchor (self .main_window ,
238+ GtkLayerShell .Edge .BOTTOM , True )
239+ else :
240+ self .main_window .move (
241+ self .main_window .get_screen ().get_width () -
242+ self .main_window .get_size ().width ,
243+ self .main_window .get_screen ().get_height () -
244+ self .main_window .get_size ().height )
245+
246+ def __present (self ) -> None :
247+ assert self .main_window is not None
248+ self .reposition ()
249+ self .main_window .present ()
250+ if not self .layer_shell :
251+ return
252+ # Under Wayland, the window size must be re-requested
253+ # every time the window is shown.
254+ current_width = self .main_window .get_allocated_width ()
255+ current_height = self .main_window .get_allocated_height ()
256+ # set size if too big
257+ max_height = int (self .main_window .get_screen ().get_height () * 0.9 )
258+ assert max_height > 0
259+ # The default for layer shell is no keyboard input.
260+ # Explicitly request exclusive access to the keyboard.
261+ GtkLayerShell .set_keyboard_mode (self .main_window ,
262+ GtkLayerShell .KeyboardMode .EXCLUSIVE )
263+ # Work around https://github.com/wmww/gtk-layer-shell/issues/167
264+ # by explicitly setting the window size.
265+ self .main_window .set_size_request (current_width ,
266+ min (current_height , max_height ))
221267
222268 def do_activate (self , * args , ** kwargs ):
223269 """
@@ -234,12 +280,24 @@ def do_activate(self, *args, **kwargs):
234280 self .reposition ()
235281 self .main_window .show_all ()
236282 self .initialize_state ()
237- # set size if too big
283+ current_width = self . main_window . get_allocated_width ()
238284 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 ))
285+ # set size if too big
286+ max_height = int (self .main_window .get_screen ().get_height () * 0.9 )
287+ assert max_height > 0
288+ if self .layer_shell :
289+ if not self .start_in_background :
290+ # The default for layer shell is no keyboard input.
291+ # Explicitly request exclusive access to the keyboard.
292+ GtkLayerShell .set_keyboard_mode (self .main_window ,
293+ GtkLayerShell .KeyboardMode .EXCLUSIVE )
294+ # Work around https://github.com/wmww/gtk-layer-shell/issues/167
295+ # by explicitly setting the window size.
296+ self .main_window .set_size_request (
297+ current_width ,
298+ min (current_height , max_height ))
299+ elif current_height > max_height :
300+ self .main_window .resize (current_height , max_height )
243301
244302 # grab a focus on the initially selected page so that keyboard
245303 # navigation works
@@ -261,8 +319,7 @@ def do_activate(self, *args, **kwargs):
261319 if self .main_window .is_visible () and not self .keep_visible :
262320 self .main_window .hide ()
263321 else :
264- self .reposition ()
265- self .main_window .present ()
322+ self .__present ()
266323
267324 def hide_menu (self ):
268325 """
@@ -331,6 +388,7 @@ def perform_setup(self):
331388 self .builder .add_from_file (str (path ))
332389
333390 self .main_window = self .builder .get_object ('main_window' )
391+ self .layer_shell = GtkLayerShell .is_supported ()
334392 self .main_notebook = self .builder .get_object ('main_notebook' )
335393
336394 self .main_window .set_events (Gdk .EventMask .FOCUS_CHANGE_MASK )
@@ -375,6 +433,10 @@ def perform_setup(self):
375433 'domain-feature-delete:' + feature ,
376434 self ._update_settings )
377435
436+ if self .layer_shell :
437+ GtkLayerShell .init_for_window (self .main_window )
438+ GtkLayerShell .set_exclusive_zone (self .main_window , 0 )
439+
378440 def load_style (self , * _args ):
379441 """Load appropriate CSS stylesheet and associated properties."""
380442 light_ref = (importlib .resources .files ('qubes_menu' ) /
@@ -415,6 +477,9 @@ def load_settings(self):
415477 position = local_vm .features .get (POSITION_FEATURE , "mouse" )
416478 if position not in POSITION_LIST :
417479 position = "mouse"
480+ if position == "mouse" and self .layer_shell :
481+ # "mouse" unsupported under Wayland
482+ position = "bottom-left" if self .kde else "top-left"
418483 self .appmenu_position = position
419484
420485 for handler in self .handlers .values ():
0 commit comments