aboutsummaryrefslogtreecommitdiff
path: root/libs/basic_games/basic_features/basic_save_game_info.py
diff options
context:
space:
mode:
Diffstat (limited to 'libs/basic_games/basic_features/basic_save_game_info.py')
-rw-r--r--libs/basic_games/basic_features/basic_save_game_info.py226
1 files changed, 226 insertions, 0 deletions
diff --git a/libs/basic_games/basic_features/basic_save_game_info.py b/libs/basic_games/basic_features/basic_save_game_info.py
new file mode 100644
index 0000000..436e724
--- /dev/null
+++ b/libs/basic_games/basic_features/basic_save_game_info.py
@@ -0,0 +1,226 @@
+# -*- encoding: utf-8 -*-
+
+import sys
+from collections.abc import Mapping
+from datetime import datetime
+from pathlib import Path
+from typing import Any, Callable, Self, Sequence
+
+from PyQt6.QtCore import QDateTime, QLocale, Qt
+from PyQt6.QtGui import QImage, QPixmap
+from PyQt6.QtWidgets import QFormLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget
+
+import mobase
+
+
+def format_date(date_time: QDateTime | datetime | str, format_str: str | None = None):
+ """Default format for date and time in the `BasicGameSaveGameInfoWidget`.
+
+ Args:
+ date_time: either a `QDateTime`/`datetime` or a string together with
+ a `format_str`.
+ format_str (optional): date/time format string (see `QDateTime.fromString`).
+
+ Returns:
+ Date and time in short locale format.
+ """
+ if isinstance(date_time, str):
+ date_time = QDateTime.fromString(date_time, format_str)
+ return QLocale.system().toString(date_time, QLocale.FormatType.ShortFormat)
+
+
+class BasicGameSaveGame(mobase.ISaveGame):
+ def __init__(self, filepath: Path):
+ super().__init__()
+ self._filepath = filepath
+
+ def getFilepath(self) -> str:
+ return self._filepath.as_posix()
+
+ def getName(self) -> str:
+ return self._filepath.name
+
+ def getCreationTime(self):
+ return QDateTime.fromSecsSinceEpoch(int(self._filepath.stat().st_mtime))
+
+ def getSaveGroupIdentifier(self) -> str:
+ return ""
+
+ def allFiles(self) -> list[str]:
+ return [self.getFilepath()]
+
+
+def get_filedate_metadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]:
+ """Returns saves file date as the metadata for `BasicGameSaveGameInfoWidget`."""
+ return {"File Date:": format_date(save.getCreationTime())}
+
+
+class BasicGameSaveGameInfoWidget(mobase.ISaveGameInfoWidget):
+ """Save game info widget to display metadata and a preview."""
+
+ def __init__(
+ self,
+ parent: QWidget | None,
+ get_preview: (
+ Callable[[Path], QPixmap | QImage | Path | str | None] | None
+ ) = lambda p: None,
+ get_metadata: (
+ Callable[[Path, mobase.ISaveGame], Mapping[str, Any] | None] | None
+ ) = get_filedate_metadata,
+ max_width: int = 320,
+ ):
+ """
+ Args:
+ parent: parent widget
+ get_preview (optional): `callback(savegame_path)` returning the
+ saves preview image or the path to it.
+ get_metadata (optional): `callback(savegame_path, ISaveGame)` returning
+ the saves metadata. By default the saves file date is shown.
+ max_width (optional): The maximum widget and (scaled) preview width.
+ Defaults to 320.
+ """
+ super().__init__(parent)
+
+ def _no_preview(p: Path) -> None:
+ return None
+
+ self._get_preview = get_preview or _no_preview
+ self._get_metadata = get_metadata or get_filedate_metadata
+ self._max_width = max_width or 320
+
+ layout = QVBoxLayout()
+
+ # Metadata form
+ self._metadata_widget = QWidget()
+ self._metadata_widget.setMaximumWidth(self._max_width)
+ self._metadata_layout = form_layout = QFormLayout(self._metadata_widget)
+ form_layout.setContentsMargins(0, 0, 0, 0)
+ form_layout.setVerticalSpacing(2)
+ layout.addWidget(self._metadata_widget)
+ self._metadata_widget.hide() # Backwards compatibility (no metadata)
+
+ # Preview (pixmap)
+ self._label = QLabel()
+ layout.addWidget(self._label)
+ self.setLayout(layout)
+
+ self.setWindowFlags(
+ Qt.WindowType.ToolTip | Qt.WindowType.BypassGraphicsProxyWidget
+ )
+
+ def setSave(self, save: mobase.ISaveGame):
+ save_path = Path(save.getFilepath())
+
+ # Clear previous
+ self.hide()
+ self._label.clear()
+ while self._metadata_layout.count():
+ layoutItem = self._metadata_layout.takeAt(0)
+ if layoutItem is not None and (w := layoutItem.widget()):
+ w.deleteLater()
+
+ # Retrieve the pixmap and metadata:
+ preview = self._get_preview(save_path)
+ pixmap = None
+
+ # Set the preview pixmap if the preview file exits
+ if preview is not None:
+ if isinstance(preview, str):
+ preview = Path(preview)
+ if isinstance(preview, Path):
+ if preview.exists():
+ pixmap = QPixmap(str(preview))
+ else:
+ print(
+ f"Failed to retrieve the preview, file not found: {preview}",
+ file=sys.stderr,
+ )
+ elif isinstance(preview, QImage):
+ pixmap = QPixmap.fromImage(preview)
+ else:
+ pixmap = preview
+ if pixmap and not pixmap.isNull():
+ # Scale the pixmap and show it:
+ pixmap = pixmap.scaledToWidth(self._max_width)
+ self._label.setPixmap(pixmap)
+ self._label.show()
+ else:
+ self._label.hide()
+ pixmap = None
+
+ # Add metadata, file date by default.
+ metadata = self._get_metadata(save_path, save)
+ if metadata:
+ for key, value in metadata.items():
+ self._metadata_layout.addRow(*self._new_form_row(key, str(value)))
+ self._metadata_widget.show()
+ self._metadata_widget.setLayout(self._metadata_layout)
+ self._metadata_widget.adjustSize()
+ else:
+ self._metadata_widget.hide()
+
+ if metadata or pixmap:
+ self.adjustSize()
+ self.show()
+
+ def _new_form_row(self, label: str = "", field: str = ""):
+ qLabel = QLabel(text=label)
+ qLabel.setAlignment(Qt.AlignmentFlag.AlignTop)
+ qLabel.setStyleSheet("font: italic")
+ qLabel.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
+ qField = QLabel(text=field)
+ qField.setWordWrap(True)
+ qField.setAlignment(Qt.AlignmentFlag.AlignTop)
+ qField.setStyleSheet("font: bold")
+ qField.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
+ return qLabel, qField
+
+ def set_maximum_width(self, width: int):
+ self._max_width = width
+ self._metadata_widget.setMaximumWidth(width)
+
+
+class BasicGameSaveGameInfo(mobase.SaveGameInfo):
+ _get_widget: Callable[[QWidget | None], mobase.ISaveGameInfoWidget | None] | None
+
+ def __init__(
+ self,
+ get_preview: (
+ Callable[[Path], QPixmap | QImage | Path | str | None] | None
+ ) = None,
+ get_metadata: (
+ Callable[[Path, mobase.ISaveGame], Mapping[str, Any] | None] | None
+ ) = None,
+ max_width: int = 0,
+ ):
+ """Args from: `BasicGameSaveGameInfoWidget`."""
+ super().__init__()
+ self._get_widget = lambda parent: BasicGameSaveGameInfoWidget(
+ parent, get_preview, get_metadata, max_width
+ )
+
+ @classmethod
+ def with_widget(
+ cls,
+ widget: type[mobase.ISaveGameInfoWidget] | None,
+ ) -> Self:
+ """
+
+ Args:
+ widget: a custom `ISaveGameInfoWidget` instead of the default
+ `BasicGameSaveGameInfoWidget`.
+ """
+ self = cls()
+ self._get_widget = lambda parent: widget(parent) if widget else None
+ return self
+
+ def getMissingAssets(self, save: mobase.ISaveGame) -> dict[str, Sequence[str]]:
+ return {}
+
+ def getSaveGameWidget(
+ self, parent: QWidget | None = None
+ ) -> mobase.ISaveGameInfoWidget | None:
+ if self._get_widget:
+ return self._get_widget(parent)
+ else:
+ return None