Skip to content

Commit

Permalink
Add metadata to BasicGameSaveGameInfo(Widget) (#109)
Browse files Browse the repository at this point in the history
* Fix subnautica preview file path

* Add BasicGameSaveGameInfo with "File Date" to all BasicGames

* Add max_width option to BasicGameSaveGameInfo(Widget)

* Refactor Blade & Sorcery with get_metadata

* Refactor & cleanup Black & White SaveGameInfo
  • Loading branch information
ZashIn authored Oct 8, 2023
1 parent 583bb78 commit 44645a5
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 236 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,11 @@ The meta-plugin provides some useful extra feature:
`GameOriginManifestIds`, `GameEpicId` or `GameEaDesktopId`), the game will be listed
in the list of available games when creating a new MO2 instance (if the game is
installed via Steam, GOG, Origin, Epic Games / Legendary or EA Desktop).
2. **Basic save game preview:** If you use the Python version, and if you can easily obtain a picture (file)
for any saves, you can provide basic save-game preview by using the `BasicGameSaveGameInfo`.
See [games/game_witcher3.py](games/game_witcher3.py) for more details.
2. **Basic save game preview / metadata** (Python): If you can easily obtain a picture
(file) and/or metadata (like from json) for any saves, you can provide basic save-game
preview by using the `BasicGameSaveGameInfo`. See
[games/game_witcher3.py](games/game_witcher3.py) and
[games/game_bladeandsorcery.py](games/game_bladeandsorcery.py) for more details.
3. **Basic local save games** (Python): profile specific save games, as in [games/game_valheim.py](games/game_valheim.py).
4. **Basic mod data checker** (Python):
Check and fix different mod archive layouts for an automatic installation with the proper
Expand Down
195 changes: 151 additions & 44 deletions basic_features/basic_save_game_info.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
# -*- encoding: utf-8 -*-

import sys
from collections.abc import Mapping
from datetime import datetime
from pathlib import Path
from typing import Callable, Sequence
from typing import Any, Callable, Self, Sequence

import mobase
from PyQt6.QtCore import QDateTime, Qt
from PyQt6.QtCore import QDateTime, QLocale, Qt
from PyQt6.QtGui import QImage, QPixmap
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget
from PyQt6.QtWidgets import QFormLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget


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):
Expand All @@ -31,81 +49,170 @@ 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
] = lambda p: 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)

self._get_preview = get_preview
self._get_preview = get_preview or (lambda p: None)
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()
palette = self._label.palette()
palette.setColor(self._label.foregroundRole(), Qt.GlobalColor.white)
self._label.setPalette(palette)
layout.addWidget(self._label)
self.setLayout(layout)

palette = self.palette()
palette.setColor(self.backgroundRole(), Qt.GlobalColor.black)
self.setAutoFillBackground(True)
self.setPalette(palette)

self.setWindowFlags(
Qt.WindowType.ToolTip | Qt.WindowType.BypassGraphicsProxyWidget
)

def setSave(self, save: mobase.ISaveGame):
# Resize the label to (0, 0) to hide it:
self.resize(0, 0)

# Retrieve the pixmap:
value = self._get_preview(Path(save.getFilepath()))
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 value is None:
return
if metadata or pixmap:
self.adjustSize()
self.show()

if isinstance(value, Path):
pixmap = QPixmap(str(value))
elif isinstance(value, str):
pixmap = QPixmap(value)
elif isinstance(value, QImage):
pixmap = QPixmap.fromImage(value)
else:
print(
"Failed to retrieve the preview, bad return type: {}.".format(
type(value)
),
file=sys.stderr,
)
return
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

# Scale the pixmap and show it:
pixmap = pixmap.scaledToWidth(320)
self._label.setPixmap(pixmap)
self.resize(pixmap.width(), pixmap.height())
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_preview = get_preview
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_preview is not None:
return BasicGameSaveGameInfoWidget(parent, self._get_preview)
return None
if self._get_widget:
return self._get_widget(parent)
else:
return None
6 changes: 5 additions & 1 deletion basic_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths
from PyQt6.QtGui import QIcon

from .basic_features.basic_save_game_info import BasicGameSaveGame
from .basic_features.basic_save_game_info import (
BasicGameSaveGame,
BasicGameSaveGameInfo,
)


def replace_variables(value: str, game: BasicGame) -> str:
Expand Down Expand Up @@ -427,6 +430,7 @@ def is_eadesktop(self) -> bool:

def init(self, organizer: mobase.IOrganizer) -> bool:
self._organizer = organizer
self._featureMap[mobase.SaveGameInfo] = BasicGameSaveGameInfo()
if self._mappings.originWatcherExecutables.get():
from .origin_utils import OriginWatcher

Expand Down
40 changes: 1 addition & 39 deletions games/game_blackandwhite2.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import datetime
import os
import struct
import sys
import time
from pathlib import Path
from typing import BinaryIO

import mobase
from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo, Qt
from PyQt6.QtGui import QPainter, QPixmap
from PyQt6.QtWidgets import QWidget

from ..basic_features import BasicLocalSavegames
from ..basic_features.basic_save_game_info import (
BasicGameSaveGame,
BasicGameSaveGameInfo,
BasicGameSaveGameInfoWidget,
)
from ..basic_game import BasicGame

Expand Down Expand Up @@ -281,41 +278,6 @@ def _getPreview(savepath: Path):
return pixmap.copy(0, 0, width, height)


class BlackAndWhite2SaveGameInfoWidget(BasicGameSaveGameInfoWidget):
def setSave(self, save: mobase.ISaveGame):
# Resize the label to (0, 0) to hide it:
self.resize(0, 0)

# Retrieve the pixmap:
value = self._get_preview(Path(save.getFilepath()))

if value is None:
return

elif isinstance(value, QPixmap):
pixmap = value
else:
print(
"Failed to retrieve the preview, bad return type: {}.".format(
type(value)
),
file=sys.stderr,
)
return

# Scale the pixmap and show it:
# pixmap = pixmap.scaledToWidth(pixmap.width())
self._label.setPixmap(pixmap)
self.resize(pixmap.width(), pixmap.height())


class BlackAndWhite2SaveGameInfo(BasicGameSaveGameInfo):
def getSaveGameWidget(self, parent: QWidget | None = None):
if self._get_preview is not None:
return BasicGameSaveGameInfoWidget(parent, self._get_preview)
return None


PSTART_MENU = (
str(os.getenv("ProgramData")) + "\\Microsoft\\Windows\\Start Menu\\Programs"
)
Expand Down Expand Up @@ -350,7 +312,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool:
self._featureMap[mobase.LocalSavegames] = BasicLocalSavegames(
self.savesDirectory()
)
self._featureMap[mobase.SaveGameInfo] = BlackAndWhite2SaveGameInfo(_getPreview)
self._featureMap[mobase.SaveGameInfo] = BasicGameSaveGameInfo(_getPreview)
return True

def detectGame(self):
Expand Down
Loading

0 comments on commit 44645a5

Please sign in to comment.