From 2e2851dc13d73352530dd4495c7e05603b2e520d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 23:38:38 +0200 Subject: Adding upstream version 2.1.2~dev0+20240219. Signed-off-by: Daniel Baumann --- deluge/ui/console/widgets/popup.py | 398 +++++++++++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 deluge/ui/console/widgets/popup.py (limited to 'deluge/ui/console/widgets/popup.py') diff --git a/deluge/ui/console/widgets/popup.py b/deluge/ui/console/widgets/popup.py new file mode 100644 index 0000000..07d667d --- /dev/null +++ b/deluge/ui/console/widgets/popup.py @@ -0,0 +1,398 @@ +# +# Copyright (C) 2011 Nick Lanham +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging + +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: + 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: + 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 = min(index, len(self.inputs) - 1) + + 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 -- cgit v1.2.3