Extract plugins from settings.

There isn't really any need for these two types to interact. The lua
plugin manager effectively fully owned its properties, it just delegated
all reads and writes to the settings object.

Instead, break the plugin settings out into the plugin manager and
preserve the manager in the Game. This will make it possible to expose
plugin options in the NGW without breaking the game on cancel.
This commit is contained in:
Dan Albert 2023-04-25 21:25:43 -07:00
parent 664efa3ace
commit 77f1706cbb
16 changed files with 113 additions and 107 deletions

View File

@ -117,7 +117,7 @@ class ObjectiveFinder:
if isinstance( if isinstance(
ground_object, IadsBuildingGroundObject ground_object, IadsBuildingGroundObject
) and not self.game.settings.plugin_option("skynetiads"): ) and not self.game.lua_plugin_manager.is_plugin_enabled("skynetiads"):
# Prevent strike targets on IADS Buildings when skynet features # Prevent strike targets on IADS Buildings when skynet features
# are disabled as they do not serve any purpose # are disabled as they do not serve any purpose
continue continue

View File

@ -97,10 +97,12 @@ class Game:
start_date: datetime, start_date: datetime,
start_time: time | None, start_time: time | None,
settings: Settings, settings: Settings,
lua_plugin_manager: LuaPluginManager,
player_budget: float, player_budget: float,
enemy_budget: float, enemy_budget: float,
) -> None: ) -> None:
self.settings = settings self.settings = settings
self.lua_plugin_manager = lua_plugin_manager
self.theater = theater self.theater = theater
self.turn = 0 self.turn = 0
# NB: This is the *start* date. It is never updated. # NB: This is the *start* date. It is never updated.
@ -227,7 +229,13 @@ class Game:
# We need to persist this state so that names generated after game load don't # We need to persist this state so that names generated after game load don't
# conflict with those generated before exit. # conflict with those generated before exit.
naming.namegen = self.name_generator naming.namegen = self.name_generator
LuaPluginManager.load_settings(self.settings)
# The installed plugins may have changed between runs. We need to load the
# current configuration and patch in the options that were previously set.
new_plugin_manager = LuaPluginManager.load()
new_plugin_manager.update_with(self.lua_plugin_manager)
self.lua_plugin_manager = new_plugin_manager
ObjectiveDistanceCache.set_theater(self.theater) ObjectiveDistanceCache.set_theater(self.theater)
self.compute_unculled_zones(GameUpdateEvents()) self.compute_unculled_zones(GameUpdateEvents())
if not game_still_initializing: if not game_still_initializing:

View File

@ -79,14 +79,18 @@ class FlightGroupConfigurator:
if self.flight.flight_type in [ if self.flight.flight_type in [
FlightType.TRANSPORT, FlightType.TRANSPORT,
FlightType.AIR_ASSAULT, FlightType.AIR_ASSAULT,
] and self.game.settings.plugin_option("ctld"): ] and self.game.lua_plugin_manager.is_plugin_enabled("ctld"):
transfer = None transfer = None
if self.flight.flight_type == FlightType.TRANSPORT: if self.flight.flight_type == FlightType.TRANSPORT:
coalition = self.game.coalition_for(player=self.flight.blue) coalition = self.game.coalition_for(player=self.flight.blue)
transfer = coalition.transfers.transfer_for_flight(self.flight) transfer = coalition.transfers.transfer_for_flight(self.flight)
self.mission_data.logistics.append( self.mission_data.logistics.append(
LogisticsGenerator( LogisticsGenerator(
self.flight, self.group, self.mission, self.game.settings, transfer self.flight,
self.group,
self.mission,
self.game.lua_plugin_manager,
transfer,
).generate_logistics() ).generate_logistics()
) )

View File

@ -141,7 +141,7 @@ class FlotGenerator:
# If the option fc3LaserCode is enabled, force all JTAC # If the option fc3LaserCode is enabled, force all JTAC
# laser codes to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs. # laser codes to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs.
# Otherwise use 1688 for the first JTAC, 1687 for the second etc. # Otherwise use 1688 for the first JTAC, 1687 for the second etc.
if self.game.settings.plugins["plugins.ctld.fc3LaserCode"]: if self.game.lua_plugin_manager.is_option_enabled("ctld", "fc3LaserCode"):
code = 1113 code = 1113
else: else:
code = self.laser_code_registry.get_next_laser_code() code = self.laser_code_registry.get_next_laser_code()

View File

@ -8,7 +8,7 @@ from game.ato import Flight
from game.ato.flightplans.airassault import AirAssaultFlightPlan from game.ato.flightplans.airassault import AirAssaultFlightPlan
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.missiondata import CargoInfo, LogisticsInfo from game.missiongenerator.missiondata import CargoInfo, LogisticsInfo
from game.settings.settings import Settings from game.plugins import LuaPluginManager
from game.transfers import TransferOrder from game.transfers import TransferOrder
ZONE_RADIUS = 300 ZONE_RADIUS = 300
@ -21,14 +21,14 @@ class LogisticsGenerator:
flight: Flight, flight: Flight,
group: FlyingGroup[Any], group: FlyingGroup[Any],
mission: Mission, mission: Mission,
settings: Settings, lua_plugin_manager: LuaPluginManager,
transfer: Optional[TransferOrder] = None, transfer: Optional[TransferOrder] = None,
) -> None: ) -> None:
self.flight = flight self.flight = flight
self.group = group self.group = group
self.transfer = transfer self.transfer = transfer
self.mission = mission self.mission = mission
self.settings = settings self.lua_plugin_manager = lua_plugin_manager
def generate_logistics(self) -> LogisticsInfo: def generate_logistics(self) -> LogisticsInfo:
# Add Logisitcs info for the flight # Add Logisitcs info for the flight
@ -89,8 +89,8 @@ class LogisticsGenerator:
for cargo_unit_type, amount in self.transfer.units.items() for cargo_unit_type, amount in self.transfer.units.items()
] ]
if pickup_point is not None and self.settings.plugin_option( if pickup_point is not None and self.lua_plugin_manager.is_option_enabled(
"ctld.logisticunit" "ctld", "logisticunit"
): ):
# Spawn logisticsunit at pickup zones # Spawn logisticsunit at pickup zones
country = self.mission.country(self.flight.country) country = self.mission.country(self.flight.country)

View File

@ -13,7 +13,6 @@ from dcs.triggers import TriggerStart
from game.ato import FlightType from game.ato import FlightType
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.plugins import LuaPluginManager
from game.theater import TheaterGroundObject from game.theater import TheaterGroundObject
from game.theater.iadsnetwork.iadsrole import IadsRole from game.theater.iadsnetwork.iadsrole import IadsRole
from game.utils import escape_string_for_lua from game.utils import escape_string_for_lua
@ -246,7 +245,7 @@ class LuaGenerator:
self.mission.triggerrules.triggers.append(trigger) self.mission.triggerrules.triggers.append(trigger)
def inject_plugins(self) -> None: def inject_plugins(self) -> None:
for plugin in LuaPluginManager.plugins(): for plugin in self.game.lua_plugin_manager.iter_plugins():
if plugin.enabled: if plugin.enabled:
plugin.inject_scripts(self) plugin.inject_scripts(self)
plugin.inject_configuration(self) plugin.inject_configuration(self)

View File

@ -103,7 +103,7 @@ class GroundObjectGenerator:
# Special handling for scenery objects # Special handling for scenery objects
self.add_trigger_zone_for_scenery(unit) self.add_trigger_zone_for_scenery(unit)
if ( if (
self.game.settings.plugin_option("skynetiads") self.game.lua_plugin_manager.is_plugin_enabled("skynetiads")
and self.game.theater.iads_network.advanced_iads and self.game.theater.iads_network.advanced_iads
and isinstance(group, IadsGroundGroup) and isinstance(group, IadsGroundGroup)
and group.iads_role.participate and group.iads_role.participate

View File

@ -7,8 +7,6 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import List, Optional, TYPE_CHECKING from typing import List, Optional, TYPE_CHECKING
from game.settings import Settings
if TYPE_CHECKING: if TYPE_CHECKING:
from game.missiongenerator.luagenerator import LuaGenerator from game.missiongenerator.luagenerator import LuaGenerator
@ -50,26 +48,10 @@ class PluginSettings:
def __init__(self, identifier: str, enabled_by_default: bool) -> None: def __init__(self, identifier: str, enabled_by_default: bool) -> None:
self.identifier = identifier self.identifier = identifier
self.enabled_by_default = enabled_by_default self.enabled = enabled_by_default
self.settings = Settings()
self.initialize_settings()
def set_settings(self, settings: Settings) -> None:
self.settings = settings
self.initialize_settings()
def initialize_settings(self) -> None:
# Plugin options are saved in the game's Settings, but it's possible for
# plugins to change across loads. If new plugins are added or new
# options added to those plugins, initialize the new settings.
self.settings.initialize_plugin_option(self.identifier, self.enabled_by_default)
@property
def enabled(self) -> bool:
return self.settings.plugin_option(self.identifier)
def set_enabled(self, enabled: bool) -> None: def set_enabled(self, enabled: bool) -> None:
self.settings.set_plugin_option(self.identifier, enabled) self.enabled = enabled
class LuaPluginOption(PluginSettings): class LuaPluginOption(PluginSettings):
@ -173,6 +155,12 @@ class LuaPlugin(PluginSettings):
def options(self) -> List[LuaPluginOption]: def options(self) -> List[LuaPluginOption]:
return self.definition.options return self.definition.options
def is_option_enabled(self, identifier: str) -> bool:
for option in self.options:
if option.identifier == identifier:
return option.enabled
raise KeyError(f"Plugin {self.identifier} has no option {self.identifier}")
@classmethod @classmethod
def from_json(cls, name: str, path: Path) -> Optional[LuaPlugin]: def from_json(cls, name: str, path: Path) -> Optional[LuaPlugin]:
try: try:
@ -183,12 +171,6 @@ class LuaPlugin(PluginSettings):
return cls(definition) return cls(definition)
def set_settings(self, settings: Settings) -> None:
"""Attaches the plugin to a settings object."""
super().set_settings(settings)
for option in self.definition.options:
option.set_settings(self.settings)
def inject_scripts(self, lua_generator: LuaGenerator) -> None: def inject_scripts(self, lua_generator: LuaGenerator) -> None:
"""Injects the plugin's scripts into the mission.""" """Injects the plugin's scripts into the mission."""
for work_order in self.definition.work_orders: for work_order in self.definition.work_orders:
@ -231,3 +213,11 @@ class LuaPlugin(PluginSettings):
for work_order in self.definition.config_work_orders: for work_order in self.definition.config_work_orders:
work_order.work(lua_generator) work_order.work(lua_generator)
def update_with(self, other: LuaPlugin) -> None:
self.enabled = other.enabled
for option in self.options:
try:
option.enabled = other.is_option_enabled(option.identifier)
except KeyError:
continue

View File

@ -1,20 +1,21 @@
from __future__ import annotations
import json import json
import logging import logging
from collections.abc import Iterator
from pathlib import Path from pathlib import Path
from typing import Dict, List
from game.settings import Settings
from .luaplugin import LuaPlugin from .luaplugin import LuaPlugin
class LuaPluginManager: class LuaPluginManager:
"""Manages available and loaded lua plugins.""" """Manages available and loaded lua plugins."""
_plugins_loaded = False def __init__(self, plugins: dict[str, LuaPlugin]) -> None:
_plugins: Dict[str, LuaPlugin] = {} self._plugins: dict[str, LuaPlugin] = plugins
@classmethod @staticmethod
def _load_plugins(cls) -> None: def load() -> LuaPluginManager:
plugins_path = Path("resources/plugins") plugins_path = Path("resources/plugins")
path = plugins_path / "plugins.json" path = plugins_path / "plugins.json"
@ -23,6 +24,7 @@ class LuaPluginManager:
logging.info(f"Reading plugins list from {path}") logging.info(f"Reading plugins list from {path}")
plugins = {}
data = json.loads(path.read_text()) data = json.loads(path.read_text())
for name in data: for name in data:
plugin_path = plugins_path / name / "plugin.json" plugin_path = plugins_path / name / "plugin.json"
@ -34,27 +36,41 @@ class LuaPluginManager:
logging.info(f"Loading plugin {name} from {plugin_path}") logging.info(f"Loading plugin {name} from {plugin_path}")
plugin = LuaPlugin.from_json(name, plugin_path) plugin = LuaPlugin.from_json(name, plugin_path)
if plugin is not None: if plugin is not None:
cls._plugins[name] = plugin plugins[name] = plugin
cls._plugins_loaded = True return LuaPluginManager(plugins)
@classmethod def update_with(self, other: LuaPluginManager) -> None:
def _get_plugins(cls) -> Dict[str, LuaPlugin]: """Updates all setting values with those in the given plugin manager.
if not cls._plugins_loaded:
cls._load_plugins()
return cls._plugins
@classmethod When a game is loaded, LuaPluginManager.load() is called to load the latest set
def plugins(cls) -> List[LuaPlugin]: of plugins and settings. This is called with the plugin manager that was saved
return list(cls._get_plugins().values()) to the Game object to preserve any options that were set, and then the Game is
updated with this manager.
@classmethod This needs to happen because the set of available plugins (or their options) can
def load_settings(cls, settings: Settings) -> None: change between runs.
"""Attaches all loaded plugins to the given settings object.
The LuaPluginManager singleton can only be attached to a single Settings object
at a time, and plugins will update the Settings object directly, so attaching
the plugin manager to a detached Settings object (say, during the new game
wizard, but then canceling the new game) will break the settings UI.
""" """
for plugin in cls.plugins(): for plugin in self.iter_plugins():
plugin.set_settings(settings) try:
old_plugin = other.by_id(plugin.identifier)
except KeyError:
continue
plugin.update_with(old_plugin)
def iter_plugins(self) -> Iterator[LuaPlugin]:
yield from self._plugins.values()
def by_id(self, identifier: str) -> LuaPlugin:
return self._plugins[identifier]
def is_plugin_enabled(self, plugin_id: str) -> bool:
try:
return self.by_id(plugin_id).enabled
except KeyError:
return False
def is_option_enabled(self, plugin_id: str, option_id: str) -> bool:
try:
return self.by_id(plugin_id).is_option_enabled(option_id)
except KeyError:
return False

View File

@ -1,10 +1,10 @@
import logging import logging
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import Field, dataclass, field, fields from dataclasses import Field, dataclass, fields
from datetime import timedelta from datetime import timedelta
from enum import Enum, unique from enum import Enum, unique
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional, get_type_hints from typing import Any, Optional, get_type_hints
import yaml import yaml
from dcs.forcedoptions import ForcedOptions from dcs.forcedoptions import ForcedOptions
@ -490,9 +490,6 @@ class Settings:
enable_frontline_cheats: bool = False enable_frontline_cheats: bool = False
enable_base_capture_cheat: bool = False enable_base_capture_cheat: bool = False
# LUA Plugins system
plugins: Dict[str, bool] = field(default_factory=dict)
only_player_takeoff: bool = True # Legacy parameter do not use only_player_takeoff: bool = True # Legacy parameter do not use
def save_player_settings(self) -> None: def save_player_settings(self) -> None:
@ -548,22 +545,6 @@ class Settings:
"""Returns the path to the player's global settings file.""" """Returns the path to the player's global settings file."""
return liberation_user_dir() / "settings.yaml" return liberation_user_dir() / "settings.yaml"
@staticmethod
def plugin_settings_key(identifier: str) -> str:
return f"plugins.{identifier}"
def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
try:
self.plugin_option(identifier)
except KeyError:
self.set_plugin_option(identifier, default_value)
def plugin_option(self, identifier: str) -> bool:
return self.plugins[self.plugin_settings_key(identifier)]
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
self.plugins[self.plugin_settings_key(identifier)] = enabled
def __setstate__(self, state: dict[str, Any]) -> None: def __setstate__(self, state: dict[str, Any]) -> None:
# __setstate__ is called with the dict of the object being unpickled. We # __setstate__ is called with the dict of the object being unpickled. We
# can provide save compatibility for new settings options (which # can provide save compatibility for new settings options (which

View File

@ -31,6 +31,7 @@ from ..armedforces.armedforces import ArmedForces
from ..armedforces.forcegroup import ForceGroup from ..armedforces.forcegroup import ForceGroup
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
from ..data.groups import GroupTask from ..data.groups import GroupTask
from ..plugins import LuaPluginManager
from ..profiling import logged_duration from ..profiling import logged_duration
from ..settings import Settings from ..settings import Settings
@ -97,6 +98,8 @@ class GameGenerator:
start_date=self.generator_settings.start_date, start_date=self.generator_settings.start_date,
start_time=self.generator_settings.start_time, start_time=self.generator_settings.start_time,
settings=self.settings, settings=self.settings,
# TODO: Hoist into NGW so we can expose those options.
lua_plugin_manager=LuaPluginManager.load(),
player_budget=self.generator_settings.player_budget, player_budget=self.generator_settings.player_budget,
enemy_budget=self.generator_settings.enemy_budget, enemy_budget=self.generator_settings.enemy_budget,
) )

