Merge views into Match, expand history, and polish the UI
- Match page now holds players/match info, turn stats with a turn picker, and the deck lists (My deck · Preview · Opponent); the Deck and Turn tabs are gone. Section heights are draggable and remembered. - History: sortable, resizable columns; deck and commander columns; right-click to show only or hide games with a value, with filter chips; stats tiles (win rate overall, last 10/100, last 7 days, play/draw, average length and turns) that follow the filters. Commanders are backfilled for games recorded before they were stored. - Deck panel shows copies per zone against the deck total (e.g. 2/5). - Preview can be cleared with ✕, a click on the image, or Esc. - Customize menu stays open while toggling; Arrange covers deck sections and turn stats together. - Fix squeezed nav buttons and the header pushing controls off-screen. - Fix known opponent cards being double-counted when also revealed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b3c8c8e63
commit
b3325e8ecc
15 files changed
+760
-183
No files matched your search
@@ -6,7 +6,7 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QSettings, Qt, QThread, QTimer, Signal
|
||||
from PySide6.QtGui import QAction, QKeySequence
|
||||
from PySide6.QtGui import QAction, QFont, QFontMetrics, QKeySequence
|
||||
from PySide6.QtWidgets import (
|
||||
QButtonGroup,
|
||||
QFileDialog,
|
||||
@@ -17,6 +17,7 @@ from PySide6.QtWidgets import (
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QStackedWidget,
|
||||
QToolButton,
|
||||
QVBoxLayout,
|
||||
@@ -38,8 +39,8 @@ from .turn_view import TURN_DEFAULTS_OFF, TURN_TOGGLES, TurnView
|
||||
|
||||
REFRESH_MS = 150
|
||||
DECK_TOGGLES = [("preview", "Card preview pane")]
|
||||
VIEWS = ["Deck", "Match", "Turn", "History"]
|
||||
HISTORY = 3
|
||||
VIEWS = ["Match", "History"]
|
||||
MATCH, HISTORY = 0, 1
|
||||
|
||||
|
||||
class HistoryImport(QThread):
|
||||
@@ -52,17 +53,21 @@ class HistoryImport(QThread):
|
||||
self.path, self.store, self.cards = path, store, cards
|
||||
|
||||
def run(self) -> None:
|
||||
# Own card DB connection: this runs off the GUI thread.
|
||||
cards = CardDB(self.cards.path)
|
||||
saved = 0
|
||||
try:
|
||||
st = self.path.stat()
|
||||
except OSError:
|
||||
return
|
||||
stamp = f"{st.st_size}:{st.st_mtime_ns}"
|
||||
key = f"imported:{self.path}"
|
||||
if self.store.meta(key) == stamp:
|
||||
return
|
||||
# Own card DB connection: this runs off the GUI thread.
|
||||
saved = import_log(self.path, self.store, CardDB(self.cards.path))
|
||||
self.store.set_meta(key, stamp)
|
||||
st = None
|
||||
if st is not None:
|
||||
stamp = f"{st.st_size}:{st.st_mtime_ns}"
|
||||
key = f"imported:{self.path}"
|
||||
if self.store.meta(key) != stamp:
|
||||
saved = import_log(self.path, self.store, cards)
|
||||
self.store.set_meta(key, stamp)
|
||||
# Games recorded before commanders were stored.
|
||||
saved += self.store.backfill_commanders(cards)
|
||||
self.done.emit(saved)
|
||||
|
||||
|
||||
@@ -97,16 +102,20 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# Views. One DeckView is shared by the Deck page and the bottom half of the Match page.
|
||||
self.deck_view = DeckView(cards, images, self.my_sections, self.opp_sections)
|
||||
self.deck_page = QWidget()
|
||||
self.deck_page_layout = QVBoxLayout(self.deck_page)
|
||||
self.deck_page_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.match_view = MatchView(cards, self.toggles[1])
|
||||
self.match_view.hovered.connect(self.deck_view.preview.show_card)
|
||||
self.turn_view = TurnView(self.toggles[2])
|
||||
self.match_view = MatchView(cards, self.toggles[1], self.turn_view, self.deck_view)
|
||||
self.match_view.hovered.connect(self.deck_view.preview.show_card)
|
||||
if (state := self._settings.value("match/split")) is not None:
|
||||
self.match_view.split.restoreState(state)
|
||||
else:
|
||||
# Players / turn stats / deck lists: enough room for a useful number of stat rows.
|
||||
self.match_view.split.setSizes([250, 360, 560])
|
||||
self.match_view.split.splitterMoved.connect(
|
||||
lambda *_: self._settings.setValue("match/split", self.match_view.split.saveState()))
|
||||
self.history_view = HistoryView()
|
||||
self.history_view.open_game.connect(self.open_past_game)
|
||||
self.stack = QStackedWidget()
|
||||
for page in (self.deck_page, self.match_view, self.turn_view, self.history_view):
|
||||
for page in (self.match_view, self.history_view):
|
||||
self.stack.addWidget(page)
|
||||
|
||||
# Top bar
|
||||
@@ -120,12 +129,19 @@ class MainWindow(QMainWindow):
|
||||
btn.setCheckable(True)
|
||||
btn.setShortcut(QKeySequence(f"Ctrl+{i + 1}"))
|
||||
btn.setToolTip(f"{name} view (Ctrl+{i + 1})")
|
||||
# The selected button's label is bold; reserve room for that so it isn't squeezed.
|
||||
bold = QFont(btn.font())
|
||||
bold.setBold(True)
|
||||
btn.setMinimumWidth(QFontMetrics(bold).horizontalAdvance(name) + 2 * theme.NAV_PADDING_X + 4)
|
||||
btn.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
self.nav.addButton(btn, i)
|
||||
bar.addWidget(btn)
|
||||
self.nav.idClicked.connect(self.show_view)
|
||||
bar.addSpacing(16)
|
||||
self.header = QLabel("Waiting for a match…")
|
||||
self.header.setTextFormat(Qt.TextFormat.RichText)
|
||||
# Let long headers be cut off rather than squeezing the buttons around them.
|
||||
self.header.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred)
|
||||
bar.addWidget(self.header, 1)
|
||||
self.customize = QToolButton()
|
||||
self.customize.setText("⚙ Customize")
|
||||
@@ -188,7 +204,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.resize(1400, 880)
|
||||
self.history_view.set_games(history.games())
|
||||
self.show_view(int(self._settings.value("view", 0, type=int)))
|
||||
self.show_view(int(self._settings.value("view2", MATCH, type=int)))
|
||||
if log_path is not None:
|
||||
self.start_log(log_path, replay_rate)
|
||||
else:
|
||||
@@ -213,49 +229,38 @@ class MainWindow(QMainWindow):
|
||||
quit_act.setShortcut(QKeySequence.StandardKey.Quit)
|
||||
quit_act.triggered.connect(self.close)
|
||||
self.addAction(quit_act) # keep the shortcut active while the menu is closed
|
||||
unpreview = QAction("Stop previewing", self)
|
||||
unpreview.setShortcut(QKeySequence(Qt.Key.Key_Escape))
|
||||
unpreview.triggered.connect(self.deck_view.preview.clear)
|
||||
self.addAction(unpreview)
|
||||
menu.addAction(quit_act)
|
||||
btn.setMenu(menu)
|
||||
return btn
|
||||
|
||||
def _arrangeable(self) -> list[Toggles]:
|
||||
"""Toggle groups whose order can be arranged in the current view (deck view in match view too)."""
|
||||
index = self.stack.currentIndex()
|
||||
if index in (0, 1):
|
||||
return [self.my_sections, self.opp_sections]
|
||||
return [self.toggles[2]]
|
||||
return [self.my_sections, self.opp_sections, self.toggles[2]]
|
||||
|
||||
def _fill_customize(self) -> None:
|
||||
menu = self.customize_menu
|
||||
menu.clear()
|
||||
index = self.stack.currentIndex()
|
||||
if index == 2:
|
||||
self.toggles[2].fill_menu(menu)
|
||||
else:
|
||||
self.toggles[index].fill_menu(menu)
|
||||
for tg in (self.my_sections, self.opp_sections):
|
||||
sub = StayOpenMenu(f"{tg.title} sections", menu)
|
||||
tg.fill_menu(sub)
|
||||
menu.addMenu(sub)
|
||||
self.toggles[1].fill_menu(menu) # match panels
|
||||
for title, tg in (("Turn stats", self.toggles[2]), ("My deck sections", self.my_sections),
|
||||
("Opponent sections", self.opp_sections), ("Preview", self.toggles[0])):
|
||||
sub = StayOpenMenu(title, menu)
|
||||
tg.fill_menu(sub)
|
||||
menu.addMenu(sub)
|
||||
menu.addSeparator()
|
||||
arrange = menu.addAction("Arrange " + ("stats" if index == 2 else "deck sections") + "…")
|
||||
arrange.triggered.connect(self._open_arrange)
|
||||
menu.addAction("Arrange sections and stats…").triggered.connect(self._open_arrange)
|
||||
|
||||
def _open_arrange(self) -> None:
|
||||
index = self.stack.currentIndex()
|
||||
title = "Arrange turn stats" if index == 2 else "Arrange deck sections"
|
||||
ArrangeDialog(self._arrangeable(), title, self).exec()
|
||||
ArrangeDialog(self._arrangeable(), "Arrange sections and stats", self).exec()
|
||||
|
||||
def show_view(self, index: int) -> None:
|
||||
index = max(0, min(len(VIEWS) - 1, index))
|
||||
self.customize.setEnabled(index != HISTORY)
|
||||
# Move the shared deck view to wherever it is needed.
|
||||
if index == 1:
|
||||
self.match_view.deck_slot_layout.addWidget(self.deck_view)
|
||||
else:
|
||||
self.deck_page_layout.addWidget(self.deck_view)
|
||||
self.customize.setEnabled(index == MATCH)
|
||||
self.stack.setCurrentIndex(index)
|
||||
self.nav.button(index).setChecked(True)
|
||||
self._settings.setValue("view", index)
|
||||
self._settings.setValue("view2", index)
|
||||
self._force_refresh()
|
||||
|
||||
# -- menu actions ----------------------------------------------------------
|
||||
@@ -326,7 +331,7 @@ class MainWindow(QMainWindow):
|
||||
self.past_label.setText(text)
|
||||
self.past_bar.show()
|
||||
self.turn_view.selected = None
|
||||
self.show_view(0)
|
||||
self.show_view(MATCH)
|
||||
|
||||
def close_past_game(self) -> None:
|
||||
self.past = None
|
||||
@@ -364,7 +369,7 @@ class MainWindow(QMainWindow):
|
||||
self.refresh()
|
||||
|
||||
def _tick(self) -> None:
|
||||
if self.stack.currentIndex() == 1:
|
||||
if self.stack.currentIndex() == MATCH:
|
||||
self.match_view.tick(self.shown_tracker, self.now_ms())
|
||||
|
||||
def refresh(self) -> None:
|
||||
@@ -376,11 +381,9 @@ class MainWindow(QMainWindow):
|
||||
self._rendered[index] = key
|
||||
self._render_header(t)
|
||||
self.deck_view.preview.parentWidget().setVisible(self.toggles[0].on("preview"))
|
||||
if index in (0, 1):
|
||||
if index == MATCH:
|
||||
self.deck_view.render(t)
|
||||
if index == 1:
|
||||
self.match_view.render(t, self.now_ms())
|
||||
elif index == 2:
|
||||
self.turn_view.render(t)
|
||||
|
||||
def _render_header(self, t: MatchTracker) -> None:
|
||||
@@ -397,6 +400,7 @@ class MainWindow(QMainWindow):
|
||||
if not t.in_match and t.last_result:
|
||||
parts.append(f"<b>Match {t.last_result.lower()}</b>")
|
||||
self.header.setText(" · ".join(parts))
|
||||
self.header.setToolTip(" · ".join(parts))
|
||||
|
||||
messages = []
|
||||
if not self.cards.available:
|
||||
|
||||
Reference in new issue
Block a user