Merge pull request #316 from CanonicalLtd/mwhudson/stretchy-overlay

add an overlay that has nicer resizing behaviour
This commit is contained in:
Michael Hudson-Doyle 2018-04-17 12:57:15 +12:00 committed by GitHub
commit ba041718dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 56 deletions

View File

@ -20,12 +20,7 @@ configuration.
""" """
import logging import logging
from urwid import ( from urwid import Text
LineBox,
Padding as UrwidPadding,
Text,
WidgetWrap,
)
from subiquitycore.ui.buttons import ( from subiquitycore.ui.buttons import (
back_btn, back_btn,
@ -36,6 +31,7 @@ from subiquitycore.ui.buttons import (
reset_btn, reset_btn,
) )
from subiquitycore.ui.container import Columns, ListBox, Pile 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.ui.utils import button_pile, Color, Padding
from subiquitycore.view import BaseView 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 \ You will not be able to return to this or a previous screen once \
the installation has started. 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): def __init__(self, parent, controller):
self.parent = parent self.parent = parent
self.controller = controller self.controller = controller
pile = Pile([ widgets = [
UrwidPadding(Text(confirmation_text), left=2, right=2), Text(confirmation_text),
Text(""),
button_pile([ button_pile([
cancel_btn(_("No"), on_press=self.cancel), cancel_btn(_("No"), on_press=self.cancel),
danger_btn(_("Continue"), on_press=self.ok)]), danger_btn(_("Continue"), on_press=self.ok)]),
Text(""), ]
]) super().__init__(
lb = LineBox(pile, title=_("Confirm destructive action")) _("Confirm destructive action"),
super().__init__(lb) widgets,
stretchy_index=0,
focus_index=2)
def ok(self, sender): def ok(self, sender):
self.controller.finish() self.controller.finish()
@ -275,4 +273,4 @@ class FilesystemView(BaseView):
self.controller.reset() self.controller.reset()
def done(self, button): def done(self, button):
self.show_overlay(FilesystemConfirmationView(self, self.controller), min_width=0) self.show_stretchy_overlay(FilesystemConfirmation(self, self.controller))

View File

@ -18,8 +18,6 @@ import logging
from urwid import ( from urwid import (
connect_signal, connect_signal,
LineBox, LineBox,
Padding as UrwidPadding,
SolidFill,
Text, Text,
WidgetWrap, WidgetWrap,
) )
@ -31,7 +29,6 @@ from subiquitycore.ui.buttons import (
) )
from subiquitycore.ui.container import ( from subiquitycore.ui.container import (
Columns, Columns,
ListBox,
Pile, Pile,
) )
from subiquitycore.ui.form import ( from subiquitycore.ui.form import (
@ -39,6 +36,9 @@ from subiquitycore.ui.form import (
Form, Form,
) )
from subiquitycore.ui.selector import Selector, Option 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.ui.utils import button_pile, Color, Padding, screen
from subiquitycore.view import BaseView from subiquitycore.view import BaseView
@ -302,7 +302,7 @@ toggle_options = [
] ]
class ToggleQuestion(WidgetWrap): class ToggleQuestion(Stretchy):
def __init__(self, parent, setting): def __init__(self, parent, setting):
self.parent = parent self.parent = parent
@ -315,28 +315,24 @@ class ToggleQuestion(WidgetWrap):
except AttributeError: except AttributeError:
pass pass
pile = Pile([ widgets = [
ListBox([ Text(_(toggle_text)),
Text(_(toggle_text)), Text(""),
]), Padding.center_79(Columns([
(1, SolidFill(" ")),
('pack', Padding.center_79(Columns([
('pack', Text(_("Shortcut: "))), ('pack', Text(_("Shortcut: "))),
Color.string_input(self.selector), Color.string_input(self.selector),
]))), ])),
(1, SolidFill(" ")), Text(""),
('pack', button_pile([ button_pile([
ok_btn(label=_("OK"), on_press=self.ok), ok_btn(label=_("OK"), on_press=self.ok),
cancel_btn(label=_("Cancel"), on_press=self.cancel), cancel_btn(label=_("Cancel"), on_press=self.cancel),
])), ]),
]) ]
pile.focus_position = 4
super().__init__( super().__init__(
LineBox( _("Select layout toggle"),
UrwidPadding( widgets,
pile, stretchy_index=0,
left=1, right=1), focus_index=4)
_("Select layout toggle")))
def ok(self, sender): def ok(self, sender):
self.parent.remove_overlay() self.parent.remove_overlay()
@ -411,7 +407,7 @@ class KeyboardView(BaseView):
setting = KeyboardSetting(layout=layout, variant=variant) setting = KeyboardSetting(layout=layout, variant=variant)
new_setting = setting.latinizable() new_setting = setting.latinizable()
if new_setting != setting: if new_setting != setting:
self.show_overlay(ToggleQuestion(self, new_setting), height=('relative', 100)) self.show_stretchy_overlay(ToggleQuestion(self, new_setting))
return return
self.really_done(setting) self.really_done(setting)

View File

@ -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 <http://www.gnu.org/licenses/>.
"""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)

View File

@ -18,17 +18,14 @@
Contains some default key navigations 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): class BaseView(WidgetWrap):
def __init__(self, w):
self.orig_w = None
super().__init__(w)
def show_overlay(self, overlay_widget, **kw): def show_overlay(self, overlay_widget, **kw):
self.orig_w = self._w
args = dict( args = dict(
align='center', align='center',
width=('relative', 60), width=('relative', 60),
@ -42,26 +39,22 @@ class BaseView(WidgetWrap):
if isinstance(kw['width'], int): if isinstance(kw['width'], int):
kw['width'] += 2*PADDING kw['width'] += 2*PADDING
args.update(kw) args.update(kw)
if 'height' in kw:
f = SolidFill(" ")
p = 1
else:
f = Text("")
p = 'pack'
top = Pile([ top = Pile([
(p, f), ('pack', Text("")),
Columns([ Columns([
(PADDING, f), (PADDING, Text("")),
overlay_widget, overlay_widget,
(PADDING, f) (PADDING, Text(""))
]), ]),
(p, f), ('pack', Text("")),
]) ])
self._w = Overlay(top_w=top, bottom_w=self._w, **args) 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): def remove_overlay(self):
self._w = self.orig_w self._w = self._w.bottom_w
self.orig_w = None
def cancel(self): def cancel(self):
pass pass