Commit 981d1af2 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge pull request #101 from mruwek/column-ui-keymap

Introduce keymap for column_mode
parents a2a77716 b7a99d42
[column_keymap]
up = up
<Left> = prev_column
<Esc>d = delete_column
...@@ -140,5 +140,31 @@ class ConfigTest(TopydoTest): ...@@ -140,5 +140,31 @@ class ConfigTest(TopydoTest):
self.assertEqual(config("test/data/ConfigTest5.conf").link_color(), self.assertEqual(config("test/data/ConfigTest5.conf").link_color(),
config().defaults["colorscheme"]["link_color"]) config().defaults["colorscheme"]["link_color"])
def test_config24(self):
""" column_keymap test. """
keymap, keystates = config("test/data/ConfigTest6.conf").column_keymap()
self.assertEqual(keymap['pp'], 'postpone')
self.assertEqual(keymap['ps'], 'postpone_s')
self.assertEqual(keymap['pr'], 'pri')
self.assertEqual(keymap['pra'], 'cmd pri {} a')
self.assertIn('p', keystates)
self.assertIn('g', keystates)
self.assertIn('pp', keystates)
self.assertIn('ps', keystates)
self.assertIn('pr', keystates)
self.assertEqual(keymap['up'], 'up')
self.assertIn('u', keystates)
self.assertEqual(keymap['<Left>'], 'prev_column')
self.assertNotIn('<Lef', keystates)
self.assertEqual(keymap['<Esc>d'], 'delete_column')
self.assertNotIn('<Esc', keystates)
self.assertIn('<Esc>', keystates)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from test.topydo_testcase import TopydoTest
from topydo.lib.Utils import translate_key_to_config
class UtilsTest(TopydoTest):
def test_key_to_cfg(self):
ctrl_s = translate_key_to_config('ctrl s')
meta_d = translate_key_to_config('meta d')
esc = translate_key_to_config('esc')
f4 = translate_key_to_config('f4')
self.assertEqual(ctrl_s, '<C-s>')
self.assertEqual(meta_d, '<M-d>')
self.assertEqual(esc, '<Esc>')
self.assertEqual(f4, '<F4>')
if __name__ == '__main__':
unittest.main()
...@@ -63,3 +63,30 @@ append_parent_contexts = 0 ...@@ -63,3 +63,30 @@ append_parent_contexts = 0
;listcon = lscon ;listcon = lscon
;listcontext = lscon ;listcontext = lscon
;listcontexts = lscon ;listcontexts = lscon
[column_keymap]
; Keymap configuration for column-mode
gg = home
G = end
j = down
k = up
d = cmd del {}
e = cmd edit {}
u = cmd revert
x = cmd do {}
pp = postpone
ps = postpone_s
pr = pri
0 = first_column
$ = last_column
h = prev_column
l = next_column
A = append_column
I = insert_column
E = edit_column
D = delete_column
Y = copy_column
L = swap_left
R = swap_right
<Left> = prev_column
<Right> = next_column
...@@ -16,8 +16,12 @@ ...@@ -16,8 +16,12 @@
import configparser import configparser
import os import os
import re
import shlex import shlex
from itertools import accumulate
from string import ascii_lowercase
def home_config_path(p_filename): def home_config_path(p_filename):
return os.path.join(os.path.expanduser('~'), p_filename) return os.path.join(os.path.expanduser('~'), p_filename)
...@@ -109,11 +113,40 @@ class _Config: ...@@ -109,11 +113,40 @@ class _Config:
'listcontext': 'lscon', 'listcontext': 'lscon',
'listcontexts': 'lscon', 'listcontexts': 'lscon',
}, },
'column_keymap': {
'gg': 'home',
'G': 'end',
'j': 'down',
'k': 'up',
'd': 'cmd del {}',
'e': 'cmd edit {}',
'u': 'cmd revert',
'x': 'cmd do {}',
'pp': 'postpone',
'ps': 'postpone_s',
'pr': 'pri',
'0': 'first_column',
'$': 'last_column',
'h': 'prev_column',
'l': 'next_column',
'A': 'append_column',
'I': 'insert_column',
'E': 'edit_column',
'D': 'delete_column',
'Y': 'copy_column',
'L': 'swap_left',
'R': 'swap_right',
'<Left>': 'prev_column',
'<Right>': 'next_column',
},
} }
self.config = {} self.config = {}
self.cp = configparser.RawConfigParser() self.cp = configparser.RawConfigParser()
# allow uppercase config keys
self.cp.optionxform = lambda option: option
for section in self.defaults: for section in self.defaults:
self.cp.add_section(section) self.cp.add_section(section)
...@@ -311,6 +344,29 @@ class _Config: ...@@ -311,6 +344,29 @@ class _Config:
""" Returns the list format used by `ls` """ """ Returns the list format used by `ls` """
return self.cp.get('ls', 'list_format') return self.cp.get('ls', 'list_format')
def column_keymap(self):
""" Returns keymap and keystates used in column mode """
keystates = set()
shortcuts = self.cp.items('column_keymap')
keymap_dict = dict(shortcuts)
for combo, action in shortcuts:
# add all possible prefixes to keystates
combo_as_list = re.split('(<[A-Z].+?>|.)', combo)[1::2]
if len(combo_as_list) > 1:
keystates |= set(accumulate(combo_as_list[:-1]))
if action in ['pri', 'postpone', 'postpone_s']:
keystates.add(combo)
if action == 'pri':
for c in ascii_lowercase:
keymap_dict[combo + c] = 'cmd pri {} ' + c
return (keymap_dict, keystates)
def config(p_path=None, p_overrides=None): def config(p_path=None, p_overrides=None):
""" """
Retrieve the config instance. Retrieve the config instance.
......
...@@ -94,3 +94,18 @@ def get_terminal_size(p_getter=None): ...@@ -94,3 +94,18 @@ def get_terminal_size(p_getter=None):
get_terminal_size.getter = inner get_terminal_size.getter = inner
return get_terminal_size.getter() return get_terminal_size.getter()
def translate_key_to_config(p_key):
"""
Translates urwid key event to form understandable by topydo config parser.
"""
if len(p_key) > 1:
key = p_key.capitalize()
if key.startswith('Ctrl') or key.startswith('Meta'):
key = key[0] + '-' + key[5:]
key = '<' + key + '>'
else:
key = p_key
return key
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
import urwid import urwid
class ConsoleWidget(urwid.LineBox): class ConsoleWidget(urwid.LineBox):
def __init__(self, p_text=""): def __init__(self, p_text=""):
urwid.register_signal(ConsoleWidget, ['close']) urwid.register_signal(ConsoleWidget, ['close'])
...@@ -29,7 +30,10 @@ class ConsoleWidget(urwid.LineBox): ...@@ -29,7 +30,10 @@ class ConsoleWidget(urwid.LineBox):
if p_key == 'enter' or p_key == 'q' or p_key == 'esc': if p_key == 'enter' or p_key == 'q' or p_key == 'esc':
urwid.emit_signal(self, 'close') urwid.emit_signal(self, 'close')
# don't return the key, 'enter', 'escape' or 'q' are your only escape. # don't return the key, 'enter', 'escape', 'q' or ':' are your only
# escape. ':' will reenter to the cmdline.
elif p_key == ':':
urwid.emit_signal(self, 'close', True)
def render(self, p_size, focus): def render(self, p_size, focus):
""" """
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urwid
class KeystateWidget(urwid.Text):
def __init__(self):
super().__init__('', align='right')
def selectable(self):
return False
...@@ -24,6 +24,7 @@ from topydo.cli.CLIApplicationBase import CLIApplicationBase ...@@ -24,6 +24,7 @@ from topydo.cli.CLIApplicationBase import CLIApplicationBase
from topydo.Commands import get_subcommand from topydo.Commands import get_subcommand
from topydo.ui.CommandLineWidget import CommandLineWidget from topydo.ui.CommandLineWidget import CommandLineWidget
from topydo.ui.ConsoleWidget import ConsoleWidget from topydo.ui.ConsoleWidget import ConsoleWidget
from topydo.ui.KeystateWidget import KeystateWidget
from topydo.ui.TodoListWidget import TodoListWidget from topydo.ui.TodoListWidget import TodoListWidget
from topydo.ui.ViewWidget import ViewWidget from topydo.ui.ViewWidget import ViewWidget
from topydo.ui.ColumnLayout import columns from topydo.ui.ColumnLayout import columns
...@@ -37,6 +38,7 @@ from topydo.lib import TodoList ...@@ -37,6 +38,7 @@ from topydo.lib import TodoList
COLUMN_WIDTH = 40 COLUMN_WIDTH = 40
class UIView(View): class UIView(View):
""" """
A subclass of view holding user input data that constructed the view (i.e. A subclass of view holding user input data that constructed the view (i.e.
...@@ -51,6 +53,7 @@ _EDIT_COLUMN = 2 ...@@ -51,6 +53,7 @@ _EDIT_COLUMN = 2
_COPY_COLUMN = 3 _COPY_COLUMN = 3
_INSERT_COLUMN = 4 _INSERT_COLUMN = 4
class MainPile(urwid.Pile): class MainPile(urwid.Pile):
""" """
This subclass of Pile doesn't change focus on cursor up/down / mouse press This subclass of Pile doesn't change focus on cursor up/down / mouse press
...@@ -82,6 +85,7 @@ class MainPile(urwid.Pile): ...@@ -82,6 +85,7 @@ class MainPile(urwid.Pile):
if self._command_map[key] not in ('cursor up', 'cursor down'): if self._command_map[key] not in ('cursor up', 'cursor down'):
return key return key
class UIApplication(CLIApplicationBase): class UIApplication(CLIApplicationBase):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
...@@ -93,6 +97,13 @@ class UIApplication(CLIApplicationBase): ...@@ -93,6 +97,13 @@ class UIApplication(CLIApplicationBase):
self.columns = urwid.Columns([], dividechars=0, min_width=COLUMN_WIDTH) self.columns = urwid.Columns([], dividechars=0, min_width=COLUMN_WIDTH)
self.commandline = CommandLineWidget('topydo> ') self.commandline = CommandLineWidget('topydo> ')
self.keystate_widget = KeystateWidget()
self.status_line = urwid.Columns([
('weight', 1, urwid.Filler(self.commandline)),
])
self.keymap = config().column_keymap()
self._alarm = None
# console widget # console widget
self.console = ConsoleWidget() self.console = ConsoleWidget()
...@@ -102,8 +113,10 @@ class UIApplication(CLIApplicationBase): ...@@ -102,8 +113,10 @@ class UIApplication(CLIApplicationBase):
urwid.connect_signal(self.commandline, 'execute_command', urwid.connect_signal(self.commandline, 'execute_command',
self._execute_handler) self._execute_handler)
def hide_console(): def hide_console(p_focus_commandline=False):
self._console_visible = False self._console_visible = False
if p_focus_commandline:
self._focus_commandline()
urwid.connect_signal(self.console, 'close', hide_console) urwid.connect_signal(self.console, 'close', hide_console)
# view widget # view widget
...@@ -120,7 +133,7 @@ class UIApplication(CLIApplicationBase): ...@@ -120,7 +133,7 @@ class UIApplication(CLIApplicationBase):
self.mainwindow = MainPile([ self.mainwindow = MainPile([
('weight', 1, self.columns), ('weight', 1, self.columns),
(1, urwid.Filler(self.commandline)), (1, self.status_line),
]) ])
urwid.connect_signal(self.mainwindow, 'blur_console', hide_console) urwid.connect_signal(self.mainwindow, 'blur_console', hide_console)
...@@ -244,22 +257,25 @@ class UIApplication(CLIApplicationBase): ...@@ -244,22 +257,25 @@ class UIApplication(CLIApplicationBase):
self.column_mode = _COPY_COLUMN self.column_mode = _COPY_COLUMN
self._viewwidget_visible = True self._viewwidget_visible = True
def _column_action_handler(self, p_action):
dispatch = {
'first_column': self._focus_first_column,
'last_column': self._focus_last_column,
'prev_column': self._focus_previous_column,
'next_column': self._focus_next_column,
'append_column': self._append_column,
'insert_column': self._insert_column,
'edit_column': self._edit_column,
'delete_column': self._delete_column,
'copy_column': self._copy_column,
'swap_left': self._swap_column_left,
'swap_right': self._swap_column_right,
}
dispatch[p_action]()
def _handle_input(self, p_input): def _handle_input(self, p_input):
dispatch = { dispatch = {
':': self._focus_commandline, ':': self._focus_commandline,
'0': self._focus_first_column,
'$': self._focus_last_column,
'left': self._focus_previous_column,
'h': self._focus_previous_column,
'right': self._focus_next_column,
'l': self._focus_next_column,
'A': self._append_column,
'I': self._insert_column,
'E': self._edit_column,
'D': self._delete_column,
'Y': self._copy_column,
'L': self._swap_column_left,
'R': self._swap_column_right,
} }
try: try:
...@@ -307,11 +323,16 @@ class UIApplication(CLIApplicationBase): ...@@ -307,11 +323,16 @@ class UIApplication(CLIApplicationBase):
before that position. before that position.
""" """
todolist = TodoListWidget(p_view, p_view.data['title']) todolist = TodoListWidget(p_view, p_view.data['title'], self.keymap)
no_output = lambda _: None no_output = lambda _: None
urwid.connect_signal(todolist, 'execute_command', urwid.connect_signal(todolist, 'execute_command_silent',
lambda cmd: self._execute_handler(cmd, no_output)) lambda cmd: self._execute_handler(cmd, no_output))
urwid.connect_signal(todolist, 'execute_command', self._execute_handler)
urwid.connect_signal(todolist, 'refresh', self.mainloop.screen.clear) urwid.connect_signal(todolist, 'refresh', self.mainloop.screen.clear)
urwid.connect_signal(todolist, 'add_pending_action', self._set_alarm)
urwid.connect_signal(todolist, 'remove_pending_action', self._remove_alarm)
urwid.connect_signal(todolist, 'column_action', self._column_action_handler)
urwid.connect_signal(todolist, 'show_keystate', self._print_keystate)
options = self.columns.options( options = self.columns.options(
width_type='given', width_type='given',
...@@ -329,6 +350,19 @@ class UIApplication(CLIApplicationBase): ...@@ -329,6 +350,19 @@ class UIApplication(CLIApplicationBase):
self.columns.focus_position = p_pos self.columns.focus_position = p_pos
self._blur_commandline() self._blur_commandline()
def _print_keystate(self, p_keystate):
self.keystate_widget.set_text(p_keystate)
self._keystate_visible = len(p_keystate) > 0
def _set_alarm(self, p_callback):
""" Sets alarm to execute p_action specified in 0.5 sec. """
self._alarm = self.mainloop.set_alarm_in(0.5, p_callback)
def _remove_alarm(self):
""" Removes pending action alarm stored in _alarm attribute. """
self.mainloop.remove_alarm(self._alarm)
self._alarm = None
def _swap_column_left(self): def _swap_column_left(self):
pos = self.columns.focus_position pos = self.columns.focus_position
if pos > 0: if pos > 0:
...@@ -358,6 +392,23 @@ class UIApplication(CLIApplicationBase): ...@@ -358,6 +392,23 @@ class UIApplication(CLIApplicationBase):
elif p_enabled == False and self._console_visible: elif p_enabled == False and self._console_visible:
self.console.clear() self.console.clear()
del contents[2] del contents[2]
self.mainwindow.focus_position = 0
@property
def _keystate_visible(self):
contents = self.status_line.contents
return len(contents) == 2 and isinstance(contents[1][0].original_widget,
KeystateWidget)
@_keystate_visible.setter
def _keystate_visible(self, p_enabled):
contents = self.status_line.contents
if p_enabled and len(contents) == 1:
contents.append((urwid.Filler(self.keystate_widget),
('weight', 1, True)))
elif not p_enabled and self._keystate_visible:
del contents[1]
@property @property
def _viewwidget_visible(self): def _viewwidget_visible(self):
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment