mirror of
https://github.com/rr-/pq-cli.git
synced 2026-09-28 22:33:59 -06:00
ui: implement tab navigation
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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__(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
+11
-10
@@ -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")
|
||||
|
||||
@@ -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
|
||||
+18
-16
@@ -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:
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user