tobspr/RenderPipeline

View on GitHub
toolkit/plugin_configurator/main.py

Summary

Maintainability
D
1 day
Test Coverage
"""

RenderPipeline

Copyright (c) 2014-2016 tobspr <tobias.springer1@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

"""

# This tool offers an interface to configure the pipeline

from __future__ import print_function

import os
import sys
import time
from threading import Thread
from functools import partial

# Change to the current directory
os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__))))

# Append the current directory to the path
sys.path.insert(0, os.getcwd())

# Add the render pipeline to the path
sys.path.insert(0, "../../")

from rplibs.six import iteritems  # noqa
from rplibs.pyqt_imports import * #noqa

# Load the generated UI Layout
from ui.main_window_generated import Ui_MainWindow  # noqa

from rpcore.pluginbase.manager import PluginManager  # noqa
from rpcore.util.network_communication import NetworkCommunication  # noqa
from rpcore.mount_manager import MountManager  # noqa


class PluginConfigurator(QMainWindow, Ui_MainWindow):

    """ Interface to change the plugin settings """

    def __init__(self):
        

        # Init mounts
        self._mount_mgr = MountManager(None)
        self._mount_mgr.mount()

        self._plugin_mgr = PluginManager(None)
        self._plugin_mgr.requires_daytime_settings = False

        QMainWindow.__init__(self)
        Ui_MainWindow.__init__(self)
        self.setupUi(self)



        self._current_plugin = None
        self._current_plugin_instance = None
        self.lbl_restart_pipeline.hide()
        self._set_settings_visible(False)
        self._update_queue = list()

        qt_connect(self.lst_plugins, "itemSelectionChanged()",
                self.on_plugin_selected)
        qt_connect(self.lst_plugins, "itemChanged(QListWidgetItem*)",
                self.on_plugin_state_changed)
        qt_connect(self.btn_reset_plugin_settings, "clicked()",
                self.on_reset_plugin_settings)

        self._load_plugin_list()

        # Adjust column widths
        self.table_plugin_settings.setColumnWidth(0, 140)
        self.table_plugin_settings.setColumnWidth(1, 105)
        self.table_plugin_settings.setColumnWidth(2, 160)

        update_thread = Thread(target=self.update_thread, args=())
        update_thread.start()

    def closeEvent(self, event):  # noqa
        event.accept()
        import os
        os._exit(1)

    def on_reset_plugin_settings(self):
        """ Gets called when the user wants to reset settings of a plugin """

        # Ask the user if he's really sure about it
        msg = "Are you sure you want to reset the settings of '"
        msg += self._current_plugin_instance.name + "'?\n"
        msg += "This does not reset the Time of Day settings of this plugin.\n\n"
        msg += "!! This cannot be undone !! They will be lost forever (a long time!)."
        reply = QMessageBox.question(
            self, "Warning", msg, QMessageBox.Yes, QMessageBox.No)
        if reply == QMessageBox.Yes:

            QMessageBox.information(
                self, "Success",
                "Settings have been reset! You may have to restart the pipeline.")
            self._plugin_mgr.reset_plugin_settings(self._current_plugin)

            # Save config
            self._plugin_mgr.save_overrides("/$$rpconfig/plugins.yaml")

            # Always show the restart hint, even if its not always required
            self._show_restart_hint()

            # Re-render everything
            self._load_plugin_list()

    def on_plugin_state_changed(self, item):
        """ Handler when a plugin got activated/deactivated """
        plugin_id = item._plugin_id
        state = item.checkState() == Qt.Checked
        self._plugin_mgr.set_plugin_enabled(plugin_id, state)
        self._rewrite_plugin_config()
        self._show_restart_hint()

    def on_plugin_selected(self):
        """ Gets called when a plugin got selected in the plugin list """
        selected_item = self.lst_plugins.selectedItems()
        if not selected_item:
            self._current_plugin = None
            self._current_plugin_instance = None
            self._set_settings_visible(False)
            return
        assert len(selected_item) == 1
        selected_item = selected_item[0]
        self._current_plugin = selected_item._plugin_id
        self._current_plugin_instance = self._plugin_mgr.instances[self._current_plugin]
        assert(self._current_plugin_instance is not None)
        self._render_current_plugin()
        self._set_settings_visible(True)

    def update_thread(self):
        """ Internal update thread """
        while True:
            if len(self._update_queue) > 0:
                item = self._update_queue.pop(-1)
                NetworkCommunication.send_async(NetworkCommunication.CONFIG_PORT, item)

                if item.startswith("setval "):
                    setting_id = item.split()[1]
                    for entry in list(self._update_queue):
                        if entry.split()[1] == setting_id:
                            self._update_queue.remove(entry)
            time.sleep(0.2)

    def _rewrite_plugin_config(self):
        """ Rewrites the plugin configuration """
        self._plugin_mgr.save_overrides("/$$rpconfig/plugins.yaml")

    def _render_current_plugin(self):
        """ Displays the currently selected plugin """
        self.lbl_plugin_name.setText(self._current_plugin_instance.name.upper() + " <span style='color: #999; margin-left: 5px;'>[" + self._current_plugin_instance.plugin_id + "]</span>")

        version_str = "Version " + self._current_plugin_instance.version
        version_str += " by " + self._current_plugin_instance.author

        self.lbl_plugin_version.setText(version_str)
        self.lbl_plugin_desc.setText(self._current_plugin_instance.description)

        self._render_current_settings()

    def _show_restart_hint(self):
        """ Shows a hint to restart the pipeline """
        self.lbl_restart_pipeline.show()

    def _render_current_settings(self):
        """ Renders the current plugin settings """
        settings = self._plugin_mgr.settings[self._current_plugin]

        # remove all rows
        while self.table_plugin_settings.rowCount() > 0:
            self.table_plugin_settings.removeRow(0)

        label_font = QFont()
        label_font.setPointSize(10)
        label_font.setFamily("Roboto")

        desc_font = QFont()
        desc_font.setPointSize(8)
        desc_font.setFamily("Roboto")

        for index, (name, handle) in enumerate(iteritems(settings)):
            if not handle.should_be_visible(settings):
                continue

            row_index = self.table_plugin_settings.rowCount()

            # Increase row count
            self.table_plugin_settings.insertRow(self.table_plugin_settings.rowCount())

            label = QLabel()
            label.setText(handle.label)
            label.setWordWrap(True)
            label.setFont(label_font)

            if not (handle.shader_runtime or handle.runtime ):
                label.setStyleSheet("color: #999;")

            if handle.display_conditions:
                label.setStyleSheet(label.styleSheet() + "padding-left: 10px;")

            label.setMargin(10)

            self.table_plugin_settings.setCellWidget(row_index, 0, label)

            item_default = QTableWidgetItem()
            item_default.setText(str(handle.default))
            item_default.setTextAlignment(Qt.AlignCenter)
            self.table_plugin_settings.setItem(row_index, 1, item_default)

            setting_widget = self._get_widget_for_setting(name, handle)
            self.table_plugin_settings.setCellWidget(row_index, 2, setting_widget)

            label_desc = QLabel()
            label_desc.setText(handle.description)
            label_desc.setWordWrap(True)
            label_desc.setFont(desc_font)
            label_desc.setStyleSheet("color: #555;padding: 5px;")

            self.table_plugin_settings.setCellWidget(row_index, 3, label_desc)

    def _do_update_setting(self, setting_id, value):
        """ Updates a setting of the current plugin """

        # Check whether the setting is a runtime setting
        setting_handle = self._plugin_mgr.get_setting_handle(
            self._current_plugin, setting_id)

        # Skip the setting in case the value is equal
        if setting_handle.value == value:
            return

        # Otherwise set the new value
        setting_handle.set_value(value)
        self._rewrite_plugin_config()

        if not setting_handle.runtime and not setting_handle.shader_runtime:
            self._show_restart_hint()
        else:
            # In case the setting is dynamic, notice the pipeline about it:
            # print("Sending reload packet ...")
            self._update_queue.append("setval {}.{} {}".format(
                self._current_plugin, setting_id, value))

        # Update GUI, but only in case of enum and bool values, since they can trigger
        # display conditions:
        if setting_handle.type == "enum" or setting_handle.type == "bool":
            self._render_current_settings()

    def _on_setting_bool_changed(self, setting_id, value):
        self._do_update_setting(setting_id, value == Qt.Checked)

    def _on_setting_scalar_changed(self, setting_id, value):
        self._do_update_setting(setting_id, value)

    def _on_setting_enum_changed(self, setting_id, value):
        self._do_update_setting(setting_id, value)

    def _on_setting_power_of_two_changed(self, setting_id, value):
        self._do_update_setting(setting_id, value)

    def _on_setting_slider_changed(self, setting_id, setting_type, bound_objs, value):
        if setting_type == "float":
            value /= 100000.0
        self._do_update_setting(setting_id, value)

        for obj in bound_objs:
            obj.setValue(value)

    def _on_setting_spinbox_changed(self, setting_id, setting_type, bound_objs, value):
        self._do_update_setting(setting_id, value)
        # Assume objects are sliders, so we need to rescale the value
        for obj in bound_objs:
            obj.setValue(value * 100000.0 if setting_type == "float" else value)

    def _choose_path(self, setting_id, setting_handle, bound_objs):
        """ Shows a file chooser to show an path from """

        this_dir = os.path.dirname(os.path.realpath(__file__))
        plugin_dir = os.path.join(this_dir, "../../rpplugins/" + self._current_plugin, "resources")
        plugin_dir = os.path.abspath(plugin_dir)
        search_dir = os.path.join(plugin_dir, setting_handle.base_path)

        print("Plugin dir =", plugin_dir)
        print("Search dir =", search_dir)

        current_file = setting_handle.value.replace("\\", "/").split("/")[-1]
        print("Current file =", current_file)
        file_dlg = QFileDialog(self, "Choose File ..", search_dir, setting_handle.file_type)
        file_dlg.selectFile(current_file)
        # file_dlg.setViewMode(QFileDialog.Detail)

        if file_dlg.exec_():
            filename = file_dlg.selectedFiles()
            filename = filename[-1]
            print("QT selected files returned:", filename)

            filename = os.path.relpath(str(filename), plugin_dir)
            filename = filename.replace("\\", "/")
            print("Relative path is", filename)
            self._do_update_setting(setting_id, filename)

            display_file = filename.split("/")[-1]
            for obj in bound_objs:
                obj.setText(display_file)

    def _get_widget_for_setting(self, setting_id, setting):
        """ Returns an appropriate widget to control the given setting """

        widget = QWidget()
        layout = QHBoxLayout()
        layout.setAlignment(Qt.AlignCenter)
        widget.setLayout(layout)


        if setting.type == "bool":
            box = QCheckBox()
            box.setChecked(Qt.Checked if setting.value else Qt.Unchecked)
            qt_connect(box, "stateChanged(int)",
                    partial(self._on_setting_bool_changed, setting_id))
            layout.addWidget(box)

        elif setting.type == "float" or setting.type == "int":

            if setting.type == "float":
                box = QDoubleSpinBox()

                if setting.maxval - setting.minval <= 2.0:
                    box.setDecimals(4)
            else:
                box = QSpinBox()
            box.setMinimum(setting.minval)
            box.setMaximum(setting.maxval)
            box.setValue(setting.value)

            box.setAlignment(Qt.AlignCenter)

            slider = QSlider()
            slider.setOrientation(Qt.Horizontal)

            if setting.type == "float":
                box.setSingleStep(abs(setting.maxval - setting.minval) / 100.0)
                slider.setMinimum(setting.minval * 100000.0)
                slider.setMaximum(setting.maxval * 100000.0)
                slider.setValue(setting.value * 100000.0)
            elif setting.type == "int":
                box.setSingleStep(max(1, (setting.maxval - setting.minval) / 32))
                slider.setMinimum(setting.minval)
                slider.setMaximum(setting.maxval)
                slider.setValue(setting.value)

            layout.addWidget(box)
            layout.addWidget(slider)

            qt_connect(slider, "valueChanged(int)",
                    partial(self._on_setting_slider_changed, setting_id, setting.type, [box]))

            value_type = "int" if setting.type == "int" else "double"

            qt_connect(box, "valueChanged(" + value_type + ")",
                    partial(self._on_setting_spinbox_changed, setting_id, setting.type, [slider]))

        elif setting.type == "enum":
            box = QComboBox()
            for value in setting.values:
                box.addItem(value)
            qt_connect(box, "currentIndexChanged(QString)",
                    partial(self._on_setting_enum_changed, setting_id))
            box.setCurrentIndex(setting.values.index(setting.value))
            box.setMinimumWidth(145)

            layout.addWidget(box)

        elif setting.type == "power_of_two":

            box = QComboBox()
            resolutions = [str(2**i) for i in range(1, 32) if 2**i >= setting.minval and 2**i <= setting.maxval]
            for value in resolutions:
                box.addItem(value)
            qt_connect(box, "currentIndexChanged(QString)",
                    partial(self._on_setting_power_of_two_changed, setting_id))
            box.setCurrentIndex(resolutions.index(str(setting.value)))
            box.setMinimumWidth(145)
            layout.addWidget(box)

        elif setting.type == "sample_sequence":

            box = QComboBox()
            for value in setting.sequences:
                box.addItem(value)
            qt_connect(box, "currentIndexChanged(QString)",
                    partial(self._on_setting_enum_changed, setting_id))
            box.setCurrentIndex(setting.sequences.index(str(setting.value)))
            box.setMinimumWidth(145)
            layout.addWidget(box)

        elif setting.type == "path":

            label = QLabel()
            display_file = setting.value.replace("\\", "/").split("/")[-1]

            desc_font = QFont()
            desc_font.setPointSize(7)
            desc_font.setFamily("Roboto")

            label.setText(display_file)
            label.setFont(desc_font)

            button = QPushButton()
            button.setText("Choose File ...")
            qt_connect(button, "clicked()", partial(
                self._choose_path, setting_id, setting, (label,)))

            layout.addWidget(label)
            layout.addWidget(button)

        else:
            print("ERROR: Unkown setting type:", setting.type)

        return widget

    def _set_settings_visible(self, flag):
        """ Sets whether the settings panel is visible or not """
        if flag:
            self.frame_details.show()
        else:
            self.frame_details.hide()

    def _load_plugin_list(self):
        """ Reloads the whole plugin list """
        print("Loading plugin list")

        # Reset selection
        self._current_plugin = None
        self._current_plugin_instance = None
        self._set_settings_visible(False)

        # Plugins are all plugins in the plugins directory
        self._plugin_mgr.unload()
        self._plugin_mgr.load()

        self.lst_plugins.clear()
        plugins = sorted(iteritems(self._plugin_mgr.instances), key=lambda plg: plg[1].name)


        item_font = QFont()
        item_font.setBold(False)
        item_font.setPointSize(10)

        for plugin_id, instance in plugins:

            item = QListWidgetItem()
            item.setText(" " + instance.name)
            item.setFont(item_font)

            if self._plugin_mgr.is_plugin_enabled(plugin_id):
                item.setCheckState(Qt.Checked)
            else:
                item.setCheckState(Qt.Unchecked)

            item._plugin_id = plugin_id
            self.lst_plugins.addItem(item)

        self.lst_plugins.setCurrentRow(0)

# Start application
app = QApplication(sys.argv)
qt_register_fonts()
configurator = PluginConfigurator()
configurator.show()
app.exec_()