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

[PoC] Session-level settings #144

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
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
9 changes: 9 additions & 0 deletions lisp/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
class Application(metaclass=Singleton):
def __init__(self, app_conf=DummyConfiguration()):
self.session_created = Signal()
self.session_initialised = Signal()
self.session_loaded = Signal()
self.session_before_finalize = Signal()

Expand Down Expand Up @@ -125,6 +126,7 @@ def start(self, session_file=""):

if layout_name.lower() != "nodefault":
self.__new_session(layout.get_layout(layout_name))
self.session_initialised.emit(self.session)
else:
self.__new_session_dialog()

Expand All @@ -145,6 +147,7 @@ def __new_session_dialog(self):
self.__load_from_file(dialog.sessionPath)
else:
self.__new_session(dialog.selected())
self.session_initialised.emit(self.session)
else:
if self.__session is None:
# If the user close the dialog, and no layout exists
Expand Down Expand Up @@ -216,6 +219,12 @@ def __load_from_file(self, session_file):
self.session.update_properties(session_dict["session"])
self.session.session_file = abspath(session_file)

if 'plugins' in session_dict['session']:
for plugin_name, plugin_config in session_dict['session']['plugins'].items():
self.__session.set_plugin_session_config(plugin_name, plugin_config)

self.session_initialised.emit(self.session)

# Load cues
for cues_dict in session_dict.get("cues", {}):
cue_type = cues_dict.pop("_type_", "Undefined")
Expand Down
55 changes: 54 additions & 1 deletion lisp/core/configuration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is part of Linux Show Player
#
# Copyright 2018 Francesco Ceruti <ceppofrancy@gmail.com>
# Copyright 2022 Francesco Ceruti <ceppofrancy@gmail.com>
#
# Linux Show Player is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand All @@ -18,6 +18,7 @@
# Used to indicate the default behaviour when a specific option is not found to
# raise an exception. Created to enable `None' as a valid fallback value.

import inspect
import json
import logging
from abc import ABCMeta, abstractmethod
Expand Down Expand Up @@ -254,3 +255,55 @@ def _check_file(self):
def _read_json(path):
with open(path, "r") as f:
return json.load(f)


class PluginSessionConfig(Configuration):

def __init__(self, plugin):
super().__init__()
self._plugin = plugin

def get(self, path, default=_UNSET):
if not self._root:
self.read()
return super().get(path, default)

def set(self, path, value):
if not self._root:
self.read()
return super().set(path, value)

def read(self):
"""Returns a plugin's session-specific config.

Falls back to a "session.json" file (much like the app-level config "default.json" file)
or, if that doesn't exist, returns the plugin's app-level config file.

This second fallback is for plugins that might wish to set defaults on an app-level,
but also permit the user to override those defaults in/for specific sessions.
"""
if self._plugin.app.session is None:
return

plugin_name = self._plugin.__module__.split('.')[-1]

if plugin_name in self._plugin.app.session.plugins:
self._root = self._plugin.app.session.plugins[plugin_name]
return

plugin_path = path.join(
path.split(inspect.getfile(self._plugin.__class__))[0],
'session.json')

if path.exists(plugin_path):
self._root = JSONFileConfiguration._read_json(plugin_path)
return

self._root = deepcopy(self._plugin.Config._root)

def write(self):
"""Writes the plugin's session-specific config to the active session.

When the user saves the showfile, it will then be written to disk.
"""
self._plugin.app.session.set_plugin_session_config(self._plugin.__module__.split('.')[-1], self._root)
40 changes: 38 additions & 2 deletions lisp/core/plugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is part of Linux Show Player
#
# Copyright 2020 Francesco Ceruti <ceppofrancy@gmail.com>
# Copyright 2022 Francesco Ceruti <ceppofrancy@gmail.com>
#
# Linux Show Player is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand All @@ -17,7 +17,7 @@

import logging

from lisp.core.configuration import DummyConfiguration
from lisp.core.configuration import DummyConfiguration, PluginSessionConfig
from lisp.ui.ui_utils import translate

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -57,6 +57,11 @@ def __init__(self, app):
""":type app: lisp.application.Application"""
self.__app = app
self.__class__.State |= PluginState.Loaded
self.SessionConfig = None

