Skip to content

Commit

Permalink
Try to prevent crashes when using the Windows menu
Browse files Browse the repository at this point in the history
  • Loading branch information
probonopd committed Feb 18, 2025
1 parent 4cef637 commit 06ff3dd
Showing 1 changed file with 101 additions and 155 deletions.
256 changes: 101 additions & 155 deletions menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,165 +499,111 @@ def shutdown():
except Exception as e:
QtWidgets.QMessageBox.critical(None, "Error", f"Failed to shut down: {e}")

def win32_populate_windows_menu(window, windows_menu, group_by_icon=False):
"""Populates the given menu with a list of open windows using the Windows API."""

from PyQt6 import QtGui, QtWidgets
import win32gui

def clean_title(title):
"""Removes trailing '-', '|', and whitespace at both ends."""
return re.sub(r'^[\s\-\|]+|[\s\-\|]+$', '', title)

def get_icon_hash(icon):
"""Returns a hashable representation of the icon."""
if icon.isNull():
return None
pixmap = icon.pixmap(16, 16)
image = pixmap.toImage()
buffer = image.bits().asarray(image.sizeInBytes()) # Convert image to bytes
return hash(bytes(buffer)) # Create a hash from the byte data

def find_common_title(titles):
"""Finds the longest common meaningful part of window titles."""
if not titles:
return "Multiple Windows"

# Split titles into word lists and find common prefix
split_titles = [re.split(r'\s*[\-\|]\s*', title) for title in titles]
common_words = split_titles[0]

for words in split_titles[1:]:
common_words = [w1 for w1, w2 in zip(common_words, words) if w1 == w2]
if not common_words:
return "Multiple Windows"

return " - ".join(common_words)

def get_topmost_hwnd():
"""Returns the topmost window handle."""
z_order = []
current_hwnd = win32gui.GetWindow(win32gui.GetDesktopWindow(), win32con.GW_CHILD)
while current_hwnd:
if win32gui.IsWindowVisible(current_hwnd) and win32gui.GetWindow(current_hwnd, win32con.GW_OWNER) == 0:
z_order.append(current_hwnd)
current_hwnd = win32gui.GetWindow(current_hwnd, win32con.GW_HWNDNEXT)
print(z_order)
if len(z_order) < 2:
return None
return z_order[1] # Skip the desktop window

# Clear the menu
windows_menu.clear()

# Get a list of visible windows with titles
windows = []
def window_enum_handler(hwnd, windows):
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
windows.append((hwnd, win32gui.GetWindowText(hwnd)))
win32gui.EnumWindows(window_enum_handler, windows)

# Dictionary to store actions grouped by icon hash
icon_groups = {}

window_actions = []

# Print stacking order (Z-order)
topmost_hwnd = get_topmost_hwnd()
print(topmost_hwnd)

for hwnd, title in windows:
print(hwnd, title)
if title == "Desktop":
continue

clean_title_str = clean_title(title)
action = QtGui.QAction(clean_title_str, window)
action.triggered.connect(lambda checked, h=hwnd: win32_restore_window(h))

# Get window icon
icon = win32_get_icon_for_hwnd(hwnd)
if not icon.isNull():
action.setIcon(icon)

# Highlight the active window
if topmost_hwnd == hwnd:
action.setChecked(True)

# Group windows by icon hash
icon_key = get_icon_hash(icon)
if icon_key:
icon_groups.setdefault(icon_key, []).append(action)
else:
window_actions.append(action)

# Create submenus for windows with the same icon (optional)
if group_by_icon:
for icon_key, actions in icon_groups.items():
if len(actions) > 1:
# Clean the common part before using it for the submenu
common_title = find_common_title([action.text() for action in actions])
cleaned_submenu_title = clean_title(common_title)
submenu = QtWidgets.QMenu(cleaned_submenu_title, windows_menu)
submenu.setIcon(actions[0].icon())
for action in actions:
submenu.addAction(action)
windows_menu.addMenu(submenu)
else:
window_actions.append(actions[0])
else:
window_actions.extend([action for actions in icon_groups.values() for action in actions])
def win32_populate_windows_menu(window, windows_menu):
"""Safely populates the Windows menu with open windows, preventing crashes and infinite loops."""
try:
windows_menu.clear()

windows = []
def window_enum_handler(hwnd, windows):
try:
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
windows.append((hwnd, win32gui.GetWindowText(hwnd)))
except Exception as e:
print(f"Error enumerating window: {e}")

# Add remaining single-window actions, even if grouping is off
for action in window_actions:
windows_menu.addAction(action)
win32gui.EnumWindows(window_enum_handler, windows)

# Add "Show Desktop" action
windows_menu.addSeparator()
show_desktop_action = QtGui.QAction("Show Desktop", window)
show_desktop_action.triggered.connect(win32_minimize_all_windows)
windows_menu.addAction(show_desktop_action)
if not windows:
action = QtGui.QAction("No Windows Found", window)
action.setEnabled(False)
windows_menu.addAction(action)
return

