diff --git a/pqcli/ui/curses/__init__.py b/pqcli/ui/curses/__init__.py index 3196c4a..386d632 100644 --- a/pqcli/ui/curses/__init__.py +++ b/pqcli/ui/curses/__init__.py @@ -12,6 +12,7 @@ from pqcli.roster import Roster from pqcli.ui.base import BaseUserInterface from pqcli.ui.curses.colors import ( COLOR_FOCUSED, + COLOR_HIGHLIGHT, COLOR_LOGO, COLOR_LOGO_ALT, COLOR_PROGRESSBAR, @@ -57,6 +58,7 @@ class CursesUserInterface(BaseUserInterface): curses.init_pair(COLOR_SCROLLBAR_THUMB, -1, 7) curses.init_pair(COLOR_SCROLLBAR_TRACK, -1, 0) curses.init_pair(COLOR_PROGRESSBAR, 0, 9) + curses.init_pair(COLOR_HIGHLIGHT, 3, -1) def signal_handler(sig: T.Any, frame: T.Any) -> None: if self.args.use_saves: diff --git a/pqcli/ui/curses/colors.py b/pqcli/ui/curses/colors.py index 45fa121..1713123 100644 --- a/pqcli/ui/curses/colors.py +++ b/pqcli/ui/curses/colors.py @@ -6,6 +6,7 @@ COLOR_FOCUSED = 3 COLOR_SCROLLBAR_TRACK = 4 COLOR_SCROLLBAR_THUMB = 5 COLOR_PROGRESSBAR = 6 +COLOR_HIGHLIGHT = 7 def has_colors() -> bool: diff --git a/pqcli/ui/curses/views/game_view/equipment_window.py b/pqcli/ui/curses/views/game_view/equipment_window.py index 2259fd8..150a990 100644 --- a/pqcli/ui/curses/views/game_view/equipment_window.py +++ b/pqcli/ui/curses/views/game_view/equipment_window.py @@ -16,7 +16,7 @@ class EquipmentWindow(Focusable, WindowWrapper): ) self._player = player - self._player.equipment.connect("change", self.sync_equipment_change) + self._player.equipment.connect("change", self._sync_equipment_change) self.sync() @@ -24,7 +24,9 @@ class EquipmentWindow(Focusable, WindowWrapper): super().stop() self._data_table.stop() - self._player.equipment.disconnect("change", self.sync_equipment_change) + self._player.equipment.disconnect( + "change", self._sync_equipment_change + ) def sync(self) -> None: self._data_table.clear() @@ -33,12 +35,14 @@ class EquipmentWindow(Focusable, WindowWrapper): equipment_type.value.ljust(15), self._player.equipment[equipment_type] or "", ) + self._data_table.select(None) self._render() - def sync_equipment_change( + def _sync_equipment_change( self, equipment_type: EquipmentType, item_name: T.Optional[str] ) -> None: self._data_table.set(equipment_type.value, item_name or "") + self._data_table.select(equipment_type.value) self._render() def _render(self) -> None: diff --git a/pqcli/ui/curses/views/game_view/inventory_window.py b/pqcli/ui/curses/views/game_view/inventory_window.py index 927202f..9ad881c 100644 --- a/pqcli/ui/curses/views/game_view/inventory_window.py +++ b/pqcli/ui/curses/views/game_view/inventory_window.py @@ -78,6 +78,7 @@ class InventoryWindow(Focusable, DataTableProgressBarWindow): def _sync_gold(self) -> None: self._data_table.set("Gold", str(self._player.inventory.gold)) + self._data_table.select("Gold") self._render_data_table() def _sync_items(self) -> None: @@ -86,18 +87,22 @@ class InventoryWindow(Focusable, DataTableProgressBarWindow): for item in self._player.inventory: self._data_table.add(item.name, str(item.quantity)) self._data_table.scroll_to_item(0) + self._data_table.select(None) self._render_data_table() def _sync_item_add(self, item: InventoryItem) -> None: self._data_table.add(item.name, str(item.quantity)) + self._data_table.select(item.name) self._render_data_table() def _sync_item_del(self, item: InventoryItem) -> None: self._data_table.delete(item.name) + self._data_table.select(None) self._render_data_table() def _sync_item_change(self, item: InventoryItem) -> None: self._data_table.set(item.name, str(item.quantity)) + self._data_table.select(item.name) self._render_data_table() def _on_new_task(self) -> None: diff --git a/pqcli/ui/curses/views/game_view/plot_window.py b/pqcli/ui/curses/views/game_view/plot_window.py index d8d671d..a578c56 100644 --- a/pqcli/ui/curses/views/game_view/plot_window.py +++ b/pqcli/ui/curses/views/game_view/plot_window.py @@ -60,6 +60,7 @@ class PlotWindow(Focusable, ListBoxProgressBarWindow): if prev is not None: self._list_box.set(-1, "[X] " + prev[4:]) self._list_box.add("[ ] " + act_name(act_number)) + self._list_box.select(-1) self._render_list_box() def _sync_position(self) -> None: diff --git a/pqcli/ui/curses/views/game_view/quest_book_window.py b/pqcli/ui/curses/views/game_view/quest_book_window.py index 9947152..a237aab 100644 --- a/pqcli/ui/curses/views/game_view/quest_book_window.py +++ b/pqcli/ui/curses/views/game_view/quest_book_window.py @@ -48,6 +48,7 @@ class QuestBookWindow(Focusable, ListBoxProgressBarWindow): self._list_box.clear() for quest_name in self._player.quest_book.quests[-self.cutoff :]: self._sync_quest_add(quest_name) + self._list_box.select(-1) self._render_list_box() def _sync_quest_add(self, quest_name: str) -> None: @@ -56,6 +57,7 @@ class QuestBookWindow(Focusable, ListBoxProgressBarWindow): if prev is not None: self._list_box.set(-1, "[X] " + prev[4:]) self._list_box.add("[ ] " + quest_name) + self._list_box.select(-1) self._render_list_box() def _sync_position(self) -> None: diff --git a/pqcli/ui/curses/views/game_view/spell_book_window.py b/pqcli/ui/curses/views/game_view/spell_book_window.py index db4275f..877efc2 100644 --- a/pqcli/ui/curses/views/game_view/spell_book_window.py +++ b/pqcli/ui/curses/views/game_view/spell_book_window.py @@ -41,14 +41,17 @@ class SpellBookWindow(Focusable, WindowWrapper): self._data_table.clear() for spell in self._player.spell_book: self._data_table.add(spell.name, to_roman(spell.level)) + self._data_table.select(None) self._render() def _sync_spell_add(self, spell: Spell) -> None: self._data_table.add(spell.name, to_roman(spell.level)) + self._data_table.select(spell.name) self._render() def _sync_spell_change(self, spell: Spell) -> None: self._data_table.set(spell.name, to_roman(spell.level)) + self._data_table.select(spell.name) self._render() def _render(self) -> None: diff --git a/pqcli/ui/curses/widgets/data_table.py b/pqcli/ui/curses/widgets/data_table.py index f2ee2cc..9764c3b 100644 --- a/pqcli/ui/curses/widgets/data_table.py +++ b/pqcli/ui/curses/widgets/data_table.py @@ -2,6 +2,8 @@ import curses import enum import typing as T +from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors + from .scrollable import Scrollable @@ -19,6 +21,7 @@ class DataTable(Scrollable): self._align_right = align_right self._items: T.List[T.Tuple[str, str]] + self._selected: T.Optional[int] = None def add(self, text: str, value: str) -> None: self._items.append((text, value)) @@ -38,6 +41,9 @@ class DataTable(Scrollable): del self._items[idx] self.scroll_to_item(idx) + def select(self, text: T.Optional[str]) -> None: + self._selected = None if text is None else self.get_idx(text) + def get_idx(self, text: str) -> T.Optional[int]: for idx, row in enumerate(self._items): if row[0].strip() == text.strip(): @@ -48,20 +54,28 @@ class DataTable(Scrollable): assert self._pad if self._align_right: for y, row in enumerate(self._items): + if y == self._selected and has_colors(): + self._pad.attron(curses.color_pair(COLOR_HIGHLIGHT)) self._pad.move(y, 0) self._pad.addnstr(row[0], min(len(row[0]), w)) col2_x = max(0, w - len(row[1])) if col2_x < w: self._pad.move(y, col2_x) self._pad.addstr(row[1]) + if y == self._selected and has_colors(): + self._pad.attroff(curses.color_pair(COLOR_HIGHLIGHT)) else: col2_x = self._get_col_width(0) for y, row in enumerate(self._items): + if y == self._selected and has_colors(): + self._pad.attron(curses.color_pair(COLOR_HIGHLIGHT)) self._pad.addnstr(y, 0, row[0], min(len(row[0]), w)) if col2_x < w: self._pad.addnstr( y, col2_x, row[1], min(len(row[1]), w - col2_x) ) + if y == self._selected and has_colors(): + self._pad.attroff(curses.color_pair(COLOR_HIGHLIGHT)) def _get_col_width(self, x: int) -> int: return max([len(row[0]) for row in self._items] + [0]) diff --git a/pqcli/ui/curses/widgets/list_box.py b/pqcli/ui/curses/widgets/list_box.py index a757d73..867b53d 100644 --- a/pqcli/ui/curses/widgets/list_box.py +++ b/pqcli/ui/curses/widgets/list_box.py @@ -2,12 +2,18 @@ import curses import enum import typing as T +from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors + from .scrollable import Scrollable class ListBox(Scrollable): _items: T.List[str] + def __init__(self, parent: T.Any, h: int, w: int, y: int, x: int) -> None: + super().__init__(parent, h, w, y, x) + self._selected: T.Optional[int] = None + def add(self, text: str) -> None: self._items.append(text) self.scroll_to_item(-1) @@ -30,7 +36,16 @@ class ListBox(Scrollable): def delete(self, idx: int, count: int = 1) -> None: del self._items[idx : idx + count] + def select(self, idx: T.Optional[int]) -> None: + if idx < 0: + idx %= len(self._items) + self._selected = idx + def _render_impl(self, h: int, w: int) -> None: assert self._pad for y, item in enumerate(self._items): + if y == self._selected and has_colors(): + self._pad.attron(curses.color_pair(COLOR_HIGHLIGHT)) self._pad.addnstr(y, 0, item, min(len(item), w)) + if y == self._selected and has_colors(): + self._pad.attroff(curses.color_pair(COLOR_HIGHLIGHT))