mirror of
https://github.com/rr-/pq-cli.git
synced 2026-09-28 22:33:59 -06:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5545e2324a | ||
|
|
000b11c26c | ||
|
|
b9a0934e74 | ||
|
|
a823c9041e | ||
|
|
58758d35d5 | ||
|
|
e7d5947299 | ||
|
|
16ec0cfa5d | ||
|
|
ce7a422482 | ||
|
|
3cdafe4ec8 | ||
|
|
9bfbaf1646 | ||
|
|
4bb0e9250a | ||
|
|
a930b2b3db | ||
|
|
2372dcdcaa | ||
|
|
8e25fbea04 | ||
|
|
70ab6eab48 | ||
|
|
37252d2727 | ||
|
|
284f549966 | ||
|
|
904951acf4 | ||
|
|
3b19191120 | ||
|
|
72fe5439b0 | ||
|
|
88a936c293 | ||
|
|
0b74d1a36e | ||
|
|
14149cd112 | ||
|
|
bb233b82b8 | ||
|
|
723c4aae88 | ||
|
|
6bd12797f0 | ||
|
|
066fc692e4 | ||
|
|
86f59d8fb2 | ||
|
|
78e39e639e | ||
|
|
6c2fed9d40 | ||
|
|
b5b9554f9b | ||
|
|
3afa965eeb | ||
|
|
b00cf34e8f | ||
|
|
c7c5abf7c8 | ||
|
|
c8d8d646e8 | ||
|
|
7692d2e531 | ||
|
|
69cab66c9e | ||
|
|
eb07950527 |
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="ProgressQuest" src="http://progressquest.com/pq.png">
|
<img alt="Progress Quest" src="http://progressquest.com/pq.png">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Relive the great adventure… this time in the terminal realm!
|
Relive the great adventure… this time in the terminal realm!
|
||||||
@@ -19,7 +19,7 @@ Relive the great adventure… this time in the terminal realm!
|
|||||||
|
|
||||||
## How it looks like
|
## How it looks like
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -11,7 +11,22 @@ SAVE_PATH = Path(xdg.XDG_CONFIG_HOME) / "pqcli" / "save.dat"
|
|||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(prog="pqcli")
|
parser = argparse.ArgumentParser(prog="pqcli")
|
||||||
parser.add_argument("--cheats", help="enable cheats", action="store_true")
|
parser.add_argument(
|
||||||
|
"--no-config",
|
||||||
|
dest="use_config",
|
||||||
|
action="store_false",
|
||||||
|
help=(
|
||||||
|
"Don't load or save configuration files from $XDG_CONFIG_HOME "
|
||||||
|
"(player data will still be saved)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-save",
|
||||||
|
dest="use_saves",
|
||||||
|
action="store_false",
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
parser.add_argument("--cheats", action="store_true", help="???")
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -22,7 +37,8 @@ def main() -> None:
|
|||||||
try:
|
try:
|
||||||
Ui(roster, args).run()
|
Ui(roster, args).run()
|
||||||
finally:
|
finally:
|
||||||
roster.save()
|
if args.use_saves:
|
||||||
|
roster.save()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+45
-38
@@ -86,6 +86,12 @@ class Stats(SignalMixin):
|
|||||||
def best(self) -> StatType:
|
def best(self) -> StatType:
|
||||||
return max(self, key=lambda kv: kv[1])[0]
|
return max(self, key=lambda kv: kv[1])[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def best_prime(self) -> StatType:
|
||||||
|
return max(
|
||||||
|
(kv for kv in self if kv[0] in PRIME_STATS), key=lambda kv: kv[1]
|
||||||
|
)[0]
|
||||||
|
|
||||||
def increment(self, stat: StatType, qty: int = 1) -> None:
|
def increment(self, stat: StatType, qty: int = 1) -> None:
|
||||||
self._values[stat] += qty
|
self._values[stat] += qty
|
||||||
logger.info("Increased %s to %d", stat.value, self[stat])
|
logger.info("Increased %s to %d", stat.value, self[stat])
|
||||||
@@ -93,7 +99,7 @@ class Stats(SignalMixin):
|
|||||||
|
|
||||||
|
|
||||||
class QuestBook(SignalMixin):
|
class QuestBook(SignalMixin):
|
||||||
signals = ["complete_act", "complete_quest"]
|
signals = ["start_act", "start_quest"]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._quests: T.List[str] = []
|
self._quests: T.List[str] = []
|
||||||
@@ -118,7 +124,7 @@ class QuestBook(SignalMixin):
|
|||||||
|
|
||||||
def increment_act(self) -> None:
|
def increment_act(self) -> None:
|
||||||
self._act += 1
|
self._act += 1
|
||||||
self.emit("complete_act")
|
self.emit("start_act", self._act)
|
||||||
|
|
||||||
def add_quest(self, name: str) -> None:
|
def add_quest(self, name: str) -> None:
|
||||||
logger.info("Commencing quest: %s", name)
|
logger.info("Commencing quest: %s", name)
|
||||||
@@ -302,6 +308,7 @@ class Player(SignalMixin):
|
|||||||
self.race: Race = race
|
self.race: Race = race
|
||||||
self.class_: Class = class_
|
self.class_: Class = class_
|
||||||
self.stats: Stats = stats
|
self.stats: Stats = stats
|
||||||
|
self.elapsed = 0.0
|
||||||
|
|
||||||
self.exp_bar = Bar(max_=level_up_time(1))
|
self.exp_bar = Bar(max_=level_up_time(1))
|
||||||
self.level = 1
|
self.level = 1
|
||||||
@@ -311,10 +318,14 @@ class Player(SignalMixin):
|
|||||||
self.equipment = Equipment()
|
self.equipment = Equipment()
|
||||||
self.spell_book = SpellBook()
|
self.spell_book = SpellBook()
|
||||||
|
|
||||||
self.task_bar = Bar(max_=0)
|
self.task_bar = Bar(max_=1)
|
||||||
self.task: T.Optional[BaseTask] = None
|
self.task: T.Optional[BaseTask] = None
|
||||||
self.queue: T.List[BaseTask] = []
|
self.queue: T.List[BaseTask] = []
|
||||||
|
|
||||||
|
def __setstate__(self, obj) -> None:
|
||||||
|
self.__dict__.update(obj)
|
||||||
|
self.elapsed = obj.get("elapsed", 0.0)
|
||||||
|
|
||||||
def set_task(self, task: BaseTask) -> None:
|
def set_task(self, task: BaseTask) -> None:
|
||||||
self.task = task
|
self.task = task
|
||||||
self.task_bar.reset(task.duration)
|
self.task_bar.reset(task.duration)
|
||||||
@@ -423,38 +434,38 @@ class Simulation:
|
|||||||
def __init__(self, player: Player) -> None:
|
def __init__(self, player: Player) -> None:
|
||||||
self.player = player
|
self.player = player
|
||||||
self.last_tick = datetime.datetime.now()
|
self.last_tick = datetime.datetime.now()
|
||||||
self.elapsed = 0.0
|
|
||||||
|
|
||||||
def tick(self, elapsed: float = 100.0) -> None:
|
def tick(self, elapsed: float = 100.0) -> None:
|
||||||
|
self.player.elapsed += elapsed
|
||||||
|
|
||||||
if self.player.task is None:
|
if self.player.task is None:
|
||||||
self.player.set_task(RegularTask("Loading", 2_000))
|
self.player.set_task(RegularTask("Loading", 2000))
|
||||||
self.player.queue += [
|
self.player.queue += [
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"Experiencing an enigmatic and foreboding night vision",
|
"Experiencing an enigmatic and foreboding night vision",
|
||||||
10_000,
|
10000,
|
||||||
),
|
),
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"Much is revealed about that wise old bastard "
|
"Much is revealed about that wise old bastard "
|
||||||
"you'd underestimated",
|
"you'd underestimated",
|
||||||
6_000,
|
6000,
|
||||||
),
|
),
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"A shocking series of events leaves you "
|
"A shocking series of events leaves you "
|
||||||
"alone and bewildered, but resolute",
|
"alone and bewildered, but resolute",
|
||||||
6_000,
|
6000,
|
||||||
),
|
),
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"Drawing upon an unrealized reserve of determination, "
|
"Drawing upon an unrealized reserve of determination, "
|
||||||
"you set out on a long and dangerous journey",
|
"you set out on a long and dangerous journey",
|
||||||
4_000,
|
4000,
|
||||||
),
|
),
|
||||||
PlotTask(f"Loading {act_name(1)}", 2_000),
|
PlotTask(f"Loading {act_name(1)}", 2000),
|
||||||
]
|
]
|
||||||
self.player.quest_book.plot_bar.reset(28)
|
self.player.quest_book.plot_bar.reset(28)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.player.task_bar.done:
|
if not self.player.task_bar.done:
|
||||||
self.elapsed += elapsed
|
|
||||||
self.player.task_bar.increment(elapsed)
|
self.player.task_bar.increment(elapsed)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -528,7 +539,7 @@ class Simulation:
|
|||||||
self.player.set_task(
|
self.player.set_task(
|
||||||
SellTask(
|
SellTask(
|
||||||
"Selling " + indefinite(item.name, item.quantity),
|
"Selling " + indefinite(item.name, item.quantity),
|
||||||
1_000,
|
1000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
@@ -541,21 +552,19 @@ class Simulation:
|
|||||||
self.player.set_task(self.player.queue.pop(0))
|
self.player.set_task(self.player.queue.pop(0))
|
||||||
elif self.player.inventory.encum_bar.done:
|
elif self.player.inventory.encum_bar.done:
|
||||||
self.player.set_task(
|
self.player.set_task(
|
||||||
HeadingToMarketTask(
|
HeadingToMarketTask("Heading to market to sell loot", 4000)
|
||||||
"Heading to market to sell loot", 4_000
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
elif not isinstance(old, (KillTask, HeadingToKillingFieldsTask)):
|
elif not isinstance(old, (KillTask, HeadingToKillingFieldsTask)):
|
||||||
if self.player.inventory.gold > self.player.equip_price():
|
if self.player.inventory.gold > self.player.equip_price():
|
||||||
self.player.set_task(
|
self.player.set_task(
|
||||||
BuyTask(
|
BuyTask(
|
||||||
"Negotiating purchase of better equipment", 5_000
|
"Negotiating purchase of better equipment", 5000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.player.set_task(
|
self.player.set_task(
|
||||||
HeadingToKillingFieldsTask(
|
HeadingToKillingFieldsTask(
|
||||||
"Heading to the killing fields", 4_000
|
"Heading to the killing fields", 4000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -612,7 +621,7 @@ class Simulation:
|
|||||||
raise AssertionError
|
raise AssertionError
|
||||||
|
|
||||||
self.player.quest_book.add_quest(caption)
|
self.player.quest_book.add_quest(caption)
|
||||||
self.player.quest_book.emit("complete_quest")
|
self.player.quest_book.emit("start_quest", caption)
|
||||||
|
|
||||||
def interplot_cinematic(self) -> None:
|
def interplot_cinematic(self) -> None:
|
||||||
def enqueue(task: BaseTask) -> None:
|
def enqueue(task: BaseTask) -> None:
|
||||||
@@ -625,19 +634,19 @@ class Simulation:
|
|||||||
RegularTask(
|
RegularTask(
|
||||||
"Exhausted, you arrive at a friendly oasis "
|
"Exhausted, you arrive at a friendly oasis "
|
||||||
"in a hostile land",
|
"in a hostile land",
|
||||||
1_000,
|
1000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask("You greet old friends and meet new allies", 2_000)
|
RegularTask("You greet old friends and meet new allies", 2000)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"You are privy to a council of powerful do-gooders", 2_000
|
"You are privy to a council of powerful do-gooders", 2000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask("There is much to be done. You are chosen!", 1_000)
|
RegularTask("There is much to be done. You are chosen!", 1000)
|
||||||
)
|
)
|
||||||
|
|
||||||
elif choice == 1:
|
elif choice == 1:
|
||||||
@@ -645,7 +654,7 @@ class Simulation:
|
|||||||
RegularTask(
|
RegularTask(
|
||||||
"Your quarry is in sight, "
|
"Your quarry is in sight, "
|
||||||
"but a mighty enemy bars your path!",
|
"but a mighty enemy bars your path!",
|
||||||
1_000,
|
1000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -653,7 +662,7 @@ class Simulation:
|
|||||||
|
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
f"A desperate struggle commences with {nemesis}", 4_000
|
f"A desperate struggle commences with {nemesis}", 4000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -665,20 +674,20 @@ class Simulation:
|
|||||||
if s % 3 == 0:
|
if s % 3 == 0:
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
f"Locked in grim combat with {nemesis}", 2_000
|
f"Locked in grim combat with {nemesis}", 2000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif s % 3 == 1:
|
elif s % 3 == 1:
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
f"{nemesis} seems to have the upper hand", 2_000
|
f"{nemesis} seems to have the upper hand", 2000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif s % 3 == 2:
|
elif s % 3 == 2:
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
f"You seem to gain the advantage over {nemesis}",
|
f"You seem to gain the advantage over {nemesis}",
|
||||||
2_000,
|
2000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -688,12 +697,12 @@ class Simulation:
|
|||||||
RegularTask(
|
RegularTask(
|
||||||
f"Victory! {nemesis} is slain! "
|
f"Victory! {nemesis} is slain! "
|
||||||
"Exhausted, you lose conciousness",
|
"Exhausted, you lose conciousness",
|
||||||
3_000,
|
3000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"You awake in a friendly place, but the road awaits", 2_000
|
"You awake in a friendly place, but the road awaits", 2000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -703,37 +712,35 @@ class Simulation:
|
|||||||
RegularTask(
|
RegularTask(
|
||||||
"Oh sweet relief! "
|
"Oh sweet relief! "
|
||||||
f"You've reached the protection of the good {nemesis}",
|
f"You've reached the protection of the good {nemesis}",
|
||||||
2_000,
|
2000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"There is rejoicing, "
|
"There is rejoicing, "
|
||||||
f"and an unnerving encouter with {nemesis} in private",
|
f"and an unnerving encouter with {nemesis} in private",
|
||||||
3_000,
|
3000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
f"You forget your {boring_item()} and go back to get it",
|
f"You forget your {boring_item()} and go back to get it",
|
||||||
2_000,
|
2000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"What's this!? You overhear something shocking!", 2_000
|
"What's this!? You overhear something shocking!", 2000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(f"Could {nemesis} be a dirty double-dealer?", 2000)
|
||||||
f"Could {nemesis} be a dirty double-dealer?", 2_000
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"Who can possibly be trusted with this news!? ... "
|
"Who can possibly be trusted with this news!? ... "
|
||||||
"Oh yes, of course",
|
"Oh yes, of course",
|
||||||
3_000,
|
3000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -742,7 +749,7 @@ class Simulation:
|
|||||||
|
|
||||||
enqueue(
|
enqueue(
|
||||||
PlotTask(
|
PlotTask(
|
||||||
f"Loading {act_name(self.player.quest_book.act + 1)}", 1_000
|
f"Loading {act_name(self.player.quest_book.act + 1)}", 1000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -24,9 +24,9 @@ logging.basicConfig(
|
|||||||
|
|
||||||
|
|
||||||
# make things predictable
|
# make things predictable
|
||||||
SEED_A = 1664525
|
SEED_A = 1_664_525
|
||||||
SEED_B = 2141
|
SEED_B = 2141
|
||||||
SEED_C = 1013904223
|
SEED_C = 1_013_904_223
|
||||||
|
|
||||||
|
|
||||||
def custom_random(num: int) -> int:
|
def custom_random(num: int) -> int:
|
||||||
|
|||||||
+28
-3
@@ -1,7 +1,10 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import typing as T
|
import typing as T
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
import xdg
|
||||||
|
|
||||||
from pqcli import random
|
from pqcli import random
|
||||||
from pqcli.mechanic import Player
|
from pqcli.mechanic import Player
|
||||||
@@ -24,7 +27,26 @@ PALETTE: T.List[T.Tuple[str, ...]] = [
|
|||||||
("progressbar-normal", "", "", "standout"),
|
("progressbar-normal", "", "", "standout"),
|
||||||
("progressbar-done", "black", "light red"),
|
("progressbar-done", "black", "light red"),
|
||||||
("progressbar-smooth", "light red", ""),
|
("progressbar-smooth", "light red", ""),
|
||||||
|
("scrollbar", "light gray", "black"),
|
||||||
|
("scrollbar-focus", "light red", "black"),
|
||||||
|
("scrollable", "", ""),
|
||||||
|
("scrollable-focus", "", ""),
|
||||||
]
|
]
|
||||||
|
PALETTE_PATH = Path(xdg.XDG_CONFIG_HOME) / "pqcli" / "palette.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_palette() -> bool:
|
||||||
|
global PALETTE
|
||||||
|
if PALETTE_PATH.exists():
|
||||||
|
PALETTE = json.loads(PALETTE_PATH.read_text())
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def save_palette() -> None:
|
||||||
|
global PALETTE
|
||||||
|
PALETTE_PATH.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
PALETTE_PATH.write_text(json.dumps(PALETTE, indent=4))
|
||||||
|
|
||||||
|
|
||||||
class ConfirmExitDialog(ConfirmDialog):
|
class ConfirmExitDialog(ConfirmDialog):
|
||||||
@@ -41,14 +63,16 @@ 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:
|
||||||
def __init__(self, roster: Roster, args: argparse.Namespace) -> None:
|
def __init__(self, roster: Roster, args: argparse.Namespace) -> None:
|
||||||
bind_commands()
|
bind_commands()
|
||||||
|
|
||||||
|
if args.use_config:
|
||||||
|
if not load_palette():
|
||||||
|
save_palette() # create a file for the user to edit
|
||||||
|
|
||||||
self.roster = roster
|
self.roster = roster
|
||||||
self.args = args
|
self.args = args
|
||||||
self.loop = urwid.MainLoop(
|
self.loop = urwid.MainLoop(
|
||||||
@@ -138,5 +162,6 @@ class Ui:
|
|||||||
self.loop.set_alarm_in(TICK_FREQ, self.on_tick)
|
self.loop.set_alarm_in(TICK_FREQ, self.on_tick)
|
||||||
|
|
||||||
def on_save(self, _loop: urwid.MainLoop, _user_data: T.Any) -> None:
|
def on_save(self, _loop: urwid.MainLoop, _user_data: T.Any) -> None:
|
||||||
self.roster.save()
|
if self.args.use_saves:
|
||||||
|
self.roster.save()
|
||||||
self.loop.set_alarm_in(SAVE_FREQ, self.on_save)
|
self.loop.set_alarm_in(SAVE_FREQ, self.on_save)
|
||||||
|
|||||||
@@ -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__(
|
||||||
@@ -42,3 +41,7 @@ class ConfirmDialog(urwid.Overlay):
|
|||||||
|
|
||||||
def cancel(self) -> None:
|
def cancel(self) -> None:
|
||||||
self._emit("cancel")
|
self._emit("cancel")
|
||||||
|
|
||||||
|
def tick(self) -> None:
|
||||||
|
if hasattr(self.bottom_w, "tick"):
|
||||||
|
self.bottom_w.tick()
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ class CustomButton(urwid.Button):
|
|||||||
self._w = urwid.AttrMap(
|
self._w = urwid.AttrMap(
|
||||||
urwid.Columns(
|
urwid.Columns(
|
||||||
[
|
[
|
||||||
("pack", urwid.Text("< ")),
|
(urwid.PACK, urwid.Text("< ")),
|
||||||
urwid.SelectableIcon([label], _MAGIC),
|
urwid.SelectableIcon([label], _MAGIC),
|
||||||
(
|
(
|
||||||
"pack",
|
urwid.PACK,
|
||||||
urwid.Text(f"[{hint}]" if hint else "", align="right"),
|
urwid.Text(f"[{hint}]" if hint else "", align="right"),
|
||||||
),
|
),
|
||||||
("pack", urwid.Text(" >")),
|
(urwid.PACK, urwid.Text(" >")),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"button",
|
"button",
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import typing as T
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
|
|
||||||
|
|
||||||
class CustomListWalker(urwid.SimpleFocusListWalker):
|
|
||||||
def append(self, item: T.Any) -> None:
|
|
||||||
super().append(item)
|
|
||||||
self.set_focus(len(self) - 1)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomListBox(urwid.ListBox):
|
|
||||||
def __init__(self, widgets: T.List[urwid.Widget]) -> None:
|
|
||||||
super().__init__(CustomListWalker(widgets))
|
|
||||||
@@ -1,37 +1,88 @@
|
|||||||
|
import datetime
|
||||||
import typing as T
|
import typing as T
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
# workaround for https://github.com/urwid/urwid/issues/317
|
|
||||||
|
def format_float(num: float) -> str:
|
||||||
|
ret = f"{num:.01f}"
|
||||||
|
if ret.endswith("0"):
|
||||||
|
ret = ret[:-2]
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def format_timespan(timespan: datetime.timedelta) -> str:
|
||||||
|
num = timespan.total_seconds()
|
||||||
|
if num < 60.0:
|
||||||
|
return f"~{int(num)}s"
|
||||||
|
num /= 60
|
||||||
|
if num < 60.0:
|
||||||
|
return f"~{int(num)}m"
|
||||||
|
num /= 60
|
||||||
|
if num < 24.0:
|
||||||
|
return f"~{format_float(num)}h"
|
||||||
|
num /= 24
|
||||||
|
return f"~{format_float(num)}d"
|
||||||
|
|
||||||
|
|
||||||
class CustomProgressBar(urwid.Widget):
|
class CustomProgressBar(urwid.Widget):
|
||||||
_sizing = frozenset(["flow"])
|
_sizing = frozenset(["flow"])
|
||||||
text_align = "center"
|
text_align = "center"
|
||||||
eighths = u" ▏▎▍▌▋▊▉"
|
eighths = " ▏▎▍▌▋▊▉"
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, show_time: bool) -> None:
|
||||||
|
self.show_time = show_time
|
||||||
self.position = 0.0
|
self.position = 0.0
|
||||||
self.max_ = 1.0
|
self.max_ = 1.0
|
||||||
self.normal = "progressbar-normal"
|
self.normal = "progressbar-normal"
|
||||||
self.complete = "progressbar-done"
|
self.complete = "progressbar-done"
|
||||||
self.smooth = "progressbar-smooth"
|
self.smooth = "progressbar-smooth"
|
||||||
|
self.last_tick: T.Optional[datetime.datetime, float] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_left(self) -> T.Optional[datetime.timedelta]:
|
||||||
|
if self.last_tick is None:
|
||||||
|
return None
|
||||||
|
time_now = datetime.datetime.now()
|
||||||
|
time_then = self.last_tick[0]
|
||||||
|
pos_now = self.position
|
||||||
|
pos_then = self.last_tick[1]
|
||||||
|
speed = (pos_now - pos_then) / (time_now - time_then).total_seconds()
|
||||||
|
if not speed:
|
||||||
|
return None
|
||||||
|
return datetime.timedelta(seconds=(self.max_ - pos_now) / speed)
|
||||||
|
|
||||||
def set_completion(self, value: float) -> None:
|
def set_completion(self, value: float) -> None:
|
||||||
|
if self.last_tick is None or value == 0:
|
||||||
|
self.last_tick = (datetime.datetime.now(), value)
|
||||||
self.position = value
|
self.position = value
|
||||||
self._invalidate()
|
self._invalidate()
|
||||||
|
|
||||||
def set_max(self, value: T.Union[int, float]) -> None:
|
def set_max(self, value: T.Union[int, float]) -> None:
|
||||||
|
if self.max_ == value:
|
||||||
|
return
|
||||||
|
self.last_tick = None
|
||||||
self.max_ = value
|
self.max_ = value
|
||||||
self._invalidate()
|
self._invalidate()
|
||||||
|
|
||||||
|
def reset(self, cur: float, max_: T.Union[int, float]) -> None:
|
||||||
|
if self.last_tick is None or cur == 0 or max_ != self.max_:
|
||||||
|
self.last_tick = (datetime.datetime.now(), cur)
|
||||||
|
self.position = cur
|
||||||
|
self.max_ = max_
|
||||||
|
self._invalidate()
|
||||||
|
|
||||||
def rows(self, size: T.Tuple[int, int], focus: bool = False) -> int:
|
def rows(self, size: T.Tuple[int, int], focus: bool = False) -> int:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def get_text(self) -> str:
|
def get_text(self) -> str:
|
||||||
percent = min(100, max(0, int(self.position * 100 / self.max_)))
|
percent = min(100.0, max(0.0, self.position * 100.0 / self.max_))
|
||||||
return str(percent) + " %"
|
ret = f"{percent:.02f}%"
|
||||||
|
if self.time_left and self.show_time:
|
||||||
|
ret += f" ({format_timespan(self.time_left)})"
|
||||||
|
return ret
|
||||||
|
|
||||||
|
# workaround for https://github.com/urwid/urwid/issues/317
|
||||||
def render(self, size: T.Tuple[int, int], focus: bool = False) -> T.Any:
|
def render(self, size: T.Tuple[int, int], focus: bool = False) -> T.Any:
|
||||||
(maxcol,) = size
|
(maxcol,) = size
|
||||||
txt = urwid.Text(self.get_text(), self.text_align, "clip")
|
txt = urwid.Text(self.get_text(), self.text_align, "clip")
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
import urwid
|
||||||
|
|
||||||
|
from pqcli.ui.layout import NColumns, NPile
|
||||||
|
|
||||||
|
|
||||||
|
class DataTable(NPile):
|
||||||
|
def __init__(self, columns: T.Any) -> None:
|
||||||
|
self.column_count = len(columns)
|
||||||
|
self._columns = columns
|
||||||
|
self._rows: T.List[T.List[urwid.Widget]] = []
|
||||||
|
super().__init__([])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def row_count(self) -> int:
|
||||||
|
return len(self._rows)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data_rows(self) -> T.Iterable[T.List[urwid.Widget]]:
|
||||||
|
# "rows" was already used by urwid
|
||||||
|
return self._rows
|
||||||
|
|
||||||
|
def add_row(self, *widgets: T.List[urwid.Widget]) -> None:
|
||||||
|
if len(widgets) != self.column_count:
|
||||||
|
raise VauleError(f"Expected {self.column_count} widgets")
|
||||||
|
|
||||||
|
self.contents.append(
|
||||||
|
(
|
||||||
|
NColumns(
|
||||||
|
list((*x, y) for x, y in zip(self._columns, widgets))
|
||||||
|
),
|
||||||
|
(urwid.PACK, 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.resize_columns()
|
||||||
|
self._rows.append(widgets)
|
||||||
|
|
||||||
|
def delete_row(self, idx: int) -> None:
|
||||||
|
del self.contents[idx]
|
||||||
|
del self._rows[idx]
|
||||||
|
self.resize_columns()
|
||||||
|
|
||||||
|
def delete_rows(self, start_idx: int, end_idx: int) -> None:
|
||||||
|
del self.contents[start_idx:end_idx]
|
||||||
|
del self._rows[start_idx:end_idx]
|
||||||
|
self.resize_columns()
|
||||||
|
|
||||||
|
def resize_columns(self) -> None:
|
||||||
|
if not self._rows:
|
||||||
|
return
|
||||||
|
|
||||||
|
for i, sizing in enumerate(self._columns):
|
||||||
|
if isinstance(sizing, tuple) and sizing[0] == urwid.PACK:
|
||||||
|
width = max(
|
||||||
|
row_widgets[i].pack()[0] for row_widgets in self._rows
|
||||||
|
)
|
||||||
|
for item in self.contents:
|
||||||
|
columns = item[0]
|
||||||
|
columns.contents[i] = (
|
||||||
|
columns.contents[i][0],
|
||||||
|
(urwid.GIVEN, width, None),
|
||||||
|
)
|
||||||
@@ -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,7 +25,7 @@ class DoubleLineBox(urwid.AttrMap):
|
|||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
urwid.Pile([self.top_line_box, ("pack", self.bottom_line_box)]),
|
NPile([self.top_line_box, (urwid.PACK, self.bottom_line_box)]),
|
||||||
"linebox",
|
"linebox",
|
||||||
"linebox-focus",
|
"linebox-focus",
|
||||||
)
|
)
|
||||||
|
|||||||
+134
-151
@@ -15,10 +15,12 @@ from pqcli.mechanic import (
|
|||||||
Spell,
|
Spell,
|
||||||
)
|
)
|
||||||
from pqcli.ui.custom_line_box import CustomLineBox
|
from pqcli.ui.custom_line_box import CustomLineBox
|
||||||
from pqcli.ui.custom_list_box import CustomListBox
|
|
||||||
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.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 Scrollable, ScrollBar
|
||||||
|
|
||||||
|
|
||||||
class CharacterSheetView(DoubleLineBox):
|
class CharacterSheetView(DoubleLineBox):
|
||||||
@@ -27,32 +29,23 @@ class CharacterSheetView(DoubleLineBox):
|
|||||||
|
|
||||||
self.level_text = urwid.Text("")
|
self.level_text = urwid.Text("")
|
||||||
self.stat_texts = {stat: urwid.Text("") for stat in list(StatType)}
|
self.stat_texts = {stat: urwid.Text("") for stat in list(StatType)}
|
||||||
self.exp_bar = CustomProgressBar()
|
self.exp_bar = CustomProgressBar(show_time=True)
|
||||||
|
|
||||||
self.list_box = CustomListBox(
|
data_table = DataTable(columns=[(urwid.WEIGHT, 1), (urwid.WEIGHT, 2)])
|
||||||
[
|
data_table.add_row(urwid.Text("Name"), urwid.Text(player.name))
|
||||||
urwid.Columns([urwid.Text("Name"), urwid.Text(player.name)]),
|
data_table.add_row(urwid.Text("Race"), urwid.Text(player.race.name))
|
||||||
urwid.Columns(
|
data_table.add_row(urwid.Text("Class"), urwid.Text(player.class_.name))
|
||||||
[urwid.Text("Race"), urwid.Text(player.race.name)]
|
data_table.add_row(urwid.Text("Level"), self.level_text)
|
||||||
),
|
data_table.add_row(urwid.Divider(), urwid.Divider())
|
||||||
urwid.Columns(
|
for stat in StatType:
|
||||||
[urwid.Text("Class"), urwid.Text(player.class_.name)]
|
data_table.add_row(urwid.Text(stat.value), self.stat_texts[stat])
|
||||||
),
|
|
||||||
urwid.Columns([urwid.Text("Level"), self.level_text]),
|
|
||||||
urwid.Divider(),
|
|
||||||
]
|
|
||||||
+ [
|
|
||||||
urwid.Columns([urwid.Text(stat.value), self.stat_texts[stat]])
|
|
||||||
for stat in StatType
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.player.connect("level_up", self.sync_level)
|
self.player.connect("level_up", self.sync_level)
|
||||||
self.player.stats.connect("change", self.sync_stats)
|
self.player.stats.connect("change", self.sync_stats)
|
||||||
self.player.exp_bar.connect("change", self.sync_exp)
|
self.player.exp_bar.connect("change", self.sync_exp)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
top_widget=self.list_box,
|
top_widget=ScrollBar(Scrollable(data_table)),
|
||||||
bottom_widget=self.exp_bar,
|
bottom_widget=self.exp_bar,
|
||||||
top_title="Character Sheet",
|
top_title="Character Sheet",
|
||||||
bottom_title="Experience",
|
bottom_title="Experience",
|
||||||
@@ -74,8 +67,7 @@ class CharacterSheetView(DoubleLineBox):
|
|||||||
def sync_exp(self) -> None:
|
def sync_exp(self) -> None:
|
||||||
cur = self.player.exp_bar.position
|
cur = self.player.exp_bar.position
|
||||||
max_ = self.player.exp_bar.max_
|
max_ = self.player.exp_bar.max_
|
||||||
self.exp_bar.set_completion(cur)
|
self.exp_bar.reset(cur, max_)
|
||||||
self.exp_bar.set_max(max_)
|
|
||||||
self.set_bottom_title(f"Experience ({max_-cur:.0f} XP to go)")
|
self.set_bottom_title(f"Experience ({max_-cur:.0f} XP to go)")
|
||||||
|
|
||||||
|
|
||||||
@@ -83,33 +75,31 @@ class SpellBookView(CustomLineBox):
|
|||||||
def __init__(self, player: Player) -> None:
|
def __init__(self, player: Player) -> None:
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
self.list_box = CustomListBox([])
|
self.data_table = DataTable(columns=[(urwid.WEIGHT, 3), (urwid.PACK,)])
|
||||||
|
self.scrollable = Scrollable(self.data_table)
|
||||||
|
|
||||||
self.player.spell_book.connect("add", self.sync_spell_add)
|
self.player.spell_book.connect("add", self.sync_spell_add)
|
||||||
self.player.spell_book.connect("change", self.sync_spell_change)
|
self.player.spell_book.connect("change", self.sync_spell_change)
|
||||||
|
|
||||||
super().__init__(self.list_box, title="Spell Book")
|
super().__init__(ScrollBar(self.scrollable), title="Spell Book")
|
||||||
self.sync()
|
self.sync()
|
||||||
|
|
||||||
def sync(self) -> None:
|
def sync(self) -> None:
|
||||||
del self.list_box.body[:]
|
self.data_table.delete_rows(0, len(self.data_table.data_rows))
|
||||||
for spell in self.player.spell_book:
|
for spell in self.player.spell_book:
|
||||||
self.sync_spell_add(spell)
|
self.sync_spell_add(spell)
|
||||||
|
|
||||||
def sync_spell_add(self, spell: Spell) -> None:
|
def sync_spell_add(self, spell: Spell) -> None:
|
||||||
self.list_box.body.append(
|
self.data_table.add_row(
|
||||||
urwid.Columns(
|
urwid.Text(spell.name), urwid.Text(to_roman(spell.level))
|
||||||
[
|
|
||||||
("weight", 3, urwid.Text(spell.name)),
|
|
||||||
urwid.Text(to_roman(spell.level)),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
self.scrollable.set_scrollpos(len(self.data_table.data_rows) - 1)
|
||||||
|
|
||||||
def sync_spell_change(self, spell: Spell) -> None:
|
def sync_spell_change(self, spell: Spell) -> None:
|
||||||
for widget in self.list_box.body:
|
for row_widgets in self.data_table.data_rows:
|
||||||
if widget.contents[0][0].text == spell.name:
|
if row_widgets[0].text == spell.name:
|
||||||
widget.contents[1][0].set_text(to_roman(spell.level))
|
row_widgets[1].set_text(to_roman(spell.level))
|
||||||
|
self.data_table.resize_columns()
|
||||||
|
|
||||||
|
|
||||||
class EquipmentView(CustomLineBox):
|
class EquipmentView(CustomLineBox):
|
||||||
@@ -119,21 +109,16 @@ class EquipmentView(CustomLineBox):
|
|||||||
self.value_texts = {
|
self.value_texts = {
|
||||||
equipment_type: urwid.Text("") for equipment_type in EquipmentType
|
equipment_type: urwid.Text("") for equipment_type in EquipmentType
|
||||||
}
|
}
|
||||||
self.list_box = CustomListBox(
|
data_table = DataTable(columns=[(urwid.WEIGHT, 1), (urwid.WEIGHT, 3)])
|
||||||
[
|
for equipment_type in EquipmentType:
|
||||||
urwid.Columns(
|
data_table.add_row(
|
||||||
[
|
urwid.Text(equipment_type.value),
|
||||||
urwid.Text(equipment_type.value),
|
self.value_texts[equipment_type],
|
||||||
("weight", 3, self.value_texts[equipment_type]),
|
)
|
||||||
]
|
|
||||||
)
|
|
||||||
for equipment_type in EquipmentType
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.player.equipment.connect("change", self.sync_equipment_change)
|
self.player.equipment.connect("change", self.sync_equipment_change)
|
||||||
|
|
||||||
super().__init__(self.list_box, title="Equipment")
|
super().__init__(ScrollBar(Scrollable(data_table)), title="Equipment")
|
||||||
self.sync()
|
self.sync()
|
||||||
|
|
||||||
def sync(self) -> None:
|
def sync(self) -> None:
|
||||||
@@ -152,15 +137,11 @@ class InventoryView(DoubleLineBox):
|
|||||||
def __init__(self, player: Player) -> None:
|
def __init__(self, player: Player) -> None:
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
self.gold_text = urwid.Text("")
|
self.gold_text = urwid.Text("", align=urwid.RIGHT)
|
||||||
self.list_box = CustomListBox(
|
self.data_table = DataTable(columns=[(urwid.WEIGHT, 3), (urwid.PACK,)])
|
||||||
[
|
self.data_table.add_row(urwid.Text("Gold"), self.gold_text)
|
||||||
urwid.Columns(
|
self.scrollable = Scrollable(self.data_table)
|
||||||
[("weight", 3, urwid.Text("Gold")), self.gold_text]
|
self.encumbrance_bar = CustomProgressBar(show_time=False)
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
self.encumbrance_bar = CustomProgressBar()
|
|
||||||
|
|
||||||
self.player.connect("new_task", self.on_new_task)
|
self.player.connect("new_task", self.on_new_task)
|
||||||
self.player.inventory.connect("gold_change", self.sync_gold)
|
self.player.inventory.connect("gold_change", self.sync_gold)
|
||||||
@@ -172,7 +153,7 @@ class InventoryView(DoubleLineBox):
|
|||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
top_widget=self.list_box,
|
top_widget=ScrollBar(self.scrollable),
|
||||||
top_title="Inventory",
|
top_title="Inventory",
|
||||||
bottom_widget=self.encumbrance_bar,
|
bottom_widget=self.encumbrance_bar,
|
||||||
bottom_title="Encumbrance",
|
bottom_title="Encumbrance",
|
||||||
@@ -188,47 +169,43 @@ class InventoryView(DoubleLineBox):
|
|||||||
self.gold_text.set_text(str(self.player.inventory.gold))
|
self.gold_text.set_text(str(self.player.inventory.gold))
|
||||||
|
|
||||||
def sync_items(self) -> None:
|
def sync_items(self) -> None:
|
||||||
del self.list_box.body[1:]
|
self.data_table.delete_rows(1, len(self.data_table.data_rows) - 1)
|
||||||
for item in self.player.inventory:
|
for item in self.player.inventory:
|
||||||
self.sync_item_add(item)
|
self.sync_item_add(item)
|
||||||
|
|
||||||
def sync_encumbrance_position(self) -> None:
|
def sync_encumbrance_position(self) -> None:
|
||||||
cur = self.player.inventory.encum_bar.position
|
cur = self.player.inventory.encum_bar.position
|
||||||
max_ = self.player.inventory.encum_bar.max_
|
max_ = self.player.inventory.encum_bar.max_
|
||||||
self.encumbrance_bar.set_completion(cur)
|
self.encumbrance_bar.reset(cur, max_)
|
||||||
self.encumbrance_bar.set_max(max_)
|
|
||||||
self.set_bottom_title(f"Encumbrance ({cur:.0f}/{max_} cubits)")
|
self.set_bottom_title(f"Encumbrance ({cur:.0f}/{max_} cubits)")
|
||||||
|
|
||||||
def sync_item_add(self, item: InventoryItem) -> None:
|
def sync_item_add(self, item: InventoryItem) -> None:
|
||||||
self.list_box.body.append(
|
self.data_table.add_row(
|
||||||
urwid.Columns(
|
urwid.Text(item.name),
|
||||||
[
|
urwid.Text(str(item.quantity), align=urwid.RIGHT),
|
||||||
("weight", 3, urwid.Text(item.name)),
|
|
||||||
urwid.Text(str(item.quantity)),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
self.scrollable.set_scrollpos(self.scrollable.rows_max() - 1)
|
||||||
|
|
||||||
def sync_item_del(self, item: InventoryItem) -> None:
|
def sync_item_del(self, item: InventoryItem) -> None:
|
||||||
idx = self.get_item_idx_by_name(item.name)
|
idx = self.get_item_idx_by_name(item.name)
|
||||||
if idx is not None:
|
if idx is not None:
|
||||||
del self.list_box.body[idx]
|
self.data_table.delete_row(idx)
|
||||||
|
|
||||||
def sync_item_change(self, item: InventoryItem) -> None:
|
def sync_item_change(self, item: InventoryItem) -> None:
|
||||||
idx = self.get_item_idx_by_name(item.name)
|
idx = self.get_item_idx_by_name(item.name)
|
||||||
if idx is not None:
|
if idx is not None:
|
||||||
self.list_box.body[idx].contents[1][0].set_text(str(item.quantity))
|
self.data_table.data_rows[idx][1].set_text(str(item.quantity))
|
||||||
|
|
||||||
def get_item_idx_by_name(self, name: str) -> T.Optional[int]:
|
def get_item_idx_by_name(self, name: str) -> T.Optional[int]:
|
||||||
for i, column_widget in enumerate(self.list_box.body):
|
for i, row_widgets in enumerate(self.data_table.data_rows):
|
||||||
if column_widget.contents[0][0].text == name:
|
if row_widgets[0].text == name:
|
||||||
return i
|
return i
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def on_new_task(self) -> None:
|
def on_new_task(self) -> None:
|
||||||
description = self.player.task.description if self.player.task else "?"
|
description = self.player.task.description if self.player.task else "?"
|
||||||
if description.lower().startswith("sell"):
|
if description.lower().startswith("sell"):
|
||||||
self.list_box.set_focus(0)
|
self.scrollable.set_scrollpos(0)
|
||||||
|
|
||||||
|
|
||||||
class PlotView(DoubleLineBox):
|
class PlotView(DoubleLineBox):
|
||||||
@@ -237,14 +214,15 @@ class PlotView(DoubleLineBox):
|
|||||||
def __init__(self, player: Player) -> None:
|
def __init__(self, player: Player) -> None:
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
self.list_box = CustomListBox([])
|
self.data_table = DataTable(columns=[(urwid.WEIGHT, 1)])
|
||||||
self.plot_bar = CustomProgressBar()
|
self.scrollable = Scrollable(self.data_table)
|
||||||
|
self.plot_bar = CustomProgressBar(show_time=True)
|
||||||
|
|
||||||
self.player.quest_book.connect("complete_act", self.sync_act_add)
|
self.player.quest_book.connect("start_act", self.sync_act_add)
|
||||||
self.player.quest_book.plot_bar.connect("change", self.sync_position)
|
self.player.quest_book.plot_bar.connect("change", self.sync_position)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
top_widget=self.list_box,
|
top_widget=ScrollBar(self.scrollable),
|
||||||
top_title="Plot Development",
|
top_title="Plot Development",
|
||||||
bottom_widget=self.plot_bar,
|
bottom_widget=self.plot_bar,
|
||||||
)
|
)
|
||||||
@@ -255,29 +233,29 @@ class PlotView(DoubleLineBox):
|
|||||||
self.sync_position()
|
self.sync_position()
|
||||||
|
|
||||||
def sync_acts(self) -> None:
|
def sync_acts(self) -> None:
|
||||||
del self.list_box.body[:]
|
self.data_table.delete_rows(0, len(self.data_table.data_rows))
|
||||||
for act_number in range(
|
for act_number in range(
|
||||||
max(0, self.player.quest_book.act - self.cutoff),
|
max(0, self.player.quest_book.act - self.cutoff),
|
||||||
self.player.quest_book.act,
|
self.player.quest_book.act + 1,
|
||||||
):
|
):
|
||||||
self.list_box.body.append(
|
self.sync_act_add(act_number)
|
||||||
ReadOnlyCheckBox(act_name(act_number), state=True)
|
|
||||||
)
|
|
||||||
self.list_box.body.append(
|
|
||||||
ReadOnlyCheckBox(act_name(self.player.quest_book.act), state=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
def sync_act_add(self) -> None:
|
def sync_act_add(self, act_number: int) -> None:
|
||||||
del self.list_box.body[: -self.cutoff]
|
self.data_table.delete_rows(
|
||||||
if self.list_box.body:
|
0, max(0, self.data_table.row_count - self.cutoff)
|
||||||
self.list_box.body[-1].set_state(True)
|
|
||||||
self.list_box.body.append(
|
|
||||||
ReadOnlyCheckBox(act_name(self.player.quest_book.act), state=False)
|
|
||||||
)
|
)
|
||||||
|
if self.data_table.data_rows:
|
||||||
|
self.data_table.data_rows[-1][0].set_state(True)
|
||||||
|
self.data_table.add_row(
|
||||||
|
ReadOnlyCheckBox(act_name(act_number), state=False)
|
||||||
|
)
|
||||||
|
self.scrollable.set_scrollpos(self.scrollable.rows_max() - 1)
|
||||||
|
|
||||||
def sync_position(self) -> None:
|
def sync_position(self) -> None:
|
||||||
self.plot_bar.set_completion(self.player.quest_book.plot_bar.position)
|
self.plot_bar.reset(
|
||||||
self.plot_bar.set_max(self.player.quest_book.plot_bar.max_)
|
self.player.quest_book.plot_bar.position,
|
||||||
|
self.player.quest_book.plot_bar.max_,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class QuestBookView(DoubleLineBox):
|
class QuestBookView(DoubleLineBox):
|
||||||
@@ -286,14 +264,15 @@ class QuestBookView(DoubleLineBox):
|
|||||||
def __init__(self, player: Player) -> None:
|
def __init__(self, player: Player) -> None:
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
self.list_box = CustomListBox([])
|
self.data_table = DataTable(columns=[(urwid.WEIGHT, 1)])
|
||||||
self.quest_bar = CustomProgressBar()
|
self.quest_bar = CustomProgressBar(show_time=True)
|
||||||
|
self.scrollable = Scrollable(self.data_table)
|
||||||
|
|
||||||
self.player.quest_book.connect("complete_quest", self.sync_quest_add)
|
self.player.quest_book.connect("start_quest", self.sync_quest_add)
|
||||||
self.player.quest_book.quest_bar.connect("change", self.sync_position)
|
self.player.quest_book.quest_bar.connect("change", self.sync_position)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
top_widget=self.list_box,
|
top_widget=ScrollBar(self.scrollable),
|
||||||
top_title="Quests",
|
top_title="Quests",
|
||||||
bottom_widget=self.quest_bar,
|
bottom_widget=self.quest_bar,
|
||||||
)
|
)
|
||||||
@@ -304,33 +283,31 @@ class QuestBookView(DoubleLineBox):
|
|||||||
self.sync_position()
|
self.sync_position()
|
||||||
|
|
||||||
def sync_quests(self) -> None:
|
def sync_quests(self) -> None:
|
||||||
del self.list_box.body[:]
|
|
||||||
for quest_name in self.player.quest_book.quests[-self.cutoff :]:
|
for quest_name in self.player.quest_book.quests[-self.cutoff :]:
|
||||||
self.list_box.body.append(ReadOnlyCheckBox(quest_name, state=True))
|
self.sync_quest_add(quest_name)
|
||||||
if self.list_box.body:
|
|
||||||
self.list_box.body[-1].set_state(False)
|
|
||||||
|
|
||||||
def sync_quest_add(self) -> None:
|
def sync_quest_add(self, quest_name: str) -> None:
|
||||||
del self.list_box.body[: -self.cutoff]
|
self.data_table.delete_rows(
|
||||||
if self.list_box.body:
|
0, max(0, self.data_table.row_count - self.cutoff)
|
||||||
self.list_box.body[-1].set_state(True)
|
|
||||||
self.list_box.body.append(
|
|
||||||
ReadOnlyCheckBox(self.player.quest_book.quests[-1], state=False)
|
|
||||||
)
|
)
|
||||||
|
if self.data_table.data_rows:
|
||||||
|
self.data_table.data_rows[-1][0].set_state(True)
|
||||||
|
self.data_table.add_row(ReadOnlyCheckBox(quest_name, state=False))
|
||||||
|
self.scrollable.set_scrollpos(self.scrollable.rows_max() - 1)
|
||||||
|
|
||||||
def sync_position(self) -> None:
|
def sync_position(self) -> None:
|
||||||
self.quest_bar.set_completion(
|
self.quest_bar.reset(
|
||||||
self.player.quest_book.quest_bar.position
|
self.player.quest_book.quest_bar.position,
|
||||||
|
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
|
||||||
|
|
||||||
self.current_task_text = urwid.Text("...")
|
self.current_task_text = urwid.Text("...")
|
||||||
self.current_task_bar = CustomProgressBar()
|
self.current_task_bar = CustomProgressBar(show_time=False)
|
||||||
|
|
||||||
self.player.connect("new_task", self.sync_task_name)
|
self.player.connect("new_task", self.sync_task_name)
|
||||||
self.player.task_bar.connect("change", self.sync_position)
|
self.player.task_bar.connect("change", self.sync_position)
|
||||||
@@ -348,14 +325,15 @@ class TaskView(urwid.Pile):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def sync_position(self) -> None:
|
def sync_position(self) -> None:
|
||||||
self.current_task_bar.set_completion(self.player.task_bar.position)
|
self.current_task_bar.reset(
|
||||||
self.current_task_bar.set_max(self.player.task_bar.max_)
|
self.player.task_bar.position, self.player.task_bar.max_
|
||||||
|
)
|
||||||
|
|
||||||
def pack(self, size: T.Tuple[int, int]) -> T.Tuple[int, int]:
|
def pack(self, size: T.Tuple[int, int]) -> T.Tuple[int, int]:
|
||||||
return (size[0], 2)
|
return (size[0], 2)
|
||||||
|
|
||||||
|
|
||||||
class GameView(urwid.Pile):
|
class GameView(NPile):
|
||||||
signals = ["cancel"]
|
signals = ["cancel"]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -375,43 +353,32 @@ 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)
|
||||||
|
|
||||||
super().__init__(
|
self.columns = NColumns(
|
||||||
[
|
[
|
||||||
urwid.Columns(
|
(
|
||||||
[
|
urwid.WEIGHT,
|
||||||
(
|
4,
|
||||||
"weight",
|
NPile(
|
||||||
1,
|
[(19, self.character_sheet_view), self.spell_book_view]
|
||||||
urwid.Pile(
|
),
|
||||||
[
|
),
|
||||||
(19, self.character_sheet_view),
|
(
|
||||||
self.spell_book_view,
|
urwid.WEIGHT,
|
||||||
]
|
5,
|
||||||
),
|
NPile([(15, self.equipment_view), self.inventory_view]),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"weight",
|
urwid.WEIGHT,
|
||||||
2,
|
4,
|
||||||
urwid.Pile(
|
NPile([(15, self.plot_view), self.quest_book_view]),
|
||||||
[
|
|
||||||
(15, self.equipment_view),
|
|
||||||
self.inventory_view,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"weight",
|
|
||||||
2,
|
|
||||||
urwid.Pile(
|
|
||||||
[(15, self.plot_view), self.quest_book_view]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
),
|
||||||
("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")
|
||||||
|
|
||||||
@@ -421,15 +388,31 @@ class GameView(urwid.Pile):
|
|||||||
self.simulation.tick(elapsed * 1000)
|
self.simulation.tick(elapsed * 1000)
|
||||||
self.last_tick = datetime.datetime.now()
|
self.last_tick = datetime.datetime.now()
|
||||||
|
|
||||||
def unhandled_input(self, key: str) -> bool:
|
def keypress(self, size: T.Tuple[int, int], key: str) -> bool:
|
||||||
if key == "esc":
|
if key == "esc":
|
||||||
self.cancel()
|
self.cancel()
|
||||||
return True
|
return None
|
||||||
|
|
||||||
|
if self._command_map[key] == urwid.CURSOR_RIGHT:
|
||||||
|
old_focus_position = self.columns.focus.focus_position
|
||||||
|
self.columns.focus_position = min(
|
||||||
|
self.columns.focus_position + 1, len(self.columns.contents) - 1
|
||||||
|
)
|
||||||
|
self.columns.focus.focus_position = old_focus_position
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._command_map[key] == urwid.CURSOR_LEFT:
|
||||||
|
old_focus_position = self.columns.focus.focus_position
|
||||||
|
self.columns.focus_position = max(
|
||||||
|
self.columns.focus_position - 1, 0
|
||||||
|
)
|
||||||
|
self.columns.focus.focus_position = old_focus_position
|
||||||
|
return None
|
||||||
|
|
||||||
if self.args.cheats:
|
if self.args.cheats:
|
||||||
if key == "e":
|
if key == "e":
|
||||||
self.simulation.player.exp_bar.increment(1)
|
self.simulation.player.exp_bar.increment(1)
|
||||||
return True
|
return None
|
||||||
|
|
||||||
fast_forward = {"t": 1, "T": 100, "ctrl t": 10000}
|
fast_forward = {"t": 1, "T": 100, "ctrl t": 10000}
|
||||||
if key in fast_forward:
|
if key in fast_forward:
|
||||||
@@ -444,9 +427,9 @@ class GameView(urwid.Pile):
|
|||||||
for _ in range(iterations):
|
for _ in range(iterations):
|
||||||
self.simulation.tick()
|
self.simulation.tick()
|
||||||
|
|
||||||
return True
|
return None
|
||||||
|
|
||||||
return False
|
return super().keypress(size, key)
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def suppress_updates(self) -> T.Generator:
|
def suppress_updates(self) -> T.Generator:
|
||||||
|
|||||||
@@ -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
|
||||||
+20
-18
@@ -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(
|
||||||
[
|
[
|
||||||
("weight", 2, self.race_box),
|
(urwid.WEIGHT, 2, self.race_box),
|
||||||
("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):
|
||||||
@@ -13,7 +14,7 @@ class RosterView(urwid.Filler):
|
|||||||
def __init__(self, roster: Roster) -> None:
|
def __init__(self, roster: Roster) -> None:
|
||||||
self.roster = roster
|
self.roster = roster
|
||||||
|
|
||||||
logo = urwid.BigText("ProgressQuest", urwid.HalfBlock5x4Font())
|
logo = urwid.BigText("Progress Quest", urwid.HalfBlock5x4Font())
|
||||||
|
|
||||||
buttons = []
|
buttons = []
|
||||||
for player_idx, player in enumerate(self.roster.players):
|
for player_idx, player in enumerate(self.roster.players):
|
||||||
@@ -28,7 +29,8 @@ class RosterView(urwid.Filler):
|
|||||||
best_spell_name = "-"
|
best_spell_name = "-"
|
||||||
|
|
||||||
best_stat_name = (
|
best_stat_name = (
|
||||||
f"{player.stats.best.value} {player.stats[player.stats.best]}"
|
f"{player.stats.best_prime.value} "
|
||||||
|
f"{player.stats[player.stats.best_prime]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
label = (
|
label = (
|
||||||
@@ -39,7 +41,7 @@ class RosterView(urwid.Filler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
buttons.append(
|
buttons.append(
|
||||||
urwid.Columns(
|
NColumns(
|
||||||
[
|
[
|
||||||
CustomButton(
|
CustomButton(
|
||||||
label=label,
|
label=label,
|
||||||
@@ -73,13 +75,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],
|
||||||
|
|||||||
@@ -0,0 +1,498 @@
|
|||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU 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 General Public License for more details
|
||||||
|
# http://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
# Taken from stig (stig.tui.scroll).
|
||||||
|
# Changes compared to upstream:
|
||||||
|
# - reformatted the code
|
||||||
|
# - added AttrMap wrapping
|
||||||
|
# - added padding support
|
||||||
|
# - removed up/down arrow navigation (for widget navigation)
|
||||||
|
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
import urwid
|
||||||
|
from urwid.widget import BOX, FIXED, FLOW
|
||||||
|
|
||||||
|
# Scroll actions
|
||||||
|
SCROLL_LINE_UP = "line up"
|
||||||
|
SCROLL_LINE_DOWN = "line down"
|
||||||
|
SCROLL_PAGE_UP = "page up"
|
||||||
|
SCROLL_PAGE_DOWN = "page down"
|
||||||
|
SCROLL_TO_TOP = "to top"
|
||||||
|
SCROLL_TO_END = "to end"
|
||||||
|
|
||||||
|
# Scrollbar positions
|
||||||
|
SCROLLBAR_LEFT = "left"
|
||||||
|
SCROLLBAR_RIGHT = "right"
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyCanvas(urwid.Canvas):
|
||||||
|
def __init__(self, cols: int, rows: int) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._text, cs = urwid.apply_target_encoding(" ")
|
||||||
|
self._cs = cs[0][0]
|
||||||
|
self.size = cols, rows
|
||||||
|
self.cursor = None
|
||||||
|
|
||||||
|
def cols(self) -> int:
|
||||||
|
return self.size[0]
|
||||||
|
|
||||||
|
def rows(self) -> int:
|
||||||
|
return self.size[1]
|
||||||
|
|
||||||
|
def content(
|
||||||
|
self,
|
||||||
|
trim_left: int = 0,
|
||||||
|
trim_top: int = 0,
|
||||||
|
cols: T.Optional[int] = None,
|
||||||
|
rows: T.Optional[int] = None,
|
||||||
|
attr: T.Optional[str] = None,
|
||||||
|
) -> T.Generator:
|
||||||
|
if cols is None:
|
||||||
|
cols = self.size[0]
|
||||||
|
if rows is None:
|
||||||
|
rows = self.size[1]
|
||||||
|
|
||||||
|
line = [(None, self._cs, self._text * cols)]
|
||||||
|
for i in range(rows):
|
||||||
|
yield line
|
||||||
|
|
||||||
|
def content_delta(self, other: T.Any) -> T.Any:
|
||||||
|
if other is self:
|
||||||
|
return [self.cols()] * self.rows()
|
||||||
|
return self.content()
|
||||||
|
|
||||||
|
|
||||||
|
class _Scrollable(urwid.WidgetDecoration):
|
||||||
|
def __init__(self, widget):
|
||||||
|
"""Box widget that makes a fixed or flow widget vertically scrollable
|
||||||
|
|
||||||
|
TODO: Focusable widgets are handled, including switching focus, but
|
||||||
|
possibly not intuitively, depending on the arrangement of widgets.
|
||||||
|
When switching focus to a widget that is ouside of the visible part of
|
||||||
|
the original widget, the canvas scrolls up/down to the focused widget.
|
||||||
|
It would be better to scroll until the next focusable widget is in
|
||||||
|
sight first. But for that to work we must somehow obtain a list of
|
||||||
|
focusable rows in the original canvas.
|
||||||
|
"""
|
||||||
|
if not any(s in widget.sizing() for s in (FIXED, FLOW)):
|
||||||
|
raise ValueError("Not a fixed or flow widget: %r" % widget)
|
||||||
|
self._trim_top = 0
|
||||||
|
self._scroll_action = None
|
||||||
|
self._forward_keypress = None
|
||||||
|
self._old_cursor_coords = None
|
||||||
|
self._rows_max_cached = 0
|
||||||
|
super().__init__(widget)
|
||||||
|
|
||||||
|
def sizing(self):
|
||||||
|
return frozenset([BOX])
|
||||||
|
|
||||||
|
def selectable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def render(self, size, focus=False):
|
||||||
|
maxcol, maxrow = size
|
||||||
|
|
||||||
|
# Render complete original widget
|
||||||
|
ow = self._original_widget
|
||||||
|
ow_size = self._get_original_widget_size(size)
|
||||||
|
canv_full = ow.render(ow_size, focus)
|
||||||
|
|
||||||
|
# Make full canvas editable
|
||||||
|
canv = urwid.CompositeCanvas(canv_full)
|
||||||
|
canv_cols, canv_rows = canv.cols(), canv.rows()
|
||||||
|
|
||||||
|
if canv_cols <= maxcol:
|
||||||
|
pad_width = maxcol - canv_cols
|
||||||
|
if pad_width > 0:
|
||||||
|
# Canvas is narrower than available horizontal space
|
||||||
|
canv.pad_trim_left_right(0, pad_width)
|
||||||
|
|
||||||
|
if canv_rows <= maxrow:
|
||||||
|
fill_height = maxrow - canv_rows
|
||||||
|
if fill_height > 0:
|
||||||
|
# Canvas is lower than available vertical space
|
||||||
|
canv.pad_trim_top_bottom(0, fill_height)
|
||||||
|
|
||||||
|
if canv_cols <= maxcol and canv_rows <= maxrow:
|
||||||
|
# Canvas is small enough to fit without trimming
|
||||||
|
return canv
|
||||||
|
|
||||||
|
self._adjust_trim_top(canv, size)
|
||||||
|
|
||||||
|
# Trim canvas if necessary
|
||||||
|
trim_top = self._trim_top
|
||||||
|
trim_end = canv_rows - maxrow - trim_top
|
||||||
|
trim_right = canv_cols - maxcol
|
||||||
|
if trim_top > 0:
|
||||||
|
canv.trim(trim_top)
|
||||||
|
if trim_end > 0:
|
||||||
|
canv.trim_end(trim_end)
|
||||||
|
if trim_right > 0:
|
||||||
|
canv.pad_trim_left_right(0, -trim_right)
|
||||||
|
|
||||||
|
# Disable cursor display if cursor is outside of visible canvas parts
|
||||||
|
if canv.cursor is not None:
|
||||||
|
curscol, cursrow = canv.cursor
|
||||||
|
if cursrow >= maxrow or cursrow < 0:
|
||||||
|
canv.cursor = None
|
||||||
|
|
||||||
|
# Figure out whether we should forward keypresses to original widget
|
||||||
|
if canv.cursor is not None:
|
||||||
|
# Trimmed canvas contains the cursor, e.g. in an Edit widget
|
||||||
|
self._forward_keypress = True
|
||||||
|
else:
|
||||||
|
if canv_full.cursor is not None:
|
||||||
|
# Full canvas contains the cursor, but scrolled out of view
|
||||||
|
self._forward_keypress = False
|
||||||
|
else:
|
||||||
|
# Original widget does not have a cursor, but may be selectable
|
||||||
|
|
||||||
|
# FIXME: Using ow.selectable() is bad because the original
|
||||||
|
# widget may be selectable because it's a container widget with
|
||||||
|
# a key-grabbing widget that is scrolled out of view.
|
||||||
|
# ow.selectable() returns True anyway because it doesn't know
|
||||||
|
# how we trimmed our canvas.
|
||||||
|
#
|
||||||
|
# To fix this, we need to resolve ow.focus and somehow
|
||||||
|
# ask canv whether it contains bits of the focused widget.
|
||||||
|
# I can't see a way to do that.
|
||||||
|
if ow.selectable():
|
||||||
|
self._forward_keypress = True
|
||||||
|
else:
|
||||||
|
self._forward_keypress = False
|
||||||
|
|
||||||
|
return canv
|
||||||
|
|
||||||
|
def keypress(self, size, key):
|
||||||
|
# Maybe offer key to original widget
|
||||||
|
if self._forward_keypress:
|
||||||
|
ow = self._original_widget
|
||||||
|
ow_size = self._get_original_widget_size(size)
|
||||||
|
|
||||||
|
# Remember previous cursor position if possible
|
||||||
|
if hasattr(ow, "get_cursor_coords"):
|
||||||
|
self._old_cursor_coords = ow.get_cursor_coords(ow_size)
|
||||||
|
|
||||||
|
key = ow.keypress(ow_size, key)
|
||||||
|
if key is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Handle up/down, page up/down, etc
|
||||||
|
command_map = self._command_map
|
||||||
|
if command_map[key] == urwid.CURSOR_PAGE_UP:
|
||||||
|
self._scroll_action = SCROLL_PAGE_UP
|
||||||
|
elif command_map[key] == urwid.CURSOR_PAGE_DOWN:
|
||||||
|
self._scroll_action = SCROLL_PAGE_DOWN
|
||||||
|
|
||||||
|
elif command_map[key] == urwid.CURSOR_MAX_LEFT: # 'home'
|
||||||
|
self._scroll_action = SCROLL_TO_TOP
|
||||||
|
elif command_map[key] == urwid.CURSOR_MAX_RIGHT: # 'end'
|
||||||
|
self._scroll_action = SCROLL_TO_END
|
||||||
|
|
||||||
|
else:
|
||||||
|
return key
|
||||||
|
|
||||||
|
self._invalidate()
|
||||||
|
|
||||||
|
def mouse_event(self, size, event, button, col, row, focus):
|
||||||
|
ow = self._original_widget
|
||||||
|
if hasattr(ow, "mouse_event"):
|
||||||
|
ow_size = self._get_original_widget_size(size)
|
||||||
|
row += self._trim_top
|
||||||
|
return ow.mouse_event(ow_size, event, button, col, row, focus)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _adjust_trim_top(self, canv, size):
|
||||||
|
"""Adjust self._trim_top according to self._scroll_action"""
|
||||||
|
action = self._scroll_action
|
||||||
|
self._scroll_action = None
|
||||||
|
|
||||||
|
maxcol, maxrow = size
|
||||||
|
trim_top = self._trim_top
|
||||||
|
canv_rows = canv.rows()
|
||||||
|
|
||||||
|
if trim_top < 0:
|
||||||
|
# Negative trim_top values use bottom of canvas as reference
|
||||||
|
trim_top = canv_rows - maxrow + trim_top + 1
|
||||||
|
|
||||||
|
if canv_rows <= maxrow:
|
||||||
|
self._trim_top = 0 # Reset scroll position
|
||||||
|
return
|
||||||
|
|
||||||
|
def ensure_bounds(new_trim_top):
|
||||||
|
return max(0, min(canv_rows - maxrow, new_trim_top))
|
||||||
|
|
||||||
|
if action == SCROLL_LINE_UP:
|
||||||
|
self._trim_top = ensure_bounds(trim_top - 1)
|
||||||
|
elif action == SCROLL_LINE_DOWN:
|
||||||
|
self._trim_top = ensure_bounds(trim_top + 1)
|
||||||
|
|
||||||
|
elif action == SCROLL_PAGE_UP:
|
||||||
|
self._trim_top = ensure_bounds(trim_top - maxrow + 1)
|
||||||
|
elif action == SCROLL_PAGE_DOWN:
|
||||||
|
self._trim_top = ensure_bounds(trim_top + maxrow - 1)
|
||||||
|
|
||||||
|
elif action == SCROLL_TO_TOP:
|
||||||
|
self._trim_top = 0
|
||||||
|
elif action == SCROLL_TO_END:
|
||||||
|
self._trim_top = canv_rows - maxrow
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._trim_top = ensure_bounds(trim_top)
|
||||||
|
|
||||||
|
# If the cursor was moved by the most recent keypress, adjust trim_top
|
||||||
|
# so that the new cursor position is within the displayed canvas part.
|
||||||
|
# But don't do this if the cursor is at the top/bottom edge so we can still scroll out
|
||||||
|
if (
|
||||||
|
self._old_cursor_coords is not None
|
||||||
|
and self._old_cursor_coords != canv.cursor
|
||||||
|
):
|
||||||
|
self._old_cursor_coords = None
|
||||||
|
curscol, cursrow = canv.cursor
|
||||||
|
if cursrow < self._trim_top:
|
||||||
|
self._trim_top = cursrow
|
||||||
|
elif cursrow >= self._trim_top + maxrow:
|
||||||
|
self._trim_top = max(0, cursrow - maxrow + 1)
|
||||||
|
|
||||||
|
def _get_original_widget_size(self, size):
|
||||||
|
ow = self._original_widget
|
||||||
|
sizing = ow.sizing()
|
||||||
|
if FIXED in sizing:
|
||||||
|
return ()
|
||||||
|
elif FLOW in sizing:
|
||||||
|
return (size[0],)
|
||||||
|
|
||||||
|
def get_scrollpos(self, size=None, focus=False):
|
||||||
|
"""Current scrolling position
|
||||||
|
|
||||||
|
Lower limit is 0, upper limit is the maximum number of rows with the
|
||||||
|
given maxcol minus maxrow.
|
||||||
|
|
||||||
|
NOTE: The returned value may be too low or too high if the position has
|
||||||
|
changed but the widget wasn't rendered yet.
|
||||||
|
"""
|
||||||
|
return self._trim_top
|
||||||
|
|
||||||
|
def set_scrollpos(self, position):
|
||||||
|
"""Set scrolling position
|
||||||
|
|
||||||
|
If `position` is positive it is interpreted as lines from the top.
|
||||||
|
If `position` is negative it is interpreted as lines from the bottom.
|
||||||
|
|
||||||
|
Values that are too high or too low values are automatically adjusted
|
||||||
|
during rendering.
|
||||||
|
"""
|
||||||
|
self._trim_top = int(position)
|
||||||
|
self._invalidate()
|
||||||
|
|
||||||
|
def rows_max(self, size=None, focus=False):
|
||||||
|
"""Return the number of rows for `size`
|
||||||
|
|
||||||
|
If `size` is not given, the currently rendered number of rows is returned.
|
||||||
|
"""
|
||||||
|
if size is not None:
|
||||||
|
ow = self._original_widget
|
||||||
|
ow_size = self._get_original_widget_size(size)
|
||||||
|
sizing = ow.sizing()
|
||||||
|
if FIXED in sizing:
|
||||||
|
self._rows_max_cached = ow.pack(ow_size, focus)[1]
|
||||||
|
elif FLOW in sizing:
|
||||||
|
self._rows_max_cached = ow.rows(ow_size, focus)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Not a flow/box widget: %r" % self._original_widget
|
||||||
|
)
|
||||||
|
return self._rows_max_cached
|
||||||
|
|
||||||
|
|
||||||
|
class _ScrollBar(urwid.WidgetDecoration):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
widget,
|
||||||
|
thumb_char="\N{FULL BLOCK}",
|
||||||
|
trough_char=" ",
|
||||||
|
side=SCROLLBAR_RIGHT,
|
||||||
|
width=1,
|
||||||
|
padding=0,
|
||||||
|
):
|
||||||
|
"""Box widget that adds a scrollbar to `widget`
|
||||||
|
|
||||||
|
`widget` must be a box widget with the following methods:
|
||||||
|
- `get_scrollpos` takes the arguments `size` and `focus` and returns
|
||||||
|
the index of the first visible row.
|
||||||
|
- `set_scrollpos` (optional; needed for mouse click support) takes the
|
||||||
|
index of the first visible row.
|
||||||
|
- `rows_max` takes `size` and `focus` and returns the total number of
|
||||||
|
rows `widget` can render.
|
||||||
|
|
||||||
|
`thumb_char` is the character used for the scrollbar handle.
|
||||||
|
`trough_char` is used for the space above and below the handle.
|
||||||
|
`side` must be 'left' or 'right'.
|
||||||
|
`width` specifies the number of columns the scrollbar uses.
|
||||||
|
`padding` specifies the number of columns the scrollbar is separated
|
||||||
|
from the content by.
|
||||||
|
"""
|
||||||
|
if BOX not in widget.sizing():
|
||||||
|
raise ValueError("Not a box widget: %r" % widget)
|
||||||
|
super().__init__(widget)
|
||||||
|
self._thumb_char = thumb_char
|
||||||
|
self._trough_char = trough_char
|
||||||
|
self._scrollbar_side = side
|
||||||
|
self._scrollbar_width = max(1, width)
|
||||||
|
self._padding_width = padding
|
||||||
|
self._original_widget_size = (0, 0)
|
||||||
|
|
||||||
|
def sizing(self):
|
||||||
|
return frozenset((BOX,))
|
||||||
|
|
||||||
|
def selectable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def render(self, size, focus=False):
|
||||||
|
maxcol, maxrow = size
|
||||||
|
sb_width = self._scrollbar_width
|
||||||
|
pad_width = self._padding_width
|
||||||
|
self._original_widget_size = ow_size = (
|
||||||
|
maxcol - sb_width - pad_width,
|
||||||
|
maxrow,
|
||||||
|
)
|
||||||
|
|
||||||
|
ow = self._original_widget
|
||||||
|
ow_base = self.scrolling_base_widget
|
||||||
|
ow_rows_max = ow_base.rows_max(size, focus)
|
||||||
|
if ow_rows_max <= maxrow:
|
||||||
|
# Canvas fits without scrolling - no scrollbar needed
|
||||||
|
self._original_widget_size = size
|
||||||
|
return ow.render(size, focus)
|
||||||
|
ow_rows_max = ow_base.rows_max(ow_size, focus)
|
||||||
|
|
||||||
|
ow_canv = ow.render(ow_size, focus)
|
||||||
|
|
||||||
|
pos = ow_base.get_scrollpos(ow_size, focus)
|
||||||
|
posmax = ow_rows_max - maxrow
|
||||||
|
|
||||||
|
# Thumb shrinks/grows according to the ratio of
|
||||||
|
# <number of visible lines> / <number of total lines>
|
||||||
|
thumb_weight = min(1, maxrow / max(1, ow_rows_max))
|
||||||
|
thumb_height = max(1, round(thumb_weight * maxrow))
|
||||||
|
|
||||||
|
# Thumb may only touch top/bottom if the first/last row is visible
|
||||||
|
top_weight = float(pos) / max(1, posmax)
|
||||||
|
top_height = int((maxrow - thumb_height) * top_weight)
|
||||||
|
if top_height == 0 and top_weight > 0:
|
||||||
|
top_height = 1
|
||||||
|
|
||||||
|
# Bottom part is remaining space
|
||||||
|
bottom_height = maxrow - thumb_height - top_height
|
||||||
|
assert thumb_height + top_height + bottom_height == maxrow
|
||||||
|
|
||||||
|
# Create scrollbar canvas
|
||||||
|
top = urwid.SolidCanvas(self._trough_char, sb_width, 1)
|
||||||
|
thumb = urwid.SolidCanvas(self._thumb_char, sb_width, 1)
|
||||||
|
bottom = urwid.SolidCanvas(self._trough_char, sb_width, 1)
|
||||||
|
sb_canv = urwid.CanvasCombine(
|
||||||
|
[(top, None, False)] * top_height
|
||||||
|
+ [(thumb, None, False)] * thumb_height
|
||||||
|
+ [(bottom, None, False)] * bottom_height
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create padding canvas
|
||||||
|
pad_canv = EmptyCanvas(pad_width, maxrow)
|
||||||
|
|
||||||
|
combinelist = [
|
||||||
|
(ow_canv, None, True, ow_size[0]),
|
||||||
|
(pad_canv, None, False, pad_width),
|
||||||
|
(sb_canv, None, False, sb_width),
|
||||||
|
]
|
||||||
|
if self._scrollbar_side != SCROLLBAR_LEFT:
|
||||||
|
return urwid.CanvasJoin(combinelist)
|
||||||
|
else:
|
||||||
|
return urwid.CanvasJoin(reversed(combinelist))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scrollbar_width(self):
|
||||||
|
"""Columns the scrollbar uses"""
|
||||||
|
return max(1, self._scrollbar_width)
|
||||||
|
|
||||||
|
@scrollbar_width.setter
|
||||||
|
def scrollbar_width(self, width):
|
||||||
|
self._scrollbar_width = max(1, int(width))
|
||||||
|
self._invalidate()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scrollbar_side(self):
|
||||||
|
"""Where to display the scrollbar; must be 'left' or 'right'"""
|
||||||
|
return self._scrollbar_side
|
||||||
|
|
||||||
|
@scrollbar_side.setter
|
||||||
|
def scrollbar_side(self, side):
|
||||||
|
if side not in (SCROLLBAR_LEFT, SCROLLBAR_RIGHT):
|
||||||
|
raise ValueError(
|
||||||
|
'scrollbar_side must be "left" or "right", not %r' % side
|
||||||
|
)
|
||||||
|
self._scrollbar_side = side
|
||||||
|
self._invalidate()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scrolling_base_widget(self):
|
||||||
|
"""Nearest `original_widget` that is compatible with the scrolling API"""
|
||||||
|
|
||||||
|
def orig_iter(w):
|
||||||
|
while hasattr(w, "original_widget"):
|
||||||
|
w = w.original_widget
|
||||||
|
yield w
|
||||||
|
yield w
|
||||||
|
|
||||||
|
def is_scrolling_widget(w):
|
||||||
|
return hasattr(w, "get_scrollpos") and hasattr(w, "rows_max")
|
||||||
|
|
||||||
|
for w in orig_iter(self):
|
||||||
|
if is_scrolling_widget(w):
|
||||||
|
return w
|
||||||
|
raise ValueError("Not compatible to be wrapped by ScrollBar: %r" % w)
|
||||||
|
|
||||||
|
def keypress(self, size, key):
|
||||||
|
return self._original_widget.keypress(self._original_widget_size, key)
|
||||||
|
|
||||||
|
def mouse_event(self, size, event, button, col, row, focus):
|
||||||
|
ow = self._original_widget
|
||||||
|
ow_size = self._original_widget_size
|
||||||
|
handled = False
|
||||||
|
if hasattr(ow, "mouse_event"):
|
||||||
|
handled = ow.mouse_event(ow_size, event, button, col, row, focus)
|
||||||
|
|
||||||
|
if not handled and hasattr(ow, "set_scrollpos"):
|
||||||
|
if button == 4: # scroll wheel up
|
||||||
|
pos = ow.get_scrollpos(ow_size)
|
||||||
|
ow.set_scrollpos(pos - 1)
|
||||||
|
return True
|
||||||
|
elif button == 5: # scroll wheel down
|
||||||
|
pos = ow.get_scrollpos(ow_size)
|
||||||
|
ow.set_scrollpos(pos + 1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Scrollable(urwid.AttrWrap):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
_Scrollable(*args, **kwargs), "scrollable", "scrollable-focus"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollBar(urwid.AttrWrap):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
_ScrollBar(*args, **kwargs), "scrollbar", "scrollbar-focus"
|
||||||
|
)
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -1,12 +1,11 @@
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
author="rr-",
|
author="rr-",
|
||||||
author_email="rr-@sakuya.pl",
|
author_email="rr-@sakuya.pl",
|
||||||
name="pqcli",
|
name="pqcli",
|
||||||
long_description="ProgressQuest: the CLI edition",
|
long_description="Progress Quest: the CLI edition",
|
||||||
version="0.1",
|
version="0.3",
|
||||||
url="https://github.com/rr-/pq-cli.git",
|
url="https://github.com/rr-/pq-cli.git",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
entry_points={"console_scripts": ["pqcli = pqcli.__main__:main"]},
|
entry_points={"console_scripts": ["pqcli = pqcli.__main__:main"]},
|
||||||
|
|||||||
Reference in New Issue
Block a user