|
| 1 | +# SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | +# SPDX-FileCopyrightText: 2026 sp00ktober |
| 3 | +"""The Points panel: the document's named points, and the one picked: its name, kind, place and |
| 4 | +note. The Point tool sets new ones; `mhfu rig goto|walk` reads them from the saved map.toml. |
| 5 | +""" |
| 6 | + |
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +from typing import TYPE_CHECKING, Any |
| 10 | + |
| 11 | +from PySide6.QtGui import QGuiApplication |
| 12 | +from PySide6.QtWidgets import QVBoxLayout, QWidget |
| 13 | + |
| 14 | +from mhfu_studio.shell.text import plain |
| 15 | +from mhfu_studio.ui import kit |
| 16 | + |
| 17 | +from ..core.atlas import stage_title |
| 18 | +from ..tools import POINT, TOOL |
| 19 | +from .common import Gate |
| 20 | + |
| 21 | +if TYPE_CHECKING: |
| 22 | + from mhfu_studio.shell.studio import Studio |
| 23 | + |
| 24 | + from ..workspace import MapWorkspace |
| 25 | + |
| 26 | +KINDS = ( |
| 27 | + ("point", "Point: a place to go to"), |
| 28 | + ("waypoint", "Waypoint: a stop on the way"), |
| 29 | + ("climb", "Climb: the foot of a ledge"), |
| 30 | +) |
| 31 | +CLIMB_TIP = ( |
| 32 | + "Degrees the hunter faces to climb, square on to the face: 0 is +z, 90 is +x. A walk may" |
| 33 | + " climb here on its way, and climbs on arriving." |
| 34 | +) |
| 35 | + |
| 36 | + |
| 37 | +def command(name: str, folder: object) -> str: |
| 38 | + """What takes the hunter to the point in the running game.""" |
| 39 | + where = f" --map {folder}" if folder else "" |
| 40 | + return f"mhfu rig goto {name}{where}" |
| 41 | + |
| 42 | + |
| 43 | +class PointsPanel(kit.Panel): |
| 44 | + def __init__(self, ws: MapWorkspace, studio: Studio) -> None: |
| 45 | + super().__init__() |
| 46 | + self.ws, self.studio = ws, studio |
| 47 | + self.items = kit.Items( |
| 48 | + tip="The document's named points; this area's first. Click one to edit it (and" |
| 49 | + " load its area).", |
| 50 | + empty="No point yet: pick the Point tool (P) and click the ground.", |
| 51 | + ) |
| 52 | + self.items.picked.connect(self._pick) |
| 53 | + add = kit.button( |
| 54 | + "Set a point", |
| 55 | + tip="Turns on the Point tool: the next click on the ground sets a point there", |
| 56 | + on=studio.act("point tool", lambda: ws.set_tool(TOOL, POINT)), |
| 57 | + icon="ph.map-pin", |
| 58 | + ) |
| 59 | + self.name = kit.text_field( |
| 60 | + tip="The point's name, what `mhfu rig goto` takes; unique in the document", |
| 61 | + on=self._rename, |
| 62 | + ) |
| 63 | + self.kind = kit.choice( |
| 64 | + KINDS, |
| 65 | + tip="A point is somewhere to go; a waypoint a stop on the way; a climb the foot of" |
| 66 | + " a ledge, which a walk may climb", |
| 67 | + on=lambda k: self._edit("kind", kind=k), |
| 68 | + ) |
| 69 | + self.heading = kit.number( |
| 70 | + tip=CLIMB_TIP, lo=-180, hi=360, step=5, suffix=" deg", |
| 71 | + on=lambda v: self._edit("heading", heading=float(v)), |
| 72 | + ) # fmt: skip |
| 73 | + self.at = kit.Vec3( |
| 74 | + tip="Where the point is, in the area's world units; y is the ground", |
| 75 | + decimals=0, |
| 76 | + step=10, |
| 77 | + on=lambda v: self._edit("place", at=(v[0], v[1], v[2])), |
| 78 | + ) |
| 79 | + self.note = kit.text_field( |
| 80 | + tip="Anything to remember about the point", on=lambda t: self._edit("note", note=t) |
| 81 | + ) |
| 82 | + self.where = kit.label(role="muted", selectable=True) |
| 83 | + copy = kit.button( |
| 84 | + "Copy goto command", |
| 85 | + tip="Copies the command that takes the hunter here in the running game", |
| 86 | + on=self._copy, |
| 87 | + icon="ph.copy", |
| 88 | + ) |
| 89 | + remove = kit.button( |
| 90 | + "Remove", |
| 91 | + tip="Takes the point out of the document", |
| 92 | + on=self._remove, |
| 93 | + icon="ph.trash", |
| 94 | + ) |
| 95 | + form = kit.Form() |
| 96 | + form.row("Name", self.name) |
| 97 | + form.row("Kind", self.kind) |
| 98 | + form.row("Climb", self.heading) |
| 99 | + form.row("At", self.at) |
| 100 | + form.row("Note", self.note) |
| 101 | + form.row("Area", self.where) |
| 102 | + self.form = QWidget() |
| 103 | + lay = QVBoxLayout(self.form) |
| 104 | + lay.setContentsMargins(0, 0, 0, 0) |
| 105 | + lay.addWidget(form) |
| 106 | + lay.addWidget(kit.row(copy, remove, stretch=True)) |
| 107 | + |
| 108 | + page = QWidget() |
| 109 | + lay = QVBoxLayout(page) |
| 110 | + lay.setContentsMargins(0, 0, 0, 0) |
| 111 | + lay.addWidget(self.items) |
| 112 | + lay.addWidget(kit.row(add, stretch=True)) |
| 113 | + lay.addWidget(self.form) |
| 114 | + lay.addStretch(1) |
| 115 | + self.gate = Gate(page, "set points on it") |
| 116 | + self.body.addWidget(self.gate) |
| 117 | + |
| 118 | + # ---- actions ---------------------------------------------------------------------- # |
| 119 | + |
| 120 | + def _pick(self, name: object) -> None: |
| 121 | + if isinstance(name, str): |
| 122 | + self.studio.act(f"pick {name}", lambda: self.pick(name))() |
| 123 | + |
| 124 | + def pick(self, name: str) -> None: |
| 125 | + """Edit the point `name`, loading its area when another is loaded.""" |
| 126 | + ws, p = self.ws, self.ws.doc.point(name) |
| 127 | + if p is None: |
| 128 | + return |
| 129 | + ws.point = name |
| 130 | + if p not in ws.points_here(): |
| 131 | + ws.load_stage(p.stage) |
| 132 | + |
| 133 | + def _edit(self, what: str, **changes: Any) -> None: |
| 134 | + name = self.ws.point |
| 135 | + if name is not None: |
| 136 | + self.studio.act( |
| 137 | + f"point {what}", lambda: self._say(self.ws.edit_point(name, **changes)) |
| 138 | + )() |
| 139 | + |
| 140 | + def _rename(self, text: str) -> None: |
| 141 | + if self.ws.point is not None and text.strip() != self.ws.point: |
| 142 | + self._edit("name", name=text.strip()) |
| 143 | + |
| 144 | + def _say(self, refusal: str | None) -> None: |
| 145 | + if refusal: |
| 146 | + self.studio.message = f"point not changed: {refusal}" |
| 147 | + |
| 148 | + def _remove(self) -> None: |
| 149 | + name = self.ws.point |
| 150 | + if name is not None: |
| 151 | + self.studio.act(f"remove {name}", lambda: self.ws.remove_point(name))() |
| 152 | + |
| 153 | + def _copy(self) -> None: |
| 154 | + if self.ws.point is not None: |
| 155 | + text = command(self.ws.point, self.ws.doc.directory) |
| 156 | + QGuiApplication.clipboard().setText(text) |
| 157 | + self.studio.message = f"copied: {text}" |
| 158 | + |
| 159 | + # ---- sync ------------------------------------------------------------------------- # |
| 160 | + |
| 161 | + def sync(self) -> None: |
| 162 | + ws = self.ws |
| 163 | + if not self.gate.check(ws, section=False): |
| 164 | + return |
| 165 | + here = {p.name for p in ws.points_here()} |
| 166 | + ordered = sorted(ws.doc.points, key=lambda p: p.name not in here) |
| 167 | + self.items.set_items( |
| 168 | + [ |
| 169 | + kit.Item( |
| 170 | + f"{p.name} ({stage_title(p.stage)})" |
| 171 | + + ("" if p.kind == "point" else f", {p.kind}"), |
| 172 | + p.name, |
| 173 | + plain(p.note) or command(p.name, ws.doc.directory), |
| 174 | + ) |
| 175 | + for p in ordered |
| 176 | + ] |
| 177 | + ) |
| 178 | + p = ws.doc.point(ws.point) if ws.point else None |
| 179 | + self.form.setVisible(p is not None) |
| 180 | + if p is None: |
| 181 | + return |
| 182 | + kit.put(self.name, p.name) |
| 183 | + kit.put(self.kind, p.kind) |
| 184 | + kit.put(self.heading, p.heading or 0.0) |
| 185 | + self.heading.setEnabled(p.kind == "climb") |
| 186 | + kit.put(self.at, p.at) |
| 187 | + kit.put(self.note, p.note) |
| 188 | + self.where.setText(f"{stage_title(p.stage)} (st{p.stage:03d})") |
0 commit comments