View File

@ -1,9 +1,9 @@
"""Combo box for selecting a flight's task type.""" """Combo box for selecting a flight's task type."""
from PySide6.QtWidgets import QComboBox from PySide6.QtWidgets import QComboBox
from game.ato.flighttype import FlightType
from game.settings.settings import Settings
from game.ato.flighttype import FlightType
from game.plugins import LuaPluginManager
from game.theater import ConflictTheater, MissionTarget from game.theater import ConflictTheater, MissionTarget
@ -11,14 +11,18 @@ class QFlightTypeComboBox(QComboBox):
"""Combo box for selecting a flight task type.""" """Combo box for selecting a flight task type."""
def __init__( def __init__(
self, theater: ConflictTheater, target: MissionTarget, settings: Settings self,
theater: ConflictTheater,
target: MissionTarget,
lua_plugin_manager: LuaPluginManager,
) -> None: ) -> None:
super().__init__() super().__init__()
self.theater = theater self.theater = theater
self.target = target self.target = target
for mission_type in self.target.mission_types(for_player=True): for mission_type in self.target.mission_types(for_player=True):
if mission_type == FlightType.AIR_ASSAULT and not settings.plugin_option( if (
"ctld" mission_type == FlightType.AIR_ASSAULT
and not lua_plugin_manager.is_plugin_enabled("ctld")
): ):
# Only add Air Assault if ctld plugin is enabled # Only add Air Assault if ctld plugin is enabled
continue continue

