Files
OpCore-Simplify/Scripts/widgets/config_editor.py
Hoang Hong Quan 0e608a56ce Add GUI Support for OpCore Simplify (#512)
* Refactor OpCore-Simplify to GUI version

* New ConfigEditor

* Add requirement checks and installation in launchers

* Add GitHub Actions workflow to generate manifest.json

* Set compression level for asset

* Skip .git and __pycache__ folders

* Refactor update process to include integrity checker

* Add SMBIOS model selection

* Update README.md

* Update to main branch
2025-12-30 14:19:47 +07:00

310 lines
13 KiB
Python

from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTreeWidgetItem, QHeaderView, QAbstractItemView
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
from PyQt6.QtGui import QBrush, QColor
from qfluentwidgets import CardWidget, TreeWidget, BodyLabel, StrongBodyLabel
from Scripts.datasets.config_tooltips import get_tooltip
from Scripts.value_formatters import format_value, get_value_type
from Scripts.styles import SPACING, COLORS, RADIUS
class ConfigEditor(QWidget):
config_changed = pyqtSignal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("configEditor")
self.original_config = None
self.modified_config = None
self.context = {}
self.mainLayout = QVBoxLayout(self)
self._init_ui()
def _init_ui(self):
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.setSpacing(0)
card = CardWidget()
card.setBorderRadius(RADIUS["card"])
card_layout = QVBoxLayout(card)
card_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
card_layout.setSpacing(SPACING["medium"])
title = StrongBodyLabel("Config Editor")
card_layout.addWidget(title)
description = BodyLabel("View differences between original and modified config.plist")
description.setStyleSheet("color: {}; font-size: 13px;".format(COLORS["text_secondary"]))
card_layout.addWidget(description)
self.tree = TreeWidget()
self.tree.setHeaderLabels(["Key", "", "Original", "Modified"])
self.tree.setColumnCount(4)
self.tree.setRootIsDecorated(True)
self.tree.setItemsExpandable(True)
self.tree.setExpandsOnDoubleClick(False)
self.tree.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.tree.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.tree.itemExpanded.connect(self._update_tree_height)
self.tree.itemCollapsed.connect(self._update_tree_height)
header = self.tree.header()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
self.tree.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
card_layout.addWidget(self.tree)
self.mainLayout.addWidget(card)
def load_configs(self, original, modified, context=None):
self.original_config = original
self.modified_config = modified
self.context = context or {}
self.tree.clear()
self._render_config(self.original_config, self.modified_config, [])
QTimer.singleShot(0, self._update_tree_height)
def _get_all_keys_from_both_configs(self, original, modified):
original_keys = set(original.keys()) if isinstance(original, dict) else set()
modified_keys = set(modified.keys()) if isinstance(modified, dict) else set()
return sorted(original_keys | modified_keys)
def _determine_change_type(self, original_value, modified_value, key_in_original, key_in_modified):
if not key_in_original:
return "added"
elif not key_in_modified:
return "removed"
elif original_value != modified_value:
return "modified"
return None
def _get_effective_value(self, original_value, modified_value):
return modified_value if modified_value is not None else original_value
def _get_safe_value(self, value, default = None):
return value if value is not None else default
def _set_value_columns(self, item, original_value, modified_value, change_type, is_dict=False, is_array=False):
if original_value is not None:
if is_dict:
item.setText(2, "<object: {} keys>".format(len(original_value)))
elif is_array:
item.setText(2, "<array: {} items>".format(len(original_value)))
else:
item.setText(2, format_value(original_value))
item.setData(2, Qt.ItemDataRole.UserRole, get_value_type(original_value))
else:
item.setText(2, "")
if change_type is not None and modified_value is not None:
if is_dict:
item.setText(3, "<object: {} keys>".format(len(modified_value)))
elif is_array:
item.setText(3, "<array: {} items>".format(len(modified_value)))
else:
item.setText(3, format_value(modified_value))
item.setData(3, Qt.ItemDataRole.UserRole, get_value_type(modified_value))
else:
item.setText(3, "")
def _build_path_string(self, path_parts):
if not path_parts:
return ""
return ".".join(path_parts)
def _build_array_path(self, path_parts, index):
return path_parts + [f"[{index}]"]
def _render_config(self, original, modified, path_parts, parent_item=None):
if parent_item is None:
parent_item = self.tree.invisibleRootItem()
all_keys = self._get_all_keys_from_both_configs(original, modified)
for key in all_keys:
current_path_parts = path_parts + [key]
current_path = self._build_path_string(current_path_parts)
original_value = original.get(key) if isinstance(original, dict) else None
modified_value = modified.get(key) if isinstance(modified, dict) else None
key_in_original = key in original if isinstance(original, dict) else False
key_in_modified = key in modified if isinstance(modified, dict) else False
change_type = self._determine_change_type(
original_value, modified_value, key_in_original, key_in_modified
)
effective_value = self._get_effective_value(original_value, modified_value)
if isinstance(effective_value, list):
self._render_array(
original_value if isinstance(original_value, list) else [],
modified_value if isinstance(modified_value, list) else [],
current_path_parts,
parent_item
)
continue
item = QTreeWidgetItem(parent_item)
item.setText(0, key)
item.setData(0, Qt.ItemDataRole.UserRole, current_path)
self._apply_highlighting(item, change_type)
self._setup_tooltip(item, current_path, modified_value, original_value)
if isinstance(effective_value, dict):
item.setData(3, Qt.ItemDataRole.UserRole, "dict")
self._set_value_columns(item, original_value, modified_value, change_type, is_dict=True)
self._render_config(
self._get_safe_value(original_value, {}),
self._get_safe_value(modified_value, {}),
current_path_parts,
item
)
else:
self._set_value_columns(item, original_value, modified_value, change_type)
def _render_array(self, original_array, modified_array, path_parts, parent_item):
change_type = self._determine_change_type(
original_array, modified_array,
original_array is not None, modified_array is not None
)
effective_original = self._get_safe_value(original_array, [])
effective_modified = self._get_safe_value(modified_array, [])
path_string = self._build_path_string(path_parts)
array_key = path_parts[-1] if path_parts else "array"
item = QTreeWidgetItem(parent_item)
item.setText(0, array_key)
item.setData(0, Qt.ItemDataRole.UserRole, path_string)
item.setData(3, Qt.ItemDataRole.UserRole, "array")
self._apply_highlighting(item, change_type)
self._setup_tooltip(item, path_string, modified_array, original_array)
self._set_value_columns(item, original_array, modified_array, change_type, is_array=True)
original_len = len(effective_original)
modified_len = len(effective_modified)
max_len = max(original_len, modified_len)
for i in range(max_len):
original_element = effective_original[i] if i < original_len else None
modified_element = effective_modified[i] if i < modified_len else None
element_change_type = self._determine_change_type(
original_element, modified_element,
original_element is not None, modified_element is not None
)
effective_element = self._get_effective_value(original_element, modified_element)
if effective_element is None:
continue
element_path_parts = self._build_array_path(path_parts, i)
element_path = self._build_path_string(element_path_parts)
element_item = QTreeWidgetItem(item)
element_item.setText(0, "[{}]".format(i))
element_item.setData(0, Qt.ItemDataRole.UserRole, element_path)
self._apply_highlighting(element_item, element_change_type)
self._setup_tooltip(element_item, element_path, modified_element, original_element)
if isinstance(effective_element, dict):
element_item.setData(3, Qt.ItemDataRole.UserRole, "dict")
self._set_value_columns(element_item, original_element, modified_element, element_change_type, is_dict=True)
self._render_config(
self._get_safe_value(original_element, {}),
self._get_safe_value(modified_element, {}),
element_path_parts,
element_item
)
elif isinstance(effective_element, list):
element_item.setData(3, Qt.ItemDataRole.UserRole, "array")
self._set_value_columns(element_item, original_element, modified_element, element_change_type, is_array=True)
self._render_array(
self._get_safe_value(original_element, []),
self._get_safe_value(modified_element, []),
element_path_parts,
element_item
)
else:
self._set_value_columns(element_item, original_element, modified_element, element_change_type)
def _apply_highlighting(self, item, change_type=None):
if change_type == "added":
color = "#E3F2FD"
status_text = "A"
elif change_type == "removed":
color = "#FFEBEE"
status_text = "R"
elif change_type == "modified":
color = "#FFF9C4"
status_text = "M"
else:
color = None
status_text = ""
item.setText(1, status_text)
if color:
brush = QBrush(QColor(color))
else:
brush = QBrush()
for col in range(4):
item.setBackground(col, brush)
def _setup_tooltip(self, item, key_path, value, original_value=None):
tooltip_text = get_tooltip(key_path, value, original_value, self.context)
item.setToolTip(0, tooltip_text)
def _calculate_tree_height(self):
if self.tree.topLevelItemCount() == 0:
return self.tree.header().height() if self.tree.header().isVisible() else 0
header_height = self.tree.header().height() if self.tree.header().isVisible() else 0
first_item = self.tree.topLevelItem(0)
row_height = 24
if first_item:
rect = self.tree.visualItemRect(first_item)
if rect.height() > 0:
row_height = rect.height()
else:
font_metrics = self.tree.fontMetrics()
row_height = font_metrics.height() + 6
def count_visible_rows(item):
count = 1
if item.isExpanded():
for i in range(item.childCount()):
count += count_visible_rows(item.child(i))
return count
total_rows = 0
for i in range(self.tree.topLevelItemCount()):
total_rows += count_visible_rows(self.tree.topLevelItem(i))
padding = 10
return header_height + (total_rows * row_height) + padding
def _update_tree_height(self):
height = self._calculate_tree_height()
if height > 0:
self.tree.setFixedHeight(height)