597 lines
22 KiB
Python
597 lines
22 KiB
Python
#!/usr/bin/env python
|
||
|
||
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
|
||
|
||
# 创建一个简单的UniversalMessageDialog类,避免复杂的导入问题
|
||
class UniversalMessageDialog(QDialog):
|
||
@staticmethod
|
||
def show_success(parent, title, message, show_cancel=True, confirm_text="确认"):
|
||
msg_box = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent)
|
||
if show_cancel:
|
||
msg_box.addButton(QMessageBox.Cancel)
|
||
msg_box.button(QMessageBox.Ok).setText(confirm_text)
|
||
return msg_box.exec_()
|
||
|
||
@staticmethod
|
||
def show_warning(parent, title, message, show_cancel=True, confirm_text="确认"):
|
||
msg_box = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent)
|
||
if show_cancel:
|
||
msg_box.addButton(QMessageBox.Cancel)
|
||
msg_box.button(QMessageBox.Ok).setText(confirm_text)
|
||
return msg_box.exec_()
|
||
|
||
@staticmethod
|
||
def show_error(parent, title, message, show_cancel=True, confirm_text="确认"):
|
||
msg_box = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent)
|
||
if show_cancel:
|
||
msg_box.addButton(QMessageBox.Cancel)
|
||
msg_box.button(QMessageBox.Ok).setText(confirm_text)
|
||
return msg_box.exec_()
|
||
|
||
@staticmethod
|
||
def show_info(parent, title, message, show_cancel=True, confirm_text="确认"):
|
||
msg_box = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent)
|
||
if show_cancel:
|
||
msg_box.addButton(QMessageBox.Cancel)
|
||
msg_box.button(QMessageBox.Ok).setText(confirm_text)
|
||
return msg_box.exec_()
|
||
|
||
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()
|
||
|
||
# 启动通信文件监听线程
|
||
comm_thread = Thread(target=self.communication_thread, args=())
|
||
comm_thread.daemon = True # 设置为守护线 程,主程序退出时自动结束
|
||
comm_thread.start()
|
||
|
||
def closeEvent(self, event): # noqa
|
||
event.accept()
|
||
# 通知主程序配置可能已更改
|
||
try:
|
||
# 发送一个信号或者写入一个标记文件来通知主程序
|
||
marker_file = "/tmp/rp_plugin_config_changed.flag"
|
||
with open(marker_file, "w") as f:
|
||
f.write("Plugin configuration may have changed")
|
||
except Exception as e:
|
||
print(f"无法创建配置更改标记文件: {e}")
|
||
|
||
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:
|
||
|
||
UniversalMessageDialog.show_success(
|
||
self,
|
||
"Success",
|
||
"Settings have been reset! You may have to restart the pipeline.",
|
||
show_cancel=False,
|
||
confirm_text="OK"
|
||
)
|
||
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(int(setting.minval * 100000.0))
|
||
slider.setMaximum(int(setting.maxval * 100000.0))
|
||
slider.setValue(int(setting.value * 100000.0))
|
||
elif setting.type == "int":
|
||
box.setSingleStep(int(max(1, (setting.maxval - setting.minval) / 32)))
|
||
slider.setMinimum(int(setting.minval))
|
||
slider.setMaximum(int(setting.maxval))
|
||
slider.setValue(int(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)
|
||
|
||
def communication_thread(self):
|
||
"""监听来自主程序的通信文件"""
|
||
import json
|
||
import tempfile
|
||
|
||
comm_dir = os.path.join(tempfile.gettempdir(), "daytime_editor_comm")
|
||
comm_file = os.path.join(comm_dir, "sun_azimuth_command.json")
|
||
|
||
print("🔄 Day Time Editor通信监听已启动")
|
||
print(f" 监听文件: {comm_file}")
|
||
|
||
last_timestamp = 0
|
||
|
||
while True:
|
||
try:
|
||
if os.path.exists(comm_file):
|
||
with open(comm_file, 'r') as f:
|
||
command = json.load(f)
|
||
|
||
# 检查是否是新命令(避免重复执行)
|
||
if command.get('timestamp', 0) > last_timestamp:
|
||
last_timestamp = command.get('timestamp', 0)
|
||
|
||
if command.get('command') == 'set_sun_azimuth':
|
||
azimuth_value = command.get('value', 0)
|
||
print(f"📨 收到Sun Azimuth命令: {azimuth_value}°")
|
||
|
||
# 在主线程中执行UI更新
|
||
QTimer.singleShot(0, partial(self.apply_sun_azimuth, azimuth_value))
|
||
|
||
time.sleep(0.1) # 100ms检查一次
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 通信监听错误: {e}")
|
||
time.sleep(1) # 出错时等待1秒再重试
|
||
|
||
def apply_sun_azimuth(self, azimuth_degrees):
|
||
"""应用Sun Azimuth值到Day Time Editor"""
|
||
try:
|
||
print(f"🌞 正在应用Sun Azimuth: {azimuth_degrees}°")
|
||
|
||
# 查找Sun Azimuth相关的控件
|
||
# 这里需要根据实际的UI结构来实现
|
||
# 先尝试找到可能的控件
|
||
|
||
# 方法1:查找包含"azimuth"的控件
|
||
for widget_name in dir(self):
|
||
widget = getattr(self, widget_name, None)
|
||
if widget and hasattr(widget, 'setValue'):
|
||
if 'azimuth' in widget_name.lower() or 'sun' in widget_name.lower():
|
||
print(f" 尝试设置控件: {widget_name}")
|
||
try:
|
||
widget.setValue(azimuth_degrees)
|
||
print(f"✅ 成功设置 {widget_name} = {azimuth_degrees}°")
|
||
return
|
||
except Exception as e:
|
||
print(f" 设置失败: {e}")
|
||
|
||
# 方法2:查找表格中的Sun Azimuth设置
|
||
if hasattr(self, 'table_plugin_settings'):
|
||
table = self.table_plugin_settings
|
||
for row in range(table.rowCount()):
|
||
item = table.item(row, 0) # 第一列通常是设置名称
|
||
if item and 'azimuth' in item.text().lower():
|
||
# 找到Sun Azimuth行,设置第二列的值
|
||
value_item = table.item(row, 1)
|
||
if value_item:
|
||
value_item.setText(str(azimuth_degrees))
|
||
print(f"✅ 在表格中设置Sun Azimuth = {azimuth_degrees}°")
|
||
return
|
||
|
||
print("⚠️ 未找到Sun Azimuth控件,请检查UI结构")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 应用Sun Azimuth失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
# Start application
|
||
app = QApplication(sys.argv)
|
||
qt_register_fonts()
|
||
configurator = PluginConfigurator()
|
||
configurator.show()
|
||
app.exec_()
|