View File

@ -4,22 +4,22 @@ from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QComboBox, QComboBox,
QDialog, QDialog,
QHBoxLayout,
QLabel, QLabel,
QLineEdit,
QMessageBox, QMessageBox,
QPushButton, QPushButton,
QVBoxLayout, QVBoxLayout,
QLineEdit,
QHBoxLayout,
) )
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game import Game from game import Game
from game.ato.flight import Flight
from game.ato.flightroster import FlightRoster
from game.ato.package import Package
from game.ato.starttype import StartType from game.ato.starttype import StartType
from game.squadrons.squadron import Squadron from game.squadrons.squadron import Squadron
from game.theater import ControlPoint, OffMapSpawn from game.theater import ControlPoint, OffMapSpawn
from game.ato.package import Package
from game.ato.flightroster import FlightRoster
from game.ato.flight import Flight
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.QLabeledWidget import QLabeledWidget
@ -51,7 +51,7 @@ class QFlightCreator(QDialog):
layout = QVBoxLayout() layout = QVBoxLayout()
self.task_selector = QFlightTypeComboBox( self.task_selector = QFlightTypeComboBox(
self.game.theater, package.target, self.game.settings self.game.theater, package.target, self.game.lua_plugin_manager
) )
self.task_selector.setCurrentIndex(0) self.task_selector.setCurrentIndex(0)
self.task_selector.currentIndexChanged.connect(self.on_task_changed) self.task_selector.currentIndexChanged.connect(self.on_task_changed)

