Skip to content

Commit

Permalink
Tracker: Display computed tooltips for entrances
Browse files Browse the repository at this point in the history
  • Loading branch information
YourAverageLink committed Jul 25, 2024
1 parent 056bd72 commit b64b22f
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 5 deletions.
144 changes: 140 additions & 4 deletions gui/components/tracker_entrance_label.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
from PySide6.QtWidgets import QLabel
from PySide6.QtGui import QCursor, QMouseEvent
import platform
from PySide6.QtWidgets import QLabel, QToolTip
from PySide6.QtGui import QCursor, QMouseEvent, QFontMetrics, QTextDocumentFragment
from PySide6 import QtCore
from PySide6.QtCore import Signal
from PySide6.QtCore import Signal, QPoint

from logic.entrance import Entrance
from logic.search import Search
from logic.requirements import *

from constants.guiconstants import TRACKER_LOCATION_TOOLTIP_STYLESHEET
from logic.tooltips.tooltips import pretty_name, sort_requirement


class TrackerEntranceLabel(QLabel):

default_stylesheet = "border-width: 1px; border-color: gray; color: COLOR;"
default_stylesheet = (
"QLabel { border-width: 1px; border-color: gray; color: COLOR; }"
)
choose_target = Signal(Entrance, str)
disconnect_entrance = Signal(Entrance, str)

Expand All @@ -19,18 +25,21 @@ def __init__(
entrance_: Entrance,
parent_area_name_: str,
recent_search_: Search,
world_,
show_full_connection_: bool,
) -> None:
super().__init__()
self.entrance = entrance_
self.parent_area_name = parent_area_name_
self.recent_search = recent_search_
self.world = world_
self.show_full_connection = show_full_connection_
self.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor))
self.setMargin(10)
self.setMinimumHeight(30)
self.setMaximumWidth(273)
self.setWordWrap(True)
self.setMouseTracking(True)
self.update_text()

def update_text(self, recent_search_: Search | None = None) -> None:
Expand All @@ -48,6 +57,11 @@ def update_text(self, recent_search_: Search | None = None) -> None:
f"{first_part}{original_connected} -> {connected_area.name if connected_area else '?'}"
)

self.update_color(recent_search_)

