Skip to content

Commit

Permalink
Input Gestures dialog: Refactor using VirtualTree
Browse files Browse the repository at this point in the history
  • Loading branch information
JulienCochuyt committed Oct 5, 2019
1 parent 52f3e04 commit e9cbc95
Showing 1 changed file with 188 additions and 78 deletions.
266 changes: 188 additions & 78 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
import os
import copy
import re
import threading
from typing import Sequence, Tuple, Union
import wx
from wx.lib import scrolledpanel
from wx.lib.expando import ExpandoTextCtrl
from wx.lib.mixins.treemixin import VirtualTree
import wx.lib.newevent
import winUser
import logHandler
Expand Down Expand Up @@ -3237,12 +3240,83 @@ def onFilterEditTextChange(self, evt):
self._refreshVisibleItems()
evt.Skip()


#: A type hint used throughout the InputGesturesDialog
FlattenedGestureMappings = Sequence[Tuple[str, Sequence[Tuple[str, inputCore.AllGesturesScriptInfo]]]]


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

def __init__(self, parent):
self.populateTreeTimer = None
class GesturesTree(VirtualTree, wx.TreeCtrl):
def __init__(self, parent):
super().__init__(
parent,
size=wx.Size(600, 400),
style=wx.TR_HAS_BUTTONS | wx.TR_HIDE_ROOT | wx.TR_LINES_AT_ROOT | wx.TR_SINGLE
)

def OnGetChildrenCount(self, index: Tuple[int, ...]) -> int:
if not index: # Root node
return len(self.Parent.filteredGestures)
commands = self.Parent.filteredGestures[index[0]][1]
if len(index) == 1: # Category
return len(commands)
scriptInfo = commands[index[1]][1]
if len(index) == 2: # Command
count = len(scriptInfo.gestures)
if (
self.Parent.newGesturePromptIndex
and self.Parent.newGesturePromptIndex[:2] == index
):
count += 1
return count
assert len(index) == 3
return 0 # Gesture

def OnGetItemText(self, index: Tuple[int, ...], column: int = 0) -> str:
if index == self.Parent.newGesturePromptIndex:
# Translators: The prompt to enter a gesture in the Input Gestures dialog.
return _("Enter input gesture:")
category, commands = self.Parent.filteredGestures[index[0]]
if len(index) == 1:
return category
command, scriptInfo = commands[index[1]]
if len(index) == 2:
return command
assert len(index) == 3
return self.Parent._formatGesture(scriptInfo.gestures[index[2]])

def getData(self, index: Tuple[int, ...]) -> Union[inputCore.AllGesturesScriptInfo, str]:
assert 2 <= len(index) <= 3
category, commands = self.Parent.filteredGestures[index[0]]
command, scriptInfo = commands[index[1]]
if len(index) == 2:
return scriptInfo
return scriptInfo.gestures[index[2]]

def __init__(self, parent: "InputGesturesDialog"):
#: Token used to cancel async filtering
self.filterToken: object = False
#: Token used to cancel async refresh of the tree
self.refreshTreeToken: object = None
#: The index in the GesturesTree of the prompt for entering a new gesture
self.newGesturePromptIndex: Tuple[int, int, int] = None
gestures = inputCore.manager.getAllGestureMappings(
obj=gui.mainFrame.prevFocus,
ancestors=gui.mainFrame.prevFocusAncestors
)
# Flatten the gestures mappings for faster access by the VirtualTree
self.flattenedGestures: FlattenedGestureMappings = []
for category in sorted(gestures):
commands = gestures[category]
self.flattenedGestures.append((
category,
[(command, commands[command]) for command in sorted(commands)]
))
#: The L{GesturesTree} actually reads from this attribute
self.filteredGestures: FlattenedGestureMappings = self.flattenedGestures
super().__init__(parent, resizeable=True)

def makeSettings(self, settingsSizer):
Expand All @@ -3257,15 +3331,10 @@ def makeSettings(self, settingsSizer):
settingsSizer.AddSpacer(5)
filter.Bind(wx.EVT_TEXT, self.onFilterChange, filter)

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 )

self.treeRoot = tree.AddRoot("root")
tree = self.tree = self.GesturesTree(self)
tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.onTreeSelect)
settingsSizer.Add(tree, proportion=1, flag=wx.EXPAND)

self.gestures = inputCore.manager.getAllGestureMappings(obj=gui.mainFrame.prevFocus, ancestors=gui.mainFrame.prevFocusAncestors)
self.populateTree()

settingsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_VERTICAL)

bHelper = guiHelper.ButtonHelper(wx.HORIZONTAL)
Expand All @@ -3286,50 +3355,83 @@ def makeSettings(self, settingsSizer):
settingsSizer.Add(bHelper.sizer)

def postInit(self):
self.tree.RefreshItems()
self.tree.SetFocus()

POPULATE_TREE_DELAY_MS = 300