View File

@ -61,8 +61,9 @@ class QFlightWaypointTab(QFrame):
self.recreate_buttons.clear() self.recreate_buttons.clear()
for task in self.package.target.mission_types(for_player=True): for task in self.package.target.mission_types(for_player=True):
if task == FlightType.AIR_ASSAULT and not self.game.settings.plugin_option( if (
"ctld" task == FlightType.AIR_ASSAULT
and not self.game.lua_plugin_manager.is_plugin_enabled("ctld")
): ):
# Only add Air Assault if ctld plugin is enabled # Only add Air Assault if ctld plugin is enabled
continue continue

View File

@ -285,7 +285,7 @@ class QSettingsWindow(QDialog):
self.categoryModel.appendRow(cheat) self.categoryModel.appendRow(cheat)
self.right_layout.addWidget(self.cheatPage) self.right_layout.addWidget(self.cheatPage)
self.pluginsPage = PluginsPage() self.pluginsPage = PluginsPage(self.game.lua_plugin_manager)
plugins = QStandardItem("LUA Plugins") plugins = QStandardItem("LUA Plugins")
plugins.setIcon(CONST.ICONS["Plugins"]) plugins.setIcon(CONST.ICONS["Plugins"])
plugins.setEditable(False) plugins.setEditable(False)
@ -293,7 +293,7 @@ class QSettingsWindow(QDialog):
self.categoryModel.appendRow(plugins) self.categoryModel.appendRow(plugins)
self.right_layout.addWidget(self.pluginsPage) self.right_layout.addWidget(self.pluginsPage)
self.pluginsOptionsPage = PluginOptionsPage() self.pluginsOptionsPage = PluginOptionsPage(self.game.lua_plugin_manager)
pluginsOptions = QStandardItem("LUA Plugins Options") pluginsOptions = QStandardItem("LUA Plugins Options")
pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"]) pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"])
pluginsOptions.setEditable(False) pluginsOptions.setEditable(False)

View File

@ -12,14 +12,14 @@ from game.plugins import LuaPlugin, LuaPluginManager
class PluginsBox(QGroupBox): class PluginsBox(QGroupBox):
def __init__(self) -> None: def __init__(self, manager: LuaPluginManager) -> None:
super().__init__("Plugins") super().__init__("Plugins")
layout = QGridLayout() layout = QGridLayout()
layout.setAlignment(Qt.AlignTop) layout.setAlignment(Qt.AlignTop)
self.setLayout(layout) self.setLayout(layout)
for row, plugin in enumerate(LuaPluginManager.plugins()): for row, plugin in enumerate(manager.iter_plugins()):
if not plugin.show_in_ui: if not plugin.show_in_ui:
continue continue
@ -32,14 +32,14 @@ class PluginsBox(QGroupBox):
class PluginsPage(QWidget): class PluginsPage(QWidget):
def __init__(self) -> None: def __init__(self, manager: LuaPluginManager) -> None:
super().__init__() super().__init__()
layout = QVBoxLayout() layout = QVBoxLayout()
layout.setAlignment(Qt.AlignTop) layout.setAlignment(Qt.AlignTop)
self.setLayout(layout) self.setLayout(layout)
layout.addWidget(PluginsBox()) layout.addWidget(PluginsBox(manager))
class PluginOptionsBox(QGroupBox): class PluginOptionsBox(QGroupBox):
@ -60,13 +60,13 @@ class PluginOptionsBox(QGroupBox):
class PluginOptionsPage(QWidget): class PluginOptionsPage(QWidget):
def __init__(self) -> None: def __init__(self, manager: LuaPluginManager) -> None:
super().__init__() super().__init__()
layout = QVBoxLayout() layout = QVBoxLayout()
layout.setAlignment(Qt.AlignTop) layout.setAlignment(Qt.AlignTop)
self.setLayout(layout) self.setLayout(layout)
for plugin in LuaPluginManager.plugins(): for plugin in manager.iter_plugins():
if plugin.options: if plugin.options:
layout.addWidget(PluginOptionsBox(plugin)) layout.addWidget(PluginOptionsBox(plugin))