Support multiple key input

Change-Id: I0cd0884e0e1f4f0fa82f93e5f7438ff00c5a992a
This commit is contained in:
James E. Blair 2015-10-31 18:54:30 +09:00
parent 6f9e0ceb2f
commit 540c8dd7cc
8 changed files with 124 additions and 41 deletions

View File

@ -99,6 +99,10 @@ keymaps:
review: ['r', 'R']
- name: osx #OS X blocks ctrl+o
change-search: 'ctrl s'
# To specify a sequence of keys, they must be a list of keystrokes
# within a list of key series. For example:
- name: vi
quit: [[':', 'q']]
# The default keymap may be selected with the '-k KEYMAP' command line
# option, or with the following line:

View File

@ -77,18 +77,23 @@ class StatusHeader(urwid.WidgetWrap):
self.error = None
self.offline = None
self.title = None
self.message = None
self.sync = None
self.held = None
self._error = False
self._offline = False
self._title = ''
self._message = ''
self._sync = 0
self._held = 0
self.held_key = self.app.config.keymap.formatKeys(keymap.LIST_HELD)
def update(self, title=None, error=None, offline=None, refresh=True, held=None):
def update(self, title=None, message=None, error=None,
offline=None, refresh=True, held=None):
if title is not None:
self.title = title
if message is not None:
self.message = message
if error is not None:
self.error = error
if offline is not None:
@ -100,9 +105,11 @@ class StatusHeader(urwid.WidgetWrap):
self.refresh()
def refresh(self):
if self._title != self.title:
if (self._title != self.title or self._message != self.message):
self._title = self.title
self.title_widget.set_text(self._title)
self._message = self.message
t = self.message or self.title
self.title_widget.set_text(t)
if self._held != self.held:
self._held = self.held
if self._held:
@ -145,12 +152,14 @@ class SearchDialog(mywid.ButtonDialog):
ring=app.ring)
def keypress(self, size, key):
r = super(SearchDialog, self).keypress(size, key)
commands = self.app.config.keymap.getCommands(r)
if not self.app.input_buffer:
key = super(SearchDialog, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
if keymap.ACTIVATE in commands:
self._emit('search')
return None
return r
return key
# From: cpython/file/2.7/Lib/webbrowser.py with modification to
# redirect stdin/out/err.
@ -209,6 +218,7 @@ class App(object):
self.log.debug("Starting")
self.ring = mywid.KillRing()
self.input_buffer = []
webbrowser.register('xdg-open', None, BackgroundBrowser("xdg-open"))
self.fetch_missing_refs = fetch_missing_refs
@ -268,11 +278,17 @@ class App(object):
self.popup(dialog)
def clearInputBuffer(self):
if self.input_buffer:
self.input_buffer = []
self.status.update(message='')
def changeScreen(self, widget, push=True):
self.log.debug("Changing screen to %s" % (widget,))
self.status.update(error=False, title=widget.title)
if push:
self.screens.append(self.loop.widget)
self.clearInputBuffer()
self.loop.widget = widget
def backScreen(self, target_widget=None):
@ -285,6 +301,7 @@ class App(object):
self.log.debug("Popping screen to %s" % (widget,))
if hasattr(widget, 'title'):
self.status.update(title=widget.title)
self.clearInputBuffer()
self.loop.widget = widget
self.refresh(force=True)
@ -298,6 +315,7 @@ class App(object):
self.log.debug("Clearing screen history")
while self.screens:
widget = self.screens.pop()
self.clearInputBuffer()
self.loop.widget = widget
def refresh(self, data=None, force=False):
@ -329,6 +347,7 @@ class App(object):
def popup(self, widget,
relative_width=50, relative_height=25,
min_width=20, min_height=8):
self.clearInputBuffer()
overlay = urwid.Overlay(widget, self.loop.widget,
'center', ('relative', relative_width),
'middle', ('relative', relative_height),
@ -506,7 +525,9 @@ class App(object):
return None
def unhandledInput(self, key):
commands = self.config.keymap.getCommands(key)
# get commands from buffer
keys = self.input_buffer + [key]
commands = self.config.keymap.getCommands(keys)
if keymap.PREV_SCREEN in commands:
self.backScreen()
elif keymap.TOP_SCREEN in commands:
@ -524,6 +545,11 @@ class App(object):
d = self.config.dashboards[key]
view = view_change_list.ChangeListView(self, d['query'], d['name'])
self.changeScreen(view)
elif keymap.FURTHER_INPUT in commands:
self.input_buffer.append(key)
self.status.update(message=''.join(self.input_buffer))
return
self.clearInputBuffer()
def openURL(self, url):
self.log.debug("Open URL %s" % url)

View File

@ -15,6 +15,7 @@
import re
import string
import logging
import urwid
@ -29,10 +30,10 @@ CURSOR_PAGE_DOWN = urwid.CURSOR_PAGE_DOWN
CURSOR_MAX_LEFT = urwid.CURSOR_MAX_LEFT
CURSOR_MAX_RIGHT = urwid.CURSOR_MAX_RIGHT
ACTIVATE = urwid.ACTIVATE
# Global gertty commands:
KILL = 'kill'
YANK = 'yank'
YANK_POP = 'yank pop'
# Global gertty commands:
PREV_SCREEN = 'previous screen'
TOP_SCREEN = 'top screen'
HELP = 'help'
@ -74,6 +75,8 @@ SELECT_PATCHSETS = 'select patchsets'
NEXT_SELECTABLE = 'next selectable'
PREV_SELECTABLE = 'prev selectable'
INTERACTIVE_SEARCH = 'interactive search'
# Special:
FURTHER_INPUT = 'further input'
DEFAULT_KEYMAP = {
REDRAW_SCREEN: 'ctrl l',
@ -93,7 +96,7 @@ DEFAULT_KEYMAP = {
PREV_SCREEN: 'esc',
TOP_SCREEN: 'meta home',
HELP: ['f1', '?'],
QUIT: 'ctrl q',
QUIT: ['ctrl q'],
CHANGE_SEARCH: 'ctrl o',
REFINE_CHANGE_SEARCH: 'meta o',
LIST_HELD: 'f12',
@ -157,15 +160,32 @@ FORMAT_SUBS = (
)
def formatKey(key):
if type(key) == type([]):
return ''.join([formatKey(k) for k in key])
for subre, repl in FORMAT_SUBS:
key = subre.sub(repl, key)
return key
class Key(object):
def __init__(self, key):
self.key = key
self.keys = {}
self.commands = []
def addKey(self, key):
if key not in self.keys:
self.keys[key] = Key(key)
return self.keys[key]
def __repr__(self):
return '%s %s %s' % (self.__class__.__name__, self.key, self.keys.keys())
class KeyMap(object):
def __init__(self, config):
# key -> [commands]
self.keymap = {}
self.keytree = Key(None)
self.commandmap = {}
self.multikeys = ''
self.update(DEFAULT_KEYMAP)
self.update(config)
@ -178,26 +198,42 @@ class KeyMap(object):
if type(keys) != type([]):
keys = [keys]
self.commandmap[command] = keys
self.keymap = {}
self.keytree = Key(None)
for command, keys in self.commandmap.items():
for key in keys:
if key in self.keymap:
self.keymap[key].append(command)
if isinstance(key, list):
# This is a command series
tree = self.keytree
for i, innerkey in enumerate(key):
tree = tree.addKey(innerkey)
if i+1 == len(key):
tree.commands.append(command)
else:
self.keymap[key] = [command]
tree = self.keytree.addKey(key)
tree.commands.append(command)
def getCommands(self, key):
return self.keymap.get(key, [])
def getCommands(self, keys):
if not keys:
return []
tree = self.keytree
for key in keys:
tree = tree.keys.get(key)
if not tree:
return []
ret = tree.commands[:]
if tree.keys:
ret.append(FURTHER_INPUT)
return ret
def getKeys(self, command):
return self.commandmap.get(command, [])
def updateCommandMap(self):
"Update the urwid command map with this keymap"
for key, commands in self.keymap.items():
for command in commands:
for key in self.keytree.keys.values():
for command in key.commands:
if command in URWID_COMMANDS:
urwid.command_map[key]=command
urwid.command_map[key.key]=command
def formatKeys(self, command):
keys = self.getKeys(command)

View File

@ -47,12 +47,14 @@ class EditTopicDialog(mywid.ButtonDialog):
ring=app.ring)
def keypress(self, size, key):
r = super(EditTopicDialog, self).keypress(size, key)
commands = self.app.config.keymap.getCommands(r)
if not self.app.input_buffer:
key = super(EditTopicDialog, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
if keymap.ACTIVATE in commands:
self._emit('save')
return None
return r
return key
class CherryPickDialog(urwid.WidgetWrap):
signals = ['save', 'cancel']
@ -187,12 +189,14 @@ class ReviewDialog(urwid.WidgetWrap):
return (approvals, message)
def keypress(self, size, key):
r = super(ReviewDialog, self).keypress(size, key)
commands = self.app.config.keymap.getCommands(r)
if not self.app.input_buffer:
key = super(ReviewDialog, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
if keymap.PREV_SCREEN in commands:
self._emit('cancel')
return None
return r
return key
class ReviewButton(mywid.FixedButton):
def __init__(self, revision_row):
@ -786,8 +790,10 @@ class ChangeView(urwid.WidgetWrap):
return self.app.toggleHeldChange(self.change_key)
def keypress(self, size, key):
r = super(ChangeView, self).keypress(size, key)
commands = self.app.config.keymap.getCommands(r)
if not self.app.input_buffer:
key = super(ChangeView, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
if keymap.TOGGLE_REVIEWED in commands:
self.toggleReviewed()
self.refresh()
@ -870,10 +876,10 @@ class ChangeView(urwid.WidgetWrap):
if keymap.CHERRY_PICK_CHANGE in commands:
self.cherryPickChange()
return None
if r in self.app.config.reviewkeys:
self.reviewKey(self.app.config.reviewkeys[r])
if key in self.app.config.reviewkeys:
self.reviewKey(self.app.config.reviewkeys[key])
return None
return r
return key
def diff(self, revision_key):
if self.app.config.diff_view == 'unified':

View File

@ -424,8 +424,10 @@ class ChangeListView(urwid.WidgetWrap):
self.listbox.focus_position = pos
def keypress(self, size, key):
r = super(ChangeListView, self).keypress(size, key)
commands = self.app.config.keymap.getCommands(r)
if not self.app.input_buffer:
key = super(ChangeListView, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
if keymap.TOGGLE_LIST_REVIEWED in commands:
self.unreviewed = not self.unreviewed
self.refresh()

View File

@ -451,7 +451,7 @@ class BaseDiffView(urwid.WidgetWrap):
self.interactiveSearch(self.search)
return None
else:
commands = self.app.config.keymap.getCommands(key)
commands = self.app.config.keymap.getCommands([key])
if keymap.INTERACTIVE_SEARCH in commands:
self.nextSearchResult()
return None
@ -464,8 +464,11 @@ class BaseDiffView(urwid.WidgetWrap):
return None
old_focus = self.listbox.focus
r = super(BaseDiffView, self).keypress(size, key)
if not self.app.input_buffer:
r = super(BaseDiffView, self).keypress(size, key)
new_focus = self.listbox.focus
keys = self.app.input_buffer + [r]
commands = self.app.config.keymap.getCommands(keys)
context = self.getContextAtTop(size)
if context:
@ -474,7 +477,6 @@ class BaseDiffView(urwid.WidgetWrap):
else:
self.file_reminder.set('', '')
commands = self.app.config.keymap.getCommands(r)
if (isinstance(old_focus, BaseDiffCommentEdit) and
(old_focus != new_focus or (keymap.PREV_SCREEN in commands))):
self.cleanupEdit(old_focus)

View File

@ -152,8 +152,12 @@ class ProjectListView(urwid.WidgetWrap):
project_name, project_key=project_key, unreviewed=True))
def keypress(self, size, key):
r = super(ProjectListView, self).keypress(size, key)
commands = self.app.config.keymap.getCommands(r)
if not self.app.input_buffer:
key = super(ProjectListView, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
if not self.app.input_buffer and keymap.FURTHER_INPUT not in commands:
self.app.clearInputBuffer()
if keymap.TOGGLE_LIST_REVIEWED in commands:
self.unreviewed = not self.unreviewed
self.refresh()
@ -177,4 +181,4 @@ class ProjectListView(urwid.WidgetWrap):
sync.SyncSubscribedProjectsTask(sync.HIGH_PRIORITY))
self.app.status.update()
return None
return r
return key

View File

@ -49,8 +49,11 @@ class SideDiffCommentEdit(BaseDiffCommentEdit):
self.focus_position = 1
def keypress(self, size, key):
r = super(SideDiffCommentEdit, self).keypress(size, key)
commands = self.app.config.keymap.getCommands(r)
if not self.app.input_buffer:
key = super(SideDiffCommentEdit, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
if ((keymap.NEXT_SELECTABLE in commands) or
(keymap.PREV_SELECTABLE in commands)):
if ((self.context.old_ln is not None and
@ -61,7 +64,7 @@ class SideDiffCommentEdit(BaseDiffCommentEdit):
else:
self.focus_position = 3
return None
return r
return key
class SideDiffComment(BaseDiffComment):
def __init__(self, context, old, new):