deluge/deluge/ui/console/widgets/popup.py
Calum Lind 930cf87103
[Lint] Update pre-commit apps to latest versions
Also update github CI action versions
2023-02-24 14:59:15 +00:00

398 lines
13 KiB
Python

#
# 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.
#
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 = 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