27 Commits
Author SHA1 Message Date
Mikaël Capelle fb91a1d2b6 Merge pull request #2 from Holt59/fix-optional-plugins
Fix not-selected plugins
2021-03-13 13:49:42 +01:00
Mikaël Capelle 9f71ac1517 Move not-selected plugins to 'optional'. 2021-03-13 13:48:45 +01:00
Mikaël Capelle 775d764950 Fix translations. 2021-02-06 19:28:26 +01:00
Mikaël Capelle 538a0f73e9 Make placeholder text not-translatable. 2021-02-03 18:35:19 +01:00
Mikaël Capelle ecefc5eb8a Fix FOMOD/OMOD detection. 2020-11-27 20:30:06 +01:00
Mikaël Capelle b9653b8322 Merge pull request #1 from ModOrganizer2/cmake-common
Add CMakeLists for MO2 build.
2020-11-25 17:51:27 +01:00
Mikaël Capelle b39be8c295 Add CMakeLists for MO2 build. 2020-11-23 10:56:59 +01:00
Mikaël Capelle 28deb0ce25 Fix release script. 2020-11-23 09:15:55 +01:00
Mikaël Capelle 1cf557a33c Bump version to 1.0.2. 2020-11-17 22:08:58 +01:00
Mikaël Capelle 753871d62a Fix getPluginStatus. 2020-11-17 22:05:56 +01:00
Mikaël Capelle 8b49e0d3cd Do not move non-enabled ESPs to optional. 2020-11-17 19:38:22 +01:00
Mikaël Capelle 52e20cf676 Update mobase stubs in setup.cfg. 2020-11-15 21:53:05 +01:00
Mikaël Capelle b695baaf1d Bump version to 1.0.1. 2020-11-15 21:42:54 +01:00
Mikaël Capelle c0104b0951 Remember previous install options. Minor fixes. 2020-11-15 21:40:50 +01:00
Mikaël Capelle 759291e6d4 Fix missing import. 2020-11-15 19:48:35 +01:00
Mikaël Capelle 83d39ee762 Handle OBSE INI tweaks. 2020-11-15 19:47:33 +01:00
Mikaël Capelle c1385c7b96 Hide previous pixmap in select options. 2020-11-15 16:46:07 +01:00
Mikaël Capelle a6dadf39eb Remove unused imports. Fix strings. 2020-11-09 22:37:22 +01:00
Mikaël Capelle c2b6d8da43 More Tweaks stuff. 2020-11-09 22:35:25 +01:00
Mikaël Capelle 4fe5299a42 Fix context when going back. 2020-11-09 21:47:05 +01:00
Mikaël Capelle 88ac3075f0 Display INI Tweaks at the end. 2020-11-09 21:46:54 +01:00
Mikaël Capelle a5e896ff1b Allow subpackages containing INI tweaks or docs. 2020-11-09 21:46:07 +01:00
Mikaël Capelle a3722f900b Double-click on item go to next page for SelectOne. 2020-11-09 13:12:24 +01:00
Mikaël Capelle d3df91babb Fix installer complete page. 2020-11-09 13:03:53 +01:00
Mikaël Capelle cdc2f14347 Add prefer_fomod and prefer_omod settings. 2020-11-09 12:32:10 +01:00
Mikaël Capelle 29e4cad187 Fix mypy for CI. 2020-11-09 11:06:48 +01:00
Mikaël Capelle ec01ca3e92 Update README and scripts. 2020-11-09 11:04:07 +01:00
19 changed files with 904 additions and 251 deletions
+24
View File
@@ -0,0 +1,24 @@
name: linters
on: [push, pull_request]
jobs:
checks:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- name: Test with tox
run: tox -e py38-lint
+7
View File
@@ -8,5 +8,12 @@ src/ui/*.py
.tox
.vscode
# MO2 build files:
vsbuild
mob.log
# Generated files:
lib
src/lib
installer_wizard
installer_wizard-*.zip
+13
View File
@@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.16)
project(installer_wizard LANGUAGES NONE)
set(project_type python_module_plugin)
set(enable_warnings OFF)
if(DEFINED DEPENDENCIES_DIR)
include(${DEPENDENCIES_DIR}/modorganizer_super/cmake_common/project.cmake)
else()
include(../cmake_common/project.cmake)
endif()
add_subdirectory(src)
+67 -2
View File
@@ -2,9 +2,74 @@
This plugin can be used to install BAIN archives containing a wizard script (`wizard.txt`).
## How to use?
## How to install?
Go to the releases page and download the latest release for your MO2 version.
Go to the [releases page](https://github.com/Holt59/modorganizer-installer_wizard/releases) and download
the latest release for your MO2 version.
### A few words on INI Tweaks
Mod Organizer 2 does not currently manage INI Tweaks, so the Wizard installer is partially functional
regarding them.
- The installer will create proper INI Tweaks when requested, but these will not be applied
to the game INI files automatically. If INI Tweaks are present, a pop-up should appear at
the end of the installation.
- INI Tweaks for OBSE script are directly applied to the OBSE scripts.
## How to contribute?
### Setting-up the environment
Below are the steps to setup a development environment.
1. Clone this repository into the Mod Organizer 2 plugins folder.
```bash
# (Optional) you can change the name of the folder:
git clone https://github.com/Holt59/modorganizer-installer_wizard installer_wizard
```
2. **Requirements:** You need a Python 3.8 installation. The list of requirements is in
[`requirements.txt`](requirements.txt):
```bash
# Those are only the development requirements.
pip install -r requirements
```
3. "Build" the installer:
```bash
# This will install the 3rd party libraries in src/lib (required for the installer) and convert the .ui files into .py files.
make.ps1
```
4. Create a root `__init__.py` - MO2 will not find and load the plugin unless there is a
`__init__.py` file in the root of the folder, so you need to create one:
```python
from .src import createPlugin
```
### Opening a Pull-Request
Once you are satisfied with your changes, you can
[open a pull-request](https://github.com/Holt59/modorganizer-installer_wizard/pulls).
Before doing so, you should check that your code is properly
formatted and clean:
```bash
# The -vv option is mandatory, otherwise tox will crash...
tox -vv -e py38-lint
```
### The interpreter
The interpreter used by the installer is from the
[`bain-wizard-interpreter`](https://github.com/Holt59/bain-wizard-interpreter) package.
For issues related to interpreter (i.e. the script is wrongly parsed), open issues on the interpreter repository.
## License
+3 -2
View File
@@ -6,8 +6,9 @@ $target = ".\installer_wizard\"
Remove-Item -Recurse -Force -ErrorAction Ignore $target
New-Item -Path $target -Type Directory | Out-Null
Copy-Item -Recurse -Path .\src\* -Exclude "*.ui", "__pycache__" -Destination .\installer_wizard
Copy-Item .\installer_wizard_en.ts .\README.md .\LICENSE $target
Copy-Item -Recurse -Path .\src\* -Exclude "*.ui" -Destination $target
Get-ChildItem -Recurse $target -Include "__pycache__" | Remove-Item -Recurse -Force
Copy-Item .\installer_wizard_en.ts, .\README.md, .\LICENSE $target
# Find the version:
$ctx = Get-Content .\src\installer.py | Select-String -Pattern "def version\(self\):" -Context 0, 1
+2 -2
View File
@@ -1,5 +1,5 @@
# Install the lib:
pip install --target=.\src\lib --upgrade git+https://github.com/Holt59/bain-wizard-interpreter | Out-Null
pip install --target=.\src\lib --upgrade -r .\plugin-requirements.txt | Out-Null
# Convert ui files:
Get-ChildItem -Recurse -File -Include "*.ui" | ForEach-Object {
@@ -7,4 +7,4 @@ Get-ChildItem -Recurse -File -Include "*.ui" | ForEach-Object {
}
# Generate the .ts file:
pylupdate5 (Get-ChildItem -Exclude lib | Get-ChildItem -Recurse -File -Include "*.py") -ts installer_wizard_en.ts
pylupdate5 (Get-ChildItem src -Exclude lib | Get-ChildItem -Recurse -File -Include "*.py") -ts installer_wizard_en.ts
+1
View File
@@ -0,0 +1 @@
git+https://github.com/Holt59/bain-wizard-interpreter
+7
View File
@@ -0,0 +1,7 @@
black
PyQt5
mypy
flake8
flake8-black
mobase-stubs
git+https://github.com/Holt59/bain-wizard-interpreter
+5 -1
View File
@@ -17,6 +17,9 @@ ignore_missing_imports = True
[mypy-.ui.*]
ignore_missing_imports = True
[mypy-src.ui.*]
ignore_missing_imports = True
[mypy-wizard.antlr4.*]
ignore_errors = True
@@ -34,7 +37,8 @@ deps =
mypy
flake8
flake8-black
mobase-stubs
mobase-stubs==2.4.0.a3
git+https://github.com/Holt59/bain-wizard-interpreter
commands =
black src --check --diff --exclude "lib" --exclude "ui"
flake8 src
+7
View File
@@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.16)
if(DEFINED DEPENDENCIES_DIR)
include(${DEPENDENCIES_DIR}/modorganizer_super/cmake_common/src.cmake)
else()
include(../../cmake_common/src.cmake)
endif()
+136 -37
View File
@@ -1,10 +1,11 @@
# -*- encoding: utf-8 -*-
from pathlib import Path
from typing import Any, List, Mapping, Tuple
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QPixmap, QKeySequence
from PyQt5.QtGui import QPixmap, QKeySequence, QFontDatabase
from PyQt5.QtWidgets import QApplication
from PyQt5 import QtWidgets
import mobase
@@ -29,6 +30,9 @@ from wizard.errors import WizardError
from wizard.interpreter import WizardInterpreter
from wizard.manager import SelectOption
from wizard.runner import WizardRunnerState, WizardRunnerKeywordVisitor
from wizard.tweaks import WizardINISetting
from .utils import make_ini_tweaks
WizardRunnerContext = WizardInterpreterContext[WizardRunnerState, Any]
@@ -133,19 +137,25 @@ class WizardInstallerRequiresVersionPage(QtWidgets.QWidget):
class WizardInstallerSelectPage(QtWidgets.QWidget):
_context: WizardRunnerContext
# Signal emitted when an item is double-clicked, only for SelectOne
# context:
itemDoubleClicked = pyqtSignal()
_context: WizardSelectContext
_images: Mapping[Path, Path]
def __init__(
self,
context: WizardSelectContext,
images: Mapping[Path, Path],
options: Optional[List[str]],
parent: QtWidgets.QWidget,
):
"""
Args:
context: The context for this page.
images: A mapping from path (in the archive) to extracted path.
options: Potential list of options to select. Might not exactly match.
parent: The parent widget.
"""
super().__init__(parent)
@@ -159,29 +169,43 @@ class WizardInstallerSelectPage(QtWidgets.QWidget):
self.ui.optionList.currentItemChanged.connect(self.onCurrentItemChanged)
# Create list item widgets:
options = context.options
for option in options:
for option in context.options:
item = QtWidgets.QListWidgetItem()
self.ui.optionList.addItem(item)
self.update_context(context)
# Extract previous select options:
previous_options = []
if options:
previous_options = [
option for option in context.options if option.name in options
]
else:
if isinstance(context, WizardSelectManyContext):
previous_options = context.defaults
elif isinstance(context, WizardSelectOneContext):
previous_options = [context.default]
# Set the default values:
for i, option in enumerate(options):
for i, option in enumerate(context.options):
item = self.ui.optionList.item(i)
if isinstance(context, WizardSelectManyContext):
item.setFlags(item.flags() | Qt.ItemIsUserCheckable) # type: ignore
if option in context.defaults:
if option in previous_options:
item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
elif (
isinstance(context, WizardSelectOneContext)
and option is context.default
and option in previous_options
):
item.setSelected(True)
self.ui.optionList.setCurrentItem(item)
if isinstance(context, WizardSelectOneContext):
self.ui.optionList.doubleClicked.connect(self.itemDoubleClicked.emit)
def update_context(self, context: WizardSelectContext):
self._context = context
@@ -189,10 +213,6 @@ class WizardInstallerSelectPage(QtWidgets.QWidget):
options = self._context.options
assert len(options) == self.ui.optionList.count()
self.ui.selectDescriptionFrame.setStyleSheet(
# "QFrame { border: 1px solid red; }"
"QFrame { border-color: red; }"
)
self.ui.selectDescriptionLabel.setText(context.description)
self.ui.selectDescriptionLabel.setMargin(4)
@@ -216,20 +236,24 @@ class WizardInstallerSelectPage(QtWidgets.QWidget):
target = self._images[Path(image)]
self.ui.imageLabel.setPixmap(QPixmap(target.as_posix()))
else:
self.ui.imageLabel.setText("")
self.ui.imageLabel.setPixmap(QPixmap())
def selected(self) -> WizardSelectContext:
def selectedOptions(self) -> List[SelectOption]:
options = []
if isinstance(self._context, WizardSelectOneContext):
return self._context.select(
self.ui.optionList.currentItem().data(Qt.UserRole)
)
elif isinstance(self._context, WizardSelectManyContext):
options = []
options.append(self.ui.optionList.currentItem().data(Qt.UserRole))
else:
for i in range(self.ui.optionList.count()):
item = self.ui.optionList.item(i)
if item.checkState() == Qt.Checked:
options.append(item.data(Qt.UserRole))
return self._context.select(options)
return options
def selected(self) -> WizardSelectContext:
if isinstance(self._context, WizardSelectOneContext):
return self._context.select(self.selectedOptions()[0])
elif isinstance(self._context, WizardSelectManyContext):
return self._context.select(self.selectedOptions())
else:
return self._context # type: ignore
@@ -255,36 +279,57 @@ class WizardInstallerCompletePage(QtWidgets.QWidget):
kvisitor: WizardRunnerKeywordVisitor = context.factory.kvisitor # type: ignore
# The list of plugins in selected sub-packages:
plugins: List[str] = []
plugins: Set[str] = set()
# SubPackages:
for sp in kvisitor.subpackages:
item = QtWidgets.QListWidgetItem()
item.setText(sp.name)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable) # type: ignore
if sp.name in self.state.subpackages:
item.setCheckState(Qt.Checked)
plugins.extend(kvisitor.plugins_for(sp))
else:
item.setCheckState(Qt.Unchecked)
item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) # type: ignore
plugins.update(kvisitor.plugins_for(sp))
self.ui.subpackagesList.addItem(item)
# Switch the renamed plugins:
for plugin in list(plugins):
if plugin in self.state.renames:
plugins.remove(plugin)
plugins.add(self.state.renames[plugin])
# Plugins:
for plugin in sorted(plugins):
item = QtWidgets.QListWidgetItem()
item.setText(plugin)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable) # type: ignore
if plugin in self.state.plugins:
item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
item.setFlags(item.flags() & ~Qt.ItemIsUserCheckable) # type: ignore
self.ui.pluginsList.addItem(item)
# INI Tweaks:
self.ui.tweaksWidget.setVisible(bool(self.state.tweaks))
self.ui.tweaksList.currentItemChanged.connect(self.onCurrentTweakItemChanged)
if self.state.tweaks:
# TODO:
pass
# Group the tweaks per file:
tweaks = {
file: self.state.tweaks.tweaks(file)
for file in self.state.tweaks.files()
}
for file, ftweaks in tweaks.items():
item = QtWidgets.QListWidgetItem()
item.setText(file.replace("\\", "/"))
item.setData(Qt.UserRole, ftweaks)
self.ui.tweaksList.addItem(item)
self.ui.tweaksTextEdit.setFont(
QFontDatabase.systemFont(QFontDatabase.FixedFont)
)
self.ui.tweaksList.setCurrentRow(0)
# Notes:
md = ""
@@ -293,6 +338,15 @@ class WizardInstallerCompletePage(QtWidgets.QWidget):
self.ui.notesTextEdit.document().setIndentWidth(10)
self.ui.notesTextEdit.setMarkdown(md)
def onCurrentTweakItemChanged(
self, current: QtWidgets.QListWidgetItem, previous: QtWidgets.QListWidgetItem
):
# Clear text area and create the tweaks:
self.ui.tweaksTextEdit.clear()
self.ui.tweaksTextEdit.appendPlainText(
make_ini_tweaks(current.data(Qt.UserRole))
)
def subpackages(self) -> List[str]:
"""
Returns:
@@ -306,19 +360,30 @@ class WizardInstallerCompletePage(QtWidgets.QWidget):
sp.append(item.text())
return sp
def plugins(self) -> List[str]:
def plugins(self) -> Dict[str, bool]:
"""
Returns:
The list of plugins selected in the UI (either automatically by the
interpreter or by the user).
"""
sp: List[str] = []
sp: Dict[str, bool] = {}
for i in range(self.ui.pluginsList.count()):
item = self.ui.pluginsList.item(i)
if item.checkState() == Qt.Checked:
sp.append(item.text())
sp[item.text()] = item.checkState() == Qt.Checked
return sp
def tweaks(self) -> Mapping[str, List[WizardINISetting]]:
"""
Returns:
The list of tweaks created by the wizard. The returned value maps filenames
to INI tweaks.
"""
rets = {}
for i in range(self.ui.tweaksList.count()):
item = self.ui.tweaksList.item(i)
rets[item.text()] = item.data(Qt.UserRole)
return rets
class WizardInstallerCancelPage(QtWidgets.QWidget):
def __init__(
@@ -379,6 +444,7 @@ class WizardInstallerDialog(QtWidgets.QDialog):
# The interpreter:
_interpreter: WizardInterpreter
_images: Mapping[Path, Path]
_options: Mapping[str, List[str]]
# The Wizard MO2 interface:
_start_context: WizardTopLevelContext
@@ -393,6 +459,7 @@ class WizardInstallerDialog(QtWidgets.QDialog):
context: WizardTopLevelContext[WizardRunnerState],
name: mobase.GuessedString,
images: Mapping[Path, Path],
options: Mapping[str, List[str]],
parent: QtWidgets.QWidget,
):
"""
@@ -401,6 +468,7 @@ class WizardInstallerDialog(QtWidgets.QDialog):
context: The initial context of the script.
name: The name of the mod.
images: A mapping from path (in the archive) to extracted path.
options: The previously selected options.
parent: The parent widget.
"""
super().__init__(parent)
@@ -408,6 +476,7 @@ class WizardInstallerDialog(QtWidgets.QDialog):
self._organizer = organizer
self._interpreter = interpreter
self._images = images
self._options = options
self._start_context = context
self._pages = {}
@@ -461,7 +530,7 @@ class WizardInstallerDialog(QtWidgets.QDialog):
assert isinstance(widget, WizardInstallerCompletePage)
return widget.subpackages()
def plugins(self):
def plugins(self) -> Dict[str, bool]:
"""
Returns:
The list of plugins to install and enable. Only valid if exec() returned
@@ -482,6 +551,29 @@ class WizardInstallerDialog(QtWidgets.QDialog):
assert isinstance(widget, WizardInstallerCompletePage)
return widget.state.renames
def tweaks(self) -> Mapping[str, List[WizardINISetting]]:
"""
Returns:
The list of tweaks per file. Only valid if exec() returned Accepted.
"""
widget = self.ui.stackedWidget.currentWidget()
assert isinstance(widget, WizardInstallerCompletePage)
return widget.tweaks()
def selectedOptions(self) -> Mapping[str, List[str]]:
"""
Returns:
The list of all currently selected options.
"""
result = {}
for i in range(self.ui.stackedWidget.count()):
page = self.ui.stackedWidget.widget(i)
if isinstance(page, WizardInstallerSelectPage):
result[page._context.description] = [
option.name for option in page.selectedOptions()
]
return result
def isManualRequested(self):
return self._manual
@@ -510,6 +602,7 @@ class WizardInstallerDialog(QtWidgets.QDialog):
if context.context in self._pages:
page = self._pages[context.context]
page.update_context(context)
else:
page = self._make_page(context)
@@ -537,13 +630,13 @@ class WizardInstallerDialog(QtWidgets.QDialog):
name: str = self.ui.nextBtn.text()
if isinstance(widget, WizardInstallerSelectPage):
name = self._tr("Next")
name = self.__tr("Next")
elif isinstance(widget, WizardInstallerRequiresVersionPage):
name = self._tr("Install anyway")
name = self.__tr("Install anyway")
elif isinstance(widget, (WizardInstallerCancelPage, WizardInstallerErrorPage)):
self.ui.nextBtn.setDisabled(True)
else:
name = self._tr("Install")
name = self.__tr("Install")
self.ui.nextBtn.setText(name)
@@ -566,7 +659,13 @@ class WizardInstallerDialog(QtWidgets.QDialog):
def _make_page(self, context: WizardRunnerContext) -> QtWidgets.QWidget:
page: QtWidgets.QWidget
if isinstance(context, WizardSelectContext):
page = WizardInstallerSelectPage(context, self._images, self)
page = WizardInstallerSelectPage(
context,
self._images,
self._options.get(context.description, None),
self,
)
page.itemDoubleClicked.connect(self.nextClicked)
self._pages[context.context] = page # type: ignore
elif isinstance(context, WizardRequireVersionsContext):
page = WizardInstallerRequiresVersionPage(context, self._organizer, self)
@@ -590,5 +689,5 @@ class WizardInstallerDialog(QtWidgets.QDialog):
self._update_focus()
return super().exec()
def _tr(self, txt: str) -> str:
return QtWidgets.QApplication.translate("WizardInstaller", txt)
def __tr(self, str):
return QApplication.translate("WizardInstallerDialog", str)
+152 -40
View File
@@ -1,10 +1,12 @@
# -*- encoding: utf-8 -*-
import re
import os
import sys
from collections import defaultdict
from pathlib import Path
from typing import Optional, List, Union
from typing import Dict, Optional, List, Union
# MO2 ships with PyQt5, so you can use it in your plugins:
from PyQt5.QtWidgets import QApplication
@@ -16,6 +18,7 @@ from wizard.runner import WizardRunnerState
from .dialog import WizardInstallerDialog
from .runner import make_interpreter
from .utils import make_ini_tweaks, merge_ini_tweaks
class WizardInstaller(mobase.IPluginInstallerSimple):
@@ -27,8 +30,16 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
it is valid (for this installer) and then modify it if required before extraction.
"""
# Regex used to parse settings:
RE_DESCRIPTION = re.compile(r"select([0-9]+)-description")
RE_OPTION = re.compile(r"select([0-9]+)-option([0-9]+)")
_organizer: mobase.IOrganizer
# List of selected options:
_installerOptions: Dict[str, List[str]]
_installerUsed: bool
def __init__(self):
super().__init__()
@@ -42,14 +53,17 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
def name(self):
return "BAIN Wizard Installer"
def localizedName(self) -> str:
return self.__tr("BAIN Wizard Installer")
def author(self):
return "Holt59"
def description(self):
return self._tr("Installer for BAIN archive containing wizard scripts.")
return self.__tr("Installer for BAIN archive containing wizard scripts.")
def version(self):
return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.BETA)
return mobase.VersionInfo(1, 0, 2)
def isActive(self):
return self._organizer.pluginSetting(self.name(), "enabled")
@@ -57,9 +71,18 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
def settings(self):
return [
mobase.PluginSetting("enabled", "check to enable this plugin", True),
# TODO: I think this should be below FOMOD and above OMOD but this is
# currently not possible since OMOD has a priority of 500...
mobase.PluginSetting("priority", "priority of this installer", 80),
mobase.PluginSetting(
"prefer_fomod",
"prefer FOMOD installer over this one when possible",
True,
),
mobase.PluginSetting(
"prefer_omod",
"prefer OMOD installer over this one when possible",
False,
),
# Above FOMOD:
mobase.PluginSetting("priority", "priority of this installer", 120),
]
# Method for IPluginInstallerSimple:
@@ -73,14 +96,51 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
def onInstallationStart(
self, archive: str, reinstallation: bool, mod: Optional[mobase.IModInterface]
):
# TODO: Reload choices.
pass
self._installerUsed = False
self._installerOptions = {}
if mod:
settings = mod.pluginSettings(self.name())
# First extract the description:
descriptions: Dict[int, str] = {}
options: Dict[int, Dict[int, str]] = defaultdict(dict)
for setting, value in settings.items():
mdesc = WizardInstaller.RE_DESCRIPTION.match(setting)
if mdesc:
select = int(mdesc.group(1))
descriptions[select] = str(value)
mopt = WizardInstaller.RE_OPTION.match(setting)
if mopt:
select = int(mopt.group(1))
index = int(mopt.group(2))
options[select][index] = str(value)
for kdesc, desc in descriptions.items():
self._installerOptions[desc] = []
if kdesc in options:
for index in sorted(options[kdesc].keys()):
self._installerOptions[desc].append(options[kdesc][index])
def onInstallationEnd(
self, result: mobase.InstallResult, mod: Optional[mobase.IModInterface]
):
# TODO: Save choices.
pass
if result != mobase.InstallResult.SUCCESS or not self._installerUsed or not mod:
return
mod.clearPluginSettings(self.name())
for i, desc in enumerate(self._installerOptions):
mod.setPluginSetting(self.name(), f"select{i}-description", desc)
for iopt, opt in enumerate(self._installerOptions[desc]):
mod.setPluginSetting(self.name(), f"select{i}-option{iopt}", opt)
def _hasFomodInstaller(self) -> bool:
# Do not consider the NCC installer.
return self._organizer.isPluginEnabled("Fomod Installer")
def _hasOmodInstaller(self) -> bool:
return self._organizer.isPluginEnabled("Omod Installer")
def _getWizardArchiveBase(
self, tree: mobase.IFileTree, data_name: str, checker: mobase.ModDataChecker
@@ -107,17 +167,17 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
return None
def _getImageEntries(
def _getEntriesToExtract(
self,
tree: mobase.IFileTree,
extensions: List[str] = ["png", "jpg", "jpeg", "gif", "bmp"],
extensions: List[str] = ["png", "jpg", "jpeg", "gif", "bmp", "ini"],
) -> List[mobase.FileTreeEntry]:
"""
Retrieve all the entries corresponding to images in the given tree.
Retrieve all the entries to extract from the given tree.
Args:
tree: The tree containing the images.
extensions: The extensions for images.
tree: The tree.
extensions: The extensions of files.
Returns:
A list of entries corresponding to files with the given extensions.
@@ -153,9 +213,24 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
mobase.ModDataChecker # type: ignore
)
# Do the actual check - Convert to bool or do `is not None` to avoid
# issue with Python > C++ conversion:
return bool(self._getWizardArchiveBase(tree, data_name, checker))
# Retrieve the base:
base = self._getWizardArchiveBase(tree, data_name, checker)
if not base:
return False
# Check FOMOD:
fomod = base.exists("fomod/ModuleConfig.xml")
if (
fomod
and self._hasFomodInstaller()
and self._organizer.pluginSetting(self.name(), "prefer_fomod")
):
return False
# TODO: Check OMOD?
return True
def install(
self,
@@ -199,12 +274,11 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
if wizard is None:
return mobase.InstallResult.NOT_ATTEMPTED
images = self._getImageEntries(otree)
to_extract = self._getEntriesToExtract(otree)
# Extract the script:
# TODO: Extract pictures, similar to the FOMOD installer.
paths = self._manager().extractFiles([wizard] + images, silent=False)
if len(paths) != len(images) + 1:
paths = self._manager().extractFiles([wizard] + to_extract, silent=False)
if len(paths) != len(to_extract) + 1:
return mobase.InstallResult.FAILED
interpreter = make_interpreter(base, self._organizer)
@@ -216,11 +290,16 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
interpreter,
interpreter.make_top_level_context(Path(script), WizardRunnerState()),
name,
{Path(entry.path()): Path(path) for entry, path in zip(images, paths[1:])},
{
Path(entry.path()): Path(path)
for entry, path in zip(to_extract, paths[1:])
if not path.endswith(".ini")
},
self._installerOptions,
self._parentWidget(),
)
dialog.scriptButtonClicked.connect(lambda: os.startfile(script))
dialog.scriptButtonClicked.connect(lambda: os.startfile(script)) # type: ignore
# Note: Unlike the official installer, we do not have a "silent" setting,
# but it is really simple to add it.
@@ -245,21 +324,6 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
tree.merge(entry)
# Move all non-selected plugins to "optional":
optionals: List[mobase.FileTreeEntry] = []
for entry in tree:
if entry.isFile() and (
entry.hasSuffix(["esl", "esp", "esm"])
and entry.name() not in dialog.plugins()
):
optionals.append(entry)
# Do not keep empty optional folder:
if optionals:
optional = tree.addDirectory("optional")
for entry in optionals:
optional.insert(entry)
# Handle renames:
for original, new in dialog.renames().items():
# Entry should be at the root:
@@ -271,6 +335,54 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
tree.move(entry, new)
# Move not selected plugins to optional:
for plugin, enabled in dialog.plugins().items():
if not enabled:
entry = tree.find(plugin)
if not entry:
continue # silently fail since the plugin should be disabled
tree.addDirectory("optional").insert(entry)
# TODO: INI Tweaks:
alltweaks = dialog.tweaks()
for filename, tweaks in alltweaks.items():
# Find the original file (if any):
o_entry = tree.find(filename)
o_filename: Optional[str] = None
if o_entry:
# Find the filepath from the list of extracted files:
index = to_extract.index(o_entry)
# +1 because the first one is the script.
o_filename = paths[index + 1]
# If the file existed before, we keep the new one at the same
# place:
if o_entry or Path(filename).parts[0].lower() == "ini tweaks":
entry = tree.addFile(filename, replace_if_exists=True)
# Otherwise we create it in INI Tweaks
else:
entry = tree.addFile(
os.path.join("INI Tweaks", filename), replace_if_exists=True
)
filepath = self._manager().createFile(entry)
if not o_filename:
data = make_ini_tweaks(tweaks)
else:
data = merge_ini_tweaks(tweaks, Path(o_filename))
with open(filepath, "w") as fp:
fp.write(data)
# Mark stuff for saving:
self._installerUsed = True
self._installerOptions = dict(dialog.selectedOptions())
# Return the tree:
return tree
@@ -284,7 +396,7 @@ class WizardInstaller(mobase.IPluginInstallerSimple):
else:
return mobase.InstallResult.CANCELED
def _tr(self, str):
def __tr(self, str) -> str:
# We need this to translate string in Python. Check the common documentation
# for more details:
return QApplication.translate("WizardInstaller", str)
@@ -1,34 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS><TS version="2.0">
<context>
<name>WizardInstallerComplete</name>
<name>WizardInstaller</name>
<message>
<location filename="src/ui/wizardinstallercomplete.py" line="92"/>
<source>Form</source>
<location filename="installer.py" line="63"/>
<source>Installer for BAIN archive containing wizard scripts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallercomplete.py" line="93"/>
<location filename="installer.py" line="57"/>
<source>BAIN Wizard Installer</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WizardInstallerComplete</name>
<message>
<location filename="ui/wizardinstallercomplete.ui" line="40"/>
<source>The installer script has finished. The following sub-packages and plugins will be installed.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallercomplete.py" line="94"/>
<location filename="ui/wizardinstallercomplete.ui" line="111"/>
<source>Sub-Packages:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallercomplete.py" line="95"/>
<location filename="ui/wizardinstallercomplete.ui" line="128"/>
<source>Plugins:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallercomplete.py" line="96"/>
<location filename="ui/wizardinstallercomplete.ui" line="180"/>
<source>INI Tweaks:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallercomplete.py" line="97"/>
<location filename="ui/wizardinstallercomplete.ui" line="236"/>
<source>Notes:</source>
<translation type="unfinished"></translation>
</message>
@@ -36,73 +44,60 @@
<context>
<name>WizardInstallerDialog</name>
<message>
<location filename="src/ui/wizardinstallerdialog.py" line="75"/>
<source>BAIN Wizard Installer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerdialog.py" line="76"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerdialog.py" line="77"/>
<source>Manual</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerdialog.py" line="78"/>
<source>Script</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerdialog.py" line="79"/>
<source>Back</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerdialog.py" line="80"/>
<location filename="ui/wizardinstallerdialog.ui" line="102"/>
<source>Next</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerdialog.py" line="81"/>
<location filename="dialog.py" line="635"/>
<source>Install anyway</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="dialog.py" line="639"/>
<source>Install</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/wizardinstallerdialog.ui" line="14"/>
<source>BAIN Wizard Installer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/wizardinstallerdialog.ui" line="25"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/wizardinstallerdialog.ui" line="56"/>
<source>Manual</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/wizardinstallerdialog.ui" line="66"/>
<source>Script</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/wizardinstallerdialog.ui" line="92"/>
<source>Back</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/wizardinstallerdialog.ui" line="109"/>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WizardInstallerError</name>
<message>
<location filename="src/ui/wizardinstallererror.py" line="52"/>
<source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallererror.py" line="54"/>
<source>TextLabel</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WizardInstallerPage</name>
<message>
<location filename="src/ui/wizardinstallerpage.py" line="91"/>
<source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerpage.py" line="92"/>
<source>TextLabel</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerpage.py" line="93"/>
<location filename="ui/wizardinstallerpage.ui" line="114"/>
<source>Options:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerpage.py" line="94"/>
<location filename="ui/wizardinstallerpage.ui" line="148"/>
<source>Description:</source>
<translation type="unfinished"></translation>
</message>
@@ -110,52 +105,47 @@
<context>
<name>WizardInstallerRequires</name>
<message>
<location filename="src/ui/wizardinstallerrequires.py" line="113"/>
<source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerrequires.py" line="114"/>
<location filename="ui/wizardinstallerrequires.ui" line="34"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Warning:&lt;/span&gt; The following version requirements are not met for using this installer.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerrequires.py" line="115"/>
<location filename="ui/wizardinstallerrequires.ui" line="44"/>
<source>Requirements</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerrequires.py" line="116"/>
<location filename="ui/wizardinstallerrequires.ui" line="52"/>
<source>Graphics Extender</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerrequires.py" line="117"/>
<location filename="ui/wizardinstallerrequires.ui" line="76"/>
<source>Wrye Bash</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerrequires.py" line="118"/>
<location filename="ui/wizardinstallerrequires.ui" line="86"/>
<source>Need</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerrequires.py" line="119"/>
<location filename="ui/wizardinstallerrequires.ui" line="103"/>
<source>Game</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerrequires.py" line="122"/>
<location filename="ui/wizardinstallerrequires.ui" line="151"/>
<source>N/A</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerrequires.py" line="121"/>
<location filename="ui/wizardinstallerrequires.ui" line="141"/>
<source>Have</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/ui/wizardinstallerrequires.py" line="123"/>
<location filename="ui/wizardinstallerrequires.ui" line="165"/>
<source>Script Extender</source>
<translation type="unfinished"></translation>
</message>
+24 -5
View File
@@ -73,6 +73,21 @@ class MO2ManagerModInterface(ManagerModInterface):
if checker:
if checker.dataLooksValid(entry) == mobase.ModDataChecker.VALID:
self._subpackages.append(MO2SubPackage(entry))
continue
# Add entry with INI tweaks:
if entry.exists("INI Tweaks") or entry.exists("INI"):
self._subpackages.append(MO2SubPackage(entry))
continue
# We add folder with format "XXX Docs" where "XXX" is a number.
parts = entry.name().split()
if (
len(parts) >= 2
and parts[0].isdigit()
and parts[1].lower().startswith("doc")
):
self._subpackages.append(MO2SubPackage(entry))
@property
def subpackages(self) -> SubPackages:
@@ -129,7 +144,11 @@ class MO2ManagerModInterface(ManagerModInterface):
path = None
else:
path = Path(filepath)
files = self._organizer.findFiles(path.parent.as_posix(), path.name)
parent = path.parent.as_posix()
if parent == ".":
parent = ""
files = self._organizer.findFiles(parent, "*" + path.name)
if files:
path = Path(files[0])
else:
@@ -146,11 +165,11 @@ class MO2ManagerModInterface(ManagerModInterface):
def getPluginStatus(self, filename) -> int:
state = self._organizer.pluginList().state(filename)
if state & mobase.PluginState.MISSING:
return -1
if state & mobase.PluginState.INACTIVE:
if state == mobase.PluginState.ACTIVE:
return 2
if state == mobase.PluginState.INACTIVE:
return 0 # Or 1?
return 2
return -1
def getFilename(self, filepath: str) -> str:
path = self._resolve(filepath)
+203 -82
View File
@@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string notr="true"/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
@@ -30,52 +30,32 @@
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>The installer script has finished. The following sub-packages and plugins will be installed.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Sub-Packages:</string>
</property>
<property name="heading" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="subpackagesList"/>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Plugins:</string>
</property>
<property name="heading" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="pluginsList"/>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QWidget" name="tweaksWidget" native="true">
<layout class="QVBoxLayout" name="verticalLayout_7">
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_11">
<property name="leftMargin">
<number>0</number>
</property>
@@ -89,58 +69,199 @@
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLabel" name="tweaksLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="lineWidth">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QWidget" name="packagesWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>15</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<property name="leftMargin">
<number>0</number>
</property>
<property name="text">
<string>INI Tweaks:</string>
<property name="topMargin">
<number>0</number>
</property>
<property name="heading" stdset="0">
<bool>true</bool>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="tweaksLayout">
<item>
<widget class="QPlainTextEdit" name="tweaksTextEdit1"/>
</item>
<item>
<widget class="QPlainTextEdit" name="tweaksTextEdit2"/>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Sub-Packages:</string>
</property>
<property name="heading" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="subpackagesList"/>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Plugins:</string>
</property>
<property name="heading" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="pluginsList"/>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tweaksWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLabel" name="tweaksLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="text">
<string>INI Tweaks:</string>
</property>
<property name="heading" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="tweaksLayout">
<item>
<widget class="QListWidget" name="tweaksList"/>
</item>
<item>
<widget class="QPlainTextEdit" name="tweaksTextEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="notesWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_10">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QLabel" name="notesLabel">
<property name="text">
<string>Notes:</string>
</property>
<property name="heading" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="notesTextEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="notesLabel">
<property name="text">
<string>Notes:</string>
</property>
<property name="heading" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="notesTextEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
+13 -3
View File
@@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string notr="true"/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
@@ -45,7 +45,7 @@
</size>
</property>
<property name="text">
<string>TextLabel</string>
<string notr="true"/>
</property>
</widget>
</item>
@@ -68,7 +68,7 @@
<item>
<widget class="QLabel" name="titleLabel">
<property name="text">
<string>TextLabel</string>
<string notr="true"/>
</property>
<property name="wordWrap">
<bool>true</bool>
@@ -82,6 +82,16 @@
<property name="readOnly">
<bool>true</bool>
</property>
<property name="markdown">
<string notr="true"/>
</property>
<property name="html">
<string notr="true">&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
+2 -2
View File
@@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string notr="true"/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
@@ -93,7 +93,7 @@
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string>TextLabel</string>
<string notr="true"/>
</property>
<property name="wordWrap">
<bool>true</bool>
+1 -1
View File
@@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string notr="true"/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
+173
View File
@@ -0,0 +1,173 @@
# -*- encoding: utf-8 -*-
import re
from pathlib import Path
from typing import Dict, List, Mapping
from wizard.tweaks import WizardINISetting, WizardINISettingEdit
def make_obscript_ini_tweaks(tweaks: List[WizardINISetting]) -> str:
lines = []
for tweak in tweaks:
if not isinstance(tweak, WizardINISettingEdit):
continue
line = f"{tweak.section} {tweak.setting} to {tweak.value}"
if tweak.comment:
line += f" ; {tweak.comment}"
lines.append(line)
return "\n".join(["; Generated by Mod Organizer 2 via Wizard"] + sorted(lines))
def make_standard_ini_tweaks(tweaks: List[WizardINISetting]) -> str:
# Group Tweaks per section:
sections: Mapping[str, List[WizardINISetting]] = {m.section: [] for m in tweaks}
for m in tweaks:
sections[m.section].append(m)
lines = []
for k in sorted(sections):
s_tweaks = sorted(sections[k], key=lambda m: m.setting)
lines.append(f"[{k}]")
for tw in s_tweaks:
if isinstance(tw, WizardINISettingEdit):
line = f"{tw.setting} = {tw.value}"
if tw.comment:
line += f" # {tw.comment}"
else:
line = f"# {tw.setting} - disabled"
lines.append(line)
lines.append("\n")
return "\n".join(lines[:-1])
def merge_standard_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str:
import sys
print(f"Cannot merge INI Tweaks for {file.name}.", file=sys.stderr)
return make_standard_ini_tweaks(tweaks)
def merge_obscript_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str:
# This is from Wrye Bash (and the function is inspired from Wrye Bash):
reComment = re.compile(";.*", re.U)
reDeleted = re.compile("" r";-(\w.*?)$", re.U)
reSet = re.compile("" r"\s*set\s+(.+?)\s+to\s+(.*)", re.I | re.U)
reSetGS = re.compile("" r"\s*setGS\s+(.+?)\s+(.*)", re.I | re.U)
reSetNGS = re.compile("" r"\s*SetNumericGameSetting\s+(.+?)\s+(.*)", re.I | re.U)
_regex_tuples = (
(reSet, "set", "set {} to {}"),
(reSetGS, "setgs", "setGS {} {}"),
(reSetNGS, "setnumericgamesetting", "SetNumericGameSetting {} {}"),
)
def _parse_obse_line(line):
for regex, sectionKey, format_string in _regex_tuples:
match = regex.match(line)
if match:
return match, sectionKey, format_string
return None, None, None
# Read the original file:
with open(file, "r") as fp:
olines = fp.readlines()
# Map setting name to value:
settings: Dict[str, Dict[str, WizardINISettingEdit]] = {}
deleted: Dict[str, Dict[str, WizardINISetting]] = {}
for tweak in tweaks:
if isinstance(tweak, WizardINISettingEdit):
if tweak.section not in settings:
settings[tweak.section.lower()] = {}
settings[tweak.section.lower()][tweak.setting] = tweak
else:
if tweak.section not in deleted:
deleted[tweak.section.lower()] = {}
deleted[tweak.section.lower()][tweak.setting] = tweak
# Create the lines:
lines = []
for line in olines:
line = line.rstrip()
maDeleted = reDeleted.match(line)
if maDeleted:
stripped = maDeleted.group(1)
else:
stripped = line
stripped = reComment.sub("", stripped).strip()
match, section_key, format_string = _parse_obse_line(stripped)
if match:
setting = match.group(1)
if section_key in settings and setting in settings[section_key]:
value = settings[section_key][setting].value
line = format_string.format(setting, value)
comment = ""
if settings[section_key][setting].comment:
comment = settings[section_key][setting].comment # type: ignore
comment += " "
comment += f"(set by MO2 via Wizard, was {match.group(2)})"
line = f"{line} ; {comment}"
del settings[section_key][setting]
elif (
not maDeleted
and section_key in deleted
and setting in deleted[section_key]
):
line = f";-{line}"
lines.append(line)
for section in settings.values():
for setting in section.values():
if setting.section.lower() == "set":
line = f"{setting.section} {setting.setting} to {setting.value}"
else:
line = f"{setting.section} {setting.setting} {setting.value}"
if setting.comment:
comment = setting.comment + " (set by MO2 via Wizard)"
else:
comment = "(set by MO2 via Wizard)"
line = f"{line} ; {comment}"
lines.append(line)
return "\n".join(lines)
def make_ini_tweaks(tweaks: List[WizardINISetting]) -> str:
is_set = [
tw.section.lower() in ("set", "setgs", "setnumericgamesetting") for tw in tweaks
]
# Assume there is no mix.
if all(is_set):
return make_obscript_ini_tweaks(tweaks)
else:
return make_standard_ini_tweaks(tweaks)
def merge_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str:
is_set = [
tw.section.lower() in ("set", "setgs", "setnumericgamesetting") for tw in tweaks
]
# Assume there is no mix.
if all(is_set):
return merge_obscript_ini_tweaks(tweaks, file)
else:
return merge_standard_ini_tweaks(tweaks, file)