app.session_created.connect(self._on_session_created)
app.session_initialised.connect(self._on_session_initialised)
app.session_before_finalize.connect(self._pre_session_deinitialisation)

@property
def app(self):
Expand Down Expand Up @@ -115,3 +120,34 @@ def status_text(cls):
)

return translate("PluginsStatusText", "Plugin disabled. Enable to use.")

def _on_session_created(self, _):
"""Called immediately after a session is created.

In the case of a file load, this gets called before the session
properties and cues are restored.
"""
self.SessionConfig = PluginSessionConfig(self)
self.SessionConfig.changed.connect(self._on_session_config_altered)
self.SessionConfig.updated.connect(self._on_session_config_altered)

def _on_session_initialised(self, _):
"""Called once a session is fully initialised.

For new sessions, there is not much difference between this and `app.session_created`

For loaded sessions, this is called *after* the various showfile properties have
been set, but *before* the cues are restored.
"""
pass

def _pre_session_deinitialisation(self, _):
"""Called just before a session is deinitialised.

The session object still exists at this point, so may still be accessed.
"""
pass

def _on_session_config_altered(self, _):
"""Called whenever the session config is changed or updated in any way."""
pass
2 changes: 1 addition & 1 deletion lisp/core/plugins_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def register_plugin(self, name, plugin):
mod_path = path.dirname(inspect.getfile(plugin))
mod_name = plugin.__module__.split(".")[-1]

# Load plugin configuration
# Load plugin (app-level) configuration
user_config_path = path.join(
app_dirs.user_config_dir, mod_name + ".json"
)
Expand Down
5 changes: 5 additions & 0 deletions lisp/core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Session(HasInstanceProperties):
session_file = Property(default="")
"""The current session-file path, must be absolute."""

plugins = Property(default={})

def __init__(self, layout):
super().__init__()
self.finalized = Signal()
Expand Down Expand Up @@ -59,6 +61,9 @@ def abs_path(self, rel_path):

return rel_path

def set_plugin_session_config(self, plugin_name, plugin_session_config):
self.plugins[plugin_name] = plugin_session_config

def rel_path(self, abs_path):
"""Return a relative (to the session-file) version of the given path."""
return os.path.relpath(abs_path, start=self.dir())
Expand Down
12 changes: 12 additions & 0 deletions lisp/ui/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from lisp.ui.logging.status import LogStatusIcon, LogMessageWidget
from lisp.ui.logging.viewer import LogViewer
from lisp.ui.settings.app_configuration import AppConfigurationDialog
from lisp.ui.settings.session_configuration import SessionConfigurationDialog
from lisp.ui.ui_utils import translate
from lisp.ui.widgets import DigitalLabelClock

Expand Down Expand Up @@ -159,6 +160,12 @@ def __init__(self, app, title="Linux Show Player", **kwargs):
self.menuEdit.addSeparator()
self.menuEdit.addAction(self.multiEdit)

# menuTools
self.sessionPreferences = QAction(self)
self.sessionPreferences.triggered.connect(self._show_session_preferences)
self.menuTools.addAction(self.sessionPreferences)
self.menuTools.addSeparator()

# menuAbout
self.actionAbout = QAction(self)
self.actionAbout.triggered.connect(self.__about)
Expand Down Expand Up @@ -227,6 +234,7 @@ def retranslateUi(self):
# menuTools
self.menuTools.setTitle(translate("MainWindow", "&Tools"))
self.multiEdit.setText(translate("MainWindow", "Edit selection"))
self.sessionPreferences.setText(translate("MainWindow", "Session Preferences"))
# menuAbout
self.menuAbout.setTitle(translate("MainWindow", "&About"))
self.actionAbout.setText(translate("MainWindow", "About"))
Expand Down Expand Up @@ -321,6 +329,10 @@ def closeEvent(self, event):
else:
event.ignore()

def _show_session_preferences(self):
prefUi = SessionConfigurationDialog(parent=self)
prefUi.exec_()

def __beforeSessionFinalize(self):
self.centralWidget().layout().removeWidget(
self._app.session.layout.view
Expand Down
111 changes: 9 additions & 102 deletions lisp/ui/settings/app_configuration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is part of Linux Show Player
#
# Copyright 2016 Francesco Ceruti <ceppofrancy@gmail.com>
# Copyright 2022 Francesco Ceruti <ceppofrancy@gmail.com>
#
# Linux Show Player is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand All @@ -15,127 +15,34 @@
# You should have received a copy of the GNU General Public License
# along with Linux Show Player. If not, see <http://www.gnu.org/licenses/>.

import logging
from collections import namedtuple

from PyQt5 import QtCore
from PyQt5.QtCore import QModelIndex
from PyQt5.QtWidgets import QVBoxLayout, QDialogButtonBox, QDialog

from lisp.core.dicttree import DictNode
from lisp.ui.settings.configuration_dialog import ConfigurationDialog
from lisp.ui.ui_utils import translate
from lisp.ui.widgets.pagestreewidget import PagesTreeModel, PagesTreeWidget

logger = logging.getLogger(__name__)

PageEntry = namedtuple("PageEntry", ("page", "config"))


class AppConfigurationDialog(QDialog):
class AppConfigurationDialog(ConfigurationDialog):
PagesRegistry = DictNode()

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.setWindowTitle(translate("AppConfiguration", "LiSP preferences"))
self.setWindowModality(QtCore.Qt.WindowModal)
self.setMinimumSize(800, 510)
self.resize(800, 510)
self.setLayout(QVBoxLayout())

self.configurations = {}

self.model = PagesTreeModel(tr_context="SettingsPageName")
for page_node in AppConfigurationDialog.PagesRegistry.children:
self._populateModel(QModelIndex(), page_node)

self.mainPage = PagesTreeWidget(self.model)
self.mainPage.selectFirst()
self.layout().addWidget(self.mainPage)

self.dialogButtons = QDialogButtonBox(self)
self.dialogButtons.setStandardButtons(
QDialogButtonBox.Cancel
| QDialogButtonBox.Apply
| QDialogButtonBox.Ok
)
self.layout().addWidget(self.dialogButtons)

self.dialogButtons.button(QDialogButtonBox.Cancel).clicked.connect(
self.reject
)
self.dialogButtons.button(QDialogButtonBox.Apply).clicked.connect(
self.applySettings
)
self.dialogButtons.button(QDialogButtonBox.Ok).clicked.connect(
self.__onOk
)

def applySettings(self):
for conf, pages in self.configurations.items():
for page in pages:
conf.update(page.getSettings())

conf.write()

def _populateModel(self, model_parent, page_parent):
if page_parent.value is not None:
page_class = page_parent.value.page
config = page_parent.value.config
else:
page_class = None
config = None

try:
if page_class is None:
# The current node have no page, use the parent model-index
# as parent for it's children
model_index = model_parent
else:
page_instance = page_class()
page_instance.loadSettings(config)
model_index = self.model.addPage(
page_instance, parent=model_parent
)

# Keep track of configurations and corresponding pages
self.configurations.setdefault(config, []).append(page_instance)
except Exception:
if not isinstance(page_class, type):
page_name = "InvalidPage"
else:
page_name = getattr(page_class, "Name", page_class.__name__)

logger.warning(
translate(
"AppConfigurationWarning",
'Cannot load configuration page: "{}" ({})',
).format(page_name, page_parent.path()),
exc_info=True,
)
else:
for page_node in page_parent.children:
self._populateModel(model_index, page_node)

def __onOk(self):
self.applySettings()
self.accept()

@staticmethod
def registerSettingsPage(path, page, config):
def _getConfigFromPageEntry(page_parent):
return page_parent.value.config

@classmethod
def registerSettingsPage(cls, path, page, config):
"""
:param path: indicate the page "position": 'category.sub.key'
:type path: str
:type page: type
:type config: lisp.core.configuration.Configuration
"""
AppConfigurationDialog.PagesRegistry.set(
cls.PagesRegistry.set(
path, PageEntry(page=page, config=config)
)

@staticmethod
def unregisterSettingsPage(path):
"""
:param path: indicate the page "position": 'category.sub.key'
:type path: str
"""
AppConfigurationDialog.PagesRegistry.pop(path)
Loading