def win32_minimize_all_windows():
def window_enum_handler(hwnd, windows):
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
windows.append((hwnd, win32gui.GetWindowText(hwnd)))
return True
windows = []
win32gui.EnumWindows(window_enum_handler, windows)
for hwnd, title in windows:
# Minimize all windows except the desktop window (called "Desktop")
if title != "Desktop":
win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
for hwnd, title in windows:
if not title.strip() or title.lower() == "desktop":
continue # Skip invalid or empty titles

clean_title = re.sub(r'^[\s\-\|]+|[\s\-\|]+$', '', title)
action = QtGui.QAction(clean_title, window)
action.triggered.connect(lambda checked, h=hwnd: win32_restore_window(h))

# Try to get the window icon safely
try:
icon = win32_get_icon_for_hwnd(hwnd)
if not icon.isNull():
action.setIcon(icon)
except Exception:
pass # Ignore icon failures

windows_menu.addAction(action)

# Add "Show Desktop" action
windows_menu.addSeparator()
show_desktop_action = QtGui.QAction("Show Desktop", window)
show_desktop_action.triggered.connect(win32_minimize_all_windows)
windows_menu.addAction(show_desktop_action)

except Exception as e:
print(f"Error populating Windows menu: {e}")

def win32_restore_window(hwnd):
# If minimized, restore the window
if win32gui.IsIconic(hwnd):
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
win32gui.SetForegroundWindow(hwnd)
"""Restores and brings a window to the front."""
try:
if win32gui.IsIconic(hwnd):
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
win32gui.SetForegroundWindow(hwnd)
except Exception as e:
print(f"Error restoring window: {e}")

def win32_minimize_all_windows():
"""Minimizes all visible windows except the desktop."""
try:
def window_enum_handler(hwnd, windows):
try:
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
windows.append(hwnd)
except Exception:
pass

windows = []
win32gui.EnumWindows(window_enum_handler, windows)

for hwnd in windows:
if win32gui.GetWindowText(hwnd).lower() != "desktop":
win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
except Exception as e:
print(f"Error minimizing windows: {e}")

def win32_get_icon_for_hwnd(hwnd, size=16):
"""Get the icon for a window by its handle."""
hicon = win32gui.SendMessage(hwnd, win32con.WM_GETICON, win32con.ICON_SMALL, 0)
if not hicon:
hicon = win32gui.GetClassLong(hwnd, win32con.GCL_HICON)
hdc_screen = win32gui.GetDC(0)
hdc = win32ui.CreateDCFromHandle(hdc_screen)
mem_dc = hdc.CreateCompatibleDC()
hbmp = win32ui.CreateBitmap()
hbmp.CreateCompatibleBitmap(hdc, size, size)
mem_dc.SelectObject(hbmp)

win32gui.DrawIconEx(mem_dc.GetSafeHdc(), 0, 0, hicon, size, size, 0, None, win32con.DI_NORMAL)
bmpinfo = hbmp.GetInfo()
bmp_bytes = hbmp.GetBitmapBits(True)

qt_image = QtGui.QImage(bmp_bytes, bmpinfo["bmWidth"], bmpinfo["bmHeight"], bmpinfo["bmWidthBytes"], QtGui.QImage.Format.Format_ARGB32)
pixmap = QtGui.QPixmap.fromImage(qt_image)

mem_dc.DeleteDC()
win32gui.ReleaseDC(0, hdc_screen)
win32gui.DeleteObject(hbmp.GetHandle())

return QtGui.QIcon(pixmap)
"""Retrieves the window icon safely, returning a default if none exists."""
try:
hicon = win32gui.SendMessage(hwnd, win32con.WM_GETICON, win32con.ICON_SMALL, 0)
if not hicon:
hicon = win32gui.GetClassLong(hwnd, win32con.GCL_HICON)

if not hicon:
return QtGui.QIcon() # Return empty icon

hdc_screen = win32gui.GetDC(0)
hdc = win32ui.CreateDCFromHandle(hdc_screen)
mem_dc = hdc.CreateCompatibleDC()
hbmp = win32ui.CreateBitmap()
hbmp.CreateCompatibleBitmap(hdc, size, size)
mem_dc.SelectObject(hbmp)

win32gui.DrawIconEx(mem_dc.GetSafeHdc(), 0, 0, hicon, size, size, 0, None, win32con.DI_NORMAL)
bmpinfo = hbmp.GetInfo()
bmp_bytes = hbmp.GetBitmapBits(True)

qt_image = QtGui.QImage(bmp_bytes, bmpinfo["bmWidth"], bmpinfo["bmHeight"], bmpinfo["bmWidthBytes"], QtGui.QImage.Format.Format_ARGB32)
pixmap = QtGui.QPixmap.fromImage(qt_image)

mem_dc.DeleteDC()
win32gui.ReleaseDC(0, hdc_screen)
win32gui.DeleteObject(hbmp.GetHandle())

return QtGui.QIcon(pixmap)
except Exception as e:
print(f"Error retrieving window icon: {e}")
return QtGui.QIcon() # Return an empty icon to avoid crashes

0 comments on commit 06ff3dd

Please sign in to comment.