def update_color(self, recent_search_: Search | None = None) -> None:
if recent_search_ is not None:
self.recent_search = recent_search_
# Set the color as blue if accessible, or red if not
color = "red"
if (
Expand All @@ -65,6 +79,7 @@ def update_text(self, recent_search_: Search | None = None) -> None:

self.setStyleSheet(
TrackerEntranceLabel.default_stylesheet.replace("COLOR", color)
+ TRACKER_LOCATION_TOOLTIP_STYLESHEET
)

def mouseReleaseEvent(self, ev: QMouseEvent) -> None:
Expand All @@ -74,3 +89,124 @@ def mouseReleaseEvent(self, ev: QMouseEvent) -> None:
self.disconnect_entrance.emit(self.entrance, self.parent_area_name)
self.update_text()
return super().mouseReleaseEvent(ev)

def mouseMoveEvent(self, ev: QMouseEvent) -> None:
coords = self.mapToGlobal(QPoint(-2, self.height() - 15))
# For whatever reason, MacOS calculates this position differently,
# so we must offset the height to compensate
if platform.system() == "Darwin":
coords.setY(coords.y() - 18)
QToolTip.showText(coords, self.get_tooltip_text(), self)

return super().mouseMoveEvent(ev)

def get_tooltip_text(self) -> str:
req = self.entrance.computed_requirement
sort_requirement(req)
match req.type:
case RequirementType.AND:
# Computed requirements have a top-level AND requirement
# We display them as a list of bullet points to the user
# This fetches a list of the terms ANDed together
text = [self.format_requirement(a) for a in req.args]
case _:
# The requirement is just one term, so format the requirement
text = [self.format_requirement(req)]

tooltip_font_metrics = QFontMetrics(QToolTip.font())
# Find the width of the longest requirement description, adding a 16px buffer for the bullet point
max_line_width = (
max(
[
tooltip_font_metrics.horizontalAdvance(
QTextDocumentFragment.fromHtml(line).toPlainText()
)
for line in text + ["Item Requirements:"]
]
)
+ 16
)
# Set the tooltip's min and max width to ensure the tooltip is the right size and line-breaks properly
self.setStyleSheet(
self.styleSheet()
.replace("MINWIDTH", str(min(max_line_width, self.width() - 3)))
.replace("MAXWIDTH", str(self.width() - 3))
)
return (
"Item Requirements:"
+ '<ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 8px; margin-right: 0px; -qt-list-indent:0;"><li>'
+ "</li><li>".join(text)
+ "</li></ul>"
)

def format_requirement(self, req: Requirement, is_top_level=True) -> str:
match req.type:
case RequirementType.IMPOSSIBLE:
return '<span style="color:red">Impossible (please discover an entrance first)</span>'
case RequirementType.NOTHING:
return '<span style="color:dodgerblue">Nothing</span>'
case RequirementType.ITEM:
# Determine if the user has marked this item
color = (
"dodgerblue"
if evaluate_requirement_at_time(
req, self.recent_search, TOD.ALL, self.world
)
else "red"
)
# Get a pretty name for the item if it is the first stage of a progressive item
name = pretty_name(req.args[0].name, 1)
return f'<span style="color:{color}">{name}</span>'
case RequirementType.COUNT:
# Determine if the user has enough of this item marked
color = (
"dodgerblue"
if evaluate_requirement_at_time(
req, self.recent_search, TOD.ALL, self.world
)
else "red"
)
# Get a pretty name for the progressive item
name = pretty_name(req.args[1].name, req.args[0])
return f'<span style="color:{color}">{name}</span>'
case RequirementType.WALLET_CAPACITY:
# Determine if the user has enough wallet capacity for this requirement
color = (
"dodgerblue"
if evaluate_requirement_at_time(
req, self.recent_search, TOD.ALL, self.world
)
else "red"
)
# TODO: Properly expand into wallet combinations
return f'<span style="color:{color}">Wallet >= {req.args[0]}</span>'
case RequirementType.GRATITUDE_CRYSTALS:
# Determine if the user has enough gratitude crystals marked
color = (
"dodgerblue"
if evaluate_requirement_at_time(
req, self.recent_search, TOD.ALL, self.world
)
else "red"
)
return f'<span style="color:{color}">{req.args[0]} Gratitude Crystals</span>'
case RequirementType.OR:
# Recursively join requirements with "or"
# Only include parentheses if not at the top level (where they'd be redundant)
return (
("" if is_top_level else "(")
+ " or ".join([self.format_requirement(a, False) for a in req.args])
+ ("" if is_top_level else ")")
)
case RequirementType.AND:
# Recursively join requirements with "and"
# Only include parentheses if not at the top level (where they'd be redundant)
return (
("" if is_top_level else "(")
+ " and ".join(
[self.format_requirement(a, False) for a in req.args]
)
+ ("" if is_top_level else ")")
)
case _:
raise ValueError("unreachable")
1 change: 1 addition & 0 deletions gui/components/tracker_location_label.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from logic.search import Search

from filepathconstants import TRACKER_ASSETS_PATH
from logic.tooltips.tooltips import pretty_name, sort_requirement


class TrackerLocationLabel(QLabel):
Expand Down
6 changes: 5 additions & 1 deletion gui/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1153,7 +1153,11 @@ def show_area_entrances(self, area_name: str) -> None:
]
)
entrance_label = TrackerEntranceLabel(
entrance, area_name, area_button.recent_search, show_full_connection
entrance,
area_name,
area_button.recent_search,
self.world,
show_full_connection,
)
entrance_label.choose_target.connect(self.show_target_selection_info)
entrance_label.disconnect_entrance.connect(
Expand Down
42 changes: 42 additions & 0 deletions logic/tooltips/tooltips.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from ..item_pool import get_complete_item_pool
from ..search import Search, SearchMode
from ..requirements import Requirement, RequirementType, ALL_TODS, visit_requirement
from typing import Callable
from constants.trackerprettyitems import PRETTY_ITEM_NAMES
from ..world import World, TOD, LocationAccess
from ..entrance import Entrance
from ..area import EventAccess, Area
Expand Down Expand Up @@ -393,3 +395,43 @@ def evaluate_partial_requirement(

case RequirementType.NIGHT:
return DNF.true() if time & TOD.NIGHT else DNF.false()


def num_terms(req: Requirement):
if req.type == RequirementType.AND or req.type == RequirementType.OR:
return sum(map(num_terms, req.args))
return 1


def sort_requirement(req: Requirement):
def by_length(req: Requirement):
if req.type == RequirementType.AND or req.type == RequirementType.OR:
return num_terms(req)
return -1

def by_item(req: Requirement):
if req.type == RequirementType.ITEM:
return pretty_name(req.args[0].name, 1)
elif req.type == RequirementType.COUNT:
return pretty_name(req.args[1].name, req.args[0])
elif req.type == RequirementType.AND or req.type == RequirementType.OR:
return by_item(req.args[0])
return ""

def sort_key(req: Requirement):
return (by_length(req), by_item(req))

if req.type == RequirementType.AND or req.type == RequirementType.OR:
for expr in req.args:
sort_requirement(expr)
req.args.sort(key=sort_key)


def pretty_name(item, count):
if (pretty_name := PRETTY_ITEM_NAMES.get((item, count), None)) is not None:
return pretty_name

if count > 1:
return f"{item} x {count}"
else:
return item

0 comments on commit b64b22f

Please sign in to comment.