1212import os
1313import copy
1414import re
15+ import threading
1516import wx
1617from wx .lib import scrolledpanel
1718from wx .lib .expando import ExpandoTextCtrl
19+ from wx .lib .mixins .treemixin import VirtualTree
1820import wx .lib .newevent
1921import winUser
2022import logHandler
@@ -3237,11 +3239,76 @@ def onFilterEditTextChange(self, evt):
32373239 self ._refreshVisibleItems ()
32383240 evt .Skip ()
32393241
3242+
32403243class 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