Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metadata to BasicGameSaveGameInfo(Widget) #109

Merged
merged 7 commits into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading