1+ import ctypes
12import logging
23import os
34import shutil
45import subprocess
56import threading
67
7- from PyQt6 .QtCore import QEvent , QSize , Qt , pyqtSignal
8- from PyQt6 .QtGui import QColor , QCursor , QIcon , QPainter , QPixmap
9- from PyQt6 .QtWidgets import QMenu , QSystemTrayIcon
8+ import win32con
9+ import win32gui
10+ from PyQt6 .QtCore import QSize , Qt , pyqtSignal
11+ from PyQt6 .QtGui import QColor , QIcon , QPainter , QPixmap
12+ from PyQt6 .QtWidgets import QSystemTrayIcon
1013
1114from core .bar_manager import BarManager
1215from core .ui .views .about import AboutDialog
1316from core .utils .controller import exit_application , reload_application
1417from core .utils .shell_utils import shell_open
1518from core .utils .update_service import register_update_callback
16- from core .utils .win32 .utils import apply_qmenu_style , disable_autostart , enable_autostart , is_autostart_enabled
19+ from core .utils .win32 .utils import disable_autostart , enable_autostart , is_autostart_enabled
1720from settings import (
1821 APP_NAME ,
1922 DEFAULT_CONFIG_DIRECTORY ,
@@ -42,35 +45,26 @@ def __init__(self, bar_manager: BarManager):
4245 self ._update_signal .connect (self ._set_update_badge )
4346 register_update_callback (self ._update_signal .emit )
4447
45- def eventFilter (self , obj , event ):
46- if event .type () == QEvent .Type .MouseButtonPress :
47- if self .menu and self .menu .isVisible ():
48- global_pos = event .globalPosition ().toPoint ()
49- all_menus = [self .menu ]
50- all_menus += [act .menu () for act in self .menu .actions () if act .menu () and act .menu ().isVisible ()]
51- if not any (m .geometry ().contains (global_pos ) for m in all_menus ):
52- self .menu .hide ()
53- self .menu .deleteLater ()
54- return True
55- return super ().eventFilter (obj , event )
56-
5748 def _on_tray_activated (self , reason ):
5849 if reason == QSystemTrayIcon .ActivationReason .Context :
59- self ._load_context_menu ()
60- self .menu .popup (QCursor .pos ())
61- self .menu .activateWindow ()
50+ self ._show_context_menu ()
6251
6352 def _load_config (self ):
6453 config = self ._bar_manager .config
54+ self .komorebi_enabled = False
55+ self .glazewm_enabled = False
56+
6557 if config and config .komorebi :
6658 self .komorebi_start = config .komorebi .start_command
6759 self .komorebi_stop = config .komorebi .stop_command
6860 self .komorebi_reload = config .komorebi .reload_command
61+ self .komorebi_enabled = any ([self .komorebi_start , self .komorebi_stop , self .komorebi_reload ])
6962
7063 if config and config .glazewm :
7164 self .glazewm_start = config .glazewm .start_command
7265 self .glazewm_stop = config .glazewm .stop_command
7366 self .glazewm_reload = config .glazewm .reload_command
67+ self .glazewm_enabled = any ([self .glazewm_start , self .glazewm_stop , self .glazewm_reload ])
7468
7569 def _load_favicon (self ):
7670 # Get the current directory of the script
@@ -89,120 +83,109 @@ def _set_update_badge(self, release_info=None):
8983 painter .end ()
9084 self .setIcon (QIcon (base ))
9185 self .setToolTip ("Update available" )
92- self ._update_available = True
93-
94- def _load_context_menu (self ):
95- self .menu = QMenu ()
96- self .menu .setWindowModality (Qt .WindowModality .WindowModal )
97- apply_qmenu_style (self .menu )
98- style_sheet = """
99- QMenu {
100- background-color: #202020;
101- color: #ffffff;
102- border:1px solid #303030;
103- padding:5px 0;
104- margin:0;
105- border-radius:8px
106- }
107- QMenu::item {
108- margin:0 4px;
109- padding: 4px 24px 5px 24px;
110- border-radius: 4px;
111- font-size: 11px;
112- font-weight: 600;
113- font-family: 'Segoe UI';
114- }
115- QMenu::item:selected {
116- background-color: #333333;
117- }
118- QMenu::separator {
119- height: 1px;
120- background: #404040;
121- margin: 4px 8px;
122- }
123- QMenu::right-arrow {
124- width: 8px;
125- height: 8px;
126- padding-right:24px;
127- }
128- """
129- self .menu .setStyleSheet (style_sheet )
130-
131- if self ._update_available :
132- update_action = self .menu .addAction ("Update Available" )
133- update_action .triggered .connect (self ._open_update_dialog )
134- self .menu .addSeparator ()
135-
136- open_config_action = self .menu .addAction ("Open Config" )
137- open_config_action .triggered .connect (self ._open_config )
138- if os .path .exists (THEME_EXE_PATH ):
139- yasb_themes_action = self .menu .addAction ("Get Themes" )
140- yasb_themes_action .triggered .connect (lambda : os .startfile (THEME_EXE_PATH ))
141-
142- reload_action = self .menu .addAction ("Reload YASB" )
143- reload_action .triggered .connect (self ._reload_application )
144- self .reload_action = reload_action
145-
146- self .menu .addSeparator ()
147- if self .is_wm_installed ("komorebi" ):
148- komorebi_menu = self .menu .addMenu ("Komorebi" )
149- start_komorebi = komorebi_menu .addAction ("Start Komorebi" )
150- start_komorebi .triggered .connect (
151- lambda checked = False , wm = "Komorebi" , cmd = self .komorebi_start : self ._run_wm_command (wm , cmd )
152- )
153-
154- stop_komorebi = komorebi_menu .addAction ("Stop Komorebi" )
155- stop_komorebi .triggered .connect (
156- lambda checked = False , wm = "Komorebi" , cmd = self .komorebi_stop : self ._run_wm_command (wm , cmd )
157- )
15886
159- reload_komorebi = komorebi_menu .addAction ("Reload Komorebi" )
160- reload_komorebi .triggered .connect (
161- lambda checked = False , wm = "Komorebi" , cmd = self .komorebi_reload : self ._run_wm_command (wm , cmd )
162- )
163-
164- apply_qmenu_style (komorebi_menu )
165-
166- self .menu .addSeparator ()
167-
168- if self .is_wm_installed ("glazewm" ):
169- glazewm_menu = self .menu .addMenu ("Glazewm" )
170- start_glazewm = glazewm_menu .addAction ("Start Glazewm" )
171- start_glazewm .triggered .connect (
172- lambda checked = False , wm = "Glazewm" , cmd = self .glazewm_start : self ._run_wm_command (wm , cmd )
173- )
174-
175- stop_glazewm = glazewm_menu .addAction ("Stop Glazewm" )
176- stop_glazewm .triggered .connect (
177- lambda checked = False , wm = "Glazewm" , cmd = self .glazewm_stop : self ._run_wm_command (wm , cmd )
178- )
179-
180- reload_glazewm = glazewm_menu .addAction ("Reload Glazewm" )
181- reload_glazewm .triggered .connect (
182- lambda checked = False , wm = "Glazewm" , cmd = self .glazewm_reload : self ._run_wm_command (wm , cmd )
87+ def _try_enable_dark_menu (self , hwnd ):
88+ try :
89+ uxtheme = ctypes .WinDLL ("uxtheme.dll" )
90+ uxtheme [135 ](1 ) # undocumented SetPreferredAppMode(AllowDark)
91+ uxtheme [133 ](hwnd , True ) # undocumented AllowDarkModeForWindow
92+ uxtheme [136 ]() # undocumented FlushMenuThemes
93+ except Exception :
94+ logging .debug ("Native dark tray menu unavailable" , exc_info = True )
95+
96+ def _show_context_menu (self ):
97+ """Builds and shows the system tray context menu with dynamic options based on configuration and state."""
98+ hmenu = None
99+ selected_action = None
100+ try :
101+ hmenu = win32gui .CreatePopupMenu ()
102+ actions = {}
103+ cmd_id = [1 ]
104+
105+ def add_item (menu , label , action ):
106+ win32gui .AppendMenu (menu , win32con .MF_STRING , cmd_id [0 ], label )
107+ actions [cmd_id [0 ]] = action
108+ cmd_id [0 ] += 1
109+
110+ def add_sep (menu ):
111+ win32gui .AppendMenu (menu , win32con .MF_SEPARATOR , 0 , "" )
112+
113+ if self ._update_available :
114+ add_item (hmenu , "Update Available" , self ._open_update_dialog )
115+ add_sep (hmenu )
116+
117+ add_item (hmenu , "Open Config" , self ._open_config )
118+ if os .path .exists (THEME_EXE_PATH ):
119+ add_item (hmenu , "Get Themes" , lambda : os .startfile (THEME_EXE_PATH ))
120+ add_item (hmenu , "Reload YASB" , self ._reload_application )
121+ add_sep (hmenu )
122+
123+ if self .komorebi_enabled and self .is_wm_installed ("komorebi" ):
124+ km_sub = win32gui .CreatePopupMenu ()
125+ if self .komorebi_start :
126+ add_item (km_sub , "Start Komorebi" , lambda : self ._run_wm_command ("Komorebi" , self .komorebi_start ))
127+ if self .komorebi_stop :
128+ add_item (km_sub , "Stop Komorebi" , lambda : self ._run_wm_command ("Komorebi" , self .komorebi_stop ))
129+ if self .komorebi_reload :
130+ add_item (km_sub , "Reload Komorebi" , lambda : self ._run_wm_command ("Komorebi" , self .komorebi_reload ))
131+ win32gui .AppendMenu (hmenu , win32con .MF_POPUP , km_sub , "Komorebi" )
132+ add_sep (hmenu )
133+
134+ if self .glazewm_enabled and self .is_wm_installed ("glazewm" ):
135+ gw_sub = win32gui .CreatePopupMenu ()
136+ if self .glazewm_start :
137+ add_item (gw_sub , "Start GlazeWM" , lambda : self ._run_wm_command ("Glazewm" , self .glazewm_start ))
138+ if self .glazewm_stop :
139+ add_item (gw_sub , "Stop GlazeWM" , lambda : self ._run_wm_command ("Glazewm" , self .glazewm_stop ))
140+ if self .glazewm_reload :
141+ add_item (gw_sub , "Reload GlazeWM" , lambda : self ._run_wm_command ("Glazewm" , self .glazewm_reload ))
142+ win32gui .AppendMenu (hmenu , win32con .MF_POPUP , gw_sub , "GlazeWM" )
143+ add_sep (hmenu )
144+
145+ if AUTOSTART_FILE :
146+ if self ._check_startup ():
147+ add_item (hmenu , "Disable Autostart" , self ._disable_startup )
148+ else :
149+ add_item (hmenu , "Enable Autostart" , self ._enable_startup )
150+
151+ add_item (hmenu , "Help" , lambda : self ._open_in_browser (GITHUB_WIKI_URL ))
152+ add_item (hmenu , "About" , self ._show_about_dialog )
153+ add_sep (hmenu )
154+ add_item (hmenu , "Exit" , self ._exit_application )
155+
156+ bars = self ._bar_manager .bars
157+ hwnd = int (bars [0 ].winId ()) if bars else win32gui .GetDesktopWindow ()
158+ x , y = win32gui .GetCursorPos ()
159+ self ._try_enable_dark_menu (hwnd )
160+ try :
161+ win32gui .SetForegroundWindow (hwnd )
162+ except Exception :
163+ logging .debug ("Failed to set tray menu owner as foreground window" , exc_info = True )
164+ cmd = win32gui .TrackPopupMenu (
165+ hmenu ,
166+ win32con .TPM_LEFTALIGN | win32con .TPM_RETURNCMD | win32con .TPM_NONOTIFY ,
167+ x ,
168+ y ,
169+ 0 ,
170+ hwnd ,
171+ None ,
183172 )
184-
185- apply_qmenu_style (glazewm_menu )
186-
187- self .menu .addSeparator ()
188-
189- if AUTOSTART_FILE :
190- if self ._chek_startup ():
191- disable_startup_action = self .menu .addAction ("Disable Autostart" )
192- disable_startup_action .triggered .connect (self ._disable_startup )
193- else :
194- enable_startup_action = self .menu .addAction ("Enable Autostart" )
195- enable_startup_action .triggered .connect (self ._enable_startup )
196-
197- help_action = self .menu .addAction ("Help" )
198- help_action .triggered .connect (lambda : self ._open_in_browser (GITHUB_WIKI_URL ))
199-
200- about_action = self .menu .addAction ("About" )
201- about_action .triggered .connect (self ._show_about_dialog )
202-
203- self .menu .addSeparator ()
204- exit_action = self .menu .addAction ("Exit" )
205- exit_action .triggered .connect (self ._exit_application )
173+ selected_action = actions .get (cmd )
174+ try :
175+ win32gui .PostMessage (hwnd , win32con .WM_NULL , 0 , 0 )
176+ except Exception :
177+ logging .debug ("Failed to post tray menu cleanup message" , exc_info = True )
178+ except Exception :
179+ logging .exception ("Failed to show context menu" )
180+ finally :
181+ if hmenu :
182+ win32gui .DestroyMenu (hmenu )
183+
184+ if selected_action :
185+ try :
186+ selected_action ()
187+ except Exception :
188+ logging .exception ("Tray menu action failed" )
206189
207190 def is_wm_installed (self , wm ) -> bool :
208191 try :
@@ -218,11 +201,8 @@ def _enable_startup(self):
218201 def _disable_startup (self ):
219202 disable_autostart (APP_NAME )
220203
221- def _chek_startup (self ):
222- if is_autostart_enabled (APP_NAME ):
223- return True
224- else :
225- return False
204+ def _check_startup (self ) -> bool :
205+ return bool (is_autostart_enabled (APP_NAME ))
226206
227207 def _open_config (self ):
228208 try :
0 commit comments