summaryrefslogtreecommitdiffstats
path: root/deluge/ui/console/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/ui/console/widgets')
-rw-r--r--deluge/ui/console/widgets/__init__.py7
-rw-r--r--deluge/ui/console/widgets/fields.py1210
-rw-r--r--deluge/ui/console/widgets/inputpane.py397
-rw-r--r--deluge/ui/console/widgets/popup.py402
-rw-r--r--deluge/ui/console/widgets/sidebar.py82
-rw-r--r--deluge/ui/console/widgets/statusbars.py122
-rw-r--r--deluge/ui/console/widgets/window.py185
7 files changed, 2405 insertions, 0 deletions
diff --git a/deluge/ui/console/widgets/__init__.py b/deluge/ui/console/widgets/__init__.py
new file mode 100644
index 0000000..a11e3f2
--- /dev/null
+++ b/deluge/ui/console/widgets/__init__.py
@@ -0,0 +1,7 @@
+from __future__ import unicode_literals
+
+from deluge.ui.console.widgets.inputpane import BaseInputPane
+from deluge.ui.console.widgets.statusbars import StatusBars
+from deluge.ui.console.widgets.window import BaseWindow
+
+__all__ = ['BaseInputPane', 'StatusBars', 'BaseWindow']
diff --git a/deluge/ui/console/widgets/fields.py b/deluge/ui/console/widgets/fields.py
new file mode 100644
index 0000000..1966c66
--- /dev/null
+++ b/deluge/ui/console/widgets/fields.py
@@ -0,0 +1,1210 @@
+# -*- coding: utf-8 -*-
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+import logging
+import os
+
+from deluge.common import PY2
+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(BaseField, self).__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!}%%(%s)s{!%%(%s)s!}' % (
+ 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 = '%s %s' % (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 = '%s %.2f' % (self.label, self.value)
+ else:
+ self.txt = '%s %s' % (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(CheckedInput, self).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(SelectInput, self).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 = '' if PY2 else 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 (
+ SelectablePopup,
+ ) # Must import here
+
+ 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,
+ '%s%s' % (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
diff --git a/deluge/ui/console/widgets/inputpane.py b/deluge/ui/console/widgets/inputpane.py
new file mode 100644
index 0000000..097a6cb
--- /dev/null
+++ b/deluge/ui/console/widgets/inputpane.py
@@ -0,0 +1,397 @@
+# -*- coding: utf-8 -*-
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+import logging
+
+from deluge.decorators import overrides
+from deluge.ui.console.modes.basemode import InputKeyHandler, move_cursor
+from deluge.ui.console.utils import curses_util as util
+from deluge.ui.console.widgets.fields import (
+ CheckedInput,
+ CheckedPlusInput,
+ ComboInput,
+ DividerField,
+ FloatSpinInput,
+ Header,
+ InfoField,
+ IntSpinInput,
+ NoInputField,
+ SelectInput,
+ TextArea,
+ TextField,
+ TextInput,
+)
+
+try:
+ import curses
+except ImportError:
+ pass
+
+log = logging.getLogger(__name__)
+
+
+class BaseInputPane(InputKeyHandler):
+ def __init__(
+ self,
+ mode,
+ allow_rearrange=False,
+ immediate_action=False,
+ set_first_input_active=True,
+ border_off_west=0,
+ border_off_north=0,
+ border_off_east=0,
+ border_off_south=0,
+ active_wrap=False,
+ **kwargs
+ ):
+ InputKeyHandler.__init__(self)
+ self.inputs = []
+ self.mode = mode
+ self.active_input = 0
+ self.set_first_input_active = set_first_input_active
+ self.allow_rearrange = allow_rearrange
+ self.immediate_action = immediate_action
+ self.move_active_many = 4
+ self.active_wrap = active_wrap
+ self.lineoff = 0
+ self.border_off_west = border_off_west
+ self.border_off_north = border_off_north
+ self.border_off_east = border_off_east
+ self.border_off_south = border_off_south
+ self.last_lineoff_move = 0
+
+ if not hasattr(self, 'visible_content_pane_height'):
+ log.error(
+ 'The class "%s" does not have the attribute "%s" required by super class "%s"',
+ self.__class__.__name__,
+ 'visible_content_pane_height',
+ BaseInputPane.__name__,
+ )
+ raise AttributeError('visible_content_pane_height')
+
+ @property
+ def visible_content_pane_width(self):
+ return self.mode.width
+
+ def add_spaces(self, num):
+ string = ''
+ for i in range(num):
+ string += '\n'
+
+ self.add_text_area('space %d' % len(self.inputs), string)
+
+ def add_text(self, string):
+ self.add_text_area('', string)
+
+ def move(self, r, c):
+ self._cursor_row = r
+ self._cursor_col = c
+
+ def get_input(self, name):
+ for e in self.inputs:
+ if e.name == name:
+ return e
+
+ def _add_input(self, input_element):
+ for e in self.inputs:
+ if isinstance(e, NoInputField):
+ continue
+ if e.name == input_element.name:
+ import traceback
+
+ log.warning(
+ 'Input element with name "%s" already exists in input pane (%s):\n%s',
+ input_element.name,
+ e,
+ ''.join(traceback.format_stack(limit=5)),
+ )
+ return
+
+ self.inputs.append(input_element)
+ if self.set_first_input_active and input_element.selectable():
+ self.active_input = len(self.inputs) - 1
+ self.set_first_input_active = False
+ return input_element
+
+ def add_header(self, header, space_above=False, space_below=False, **kwargs):
+ return self._add_input(Header(self, header, space_above, space_below, **kwargs))
+
+ def add_info_field(self, name, label, value):
+ return self._add_input(InfoField(self, name, label, value))
+
+ def add_text_field(self, name, message, selectable=True, col='+1', **kwargs):
+ return self._add_input(
+ TextField(self, name, message, selectable=selectable, col=col, **kwargs)
+ )
+
+ def add_text_area(self, name, message, **kwargs):
+ return self._add_input(TextArea(self, name, message, **kwargs))
+
+ def add_divider_field(self, name, message, **kwargs):
+ return self._add_input(DividerField(self, name, message, **kwargs))
+
+ def add_text_input(self, name, message, value='', col='+1', **kwargs):
+ """
+ Add a text input field
+
+ :param message: string to display above the input field
+ :param name: name of the field, for the return callback
+ :param value: initial value of the field
+ :param complete: should completion be run when tab is hit and this field is active
+ """
+ return self._add_input(
+ TextInput(
+ self,
+ name,
+ message,
+ self.move,
+ self.visible_content_pane_width,
+ value,
+ col=col,
+ **kwargs
+ )
+ )
+
+ def add_select_input(self, name, message, opts, vals, default_index=0, **kwargs):
+ return self._add_input(
+ SelectInput(self, name, message, opts, vals, default_index, **kwargs)
+ )
+
+ def add_checked_input(self, name, message, checked=False, col='+1', **kwargs):
+ return self._add_input(
+ CheckedInput(self, name, message, checked=checked, col=col, **kwargs)
+ )
+
+ def add_checkedplus_input(
+ self, name, message, child, checked=False, col='+1', **kwargs
+ ):
+ return self._add_input(
+ CheckedPlusInput(
+ self, name, message, child, checked=checked, col=col, **kwargs
+ )
+ )
+
+ def add_float_spin_input(self, name, message, value=0.0, col='+1', **kwargs):
+ return self._add_input(
+ FloatSpinInput(self, name, message, self.move, value, col=col, **kwargs)
+ )
+
+ def add_int_spin_input(self, name, message, value=0, col='+1', **kwargs):
+ return self._add_input(
+ IntSpinInput(self, name, message, self.move, value, col=col, **kwargs)
+ )
+
+ def add_combo_input(self, name, message, choices, col='+1', **kwargs):
+ return self._add_input(
+ ComboInput(self, name, message, choices, col=col, **kwargs)
+ )
+
+ @overrides(InputKeyHandler)
+ def handle_read(self, c):
+ if not self.inputs: # no inputs added yet
+ return util.ReadState.IGNORED
+ ret = self.inputs[self.active_input].handle_read(c)
+ if ret != util.ReadState.IGNORED:
+ if self.immediate_action:
+ self.immediate_action_cb(
+ state_changed=False if ret == util.ReadState.READ else True
+ )
+ return ret
+
+ ret = util.ReadState.READ
+
+ if c == curses.KEY_UP:
+ self.move_active_up(1)
+ elif c == curses.KEY_DOWN:
+ self.move_active_down(1)
+ elif c == curses.KEY_HOME:
+ self.move_active_up(len(self.inputs))
+ elif c == curses.KEY_END:
+ self.move_active_down(len(self.inputs))
+ elif c == curses.KEY_PPAGE:
+ self.move_active_up(self.move_active_many)
+ elif c == curses.KEY_NPAGE:
+ self.move_active_down(self.move_active_many)
+ elif c == util.KEY_ALT_AND_ARROW_UP:
+ self.lineoff = max(self.lineoff - 1, 0)
+ elif c == util.KEY_ALT_AND_ARROW_DOWN:
+ tot_height = self.get_content_height()
+ self.lineoff = min(
+ self.lineoff + 1, tot_height - self.visible_content_pane_height
+ )
+ elif c == util.KEY_CTRL_AND_ARROW_UP:
+ if not self.allow_rearrange:
+ return ret
+ val = self.inputs.pop(self.active_input)
+ self.active_input -= 1
+ self.inputs.insert(self.active_input, val)
+ if self.immediate_action:
+ self.immediate_action_cb(state_changed=True)
+ elif c == util.KEY_CTRL_AND_ARROW_DOWN:
+ if not self.allow_rearrange:
+ return ret
+ val = self.inputs.pop(self.active_input)
+ self.active_input += 1
+ self.inputs.insert(self.active_input, val)
+ if self.immediate_action:
+ self.immediate_action_cb(state_changed=True)
+ else:
+ ret = util.ReadState.IGNORED
+ return ret
+
+ def get_values(self):
+ vals = {}
+ for i, ipt in enumerate(self.inputs):
+ if not ipt.has_input():
+ continue
+ vals[ipt.name] = {
+ 'value': ipt.get_value(),
+ 'order': i,
+ 'active': self.active_input == i,
+ }
+ return vals
+
+ def immediate_action_cb(self, state_changed=True):
+ pass
+
+ def move_active(self, direction, amount):
+ """
+ direction == -1: Up
+ direction == 1: Down
+
+ """
+ self.last_lineoff_move = direction * amount
+
+ if direction > 0:
+ if self.active_wrap:
+ limit = self.active_input - 1
+ if limit < 0:
+ limit = len(self.inputs) + limit
+ else:
+ limit = len(self.inputs) - 1
+ else:
+ limit = 0
+ if self.active_wrap:
+ limit = self.active_input + 1
+
+ def next_move(nc, direction, limit):
+ next_index = nc
+ while next_index != limit:
+ next_index += direction
+ if direction > 0:
+ next_index %= len(self.inputs)
+ elif next_index < 0:
+ next_index = len(self.inputs) + next_index
+
+ if self.inputs[next_index].selectable():
+ return next_index
+ if next_index == limit:
+ return nc
+ return nc
+
+ next_sel = self.active_input
+ for a in range(amount):
+ cur_sel = next_sel
+ next_sel = next_move(next_sel, direction, limit)
+ if cur_sel == next_sel:
+ tot_height = (
+ self.get_content_height()
+ + self.border_off_north
+ + self.border_off_south
+ )
+ if direction > 0:
+ self.lineoff = min(
+ self.lineoff + 1, tot_height - self.visible_content_pane_height
+ )
+ else:
+ self.lineoff = max(self.lineoff - 1, 0)
+
+ if next_sel is not None:
+ self.active_input = next_sel
+
+ def move_active_up(self, amount):
+ self.move_active(-1, amount)
+ if self.immediate_action:
+ self.immediate_action_cb(state_changed=False)
+
+ def move_active_down(self, amount):
+ self.move_active(1, amount)
+ if self.immediate_action:
+ self.immediate_action_cb(state_changed=False)
+
+ def get_content_height(self):
+ height = 0
+ for i, ipt in enumerate(self.inputs):
+ if ipt.depend_skip():
+ continue
+ height += ipt.height
+ return height
+
+ def ensure_active_visible(self):
+ start_row = 0
+ end_row = self.border_off_north
+ for i, ipt in enumerate(self.inputs):
+ if ipt.depend_skip():
+ continue
+ start_row = end_row
+ end_row += ipt.height
+ if i != self.active_input or not ipt.has_input():
+ continue
+ height = self.visible_content_pane_height
+ if end_row > height + self.lineoff:
+ self.lineoff += end_row - (
+ height + self.lineoff
+ ) # Correct result depends on paranthesis
+ elif start_row < self.lineoff:
+ self.lineoff -= self.lineoff - start_row
+ break
+
+ def render_inputs(self, focused=False):
+ self._cursor_row = -1
+ self._cursor_col = -1
+ util.safe_curs_set(util.Curser.INVISIBLE)
+
+ self.ensure_active_visible()
+
+ crow = self.border_off_north
+ for i, ipt in enumerate(self.inputs):
+ if ipt.depend_skip():
+ continue
+ col = self.border_off_west
+ field_width = self.width - self.border_off_east - self.border_off_west
+ cursor_offset = self.border_off_west
+
+ if ipt.default_col != -1:
+ default_col = int(ipt.default_col)
+ if isinstance(ipt.default_col, ''.__class__) and ipt.default_col[0] in [
+ '+',
+ '-',
+ ]:
+ col += default_col
+ cursor_offset += default_col
+ field_width -= default_col # Increase to col must be reflected here
+ else:
+ col = default_col
+ crow += ipt.render(
+ self.screen,
+ crow,
+ width=field_width,
+ active=i == self.active_input,
+ focused=focused,
+ col=col,
+ cursor_offset=cursor_offset,
+ )
+
+ if self._cursor_row >= 0:
+ util.safe_curs_set(util.Curser.VERY_VISIBLE)
+ move_cursor(self.screen, self._cursor_row, self._cursor_col)
diff --git a/deluge/ui/console/widgets/popup.py b/deluge/ui/console/widgets/popup.py
new file mode 100644
index 0000000..d588bbb
--- /dev/null
+++ b/deluge/ui/console/widgets/popup.py
@@ -0,0 +1,402 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+import logging
+
+from deluge.decorators import overrides
+from deluge.ui.console.modes.basemode import InputKeyHandler
+from deluge.ui.console.utils import curses_util as util
+from deluge.ui.console.utils import format_utils
+from deluge.ui.console.widgets import BaseInputPane, BaseWindow
+
+try:
+ import curses
+except ImportError:
+ pass
+
+log = logging.getLogger(__name__)
+
+
+class ALIGN(object):
+ TOP_LEFT = 1
+ TOP_CENTER = 2
+ TOP_RIGHT = 3
+ MIDDLE_LEFT = 4
+ MIDDLE_CENTER = 5
+ MIDDLE_RIGHT = 6
+ BOTTOM_LEFT = 7
+ BOTTOM_CENTER = 8
+ BOTTOM_RIGHT = 9
+ DEFAULT = MIDDLE_CENTER
+
+
+class PopupsHandler(object):
+ def __init__(self):
+ self._popups = []
+
+ @property
+ def popup(self):
+ if self._popups:
+ return self._popups[-1]
+ return None
+
+ def push_popup(self, pu, clear=False):
+ if clear:
+ self._popups = []
+ self._popups.append(pu)
+
+ def pop_popup(self):
+ if self.popup:
+ return self._popups.pop()
+
+ def report_message(self, title, message):
+ self.push_popup(MessagePopup(self, title, message))
+
+
+class Popup(BaseWindow, InputKeyHandler):
+ def __init__(
+ self,
+ parent_mode,
+ title,
+ width_req=0,
+ height_req=0,
+ align=ALIGN.DEFAULT,
+ close_cb=None,
+ encoding=None,
+ base_popup=None,
+ **kwargs
+ ):
+ """
+ Init a new popup. The default constructor will handle sizing and borders and the like.
+
+ Args:
+ parent_mode (basemode subclass): The mode which the popup will be drawn over
+ title (str): the title of the popup window
+ width_req (int or float): An integer value will be used as the width of the popup in character.
+ A float value will indicate the requested ratio in relation to the
+ parents screen width.
+ height_req (int or float): An integer value will be used as the height of the popup in character.
+ A float value will indicate the requested ratio in relation to the
+ parents screen height.
+ align (ALIGN): The alignment controlling the position of the popup on the screen.
+ close_cb (func): Function to be called when the popup is closed
+ encoding (str): The terminal encoding
+ base_popup (Popup): A popup used to inherit width_req and height_req if not explicitly specified.
+
+ Note: The parent mode is responsible for calling refresh on any popups it wants to show.
+ This should be called as the last thing in the parents refresh method.
+
+ The parent *must* also call read_input on the popup instead of/in addition to
+ running its own read_input code if it wants to have the popup handle user input.
+
+ Popups have two methods that must be implemented:
+
+ refresh(self) - draw the popup window to screen. this default mode simply draws a bordered window
+ with the supplied title to the screen
+
+ read_input(self) - handle user input to the popup.
+
+ """
+ InputKeyHandler.__init__(self)
+ self.parent = parent_mode
+ self.close_cb = close_cb
+ self.height_req = height_req
+ self.width_req = width_req
+ self.align = align
+ if base_popup:
+ if not self.width_req:
+ self.width_req = base_popup.width_req
+ if not self.height_req:
+ self.height_req = base_popup.height_req
+
+ hr, wr, posy, posx = self.calculate_size()
+ BaseWindow.__init__(self, title, wr, hr, encoding=None)
+ self.move_window(posy, posx)
+ self._closed = False
+
+ @overrides(BaseWindow)
+ def refresh(self):
+ self.screen.erase()
+ height = self.get_content_height()
+ self.ensure_content_pane_height(
+ height + self.border_off_north + self.border_off_south
+ )
+ BaseInputPane.render_inputs(self, focused=True)
+ BaseWindow.refresh(self)
+
+ def calculate_size(self):
+
+ if isinstance(self.height_req, float) and 0.0 < self.height_req <= 1.0:
+ height = int((self.parent.rows - 2) * self.height_req)
+ else:
+ height = self.height_req
+
+ if isinstance(self.width_req, float) and 0.0 < self.width_req <= 1.0:
+ width = int((self.parent.cols - 2) * self.width_req)
+ else:
+ width = self.width_req
+
+ # Height
+ if height == 0:
+ height = int(self.parent.rows / 2)
+ elif height == -1:
+ height = self.parent.rows - 2
+ elif height > self.parent.rows - 2:
+ height = self.parent.rows - 2
+
+ # Width
+ if width == 0:
+ width = int(self.parent.cols / 2)
+ elif width == -1:
+ width = self.parent.cols
+ elif width >= self.parent.cols:
+ width = self.parent.cols
+
+ if self.align in [ALIGN.TOP_CENTER, ALIGN.TOP_LEFT, ALIGN.TOP_RIGHT]:
+ begin_y = 1
+ elif self.align in [ALIGN.MIDDLE_CENTER, ALIGN.MIDDLE_LEFT, ALIGN.MIDDLE_RIGHT]:
+ begin_y = (self.parent.rows / 2) - (height / 2)
+ elif self.align in [ALIGN.BOTTOM_CENTER, ALIGN.BOTTOM_LEFT, ALIGN.BOTTOM_RIGHT]:
+ begin_y = self.parent.rows - height - 1
+
+ if self.align in [ALIGN.TOP_LEFT, ALIGN.MIDDLE_LEFT, ALIGN.BOTTOM_LEFT]:
+ begin_x = 0
+ elif self.align in [ALIGN.TOP_CENTER, ALIGN.MIDDLE_CENTER, ALIGN.BOTTOM_CENTER]:
+ begin_x = (self.parent.cols / 2) - (width / 2)
+ elif self.align in [ALIGN.TOP_RIGHT, ALIGN.MIDDLE_RIGHT, ALIGN.BOTTOM_RIGHT]:
+ begin_x = self.parent.cols - width
+
+ return height, width, begin_y, begin_x
+
+ def handle_resize(self):
+ height, width, begin_y, begin_x = self.calculate_size()
+ self.resize_window(height, width)
+ self.move_window(begin_y, begin_x)
+
+ def closed(self):
+ return self._closed
+
+ def close(self, *args, **kwargs):
+ self._closed = True
+ if kwargs.get('call_cb', True):
+ self._call_close_cb(*args)
+ self.panel.hide()
+
+ def _call_close_cb(self, *args, **kwargs):
+ if self.close_cb:
+ self.close_cb(*args, base_popup=self, **kwargs)
+
+ @overrides(InputKeyHandler)
+ def handle_read(self, c):
+ if c == util.KEY_ESC: # close on esc, no action
+ self.close(None)
+ return util.ReadState.READ
+ return util.ReadState.IGNORED
+
+
+class SelectablePopup(BaseInputPane, Popup):
+ """
+ A popup which will let the user select from some of the lines that are added.
+ """
+
+ def __init__(
+ self,
+ parent_mode,
+ title,
+ selection_cb,
+ close_cb=None,
+ input_cb=None,
+ allow_rearrange=False,
+ immediate_action=False,
+ **kwargs
+ ):
+ """
+ Args:
+ parent_mode (basemode subclass): The mode which the popup will be drawn over
+ title (str): the title of the popup window
+ selection_cb (func): Function to be called on selection
+ close_cb (func, optional): Function to be called when the popup is closed
+ input_cb (func, optional): Function to be called on every keyboard input
+ allow_rearrange (bool): Allow rearranging the selectable value
+ immediate_action (bool): If immediate_action_cb should be called for every action
+ kwargs (dict): Arguments passed to Popup
+
+ """
+ Popup.__init__(self, parent_mode, title, close_cb=close_cb, **kwargs)
+ kwargs.update(
+ {'allow_rearrange': allow_rearrange, 'immediate_action': immediate_action}
+ )
+ BaseInputPane.__init__(self, self, **kwargs)
+ self.selection_cb = selection_cb
+ self.input_cb = input_cb
+ self.hotkeys = {}
+ self.cb_arg = {}
+ self.cb_args = kwargs.get('cb_args', {})
+ if 'base_popup' not in self.cb_args:
+ self.cb_args['base_popup'] = self
+
+ @property
+ @overrides(BaseWindow)
+ def visible_content_pane_height(self):
+ """We want to use the Popup property"""
+ return Popup.visible_content_pane_height.fget(self)
+
+ def current_selection(self):
+ """Returns a tuple of (selected index, selected data)."""
+ return self.active_input
+
+ def set_selection(self, index):
+ """Set a selected index"""
+ self.active_input = index
+
+ def add_line(
+ self,
+ name,
+ string,
+ use_underline=True,
+ cb_arg=None,
+ foreground=None,
+ selectable=True,
+ selected=False,
+ **kwargs
+ ):
+ hotkey = None
+ self.cb_arg[name] = cb_arg
+ if use_underline:
+ udx = string.find('_')
+ if udx >= 0:
+ hotkey = string[udx].lower()
+ string = (
+ string[:udx]
+ + '{!+underline!}'
+ + string[udx + 1]
+ + '{!-underline!}'
+ + string[udx + 2 :]
+ )
+
+ kwargs['selectable'] = selectable
+ if foreground:
+ kwargs['color_active'] = '%s,white' % foreground
+ kwargs['color'] = '%s,black' % foreground
+
+ field = self.add_text_field(name, string, **kwargs)
+ if hotkey:
+ self.hotkeys[hotkey] = field
+
+ if selected:
+ self.set_selection(len(self.inputs) - 1)
+
+ @overrides(Popup, BaseInputPane)
+ def handle_read(self, c):
+ if c in [curses.KEY_ENTER, util.KEY_ENTER2]:
+ for k, v in self.get_values().items():
+ if v['active']:
+ if self.selection_cb(k, **dict(self.cb_args, data=self.cb_arg)):
+ self.close(None)
+ return util.ReadState.READ
+ else:
+ ret = BaseInputPane.handle_read(self, c)
+ if ret != util.ReadState.IGNORED:
+ return ret
+ ret = Popup.handle_read(self, c)
+ if ret != util.ReadState.IGNORED:
+ if self.selection_cb(None):
+ self.close(None)
+ return ret
+
+ if self.input_cb:
+ self.input_cb(c)
+
+ self.refresh()
+ return util.ReadState.IGNORED
+
+ def add_divider(self, message=None, char='-', fill_width=True, color='white'):
+ if message is not None:
+ fill_width = False
+ else:
+ message = char
+ self.add_divider_field('', message, selectable=False, fill_width=fill_width)
+
+
+class MessagePopup(Popup, BaseInputPane):
+ """
+ Popup that just displays a message
+ """
+
+ def __init__(
+ self,
+ parent_mode,
+ title,
+ message,
+ align=ALIGN.DEFAULT,
+ height_req=0.75,
+ width_req=0.5,
+ **kwargs
+ ):
+ self.message = message
+ Popup.__init__(
+ self,
+ parent_mode,
+ title,
+ align=align,
+ height_req=height_req,
+ width_req=width_req,
+ )
+ BaseInputPane.__init__(self, self, immediate_action=True, **kwargs)
+ lns = format_utils.wrap_string(self.message, self.width - 3, 3, True)
+
+ if isinstance(self.height_req, float):
+ self.height_req = min(len(lns) + 2, int(parent_mode.rows * self.height_req))
+
+ self.handle_resize()
+ self.no_refresh = False
+ self.add_text_area('TextMessage', message)
+
+ @overrides(Popup, BaseInputPane)
+ def handle_read(self, c):
+ ret = BaseInputPane.handle_read(self, c)
+ if ret != util.ReadState.IGNORED:
+ return ret
+ return Popup.handle_read(self, c)
+
+
+class InputPopup(Popup, BaseInputPane):
+ def __init__(self, parent_mode, title, **kwargs):
+ Popup.__init__(self, parent_mode, title, **kwargs)
+ BaseInputPane.__init__(self, self, **kwargs)
+ # We need to replicate some things in order to wrap our inputs
+ self.encoding = parent_mode.encoding
+
+ def _handle_callback(self, state_changed=True, close=True):
+ self._call_close_cb(self.get_values(), state_changed=state_changed, close=close)
+
+ @overrides(BaseInputPane)
+ def immediate_action_cb(self, state_changed=True):
+ self._handle_callback(state_changed=state_changed, close=False)
+
+ @overrides(Popup, BaseInputPane)
+ def handle_read(self, c):
+ ret = BaseInputPane.handle_read(self, c)
+ if ret != util.ReadState.IGNORED:
+ return ret
+
+ if c in [curses.KEY_ENTER, util.KEY_ENTER2]:
+ if self.close_cb:
+ self._handle_callback(state_changed=False, close=False)
+ util.safe_curs_set(util.Curser.INVISIBLE)
+ return util.ReadState.READ
+ elif c == util.KEY_ESC: # close on esc, no action
+ self._handle_callback(state_changed=False, close=True)
+ self.close(None)
+ return util.ReadState.READ
+
+ self.refresh()
+ return util.ReadState.READ
diff --git a/deluge/ui/console/widgets/sidebar.py b/deluge/ui/console/widgets/sidebar.py
new file mode 100644
index 0000000..cc23717
--- /dev/null
+++ b/deluge/ui/console/widgets/sidebar.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 bendikro <bro.devel+deluge@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.
+#
+
+from __future__ import unicode_literals
+
+import curses
+import logging
+
+from deluge.decorators import overrides
+from deluge.ui.console.modes.basemode import add_string
+from deluge.ui.console.utils import curses_util as util
+from deluge.ui.console.widgets import BaseInputPane, BaseWindow
+
+log = logging.getLogger(__name__)
+
+
+class Sidebar(BaseInputPane, BaseWindow):
+ """Base sidebar widget that handles choosing a selected widget
+ with Up/Down arrows.
+
+ Shows the different states of the torrents and allows to filter the
+ torrents based on state.
+
+ """
+
+ def __init__(
+ self, torrentlist, width, height, title=None, allow_resize=False, **kwargs
+ ):
+ BaseWindow.__init__(self, title, width, height, posy=1)
+ BaseInputPane.__init__(self, self, immediate_action=True, **kwargs)
+ self.parent = torrentlist
+ self.focused = False
+ self.allow_resize = allow_resize
+
+ def set_focused(self, focused):
+ self.focused = focused
+
+ def has_focus(self):
+ return self.focused and not self.hidden()
+
+ @overrides(BaseInputPane)
+ def handle_read(self, c):
+ if c == curses.KEY_UP:
+ self.move_active_up(1)
+ elif c == curses.KEY_DOWN:
+ self.move_active_down(1)
+ elif self.allow_resize and c in [ord('+'), ord('-')]:
+ width = self.visible_content_pane_width + (1 if c == ord('+') else -1)
+ self.on_resize(width)
+ else:
+ return BaseInputPane.handle_read(self, c)
+ return util.ReadState.READ
+
+ def on_resize(self, width):
+ self.resize_window(self.height, width)
+
+ @overrides(BaseWindow)
+ def refresh(self):
+ height = self.get_content_height()
+ self.ensure_content_pane_height(
+ height + self.border_off_north + self.border_off_south
+ )
+ BaseInputPane.render_inputs(self, focused=self.has_focus())
+ BaseWindow.refresh(self)
+
+ def _refresh(self):
+ self.screen.erase()
+ height = self.get_content_height()
+ self.ensure_content_pane_height(
+ height + self.border_off_north + self.border_off_south
+ )
+ BaseInputPane.render_inputs(self, focused=True)
+ BaseWindow.refresh(self)
+
+ def add_string(self, row, string, scr=None, **kwargs):
+ add_string(row, string, self.screen, self.parent.encoding, **kwargs)
diff --git a/deluge/ui/console/widgets/statusbars.py b/deluge/ui/console/widgets/statusbars.py
new file mode 100644
index 0000000..fcf4f2f
--- /dev/null
+++ b/deluge/ui/console/widgets/statusbars.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+import deluge.common
+import deluge.component as component
+from deluge.core.preferencesmanager import DEFAULT_PREFS
+from deluge.ui.client import client
+
+
+class StatusBars(component.Component):
+ def __init__(self):
+ component.Component.__init__(self, 'StatusBars', 2, depend=['CoreConfig'])
+ self.config = component.get('CoreConfig')
+
+ # Hold some values we get from the core
+ self.connections = 0
+ self.download = ''
+ self.upload = ''
+ self.dht = 0
+ self.external_ip = ''
+
+ # Default values
+ self.topbar = '{!status!}Deluge %s Console - ' % deluge.common.get_version()
+ self.bottombar = '{!status!}C: %s' % self.connections
+
+ def start(self):
+ self.update()
+
+ def update(self):
+ def on_get_session_status(status):
+ self.upload = deluge.common.fsize(status['payload_upload_rate'])
+ self.download = deluge.common.fsize(status['payload_download_rate'])
+ self.connections = status['num_peers']
+ if 'dht_nodes' in status:
+ self.dht = status['dht_nodes']
+
+ self.update_statusbars()
+
+ def on_get_external_ip(external_ip):
+ self.external_ip = external_ip
+
+ keys = ['num_peers', 'payload_upload_rate', 'payload_download_rate']
+
+ if self.config['dht']:
+ keys.append('dht_nodes')
+
+ client.core.get_session_status(keys).addCallback(on_get_session_status)
+ client.core.get_external_ip().addCallback(on_get_external_ip)
+
+ def update_statusbars(self):
+ # Update the topbar string
+ self.topbar = '{!status!}Deluge %s Console - ' % deluge.common.get_version()
+
+ if client.connected():
+ info = client.connection_info()
+ connection_info = ''
+
+ # Client name
+ if info[2] == 'localclient':
+ connection_info += '{!white,blue!}%s'
+ else:
+ connection_info += '{!green,blue,bold!}%s'
+
+ # Hostname
+ if info[0] == '127.0.0.1':
+ connection_info += '{!white,blue,bold!}@{!white,blue!}%s'
+ else:
+ connection_info += '{!white,blue,bold!}@{!red,blue,bold!}%s'
+
+ # Port
+ if info[1] == DEFAULT_PREFS['daemon_port']:
+ connection_info += '{!white,blue!}:%s'
+ else:
+ connection_info += '{!status!}:%s'
+
+ # Change color back to normal, just in case
+ connection_info += '{!status!}'
+
+ self.topbar += connection_info % (info[2], info[0], info[1])
+ else:
+ self.topbar += 'Not Connected'
+
+ # Update the bottombar string
+ self.bottombar = '{!status!}C: {!white,blue!}%s{!status!}' % self.connections
+
+ if self.config['max_connections_global'] > -1:
+ self.bottombar += ' (%s)' % self.config['max_connections_global']
+
+ if self.download != '0.0 KiB':
+ self.bottombar += ' D: {!magenta,blue,bold!}%s{!status!}' % self.download
+ else:
+ self.bottombar += ' D: {!white,blue!}%s{!status!}' % self.download
+
+ if self.config['max_download_speed'] > -1:
+ self.bottombar += (
+ ' (%s ' % self.config['max_download_speed'] + _('KiB/s') + ')'
+ )
+
+ if self.upload != '0.0 KiB':
+ self.bottombar += ' U: {!green,blue,bold!}%s{!status!}' % self.upload
+ else:
+ self.bottombar += ' U: {!white,blue!}%s{!status!}' % self.upload
+
+ if self.config['max_upload_speed'] > -1:
+ self.bottombar += (
+ ' (%s ' % self.config['max_upload_speed'] + _('KiB/s') + ')'
+ )
+
+ if self.config['dht']:
+ self.bottombar += ' ' + _('DHT') + ': {!white,blue!}%s{!status!}' % self.dht
+
+ self.bottombar += ' ' + _('IP {!white,blue!}%s{!status!}') % (
+ self.external_ip if self.external_ip else _('n/a')
+ )
diff --git a/deluge/ui/console/widgets/window.py b/deluge/ui/console/widgets/window.py
new file mode 100644
index 0000000..2ef3528
--- /dev/null
+++ b/deluge/ui/console/widgets/window.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+import logging
+
+from deluge.ui.console.modes.basemode import add_string, mkpad, mkpanel
+from deluge.ui.console.utils.colors import get_color_pair
+
+try:
+ import curses
+except ImportError:
+ pass
+
+log = logging.getLogger(__name__)
+
+
+class BaseWindow(object):
+ """
+ BaseWindow creates a curses screen to be used for showing panels and popup dialogs
+ """
+
+ def __init__(self, title, width, height, posy=0, posx=0, encoding=None):
+ """
+ Args:
+ title (str): The title of the panel
+ width (int): Width of the panel
+ height (int): Height of the panel
+ posy (int): Position of the panel's first row relative to the terminal screen
+ posx (int): Position of the panel's first column relative to the terminal screen
+ encoding (str): Terminal encoding
+ """
+ self.title = title
+ self.posy, self.posx = posy, posx
+ if encoding is None:
+ from deluge import component
+
+ encoding = component.get('ConsoleUI').encoding
+ self.encoding = encoding
+
+ self.panel = mkpanel(curses.COLOR_GREEN, height, width, posy, posx)
+ self.outer_screen = self.panel.window()
+ self.outer_screen.bkgdset(0, curses.COLOR_RED)
+ by, bx = self.outer_screen.getbegyx()
+ self.screen = mkpad(get_color_pair('white', 'black'), height - 1, width - 2)
+ self._height, self._width = self.outer_screen.getmaxyx()
+
+ @property
+ def height(self):
+ return self._height
+
+ @property
+ def width(self):
+ return self._width
+
+ def add_string(self, row, string, scr=None, **kwargs):
+ scr = scr if scr else self.screen
+ add_string(row, string, scr, self.encoding, **kwargs)
+
+ def hide(self):
+ self.panel.hide()
+
+ def show(self):
+ self.panel.show()
+
+ def hidden(self):
+ return self.panel.hidden()
+
+ def set_title(self, title):
+ self.title = title
+
+ @property
+ def visible_content_pane_size(self):
+ y, x = self.outer_screen.getmaxyx()
+ return (y - 2, x - 2)
+
+ @property
+ def visible_content_pane_height(self):
+ y, x = self.visible_content_pane_size
+ return y
+
+ @property
+ def visible_content_pane_width(self):
+ y, x = self.visible_content_pane_size
+ return x
+
+ def getmaxyx(self):
+ return self.screen.getmaxyx()
+
+ def resize_window(self, rows, cols):
+ self.outer_screen.resize(rows, cols)
+ self.screen.resize(rows - 2, cols - 2)
+ self._height, self._width = rows, cols
+
+ def move_window(self, posy, posx):
+ posy = int(posy)
+ posx = int(posx)
+ self.outer_screen.mvwin(posy, posx)
+ self.posy = posy
+ self.posx = posx
+ self._height, self._width = self.screen.getmaxyx()
+
+ def ensure_content_pane_height(self, height):
+ max_y, max_x = self.screen.getmaxyx()
+ if max_y < height:
+ self.screen.resize(height, max_x)
+
+ def draw_scroll_indicator(self, screen):
+ content_height = self.get_content_height()
+ if content_height <= self.visible_content_pane_height:
+ return
+
+ percent_scroll = float(self.lineoff) / (
+ content_height - self.visible_content_pane_height
+ )
+ indicator_row = int(self.visible_content_pane_height * percent_scroll) + 1
+
+ # Never greater than height
+ indicator_row = min(indicator_row, self.visible_content_pane_height)
+ indicator_col = self.width + 1
+
+ add_string(
+ indicator_row,
+ '{!red,black,bold!}#',
+ screen,
+ self.encoding,
+ col=indicator_col,
+ pad=False,
+ trim=False,
+ )
+
+ def refresh(self):
+ height, width = self.visible_content_pane_size
+ self.outer_screen.erase()
+ self.outer_screen.border(0, 0, 0, 0)
+
+ if self.title:
+ toff = max(1, (self.width // 2) - (len(self.title) // 2))
+ self.add_string(
+ 0,
+ '{!white,black,bold!}%s' % self.title,
+ scr=self.outer_screen,
+ col=toff,
+ pad=False,
+ )
+
+ self.draw_scroll_indicator(self.outer_screen)
+ self.outer_screen.noutrefresh()
+
+ try:
+ # pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol
+ # the p arguments refer to the upper left corner of the pad region to be displayed and
+ # the s arguments define a clipping box on the screen within which the pad region is to be displayed.
+ pminrow = self.lineoff
+ pmincol = 0
+ sminrow = self.posy + 1
+ smincol = self.posx + 1
+ smaxrow = height + self.posy
+ smaxcol = width + self.posx
+ self.screen.noutrefresh(
+ pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol
+ )
+ except curses.error as ex:
+ import traceback
+
+ log.warning(
+ 'Error on screen.noutrefresh(%s, %s, %s, %s, %s, %s) Error: %s\nStack: %s',
+ pminrow,
+ pmincol,
+ sminrow,
+ smincol,
+ smaxrow,
+ smaxcol,
+ ex,
+ ''.join(traceback.format_stack()),
+ )