def populateTree(self, *args, **kwargs):
def delayedCall(*args, **kwargs):
self.tree.Freeze()
try:
self._populateTree(*args, **kwargs)
finally:
self.tree.Thaw()
if self.populateTreeTimer is None:
self.populateTreeTimer = wx.CallLater(self.POPULATE_TREE_DELAY_MS, delayedCall, *args, **kwargs)
else:
self.populateTreeTimer.Start(self.POPULATE_TREE_DELAY_MS, *args, **kwargs)

def _populateTree(self, filter=''):
if filter:
#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.
# #5060: Escape the filter text to prevent unexpected matches and regexp errors.
# Because we're escaping, words must then be split on "\ ".
filter = re.escape(filter)
filterReg = re.compile(r'(?=.*?' + r')(?=.*?'.join(filter.split('\ ')) + r')', re.U|re.IGNORECASE)
self.tree.DeleteChildren(self.treeRoot)
for category in sorted(self.gestures):
treeCat = self.tree.AppendItem(self.treeRoot, category)
commands = self.gestures[category]
for command in sorted(commands):
if filter and not filterReg.match(command):
continue
treeCom = self.tree.AppendItem(treeCat, command)
commandInfo = commands[command]
self.tree.SetItemData(treeCom, commandInfo)
for gesture in commandInfo.gestures:
treeGes = self.tree.AppendItem(treeCom, self._formatGesture(gesture))
self.tree.SetItemData(treeGes, gesture)
if not self.tree.ItemHasChildren(treeCat):
self.tree.Delete(treeCat)
elif filter:
self.tree.Expand(treeCat)
def _onWindowDestroy(self, evt):
super()._onWindowDestroy(evt)
self.filterToken = None
self.refreshTreeToken = None

def onFilterChange(self, evt):
filter=evt.GetEventObject().GetValue()
self.populateTree(filter)
filter = evt.GetEventObject().GetValue()
token = self.filterToken = object()
log.debug(f"new filter token {token}")
self.filter(token, filter)

def filter(self, token: object, filter: str):

def run():
try:
self._filter(token, filter)
except: # noqa: E722
log.exception()

threading.Thread(
target=run,
name=f"{self.__class__.__module__}.{self.filter.__func__.__qualname__}",
).start()

def _filter(self, token: object, filter: str):
if not filter:
refreshTreeToken = self.refreshTreeToken = object()
log.debug(f"new refreshTree token {refreshTreeToken}")
wx.CallAfter(self.refreshTree, refreshTreeToken, self.flattenedGestures, delayed=False)
return
filteredGestures = []
# 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.
# #5060: Escape the filter text to prevent unexpected matches and regexp errors.
# Because we're escaping, words must then be split on r"\ ".
filter = re.escape(filter)
pattern = re.compile(r"(?=.*?" + r")(?=.*?".join(filter.split(r"\ ")) + r")", re.U | re.IGNORECASE)
for category, commands in self.flattenedGestures:
if token is not self.filterToken:
log.debug(f"filter token {token} superseded by {self.filterToken}")
return
filteredCommands = [
(command, scriptInfo)
for command, scriptInfo in commands
if pattern.match(command)
]
if filteredCommands:
filteredGestures.append((category, filteredCommands))
if token is not self.filterToken:
log.debug(f"filter token {token} superseded by {self.filterToken}")
return
refreshTreeToken = self.refreshTreeToken = object()
log.debug(f"new refreshTree token {refreshTreeToken}")
wx.CallAfter(self.refreshTree, refreshTreeToken, filteredGestures, delayed=True)

def refreshTree(self, token: object, filteredGestures: FlattenedGestureMappings, delayed: bool = False):
if delayed:
wx.CallLater(300, self.refreshTree, token, filteredGestures, delayed=False)
return
if token is not self.refreshTreeToken:
log.debug(f"refreshTree token {token} superseded by {self.refreshTreeToken}")
return
self.tree.CollapseAll()
self.filteredGestures = filteredGestures
self.tree.RefreshItems()
if filteredGestures is self.flattenedGestures:
# Do not forcibly expand the categories if there is no filter
return
for index in range(len(self.filteredGestures)):
if token is not self.refreshTreeToken:
log.debug(f"refreshTree token {token} superseded by {self.refreshTreeToken}")
return
# Expand categories
self.tree.Expand(self.tree.GetItemByIndex((index,)))

def _formatGesture(self, identifier):
try:
Expand All @@ -3342,85 +3444,93 @@ def _formatGesture(self, identifier):
return identifier

def onTreeSelect(self, evt):
if evt:
evt.Skip()
# #7077: Check if the treeview is still alive.
try:
item = self.tree.Selection
index = self.tree.GetIndexOfItem(self.tree.Selection)
except RuntimeError:
return
data = self.tree.GetItemData(item)
isCommand = isinstance(data, inputCore.AllGesturesScriptInfo)
isGesture = isinstance(data, str)
isCommand = len(index) == 2
isGesture = len(index) == 3
self.addButton.Enabled = isCommand or isGesture
self.removeButton.Enabled = isGesture

