66
77import os
88import ctypes
9+ from enum import IntEnum
10+ from typing import Optional
911
1012import buildVersion
13+ import keyboardHandler
1114import shellapi
1215import winUser
1316import wx
1720import installer
1821from logHandler import log
1922import gui
20- from gui import guiHelper
23+ from gui import guiHelper , ExpandoTextCtrl
24+ import inputCore
2125from gui .dpiScalingHelper import DpiScalingHelperMixin
2226import tones
2327
24- def doInstall (createDesktopShortcut ,startOnLogon ,copyPortableConfig ,isUpdate ,silent = False ,startAfterInstall = True ):
28+
29+ def doInstall (
30+ createDesktopShortcut ,
31+ startOnLogon ,
32+ copyPortableConfig ,
33+ isUpdate ,
34+ silent = False ,
35+ startAfterInstall = True ,
36+ hotkeyCode : Optional [int ] = 0
37+ ):
2538 progressDialog = gui .IndeterminateProgressDialog (gui .mainFrame ,
2639 # Translators: The title of the dialog presented while NVDA is being updated.
2740 _ ("Updating NVDA" ) if isUpdate
@@ -32,7 +45,17 @@ def doInstall(createDesktopShortcut,startOnLogon,copyPortableConfig,isUpdate,sil
3245 # Translators: The message displayed while NVDA is being installed.
3346 else _ ("Please wait while NVDA is being installed" ))
3447 try :
35- res = config .execElevated (config .SLAVE_FILENAME ,["install" ,str (int (createDesktopShortcut )),str (int (startOnLogon ))],wait = True ,handleAlreadyElevated = True )
48+ res = config .execElevated (
49+ config .SLAVE_FILENAME ,
50+ [
51+ "install" ,
52+ str (int (createDesktopShortcut )),
53+ str (int (startOnLogon )),
54+ str (int (hotkeyCode )),
55+ ],
56+ wait = True ,
57+ handleAlreadyElevated = True
58+ )
3659 if res == 2 : raise installer .RetriableFailure
3760 if copyPortableConfig :
3861 installedUserConfigPath = config .getInstalledUserConfigPath ()
@@ -165,11 +188,44 @@ def __init__(self, parent, isUpdate):
165188 self .createDesktopShortcutCheckbox = optionsSizer .addItem (wx .CheckBox (self , label = keepShortCutText ))
166189 else :
167190 # Translators: The label of the option to create a desktop shortcut in the Install NVDA dialog.
168- # If the shortcut key has been changed for this locale,
169- # this change must also be reflected here.
170- createShortcutText = _ ("Create &desktop icon and shortcut key (control+alt+n)" )
191+ # Shortcuts defaults can no longer be set based on locale, instead they are set by the user.
192+ createShortcutText = _ ("Create &desktop icon" )
171193 self .createDesktopShortcutCheckbox = optionsSizer .addItem (wx .CheckBox (self , label = createShortcutText ))
172- self .createDesktopShortcutCheckbox .Value = shortcutIsPrevInstalled if self .isUpdate else True
194+ self .createDesktopShortcutCheckbox .Value = shortcutIsPrevInstalled if self .isUpdate else True
195+
196+ # Translators: A label for the grouping to create a shortcut key in the install NVDA dialog.
197+ shortcutGroupLabel = _ ("Shortcut key" )
198+ shortcutGroup = guiHelper .BoxSizerHelper (
199+ parent = self ,
200+ sizer = wx .StaticBoxSizer (orient = wx .HORIZONTAL , parent = self , label = shortcutGroupLabel )
201+ )
202+ optionsSizer .addItem (shortcutGroup )
203+
204+ self .shortcutHotkeyCtrl : ExpandoTextCtrl = shortcutGroup .addItem (ExpandoTextCtrl (
205+ self ,
206+ size = (self .scaleSize (250 ), - 1 ),
207+ value = "" , # todo: fetch current shortcut value??
208+ style = wx .TE_READONLY
209+ ))
210+ self .hotkeycode = 0 # todo: set to current shortcut/hotkey value.
211+
212+ self .shortcutHotkeyCtrl .Bind (wx .EVT_SET_FOCUS , self ._onSetFocusHotkeyChar )
213+ self .shortcutHotkeyCtrl .Bind (wx .EVT_KILL_FOCUS , self ._onKillFocusHotkeyChar )
214+ self ._listenForHotKeys = False
215+ changeShortcutButton = wx .Button (
216+ self ,
217+ # Translators: This is the label for the button used to change the shortcut for NVDA,
218+ # It appears in the context of the install NVDA dialog.
219+ label = _ ("Change..." )
220+ )
221+
222+ shortcutGroup .addItem (
223+ guiHelper .associateElements (
224+ self .shortcutHotkeyCtrl ,
225+ changeShortcutButton
226+ )
227+ )
228+ changeShortcutButton .Bind (wx .EVT_BUTTON , self ._onChangeShortcut )
173229
174230 # Translators: The label of a checkbox option in the Install NVDA dialog.
175231 createPortableText = _ ("Copy &portable configuration to current user account" )
@@ -204,9 +260,96 @@ def __init__(self, parent, isUpdate):
204260 mainSizer .Fit (self )
205261 self .CentreOnScreen ()
206262
263+ def _onChangeShortcut (self , evt ):
264+ self ._listenForHotKeys = True
265+ self .shortcutHotkeyCtrl .SetValue ("" )
266+ self .shortcutHotkeyCtrl .SetFocus ()
267+
268+ def _onKillFocusHotkeyChar (self , evt ):
269+ """Focus can be lost by clicking elsewhere, cancel listen for hotkeys"""
270+ evt .Skip ()
271+ log .debug ("kill focus" )
272+ self ._listenForHotKeys = False
273+ inputCore .manager ._captureFunc = None
274+
275+ def _onSetFocusHotkeyChar (self , evt : wx .FocusEvent ):
276+ evt .Skip ()
277+ log .debug ("got focus" )
278+ if inputCore .manager ._captureFunc or not self ._listenForHotKeys :
279+ log .debug (f"Not adding capture func: listenForHotKeys: { self ._listenForHotKeys } " )
280+ return
281+
282+ def addGestureCaptor (gesture ):
283+ log .debug (f"Got gesture: { gesture } " )
284+ if gesture .isModifier :
285+ return False
286+ self ._listenForHotKeys = False
287+ inputCore .manager ._captureFunc = None # one capture per button press, don't want to get stuck in control
288+ wx .CallAfter (self ._showGesture , gesture )
289+ return False
290+ inputCore .manager ._captureFunc = addGestureCaptor
291+
292+ @staticmethod
293+ def _createHotkey (gesture : keyboardHandler .KeyboardInputGesture ):
294+ # From https://docs.microsoft.com/en-gb/windows/win32/shell/shelllinkobject-hotkey
295+ # The link's keyboard shortcut. The virtual keyboard shortcut is in the low-order byte, and the modifier flags
296+ # are in the high-order byte. Use hotKeyModifiers enum for modifiers
297+ # See also the following link which contains a full explanation of the LNK file format, including limitations
298+ # for the hotkey field:
299+ # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-shllink/16cb4ca1-9339-4d0c-a68d-bf1d6cc0f943?redirectedfrom=MSDN
300+ class hotKeyModifier (IntEnum ):
301+ NONE = 0
302+ SHIFT = 1
303+ CTRL = 2
304+ ALT = 4
305+ Extended = 8
306+
307+ @classmethod
308+ def fromVkey (cls , vkey : int ):
309+ vkeyMap = {
310+ winUser .VK_LCONTROL : cls .CTRL ,
311+ winUser .VK_RCONTROL : cls .CTRL ,
312+ winUser .VK_CONTROL : cls .CTRL ,
313+ winUser .VK_LSHIFT : cls .SHIFT ,
314+ winUser .VK_RSHIFT : cls .SHIFT ,
315+ winUser .VK_SHIFT : cls .SHIFT ,
316+ winUser .VK_LMENU : cls .ALT ,
317+ winUser .VK_RMENU : cls .ALT ,
318+ winUser .VK_MENU : cls .ALT ,
319+ }
320+ return vkeyMap .get (vkey , cls .NONE )
321+
322+ lowOrderByte = gesture .vkCode
323+ log .debug (f"Low order (vkey): { lowOrderByte } " )
324+ highOrderByte = 0
325+ for modVK , extended in gesture .modifiers :
326+ mod = hotKeyModifier .fromVkey (modVK )
327+ log .debug (f"High order (modifier key): { mod } , from: { modVK } " )
328+ highOrderByte |= mod .value
329+ return int .from_bytes (
330+ bytes (bytearray ([lowOrderByte , highOrderByte ])),
331+ byteorder = "little" ,
332+ signed = False
333+ )
334+
335+ def _showGesture (self , gesture : keyboardHandler .KeyboardInputGesture ):
336+ log .debug (f"show gesture: { gesture .normalizedIdentifiers } " )
337+ if not isinstance (gesture , keyboardHandler .KeyboardInputGesture ):
338+ log .debugWarning ("Not a KeyboardInputGesture, discarding." )
339+ return
340+ self .shortcutHotkeyCtrl .SetValue (gesture .displayName )
341+ self .hotkeycode = self ._createHotkey (gesture )
342+ wx .CallAfter (lambda : self .shortcutHotkeyCtrl .SelectAll ())
343+
207344 def onInstall (self , evt ):
208345 self .Hide ()
209- doInstall (self .createDesktopShortcutCheckbox .Value ,self .startOnLogonCheckbox .Value ,self .copyPortableConfigCheckbox .Value ,self .isUpdate )
346+ doInstall (
347+ self .createDesktopShortcutCheckbox .Value ,
348+ self .startOnLogonCheckbox .Value ,
349+ self .copyPortableConfigCheckbox .Value ,
350+ self .isUpdate ,
351+ hotkeyCode = self .hotkeycode
352+ )
210353 self .Destroy ()
211354
212355 def onCancel (self , evt ):
0 commit comments