Skip to content

Commit 15478f8

Browse files
committed
Input Gestures dialog: Refactor using VirtualTree
1 parent 2e2d39a commit 15478f8

1 file changed

Lines changed: 183 additions & 61 deletions

File tree

source/gui/settingsDialogs.py

Lines changed: 183 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import os
1313
import copy
1414
import re
15+
import threading
1516
import wx
1617
from wx.lib import scrolledpanel
1718
from wx.lib.expando import ExpandoTextCtrl
19+
from wx.lib.mixins.treemixin import VirtualTree
1820
import wx.lib.newevent
1921
import winUser
2022
import logHandler
@@ -3237,11 +3239,76 @@ def onFilterEditTextChange(self, evt):
32373239
self._refreshVisibleItems()
32383240
evt.Skip()
32393241

3242+
32403243
class InputGesturesDialog(SettingsDialog):
32413244
# Translators: The title of the Input Gestures dialog where the user can remap input gestures for commands.
32423245
title = _("Input Gestures")
32433246

3247+
class GesturesTree(VirtualTree, wx.TreeCtrl):
3248+
def __init__(self, parent):
3249+
super().__init__(
3250+
parent,
3251+
size=wx.Size(600, 400),
3252+
style=wx.TR_HAS_BUTTONS | wx.TR_HIDE_ROOT | wx.TR_LINES_AT_ROOT | wx.TR_SINGLE
3253+
)
3254+
3255+
def OnGetChildrenCount(self, index):
3256+
if not index: # Root node
3257+
return len(self.Parent.filteredGestures)
3258+
commands = self.Parent.filteredGestures[index[0]][1]
3259+
if len(index) == 1: # Category
3260+
return len(commands)
3261+
scriptInfo = commands[index[1]][1]
3262+
if len(index) == 2: # Command
3263+
count = len(scriptInfo.gestures)
3264+
if (
3265+
self.Parent.newGesturePromptIndex
3266+
and self.Parent.newGesturePromptIndex[:2] == index
3267+
):
3268+
count += 1
3269+
return count
3270+
assert len(index) == 3
3271+
return 0 # Gesture
3272+
3273+
def OnGetItemText(self, index, column=0):
3274+
if index == self.Parent.newGesturePromptIndex:
3275+
# Translators: The prompt to enter a gesture in the Input Gestures dialog.
3276+
return _("Enter input gesture:")
3277+
category, commands = self.Parent.filteredGestures[index[0]]
3278+
if len(index) == 1:
3279+
return category
3280+
command, scriptInfo = commands[index[1]]
3281+
if len(index) == 2:
3282+
return command
3283+
assert len(index) == 3
3284+
return self.Parent._formatGesture(scriptInfo.gestures[index[2]])
3285+
3286+
def getData(self, index):
3287+
assert 2 <= len(index) <= 3
3288+
category, commands = self.Parent.filteredGestures[index[0]]
3289+
command, scriptInfo = commands[index[1]]
3290+
if len(index) == 2:
3291+
return scriptInfo
3292+
return scriptInfo.gestures[index[2]]
3293+
32443294
def __init__(self, parent):
3295+
self.populateTreeTimer = None
3296+
self.filterTimer = None
3297+
self.filterLock = threading.Lock()
3298+
self.filterCanceled = True
3299+
self.newGesturePromptIndex = None
3300+
gestures = inputCore.manager.getAllGestureMappings(
3301+
obj=gui.mainFrame.prevFocus,
3302+
ancestors=gui.mainFrame.prevFocusAncestors
3303+
)
3304+
self.flattenedGestures = []
3305+
for category in sorted(gestures):
3306+
commands = gestures[category]
3307+
self.flattenedGestures.append((
3308+
category,
3309+
[(command, commands[command]) for command in sorted(commands)]
3310+
))
3311+
self.filteredGestures = self.flattenedGestures
32453312
super().__init__(parent, resizeable=True)
32463313

32473314
def makeSettings(self, settingsSizer):
@@ -3256,15 +3323,10 @@ def makeSettings(self, settingsSizer):
32563323
settingsSizer.AddSpacer(5)
32573324
filter.Bind(wx.EVT_TEXT, self.onFilterChange, filter)
32583325

3259-
tree = self.tree = wx.TreeCtrl(self, size=wx.Size(600, 400), style=wx.TR_HAS_BUTTONS | wx.TR_HIDE_ROOT | wx.TR_LINES_AT_ROOT | wx.TR_SINGLE )
3260-
3261-
self.treeRoot = tree.AddRoot("root")
3326+
tree = self.tree = self.GesturesTree(self)
32623327
tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.onTreeSelect)
32633328
settingsSizer.Add(tree, proportion=1, flag=wx.EXPAND)
32643329

3265-
self.gestures = inputCore.manager.getAllGestureMappings(obj=gui.mainFrame.prevFocus, ancestors=gui.mainFrame.prevFocusAncestors)
3266-
self.populateTree()
3267-
32683330
settingsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_VERTICAL)
32693331

32703332
bHelper = guiHelper.ButtonHelper(wx.HORIZONTAL)
@@ -3285,36 +3347,88 @@ def makeSettings(self, settingsSizer):
32853347
settingsSizer.Add(bHelper.sizer)
32863348

32873349
def postInit(self):
3350+
self.tree.RefreshItems()
32883351
self.tree.SetFocus()
32893352

3290-
def populateTree(self, filter=''):
3291-
if filter:
3292-
#This regexp uses a positive lookahead (?=...) for every word in the filter, which just makes sure the word is present in the string to be tested without matching position or order.
3293-
# #5060: Escape the filter text to prevent unexpected matches and regexp errors.
3294-
# Because we're escaping, words must then be split on "\ ".
3295-
filter = re.escape(filter)
3296-
filterReg = re.compile(r'(?=.*?' + r')(?=.*?'.join(filter.split('\ ')) + r')', re.U|re.IGNORECASE)
3297-
for category in sorted(self.gestures):
3298-
treeCat = self.tree.AppendItem(self.treeRoot, category)
3299-
commands = self.gestures[category]
3300-
for command in sorted(commands):
3301-
if filter and not filterReg.match(command):
3302-
continue
3303-
treeCom = self.tree.AppendItem(treeCat, command)
3304-
commandInfo = commands[command]
3305-
self.tree.SetItemData(treeCom, commandInfo)
3306-
for gesture in commandInfo.gestures:
3307-
treeGes = self.tree.AppendItem(treeCom, self._formatGesture(gesture))
3308-
self.tree.SetItemData(treeGes, gesture)
3309-
if not self.tree.ItemHasChildren(treeCat):
3310-
self.tree.Delete(treeCat)
3311-
elif filter:
3312-
self.tree.Expand(treeCat)
3353+
FILTER_DELAY_MS = 300
3354+
3355+
def filter(self, filter):
3356+
if not self.filterLock.acquire(blocking=False):
3357+
self.filterCanceled = True
3358+
if not self.filterLock.acquire(timeout=5):
3359+
log.error("Filtering takes too long, giving up")
3360+
return
3361+
self.filterCanceled = False
3362+
3363+
def run():
3364+
try:
3365+
self._filter(filter)
3366+
except: # noqa: E722
3367+
log.exception()
3368+
finally:
3369+
self.filterLock.release()
3370+
3371+
threading.Thread(
3372+
target=run,
3373+
name=f"{self.__class__.__module__}.{self.filter.__func__.__qualname__}",
3374+
).start()
3375+
3376+
# if not filter:
3377+
# if self.filterTimer:
3378+
# self.filterTimer.Stop()
3379+
# self._filter(filter)
3380+
# return
3381+
#
3382+
# def delayedCall(filter):
3383+
# self.tree.Freeze()
3384+
# try:
3385+
# self._filter(filter)
3386+
# finally:
3387+
# self.tree.Thaw()
3388+
# pass
3389+
#
3390+
# if self.filterTimer is None:
3391+
# self.filterTimer = wx.CallLater(self.FILTER_DELAY_MS, delayedCall, filter)
3392+
# else:
3393+
# self.filterTimer.Start(self.FILTER_DELAY_MS, filter)
3394+
3395+
def _filter(self, filter):
3396+
if not self:
3397+
return
3398+
self.tree.CollapseAll()
3399+
if not filter:
3400+
self.filteredGestures = self.flattenedGestures
3401+
self.tree.RefreshItems()
3402+
return
3403+
self.filteredGestures = []
3404+
# This regexp uses a positive lookahead (?=...) for every word in the filter, which just makes sure
3405+
# the word is present in the string to be tested without matching position or order.
3406+
# #5060: Escape the filter text to prevent unexpected matches and regexp errors.
3407+
# Because we're escaping, words must then be split on r"\ ".
3408+
filter = re.escape(filter)
3409+
filterReg = re.compile(r"(?=.*?" + r")(?=.*?".join(filter.split(r"\ ")) + r")", re.U | re.IGNORECASE)
3410+
for category, commands in self.flattenedGestures:
3411+
if self.filterCanceled:
3412+
log.warning("canceled 1")
3413+
return
3414+
filteredCommands = [
3415+
(command, scriptInfo)
3416+
for command, scriptInfo in commands
3417+
if filterReg.match(command)
3418+
]
3419+
if filteredCommands:
3420+
self.filteredGestures.append((category, filteredCommands))
3421+
self.tree.RefreshItems()
3422+
for index in range(len(self.filteredGestures)):
3423+
if self.filterCanceled:
3424+
log.warning("canceled 2")
3425+
return
3426+
# Expand categories
3427+
self.tree.Expand(self.tree.GetItemByIndex((index,)))
33133428

