From 284f5499660e2a5fa643c9bc51a89c592ec68871 Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 8 Nov 2018 21:52:52 +0100 Subject: [PATCH] ui: implement tab navigation --- pqcli/ui/__init__.py | 2 - pqcli/ui/confirm_dialog.py | 5 +- pqcli/ui/data_table.py | 6 +- pqcli/ui/double_line_box.py | 6 +- pqcli/ui/game_view.py | 21 ++++--- pqcli/ui/layout.py | 119 ++++++++++++++++++++++++++++++++++++ pqcli/ui/new_game_view.py | 34 ++++++----- pqcli/ui/roster_view.py | 12 ++-- pqcli/ui/scrollable.py | 2 +- setup.py | 1 - 10 files changed, 163 insertions(+), 45 deletions(-) create mode 100644 pqcli/ui/layout.py diff --git a/pqcli/ui/__init__.py b/pqcli/ui/__init__.py index a497fcd..316401d 100644 --- a/pqcli/ui/__init__.py +++ b/pqcli/ui/__init__.py @@ -63,8 +63,6 @@ def bind_commands() -> None: }.items(): for key_variant in {key, key.upper()}: urwid.command_map[key_variant] = f"cursor {direction}" - urwid.command_map["tab"] = f"cursor down" - urwid.command_map["shift tab"] = f"cursor up" class Ui: diff --git a/pqcli/ui/confirm_dialog.py b/pqcli/ui/confirm_dialog.py index 1603131..b0baecb 100644 --- a/pqcli/ui/confirm_dialog.py +++ b/pqcli/ui/confirm_dialog.py @@ -4,6 +4,7 @@ import urwid from pqcli.ui.custom_button import CustomButton from pqcli.ui.custom_line_box import CustomLineBox +from pqcli.ui.layout import NPile class ConfirmDialog(urwid.Overlay): @@ -19,9 +20,7 @@ class ConfirmDialog(urwid.Overlay): ) line_box = CustomLineBox( - urwid.ListBox( - urwid.SimpleFocusListWalker([question, no_btn, yes_btn]) - ) + urwid.Filler(NPile([question, no_btn, yes_btn], outermost=True)) ) super().__init__( diff --git a/pqcli/ui/data_table.py b/pqcli/ui/data_table.py index 0b661c2..b124928 100644 --- a/pqcli/ui/data_table.py +++ b/pqcli/ui/data_table.py @@ -2,8 +2,10 @@ import typing as T import urwid +from pqcli.ui.layout import NColumns, NPile -class DataTable(urwid.Pile): + +class DataTable(NPile): def __init__(self, columns: T.Any) -> None: self.column_count = len(columns) self._columns = columns @@ -25,7 +27,7 @@ class DataTable(urwid.Pile): self.contents.append( ( - urwid.Columns( + NColumns( list((*x, y) for x, y in zip(self._columns, widgets)) ), (urwid.PACK, 1), diff --git a/pqcli/ui/double_line_box.py b/pqcli/ui/double_line_box.py index a495450..95caf3e 100644 --- a/pqcli/ui/double_line_box.py +++ b/pqcli/ui/double_line_box.py @@ -1,5 +1,7 @@ import urwid +from pqcli.ui.layout import NPile + class DoubleLineBox(urwid.AttrMap): def __init__( @@ -23,9 +25,7 @@ class DoubleLineBox(urwid.AttrMap): ) super().__init__( - urwid.Pile( - [self.top_line_box, (urwid.PACK, self.bottom_line_box)] - ), + NPile([self.top_line_box, (urwid.PACK, self.bottom_line_box)]), "linebox", "linebox-focus", ) diff --git a/pqcli/ui/game_view.py b/pqcli/ui/game_view.py index e828a43..c181922 100644 --- a/pqcli/ui/game_view.py +++ b/pqcli/ui/game_view.py @@ -18,8 +18,9 @@ from pqcli.ui.custom_line_box import CustomLineBox from pqcli.ui.custom_progress_bar import CustomProgressBar from pqcli.ui.data_table import DataTable from pqcli.ui.double_line_box import DoubleLineBox +from pqcli.ui.layout import NColumns, NPile from pqcli.ui.read_only_check_box import ReadOnlyCheckBox -from pqcli.ui.scrollable import ScrollBar, Scrollable +from pqcli.ui.scrollable import Scrollable, ScrollBar class CharacterSheetView(DoubleLineBox): @@ -301,7 +302,7 @@ class QuestBookView(DoubleLineBox): self.quest_bar.set_max(self.player.quest_book.quest_bar.max_) -class TaskView(urwid.Pile): +class TaskView(NPile): def __init__(self, player: Player) -> None: self.player = player @@ -331,7 +332,7 @@ class TaskView(urwid.Pile): return (size[0], 2) -class GameView(urwid.Pile): +class GameView(NPile): signals = ["cancel"] def __init__( @@ -351,31 +352,31 @@ class GameView(urwid.Pile): self.quest_book_view = QuestBookView(player) self.task_view = TaskView(player) - self.columns = urwid.Columns( + self.columns = NColumns( [ ( urwid.WEIGHT, 1, - urwid.Pile( + NPile( [(19, self.character_sheet_view), self.spell_book_view] ), ), ( urwid.WEIGHT, 2, - urwid.Pile( - [(15, self.equipment_view), self.inventory_view] - ), + NPile([(15, self.equipment_view), self.inventory_view]), ), ( urwid.WEIGHT, 2, - urwid.Pile([(15, self.plot_view), self.quest_book_view]), + NPile([(15, self.plot_view), self.quest_book_view]), ), ] ) - super().__init__([self.columns, (urwid.PACK, self.task_view)]) + super().__init__( + [self.columns, (urwid.PACK, self.task_view)], outermost=True + ) def cancel(self) -> None: self._emit("cancel") diff --git a/pqcli/ui/layout.py b/pqcli/ui/layout.py new file mode 100644 index 0000000..6580fa0 --- /dev/null +++ b/pqcli/ui/layout.py @@ -0,0 +1,119 @@ +# Copyright (c) 2013-2016 Christian Geier et al. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import typing as T + +import urwid + + +class SupportsNext: + """ + Classes inheriting from SupportsNext must implement the following methods: + _select_first_selectable + _select_last_selectable + """ + + def __init__(self, *args, **kwargs): + self.outermost = kwargs.get("outermost", False) + if "outermost" in kwargs: + kwargs.pop("outermost") + super().__init__(*args, **kwargs) + + +class NextMixin(SupportsNext): + """Implements SupportsNext for urwid.Pile and urwid.Columns""" + + def _select_first_selectable(self): + """ + select our first selectable item + (recursively if that item SupportsNext) + """ + i = self._first_selectable() + self.set_focus(i) + if isinstance(self.contents[i][0], SupportsNext): + self.contents[i][0]._select_first_selectable() + + def _select_last_selectable(self): + """ + select our last selectable item + (recursively if that item SupportsNext) + """ + i = self._last_selectable() + self.set_focus(i) + if isinstance(self._contents[i][0], SupportsNext): + self.contents[i][0]._select_last_selectable() + + def _first_selectable(self): + """return sequence number of self.contents last selectable item""" + for j in range(0, len(self._contents)): + if self._contents[j][0].selectable(): + return j + return False + + def _last_selectable(self): + """return sequence number of self._contents last selectable item""" + for j in range(len(self._contents) - 1, -1, -1): + if self._contents[j][0].selectable(): + return j + return False + + def keypress(self, size, key) -> T.Optional[str]: + key = super().keypress(size, key) + + if key == "tab": + if ( + self.outermost + and self.focus_position == self._last_selectable() + ): + self._select_first_selectable() + return None + + for i in range(self.focus_position + 1, len(self._contents)): + if self._contents[i][0].selectable(): + self.set_focus(i) + if isinstance(self._contents[i][0], SupportsNext): + self._contents[i][0]._select_first_selectable() + return None + + elif key == "shift tab": + if ( + self.outermost + and self.focus_position == self._first_selectable() + ): + self._select_last_selectable() + return None + + for i in range(self.focus_position - 1, 0 - 1, -1): + if self._contents[i][0].selectable(): + self.set_focus(i) + if isinstance(self._contents[i][0], SupportsNext): + self._contents[i][0]._select_last_selectable() + return None + + return key + + +class NPile(NextMixin, urwid.Pile): + pass + + +class NColumns(NextMixin, urwid.Columns): + pass diff --git a/pqcli/ui/new_game_view.py b/pqcli/ui/new_game_view.py index 9ab544d..a0dbfbd 100644 --- a/pqcli/ui/new_game_view.py +++ b/pqcli/ui/new_game_view.py @@ -8,6 +8,7 @@ from pqcli.config import CLASSES, PRIME_STATS, RACES from pqcli.mechanic import StatsBuilder, create_player, generate_name from pqcli.ui.custom_button import CustomButton from pqcli.ui.custom_line_box import CustomLineBox +from pqcli.ui.layout import NColumns, NPile class StatsBox(CustomLineBox): @@ -29,17 +30,17 @@ class StatsBox(CustomLineBox): label_texts = [urwid.Text(f"{stat.value}: ") for stat in PRIME_STATS] super().__init__( - urwid.ListBox( - [ - urwid.Columns( - [urwid.Pile(label_texts), urwid.Pile(value_texts)] - ), - urwid.Divider(), - urwid.Columns([urwid.Text("Total: "), self.total_label]), - urwid.Divider(), - roll_button, - unroll_button, - ] + urwid.Filler( + NPile( + [ + NColumns([NPile(label_texts), NPile(value_texts)]), + urwid.Divider(), + NColumns([urwid.Text("Total: "), self.total_label]), + urwid.Divider(), + roll_button, + unroll_button, + ] + ) ), title="Stats", ) @@ -115,7 +116,7 @@ class ClassBox(CustomLineBox): self.class_ = user_data -class NewGameView(urwid.Pile): +class NewGameView(NPile): signals = ["confirm", "cancel"] def __init__(self) -> None: @@ -134,7 +135,7 @@ class NewGameView(urwid.Pile): buttons_box = urwid.Filler( urwid.Padding( - urwid.Pile( + NPile( [ CustomButton( "Sold!", hint="F10", on_press=self.on_confirm_press @@ -162,14 +163,15 @@ class NewGameView(urwid.Pile): ) ), ), - urwid.Columns( + NColumns( [ (urwid.WEIGHT, 2, self.race_box), (urwid.WEIGHT, 2, self.class_box), - urwid.Pile([(13, self.stats_box), buttons_box]), + NPile([(13, self.stats_box), buttons_box]), ] ), - ] + ], + outermost=True, ) def generate_random_char_name(self) -> None: diff --git a/pqcli/ui/roster_view.py b/pqcli/ui/roster_view.py index 9dded89..2d8f600 100644 --- a/pqcli/ui/roster_view.py +++ b/pqcli/ui/roster_view.py @@ -5,6 +5,7 @@ import urwid from pqcli.lingo import act_name, to_roman from pqcli.roster import Roster from pqcli.ui.custom_button import CustomButton +from pqcli.ui.layout import NColumns, NPile class RosterView(urwid.Filler): @@ -39,7 +40,7 @@ class RosterView(urwid.Filler): ) buttons.append( - urwid.Columns( + NColumns( [ CustomButton( label=label, @@ -73,13 +74,10 @@ class RosterView(urwid.Filler): super().__init__( urwid.Padding( - urwid.Pile( + NPile( [urwid.Padding(logo, width="clip"), urwid.Divider()] - + buttons - + [ - urwid.Divider(), - urwid.Text("Use arrow keys to move around."), - ] + + buttons, + outermost=True, ), align="center", width=logo.pack()[0], diff --git a/pqcli/ui/scrollable.py b/pqcli/ui/scrollable.py index 9fb772b..3601a08 100644 --- a/pqcli/ui/scrollable.py +++ b/pqcli/ui/scrollable.py @@ -19,7 +19,7 @@ import typing as T import urwid -from urwid.widget import BOX, FLOW, FIXED +from urwid.widget import BOX, FIXED, FLOW # Scroll actions SCROLL_LINE_UP = "line up" diff --git a/setup.py b/setup.py index 3bd380b..6da2498 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ from setuptools import find_packages, setup - setup( author="rr-", author_email="rr-@sakuya.pl",