Skip to content

Commit b3bc341

Browse files
authored
Merge 9e16ec1 into 29572fb
2 parents 29572fb + 9e16ec1 commit b3bc341

4 files changed

Lines changed: 319 additions & 59 deletions

File tree

launcher/nvdaLauncher.nsi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ file /R "${NVDADistDir}\"
8282
${GetParameters} $0
8383
Banner::destroy
8484
exec:
85-
execWait "$PLUGINSDIR\app\nvda_noUIAccess.exe $0 -r --launcher" $1
85+
execWait "$PLUGINSDIR\app\nvda_noUIAccess.exe $0 -r --launcher --debug-logging" $1
8686
;If exit code is 3 then execute again (restart)
8787
intcmp $1 3 exec +1
8888
SectionEnd

source/gui/installerGui.py

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66

77
import os
88
import ctypes
9+
from enum import IntEnum
10+
from typing import Optional
911

1012
import buildVersion
13+
import keyboardHandler
1114
import shellapi
1215
import winUser
1316
import wx
@@ -17,11 +20,21 @@
1720
import installer
1821
from logHandler import log
1922
import gui
20-
from gui import guiHelper
23+
from gui import guiHelper, ExpandoTextCtrl
24+
import inputCore
2125
from gui.dpiScalingHelper import DpiScalingHelperMixin
2226
import 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

Comments
 (0)