From 6a76f8ad02f6e04616b16574807563d4724be9cb Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Tue, 17 Apr 2018 12:22:00 +1200 Subject: [PATCH 1/3] add a version of Overlay that copes with small and large screens better See the docstring for the long version. --- subiquitycore/ui/stretchy.py | 145 +++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 subiquitycore/ui/stretchy.py diff --git a/subiquitycore/ui/stretchy.py b/subiquitycore/ui/stretchy.py new file mode 100644 index 00000000..414950b6 --- /dev/null +++ b/subiquitycore/ui/stretchy.py @@ -0,0 +1,145 @@ +# Copyright 2018 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 . + +"""Custom overlay that only takes the space it needs but can still scroll. + +There are a couple of dialogs in subiquity that basically have the form: + + +------ dialog title --------+ + | | + | potentially quite long | + | block of text that might | + | take several lines and so | + | need to be scrollable | + | | + | maybe another widget | + | | + | [ OK ] | + | [ Cancel ] | + | | + +----------------------------+ + +The way urwid works makes doing this nicely hard. Simply putting the +text in a ListBox "works" but because the listbox is a box widget we +can't let it choose its height: either we'll end up having the text +scrollable in some situations where there is enough space on the +screen to show it all or when the screen is large there will be a +massive ugly space between the text and the following widgets. + +Because you can't make a widget that behaves the way we want, this +module gives a way of providing a list of widgets to display in a Pile +and and nominating one of them to be made scrollable. A title, a list +of widgets and index to be scrollable is bundled up into an object +called a "stretchy" but is a bad name but at least easily greppable. +""" + + +import urwid + +from subiquitycore.ui.container import ListBox, Pile + +class Stretchy: + def __init__(self, title, widgets, stretchy_index, focus_index): + """ + title: goes in the LineBox + widgets: list of widgets to put in the pile + stretchy_index: index into widgets of widget to wrap in ListBox + focus_index: index into widgets of initial focus + """ + self.title = title + self.widgets = widgets + self.stretchy_index = stretchy_index + self.focus_index = focus_index + + @property + def stretchy_w(self): + return self.widgets[self.stretchy_index] + + +class StretchyOverlay(urwid.Widget): + _selectable = True + _sizing = frozenset([urwid.BOX]) + def __init__(self, bottom_w, stretchy): + self.bottom_w = bottom_w + self.stretchy = stretchy + self.listbox = ListBox([stretchy.stretchy_w]) + def entry(i, w): + if i == stretchy.stretchy_index: + return ('weight', 1, self.listbox) + else: + return ('pack', w) + # this Filler/Padding/LineBox/Filler/Padding construction + # seems ridiculous but it works. + self.top_w = urwid.Filler( + urwid.Padding( + urwid.LineBox( + urwid.Filler( + urwid.Padding( + Pile( + [entry(i, w) for (i, w) in enumerate(stretchy.widgets)], + focus_item=stretchy.widgets[stretchy.focus_index]), + left=2, right=2), + top=1, bottom=1, height=('relative', 100)), + title=stretchy.title), + left=3, right=3), + top=1, bottom=1, height=('relative', 100)) + + def _top_size(self, size, focus): + # Returns the size of the top widget and whether the scollbar will be shown. + + maxcol, maxrow = size # we are a BOX widget + outercol = min(maxcol, 80) + innercol = outercol - 10 # (3 outer padding, 1 line, 2 inner padding) x 2 + fixed_rows = 6 # lines at top and bottom and padding + + for i, widget in enumerate(self.stretchy.widgets): + if i == self.stretchy.stretchy_index: + continue + if urwid.FLOW in widget.sizing(): + rows = widget.rows((innercol,), focus) + fixed_rows += rows + else: + w_size = widget.pack((), focus) + fixed_rows += w_size[1] + + if fixed_rows > maxrow: + # There's no space for the stretchy widget at all, + # probably something will break but well let's defer the + # problem for now. + return (outercol, size[1]), False + + stretchy_ideal_rows = self.stretchy.stretchy_w.rows((innercol,), focus) + + if maxrow - fixed_rows >= stretchy_ideal_rows: + return (outercol, stretchy_ideal_rows + fixed_rows), False + else: + return (outercol, size[1]), True + + def keypress(self, size, key): + top_size, scrollbar_visible = self._top_size(size, True) + self.listbox._selectable = scrollbar_visible + return self.top_w.keypress(top_size, key) + + def render(self, size, focus): + bottom_c = self.bottom_w.render(size, False) + if not bottom_c.cols() or not bottom_c.rows(): + return urwid.CompositeCanvas(bottom_c) + + top_size, _ = self._top_size(size, focus) + top_c = self.top_w.render(top_size, focus) + top_c = urwid.CompositeCanvas(top_c) + left = (size[0] - top_size[0]) // 2 + top = (size[1] - top_size[1]) // 2 + return urwid.CanvasOverlay(top_c, bottom_c, left, top) From 7eafa420fbc284d5957aea61540e06586b07709e Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Tue, 17 Apr 2018 12:23:11 +1200 Subject: [PATCH 2/3] add show_stretch_overlay, simplify show_overlay --- subiquitycore/view.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/subiquitycore/view.py b/subiquitycore/view.py index d95c084f..f295b12d 100644 --- a/subiquitycore/view.py +++ b/subiquitycore/view.py @@ -18,17 +18,14 @@ Contains some default key navigations """ -from urwid import Columns, Overlay, Pile, SolidFill, Text, WidgetWrap +from subiquitycore.ui.stretchy import StretchyOverlay + +from urwid import Columns, Overlay, Pile, Text, WidgetWrap class BaseView(WidgetWrap): - def __init__(self, w): - self.orig_w = None - super().__init__(w) - def show_overlay(self, overlay_widget, **kw): - self.orig_w = self._w args = dict( align='center', width=('relative', 60), @@ -42,26 +39,22 @@ class BaseView(WidgetWrap): if isinstance(kw['width'], int): kw['width'] += 2*PADDING args.update(kw) - if 'height' in kw: - f = SolidFill(" ") - p = 1 - else: - f = Text("") - p = 'pack' top = Pile([ - (p, f), + ('pack', Text("")), Columns([ - (PADDING, f), + (PADDING, Text("")), overlay_widget, - (PADDING, f) + (PADDING, Text("")) ]), - (p, f), + ('pack', Text("")), ]) self._w = Overlay(top_w=top, bottom_w=self._w, **args) + def show_stretchy_overlay(self, stretchy): + self._w = StretchyOverlay(self._w, stretchy) + def remove_overlay(self): - self._w = self.orig_w - self.orig_w = None + self._w = self._w.bottom_w def cancel(self): pass From 458520e6718b02db4066070bf82293757f83ffd5 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Tue, 17 Apr 2018 12:24:07 +1200 Subject: [PATCH 3/3] use the new stretchy overlays for the layout toggle and the install confirmation --- subiquity/ui/views/filesystem/filesystem.py | 30 ++++++++-------- subiquity/ui/views/keyboard.py | 40 ++++++++++----------- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/subiquity/ui/views/filesystem/filesystem.py b/subiquity/ui/views/filesystem/filesystem.py index a7d6e1b8..c0c67d5e 100644 --- a/subiquity/ui/views/filesystem/filesystem.py +++ b/subiquity/ui/views/filesystem/filesystem.py @@ -20,12 +20,7 @@ configuration. """ import logging -from urwid import ( - LineBox, - Padding as UrwidPadding, - Text, - WidgetWrap, - ) +from urwid import Text from subiquitycore.ui.buttons import ( back_btn, @@ -36,6 +31,7 @@ from subiquitycore.ui.buttons import ( reset_btn, ) from subiquitycore.ui.container import Columns, ListBox, Pile +from subiquitycore.ui.stretchy import Stretchy from subiquitycore.ui.utils import button_pile, Color, Padding from subiquitycore.view import BaseView @@ -52,22 +48,24 @@ result in the loss of data on the disks selected to be formatted. You will not be able to return to this or a previous screen once \ the installation has started. -Are you sure you want to continue? -""") +Are you sure you want to continue?""") -class FilesystemConfirmationView(WidgetWrap): +class FilesystemConfirmation(Stretchy): def __init__(self, parent, controller): self.parent = parent self.controller = controller - pile = Pile([ - UrwidPadding(Text(confirmation_text), left=2, right=2), + widgets = [ + Text(confirmation_text), + Text(""), button_pile([ cancel_btn(_("No"), on_press=self.cancel), danger_btn(_("Continue"), on_press=self.ok)]), - Text(""), - ]) - lb = LineBox(pile, title=_("Confirm destructive action")) - super().__init__(lb) + ] + super().__init__( + _("Confirm destructive action"), + widgets, + stretchy_index=0, + focus_index=2) def ok(self, sender): self.controller.finish() @@ -275,4 +273,4 @@ class FilesystemView(BaseView): self.controller.reset() def done(self, button): - self.show_overlay(FilesystemConfirmationView(self, self.controller), min_width=0) + self.show_stretchy_overlay(FilesystemConfirmation(self, self.controller)) diff --git a/subiquity/ui/views/keyboard.py b/subiquity/ui/views/keyboard.py index 753f325e..c5dfd709 100644 --- a/subiquity/ui/views/keyboard.py +++ b/subiquity/ui/views/keyboard.py @@ -18,8 +18,6 @@ import logging from urwid import ( connect_signal, LineBox, - Padding as UrwidPadding, - SolidFill, Text, WidgetWrap, ) @@ -31,7 +29,6 @@ from subiquitycore.ui.buttons import ( ) from subiquitycore.ui.container import ( Columns, - ListBox, Pile, ) from subiquitycore.ui.form import ( @@ -39,6 +36,9 @@ from subiquitycore.ui.form import ( Form, ) from subiquitycore.ui.selector import Selector, Option +from subiquitycore.ui.stretchy import ( + Stretchy, + ) from subiquitycore.ui.utils import button_pile, Color, Padding, screen from subiquitycore.view import BaseView @@ -302,7 +302,7 @@ toggle_options = [ ] -class ToggleQuestion(WidgetWrap): +class ToggleQuestion(Stretchy): def __init__(self, parent, setting): self.parent = parent @@ -315,28 +315,24 @@ class ToggleQuestion(WidgetWrap): except AttributeError: pass - pile = Pile([ - ListBox([ - Text(_(toggle_text)), - ]), - (1, SolidFill(" ")), - ('pack', Padding.center_79(Columns([ + widgets = [ + Text(_(toggle_text)), + Text(""), + Padding.center_79(Columns([ ('pack', Text(_("Shortcut: "))), Color.string_input(self.selector), - ]))), - (1, SolidFill(" ")), - ('pack', button_pile([ + ])), + Text(""), + button_pile([ ok_btn(label=_("OK"), on_press=self.ok), cancel_btn(label=_("Cancel"), on_press=self.cancel), - ])), - ]) - pile.focus_position = 4 + ]), + ] super().__init__( - LineBox( - UrwidPadding( - pile, - left=1, right=1), - _("Select layout toggle"))) + _("Select layout toggle"), + widgets, + stretchy_index=0, + focus_index=4) def ok(self, sender): self.parent.remove_overlay() @@ -411,7 +407,7 @@ class KeyboardView(BaseView): setting = KeyboardSetting(layout=layout, variant=variant) new_setting = setting.latinizable() if new_setting != setting: - self.show_overlay(ToggleQuestion(self, new_setting), height=('relative', 100)) + self.show_stretchy_overlay(ToggleQuestion(self, new_setting)) return self.really_done(setting)