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