# Copyright 2015 Canonical, Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """ UI utilities """ from functools import partialmethod import logging from urwid import ( ACTIVATE, AttrMap, CompositeCanvas, connect_signal, Padding as _Padding, SelectableIcon, Text, WidgetDecoration, WidgetDisable, ) from subiquitycore.ui.container import ListBox, Pile from subiquitycore.ui.table import TableRow from subiquitycore.ui.width import widget_width log = logging.getLogger("subiquitycore.ui.utils") def apply_padders(cls): """ Decorator for generating useful padding methods Loops through and generates methods like: Padding.push_1(Widget) Sets the left padding attribute by 1 Padding.pull_24(Widget) Sets right padding attribute by 24. Padding.center_50(Widget) Provides center padding with a relative width of 50 """ padding_count = 100 for i in range(1, padding_count): setattr(cls, 'push_{}'.format(i), partialmethod(_Padding, left=i)) setattr(cls, 'pull_{}'.format(i), partialmethod(_Padding, right=i)) setattr(cls, 'fixed_{}'.format(i), partialmethod(_Padding, align='center', width=i, min_width=i)) setattr(cls, 'center_{}'.format(i), partialmethod(_Padding, align='center', width=('relative', i))) setattr(cls, 'left_{}'.format(i), partialmethod(_Padding, align='left', width=('relative', i))) setattr(cls, 'right_{}'.format(i), partialmethod(_Padding, align='right', width=('relative', i))) return cls @apply_padders class Padding: """ Padding methods .. py:meth:: push_X(:class:`urwid.Widget`) This method supports padding the left side of the widget from 1-99, for example: .. code:: Padding.push_20(Text("This will be indented 20 columns") .. py:meth:: pull_X(:class:`urwid.Widget`) This method supports padding the right side of the widget from 1-99, for example: .. code:: Padding.pull_20(Text("This will be right indented 20 columns") .. py:meth:: fixed_X(:class:`urwid.Widget`) This method supports padding the widget to a fixed size and centering it. from 1-99, for example: .. code:: Padding.fixed_20(Text("This will be centered and fixed sized of 20 columns")) .. py:meth:: center_X(:class:`urwid.Widget`) This method centers a widget with X being the relative width of the widget. .. code:: Padding.center_10(Text("This will be centered with a " "width of 10 columns")) .. py:meth:: left_X(:class:`urwid.Widget`) This method aligns a widget left with X being the relative width of the widget. .. code:: Padding.left_10(Text("This will be left aligned with a " "width of 10 columns")) .. py:meth:: right_X(:class:`urwid.Widget`) This method right aligns a widget with X being the relative width of the widget. .. code:: Padding.right_10(Text("This will be right aligned with a " "width of 10 columns")) """ line_break = partialmethod(Text) # This makes assumptions about the style names defined by both # subiquity and console_conf. The fix is to stop using the Color class # below, I think. STYLE_NAMES = set([ 'body', 'danger_button focus', 'danger_button', 'done_button focus', 'done_button', 'frame_footer', 'frame_header', 'info_error', 'info_minor', 'info_primary', 'menu_button focus', 'menu_button', 'other_button focus', 'other_button', 'progress_complete', 'progress_incomplete', 'scrollbar focus', 'scrollbar', 'string_input focus', 'string_input', ]) def apply_style_map(cls): """ Applies AttrMap attributes to Color class Eg: Color.frame_header(Text("I'm text in the Orange frame header")) Color.body(Text("Im text in wrapped with the body color")) """ for k in STYLE_NAMES: kf = k + ' focus' if kf in STYLE_NAMES: setattr(cls, k, partialmethod(AttrMap, attr_map=k, focus_map=kf)) else: setattr(cls, k, partialmethod(AttrMap, attr_map=k)) return cls @apply_style_map class Color: """ Partial methods for :class:`~subiquity.palette.STYLES` .. py:meth:: frame_header(:class:`urwid.Widget`) This method colors widget based on the style map used. .. code:: Color.frame_header(Text("This will use foreground and background " "defined from the STYLES attribute")) """ pass _disable_everything_map = {k: 'info_minor' for k in STYLE_NAMES | set([None])} def disabled(w): return WidgetDisable(AttrMap(w, _disable_everything_map)) def button_pile(buttons): width = 14 for button in buttons: width = max(widget_width(button), width) return _Padding( Pile(buttons), min_width=width, width=width, align='center') def screen(rows, buttons, focus_buttons=True, excerpt=None, narrow_rows=False): """Helper to create a common screen layout. The commonest screen layout in subiquity is: [ 1 line padding (optional) ] excerpt (optional) [ 1 line padding ] Box widget (usually a ListBox) [ 1 line padding ] a button_pile [ 1 line padding ] This helper makes creating this a 1-liner. """ if isinstance(rows, list): rows = ListBox(rows) if narrow_rows: rows = Padding.center_63(rows) if buttons is None: focus_buttons = False elif isinstance(buttons, list): buttons = button_pile(buttons) excerpt_rows = [] if excerpt is not None: excerpt_rows = [ ('pack', Text("")), ('pack', Text(excerpt)), ] body = [ ('pack', Text("")), rows, ('pack', Text("")), ] if buttons is not None: body.extend([ ('pack', buttons), ('pack', Text("")), ]) pile = Pile(excerpt_rows + body) if focus_buttons: pile.focus_position = len(excerpt_rows) + 3 return Padding.center_79(pile) class CursorOverride(WidgetDecoration): """Decoration to override where the cursor goes when a widget is focused. """ has_original_width = True def __init__(self, w, cursor_x=0): super().__init__(w) self.cursor_x = cursor_x def get_cursor_coords(self, size): return self.cursor_x, 0 def rows(self, size, focus): return self._original_widget.rows(size, focus) def keypress(self, size, focus): return self._original_widget.keypress(size, focus) def render(self, size, focus=False): c = self._original_widget.render(size, focus) if focus: # create a new canvas so we can add a cursor c = CompositeCanvas(c) c.cursor = self.get_cursor_coords(size) return c class ClickableIcon(SelectableIcon): """Like Button, but simpler. """ signals = ['click'] def keypress(self, size, key): if self._command_map[key] != ACTIVATE: return key self._emit('click') def make_action_menu_row( cells, menu, attr_map='menu_button', focus_map='menu_button focus', cursor_x=2): row = TableRow(cells) if not isinstance(attr_map, dict): attr_map = {None: attr_map} if not isinstance(focus_map, dict): focus_map = {None: focus_map} am = AttrMap(CursorOverride(row, cursor_x=cursor_x), attr_map, focus_map) connect_signal(menu, 'open', lambda menu: am.set_attr_map(focus_map)) connect_signal(menu, 'close', lambda menu: am.set_attr_map(attr_map)) return am