312 lines
12 KiB
Python
312 lines
12 KiB
Python
# -*- 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.
|
|
#
|
|
|
|
try:
|
|
import curses
|
|
except ImportError:
|
|
pass
|
|
|
|
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)
|
|
|
|
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.warn("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, basestring) 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)
|