1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
4# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
5# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
6#
7# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
8# the additional special exception to link portions of this program with the OpenSSL library.
9# See LICENSE for more details.
10#
11
12from __future__ import unicode_literals
13
14import logging
15
16from deluge.decorators import overrides
17from deluge.ui.console.modes.basemode import InputKeyHandler, move_cursor
18from deluge.ui.console.utils import curses_util as util
19from deluge.ui.console.widgets.fields import (
20    CheckedInput,
21    CheckedPlusInput,
22    ComboInput,
23    DividerField,
24    FloatSpinInput,
25    Header,
26    InfoField,
27    IntSpinInput,
28    NoInputField,
29    SelectInput,
30    TextArea,
31    TextField,
32    TextInput,
33)
34
35try:
36    import curses
37except ImportError:
38    pass
39
40log = logging.getLogger(__name__)
41
42
43class BaseInputPane(InputKeyHandler):
44    def __init__(
45        self,
46        mode,
47        allow_rearrange=False,
48        immediate_action=False,
49        set_first_input_active=True,
50        border_off_west=0,
51        border_off_north=0,
52        border_off_east=0,
53        border_off_south=0,
54        active_wrap=False,
55        **kwargs
56    ):
57        InputKeyHandler.__init__(self)
58        self.inputs = []
59        self.mode = mode
60        self.active_input = 0
61        self.set_first_input_active = set_first_input_active
62        self.allow_rearrange = allow_rearrange
63        self.immediate_action = immediate_action
64        self.move_active_many = 4
65        self.active_wrap = active_wrap
66        self.lineoff = 0
67        self.border_off_west = border_off_west
68        self.border_off_north = border_off_north
69        self.border_off_east = border_off_east
70        self.border_off_south = border_off_south
71        self.last_lineoff_move = 0
72
73        if not hasattr(self, 'visible_content_pane_height'):
74            log.error(
75                'The class "%s" does not have the attribute "%s" required by super class "%s"',
76                self.__class__.__name__,
77                'visible_content_pane_height',
78                BaseInputPane.__name__,
79            )
80            raise AttributeError('visible_content_pane_height')
81
82    @property
83    def visible_content_pane_width(self):
84        return self.mode.width
85
86    def add_spaces(self, num):
87        string = ''
88        for i in range(num):
89            string += '\n'
90
91        self.add_text_area('space %d' % len(self.inputs), string)
92
93    def add_text(self, string):
94        self.add_text_area('', string)
95
96    def move(self, r, c):
97        self._cursor_row = r
98        self._cursor_col = c
99
100    def get_input(self, name):
101        for e in self.inputs:
102            if e.name == name:
103                return e
104
105    def _add_input(self, input_element):
106        for e in self.inputs:
107            if isinstance(e, NoInputField):
108                continue
109            if e.name == input_element.name:
110                import traceback
111
112                log.warning(
113                    'Input element with name "%s" already exists in input pane (%s):\n%s',
114                    input_element.name,
115                    e,
116                    ''.join(traceback.format_stack(limit=5)),
117                )
118                return
119
120        self.inputs.append(input_element)
121        if self.set_first_input_active and input_element.selectable():
122            self.active_input = len(self.inputs) - 1
123            self.set_first_input_active = False
124        return input_element
125
126    def add_header(self, header, space_above=False, space_below=False, **kwargs):
127        return self._add_input(Header(self, header, space_above, space_below, **kwargs))
128
129    def add_info_field(self, name, label, value):
130        return self._add_input(InfoField(self, name, label, value))
131
132    def add_text_field(self, name, message, selectable=True, col='+1', **kwargs):
133        return self._add_input(
134            TextField(self, name, message, selectable=selectable, col=col, **kwargs)
135        )
136
137    def add_text_area(self, name, message, **kwargs):
138        return self._add_input(TextArea(self, name, message, **kwargs))
139
140    def add_divider_field(self, name, message, **kwargs):
141        return self._add_input(DividerField(self, name, message, **kwargs))
142
143    def add_text_input(self, name, message, value='', col='+1', **kwargs):
144        """
145        Add a text input field
146
147        :param message: string to display above the input field
148        :param name: name of the field, for the return callback
149        :param value: initial value of the field
150        :param complete: should completion be run when tab is hit and this field is active
151        """
152        return self._add_input(
153            TextInput(
154                self,
155                name,
156                message,
157                self.move,
158                self.visible_content_pane_width,
159                value,
160                col=col,
161                **kwargs
162            )
163        )
164
165    def add_select_input(self, name, message, opts, vals, default_index=0, **kwargs):
166        return self._add_input(
167            SelectInput(self, name, message, opts, vals, default_index, **kwargs)
168        )
169
170    def add_checked_input(self, name, message, checked=False, col='+1', **kwargs):
171        return self._add_input(
172            CheckedInput(self, name, message, checked=checked, col=col, **kwargs)
173        )
174
175    def add_checkedplus_input(
176        self, name, message, child, checked=False, col='+1', **kwargs
177    ):
178        return self._add_input(
179            CheckedPlusInput(
180                self, name, message, child, checked=checked, col=col, **kwargs
181            )
182        )
183
184    def add_float_spin_input(self, name, message, value=0.0, col='+1', **kwargs):
185        return self._add_input(
186            FloatSpinInput(self, name, message, self.move, value, col=col, **kwargs)
187        )
188
189    def add_int_spin_input(self, name, message, value=0, col='+1', **kwargs):
190        return self._add_input(
191            IntSpinInput(self, name, message, self.move, value, col=col, **kwargs)
192        )
193
194    def add_combo_input(self, name, message, choices, col='+1', **kwargs):
195        return self._add_input(
196            ComboInput(self, name, message, choices, col=col, **kwargs)
197        )
198
199    @overrides(InputKeyHandler)
200    def handle_read(self, c):
201        if not self.inputs:  # no inputs added yet
202            return util.ReadState.IGNORED
203        ret = self.inputs[self.active_input].handle_read(c)
204        if ret != util.ReadState.IGNORED:
205            if self.immediate_action:
206                self.immediate_action_cb(
207                    state_changed=False if ret == util.ReadState.READ else True
208                )
209            return ret
210
211        ret = util.ReadState.READ
212
213        if c == curses.KEY_UP:
214            self.move_active_up(1)
215        elif c == curses.KEY_DOWN:
216            self.move_active_down(1)
217        elif c == curses.KEY_HOME:
218            self.move_active_up(len(self.inputs))
219        elif c == curses.KEY_END:
220            self.move_active_down(len(self.inputs))
221        elif c == curses.KEY_PPAGE:
222            self.move_active_up(self.move_active_many)
223        elif c == curses.KEY_NPAGE:
224            self.move_active_down(self.move_active_many)
225        elif c == util.KEY_ALT_AND_ARROW_UP:
226            self.lineoff = max(self.lineoff - 1, 0)
227        elif c == util.KEY_ALT_AND_ARROW_DOWN:
228            tot_height = self.get_content_height()
229            self.lineoff = min(
230                self.lineoff + 1, tot_height - self.visible_content_pane_height
231            )
232        elif c == util.KEY_CTRL_AND_ARROW_UP:
233            if not self.allow_rearrange:
234                return ret
235            val = self.inputs.pop(self.active_input)
236            self.active_input -= 1
237            self.inputs.insert(self.active_input, val)
238            if self.immediate_action:
239                self.immediate_action_cb(state_changed=True)
240        elif c == util.KEY_CTRL_AND_ARROW_DOWN:
241            if not self.allow_rearrange:
242                return ret
243            val = self.inputs.pop(self.active_input)
244            self.active_input += 1
245            self.inputs.insert(self.active_input, val)
246            if self.immediate_action:
247                self.immediate_action_cb(state_changed=True)
248        else:
249            ret = util.ReadState.IGNORED
250        return ret
251
252    def get_values(self):
253        vals = {}
254        for i, ipt in enumerate(self.inputs):
255            if not ipt.has_input():
256                continue
257            vals[ipt.name] = {
258                'value': ipt.get_value(),
259                'order': i,
260                'active': self.active_input == i,
261            }
262        return vals
263
264    def immediate_action_cb(self, state_changed=True):
265        pass
266
267    def move_active(self, direction, amount):
268        """
269        direction == -1: Up
270        direction ==  1: Down
271
272        """
273        self.last_lineoff_move = direction * amount
274
275        if direction > 0:
276            if self.active_wrap:
277                limit = self.active_input - 1
278                if limit < 0:
279                    limit = len(self.inputs) + limit
280            else:
281                limit = len(self.inputs) - 1
282        else:
283            limit = 0
284            if self.active_wrap:
285                limit = self.active_input + 1
286
287        def next_move(nc, direction, limit):
288            next_index = nc
289            while next_index != limit:
290                next_index += direction
291                if direction > 0:
292                    next_index %= len(self.inputs)
293                elif next_index < 0:
294                    next_index = len(self.inputs) + next_index
295
296                if self.inputs[next_index].selectable():
297                    return next_index
298                if next_index == limit:
299                    return nc
300            return nc
301
302        next_sel = self.active_input
303        for a in range(amount):
304            cur_sel = next_sel
305            next_sel = next_move(next_sel, direction, limit)
306            if cur_sel == next_sel:
307                tot_height = (
308                    self.get_content_height()
309                    + self.border_off_north
310                    + self.border_off_south
311                )
312                if direction > 0:
313                    self.lineoff = min(
314                        self.lineoff + 1, tot_height - self.visible_content_pane_height
315                    )
316                else:
317                    self.lineoff = max(self.lineoff - 1, 0)
318
319        if next_sel is not None:
320            self.active_input = next_sel
321
322    def move_active_up(self, amount):
323        self.move_active(-1, amount)
324        if self.immediate_action:
325            self.immediate_action_cb(state_changed=False)
326
327    def move_active_down(self, amount):
328        self.move_active(1, amount)
329        if self.immediate_action:
330            self.immediate_action_cb(state_changed=False)
331
332    def get_content_height(self):
333        height = 0
334        for i, ipt in enumerate(self.inputs):
335            if ipt.depend_skip():
336                continue
337            height += ipt.height
338        return height
339
340    def ensure_active_visible(self):
341        start_row = 0
342        end_row = self.border_off_north
343        for i, ipt in enumerate(self.inputs):
344            if ipt.depend_skip():
345                continue
346            start_row = end_row
347            end_row += ipt.height
348            if i != self.active_input or not ipt.has_input():
349                continue
350            height = self.visible_content_pane_height
351            if end_row > height + self.lineoff:
352                self.lineoff += end_row - (
353                    height + self.lineoff
354                )  # Correct result depends on paranthesis
355            elif start_row < self.lineoff:
356                self.lineoff -= self.lineoff - start_row
357            break
358
359    def render_inputs(self, focused=False):
360        self._cursor_row = -1
361        self._cursor_col = -1
362        util.safe_curs_set(util.Curser.INVISIBLE)
363
364        self.ensure_active_visible()
365
366        crow = self.border_off_north
367        for i, ipt in enumerate(self.inputs):
368            if ipt.depend_skip():
369                continue
370            col = self.border_off_west
371            field_width = self.width - self.border_off_east - self.border_off_west
372            cursor_offset = self.border_off_west
373
374            if ipt.default_col != -1:
375                default_col = int(ipt.default_col)
376                if isinstance(ipt.default_col, ''.__class__) and ipt.default_col[0] in [
377                    '+',
378                    '-',
379                ]:
380                    col += default_col
381                    cursor_offset += default_col
382                    field_width -= default_col  # Increase to col must be reflected here
383                else:
384                    col = default_col
385            crow += ipt.render(
386                self.screen,
387                crow,
388                width=field_width,
389                active=i == self.active_input,
390                focused=focused,
391                col=col,
392                cursor_offset=cursor_offset,
393            )
394
395        if self._cursor_row >= 0:
396            util.safe_curs_set(util.Curser.VERY_VISIBLE)
397            move_cursor(self.screen, self._cursor_row, self._cursor_col)
398