mirror of
https://github.com/rr-/pq-cli.git
synced 2026-09-29 06:44:00 -06:00
ui: implement tab navigation
This commit is contained in:
@@ -63,8 +63,6 @@ def bind_commands() -> None:
|
|||||||
}.items():
|
}.items():
|
||||||
for key_variant in {key, key.upper()}:
|
for key_variant in {key, key.upper()}:
|
||||||
urwid.command_map[key_variant] = f"cursor {direction}"
|
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:
|
class Ui:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import urwid
|
|||||||
|
|
||||||
from pqcli.ui.custom_button import CustomButton
|
from pqcli.ui.custom_button import CustomButton
|
||||||
from pqcli.ui.custom_line_box import CustomLineBox
|
from pqcli.ui.custom_line_box import CustomLineBox
|
||||||
|
from pqcli.ui.layout import NPile
|
||||||
|
|
||||||
|
|
||||||
class ConfirmDialog(urwid.Overlay):
|
class ConfirmDialog(urwid.Overlay):
|
||||||
@@ -19,9 +20,7 @@ class ConfirmDialog(urwid.Overlay):
|
|||||||
)
|
)
|
||||||
|
|
||||||
line_box = CustomLineBox(
|
line_box = CustomLineBox(
|
||||||
urwid.ListBox(
|
urwid.Filler(NPile([question, no_btn, yes_btn], outermost=True))
|
||||||
urwid.SimpleFocusListWalker([question, no_btn, yes_btn])
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import typing as T
|
|||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
|
from pqcli.ui.layout import NColumns, NPile
|
||||||
|
|
||||||
class DataTable(urwid.Pile):
|
|
||||||
|
class DataTable(NPile):
|
||||||
def __init__(self, columns: T.Any) -> None:
|
def __init__(self, columns: T.Any) -> None:
|
||||||
self.column_count = len(columns)
|
self.column_count = len(columns)
|
||||||
self._columns = columns
|
self._columns = columns
|
||||||
@@ -25,7 +27,7 @@ class DataTable(urwid.Pile):
|
|||||||
|
|
||||||
self.contents.append(
|
self.contents.append(
|
||||||
(
|
(
|
||||||
urwid.Columns(
|
NColumns(
|
||||||
list((*x, y) for x, y in zip(self._columns, widgets))
|
list((*x, y) for x, y in zip(self._columns, widgets))
|
||||||
),
|
),
|
||||||
(urwid.PACK, 1),
|
(urwid.PACK, 1),
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
|
from pqcli.ui.layout import NPile
|
||||||
|
|
||||||
|
|
||||||
class DoubleLineBox(urwid.AttrMap):
|
class DoubleLineBox(urwid.AttrMap):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -23,9 +25,7 @@ class DoubleLineBox(urwid.AttrMap):
|
|||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
urwid.Pile(
|
NPile([self.top_line_box, (urwid.PACK, self.bottom_line_box)]),
|
||||||
[self.top_line_box, (urwid.PACK, self.bottom_line_box)]
|
|
||||||
),
|
|
||||||
"linebox",
|
"linebox",
|
||||||
"linebox-focus",
|
"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.custom_progress_bar import CustomProgressBar
|
||||||
from pqcli.ui.data_table import DataTable
|
from pqcli.ui.data_table import DataTable
|
||||||
from pqcli.ui.double_line_box import DoubleLineBox
|
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.read_only_check_box import ReadOnlyCheckBox
|
||||||
from pqcli.ui.scrollable import ScrollBar, Scrollable
|
from pqcli.ui.scrollable import Scrollable, ScrollBar
|
||||||
|
|
||||||
|
|
||||||
class CharacterSheetView(DoubleLineBox):
|
class CharacterSheetView(DoubleLineBox):
|
||||||
@@ -301,7 +302,7 @@ class QuestBookView(DoubleLineBox):
|
|||||||
self.quest_bar.set_max(self.player.quest_book.quest_bar.max_)
|
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:
|
def __init__(self, player: Player) -> None:
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
@@ -331,7 +332,7 @@ class TaskView(urwid.Pile):
|
|||||||
return (size[0], 2)
|
return (size[0], 2)
|
||||||
|
|
||||||
|
|
||||||
class GameView(urwid.Pile):
|
class GameView(NPile):
|
||||||
signals = ["cancel"]
|
signals = ["cancel"]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -351,31 +352,31 @@ class GameView(urwid.Pile):
|
|||||||
self.quest_book_view = QuestBookView(player)
|
self.quest_book_view = QuestBookView(player)
|
||||||
self.task_view = TaskView(player)
|
self.task_view = TaskView(player)
|
||||||
|
|
||||||
self.columns = urwid.Columns(
|
self.columns = NColumns(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
urwid.WEIGHT,
|
urwid.WEIGHT,
|
||||||
1,
|
1,
|
||||||
urwid.Pile(
|
NPile(
|
||||||
[(19, self.character_sheet_view), self.spell_book_view]
|
[(19, self.character_sheet_view), self.spell_book_view]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
urwid.WEIGHT,
|
urwid.WEIGHT,
|
||||||
2,
|
2,
|
||||||
urwid.Pile(
|
NPile([(15, self.equipment_view), self.inventory_view]),
|
||||||
[(15, self.equipment_view), self.inventory_view]
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
urwid.WEIGHT,
|
urwid.WEIGHT,
|
||||||
2,
|
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:
|
def cancel(self) -> None:
|
||||||
self._emit("cancel")
|
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.mechanic import StatsBuilder, create_player, generate_name
|
||||||
from pqcli.ui.custom_button import CustomButton
|
from pqcli.ui.custom_button import CustomButton
|
||||||
from pqcli.ui.custom_line_box import CustomLineBox
|
from pqcli.ui.custom_line_box import CustomLineBox
|
||||||
|
from pqcli.ui.layout import NColumns, NPile
|
||||||
|
|
||||||
|
|
||||||
class StatsBox(CustomLineBox):
|
class StatsBox(CustomLineBox):
|
||||||
@@ -29,17 +30,17 @@ class StatsBox(CustomLineBox):
|
|||||||
label_texts = [urwid.Text(f"{stat.value}: ") for stat in PRIME_STATS]
|
label_texts = [urwid.Text(f"{stat.value}: ") for stat in PRIME_STATS]
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
urwid.ListBox(
|
urwid.Filler(
|
||||||
[
|
NPile(
|
||||||
urwid.Columns(
|
[
|
||||||
[urwid.Pile(label_texts), urwid.Pile(value_texts)]
|
NColumns([NPile(label_texts), NPile(value_texts)]),
|
||||||
),
|
urwid.Divider(),
|
||||||
urwid.Divider(),
|
NColumns([urwid.Text("Total: "), self.total_label]),
|
||||||
urwid.Columns([urwid.Text("Total: "), self.total_label]),
|
urwid.Divider(),
|
||||||
urwid.Divider(),
|
roll_button,
|
||||||
roll_button,
|
unroll_button,
|
||||||
unroll_button,
|
]
|
||||||
]
|
)
|
||||||
),
|
),
|
||||||
title="Stats",
|
title="Stats",
|
||||||
)
|
)
|
||||||
@@ -115,7 +116,7 @@ class ClassBox(CustomLineBox):
|
|||||||
self.class_ = user_data
|
self.class_ = user_data
|
||||||
|
|
||||||
|
|
||||||
class NewGameView(urwid.Pile):
|
class NewGameView(NPile):
|
||||||
signals = ["confirm", "cancel"]
|
signals = ["confirm", "cancel"]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@@ -134,7 +135,7 @@ class NewGameView(urwid.Pile):
|
|||||||
|
|
||||||
buttons_box = urwid.Filler(
|
buttons_box = urwid.Filler(
|
||||||
urwid.Padding(
|
urwid.Padding(
|
||||||
urwid.Pile(
|
NPile(
|
||||||
[
|
[
|
||||||
CustomButton(
|
CustomButton(
|
||||||
"Sold!", hint="F10", on_press=self.on_confirm_press
|
"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.race_box),
|
||||||
(urwid.WEIGHT, 2, self.class_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:
|
def generate_random_char_name(self) -> None:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import urwid
|
|||||||
from pqcli.lingo import act_name, to_roman
|
from pqcli.lingo import act_name, to_roman
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
from pqcli.ui.custom_button import CustomButton
|
from pqcli.ui.custom_button import CustomButton
|
||||||
|
from pqcli.ui.layout import NColumns, NPile
|
||||||
|
|
||||||
|
|
||||||
class RosterView(urwid.Filler):
|
class RosterView(urwid.Filler):
|
||||||
@@ -39,7 +40,7 @@ class RosterView(urwid.Filler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
buttons.append(
|
buttons.append(
|
||||||
urwid.Columns(
|
NColumns(
|
||||||
[
|
[
|
||||||
CustomButton(
|
CustomButton(
|
||||||
label=label,
|
label=label,
|
||||||
@@ -73,13 +74,10 @@ class RosterView(urwid.Filler):
|
|||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
urwid.Padding(
|
urwid.Padding(
|
||||||
urwid.Pile(
|
NPile(
|
||||||
[urwid.Padding(logo, width="clip"), urwid.Divider()]
|
[urwid.Padding(logo, width="clip"), urwid.Divider()]
|
||||||
+ buttons
|
+ buttons,
|
||||||
+ [
|
outermost=True,
|
||||||
urwid.Divider(),
|
|
||||||
urwid.Text("Use arrow keys to move around."),
|
|
||||||
]
|
|
||||||
),
|
),
|
||||||
align="center",
|
align="center",
|
||||||
width=logo.pack()[0],
|
width=logo.pack()[0],
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
import typing as T
|
import typing as T
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
from urwid.widget import BOX, FLOW, FIXED
|
from urwid.widget import BOX, FIXED, FLOW
|
||||||
|
|
||||||
# Scroll actions
|
# Scroll actions
|
||||||
SCROLL_LINE_UP = "line up"
|
SCROLL_LINE_UP = "line up"
|
||||||
|
|||||||
Reference in New Issue
Block a user