mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 16:58:12 +00:00
Compare commits
9 Commits
master
...
feature/10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da03e30a2 | ||
|
|
4a566ec7c3 | ||
|
|
085d39fde8 | ||
|
|
d5f1224c33 | ||
|
|
e97e3d74c8 | ||
|
|
1af4169447 | ||
|
|
ea728f7809 | ||
|
|
a4e6fca73d | ||
|
|
9aafb5c0fb |
@@ -8,8 +8,7 @@ from src.engines.blender.blender_engine import Blender
|
|||||||
from src.engines.core.base_downloader import EngineDownloader
|
from src.engines.core.base_downloader import EngineDownloader
|
||||||
from src.utilities.misc_helper import current_system_os, current_system_cpu
|
from src.utilities.misc_helper import current_system_os, current_system_cpu
|
||||||
|
|
||||||
# url = "https://download.blender.org/release/"
|
url = "https://download.blender.org/release/"
|
||||||
url = "https://ftp.nluug.nl/pub/graphics/blender/release/" # much faster mirror for testing
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ class EngineManager:
|
|||||||
def supported_engines():
|
def supported_engines():
|
||||||
return [Blender, FFMPEG]
|
return [Blender, FFMPEG]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def downloadable_engines(cls):
|
||||||
|
return [engine for engine in cls.supported_engines() if hasattr(engine, "downloader") and engine.downloader()]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def engine_with_name(cls, engine_name):
|
def engine_with_name(cls, engine_name):
|
||||||
for obj in cls.supported_engines():
|
for obj in cls.supported_engines():
|
||||||
@@ -30,7 +34,7 @@ class EngineManager:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_engines(cls, filter_name=None, include_corrupt=False):
|
def get_engines(cls, filter_name=None, include_corrupt=False, ignore_system=False):
|
||||||
|
|
||||||
if not cls.engines_path:
|
if not cls.engines_path:
|
||||||
raise FileNotFoundError("Engine path is not set")
|
raise FileNotFoundError("Engine path is not set")
|
||||||
@@ -92,6 +96,7 @@ class EngineManager:
|
|||||||
'type': 'system'
|
'type': 'system'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if not ignore_system:
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
futures = {
|
futures = {
|
||||||
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
||||||
@@ -107,31 +112,31 @@ class EngineManager:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all_versions_for_engine(cls, engine_name, include_corrupt=False):
|
def all_versions_for_engine(cls, engine_name, include_corrupt=False, ignore_system=False):
|
||||||
versions = cls.get_engines(filter_name=engine_name, include_corrupt=include_corrupt)
|
versions = cls.get_engines(filter_name=engine_name, include_corrupt=include_corrupt, ignore_system=ignore_system)
|
||||||
sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True)
|
sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True)
|
||||||
return sorted_versions
|
return sorted_versions
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def newest_engine_version(cls, engine, system_os=None, cpu=None):
|
def newest_engine_version(cls, engine, system_os=None, cpu=None, ignore_system=None):
|
||||||
system_os = system_os or current_system_os()
|
system_os = system_os or current_system_os()
|
||||||
cpu = cpu or current_system_cpu()
|
cpu = cpu or current_system_cpu()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filtered = [x for x in cls.all_versions_for_engine(engine) if x['system_os'] == system_os and
|
filtered = [x for x in cls.all_versions_for_engine(engine, ignore_system=ignore_system)
|
||||||
x['cpu'] == cpu]
|
if x['system_os'] == system_os and x['cpu'] == cpu]
|
||||||
return filtered[0]
|
return filtered[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}")
|
logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None):
|
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None, ignore_system=False):
|
||||||
system_os = system_os or current_system_os()
|
system_os = system_os or current_system_os()
|
||||||
cpu = cpu or current_system_cpu()
|
cpu = cpu or current_system_cpu()
|
||||||
|
|
||||||
filtered = [x for x in cls.get_engines(filter_name=engine) if x['system_os'] == system_os and
|
filtered = [x for x in cls.get_engines(filter_name=engine, ignore_system=ignore_system) if
|
||||||
x['cpu'] == cpu and x['version'] == version]
|
x['system_os'] == system_os and x['cpu'] == cpu and x['version'] == version]
|
||||||
return filtered[0] if filtered else False
|
return filtered[0] if filtered else False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -164,7 +169,7 @@ class EngineManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download_engine(cls, engine, version, system_os=None, cpu=None, background=False):
|
def download_engine(cls, engine, version, system_os=None, cpu=None, background=False, ignore_system=False):
|
||||||
|
|
||||||
engine_to_download = cls.engine_with_name(engine)
|
engine_to_download = cls.engine_with_name(engine)
|
||||||
existing_task = cls.get_existing_download_task(engine, version, system_os, cpu)
|
existing_task = cls.get_existing_download_task(engine, version, system_os, cpu)
|
||||||
@@ -187,7 +192,7 @@ class EngineManager:
|
|||||||
return thread
|
return thread
|
||||||
|
|
||||||
thread.join()
|
thread.join()
|
||||||
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu) # Check that engine downloaded
|
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu, ignore_system) # Check that engine downloaded
|
||||||
if not found_engine:
|
if not found_engine:
|
||||||
logger.error(f"Error downloading {engine}")
|
logger.error(f"Error downloading {engine}")
|
||||||
return found_engine
|
return found_engine
|
||||||
@@ -213,31 +218,21 @@ class EngineManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_all_engines(cls):
|
def is_engine_update_available(cls, engine_class, ignore_system_installs=False):
|
||||||
def engine_update_task(engine_class):
|
|
||||||
logger.debug(f"Checking for updates to {engine_class.name()}")
|
logger.debug(f"Checking for updates to {engine_class.name()}")
|
||||||
latest_version = engine_class.downloader().find_most_recent_version()
|
latest_version = engine_class.downloader().find_most_recent_version()
|
||||||
|
|
||||||
if not latest_version:
|
if not latest_version:
|
||||||
logger.warning(f"Could not find most recent version of {engine.name()} to download")
|
logger.warning(f"Could not find most recent version of {engine_class.name()} to download")
|
||||||
return
|
return
|
||||||
|
|
||||||
version_num = latest_version.get('version')
|
version_num = latest_version.get('version')
|
||||||
if cls.is_version_downloaded(engine_class.name(), version_num):
|
if cls.is_version_downloaded(engine_class.name(), version_num, ignore_system=ignore_system_installs):
|
||||||
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
|
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
|
||||||
return
|
return
|
||||||
|
|
||||||
# download the engine
|
return latest_version
|
||||||
logger.info(f"Downloading latest version of {engine_class.name()} ({version_num})...")
|
|
||||||
cls.download_engine(engine=engine_class.name(), version=version_num, background=True)
|
|
||||||
|
|
||||||
logger.info(f"Checking for updates for render engines...")
|
|
||||||
threads = []
|
|
||||||
for engine in cls.supported_engines():
|
|
||||||
if engine.downloader():
|
|
||||||
thread = threading.Thread(target=engine_update_task, args=(engine,))
|
|
||||||
threads.append(thread)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_worker(cls, renderer, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
|
def create_worker(cls, renderer, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
|
||||||
@@ -305,7 +300,8 @@ class EngineDownloadWorker(threading.Thread):
|
|||||||
self.cpu = cpu
|
self.cpu = cpu
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu)
|
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu,
|
||||||
|
ignore_system=True)
|
||||||
if existing_download:
|
if existing_download:
|
||||||
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
|
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
|
||||||
return existing_download
|
return existing_download
|
||||||
|
|||||||
20
src/init.py
20
src/init.py
@@ -5,6 +5,9 @@ import socket
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QSettings
|
||||||
|
|
||||||
from src.api.api_server import start_server
|
from src.api.api_server import start_server
|
||||||
from src.api.preview_manager import PreviewManager
|
from src.api.preview_manager import PreviewManager
|
||||||
@@ -16,7 +19,7 @@ from src.utilities.config import Config
|
|||||||
from src.utilities.misc_helper import (system_safe_path, current_system_cpu, current_system_os,
|
from src.utilities.misc_helper import (system_safe_path, current_system_cpu, current_system_os,
|
||||||
current_system_os_version, check_for_updates)
|
current_system_os_version, check_for_updates)
|
||||||
from src.utilities.zeroconf_server import ZeroconfServer
|
from src.utilities.zeroconf_server import ZeroconfServer
|
||||||
from version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
|
from version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER, APP_AUTHOR
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
@@ -66,6 +69,8 @@ def run(server_only=False) -> int:
|
|||||||
APP_VERSION))
|
APP_VERSION))
|
||||||
update_thread.start()
|
update_thread.start()
|
||||||
|
|
||||||
|
settings = QSettings(APP_AUTHOR, APP_NAME)
|
||||||
|
|
||||||
# main start
|
# main start
|
||||||
logger.info(f"Starting {APP_NAME} Render Server")
|
logger.info(f"Starting {APP_NAME} Render Server")
|
||||||
return_code = 0
|
return_code = 0
|
||||||
@@ -92,9 +97,16 @@ def run(server_only=False) -> int:
|
|||||||
ServerProxyManager.subscribe_to_listener()
|
ServerProxyManager.subscribe_to_listener()
|
||||||
DistributedJobManager.subscribe_to_listener()
|
DistributedJobManager.subscribe_to_listener()
|
||||||
|
|
||||||
# check for updates for render engines if configured or on first launch
|
# check for updates for render engines if configured
|
||||||
if Config.update_engines_on_launch or not EngineManager.get_engines():
|
ignore_system = settings.value("engines_ignore_system_installs", False)
|
||||||
EngineManager.update_all_engines()
|
if settings.value('check_for_engine_updates_on_launch', False):
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
if settings.value(f'engine_download-{engine.name()}', False):
|
||||||
|
update_result = EngineManager.is_engine_update_available(engine, ignore_system_installs=ignore_system)
|
||||||
|
EngineManager.download_engine(engine=engine.name(), version=update_result['version'],
|
||||||
|
background=True,
|
||||||
|
ignore_system=ignore_system)
|
||||||
|
settings.setValue("engines_last_update_time", datetime.now().isoformat())
|
||||||
|
|
||||||
# get hostname
|
# get hostname
|
||||||
local_hostname = socket.gethostname()
|
local_hostname = socket.gethostname()
|
||||||
|
|||||||
481
src/ui/settings_window.py
Normal file
481
src/ui/settings_window.py
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import humanize
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from PyQt6 import QtCore
|
||||||
|
from PyQt6.QtCore import Qt, QSettings, pyqtSignal as Signal
|
||||||
|
from PyQt6.QtGui import QIcon
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QListWidget, QListWidgetItem, QStackedWidget, QVBoxLayout, \
|
||||||
|
QWidget, QLabel, QCheckBox, QLineEdit, \
|
||||||
|
QComboBox, QPushButton, QHBoxLayout, QGroupBox, QTableWidget, QAbstractItemView, QTableWidgetItem, QHeaderView, \
|
||||||
|
QMessageBox
|
||||||
|
|
||||||
|
from api.server_proxy import RenderServerProxy
|
||||||
|
from engines.engine_manager import EngineManager
|
||||||
|
from utilities.config import Config
|
||||||
|
from utilities.misc_helper import launch_url, system_safe_path
|
||||||
|
from version import APP_AUTHOR, APP_NAME
|
||||||
|
|
||||||
|
settings = QSettings(APP_AUTHOR, APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsWindow(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
if not EngineManager.engines_path: # fix issue where sometimes path was not set
|
||||||
|
EngineManager.engines_path = system_safe_path(
|
||||||
|
os.path.join(os.path.join(os.path.expanduser(Config.upload_folder),
|
||||||
|
'engines')))
|
||||||
|
|
||||||
|
self.installed_engines_table = None
|
||||||
|
|
||||||
|
self.setWindowTitle("Settings")
|
||||||
|
|
||||||
|
# Create the main layout
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Create the sidebar (QListWidget) for navigation
|
||||||
|
self.sidebar = QListWidget()
|
||||||
|
self.sidebar.setFixedWidth(150)
|
||||||
|
|
||||||
|
# Set the icon size
|
||||||
|
self.sidebar.setIconSize(QtCore.QSize(32, 32)) # Increase the icon size to 32x32 pixels
|
||||||
|
|
||||||
|
# Adjust the font size for the sidebar items
|
||||||
|
font = self.sidebar.font()
|
||||||
|
font.setPointSize(12) # Increase the font size
|
||||||
|
self.sidebar.setFont(font)
|
||||||
|
|
||||||
|
# Add items with icons to the sidebar
|
||||||
|
resources_dir = os.path.join(Path(__file__).resolve().parent.parent.parent, 'resources')
|
||||||
|
self.add_sidebar_item("General", os.path.join(resources_dir, "Gear.png"))
|
||||||
|
self.add_sidebar_item("Server", os.path.join(resources_dir, "Server.png"))
|
||||||
|
self.add_sidebar_item("Engines", os.path.join(resources_dir, "Blender.png"))
|
||||||
|
self.sidebar.setCurrentRow(0)
|
||||||
|
|
||||||
|
# Create the stacked widget to hold different settings pages
|
||||||
|
self.stacked_widget = QStackedWidget()
|
||||||
|
|
||||||
|
# Create pages for each section
|
||||||
|
general_page = self.create_general_page()
|
||||||
|
network_page = self.create_network_page()
|
||||||
|
engines_page = self.create_engines_page()
|
||||||
|
|
||||||
|
# Add pages to the stacked widget
|
||||||
|
self.stacked_widget.addWidget(general_page)
|
||||||
|
self.stacked_widget.addWidget(network_page)
|
||||||
|
self.stacked_widget.addWidget(engines_page)
|
||||||
|
|
||||||
|
# Connect the sidebar to the stacked widget
|
||||||
|
self.sidebar.currentRowChanged.connect(self.stacked_widget.setCurrentIndex)
|
||||||
|
|
||||||
|
# Create a horizontal layout to hold the sidebar and stacked widget
|
||||||
|
content_layout = QHBoxLayout()
|
||||||
|
content_layout.addWidget(self.sidebar)
|
||||||
|
content_layout.addWidget(self.stacked_widget)
|
||||||
|
|
||||||
|
# Add the content layout to the main layout
|
||||||
|
main_layout.addLayout(content_layout)
|
||||||
|
|
||||||
|
# Add the "OK" button at the bottom
|
||||||
|
ok_button = QPushButton("OK")
|
||||||
|
ok_button.clicked.connect(self.close)
|
||||||
|
ok_button.setFixedWidth(80)
|
||||||
|
ok_button.setDefault(True)
|
||||||
|
main_layout.addWidget(ok_button, alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
|
|
||||||
|
# Create a central widget and set the layout
|
||||||
|
central_widget = QWidget()
|
||||||
|
central_widget.setLayout(main_layout)
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
self.setMinimumSize(700, 400)
|
||||||
|
|
||||||
|
def add_sidebar_item(self, name, icon_path):
|
||||||
|
"""Add an item with an icon to the sidebar."""
|
||||||
|
item = QListWidgetItem(QIcon(icon_path), name)
|
||||||
|
self.sidebar.addItem(item)
|
||||||
|
|
||||||
|
def create_general_page(self):
|
||||||
|
"""Create the General settings page."""
|
||||||
|
page = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Startup Settings Group
|
||||||
|
startup_group = QGroupBox("Startup Settings")
|
||||||
|
startup_layout = QVBoxLayout()
|
||||||
|
# startup_layout.addWidget(QCheckBox("Start application on system startup"))
|
||||||
|
check_for_updates_checkbox = QCheckBox("Check for updates automatically")
|
||||||
|
check_for_updates_checkbox.setChecked(settings.value("auto_check_for_updates", True))
|
||||||
|
check_for_updates_checkbox.stateChanged.connect(lambda state: settings.setValue("auto_check_for_updates", bool(state)))
|
||||||
|
startup_layout.addWidget(check_for_updates_checkbox)
|
||||||
|
startup_group.setLayout(startup_layout)
|
||||||
|
|
||||||
|
# Local Files Group
|
||||||
|
data_path = os.path.expanduser(Config.upload_folder)
|
||||||
|
path_size = sum(f.stat().st_size for f in Path(data_path).rglob('*') if f.is_file())
|
||||||
|
database_group = QGroupBox("Local Files")
|
||||||
|
database_layout = QVBoxLayout()
|
||||||
|
database_layout.addWidget(QLabel(f"Local Directory: {data_path}"))
|
||||||
|
database_layout.addWidget(QLabel(f"Size: {humanize.naturalsize(path_size, binary=True)}"))
|
||||||
|
open_database_path_button = QPushButton("Open Directory")
|
||||||
|
open_database_path_button.clicked.connect(lambda: launch_url(data_path))
|
||||||
|
open_database_path_button.setFixedWidth(200)
|
||||||
|
database_layout.addWidget(open_database_path_button)
|
||||||
|
database_group.setLayout(database_layout)
|
||||||
|
|
||||||
|
|
||||||
|
# Render Settings Group
|
||||||
|
render_settings_group = QGroupBox("Render Settings")
|
||||||
|
render_settings_layout = QVBoxLayout()
|
||||||
|
render_settings_layout.addWidget(QLabel("Restrict to render nodes with same:"))
|
||||||
|
require_same_engine_checkbox = QCheckBox("Renderer Version")
|
||||||
|
require_same_engine_checkbox.setChecked(settings.value("render_require_same_engine_version"))
|
||||||
|
require_same_engine_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_engine_version", bool(state)))
|
||||||
|
render_settings_layout.addWidget(require_same_engine_checkbox)
|
||||||
|
require_same_cpu_checkbox = QCheckBox("CPU Architecture")
|
||||||
|
require_same_cpu_checkbox.setChecked(settings.value("render_require_same_cpu_type"))
|
||||||
|
require_same_cpu_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_cpu_type", bool(state)))
|
||||||
|
render_settings_layout.addWidget(require_same_cpu_checkbox)
|
||||||
|
require_same_os_checkbox = QCheckBox("Operating System")
|
||||||
|
require_same_os_checkbox.setChecked(settings.value("render_require_same_os"))
|
||||||
|
require_same_os_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_os", bool(state)))
|
||||||
|
render_settings_layout.addWidget(require_same_os_checkbox)
|
||||||
|
render_settings_group.setLayout(render_settings_layout)
|
||||||
|
|
||||||
|
layout.addWidget(startup_group)
|
||||||
|
layout.addWidget(database_group)
|
||||||
|
layout.addWidget(render_settings_group)
|
||||||
|
|
||||||
|
layout.addStretch() # Add a stretch to push content to the top
|
||||||
|
page.setLayout(layout)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def create_network_page(self):
|
||||||
|
"""Create the Network settings page."""
|
||||||
|
page = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Sharing Settings Group
|
||||||
|
sharing_group = QGroupBox("Sharing Settings")
|
||||||
|
sharing_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
enable_sharing_checkbox = QCheckBox("Enable other computers on the network to render to this machine")
|
||||||
|
enable_sharing_checkbox.setChecked(settings.value("enable_network_sharing", False))
|
||||||
|
enable_sharing_checkbox.stateChanged.connect(self.toggle_render_sharing)
|
||||||
|
sharing_layout.addWidget(enable_sharing_checkbox)
|
||||||
|
|
||||||
|
password_layout = QHBoxLayout()
|
||||||
|
password_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.enable_network_password_checkbox = QCheckBox("Enable network password:")
|
||||||
|
self.enable_network_password_checkbox.setChecked(settings.value("enable_network_password", False))
|
||||||
|
self.enable_network_password_checkbox.stateChanged.connect(self.enable_network_password_changed)
|
||||||
|
sharing_layout.addWidget(self.enable_network_password_checkbox)
|
||||||
|
self.network_password_line = QLineEdit()
|
||||||
|
self.network_password_line.setPlaceholderText("Enter a password")
|
||||||
|
self.network_password_line.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
self.network_password_line.setEnabled(settings.value("enable_network_password", False))
|
||||||
|
password_layout.addWidget(self.network_password_line)
|
||||||
|
self.show_password_button = QPushButton("Show")
|
||||||
|
self.show_password_button.setEnabled(settings.value("enable_network_password", False))
|
||||||
|
self.show_password_button.clicked.connect(self.show_password_button_pressed)
|
||||||
|
password_layout.addWidget(self.show_password_button)
|
||||||
|
sharing_layout.addLayout(password_layout)
|
||||||
|
|
||||||
|
sharing_group.setLayout(sharing_layout)
|
||||||
|
|
||||||
|
layout.addWidget(sharing_group)
|
||||||
|
|
||||||
|
layout.addStretch() # Add a stretch to push content to the top
|
||||||
|
page.setLayout(layout)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def toggle_render_sharing(self, enable_sharing):
|
||||||
|
settings.setValue("enable_network_sharing", enable_sharing)
|
||||||
|
self.enable_network_password_checkbox.setEnabled(enable_sharing)
|
||||||
|
enable_password = enable_sharing and settings.value("enable_network_password", False)
|
||||||
|
self.network_password_line.setEnabled(enable_password)
|
||||||
|
self.show_password_button.setEnabled(enable_password)
|
||||||
|
|
||||||
|
def enable_network_password_changed(self, new_value):
|
||||||
|
settings.setValue("enable_network_password", new_value)
|
||||||
|
self.network_password_line.setEnabled(new_value)
|
||||||
|
self.show_password_button.setEnabled(new_value)
|
||||||
|
|
||||||
|
def show_password_button_pressed(self):
|
||||||
|
# toggle showing / hiding the password
|
||||||
|
show_pass = self.show_password_button.text() == "Show"
|
||||||
|
self.show_password_button.setText("Hide" if show_pass else "Show")
|
||||||
|
self.network_password_line.setEchoMode(QLineEdit.EchoMode.Normal if show_pass else QLineEdit.EchoMode.Normal)
|
||||||
|
|
||||||
|
def create_engines_page(self):
|
||||||
|
"""Create the Engines settings page."""
|
||||||
|
page = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Installed Engines Group
|
||||||
|
installed_group = QGroupBox("Installed Engines")
|
||||||
|
installed_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Setup table
|
||||||
|
self.installed_engines_table = EngineTableWidget()
|
||||||
|
self.installed_engines_table.row_selected.connect(self.engine_table_selected)
|
||||||
|
installed_layout.addWidget(self.installed_engines_table)
|
||||||
|
|
||||||
|
# Ignore system installs
|
||||||
|
engine_ignore_system_installs_checkbox = QCheckBox("Ignore system installs")
|
||||||
|
engine_ignore_system_installs_checkbox.setChecked(settings.value("engines_ignore_system_installs", False))
|
||||||
|
engine_ignore_system_installs_checkbox.stateChanged.connect(self.change_ignore_system_installs)
|
||||||
|
installed_layout.addWidget(engine_ignore_system_installs_checkbox)
|
||||||
|
|
||||||
|
# Engine Launch / Delete buttons
|
||||||
|
installed_buttons_layout = QHBoxLayout()
|
||||||
|
self.launch_engine_button = QPushButton("Launch")
|
||||||
|
self.launch_engine_button.setEnabled(False)
|
||||||
|
self.launch_engine_button.clicked.connect(self.launch_selected_engine)
|
||||||
|
self.delete_engine_button = QPushButton("Delete")
|
||||||
|
self.delete_engine_button.setEnabled(False)
|
||||||
|
self.delete_engine_button.clicked.connect(self.delete_selected_engine)
|
||||||
|
|
||||||
|
installed_buttons_layout.addWidget(self.launch_engine_button)
|
||||||
|
installed_buttons_layout.addWidget(self.delete_engine_button)
|
||||||
|
installed_layout.addLayout(installed_buttons_layout)
|
||||||
|
installed_group.setLayout(installed_layout)
|
||||||
|
|
||||||
|
# Engine Updates Group
|
||||||
|
engine_updates_group = QGroupBox("Auto-Install")
|
||||||
|
engine_updates_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
engine_download_layout = QHBoxLayout()
|
||||||
|
engine_download_layout.addWidget(QLabel("Enable Downloads for:"))
|
||||||
|
|
||||||
|
at_least_one_downloadable = False
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
engine_download_check = QCheckBox(engine.name())
|
||||||
|
is_checked = settings.value(f"engine_download-{engine.name()}", False)
|
||||||
|
at_least_one_downloadable |= is_checked
|
||||||
|
engine_download_check.setChecked(is_checked)
|
||||||
|
# Capture the checkbox correctly using a default argument in lambda
|
||||||
|
engine_download_check.clicked.connect(
|
||||||
|
lambda state, checkbox=engine_download_check: self.engine_download_settings_changed(state, checkbox.text())
|
||||||
|
)
|
||||||
|
engine_download_layout.addWidget(engine_download_check)
|
||||||
|
|
||||||
|
engine_updates_layout.addLayout(engine_download_layout)
|
||||||
|
|
||||||
|
check_for_engine_updates_checkbox = QCheckBox("Check for new versions on launch")
|
||||||
|
check_for_engine_updates_checkbox.setChecked(settings.value('check_for_engine_updates_on_launch', True))
|
||||||
|
check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
|
||||||
|
check_for_engine_updates_checkbox.stateChanged.connect(
|
||||||
|
lambda state: settings.setValue("check_for_engine_updates_on_launch", bool(state)))
|
||||||
|
engine_updates_layout.addWidget(check_for_engine_updates_checkbox)
|
||||||
|
self.engines_last_update_label = QLabel()
|
||||||
|
self.update_last_checked_label()
|
||||||
|
self.engines_last_update_label.setEnabled(at_least_one_downloadable)
|
||||||
|
engine_updates_layout.addWidget(self.engines_last_update_label)
|
||||||
|
self.check_for_new_engines_button = QPushButton("Check for New Versions...")
|
||||||
|
self.check_for_new_engines_button.setEnabled(at_least_one_downloadable)
|
||||||
|
self.check_for_new_engines_button.clicked.connect(self.check_for_new_engines)
|
||||||
|
engine_updates_layout.addWidget(self.check_for_new_engines_button)
|
||||||
|
engine_updates_group.setLayout(engine_updates_layout)
|
||||||
|
|
||||||
|
layout.addWidget(installed_group)
|
||||||
|
layout.addWidget(engine_updates_group)
|
||||||
|
|
||||||
|
layout.addStretch() # Add a stretch to push content to the top
|
||||||
|
page.setLayout(layout)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def change_ignore_system_installs(self, value):
|
||||||
|
settings.setValue("engines_ignore_system_installs", bool(value))
|
||||||
|
self.installed_engines_table.update_table()
|
||||||
|
|
||||||
|
def update_last_checked_label(self):
|
||||||
|
"""Retrieve the last check timestamp and return a human-friendly string."""
|
||||||
|
last_checked_str = settings.value("engines_last_update_time", None)
|
||||||
|
if not last_checked_str:
|
||||||
|
time_string = "Never"
|
||||||
|
else:
|
||||||
|
last_checked_dt = datetime.fromisoformat(last_checked_str)
|
||||||
|
now = datetime.now()
|
||||||
|
time_string = humanize.naturaltime(now - last_checked_dt)
|
||||||
|
self.engines_last_update_label.setText(f"Last Updated: {time_string}")
|
||||||
|
|
||||||
|
def engine_download_settings_changed(self, state, engine_name):
|
||||||
|
settings.setValue(f"engine_download-{engine_name}", state)
|
||||||
|
|
||||||
|
at_least_one_downloadable = False
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
at_least_one_downloadable |= settings.value(f"engine_download-{engine.name()}", False)
|
||||||
|
self.check_for_new_engines_button.setEnabled(at_least_one_downloadable)
|
||||||
|
self.check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
|
||||||
|
self.engines_last_update_label.setEnabled(at_least_one_downloadable)
|
||||||
|
|
||||||
|
def delete_selected_engine(self):
|
||||||
|
engine_info = self.installed_engines_table.selected_engine_data()
|
||||||
|
reply = QMessageBox.question(self, f"Delete {engine_info['engine']} {engine_info['version']}?",
|
||||||
|
f"Do you want to delete {engine_info['engine']} {engine_info['version']}?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
|
||||||
|
if reply is not QMessageBox.StandardButton.Yes:
|
||||||
|
return
|
||||||
|
|
||||||
|
delete_result = EngineManager.delete_engine_download(engine_info.get('engine'),
|
||||||
|
engine_info.get('version'),
|
||||||
|
engine_info.get('system_os'),
|
||||||
|
engine_info.get('cpu'))
|
||||||
|
|
||||||
|
if delete_result:
|
||||||
|
QMessageBox.information(self, f"{engine_info['engine']} {engine_info['version']} Deleted",
|
||||||
|
f"{engine_info['engine']} {engine_info['version']} deleted successfully",
|
||||||
|
QMessageBox.StandardButton.Ok)
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, f"Unknown Error",
|
||||||
|
f"Unknown error while deleting {engine_info['engine']} {engine_info['version']}.",
|
||||||
|
QMessageBox.StandardButton.Ok)
|
||||||
|
|
||||||
|
self.installed_engines_table.update_table(use_cached=False)
|
||||||
|
|
||||||
|
def launch_selected_engine(self):
|
||||||
|
engine_info = self.installed_engines_table.selected_engine_data()
|
||||||
|
if engine_info:
|
||||||
|
launch_url(engine_info['path'])
|
||||||
|
|
||||||
|
def engine_table_selected(self):
|
||||||
|
engine_data = self.installed_engines_table.selected_engine_data()
|
||||||
|
if engine_data:
|
||||||
|
self.launch_engine_button.setEnabled(bool(engine_data.get('path') or True))
|
||||||
|
self.delete_engine_button.setEnabled(engine_data.get('type') == 'managed')
|
||||||
|
else:
|
||||||
|
self.launch_engine_button.setEnabled(False)
|
||||||
|
self.delete_engine_button.setEnabled(False)
|
||||||
|
|
||||||
|
def check_for_new_engines(self):
|
||||||
|
|
||||||
|
ignore_system = settings.value("engines_ignore_system_installs", False)
|
||||||
|
messagebox_shown = False
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
if settings.value(f'engine_download-{engine.name()}', False):
|
||||||
|
result = EngineManager.is_engine_update_available(engine, ignore_system_installs=ignore_system)
|
||||||
|
if result:
|
||||||
|
result['name'] = engine.name()
|
||||||
|
msg_box = QMessageBox()
|
||||||
|
msg_box.setWindowTitle(f"{result['name']} ({result['version']}) Available")
|
||||||
|
msg_box.setText(f"A new version of {result['name']} is available ({result['version']}).\n\n"
|
||||||
|
f"Would you like to download it now?")
|
||||||
|
msg_box.setIcon(QMessageBox.Icon.Question)
|
||||||
|
msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
msg_result = msg_box.exec()
|
||||||
|
messagebox_shown = True
|
||||||
|
if msg_result == QMessageBox.StandardButton.Yes:
|
||||||
|
EngineManager.download_engine(engine=engine.name(), version=result['version'], background=True,
|
||||||
|
ignore_system=ignore_system)
|
||||||
|
self.update_engine_download_status()
|
||||||
|
|
||||||
|
if not messagebox_shown:
|
||||||
|
msg_box = QMessageBox()
|
||||||
|
msg_box.setWindowTitle("No Updates Available")
|
||||||
|
msg_box.setText("All your render engines are up-to-date.")
|
||||||
|
msg_box.setIcon(QMessageBox.Icon.Information)
|
||||||
|
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||||
|
msg_box.exec()
|
||||||
|
|
||||||
|
settings.setValue("engines_last_update_time", datetime.now().isoformat())
|
||||||
|
self.update_engine_download_status()
|
||||||
|
|
||||||
|
def update_engine_download_status(self):
|
||||||
|
running_tasks = [x for x in EngineManager.download_tasks if x.is_alive()]
|
||||||
|
if not running_tasks:
|
||||||
|
self.update_last_checked_label()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.engines_last_update_label.setText(f"Downloading {running_tasks[0].engine} ({running_tasks[0].version})...")
|
||||||
|
|
||||||
|
|
||||||
|
class EngineTableWidget(QWidget):
|
||||||
|
row_selected = Signal()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.table = QTableWidget(0, 4)
|
||||||
|
self.table.setHorizontalHeaderLabels(["Engine", "Version", "Type", "Path"])
|
||||||
|
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
self.table.verticalHeader().setVisible(False)
|
||||||
|
# self.table_widget.itemSelectionChanged.connect(self.engine_picked)
|
||||||
|
self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
|
self.table.selectionModel().selectionChanged.connect(self.on_selection_changed)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
layout.addWidget(self.table)
|
||||||
|
|
||||||
|
self.raw_server_data = None
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
"""Runs when the widget is about to be shown."""
|
||||||
|
self.update_table()
|
||||||
|
super().showEvent(event) # Ensure normal event processing
|
||||||
|
|
||||||
|
def update_table(self, use_cached=True):
|
||||||
|
if not self.raw_server_data or not use_cached:
|
||||||
|
self.raw_server_data = RenderServerProxy(socket.gethostname()).get_renderer_info()
|
||||||
|
if not self.raw_server_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
table_data = [] # convert the data into a flat list
|
||||||
|
for _, engine_data in self.raw_server_data.items():
|
||||||
|
table_data.extend(engine_data['versions'])
|
||||||
|
|
||||||
|
if settings.value("engines_ignore_system_installs", False):
|
||||||
|
table_data = [x for x in table_data if x['type'] != 'system']
|
||||||
|
|
||||||
|
self.table.clear()
|
||||||
|
self.table.setRowCount(len(table_data))
|
||||||
|
self.table.setColumnCount(4)
|
||||||
|
|
||||||
|
self.table.setHorizontalHeaderLabels(['Engine', 'Version', 'Type', 'Path'])
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||||
|
|
||||||
|
for row, engine in enumerate(table_data):
|
||||||
|
self.table.setItem(row, 0, QTableWidgetItem(engine['engine']))
|
||||||
|
self.table.setItem(row, 1, QTableWidgetItem(engine['version']))
|
||||||
|
self.table.setItem(row, 2, QTableWidgetItem(engine['type']))
|
||||||
|
self.table.setItem(row, 3, QTableWidgetItem(engine['path']))
|
||||||
|
|
||||||
|
self.table.selectRow(0)
|
||||||
|
|
||||||
|
def selected_engine_data(self):
|
||||||
|
"""Returns the data from the selected row as a dictionary."""
|
||||||
|
row = self.table.currentRow() # Get the selected row index
|
||||||
|
|
||||||
|
if row < 0 or not len(self.table.selectedItems()): # No row selected
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"engine": self.table.item(row, 0).text(),
|
||||||
|
"version": self.table.item(row, 1).text(),
|
||||||
|
"type": self.table.item(row, 2).text(),
|
||||||
|
"path": self.table.item(row, 3).text(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def on_selection_changed(self):
|
||||||
|
self.row_selected.emit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication([])
|
||||||
|
window = SettingsWindow()
|
||||||
|
window.show()
|
||||||
|
app.exec()
|
||||||
@@ -14,6 +14,8 @@ class MenuBar(QMenuBar):
|
|||||||
def __init__(self, parent=None) -> None:
|
def __init__(self, parent=None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.settings_window = None
|
||||||
|
|
||||||
# setup menus
|
# setup menus
|
||||||
file_menu = self.addMenu("File")
|
file_menu = self.addMenu("File")
|
||||||
# edit_menu = self.addMenu("Edit")
|
# edit_menu = self.addMenu("Edit")
|
||||||
@@ -30,7 +32,7 @@ class MenuBar(QMenuBar):
|
|||||||
settings_action = QAction("Settings...", self)
|
settings_action = QAction("Settings...", self)
|
||||||
settings_action.triggered.connect(self.show_settings)
|
settings_action.triggered.connect(self.show_settings)
|
||||||
settings_action.setShortcut(f'Ctrl+,')
|
settings_action.setShortcut(f'Ctrl+,')
|
||||||
# file_menu.addAction(settings_action) # todo: enable once we have a setting screen
|
file_menu.addAction(settings_action)
|
||||||
# exit
|
# exit
|
||||||
exit_action = QAction('&Exit', self)
|
exit_action = QAction('&Exit', self)
|
||||||
exit_action.setShortcut('Ctrl+Q')
|
exit_action.setShortcut('Ctrl+Q')
|
||||||
@@ -49,7 +51,9 @@ class MenuBar(QMenuBar):
|
|||||||
self.parent().new_job()
|
self.parent().new_job()
|
||||||
|
|
||||||
def show_settings(self):
|
def show_settings(self):
|
||||||
pass
|
from src.ui.settings_window import SettingsWindow
|
||||||
|
self.settings_window = SettingsWindow()
|
||||||
|
self.settings_window.show()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_about():
|
def show_about():
|
||||||
|
|||||||
Reference in New Issue
Block a user