Skip to content

Commit

Permalink
Global shortcuts on Windows, do not exit Desktop on Alt-F4
Browse files Browse the repository at this point in the history
  • Loading branch information
probonopd committed Feb 13, 2025
1 parent f03be60 commit 8af8727
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 13 deletions.
13 changes: 2 additions & 11 deletions menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ def create_menus(window):

if sys.platform == "win32":
run_action = QtGui.QAction("Run...", window)
run_action.triggered.connect(run_dialog)
window.go_menu.addAction(run_action)
run_action.setShortcut("Meta+R")
window.go_menu.addSeparator()
Expand Down Expand Up @@ -315,22 +314,14 @@ def populate_volumes(window):
window.go_menu.removeAction(action)

drives = QtCore.QStorageInfo.mountedVolumes()
# Remove all that start with anything but /mnt, /run/media, /media, /Volumes, /Volumes.localized, /net
drives = [drive for drive in drives if os.path.commonprefix(["/mnt", "/run/media", "/media", "/Volumes", "/Volumes.localized", "/net"]).startswith(drive.rootPath())]
for drive in drives:
drive_action = QtGui.QAction(drive.displayName(), window)
drive_action.triggered.connect(lambda checked, d=drive.rootPath(): window.open_drive(d))
drive_action.is_volume = True
window.go_menu.addAction(drive_action)

def run_dialog():
"""
Open the Windows Run dialog.
"""
if sys.platform != "win32":
return
from win32com.client import Dispatch
shell = Dispatch("WScript.Shell")
shell.Run("rundll32.exe shell32.dll,#61")

def show_current_time(window):
"""
Show a message box displaying the current time.
Expand Down
10 changes: 8 additions & 2 deletions siracusa.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@
* Windows is ideal for testing because one can test the same code easily using WSL on Debian without and with Wayland, and on Windows natively.
"""

import os, sys, signal, json, shutil, math, time
import os, sys, signal, json, shutil, math, time, ctypes

from PyQt6 import QtWidgets, QtGui, QtCore

if sys.platform == "win32":
from win32com.client import Dispatch
import windows_struts
import windows_hotkeys

import getinfo, menus, fileops

Expand Down Expand Up @@ -270,7 +271,7 @@ def __init__(self, file_path: str, pos: QtCore.QPointF, width = item_width, heig
self.is_folder = os.path.isdir(file_path)

file_info = QtCore.QFileInfo(file_path)
if self.is_folder:
if self.is_folder and not os.path.ismount(file_path):
self.icon = QtGui.QIcon.fromTheme("folder")
else:
self.icon = icon_provider.icon(file_info)
Expand Down Expand Up @@ -1358,6 +1359,11 @@ def __init__(self):
desktop_window.show()
self.desktop_window = desktop_window

# Register global hotkeys
if sys.platform == "win32":
hwnd = int(desktop_window.winId())
windows_hotkeys.HotKeyManager(hwnd).run()

def handle_drive_removal(self, drive):
print(f"Drive {drive} removed")

Expand Down
91 changes: 91 additions & 0 deletions windows_hotkeys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""
Global hotkeys for Windows refactored into a class-based design
"""

import ctypes
import win32con
from ctypes import byref, wintypes
from win32com.client import Dispatch

class HotKeyManager:
def __init__(self, desktop_window_hwnd=None):
self.desktop_window_hwnd = desktop_window_hwnd
self.user32 = ctypes.windll.user32
self.VK_R = 0x52

# Define hotkeys and their modifiers
self.hotkeys = {
'alt_f4': (win32con.VK_F4, win32con.MOD_ALT),
'win_r' : (self.VK_R, win32con.MOD_WIN)
}

# Map each hotkey to its handler function
self.actions = {
'alt_f4': self.handle_alt_f4,
'win_r' : self.handle_win_r
}

# We'll store mappings of hotkey id to key name to ease reverse lookup
self.id_to_key = {}

def handle_alt_f4(self):
print("Alt+F4 pressed")
hwnd = self.user32.GetForegroundWindow()
print("Foreground window:", hwnd)
if self.desktop_window_hwnd:
if hwnd == self.desktop_window_hwnd:
print("Desktop window is active, not closing")
return
"""buf = ctypes.create_string_buffer(512)
self.user32.GetWindowTextA(hwnd, buf, len(buf))
print(buf.value)
if buf.value != b"Desktop":"""
self.user32.PostMessageA(hwnd, win32con.WM_CLOSE, 0, 0)

def handle_win_r(self):
print("Win+R pressed")
Dispatch("WScript.Shell").Run("rundll32.exe shell32.dll,#61")

def register_hotkeys(self):
print("Registering hotkeys...")
# Iterate over hotkeys and register them with user32
for id, key in enumerate(self.hotkeys, start=1):
vk, mod = self.hotkeys[key]
self.id_to_key[id] = key # map id to key name
print(f"Registering {key}: id {id}, vk {vk}, mod {mod}")
if not self.user32.RegisterHotKey(None, id, mod, vk):
print(f"Unable to register hotkey for {key}")

def unregister_hotkeys(self):
print("Unregistering hotkeys...")
# Unregister all hotkeys using the stored IDs
for id in self.id_to_key:
self.user32.UnregisterHotKey(None, id)
print(f"Unregistered hotkey id {id}")

def run(self):
"""
Runs the hotkey manager: registers hotkeys and processes messages in a loop.
"""
# FIXME: Find a way that is not polling to wait for messages and is less CPU intensive
self.register_hotkeys()
try:
m = wintypes.MSG()
while self.user32.GetMessageA(byref(m), None, 0, 0):
# Check if the message is a hotkey message.
if m.message == win32con.WM_HOTKEY:
hotkey_id = m.wParam
key = self.id_to_key.get(hotkey_id)
if key and key in self.actions:
action = self.actions[key]
action()
self.user32.TranslateMessage(byref(m))
self.user32.DispatchMessageA(byref(m))
finally:
self.unregister_hotkeys()


if __name__ == '__main__':
manager = HotKeyManager()
manager.run()

0 comments on commit 8af8727

Please sign in to comment.