summaryrefslogtreecommitdiffstats
path: root/deluge/ui/console/widgets/fields.py
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/ui/console/widgets/fields.py')
-rw-r--r--deluge/ui/console/widgets/fields.py1202
1 files changed, 1202 insertions, 0 deletions
diff --git a/deluge/ui/console/widgets/fields.py b/deluge/ui/console/widgets/fields.py
new file mode 100644
index 0000000..d8d892d
--- /dev/null
+++ b/deluge/ui/console/widgets/fields.py
@@ -0,0 +1,1202 @@
+#
+# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import logging
+import os
+
+from deluge.decorators import overrides
+from deluge.ui.console.modes.basemode import InputKeyHandler
+from deluge.ui.console.utils import colors
+from deluge.ui.console.utils import curses_util as util
+from deluge.ui.console.utils.format_utils import (
+ delete_alt_backspace,
+ remove_formatting,
+ wrap_string,
+)
+
+try:
+ import curses
+except ImportError:
+ pass
+
+log = logging.getLogger(__name__)
+
+
+class BaseField(InputKeyHandler):
+ def __init__(self, parent=None, name=None, selectable=True, **kwargs):
+ super().__init__()
+ self.name = name
+ self.parent = parent
+ self.fmt_keys = {}
+ self.set_fmt_key('font', 'ignore', kwargs)
+ self.set_fmt_key('color', 'white,black', kwargs)
+ self.set_fmt_key('color_end', 'white,black', kwargs)
+ self.set_fmt_key('color_active', 'black,white', kwargs)
+ self.set_fmt_key('color_unfocused', 'color', kwargs)
+ self.set_fmt_key('color_unfocused_active', 'black,whitegrey', kwargs)
+ self.set_fmt_key('font_active', 'font', kwargs)
+ self.set_fmt_key('font_unfocused', 'font', kwargs)
+ self.set_fmt_key('font_unfocused_active', 'font_active', kwargs)
+ self.default_col = kwargs.get('col', -1)
+ self._selectable = selectable
+ self.value = None
+
+ def selectable(self):
+ return self.has_input() and not self.depend_skip() and self._selectable
+
+ def set_fmt_key(self, key, default, kwargsdict=None):
+ value = self.fmt_keys.get(default, default)
+ if kwargsdict:
+ value = kwargsdict.get(key, value)
+ self.fmt_keys[key] = value
+
+ def get_fmt_keys(self, focused, active, **kwargs):
+ color_key = kwargs.get('color_key', 'color')
+ font_key = 'font'
+ if not focused:
+ color_key += '_unfocused'
+ font_key += '_unfocused'
+ if active:
+ color_key += '_active'
+ font_key += '_active'
+ return color_key, font_key
+
+ def build_fmt_string(self, focused, active, value_key='msg', **kwargs):
+ color_key, font_key = self.get_fmt_keys(focused, active, **kwargs)
+ return '{{!%({})s,%({})s!}}%({})s{{!%({})s!}}'.format(
+ color_key,
+ font_key,
+ value_key,
+ 'color_end',
+ )
+
+ def depend_skip(self):
+ return False
+
+ def has_input(self):
+ return True
+
+ @overrides(InputKeyHandler)
+ def handle_read(self, c):
+ return util.ReadState.IGNORED
+
+ def render(self, screen, row, **kwargs):
+ return 0
+
+ @property
+ def height(self):
+ return 1
+
+ def set_value(self, value):
+ self.value = value
+
+ def get_value(self):
+ return self.value
+
+
+class NoInputField(BaseField):
+ @overrides(BaseField)
+ def has_input(self):
+ return False
+
+
+class InputField(BaseField):
+ def __init__(self, parent, name, message, format_default=None, **kwargs):
+ BaseField.__init__(self, parent=parent, name=name, **kwargs)
+ self.format_default = format_default
+ self.message = None
+ self.set_message(message)
+
+ depend = None
+
+ @overrides(BaseField)
+ def handle_read(self, c):
+ if c in [curses.KEY_ENTER, util.KEY_ENTER2, util.KEY_BACKSPACE2, 113]:
+ return util.ReadState.READ
+ return util.ReadState.IGNORED
+
+ def set_message(self, msg):
+ changed = self.message != msg
+ self.message = msg
+ return changed
+
+ def set_depend(self, i, inverse=False):
+ if not isinstance(i, CheckedInput):
+ raise Exception('Can only depend on CheckedInputs')
+ self.depend = i
+ self.inverse = inverse
+
+ def depend_skip(self):
+ if not self.depend:
+ return False
+ if self.inverse:
+ return self.depend.checked
+ else:
+ return not self.depend.checked
+
+
+class Header(NoInputField):
+ def __init__(self, parent, header, space_above, space_below, **kwargs):
+ if 'name' not in kwargs:
+ kwargs['name'] = header
+ NoInputField.__init__(self, parent=parent, **kwargs)
+ self.header = '{!white,black,bold!}%s' % header
+ self.space_above = space_above
+ self.space_below = space_below
+
+ @overrides(BaseField)
+ def render(self, screen, row, col=0, **kwargs):
+ rows = 1
+ if self.space_above:
+ row += 1
+ rows += 1
+ self.parent.add_string(row, self.header, scr=screen, col=col, pad=False)
+ if self.space_below:
+ rows += 1
+ return rows
+
+ @property
+ def height(self):
+ return 1 + int(self.space_above) + int(self.space_below)
+
+
+class InfoField(NoInputField):
+ def __init__(self, parent, name, label, value, **kwargs):
+ NoInputField.__init__(self, parent=parent, name=name, **kwargs)
+ self.label = label
+ self.value = value
+ self.txt = f'{label} {value}'
+
+ @overrides(BaseField)
+ def render(self, screen, row, col=0, **kwargs):
+ self.parent.add_string(row, self.txt, scr=screen, col=col, pad=False)
+ return 1
+
+ @overrides(BaseField)
+ def set_value(self, v):
+ self.value = v
+ if isinstance(v, float):
+ self.txt = f'{self.label} {self.value:.2f}'
+ else:
+ self.txt = f'{self.label} {self.value}'
+
+
+class CheckedInput(InputField):
+ def __init__(
+ self,
+ parent,
+ name,
+ message,
+ checked=False,
+ checked_char='X',
+ unchecked_char=' ',
+ checkbox_format='[%s] ',
+ **kwargs,
+ ):
+ InputField.__init__(self, parent, name, message, **kwargs)
+ self.set_value(checked)
+ self.fmt_keys.update(
+ {
+ 'msg': message,
+ 'checkbox_format': checkbox_format,
+ 'unchecked_char': unchecked_char,
+ 'checked_char': checked_char,
+ }
+ )
+ self.set_fmt_key('font_checked', 'font', kwargs)
+ self.set_fmt_key('font_unfocused_checked', 'font_checked', kwargs)
+ self.set_fmt_key('font_active_checked', 'font_active', kwargs)
+ self.set_fmt_key('font_unfocused_active_checked', 'font_active_checked', kwargs)
+ self.set_fmt_key('color_checked', 'color', kwargs)
+ self.set_fmt_key('color_active_checked', 'color_active', kwargs)
+ self.set_fmt_key('color_unfocused_checked', 'color_checked', kwargs)
+ self.set_fmt_key(
+ 'color_unfocused_active_checked', 'color_unfocused_active', kwargs
+ )
+
+ @property
+ def checked(self):
+ return self.value
+
+ @overrides(BaseField)
+ def get_fmt_keys(self, focused, active, **kwargs):
+ color_key, font_key = super().get_fmt_keys(focused, active, **kwargs)
+ if self.checked:
+ color_key += '_checked'
+ font_key += '_checked'
+ return color_key, font_key
+
+ def build_msg_string(self, focused, active):
+ fmt_str = self.build_fmt_string(focused, active)
+ char = self.fmt_keys['checked_char' if self.checked else 'unchecked_char']
+ chk_box = ''
+ try:
+ chk_box = self.fmt_keys['checkbox_format'] % char
+ except KeyError:
+ pass
+ msg = fmt_str % self.fmt_keys
+ return chk_box + msg
+
+ @overrides(InputField)
+ def render(self, screen, row, col=0, **kwargs):
+ string = self.build_msg_string(kwargs.get('focused'), kwargs.get('active'))
+
+ self.parent.add_string(row, string, scr=screen, col=col, pad=False)
+ return 1
+
+ @overrides(InputField)
+ def handle_read(self, c):
+ if c == util.KEY_SPACE:
+ self.set_value(not self.checked)
+ return util.ReadState.CHANGED
+ return util.ReadState.IGNORED
+
+ @overrides(InputField)
+ def set_message(self, msg):
+ changed = InputField.set_message(self, msg)
+ if 'msg' in self.fmt_keys and self.fmt_keys['msg'] != msg:
+ changed = True
+ self.fmt_keys.update({'msg': msg})
+
+ return changed
+
+
+class CheckedPlusInput(CheckedInput):
+ def __init__(
+ self,
+ parent,
+ name,
+ message,
+ child,
+ child_always_visible=False,
+ show_usage_hints=True,
+ msg_fmt='%s ',
+ **kwargs,
+ ):
+ CheckedInput.__init__(self, parent, name, message, **kwargs)
+ self.child = child
+ self.child_active = False
+ self.show_usage_hints = show_usage_hints
+ self.msg_fmt = msg_fmt
+ self.child_always_visible = child_always_visible
+
+ @property
+ def height(self):
+ return max(2 if self.show_usage_hints else 1, self.child.height)
+
+ @overrides(CheckedInput)
+ def render(
+ self, screen, row, width=None, active=False, focused=False, col=0, **kwargs
+ ):
+ isact = active and not self.child_active
+ CheckedInput.render(
+ self, screen, row, width=width, active=isact, focused=focused, col=col
+ )
+ rows = 1
+ if self.show_usage_hints and (
+ self.child_always_visible or (active and self.checked)
+ ):
+ msg = '(esc to leave)' if self.child_active else '(right arrow to edit)'
+ self.parent.add_string(row + 1, msg, scr=screen, col=col, pad=False)
+ rows += 1
+
+ msglen = len(
+ self.msg_fmt % colors.strip_colors(self.build_msg_string(focused, active))
+ )
+ # show child
+ if self.checked or self.child_always_visible:
+ crows = self.child.render(
+ screen,
+ row,
+ width=width - msglen,
+ active=self.child_active and active,
+ col=col + msglen,
+ cursor_offset=msglen,
+ )
+ rows = max(rows, crows)
+ else:
+ self.parent.add_string(
+ row,
+ '(enable to view/edit value)',
+ scr=screen,
+ col=col + msglen,
+ pad=False,
+ )
+ return rows
+
+ @overrides(CheckedInput)
+ def handle_read(self, c):
+ if self.child_active:
+ if c == util.KEY_ESC: # leave child on esc
+ self.child_active = False
+ return util.ReadState.READ
+ # pass keys through to child
+ return self.child.handle_read(c)
+ else:
+ if c == util.KEY_SPACE:
+ self.set_value(not self.checked)
+ return util.ReadState.CHANGED
+ if (self.checked or self.child_always_visible) and c == curses.KEY_RIGHT:
+ self.child_active = True
+ return util.ReadState.READ
+ return util.ReadState.IGNORED
+
+ def get_child(self):
+ return self.child
+
+
+class IntSpinInput(InputField):
+ def __init__(
+ self,
+ parent,
+ name,
+ message,
+ move_func,
+ value,
+ min_val=None,
+ max_val=None,
+ inc_amt=1,
+ incr_large=10,
+ strict_validation=False,
+ fmt='%d',
+ **kwargs,
+ ):
+ InputField.__init__(self, parent, name, message, **kwargs)
+ self.convert_func = int
+ self.fmt = fmt
+ self.valstr = str(value)
+ self.default_str = self.valstr
+ self.set_value(value)
+ self.default_value = self.value
+ self.last_valid_value = self.value
+ self.last_active = False
+ self.cursor = len(self.valstr)
+ self.cursoff = (
+ colors.get_line_width(self.message) + 3
+ ) # + 4 for the " [ " in the rendered string
+ self.move_func = move_func
+ self.strict_validation = strict_validation
+ self.min_val = min_val
+ self.max_val = max_val
+ self.inc_amt = inc_amt
+ self.incr_large = incr_large
+
+ def validate_value(self, value, on_invalid=None):
+ if (self.min_val is not None) and value < self.min_val:
+ value = on_invalid if on_invalid else self.min_val
+ if (self.max_val is not None) and value > self.max_val:
+ value = on_invalid if on_invalid else self.max_val
+ return value
+
+ @overrides(InputField)
+ def render(
+ self, screen, row, active=False, focused=True, col=0, cursor_offset=0, **kwargs
+ ):
+ if active:
+ self.last_active = True
+ elif self.last_active:
+ self.set_value(
+ self.valstr, validate=True, value_on_fail=self.last_valid_value
+ )
+ self.last_active = False
+
+ fmt_str = self.build_fmt_string(focused, active, value_key='value')
+ value_format = '%(msg)s {!input!}'
+ if not self.valstr:
+ value_format += '[ ]'
+ elif self.format_default and self.valstr == self.default_str:
+ value_format += '[ {!magenta,black!}%(value)s{!input!} ]'
+ else:
+ value_format += '[ ' + fmt_str + ' ]'
+
+ self.parent.add_string(
+ row,
+ value_format
+ % dict({'msg': self.message, 'value': '%s' % self.valstr}, **self.fmt_keys),
+ scr=screen,
+ col=col,
+ pad=False,
+ )
+ if active:
+ if focused:
+ util.safe_curs_set(util.Curser.NORMAL)
+ self.move_func(row, self.cursor + self.cursoff + cursor_offset)
+ else:
+ util.safe_curs_set(util.Curser.INVISIBLE)
+ return 1
+
+ @overrides(InputField)
+ def handle_read(self, c):
+ if c == util.KEY_SPACE:
+ return util.ReadState.READ
+ elif c == curses.KEY_PPAGE:
+ self.set_value(self.value + self.inc_amt, validate=True)
+ elif c == curses.KEY_NPAGE:
+ self.set_value(self.value - self.inc_amt, validate=True)
+ elif c == util.KEY_ALT_AND_KEY_PPAGE:
+ self.set_value(self.value + self.incr_large, validate=True)
+ elif c == util.KEY_ALT_AND_KEY_NPAGE:
+ self.set_value(self.value - self.incr_large, validate=True)
+ elif c == curses.KEY_LEFT:
+ self.cursor = max(0, self.cursor - 1)
+ elif c == curses.KEY_RIGHT:
+ self.cursor = min(len(self.valstr), self.cursor + 1)
+ elif c == curses.KEY_HOME:
+ self.cursor = 0
+ elif c == curses.KEY_END:
+ self.cursor = len(self.valstr)
+ elif c == curses.KEY_BACKSPACE or c == util.KEY_BACKSPACE2:
+ if self.valstr and self.cursor > 0:
+ new_val = self.valstr[: self.cursor - 1] + self.valstr[self.cursor :]
+ self.set_value(
+ new_val,
+ validate=False,
+ cursor=self.cursor - 1,
+ cursor_on_fail=True,
+ value_on_fail=self.valstr if self.strict_validation else None,
+ )
+ elif c == curses.KEY_DC: # Del
+ if self.valstr and self.cursor <= len(self.valstr):
+ if self.cursor == 0:
+ new_val = self.valstr[1:]
+ else:
+ new_val = (
+ self.valstr[: self.cursor] + self.valstr[self.cursor + 1 :]
+ )
+ self.set_value(
+ new_val,
+ validate=False,
+ cursor=False,
+ value_on_fail=self.valstr if self.strict_validation else None,
+ cursor_on_fail=True,
+ )
+ elif c == ord('-'): # minus
+ self.set_value(
+ self.value - 1,
+ validate=True,
+ cursor=True,
+ cursor_on_fail=True,
+ value_on_fail=self.value,
+ on_invalid=self.value,
+ )
+ elif c == ord('+'): # plus
+ self.set_value(
+ self.value + 1,
+ validate=True,
+ cursor=True,
+ cursor_on_fail=True,
+ value_on_fail=self.value,
+ on_invalid=self.value,
+ )
+ elif util.is_int_chr(c):
+ if self.strict_validation:
+ new_val = (
+ self.valstr[: self.cursor - 1]
+ + chr(c)
+ + self.valstr[self.cursor - 1 :]
+ )
+ self.set_value(
+ new_val,
+ validate=True,
+ cursor=self.cursor + 1,
+ value_on_fail=self.valstr,
+ on_invalid=self.value,
+ )
+ else:
+ minus_place = self.valstr.find('-')
+ if self.cursor > minus_place:
+ new_val = (
+ self.valstr[: self.cursor] + chr(c) + self.valstr[self.cursor :]
+ )
+ self.set_value(
+ new_val,
+ validate=True,
+ cursor=self.cursor + 1,
+ on_invalid=self.value,
+ )
+ else:
+ return util.ReadState.IGNORED
+ return util.ReadState.READ
+
+ @overrides(BaseField)
+ def set_value(
+ self,
+ val,
+ cursor=True,
+ validate=False,
+ cursor_on_fail=False,
+ value_on_fail=None,
+ on_invalid=None,
+ ):
+ value = None
+ try:
+ value = self.convert_func(val)
+ if validate:
+ validated = self.validate_value(value, on_invalid)
+ if validated != value:
+ # Value was not valid, so use validated value instead.
+ # Also set cursor according to validated value
+ cursor = True
+ value = validated
+
+ new_valstr = self.fmt % value
+ if new_valstr == self.valstr:
+ # If string has not change, keep cursor
+ cursor = False
+ self.valstr = new_valstr
+ self.last_valid_value = self.value = value
+ except ValueError:
+ if value_on_fail is not None:
+ self.set_value(
+ value_on_fail,
+ cursor=cursor,
+ cursor_on_fail=cursor_on_fail,
+ validate=validate,
+ on_invalid=on_invalid,
+ )
+ return
+ self.value = None
+ self.valstr = val
+ if cursor_on_fail:
+ self.cursor = cursor
+ except TypeError:
+ import traceback
+
+ log.warning('TypeError: %s', ''.join(traceback.format_exc()))
+ else:
+ if cursor is True:
+ self.cursor = len(self.valstr)
+ elif cursor is not False:
+ self.cursor = cursor
+
+
+class FloatSpinInput(IntSpinInput):
+ def __init__(self, parent, message, name, move_func, value, precision=1, **kwargs):
+ self.precision = precision
+ IntSpinInput.__init__(self, parent, message, name, move_func, value, **kwargs)
+ self.fmt = '%%.%df' % precision
+ self.convert_func = lambda valstr: round(float(valstr), self.precision)
+ self.set_value(value)
+ self.cursor = len(self.valstr)
+
+ @overrides(IntSpinInput)
+ def handle_read(self, c):
+ if c == ord('.'):
+ minus_place = self.valstr.find('-')
+ if self.cursor <= minus_place:
+ return util.ReadState.READ
+ point_place = self.valstr.find('.')
+ if point_place >= 0:
+ return util.ReadState.READ
+ new_val = self.valstr[: self.cursor] + chr(c) + self.valstr[self.cursor :]
+ self.set_value(new_val, validate=True, cursor=self.cursor + 1)
+ else:
+ return IntSpinInput.handle_read(self, c)
+
+
+class SelectInput(InputField):
+ def __init__(
+ self,
+ parent,
+ name,
+ message,
+ opts,
+ vals,
+ active_index,
+ active_default=False,
+ require_select_action=True,
+ **kwargs,
+ ):
+ InputField.__init__(self, parent, name, message, **kwargs)
+ self.opts = opts
+ self.vals = vals
+ self.active_index = active_index
+ self.selected_index = active_index
+ self.default_option = active_index if active_default else None
+ self.require_select_action = require_select_action
+ self.fmt_keys.update({'font_active': 'bold'})
+ font_selected = kwargs.get('font_selected', 'bold,underline')
+
+ self.set_fmt_key('font_selected', font_selected, kwargs)
+ self.set_fmt_key('font_active_selected', 'font_selected', kwargs)
+ self.set_fmt_key('font_unfocused_selected', 'font_selected', kwargs)
+ self.set_fmt_key(
+ 'font_unfocused_active_selected', 'font_active_selected', kwargs
+ )
+
+ self.set_fmt_key('color_selected', 'color', kwargs)
+ self.set_fmt_key('color_active_selected', 'color_active', kwargs)
+ self.set_fmt_key('color_unfocused_selected', 'color_selected', kwargs)
+ self.set_fmt_key(
+ 'color_unfocused_active_selected', 'color_unfocused_active', kwargs
+ )
+ self.set_fmt_key('color_default_value', 'magenta,black', kwargs)
+
+ self.set_fmt_key('color_default_value', 'magenta,black')
+ self.set_fmt_key('color_default_value_active', 'magentadark,white')
+ self.set_fmt_key('color_default_value_selected', 'color_default_value', kwargs)
+ self.set_fmt_key('color_default_value_unfocused', 'color_default_value', kwargs)
+ self.set_fmt_key(
+ 'color_default_value_unfocused_selected',
+ 'color_default_value_selected',
+ kwargs,
+ )
+ self.set_fmt_key('color_default_value_active_selected', 'magentadark,white')
+ self.set_fmt_key(
+ 'color_default_value_unfocused_active_selected',
+ 'color_unfocused_active',
+ kwargs,
+ )
+
+ @property
+ def height(self):
+ return 1 + bool(self.message)
+
+ @overrides(BaseField)
+ def get_fmt_keys(self, focused, active, selected=False, **kwargs):
+ color_key, font_key = super().get_fmt_keys(focused, active, **kwargs)
+ if selected:
+ color_key += '_selected'
+ font_key += '_selected'
+ return color_key, font_key
+
+ @overrides(InputField)
+ def render(self, screen, row, active=False, focused=True, col=0, **kwargs):
+ if self.message:
+ self.parent.add_string(row, self.message, scr=screen, col=col, pad=False)
+ row += 1
+
+ off = col + 1
+ for i, opt in enumerate(self.opts):
+ self.fmt_keys['msg'] = opt
+ fmt_args = {'selected': i == self.selected_index}
+ if i == self.default_option:
+ fmt_args['color_key'] = 'color_default_value'
+ fmt = self.build_fmt_string(
+ focused, (i == self.active_index) and active, **fmt_args
+ )
+ string = '[%s]' % (fmt % self.fmt_keys)
+ self.parent.add_string(row, string, scr=screen, col=off, pad=False)
+ off += len(opt) + 3
+ if self.message:
+ return 2
+ else:
+ return 1
+
+ @overrides(InputField)
+ def handle_read(self, c):
+ if c == curses.KEY_LEFT:
+ self.active_index = max(0, self.active_index - 1)
+ if not self.require_select_action:
+ self.selected_index = self.active_index
+ elif c == curses.KEY_RIGHT:
+ self.active_index = min(len(self.opts) - 1, self.active_index + 1)
+ if not self.require_select_action:
+ self.selected_index = self.active_index
+ elif c == ord(' '):
+ if self.require_select_action:
+ self.selected_index = self.active_index
+ else:
+ return util.ReadState.IGNORED
+ return util.ReadState.READ
+
+ @overrides(BaseField)
+ def get_value(self):
+ return self.vals[self.selected_index]
+
+ @overrides(BaseField)
+ def set_value(self, value):
+ for i, val in enumerate(self.vals):
+ if value == val:
+ self.selected_index = i
+ return
+ raise Exception('Invalid value for SelectInput')
+
+
+class TextInput(InputField):
+ def __init__(
+ self,
+ parent,
+ name,
+ message,
+ move_func,
+ width,
+ value,
+ complete=False,
+ activate_input=False,
+ **kwargs,
+ ):
+ InputField.__init__(self, parent, name, message, **kwargs)
+ self.move_func = move_func
+ self._width = width
+ self.value = value if value else ''
+ self.default_value = value
+ self.complete = complete
+ self.tab_count = 0
+ self.cursor = len(self.value)
+ self.opts = None
+ self.opt_off = 0
+ self.value_offset = 0
+ self.activate_input = activate_input # Wether input must be activated
+ self.input_active = not self.activate_input
+
+ @property
+ def width(self):
+ return self._width
+
+ @property
+ def height(self):
+ return 1 + bool(self.message)
+
+ def calculate_textfield_value(self, width, cursor_offset):
+ cursor_width = width
+
+ if self.cursor > (cursor_width - 1):
+ c_pos_abs = self.cursor - cursor_width
+ if cursor_width <= (self.cursor - self.value_offset):
+ new_cur = c_pos_abs + 1
+ self.value_offset = new_cur
+ else:
+ if self.cursor >= len(self.value):
+ c_pos_abs = len(self.value) - cursor_width
+ new_cur = c_pos_abs + 1
+ self.value_offset = new_cur
+ vstr = self.value[self.value_offset :]
+
+ if len(vstr) > cursor_width:
+ vstr = vstr[:cursor_width]
+ vstr = vstr.ljust(cursor_width)
+ else:
+ if len(self.value) <= cursor_width:
+ self.value_offset = 0
+ vstr = self.value.ljust(cursor_width)
+ else:
+ self.value_offset = min(self.value_offset, self.cursor)
+ vstr = self.value[self.value_offset :]
+ if len(vstr) > cursor_width:
+ vstr = vstr[:cursor_width]
+ vstr = vstr.ljust(cursor_width)
+
+ return vstr
+
+ def calculate_cursor_pos(self, width, col):
+ cursor_width = width
+ x_pos = self.cursor + col
+
+ if (self.cursor + col - self.value_offset) > cursor_width:
+ x_pos += self.value_offset
+ else:
+ x_pos -= self.value_offset
+
+ return min(width - 1 + col, x_pos)
+
+ @overrides(InputField)
+ def render(
+ self,
+ screen,
+ row,
+ width=None,
+ active=False,
+ focused=True,
+ col=0,
+ cursor_offset=0,
+ **kwargs,
+ ):
+ if not self.value and not active and len(self.default_value) != 0:
+ self.value = self.default_value
+ self.cursor = len(self.value)
+
+ if self.message:
+ self.parent.add_string(row, self.message, scr=screen, col=col, pad=False)
+ row += 1
+
+ vstr = self.calculate_textfield_value(width, cursor_offset)
+
+ if active:
+ if self.opts:
+ self.parent.add_string(
+ row + 1, self.opts[self.opt_off :], scr=screen, col=col, pad=False
+ )
+
+ if focused and self.input_active:
+ util.safe_curs_set(
+ util.Curser.NORMAL
+ ) # Make cursor visible when text field is focused
+ x_pos = self.calculate_cursor_pos(width, col)
+ self.move_func(row, x_pos)
+
+ fmt = '{!black,white,bold!}%s'
+ if (
+ self.format_default
+ and len(self.value) != 0
+ and self.value == self.default_value
+ ):
+ fmt = '{!magenta,white!}%s'
+ if not active or not focused or self.input_active:
+ fmt = '{!white,grey,bold!}%s'
+
+ self.parent.add_string(
+ row, fmt % vstr, scr=screen, col=col, pad=False, trim=False
+ )
+ return self.height
+
+ @overrides(BaseField)
+ def set_value(self, val):
+ self.value = val
+ self.cursor = len(self.value)
+
+ @overrides(InputField)
+ def handle_read(self, c):
+ """
+ Return False when key was swallowed, i.e. we recognised
+ the key and no further action by other components should
+ be performed.
+ """
+ if self.activate_input:
+ if not self.input_active:
+ if c in [
+ curses.KEY_LEFT,
+ curses.KEY_RIGHT,
+ curses.KEY_HOME,
+ curses.KEY_END,
+ curses.KEY_ENTER,
+ util.KEY_ENTER2,
+ ]:
+ self.input_active = True
+ return util.ReadState.READ
+ else:
+ return util.ReadState.IGNORED
+ elif c == util.KEY_ESC:
+ self.input_active = False
+ return util.ReadState.READ
+
+ if c == util.KEY_TAB and self.complete:
+ # Keep track of tab hit count to know when it's double-hit
+ self.tab_count += 1
+ if self.tab_count > 1:
+ second_hit = True
+ self.tab_count = 0
+ else:
+ second_hit = False
+
+ # We only call the tab completer function if we're at the end of
+ # the input string on the cursor is on a space
+ if self.cursor == len(self.value) or self.value[self.cursor] == ' ':
+ if self.opts:
+ prev = self.opt_off
+ self.opt_off += self.width - 3
+ # now find previous double space, best guess at a split point
+ # in future could keep opts unjoined to get this really right
+ self.opt_off = self.opts.rfind(' ', 0, self.opt_off) + 2
+ if (
+ second_hit and self.opt_off == prev
+ ): # double tap and we're at the end
+ self.opt_off = 0
+ else:
+ opts = self.do_complete(self.value)
+ if len(opts) == 1: # only one option, just complete it
+ self.value = opts[0]
+ self.cursor = len(opts[0])
+ self.tab_count = 0
+ elif len(opts) > 1:
+ prefix = os.path.commonprefix(opts)
+ if prefix:
+ self.value = prefix
+ self.cursor = len(prefix)
+
+ if (
+ len(opts) > 1 and second_hit
+ ): # display multiple options on second tab hit
+ sp = self.value.rfind(os.sep) + 1
+ self.opts = ' '.join([o[sp:] for o in opts])
+
+ # Cursor movement
+ elif c == curses.KEY_LEFT:
+ self.cursor = max(0, self.cursor - 1)
+ elif c == curses.KEY_RIGHT:
+ self.cursor = min(len(self.value), self.cursor + 1)
+ elif c == curses.KEY_HOME:
+ self.cursor = 0
+ elif c == curses.KEY_END:
+ self.cursor = len(self.value)
+
+ # Delete a character in the input string based on cursor position
+ elif c == curses.KEY_BACKSPACE or c == util.KEY_BACKSPACE2:
+ if self.value and self.cursor > 0:
+ self.value = self.value[: self.cursor - 1] + self.value[self.cursor :]
+ self.cursor -= 1
+ elif c == [util.KEY_ESC, util.KEY_BACKSPACE2] or c == [
+ util.KEY_ESC,
+ curses.KEY_BACKSPACE,
+ ]:
+ self.value, self.cursor = delete_alt_backspace(self.value, self.cursor)
+ elif c == curses.KEY_DC:
+ if self.value and self.cursor < len(self.value):
+ self.value = self.value[: self.cursor] + self.value[self.cursor + 1 :]
+ elif c > 31 and c < 256:
+ # Emulate getwch
+ stroke = chr(c)
+ uchar = stroke
+ while not uchar:
+ try:
+ uchar = stroke.decode(self.parent.encoding)
+ except UnicodeDecodeError:
+ c = self.parent.parent.stdscr.getch()
+ stroke += chr(c)
+ if uchar:
+ if self.cursor == len(self.value):
+ self.value += uchar
+ else:
+ # Insert into string
+ self.value = (
+ self.value[: self.cursor] + uchar + self.value[self.cursor :]
+ )
+ # Move the cursor forward
+ self.cursor += 1
+
+ else:
+ self.opts = None
+ self.opt_off = 0
+ self.tab_count = 0
+ return util.ReadState.IGNORED
+ return util.ReadState.READ
+
+ def do_complete(self, line):
+ line = os.path.abspath(os.path.expanduser(line))
+ ret = []
+ if os.path.exists(line):
+ # This is a correct path, check to see if it's a directory
+ if os.path.isdir(line):
+ # Directory, so we need to show contents of directory
+ for f in os.listdir(line):
+ # Skip hidden
+ if f.startswith('.'):
+ continue
+ f = os.path.join(line, f)
+ if os.path.isdir(f):
+ f += os.sep
+ ret.append(f)
+ else:
+ # This is a file, but we could be looking for another file that
+ # shares a common prefix.
+ for f in os.listdir(os.path.dirname(line)):
+ if f.startswith(os.path.split(line)[1]):
+ ret.append(os.path.join(os.path.dirname(line), f))
+ else:
+ # This path does not exist, so lets do a listdir on it's parent
+ # and find any matches.
+ ret = []
+ if os.path.isdir(os.path.dirname(line)):
+ for f in os.listdir(os.path.dirname(line)):
+ if f.startswith(os.path.split(line)[1]):
+ p = os.path.join(os.path.dirname(line), f)
+
+ if os.path.isdir(p):
+ p += os.sep
+ ret.append(p)
+ return ret
+
+
+class ComboInput(InputField):
+ def __init__(
+ self, parent, name, message, choices, default=None, searchable=True, **kwargs
+ ):
+ InputField.__init__(self, parent, name, message, **kwargs)
+ self.choices = choices
+ self.default = default
+ self.set_value(default)
+ max_width = 0
+ for c in choices:
+ max_width = max(max_width, len(c[1]))
+ self.choices_width = max_width
+ self.searchable = searchable
+
+ @overrides(BaseField)
+ def render(self, screen, row, col=0, **kwargs):
+ fmt_str = self.build_fmt_string(kwargs.get('focused'), kwargs.get('active'))
+ string = '%s: [%10s]' % (self.message, fmt_str % self.fmt_keys)
+ self.parent.add_string(row, string, scr=screen, col=col, pad=False)
+ return 1
+
+ def _lang_selected(self, selected, *args, **kwargs):
+ if selected is not None:
+ self.set_value(selected)
+ self.parent.pop_popup()
+
+ @overrides(InputField)
+ def handle_read(self, c):
+ if c in [util.KEY_SPACE, curses.KEY_ENTER, util.KEY_ENTER2]:
+
+ def search_handler(key):
+ """Handle keyboard input to seach the list"""
+ if not util.is_printable_chr(key):
+ return
+ selected = select_popup.current_selection()
+
+ def select_in_range(begin, end):
+ for i in range(begin, end):
+ val = select_popup.inputs[i].get_value()
+ if val.lower().startswith(chr(key)):
+ select_popup.set_selection(i)
+ return True
+ return False
+
+ # First search downwards
+ if not select_in_range(selected + 1, len(select_popup.inputs)):
+ # No match, so start at beginning
+ select_in_range(0, selected)
+
+ from deluge.ui.console.widgets.popup import ( # Must import here
+ SelectablePopup,
+ )
+
+ select_popup = SelectablePopup(
+ self.parent,
+ ' %s ' % _('Select Language'),
+ self._lang_selected,
+ input_cb=search_handler if self.searchable else None,
+ border_off_west=1,
+ active_wrap=False,
+ width_req=self.choices_width + 12,
+ )
+ for choice in self.choices:
+ args = {'data': choice[0]}
+ select_popup.add_line(
+ choice[0],
+ choice[1],
+ selectable=True,
+ selected=choice[0] == self.get_value(),
+ **args,
+ )
+ self.parent.push_popup(select_popup)
+ return util.ReadState.CHANGED
+ return util.ReadState.IGNORED
+
+ @overrides(BaseField)
+ def set_value(self, val):
+ self.value = val
+ msg = None
+ for c in self.choices:
+ if c[0] == val:
+ msg = c[1]
+ break
+ if msg is None:
+ log.warning(
+ 'Setting value "%s" found nothing in choices: %s', val, self.choices
+ )
+ self.fmt_keys.update({'msg': msg})
+
+
+class TextField(BaseField):
+ def __init__(self, parent, name, value, selectable=True, value_fmt='%s', **kwargs):
+ BaseField.__init__(
+ self, parent=parent, name=name, selectable=selectable, **kwargs
+ )
+ self.value = value
+ self.value_fmt = value_fmt
+ self.set_value(value)
+
+ @overrides(BaseField)
+ def set_value(self, value):
+ self.value = value
+ self.txt = self.value_fmt % (value)
+
+ @overrides(BaseField)
+ def has_input(self):
+ return True
+
+ @overrides(BaseField)
+ def render(self, screen, row, active=False, focused=False, col=0, **kwargs):
+ util.safe_curs_set(
+ util.Curser.INVISIBLE
+ ) # Make cursor invisible when text field is active
+ fmt = self.build_fmt_string(focused, active)
+ self.fmt_keys['msg'] = self.txt
+ string = fmt % self.fmt_keys
+ self.parent.add_string(row, string, scr=screen, col=col, pad=False, trim=False)
+ return 1
+
+
+class TextArea(TextField):
+ def __init__(self, parent, name, value, value_fmt='%s', **kwargs):
+ TextField.__init__(
+ self, parent, name, value, selectable=False, value_fmt=value_fmt, **kwargs
+ )
+
+ @overrides(TextField)
+ def render(self, screen, row, col=0, **kwargs):
+ util.safe_curs_set(
+ util.Curser.INVISIBLE
+ ) # Make cursor invisible when text field is active
+ color = '{!white,black!}'
+ lines = wrap_string(self.txt, self.parent.width - 3, 3, True)
+
+ for i, line in enumerate(lines):
+ self.parent.add_string(
+ row + i,
+ f'{color}{line}',
+ scr=screen,
+ col=col,
+ pad=False,
+ trim=False,
+ )
+ return len(lines)
+
+ @property
+ def height(self):
+ lines = wrap_string(self.txt, self.parent.width - 3, 3, True)
+ return len(lines)
+
+ @overrides(TextField)
+ def has_input(self):
+ return False
+
+
+class DividerField(NoInputField):
+ def __init__(
+ self,
+ parent,
+ name,
+ value,
+ selectable=False,
+ fill_width=True,
+ value_fmt='%s',
+ **kwargs,
+ ):
+ NoInputField.__init__(
+ self, parent=parent, name=name, selectable=selectable, **kwargs
+ )
+ self.value = value
+ self.value_fmt = value_fmt
+ self.set_value(value)
+ self.fill_width = fill_width
+
+ @overrides(BaseField)
+ def set_value(self, value):
+ self.value = value
+ self.txt = self.value_fmt % (value)
+
+ @overrides(BaseField)
+ def render(
+ self, screen, row, active=False, focused=False, col=0, width=None, **kwargs
+ ):
+ util.safe_curs_set(
+ util.Curser.INVISIBLE
+ ) # Make cursor invisible when text field is active
+ fmt = self.build_fmt_string(focused, active)
+ self.fmt_keys['msg'] = self.txt
+ if self.fill_width:
+ self.fmt_keys['msg'] = ''
+ string_len = len(remove_formatting(fmt % self.fmt_keys))
+ fill_len = width - string_len - (len(self.txt) - 1)
+ self.fmt_keys['msg'] = self.txt * fill_len
+ string = fmt % self.fmt_keys
+ self.parent.add_string(row, string, scr=screen, col=col, pad=False, trim=False)
+ return 1