33143429
def onFilterChange(self, evt):
33153430
filter=evt.GetEventObject().GetValue()
3316-
self.tree.DeleteChildren(self.treeRoot)
3317-
self.populateTree(filter)
3431+
self.filter(filter)
33183432

33193433
def _formatGesture(self, identifier):
33203434
try:
@@ -3327,85 +3441,93 @@ def _formatGesture(self, identifier):
33273441
return identifier
33283442

33293443
def onTreeSelect(self, evt):
3444+
if evt:
3445+
evt.Skip()
33303446
# #7077: Check if the treeview is still alive.
33313447
try:
3332-
item = self.tree.Selection
3448+
index = self.tree.GetIndexOfItem(self.tree.Selection)
33333449
except RuntimeError:
33343450
return
3335-
data = self.tree.GetItemData(item)
3336-
isCommand = isinstance(data, inputCore.AllGesturesScriptInfo)
3337-
isGesture = isinstance(data, str)
3451+
isCommand = len(index) == 2
3452+
isGesture = len(index) == 3
33383453
self.addButton.Enabled = isCommand or isGesture
33393454
self.removeButton.Enabled = isGesture
33403455

33413456
def onAdd(self, evt):
33423457
if inputCore.manager._captureFunc:
33433458
return
33443459

3345-
treeCom = self.tree.Selection
3346-
scriptInfo = self.tree.GetItemData(treeCom)
3347-
if not isinstance(scriptInfo, inputCore.AllGesturesScriptInfo):
3348-
treeCom = self.tree.GetItemParent(treeCom)
3349-
scriptInfo = self.tree.GetItemData(treeCom)
3350-
# Translators: The prompt to enter a gesture in the Input Gestures dialog.
3351-
treeGes = self.tree.AppendItem(treeCom, _("Enter input gesture:"))
3352-
self.tree.SelectItem(treeGes)
3460+
selIdx = self.tree.GetIndexOfItem(self.tree.Selection)
3461+
comIdx = selIdx[:2]
3462+
scriptInfo = self.tree.getData(comIdx)
3463+
gesIdx = self.newGesturePromptIndex = comIdx + (self.tree.OnGetChildrenCount(comIdx),)
3464+
catIdx = comIdx[:1]
3465+
catItem = self.tree.GetItemByIndex(catIdx)
3466+
self.tree.RefreshChildrenRecursively(catItem)
3467+
comItem = self.tree.GetItemByIndex(comIdx)
3468+
self.tree.Expand(comItem)
3469+
gesItem = self.tree.GetItemByIndex(gesIdx)
3470+
self.tree.SelectItem(gesItem)
33533471
self.tree.SetFocus()
33543472

33553473
def addGestureCaptor(gesture):
33563474
if gesture.isModifier:
33573475
return False
33583476
inputCore.manager._captureFunc = None
3359-
wx.CallAfter(self._addCaptured, treeGes, scriptInfo, gesture)
3477+
wx.CallAfter(self._addCaptured, scriptInfo, gesture)
33603478
return False
33613479
inputCore.manager._captureFunc = addGestureCaptor
33623480

3363-
def _addCaptured(self, treeGes, scriptInfo, gesture):
3481+
def _addCaptured(self, scriptInfo, gesture):
33643482
gids = gesture.normalizedIdentifiers
33653483
if len(gids) > 1:
33663484
# Multiple choices. Present them in a pop-up menu.
33673485
menu = wx.Menu()
33683486
for gid in gids:
33693487
disp = self._formatGesture(gid)
33703488
item = menu.Append(wx.ID_ANY, disp)
3371-
self.Bind(wx.EVT_MENU,
3372-
lambda evt, gid=gid, disp=disp: self._addChoice(treeGes, scriptInfo, gid, disp),
3373-
item)
3489+
self.Bind(
3490+
wx.EVT_MENU,
3491+
lambda evt, gid=gid, disp=disp: self._addChoice(scriptInfo, gid, disp),
3492+
item
3493+
)
33743494
self.PopupMenu(menu)
3375-
if not self.tree.GetItemData(treeGes):
3495+
if self.newGesturePromptIndex:
33763496
# No item was selected, so use the first.
3377-
self._addChoice(treeGes, scriptInfo, gids[0],
3378-
self._formatGesture(gids[0]))
3497+
self._addChoice(scriptInfo, gids[0], self._formatGesture(gids[0]))
33793498
menu.Destroy()
33803499
else:
3381-
self._addChoice(treeGes, scriptInfo, gids[0],
3382-
self._formatGesture(gids[0]))
3500+
self._addChoice(scriptInfo, gids[0], self._formatGesture(gids[0]))
33833501

3384-
def _addChoice(self, treeGes, scriptInfo, gid, disp):
3502+
def _addChoice(self, scriptInfo, gid, disp):
33853503
entry = (gid, scriptInfo.moduleName, scriptInfo.className, scriptInfo.scriptName)
33863504
try:
33873505
# If this was just removed, just undo it.
33883506
self.pendingRemoves.remove(entry)
33893507
except KeyError:
33903508
self.pendingAdds.add(entry)
3391-
self.tree.SetItemText(treeGes, disp)
3392-
self.tree.SetItemData(treeGes, gid)
33933509
scriptInfo.gestures.append(gid)
3510+
catIdx = self.newGesturePromptIndex[:1]
3511+
catItem = self.tree.GetItemByIndex(catIdx)
3512+
self.newGesturePromptIndex = None
3513+
self.tree.RefreshChildrenRecursively(catItem)
33943514
self.onTreeSelect(None)
33953515

33963516
def onRemove(self, evt):
3397-
treeGes = self.tree.Selection
3398-
gesture = self.tree.GetItemData(treeGes)
3399-
treeCom = self.tree.GetItemParent(treeGes)
3400-
scriptInfo = self.tree.GetItemData(treeCom)
3517+
gesIdx = self.tree.GetIndexOfItem(self.tree.Selection)
3518+
gesture = self.tree.getData(gesIdx)
3519+
comIdx = gesIdx[:2]
3520+
scriptInfo = self.tree.getData(comIdx)
34013521
entry = (gesture, scriptInfo.moduleName, scriptInfo.className, scriptInfo.scriptName)
34023522
try:
34033523
# If this was just added, just undo it.
34043524
self.pendingAdds.remove(entry)
34053525
except KeyError:
34063526
self.pendingRemoves.add(entry)
3407-
self.tree.Delete(treeGes)
34083527
scriptInfo.gestures.remove(gesture)
3528+
catIdx = comIdx[:1]
3529+
catItem = self.tree.GetItemByIndex(catIdx)
3530+
self.tree.RefreshChildrenRecursively(catItem)
34093531
self.tree.SetFocus()
34103532

34113533
def onOk(self, evt):

0 commit comments

Comments
 (0)