def onAdd(self, evt):
if inputCore.manager._captureFunc:
return

treeCom = self.tree.Selection
scriptInfo = self.tree.GetItemData(treeCom)
if not isinstance(scriptInfo, inputCore.AllGesturesScriptInfo):
treeCom = self.tree.GetItemParent(treeCom)
scriptInfo = self.tree.GetItemData(treeCom)
# Translators: The prompt to enter a gesture in the Input Gestures dialog.
treeGes = self.tree.AppendItem(treeCom, _("Enter input gesture:"))
self.tree.SelectItem(treeGes)
selIdx = self.tree.GetIndexOfItem(self.tree.Selection)
comIdx = selIdx[:2]
scriptInfo = self.tree.getData(comIdx)
gesIdx = self.newGesturePromptIndex = comIdx + (self.tree.OnGetChildrenCount(comIdx),)
catIdx = comIdx[:1]
catItem = self.tree.GetItemByIndex(catIdx)
self.tree.RefreshChildrenRecursively(catItem)
comItem = self.tree.GetItemByIndex(comIdx)
self.tree.Expand(comItem)
gesItem = self.tree.GetItemByIndex(gesIdx)
self.tree.SelectItem(gesItem)
self.tree.SetFocus()

def addGestureCaptor(gesture):
if gesture.isModifier:
return False
inputCore.manager._captureFunc = None
wx.CallAfter(self._addCaptured, treeGes, scriptInfo, gesture)
wx.CallAfter(self._addCaptured, scriptInfo, gesture)
return False
inputCore.manager._captureFunc = addGestureCaptor

def _addCaptured(self, treeGes, scriptInfo, gesture):
def _addCaptured(self, scriptInfo, gesture):
gids = gesture.normalizedIdentifiers
if len(gids) > 1:
# Multiple choices. Present them in a pop-up menu.
menu = wx.Menu()
for gid in gids:
disp = self._formatGesture(gid)
item = menu.Append(wx.ID_ANY, disp)
self.Bind(wx.EVT_MENU,
lambda evt, gid=gid, disp=disp: self._addChoice(treeGes, scriptInfo, gid, disp),
item)
self.Bind(
wx.EVT_MENU,
lambda evt, gid=gid, disp=disp: self._addChoice(scriptInfo, gid, disp),
item
)
self.PopupMenu(menu)
if not self.tree.GetItemData(treeGes):
if self.newGesturePromptIndex:
# No item was selected, so use the first.
self._addChoice(treeGes, scriptInfo, gids[0],
self._formatGesture(gids[0]))
self._addChoice(scriptInfo, gids[0], self._formatGesture(gids[0]))
menu.Destroy()
else:
self._addChoice(treeGes, scriptInfo, gids[0],
self._formatGesture(gids[0]))
self._addChoice(scriptInfo, gids[0], self._formatGesture(gids[0]))

def _addChoice(self, treeGes, scriptInfo, gid, disp):
def _addChoice(self, scriptInfo, gid, disp):
entry = (gid, scriptInfo.moduleName, scriptInfo.className, scriptInfo.scriptName)
try:
# If this was just removed, just undo it.
self.pendingRemoves.remove(entry)
except KeyError:
self.pendingAdds.add(entry)
self.tree.SetItemText(treeGes, disp)
self.tree.SetItemData(treeGes, gid)
scriptInfo.gestures.append(gid)
catIdx = self.newGesturePromptIndex[:1]
catItem = self.tree.GetItemByIndex(catIdx)
self.newGesturePromptIndex = None
self.tree.RefreshChildrenRecursively(catItem)
self.onTreeSelect(None)

def onRemove(self, evt):
treeGes = self.tree.Selection
gesture = self.tree.GetItemData(treeGes)
treeCom = self.tree.GetItemParent(treeGes)
scriptInfo = self.tree.GetItemData(treeCom)
gesIdx = self.tree.GetIndexOfItem(self.tree.Selection)
gesture = self.tree.getData(gesIdx)
comIdx = gesIdx[:2]
scriptInfo = self.tree.getData(comIdx)
entry = (gesture, scriptInfo.moduleName, scriptInfo.className, scriptInfo.scriptName)
try:
# If this was just added, just undo it.
self.pendingAdds.remove(entry)
except KeyError:
self.pendingRemoves.add(entry)
self.tree.Delete(treeGes)
scriptInfo.gestures.remove(gesture)
catIdx = comIdx[:1]
catItem = self.tree.GetItemByIndex(catIdx)
self.tree.RefreshChildrenRecursively(catItem)
self.tree.SetFocus()

def onOk(self, evt):
Expand Down

0 comments on commit e9cbc95

Please sign in to comment.