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
This commit is contained in:
Hoang Hong Quan
2025-12-30 14:19:47 +07:00
committed by GitHub
parent 871d826ea4
commit 0e608a56ce
38 changed files with 4948 additions and 1636 deletions
+15
View File
@@ -0,0 +1,15 @@
from .home_page import HomePage
from .select_hardware_report_page import SelectHardwareReportPage
from .compatibility_page import CompatibilityPage
from .configuration_page import ConfigurationPage
from .build_page import BuildPage
from .settings_page import SettingsPage
__all__ = [
"HomePage",
"SelectHardwareReportPage",
"CompatibilityPage",
"ConfigurationPage",
"BuildPage",
"SettingsPage",
]
+552
View File
@@ -0,0 +1,552 @@
import platform
import os
import shutil
import threading
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel
from qfluentwidgets import (
SubtitleLabel, BodyLabel, CardWidget, TextEdit,
StrongBodyLabel, ProgressBar, PrimaryPushButton, FluentIcon,
ScrollArea
)
from Scripts.datasets import chipset_data
from Scripts.datasets import kext_data
from Scripts.custom_dialogs import show_confirmation
from Scripts.styles import SPACING, COLORS, RADIUS
from Scripts import ui_utils
from Scripts.widgets.config_editor import ConfigEditor
class BuildPage(ScrollArea):
build_progress_signal = pyqtSignal(str, list, int, int, bool)
build_complete_signal = pyqtSignal(bool, object)
def __init__(self, parent, ui_utils_instance=None):
super().__init__(parent)
self.setObjectName("buildPage")
self.controller = parent
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.build_in_progress = False
self.build_successful = False
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.enableTransparentBackground()
self._init_ui()
self._connect_signals()
def _init_ui(self):
self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.expandLayout.setSpacing(SPACING["large"])
self.expandLayout.addWidget(self.ui_utils.create_step_indicator(4))
header_layout = QVBoxLayout()
header_layout.setSpacing(SPACING["small"])
title = SubtitleLabel("Build OpenCore EFI")
subtitle = BodyLabel("Build your customized OpenCore EFI ready for installation")
subtitle.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
header_layout.addWidget(title)
header_layout.addWidget(subtitle)
self.expandLayout.addLayout(header_layout)
self.expandLayout.addSpacing(SPACING["medium"])
self.instructions_after_content = QWidget()
self.instructions_after_content_layout = QVBoxLayout(self.instructions_after_content)
self.instructions_after_content_layout.setContentsMargins(0, 0, 0, 0)
self.instructions_after_content_layout.setSpacing(SPACING["medium"])
self.instructions_after_build_card = self.ui_utils.custom_card(
card_type="warning",
title="Before Using Your EFI",
body="Please complete these important steps before using the built EFI:",
custom_widget=self.instructions_after_content,
parent=self.scrollWidget
)
self.instructions_after_build_card.setVisible(False)
self.expandLayout.addWidget(self.instructions_after_build_card)
build_control_card = CardWidget(self.scrollWidget)
build_control_card.setBorderRadius(RADIUS["card"])
build_control_layout = QVBoxLayout(build_control_card)
build_control_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
build_control_layout.setSpacing(SPACING["medium"])
title = StrongBodyLabel("Build Control")
build_control_layout.addWidget(title)
btn_layout = QHBoxLayout()
btn_layout.setSpacing(SPACING["medium"])
self.build_btn = PrimaryPushButton(FluentIcon.DEVELOPER_TOOLS, "Build OpenCore EFI")
self.build_btn.clicked.connect(self.start_build)
btn_layout.addWidget(self.build_btn)
self.controller.build_btn = self.build_btn
self.open_result_btn = PrimaryPushButton(FluentIcon.FOLDER, "Open Result Folder")
self.open_result_btn.clicked.connect(self.open_result)
self.open_result_btn.setEnabled(False)
btn_layout.addWidget(self.open_result_btn)
self.controller.open_result_btn = self.open_result_btn
build_control_layout.addLayout(btn_layout)
self.progress_container = QWidget()
progress_layout = QVBoxLayout(self.progress_container)
progress_layout.setContentsMargins(0, SPACING["small"], 0, 0)
progress_layout.setSpacing(SPACING["medium"])
status_row = QHBoxLayout()
status_row.setSpacing(SPACING["medium"])
self.status_icon_label = QLabel()
self.status_icon_label.setFixedSize(28, 28)
status_row.addWidget(self.status_icon_label)
self.progress_label = StrongBodyLabel("Ready to build")
self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["text_secondary"]))
status_row.addWidget(self.progress_label)
status_row.addStretch()
progress_layout.addLayout(status_row)
self.progress_bar = ProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setFixedHeight(10)
self.progress_bar.setTextVisible(True)
self.controller.progress_bar = self.progress_bar
progress_layout.addWidget(self.progress_bar)
self.controller.progress_label = self.progress_label
self.progress_container.setVisible(False)
self.progress_helper = ui_utils.ProgressStatusHelper(
self.status_icon_label,
self.progress_label,
self.progress_bar,
self.progress_container
)
build_control_layout.addWidget(self.progress_container)
self.expandLayout.addWidget(build_control_card)
log_card = CardWidget(self.scrollWidget)
log_card.setBorderRadius(RADIUS["card"])
log_card_layout = QVBoxLayout(log_card)
log_card_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
log_card_layout.setSpacing(SPACING["medium"])
log_title = StrongBodyLabel("Build Log")
log_card_layout.addWidget(log_title)
log_description = BodyLabel("Detailed build process information and status updates")
log_description.setStyleSheet("color: {}; font-size: 13px;".format(COLORS["text_secondary"]))
log_card_layout.addWidget(log_description)
self.build_log = TextEdit()
self.build_log.setReadOnly(True)
self.build_log.setMinimumHeight(400)
self.build_log.setStyleSheet(f"""
TextEdit {{
background-color: rgba(0, 0, 0, 0.03);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: {RADIUS["small"]}px;
padding: {SPACING["large"]}px;
font-family: "Consolas", "Monaco", "Courier New", monospace;
font-size: 13px;
line-height: 1.7;
}}
""")
self.controller.build_log = self.build_log
log_card_layout.addWidget(self.build_log)
self.log_card = log_card
self.log_card.setVisible(False)
self.expandLayout.addWidget(log_card)
self.config_editor = ConfigEditor(self.scrollWidget)
self.config_editor.setVisible(False)
self.expandLayout.addWidget(self.config_editor)
self.expandLayout.addStretch()
def _connect_signals(self):
self.build_progress_signal.connect(self._handle_build_progress)
self.build_complete_signal.connect(self._handle_build_complete)
def _handle_build_progress(self, title, steps, current_step_index, progress, done):
status = "success" if done else "loading"
if done:
message = "{} complete!".format(title)
else:
step_text = steps[current_step_index] if current_step_index < len(steps) else "Processing"
step_counter = "Step {}/{}".format(current_step_index + 1, len(steps))
message = "{}: {}...".format(step_counter, step_text)
if done:
final_progress = 100
else:
if "Building" in title:
final_progress = 40 + int(progress * 0.6)
else:
final_progress = progress
if hasattr(self, "progress_helper"):
self.progress_helper.update(status, message, final_progress)
if done:
self.controller.backend.u.log_message("[BUILD] {} complete!".format(title), "SUCCESS", to_build_log=True)
else:
step_text = steps[current_step_index] if current_step_index < len(steps) else "Processing"
self.controller.backend.u.log_message("[BUILD] Step {}/{}: {}...".format(current_step_index + 1, len(steps), step_text), "INFO", to_build_log=True)
def start_build(self):
if not self.controller.validate_prerequisites():
return
if self.controller.macos_state.needs_oclp:
content = (
"1. OpenCore Legacy Patcher allows restoring support for dropped GPUs and Broadcom WiFi on newer versions of macOS, and also enables AppleHDA on macOS Tahoe 26.<br>"
"2. OpenCore Legacy Patcher needs SIP disabled for applying custom kernel patches, which can cause instability, security risks and update issues.<br>"
"3. OpenCore Legacy Patcher does not officially support the Hackintosh community.<br><br>"
"<b><font color=\"{info_color}\">Support for macOS Tahoe 26:</font></b><br>"
"To patch macOS Tahoe 26, you must download OpenCore-Patcher 3.0.0 or newer from my repository: <a href=\"https://github.com/lzhoang2801/OpenCore-Legacy-Patcher/releases/tag/3.0.0\">lzhoang2801/OpenCore-Legacy-Patcher</a>.<br>"
"Official Dortania releases or older patches will NOT work with macOS Tahoe 26."
).format(error_color=COLORS["error"], info_color="#00BCD4")
if not show_confirmation("OpenCore Legacy Patcher Warning", content):
return
self.build_in_progress = True
self.build_successful = False
self.build_btn.setEnabled(False)
self.build_btn.setText("Building...")
self.open_result_btn.setEnabled(False)
self.progress_helper.update("loading", "Preparing to build...", 0)
self.instructions_after_build_card.setVisible(False)
self.build_log.clear()
self.log_card.setVisible(True)
thread = threading.Thread(target=self._start_build_thread, daemon=True)
thread.start()
def _start_build_thread(self):
try:
backend = self.controller.backend
backend.o.gather_bootloader_kexts(backend.k.kexts, self.controller.macos_state.darwin_version)
self._build_opencore_efi(
self.controller.hardware_state.customized_hardware,
self.controller.hardware_state.disabled_devices,
self.controller.smbios_state.model_name,
self.controller.macos_state.darwin_version,
self.controller.macos_state.needs_oclp
)
bios_requirements = self._check_bios_requirements(
self.controller.hardware_state.customized_hardware,
self.controller.hardware_state.customized_hardware
)
self.build_complete_signal.emit(True, bios_requirements)
except Exception as e:
self.build_complete_signal.emit(False, None)
def _check_bios_requirements(self, org_hardware_report, hardware_report):
requirements = []
org_firmware_type = org_hardware_report.get("BIOS", {}).get("Firmware Type", "Unknown")
firmware_type = hardware_report.get("BIOS", {}).get("Firmware Type", "Unknown")
if org_firmware_type == "Legacy" and firmware_type == "UEFI":
requirements.append("Enable UEFI mode (disable Legacy/CSM (Compatibility Support Module))")
secure_boot = hardware_report.get("BIOS", {}).get("Secure Boot", "Unknown")
if secure_boot != "Disabled":
requirements.append("Disable Secure Boot")
if hardware_report.get("Motherboard", {}).get("Platform") == "Desktop" and hardware_report.get("Motherboard", {}).get("Chipset") in chipset_data.IntelChipsets[112:]:
resizable_bar_enabled = any(gpu_props.get("Resizable BAR", "Disabled") == "Enabled" for gpu_props in hardware_report.get("GPU", {}).values())
if not resizable_bar_enabled:
requirements.append("Enable Above 4G Decoding")
requirements.append("Disable Resizable BAR/Smart Access Memory")
return requirements
def _build_opencore_efi(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp):
steps = [
"Copying EFI base to results folder",
"Applying ACPI patches",
"Copying kexts and snapshotting to config.plist",
"Generating config.plist",
"Cleaning up unused drivers, resources, and tools"
]
title = "Building OpenCore EFI"
current_step = 0
progress = int((current_step / len(steps)) * 100)
self.build_progress_signal.emit(title, steps, current_step, progress, False)
current_step += 1
backend = self.controller.backend
backend.u.create_folder(backend.result_dir, remove_content=True)
if not os.path.exists(backend.k.ock_files_dir):
raise Exception("Directory \"{}\" does not exist.".format(backend.k.ock_files_dir))
source_efi_dir = os.path.join(backend.k.ock_files_dir, "OpenCorePkg")
shutil.copytree(source_efi_dir, backend.result_dir, dirs_exist_ok=True)
config_file = os.path.join(backend.result_dir, "EFI", "OC", "config.plist")
config_data = backend.u.read_file(config_file)
if not config_data:
raise Exception("Error: The file {} does not exist.".format(config_file))
progress = int((current_step / len(steps)) * 100)
self.build_progress_signal.emit(title, steps, current_step, progress, False)
current_step += 1
config_data["ACPI"]["Add"] = []
config_data["ACPI"]["Delete"] = []
config_data["ACPI"]["Patch"] = []
acpi_directory = os.path.join(backend.result_dir, "EFI", "OC", "ACPI")
if backend.ac.ensure_dsdt():
backend.ac.hardware_report = hardware_report
backend.ac.disabled_devices = disabled_devices
backend.ac.acpi_directory = acpi_directory
backend.ac.smbios_model = smbios_model
backend.ac.lpc_bus_device = backend.ac.get_lpc_name()
for patch in backend.ac.patches:
if patch.checked:
if patch.name == "BATP":
patch.checked = getattr(backend.ac, patch.function_name)()
backend.k.kexts[kext_data.kext_index_by_name.get("ECEnabler")].checked = patch.checked
continue
acpi_load = getattr(backend.ac, patch.function_name)()
if not isinstance(acpi_load, dict):
continue
config_data["ACPI"]["Add"].extend(acpi_load.get("Add", []))
config_data["ACPI"]["Delete"].extend(acpi_load.get("Delete", []))
config_data["ACPI"]["Patch"].extend(acpi_load.get("Patch", []))
config_data["ACPI"]["Patch"].extend(backend.ac.dsdt_patches)
config_data["ACPI"]["Patch"] = backend.ac.apply_acpi_patches(config_data["ACPI"]["Patch"])
progress = int((current_step / len(steps)) * 100)
self.build_progress_signal.emit(title, steps, current_step, progress, False)
current_step += 1
kexts_directory = os.path.join(backend.result_dir, "EFI", "OC", "Kexts")
backend.k.install_kexts_to_efi(macos_version, kexts_directory)
config_data["Kernel"]["Add"] = backend.k.load_kexts(hardware_report, macos_version, kexts_directory)
progress = int((current_step / len(steps)) * 100)
self.build_progress_signal.emit(title, steps, current_step, progress, False)
current_step += 1
audio_layout_id = self.controller.hardware_state.audio_layout_id
audio_controller_properties = self.controller.hardware_state.audio_controller_properties
backend.co.genarate(
hardware_report,
disabled_devices,
smbios_model,
macos_version,
needs_oclp,
backend.k.kexts,
config_data,
audio_layout_id,
audio_controller_properties
)
backend.u.write_file(config_file, config_data)
progress = int((current_step / len(steps)) * 100)
self.build_progress_signal.emit(title, steps, current_step, progress, False)
files_to_remove = []
drivers_directory = os.path.join(backend.result_dir, "EFI", "OC", "Drivers")
driver_list = backend.u.find_matching_paths(drivers_directory, extension_filter=".efi")
driver_loaded = [kext.get("Path") for kext in config_data.get("UEFI").get("Drivers")]
for driver_path, type in driver_list:
if not driver_path in driver_loaded:
files_to_remove.append(os.path.join(drivers_directory, driver_path))
resources_audio_dir = os.path.join(backend.result_dir, "EFI", "OC", "Resources", "Audio")
if os.path.exists(resources_audio_dir):
files_to_remove.append(resources_audio_dir)
picker_variant = config_data.get("Misc", {}).get("Boot", {}).get("PickerVariant")
if picker_variant in (None, "Auto"):
picker_variant = "Acidanthera/GoldenGate"
if os.name == "nt":
picker_variant = picker_variant.replace("/", "\\")
resources_image_dir = os.path.join(backend.result_dir, "EFI", "OC", "Resources", "Image")
available_picker_variants = backend.u.find_matching_paths(resources_image_dir, type_filter="dir")
for variant_name, variant_type in available_picker_variants:
variant_path = os.path.join(resources_image_dir, variant_name)
if ".icns" in ", ".join(os.listdir(variant_path)):
if picker_variant not in variant_name:
files_to_remove.append(variant_path)
tools_directory = os.path.join(backend.result_dir, "EFI", "OC", "Tools")
tool_list = backend.u.find_matching_paths(tools_directory, extension_filter=".efi")
tool_loaded = [tool.get("Path") for tool in config_data.get("Misc").get("Tools")]
for tool_path, type in tool_list:
if not tool_path in tool_loaded:
files_to_remove.append(os.path.join(tools_directory, tool_path))
if "manifest.json" in os.listdir(backend.result_dir):
files_to_remove.append(os.path.join(backend.result_dir, "manifest.json"))
for file_path in files_to_remove:
try:
if os.path.isdir(file_path):
shutil.rmtree(file_path)
else:
os.remove(file_path)
except Exception as e:
backend.u.log_message("[BUILD] Failed to remove file {}: {}".format(os.path.basename(file_path), e), level="WARNING", to_build_log=True)
self.build_progress_signal.emit(title, steps, len(steps) - 1, 100, True)
def show_post_build_instructions(self, bios_requirements):
while self.instructions_after_content_layout.count():
item = self.instructions_after_content_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
if bios_requirements:
bios_header = StrongBodyLabel("1. BIOS/UEFI Settings Required:")
bios_header.setStyleSheet("color: {}; font-size: 14px;".format(COLORS["warning_text"]))
self.instructions_after_content_layout.addWidget(bios_header)
bios_text = "\n".join(["{}".format(req) for req in bios_requirements])
bios_label = BodyLabel(bios_text)
bios_label.setWordWrap(True)
bios_label.setStyleSheet("color: #424242; line-height: 1.6;")
self.instructions_after_content_layout.addWidget(bios_label)
self.instructions_after_content_layout.addSpacing(SPACING["medium"])
usb_header = StrongBodyLabel("{}. USB Port Mapping:".format(2 if bios_requirements else 1))
usb_header.setStyleSheet("color: {}; font-size: 14px;".format(COLORS["warning_text"]))
self.instructions_after_content_layout.addWidget(usb_header)
path_sep = "\\" if platform.system() == "Windows" else "/"
usb_mapping_instructions = (
"1. Use USBToolBox tool to map USB ports<br>"
"2. Add created UTBMap.kext into the EFI{path_sep}OC{path_sep}Kexts folder<br>"
"3. Remove UTBDefault.kext from the EFI{path_sep}OC{path_sep}Kexts folder<br>"
"4. Edit config.plist using ProperTree:<br>"
" a. Run OC Snapshot (Command/Ctrl + R)<br>"
" b. Enable XhciPortLimit quirk if you have more than 15 ports per controller<br>"
" c. Save the file when finished."
).format(path_sep=path_sep)
usb_label = BodyLabel(usb_mapping_instructions)
usb_label.setWordWrap(True)
usb_label.setStyleSheet("color: #424242; line-height: 1.6;")
self.instructions_after_content_layout.addWidget(usb_label)
self.instructions_after_build_card.setVisible(True)
def _handle_build_complete(self, success, bios_requirements):
self.build_in_progress = False
self.build_successful = success
if success:
self.log_card.setVisible(False)
self.progress_helper.update("success", "Build completed successfully!", 100)
self.show_post_build_instructions(bios_requirements)
self._load_configs_after_build()
self.build_btn.setText("Build OpenCore EFI")
self.build_btn.setEnabled(True)
self.open_result_btn.setEnabled(True)
success_message = "Your OpenCore EFI has been built successfully!"
if bios_requirements is not None:
success_message += " Review the important instructions below."
self.controller.update_status(success_message, "success")
else:
self.progress_helper.update("error", "Build OpenCore EFI failed", None)
self.config_editor.setVisible(False)
self.build_btn.setText("Retry Build OpenCore EFI")
self.build_btn.setEnabled(True)
self.open_result_btn.setEnabled(False)
self.controller.update_status("An error occurred during the build. Check the log for details.", "error")
def open_result(self):
result_dir = self.controller.backend.result_dir
try:
self.controller.backend.u.open_folder(result_dir)
except Exception as e:
self.controller.update_status("Failed to open result folder: {}".format(e), "warning")
def _load_configs_after_build(self):
backend = self.controller.backend
source_efi_dir = os.path.join(backend.k.ock_files_dir, "OpenCorePkg")
original_config_file = os.path.join(source_efi_dir, "EFI", "OC", "config.plist")
if not os.path.exists(original_config_file):
return
original_config = backend.u.read_file(original_config_file)
if not original_config:
return
modified_config_file = os.path.join(backend.result_dir, "EFI", "OC", "config.plist")
if not os.path.exists(modified_config_file):
return
modified_config = backend.u.read_file(modified_config_file)
if not modified_config:
return
context = {
"hardware_report": self.controller.hardware_state.hardware_report,
"macos_version": self.controller.macos_state.darwin_version,
"smbios_model": self.controller.smbios_state.model_name,
}
self.config_editor.load_configs(original_config, modified_config, context)
self.config_editor.setVisible(True)
def refresh(self):
if not self.build_in_progress:
if self.build_successful:
self.progress_container.setVisible(True)
self.open_result_btn.setEnabled(True)
else:
log_text = self.build_log.toPlainText()
if not log_text or log_text == DEFAULT_LOG_TEXT:
self.progress_container.setVisible(False)
self.log_card.setVisible(False)
self.open_result_btn.setEnabled(False)
+557
View File
@@ -0,0 +1,557 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout
from qfluentwidgets import SubtitleLabel, BodyLabel, ScrollArea, FluentIcon, GroupHeaderCardWidget, CardWidget, StrongBodyLabel
from Scripts.styles import COLORS, SPACING
from Scripts import ui_utils
from Scripts.datasets import os_data, pci_data
class CompatibilityStatusBanner:
def __init__(self, parent=None, ui_utils_instance=None, layout=None):
self.parent = parent
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self.layout = layout
self.card = None
self.body_label = None
self.note_label = None
def _create_card(self, card_type, icon, title, message, note=""):
body_text = message
if note:
body_text += "<br><br><i style=\"color: {}; font-size: 12px;\">{}</i>".format(COLORS["text_secondary"], note)
if self.card:
if self.layout:
self.layout.removeWidget(self.card)
self.card.setParent(None)
self.card.deleteLater()
self.card = self.ui_utils.custom_card(
card_type=card_type,
icon=icon,
title=title,
body=body_text,
parent=self.parent
)
self.card.setVisible(True)
if self.layout:
self.layout.insertWidget(2, self.card)
return self.card
def show_error(self, title, message, note=""):
self._create_card("error", FluentIcon.CLOSE, title, message, note)
def show_success(self, title, message, note=""):
self._create_card("success", FluentIcon.ACCEPT, title, message, note)
def setVisible(self, visible):
if self.card:
self.card.setVisible(visible)
class CompatibilityPage(ScrollArea):
def __init__(self, parent, ui_utils_instance=None):
super().__init__(parent)
self.setObjectName("compatibilityPage")
self.controller = parent
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self.contentWidget = None
self.contentLayout = None
self.native_support_label = None
self.ocl_support_label = None
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.enableTransparentBackground()
self._init_ui()
def _init_ui(self):
self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.expandLayout.setSpacing(SPACING["large"])
self.expandLayout.addWidget(self.ui_utils.create_step_indicator(2))
header_container = QWidget()
header_layout = QHBoxLayout(header_container)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(SPACING["large"])
title_block = QWidget()
title_layout = QVBoxLayout(title_block)
title_layout.setContentsMargins(0, 0, 0, 0)
title_layout.setSpacing(SPACING["tiny"])
title_label = SubtitleLabel("Hardware Compatibility")
title_layout.addWidget(title_label)
subtitle_label = BodyLabel("Review hardware compatibility with macOS")
subtitle_label.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
title_layout.addWidget(subtitle_label)
header_layout.addWidget(title_block, 1)
self.expandLayout.addWidget(header_container)
self.status_banner = CompatibilityStatusBanner(self.scrollWidget, self.ui_utils, self.expandLayout)
self.expandLayout.addSpacing(SPACING["large"])
self.contentWidget = QWidget()
self.contentLayout = QVBoxLayout(self.contentWidget)
self.contentLayout.setContentsMargins(0, 0, 0, 0)
self.contentLayout.setSpacing(SPACING["large"])
self.expandLayout.addWidget(self.contentWidget)
self.placeholder_label = BodyLabel("Load a hardware report to see compatibility information")
self.placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.placeholder_label.setStyleSheet("color: #605E5C; padding: 40px;")
self.placeholder_label.setWordWrap(True)
self.contentLayout.addWidget(self.placeholder_label)
self.contentLayout.addStretch()
def update_status_banner(self):
if not self.controller.hardware_state.hardware_report:
self.status_banner.setVisible(False)
return
if self.controller.hardware_state.compatibility_error:
self._show_error_banner()
return
self._show_support_banner()
def _show_error_banner(self):
codes = self.controller.hardware_state.compatibility_error
if isinstance(codes, str):
codes = [codes]
code_map = {
"ERROR_MISSING_SSE4": (
"Missing required SSE4.x instruction set.",
"Your CPU is not supported by macOS versions newer than Sierra (10.12)."
),
"ERROR_NO_COMPATIBLE_GPU": (
"You cannot install macOS without a supported GPU.",
"Please do NOT spam my inbox or issue tracker about this issue anymore!"
),
"ERROR_INTEL_VMD": (
"Intel VMD controllers are not supported in macOS.",
"Please disable Intel VMD in the BIOS settings and try again with new hardware report."
),
"ERROR_NO_COMPATIBLE_STORAGE": (
"No compatible storage controller for macOS was found!",
"Consider purchasing a compatible SSD NVMe for your system."
)
}
title = "Hardware Compatibility Issue"
messages = []
notes = []
for code in codes:
msg, note = code_map.get(code, (code, ""))
messages.append(msg)
if note:
notes.append(note)
self.status_banner.show_error(
title,
"\n".join(messages),
"\n".join(notes)
)
def _show_support_banner(self):
if self.controller.macos_state.native_version:
min_ver_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.native_version[0])
max_ver_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.native_version[-1])
native_range = min_ver_name if min_ver_name == max_ver_name else "{} to {}".format(min_ver_name, max_ver_name)
message = "Native macOS support: {}".format(native_range)
if self.controller.macos_state.ocl_patched_version:
oclp_max_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.ocl_patched_version[0])
oclp_min_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.ocl_patched_version[-1])
oclp_range = oclp_min_name if oclp_min_name == oclp_max_name else "{} to {}".format(oclp_min_name, oclp_max_name)
message += "\nOpenCore Legacy Patcher extended support: {}".format(oclp_range)
self.status_banner.show_success("Hardware is Compatible", message)
else:
self.status_banner.show_error(
"Incompatible Hardware",
"No supported macOS version found for this hardware configuration."
)
def format_compatibility(self, compat_tuple):
if not compat_tuple or compat_tuple == (None, None):
return "Unsupported", "#D13438"
max_ver, min_ver = compat_tuple
if max_ver and min_ver:
max_name = os_data.get_macos_name_by_darwin(max_ver)
min_name = os_data.get_macos_name_by_darwin(min_ver)
if max_name == min_name:
return "Up to {}".format(max_name), "#0078D4"
else:
return "{} to {}".format(min_name, max_name), "#107C10"
return "Unknown", "#605E5C"
def update_display(self):
if not self.contentLayout:
return
while self.contentLayout.count() > 0:
item = self.contentLayout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
if not self.controller.hardware_state.hardware_report:
self._show_placeholder()
return
report = self.controller.hardware_state.hardware_report
cards_added = 0
cards_added += self._add_cpu_card(report)
cards_added += self._add_gpu_card(report)
cards_added += self._add_sound_card(report)
cards_added += self._add_network_card(report)
cards_added += self._add_storage_card(report)
cards_added += self._add_bluetooth_card(report)
cards_added += self._add_biometric_card(report)
cards_added += self._add_sd_card(report)
if cards_added == 0:
self._show_no_data_label()
self.contentLayout.addStretch()
self.update_status_banner()
self.scrollWidget.updateGeometry()
self.scrollWidget.update()
self.update()
def _show_placeholder(self):
self.placeholder_label = BodyLabel("Load hardware report to see compatibility information")
self.placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.placeholder_label.setStyleSheet("color: #605E5C; padding: 40px;")
self.placeholder_label.setWordWrap(True)
self.contentLayout.addWidget(self.placeholder_label)
self.contentLayout.addStretch()
def _show_no_data_label(self):
no_data_card = self.ui_utils.custom_card(
card_type="error",
icon=FluentIcon.CLOSE,
title="No compatible hardware information found in the report.",
body="Please ensure the hardware report contains valid device data.",
parent=self.scrollWidget
)
self.contentLayout.addWidget(no_data_card)
def _add_compatibility_group(self, card, title, compat):
compat_text, compat_color = self.format_compatibility(compat)
self.ui_utils.add_group_with_indent(
card,
self.ui_utils.get_compatibility_icon(compat),
title,
compat_text,
self.ui_utils.create_info_widget("", compat_color),
indent_level=1
)
def _add_cpu_card(self, report):
if "CPU" not in report: return 0
cpu_info = report["CPU"]
if not isinstance(cpu_info, dict): return 0
cpu_card = GroupHeaderCardWidget(self.scrollWidget)
cpu_card.setTitle("CPU")
name = cpu_info.get("Processor Name", "Unknown")
self.ui_utils.add_group_with_indent(
cpu_card,
self.ui_utils.colored_icon(FluentIcon.TAG, COLORS["primary"]),
"Processor",
name,
indent_level=0
)
self._add_compatibility_group(cpu_card, "macOS Compatibility", cpu_info.get("Compatibility", (None, None)))
details = []
if cpu_info.get("Codename"):
details.append("Codename: {}".format(cpu_info.get("Codename")))
if cpu_info.get("Core Count"):
details.append("Cores: {}".format(cpu_info.get("Core Count")))
if details:
self.ui_utils.add_group_with_indent(
cpu_card,
self.ui_utils.colored_icon(FluentIcon.INFO, COLORS["info"]),
"Details",
"".join(details),
indent_level=1
)
self.contentLayout.addWidget(cpu_card)
return 1
def _add_gpu_card(self, report):
if "GPU" not in report or not report["GPU"]: return 0
gpu_card = GroupHeaderCardWidget(self.scrollWidget)
gpu_card.setTitle("Graphics")
for idx, (gpu_name, gpu_info) in enumerate(report["GPU"].items()):
device_type = gpu_info.get("Device Type", "Unknown")
self.ui_utils.add_group_with_indent(
gpu_card,
self.ui_utils.colored_icon(FluentIcon.PHOTO, COLORS["primary"]),
gpu_name,
"Type: {}".format(device_type),
indent_level=0
)
self._add_compatibility_group(gpu_card, "macOS Compatibility", gpu_info.get("Compatibility", (None, None)))
if "OCLP Compatibility" in gpu_info:
oclp_compat = gpu_info.get("OCLP Compatibility")
oclp_text, oclp_color = self.format_compatibility(oclp_compat)
self.ui_utils.add_group_with_indent(
gpu_card,
self.ui_utils.colored_icon(FluentIcon.IOT, COLORS["primary"]),
"OCLP Compatibility",
oclp_text,
self.ui_utils.create_info_widget("Extended support with OpenCore Legacy Patcher", COLORS["text_secondary"]),
indent_level=1
)
if "Monitor" in report:
self._add_monitor_info(gpu_card, gpu_name, gpu_info, report["Monitor"])
self.contentLayout.addWidget(gpu_card)
return 1
def _add_monitor_info(self, gpu_card, gpu_name, gpu_info, monitors):
connected_monitors = []
for monitor_name, monitor_info in monitors.items():
if monitor_info.get("Connected GPU") == gpu_name:
connector = monitor_info.get("Connector Type", "Unknown")
monitor_str = "{} ({})".format(monitor_name, connector)
manufacturer = gpu_info.get("Manufacturer", "")
raw_device_id = gpu_info.get("Device ID", "")
device_id = raw_device_id[5:] if len(raw_device_id) > 5 else raw_device_id
if "Intel" in manufacturer and device_id.startswith(("01", "04", "0A", "0C", "0D")):
if connector == "VGA":
monitor_str += " (Unsupported)"
connected_monitors.append(monitor_str)
if connected_monitors:
self.ui_utils.add_group_with_indent(
gpu_card,
self.ui_utils.colored_icon(FluentIcon.VIEW, COLORS["info"]),
"Connected Displays",
", ".join(connected_monitors),
indent_level=1
)
def _add_sound_card(self, report):
if "Sound" not in report or not report["Sound"]: return 0
sound_card = GroupHeaderCardWidget(self.scrollWidget)
sound_card.setTitle("Audio")
for audio_device, audio_props in report["Sound"].items():
self.ui_utils.add_group_with_indent(
sound_card,
self.ui_utils.colored_icon(FluentIcon.MUSIC, COLORS["primary"]),
audio_device,
"",
indent_level=0
)
self._add_compatibility_group(sound_card, "macOS Compatibility", audio_props.get("Compatibility", (None, None)))
endpoints = audio_props.get("Audio Endpoints", [])
if endpoints:
self.ui_utils.add_group_with_indent(
sound_card,
self.ui_utils.colored_icon(FluentIcon.HEADPHONE, COLORS["info"]),
"Audio Endpoints",
", ".join(endpoints),
indent_level=1
)
self.contentLayout.addWidget(sound_card)
return 1
def _add_network_card(self, report):
if "Network" not in report or not report["Network"]: return 0
network_card = GroupHeaderCardWidget(self.scrollWidget)
network_card.setTitle("Network")
for device_name, device_props in report["Network"].items():
self.ui_utils.add_group_with_indent(
network_card,
self.ui_utils.colored_icon(FluentIcon.WIFI, COLORS["primary"]),
device_name,
"",
indent_level=0
)
self._add_compatibility_group(network_card, "macOS Compatibility", device_props.get("Compatibility", (None, None)))
if "OCLP Compatibility" in device_props:
oclp_compat = device_props.get("OCLP Compatibility")
oclp_text, oclp_color = self.format_compatibility(oclp_compat)
self.ui_utils.add_group_with_indent(
network_card,
self.ui_utils.colored_icon(FluentIcon.IOT, COLORS["primary"]),
"OCLP Compatibility",
oclp_text,
self.ui_utils.create_info_widget("Extended support with OpenCore Legacy Patcher", COLORS["text_secondary"]),
indent_level=1
)
self._add_continuity_info(network_card, device_props)
self.contentLayout.addWidget(network_card)
return 1
def _add_continuity_info(self, network_card, device_props):
device_id = device_props.get("Device ID", "")
if not device_id: return
continuity_info = ""
continuity_color = COLORS["text_secondary"]
if device_id in pci_data.BroadcomWiFiIDs:
continuity_info = "Full support (AirDrop, Handoff, Universal Clipboard, Instant Hotspot, etc.)"
continuity_color = COLORS["success"]
elif device_id in pci_data.IntelWiFiIDs:
continuity_info = "Partial (Handoff and Universal Clipboard with AirportItlwm) - AirDrop, Universal Clipboard, Instant Hotspot,... not available"
continuity_color = COLORS["warning"]
elif device_id in pci_data.AtherosWiFiIDs:
continuity_info = "Limited support (No Continuity features available). Atheros cards are not recommended for macOS."
continuity_color = COLORS["error"]
if continuity_info:
self.ui_utils.add_group_with_indent(
network_card,
self.ui_utils.colored_icon(FluentIcon.SYNC, continuity_color),
"Continuity Features",
continuity_info,
self.ui_utils.create_info_widget("", continuity_color),
indent_level=1
)
def _add_storage_card(self, report):
if "Storage Controllers" not in report or not report["Storage Controllers"]: return 0
storage_card = GroupHeaderCardWidget(self.scrollWidget)
storage_card.setTitle("Storage")
for controller_name, controller_props in report["Storage Controllers"].items():
self.ui_utils.add_group_with_indent(
storage_card,
self.ui_utils.colored_icon(FluentIcon.FOLDER, COLORS["primary"]),
controller_name,
"",
indent_level=0
)
self._add_compatibility_group(storage_card, "macOS Compatibility", controller_props.get("Compatibility", (None, None)))
disk_drives = controller_props.get("Disk Drives", [])
if disk_drives:
self.ui_utils.add_group_with_indent(
storage_card,
self.ui_utils.colored_icon(FluentIcon.FOLDER, COLORS["info"]),
"Disk Drives",
", ".join(disk_drives),
indent_level=1
)
self.contentLayout.addWidget(storage_card)
return 1
def _add_bluetooth_card(self, report):
if "Bluetooth" not in report or not report["Bluetooth"]: return 0
bluetooth_card = GroupHeaderCardWidget(self.scrollWidget)
bluetooth_card.setTitle("Bluetooth")
for bluetooth_name, bluetooth_props in report["Bluetooth"].items():
self.ui_utils.add_group_with_indent(
bluetooth_card,
self.ui_utils.colored_icon(FluentIcon.BLUETOOTH, COLORS["primary"]),
bluetooth_name,
"",
indent_level=0
)
self._add_compatibility_group(bluetooth_card, "macOS Compatibility", bluetooth_props.get("Compatibility", (None, None)))
self.contentLayout.addWidget(bluetooth_card)
return 1
def _add_biometric_card(self, report):
if "Biometric" not in report or not report["Biometric"]: return 0
bio_card = GroupHeaderCardWidget(self.scrollWidget)
bio_card.setTitle("Biometric")
self.ui_utils.add_group_with_indent(
bio_card,
self.ui_utils.colored_icon(FluentIcon.CLOSE, COLORS["warning"]),
"Hardware Limitation",
"Biometric authentication in macOS requires Apple T2 Chip, which is not available for Hackintosh systems.",
self.ui_utils.create_info_widget("", COLORS["warning"]),
indent_level=0
)
for bio_device, bio_props in report["Biometric"].items():
self.ui_utils.add_group_with_indent(
bio_card,
self.ui_utils.colored_icon(FluentIcon.FINGERPRINT, COLORS["error"]),
bio_device,
"Unsupported",
indent_level=0
)
self.contentLayout.addWidget(bio_card)
return 1
def _add_sd_card(self, report):
if "SD Controller" not in report or not report["SD Controller"]: return 0
sd_card = GroupHeaderCardWidget(self.scrollWidget)
sd_card.setTitle("SD Controller")
for controller_name, controller_props in report["SD Controller"].items():
self.ui_utils.add_group_with_indent(
sd_card,
self.ui_utils.colored_icon(FluentIcon.SAVE, COLORS["primary"]),
controller_name,
"",
indent_level=0
)
self._add_compatibility_group(sd_card, "macOS Compatibility", controller_props.get("Compatibility", (None, None)))
self.contentLayout.addWidget(sd_card)
return 1
def refresh(self):
self.update_display()
+293
View File
@@ -0,0 +1,293 @@
import os
from PyQt6.QtWidgets import QWidget, QVBoxLayout
from PyQt6.QtCore import Qt
from qfluentwidgets import (
ScrollArea, SubtitleLabel, BodyLabel, FluentIcon,
PushSettingCard, ExpandGroupSettingCard,
SettingCard, PushButton
)
from Scripts.custom_dialogs import show_macos_version_dialog
from Scripts.styles import SPACING, COLORS
from Scripts import ui_utils
class macOSCard(SettingCard):
def __init__(self, controller, on_select_version, parent=None):
super().__init__(
FluentIcon.GLOBE,
"macOS Version",
"Target operating system version",
parent
)
self.controller = controller
self.versionLabel = BodyLabel(self.controller.macos_state.selected_version_name)
self.versionLabel.setStyleSheet("color: {}; margin-right: 10px;".format(COLORS["text_secondary"]))
self.selectVersionBtn = PushButton("Select Version")
self.selectVersionBtn.clicked.connect(on_select_version)
self.selectVersionBtn.setFixedWidth(150)
self.hBoxLayout.addWidget(self.versionLabel)
self.hBoxLayout.addWidget(self.selectVersionBtn)
self.hBoxLayout.addSpacing(16)
def update_version(self):
self.versionLabel.setText(self.controller.macos_state.selected_version_name)
class AudioLayoutCard(SettingCard):
def __init__(self, controller, on_select_layout, parent=None):
super().__init__(
FluentIcon.MUSIC,
"Audio Layout ID",
"Select layout ID for your audio codec",
parent
)
self.controller = controller
layout_text = str(self.controller.hardware_state.audio_layout_id) if self.controller.hardware_state.audio_layout_id is not None else "Not configured"
self.layoutLabel = BodyLabel(layout_text)
self.layoutLabel.setStyleSheet("color: {}; margin-right: 10px;".format(COLORS["text_secondary"]))
self.selectLayoutBtn = PushButton("Configure Layout")
self.selectLayoutBtn.clicked.connect(on_select_layout)
self.selectLayoutBtn.setFixedWidth(150)
self.hBoxLayout.addWidget(self.layoutLabel)
self.hBoxLayout.addWidget(self.selectLayoutBtn)
self.hBoxLayout.addSpacing(16)
self.setVisible(False)
def update_layout(self):
layout_text = str(self.controller.hardware_state.audio_layout_id) if self.controller.hardware_state.audio_layout_id is not None else "Not configured"
self.layoutLabel.setText(layout_text)
class SMBIOSModelCard(SettingCard):
def __init__(self, controller, on_select_model, parent=None):
super().__init__(
FluentIcon.TAG,
"SMBIOS Model",
"Select Mac model identifier for your system",
parent
)
self.controller = controller
model_text = self.controller.smbios_state.model_name if self.controller.smbios_state.model_name != "Not selected" else "Not configured"
self.modelLabel = BodyLabel(model_text)
self.modelLabel.setStyleSheet("color: {}; margin-right: 10px;".format(COLORS["text_secondary"]))
self.selectModelBtn = PushButton("Configure Model")
self.selectModelBtn.clicked.connect(on_select_model)
self.selectModelBtn.setFixedWidth(150)
self.hBoxLayout.addWidget(self.modelLabel)
self.hBoxLayout.addWidget(self.selectModelBtn)
self.hBoxLayout.addSpacing(16)
def update_model(self):
model_text = self.controller.smbios_state.model_name if self.controller.smbios_state.model_name != "Not selected" else "Not configured"
self.modelLabel.setText(model_text)
class ConfigurationPage(ScrollArea):
def __init__(self, parent, ui_utils_instance=None):
super().__init__(parent)
self.setObjectName("configurationPage")
self.controller = parent
self.settings = self.controller.backend.settings
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.enableTransparentBackground()
self.status_card = None
self._init_ui()
def _init_ui(self):
self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.expandLayout.setSpacing(SPACING["large"])
self.expandLayout.addWidget(self.ui_utils.create_step_indicator(3))
header_container = QWidget()
header_layout = QVBoxLayout(header_container)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(SPACING["tiny"])
title_label = SubtitleLabel("Configuration")
header_layout.addWidget(title_label)
subtitle_label = BodyLabel("Configure your OpenCore EFI settings")
subtitle_label.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
header_layout.addWidget(subtitle_label)
self.expandLayout.addWidget(header_container)
self.expandLayout.addSpacing(SPACING["large"])
self.status_start_index = self.expandLayout.count()
self._update_status_card()
self.macos_card = macOSCard(self.controller, self.select_macos_version, self.scrollWidget)
self.expandLayout.addWidget(self.macos_card)
self.acpi_card = PushSettingCard(
"Configure Patches",
FluentIcon.DEVELOPER_TOOLS,
"ACPI Patches",
"Customize system ACPI table modifications for hardware compatibility",
self.scrollWidget
)
self.acpi_card.clicked.connect(self.customize_acpi_patches)
self.expandLayout.addWidget(self.acpi_card)
self.kexts_card = PushSettingCard(
"Manage Kexts",
FluentIcon.CODE,
"Kernel Extensions",
"Configure kexts required for your hardware",
self.scrollWidget
)
self.kexts_card.clicked.connect(self.customize_kexts)
self.expandLayout.addWidget(self.kexts_card)
self.audio_layout_card = None
self.audio_layout_card_index = None
self.audio_layout_card = AudioLayoutCard(self.controller, self.customize_audio_layout, self.scrollWidget)
self.expandLayout.addWidget(self.audio_layout_card)
self.smbios_card = SMBIOSModelCard(self.controller, self.customize_smbios_model, self.scrollWidget)
self.expandLayout.addWidget(self.smbios_card)
self.expandLayout.addStretch()
def _update_status_card(self):
if self.status_card is not None:
self.expandLayout.removeWidget(self.status_card)
self.status_card.deleteLater()
self.status_card = None
disabled_devices = self.controller.hardware_state.disabled_devices or {}
status_text = ""
status_color = COLORS["text_secondary"]
bg_color = COLORS["bg_card"]
icon = FluentIcon.INFO
if disabled_devices:
status_text = "Hardware components excluded from configuration"
status_color = COLORS["text_secondary"]
bg_color = COLORS["warning_bg"]
elif not self.controller.hardware_state.hardware_report:
status_text = "Please select hardware report first"
elif not self.controller.macos_state.darwin_version:
status_text = "Please select target macOS version first"
else:
status_text = "All hardware components are compatible and enabled"
status_color = COLORS["success"]
bg_color = COLORS["success_bg"]
icon = FluentIcon.ACCEPT
self.status_card = ExpandGroupSettingCard(
icon,
"Compatibility Status",
status_text,
self.scrollWidget
)
if disabled_devices:
for device_name, device_info in disabled_devices.items():
self.ui_utils.add_group_with_indent(
self.status_card,
FluentIcon.CLOSE,
device_name,
"Incompatible" if device_info.get("Compatibility") == (None, None) else "Disabled",
)
else:
pass
self.expandLayout.insertWidget(self.status_start_index, self.status_card)
def select_macos_version(self):
if not self.controller.validate_prerequisites(require_darwin_version=False, require_customized_hardware=False):
return
selected_version = show_macos_version_dialog(
self.controller.macos_state.native_version,
self.controller.macos_state.ocl_patched_version,
self.controller.macos_state.suggested_version
)
if selected_version:
self.controller.apply_macos_version(selected_version)
self.controller.update_status("macOS version updated to {}".format(self.controller.macos_state.selected_version_name), "success")
if hasattr(self, "macos_card"):
self.macos_card.update_version()
def customize_acpi_patches(self):
if not self.controller.validate_prerequisites():
return
self.controller.backend.ac.customize_patch_selection()
self.controller.update_status("ACPI patches configuration updated successfully", "success")
def customize_kexts(self):
if not self.controller.validate_prerequisites():
return
self.controller.backend.k.kext_configuration_menu(self.controller.macos_state.darwin_version)
self.controller.update_status("Kext configuration updated successfully", "success")
def customize_audio_layout(self):
if not self.controller.validate_prerequisites():
return
audio_layout_id, audio_controller_properties = self.controller.backend.k._select_audio_codec_layout(
self.controller.hardware_state.hardware_report,
default_layout_id=self.controller.hardware_state.audio_layout_id
)
if audio_layout_id is not None:
self.controller.hardware_state.audio_layout_id = audio_layout_id
self.controller.hardware_state.audio_controller_properties = audio_controller_properties
self._update_audio_layout_card_visibility()
self.controller.update_status("Audio layout updated to {}".format(audio_layout_id), "success")
def customize_smbios_model(self):
if not self.controller.validate_prerequisites():
return
current_model = self.controller.smbios_state.model_name
selected_model = self.controller.backend.s.customize_smbios_model(self.controller.hardware_state.customized_hardware, current_model, self.controller.macos_state.darwin_version, self.controller.window())
if selected_model and selected_model != current_model:
self.controller.smbios_state.model_name = selected_model
self.controller.backend.s.smbios_specific_options(self.controller.hardware_state.customized_hardware, selected_model, self.controller.macos_state.darwin_version, self.controller.backend.ac.patches, self.controller.backend.k)
if hasattr(self, "smbios_card"):
self.smbios_card.update_model()
self.controller.update_status("SMBIOS model updated to {}".format(selected_model), "success")
def _update_audio_layout_card_visibility(self):
if self.controller.hardware_state.audio_layout_id is not None:
self.audio_layout_card.setVisible(True)
self.audio_layout_card.update_layout()
else:
self.audio_layout_card.setVisible(False)
def update_display(self):
self._update_status_card()
if hasattr(self, "macos_card"):
self.macos_card.update_version()
self._update_audio_layout_card_visibility()
if hasattr(self, "smbios_card"):
self.smbios_card.update_model()
def refresh(self):
self.update_display()
+168
View File
@@ -0,0 +1,168 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFrame
from PyQt6.QtCore import Qt
from qfluentwidgets import SubtitleLabel, BodyLabel, CardWidget, StrongBodyLabel, FluentIcon, ScrollArea
from Scripts.styles import COLORS, SPACING
from Scripts import ui_utils
class HomePage(ScrollArea):
def __init__(self, parent, ui_utils_instance=None):
super().__init__(parent)
self.setObjectName("homePage")
self.controller = parent
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.enableTransparentBackground()
self.scrollWidget.setStyleSheet("QWidget { background: transparent; }")
self._init_ui()
def _init_ui(self):
self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.expandLayout.setSpacing(SPACING["large"])
self.expandLayout.addWidget(self._create_title_label())
self.expandLayout.addWidget(self._create_hero_section())
self.expandLayout.addWidget(self._create_note_card())
self.expandLayout.addWidget(self._create_warning_card())
self.expandLayout.addWidget(self._create_guide_card())
self.expandLayout.addStretch()
def _create_title_label(self):
title_label = SubtitleLabel("Welcome to OpCore Simplify")
title_label.setStyleSheet("font-size: 24px; font-weight: bold;")
return title_label
def _create_hero_section(self):
hero_card = CardWidget()
hero_layout = QHBoxLayout(hero_card)
hero_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
hero_layout.setSpacing(SPACING["large"])
hero_text = QVBoxLayout()
hero_text.setSpacing(SPACING["medium"])
hero_title = StrongBodyLabel("Introduction")
hero_title.setStyleSheet("font-size: 18px; color: {};".format(COLORS["primary"]))
hero_text.addWidget(hero_title)
hero_body = BodyLabel(
"A specialized tool that streamlines OpenCore EFI creation by automating the essential setup process and providing standardized configurations.<br>"
"Designed to reduce manual effort while ensuring accuracy in your Hackintosh journey."
)
hero_body.setWordWrap(True)
hero_body.setStyleSheet("line-height: 1.6; font-size: 14px;")
hero_text.addWidget(hero_body)
hero_layout.addLayout(hero_text, 2)
robot_icon = self.ui_utils.build_icon_label(FluentIcon.ROBOT, COLORS["primary"], size=64)
hero_layout.addWidget(robot_icon, 1, Qt.AlignmentFlag.AlignVCenter)
return hero_card
def _create_note_card(self):
return self.ui_utils.custom_card(
card_type="note",
title="OpenCore Legacy Patcher 3.0.0 - Now Supports macOS Tahoe 26!",
body=(
"The long awaited version 3.0.0 of OpenCore Legacy Patcher is here, bringing <b>initial support for macOS Tahoe 26</b> to the community!<br><br>"
"<b>Please Note:</b><br>"
"- Only OpenCore-Patcher 3.0.0 from the <a href=\"https://github.com/lzhoang2801/OpenCore-Legacy-Patcher/releases/tag/3.0.0\" style=\"color: #0078D4; text-decoration: none;\">lzhoang2801/OpenCore-Legacy-Patcher</a> repository provides support for macOS Tahoe 26 with early patches.<br>"
"- Official Dortania releases or older patches <b>will NOT work</b> with macOS Tahoe 26."
)
)
def _create_warning_card(self):
return self.ui_utils.custom_card(
card_type="warning",
title="WARNING",
body=(
"While OpCore Simplify significantly reduces setup time, the Hackintosh journey still requires:<br><br>"
"- Understanding basic concepts from the <a href=\"https://dortania.github.io/OpenCore-Install-Guide/\" style=\"color: #F57C00; text-decoration: none;\">Dortania Guide</a><br>"
"- Testing and troubleshooting during the installation process.<br>"
"- Patience and persistence in resolving any issues that arise.<br><br>"
"Our tool does not guarantee a successful installation in the first attempt, but it should help you get started."
)
)
def _create_guide_card(self):
guide_card = CardWidget()
guide_layout = QVBoxLayout(guide_card)
guide_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
guide_layout.setSpacing(SPACING["medium"])
guide_title = StrongBodyLabel("Getting Started")
guide_title.setStyleSheet("font-size: 18px;")
guide_layout.addWidget(guide_title)
step_items = [
(FluentIcon.FOLDER_ADD, "1. Select Hardware Report", "Select hardware report of target system you want to build EFI for."),
(FluentIcon.CHECKBOX, "2. Check Compatibility", "Review hardware compatibility with macOS."),
(FluentIcon.EDIT, "3. Configure Settings", "Customize ACPI patches, kexts, and config for your OpenCore EFI."),
(FluentIcon.DEVELOPER_TOOLS, "4. Build EFI", "Generate your OpenCore EFI."),
]
for idx, (icon, title, desc) in enumerate(step_items):
guide_layout.addWidget(self._create_guide_row(icon, title, desc))
if idx < len(step_items) - 1:
guide_layout.addWidget(self._create_divider())
return guide_card
def _create_guide_row(self, icon, title, desc):
row = QWidget()
row_layout = QHBoxLayout(row)
row_layout.setContentsMargins(0, 0, 0, 0)
row_layout.setSpacing(SPACING["medium"])
icon_container = QWidget()
icon_container.setFixedWidth(40)
icon_layout = QVBoxLayout(icon_container)
icon_layout.setContentsMargins(0, 0, 0, 0)
icon_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter)
row_icon = self.ui_utils.build_icon_label(icon, COLORS["primary"], size=24)
icon_layout.addWidget(row_icon)
row_layout.addWidget(icon_container)
text_col = QVBoxLayout()
text_col.setSpacing(SPACING["tiny"])
title_label = StrongBodyLabel(title)
title_label.setStyleSheet("font-size: 14px;")
desc_label = BodyLabel(desc)
desc_label.setWordWrap(True)
desc_label.setStyleSheet("color: {}; line-height: 1.4;".format(COLORS["text_secondary"]))
text_col.addWidget(title_label)
text_col.addWidget(desc_label)
row_layout.addLayout(text_col)
return row
def _create_divider(self):
divider = QFrame()
divider.setFrameShape(QFrame.Shape.HLine)
divider.setStyleSheet("color: {};".format(COLORS["border_light"]))
return divider
def refresh(self):
pass
@@ -0,0 +1,485 @@
import os
import threading
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFileDialog, QLabel
from qfluentwidgets import (
PushButton, SubtitleLabel, BodyLabel, CardWidget, FluentIcon,
StrongBodyLabel, PrimaryPushButton, ProgressBar,
IconWidget, ExpandGroupSettingCard
)
from Scripts.datasets import os_data
from Scripts.custom_dialogs import show_info, show_confirmation
from Scripts.state import HardwareReportState, macOSVersionState, SMBIOSState
from Scripts.styles import SPACING, COLORS
from Scripts import ui_utils
class ReportDetailsGroup(ExpandGroupSettingCard):
def __init__(self, parent=None):
super().__init__(
FluentIcon.INFO,
"Hardware Report Details",
"View selected report paths and validation status",
parent
)
self.reportIcon = IconWidget(FluentIcon.INFO)
self.reportIcon.setFixedSize(16, 16)
self.reportIcon.setVisible(False)
self.acpiIcon = IconWidget(FluentIcon.INFO)
self.acpiIcon.setFixedSize(16, 16)
self.acpiIcon.setVisible(False)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.reportCard = self.addGroup(
FluentIcon.DOCUMENT,
"Report Path",
"Not selected",
self.reportIcon
)
self.acpiCard = self.addGroup(
FluentIcon.FOLDER,
"ACPI Directory",
"Not selected",
self.acpiIcon
)
self.reportCard.contentLabel.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
self.acpiCard.contentLabel.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
def update_status(self, section, path, status_type, message):
card = self.reportCard if section == "report" else self.acpiCard
icon_widget = self.reportIcon if section == "report" else self.acpiIcon
if path and path != "Not selected":
path = os.path.normpath(path)
card.setContent(path)
card.setToolTip(message if message else path)
icon = FluentIcon.INFO
color = COLORS["text_secondary"]
if status_type == "success":
color = COLORS["text_primary"]
icon = FluentIcon.ACCEPT
elif status_type == "error":
color = COLORS["error"]
icon = FluentIcon.CANCEL
elif status_type == "warning":
color = COLORS["warning"]
icon = FluentIcon.INFO
card.contentLabel.setStyleSheet("color: {};".format(color))
icon_widget.setIcon(icon)
icon_widget.setVisible(True)
class SelectHardwareReportPage(QWidget):
export_finished_signal = pyqtSignal(bool, str, str, str)
load_report_progress_signal = pyqtSignal(str, str, int)
load_report_finished_signal = pyqtSignal(bool, str, str, str)
report_validated_signal = pyqtSignal(str, str)
compatibility_checked_signal = pyqtSignal()
def __init__(self, parent, ui_utils_instance=None):
super().__init__(parent)
self.setObjectName("SelectHardwareReport")
self.controller = parent
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self._connect_signals()
self._init_ui()
def _connect_signals(self):
self.export_finished_signal.connect(self._handle_export_finished)
self.load_report_progress_signal.connect(self._handle_load_report_progress)
self.load_report_finished_signal.connect(self._handle_load_report_finished)
self.report_validated_signal.connect(self._handle_report_validated)
self.compatibility_checked_signal.connect(self._handle_compatibility_checked)
def _init_ui(self):
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.main_layout.setSpacing(SPACING["large"])
self.main_layout.addWidget(self.ui_utils.create_step_indicator(1))
header_layout = QVBoxLayout()
header_layout.setSpacing(SPACING["small"])
title = SubtitleLabel("Select Hardware Report")
subtitle = BodyLabel("Select hardware report of target system you want to build EFI for")
subtitle.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
header_layout.addWidget(title)
header_layout.addWidget(subtitle)
self.main_layout.addLayout(header_layout)
self.main_layout.addSpacing(SPACING["medium"])
self.create_instructions_card()
self.create_action_card()
self.create_report_details_group()
self.main_layout.addStretch()
def create_instructions_card(self):
card = self.ui_utils.custom_card(
card_type="note",
title="Quick Guide",
body=(
"<b>Windows Users:</b> Click <span style=\"color:#0078D4; font-weight:600;\">Export Hardware Report</span> button to generate hardware report for current system. Alternatively, you can manually generate hardware report using Hardware Sniffer tool.<br>"
"<b>Linux/macOS Users:</b> Please transfer a report generated on Windows. Native generation is not supported."
)
)
self.main_layout.addWidget(card)
def create_action_card(self):
self.action_card = CardWidget()
layout = QVBoxLayout(self.action_card)
layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
layout.setSpacing(SPACING["medium"])
title = StrongBodyLabel("Select Methods")
layout.addWidget(title)
btn_layout = QHBoxLayout()
btn_layout.setSpacing(SPACING["medium"])
self.select_btn = PrimaryPushButton(FluentIcon.FOLDER_ADD, "Select Hardware Report")
self.select_btn.clicked.connect(self.select_hardware_report)
btn_layout.addWidget(self.select_btn)
if os.name == "nt":
self.export_btn = PushButton(FluentIcon.DOWNLOAD, "Export Hardware Report")
self.export_btn.clicked.connect(self.export_hardware_report)
btn_layout.addWidget(self.export_btn)
layout.addLayout(btn_layout)
self.progress_container = QWidget()
progress_layout = QVBoxLayout(self.progress_container)
progress_layout.setContentsMargins(0, SPACING["small"], 0, 0)
progress_layout.setSpacing(SPACING["medium"])
status_row = QHBoxLayout()
status_row.setSpacing(SPACING["medium"])
self.status_icon_label = QLabel()
self.status_icon_label.setFixedSize(28, 28)
status_row.addWidget(self.status_icon_label)
self.progress_label = StrongBodyLabel("Ready")
self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["text_secondary"]))
status_row.addWidget(self.progress_label)
status_row.addStretch()
progress_layout.addLayout(status_row)
self.progress_bar = ProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setFixedHeight(10)
self.progress_bar.setTextVisible(True)
progress_layout.addWidget(self.progress_bar)
self.progress_container.setVisible(False)
layout.addWidget(self.progress_container)
self.progress_helper = ui_utils.ProgressStatusHelper(
self.status_icon_label,
self.progress_label,
self.progress_bar,
self.progress_container
)
self.main_layout.addWidget(self.action_card)
def create_report_details_group(self):
self.report_group = ReportDetailsGroup(self)
self.main_layout.addWidget(self.report_group)
def select_report_file(self):
report_path, _ = QFileDialog.getOpenFileName(
self, "Select Hardware Report", "", "JSON Files (*.json)"
)
return report_path if report_path else None
def select_acpi_folder(self):
acpi_dir = QFileDialog.getExistingDirectory(self, "Select ACPI Folder", "")
return acpi_dir if acpi_dir else None
def select_hardware_report(self):
report_path = self.select_report_file()
if not report_path:
return
report_dir = os.path.dirname(report_path)
potential_acpi = os.path.join(report_dir, "ACPI")
acpi_dir = None
if os.path.isdir(potential_acpi):
if show_confirmation("ACPI Folder Detected", "Found an ACPI folder at: {}\n\nDo you want to use this ACPI folder?".format(potential_acpi)):
acpi_dir = potential_acpi
if not acpi_dir:
acpi_dir = self.select_acpi_folder()
if not acpi_dir:
return
self.load_hardware_report(report_path, acpi_dir)
def set_detail_status(self, section, path, status_type, message):
self.report_group.update_status(section, path, status_type, message)
def suggest_macos_version(self):
if not self.controller.hardware_state.hardware_report or not self.controller.macos_state.native_version:
return None
hardware_report = self.controller.hardware_state.hardware_report
native_macos_version = self.controller.macos_state.native_version
suggested_macos_version = native_macos_version[1]
for device_type in ("GPU", "Network", "Bluetooth", "SD Controller"):
if device_type in hardware_report:
for device_name, device_props in hardware_report[device_type].items():
if device_props.get("Compatibility", (None, None)) != (None, None):
if device_type == "GPU" and device_props.get("Device Type") == "Integrated GPU":
device_id = device_props.get("Device ID", " " * 8)[5:]
if device_props.get("Manufacturer") == "AMD" or device_id.startswith(("59", "87C0")):
suggested_macos_version = "22.99.99"
elif device_id.startswith(("09", "19")):
suggested_macos_version = "21.99.99"
if self.controller.backend.u.parse_darwin_version(suggested_macos_version) > self.controller.backend.u.parse_darwin_version(device_props.get("Compatibility")[0]):
suggested_macos_version = device_props.get("Compatibility")[0]
while True:
if "Beta" in os_data.get_macos_name_by_darwin(suggested_macos_version):
suggested_macos_version = "{}{}".format(
int(suggested_macos_version[:2]) - 1, suggested_macos_version[2:])
else:
break
self.controller.macos_state.suggested_version = suggested_macos_version
def load_hardware_report(self, report_path, acpi_dir, from_export=False):
self.controller.hardware_state = HardwareReportState(report_path=report_path, acpi_dir=acpi_dir)
self.controller.macos_state = macOSVersionState()
self.controller.smbios_state = SMBIOSState()
self.controller.backend.ac.acpi.acpi_tables = {}
self.controller.backend.ac.acpi.dsdt = None
self.controller.compatibilityPage.update_display()
self.controller.configurationPage.update_display()
if not from_export:
self.progress_container.setVisible(True)
self.select_btn.setEnabled(False)
if hasattr(self, "export_btn"):
self.export_btn.setEnabled(False)
progress_offset = 40 if from_export else 0
self.progress_helper.update("loading", "Validating report...", progress_offset)
self.report_group.setExpand(True)
def load_thread():
try:
progress_scale = 0.5 if from_export else 1.0
def get_progress(base_progress):
return progress_offset + int(base_progress * progress_scale)
self.load_report_progress_signal.emit("loading", "Validating report...", get_progress(10))
is_valid, errors, warnings, validated_data = self.controller.backend.v.validate_report(report_path)
if not is_valid or errors:
error_msg = "Report Errors:\n" + "\n".join(errors)
self.load_report_finished_signal.emit(False, "validation_error", report_path, acpi_dir)
return
self.load_report_progress_signal.emit("loading", "Validating report...", get_progress(30))
self.report_validated_signal.emit(report_path, "Hardware report validated successfully.")
self.load_report_progress_signal.emit("loading", "Checking compatibility...", get_progress(35))
self.controller.hardware_state.hardware_report = validated_data
self.controller.hardware_state.hardware_report, self.controller.macos_state.native_version, self.controller.macos_state.ocl_patched_version, self.controller.hardware_state.compatibility_error = self.controller.backend.c.check_compatibility(validated_data)
self.load_report_progress_signal.emit("loading", "Checking compatibility...", get_progress(55))
self.compatibility_checked_signal.emit()
if self.controller.hardware_state.compatibility_error:
error_msg = self.controller.hardware_state.compatibility_error
if isinstance(error_msg, list):
error_msg = "\n".join(error_msg)
self.load_report_finished_signal.emit(False, "compatibility_error", report_path, acpi_dir)
return
self.load_report_progress_signal.emit("loading", "Loading ACPI tables...", get_progress(60))
self.controller.backend.ac.read_acpi_tables(acpi_dir)
self.load_report_progress_signal.emit("loading", "Loading ACPI tables...", get_progress(90))
if not self.controller.backend.ac._ensure_dsdt():
self.load_report_finished_signal.emit(False, "acpi_error", report_path, acpi_dir)
return
self.load_report_finished_signal.emit(True, "success", report_path, acpi_dir)
except Exception as e:
self.load_report_finished_signal.emit(False, "Exception: {}".format(e), report_path, acpi_dir)
thread = threading.Thread(target=load_thread, daemon=True)
thread.start()
def _handle_load_report_progress(self, status, message, progress):
self.progress_helper.update(status, message, progress)
def _handle_report_validated(self, report_path, message):
self.set_detail_status("report", report_path, "success", message)
def _handle_compatibility_checked(self):
self.controller.compatibilityPage.update_display()
def _handle_load_report_finished(self, success, error_type, report_path, acpi_dir):
self.select_btn.setEnabled(True)
if hasattr(self, "export_btn"):
self.export_btn.setEnabled(True)
if success:
count = len(self.controller.backend.ac.acpi.acpi_tables)
self.set_detail_status("acpi", acpi_dir, "success", "ACPI Tables loaded: {} tables found.".format(count))
self.progress_helper.update("success", "Hardware report loaded successfully", 100)
self.controller.update_status("Hardware report loaded successfully", "success")
self.suggest_macos_version()
self.controller.configurationPage.update_display()
else:
if error_type == "validation_error":
is_valid, errors, warnings, validated_data = self.controller.backend.v.validate_report(report_path)
msg = "Report Errors:\n" + "\n".join(errors)
self.set_detail_status("report", report_path, "error", msg)
self.progress_helper.update("error", "Report validation failed", None)
show_info("Report Validation Failed", "The hardware report has errors:\n{}\n\nPlease select a valid report file.".format("\n".join(errors)))
elif error_type == "compatibility_error":
error_msg = self.controller.hardware_state.compatibility_error
if isinstance(error_msg, list):
error_msg = "\n".join(error_msg)
compat_text = "\nCompatibility Error:\n{}".format(error_msg)
self.set_detail_status("report", report_path, "error", compat_text)
show_info("Incompatible Hardware", "Your hardware is not compatible with macOS:\n\n" + error_msg)
elif error_type == "acpi_error":
self.set_detail_status("acpi", acpi_dir, "error", "No ACPI tables found in selected folder.")
self.progress_helper.update("error", "No ACPI tables found", None)
show_info("No ACPI tables", "No ACPI tables found in ACPI folder.")
else:
self.progress_helper.update("error", "Error: {}".format(error_type), None)
self.controller.update_status("Failed to load hardware report: {}".format(error_type), "error")
def export_hardware_report(self):
self.progress_container.setVisible(True)
self.select_btn.setEnabled(False)
if hasattr(self, "export_btn"):
self.export_btn.setEnabled(False)
self.progress_helper.update("loading", "Gathering Hardware Sniffer...", 10)
current_dir = os.path.dirname(os.path.realpath(__file__))
main_dir = os.path.dirname(os.path.dirname(current_dir))
report_dir = os.path.join(main_dir, "SysReport")
def export_thread():
try:
hardware_sniffer = self.controller.backend.o.gather_hardware_sniffer()
if not hardware_sniffer:
self.export_finished_signal.emit(False, "Hardware Sniffer not found", "", "")
return
self.export_finished_signal.emit(True, "gathering_complete", hardware_sniffer, report_dir)
except Exception as e:
self.export_finished_signal.emit(False, "Exception gathering sniffer: {}".format(e), "", "")
thread = threading.Thread(target=export_thread, daemon=True)
thread.start()
def _handle_export_finished(self, success, message, hardware_sniffer_or_error, report_dir):
if not success:
self.progress_container.setVisible(False)
self.select_btn.setEnabled(True)
if hasattr(self, "export_btn"):
self.export_btn.setEnabled(True)
self.progress_helper.update("error", "Export failed", 0)
self.controller.update_status(hardware_sniffer_or_error, "error")
return
if message == "gathering_complete":
self.progress_helper.update("loading", "Exporting hardware report...", 50)
def run_export_thread():
try:
output = self.controller.backend.r.run({
"args": [hardware_sniffer_or_error, "-e", "-o", report_dir]
})
success = output[-1] == 0
error_message = ""
report_path = ""
acpi_dir = ""
if success:
report_path = os.path.join(report_dir, "Report.json")
acpi_dir = os.path.join(report_dir, "ACPI")
error_message = "Export successful"
else:
error_code = output[-1]
if error_code == 3: error_message = "Error collecting hardware."
elif error_code == 4: error_message = "Error generating hardware report."
elif error_code == 5: error_message = "Error dumping ACPI tables."
else: error_message = "Unknown error."
paths = "{}|||{}".format(report_path, acpi_dir) if report_path and acpi_dir else ""
self.export_finished_signal.emit(success, "export_complete", error_message, paths)
except Exception as e:
self.export_finished_signal.emit(False, "export_complete", "Exception: {}".format(e), "")
thread = threading.Thread(target=run_export_thread, daemon=True)
thread.start()
return
if message == "export_complete":
self.progress_container.setVisible(False)
self.select_btn.setEnabled(True)
if hasattr(self, "export_btn"):
self.export_btn.setEnabled(True)
self.controller.backend.u.log_message("[EXPORT] Export at: {}".format(report_dir), level="INFO")
if success:
if report_dir and "|||" in report_dir:
report_path, acpi_dir = report_dir.split("|||", 1)
else:
report_path = ""
acpi_dir = ""
if report_path and acpi_dir:
self.load_hardware_report(report_path, acpi_dir, from_export=True)
else:
self.progress_helper.update("error", "Export completed but paths are invalid", None)
self.controller.update_status("Export completed but paths are invalid", "error")
else:
self.progress_helper.update("error", "Export failed: {}".format(hardware_sniffer_or_error), None)
self.controller.update_status("Export failed: {}".format(hardware_sniffer_or_error), "error")
+271
View File
@@ -0,0 +1,271 @@
import os
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QFileDialog
)
from PyQt6.QtCore import Qt
from qfluentwidgets import (
ScrollArea, BodyLabel, PushButton, LineEdit, FluentIcon,
SettingCardGroup, SwitchSettingCard, ComboBoxSettingCard,
PushSettingCard, SpinBox,
OptionsConfigItem, OptionsValidator, HyperlinkCard,
StrongBodyLabel, CaptionLabel, SettingCard, SubtitleLabel,
setTheme, Theme
)
from Scripts.custom_dialogs import show_confirmation
from Scripts.styles import COLORS, SPACING
class SettingsPage(ScrollArea):
def __init__(self, parent):
super().__init__(parent)
self.setObjectName("settingsPage")
self.controller = parent
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.settings = self.controller.backend.settings
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.enableTransparentBackground()
self._init_ui()
def _init_ui(self):
self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.expandLayout.setSpacing(SPACING["large"])
header_container = QWidget()
header_layout = QVBoxLayout(header_container)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(SPACING["tiny"])
title_label = SubtitleLabel("Settings")
header_layout.addWidget(title_label)
subtitle_label = BodyLabel("Configure OpCore Simplify preferences")
subtitle_label.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
header_layout.addWidget(subtitle_label)
self.expandLayout.addWidget(header_container)
self.expandLayout.addSpacing(SPACING["medium"])
self.build_output_group = self.create_build_output_group()
self.expandLayout.addWidget(self.build_output_group)
self.macos_group = self.create_macos_version_group()
self.expandLayout.addWidget(self.macos_group)
#self.appearance_group = self.create_appearance_group()
#self.expandLayout.addWidget(self.appearance_group)
self.update_group = self.create_update_settings_group()
self.expandLayout.addWidget(self.update_group)
self.advanced_group = self.create_advanced_group()
self.expandLayout.addWidget(self.advanced_group)
self.help_group = self.create_help_group()
self.expandLayout.addWidget(self.help_group)
self.bottom_widget = QWidget()
bottom_layout = QHBoxLayout(self.bottom_widget)
bottom_layout.setContentsMargins(0, SPACING["large"], 0, SPACING["large"])
bottom_layout.setSpacing(SPACING["medium"])
bottom_layout.addStretch()
reset_btn = PushButton("Reset All to Defaults", self.bottom_widget)
reset_btn.setIcon(FluentIcon.CANCEL)
reset_btn.clicked.connect(self.reset_to_defaults)
bottom_layout.addWidget(reset_btn)
self.expandLayout.addWidget(self.bottom_widget)
for card in self.findChildren(SettingCard):
card.setIconSize(18, 18)
def _update_widget_value(self, widget, value):
if widget is None:
return
if isinstance(widget, SwitchSettingCard):
widget.switchButton.setChecked(value)
elif isinstance(widget, (ComboBoxSettingCard, OptionsConfigItem)):
widget.setValue(value)
elif isinstance(widget, SpinBox):
widget.setValue(value)
elif isinstance(widget, LineEdit):
widget.setText(value)
elif isinstance(widget, PushSettingCard):
widget.setContent(value or "Use temporary directory (default)")
def create_build_output_group(self):
group = SettingCardGroup("Build Output", self.scrollWidget)
self.output_dir_card = PushSettingCard(
"Browse",
FluentIcon.FOLDER,
"Output Directory",
self.settings.get("build_output_directory") or "Use temporary directory (default)",
group
)
self.output_dir_card.setObjectName("build_output_directory")
self.output_dir_card.clicked.connect(self.browse_output_directory)
group.addSettingCard(self.output_dir_card)
return group
def create_macos_version_group(self):
group = SettingCardGroup("macOS Version", self.scrollWidget)
self.include_beta_card = SwitchSettingCard(
FluentIcon.UPDATE,
"Include beta version",
"Show major beta macOS versions in version selection menus. Enable to test new macOS releases.",
configItem=None,
parent=group
)
self.include_beta_card.setObjectName("include_beta_versions")
self.include_beta_card.switchButton.setChecked(self.settings.get_include_beta_versions())
self.include_beta_card.switchButton.checkedChanged.connect(lambda checked: self.settings.set("include_beta_versions", checked))
group.addSettingCard(self.include_beta_card)
return group
def create_appearance_group(self):
group = SettingCardGroup("Appearance", self.scrollWidget)
theme_values = [
"Light",
#"Dark",
]
theme_value = self.settings.get_theme()
if theme_value not in theme_values:
theme_value = "Light"
self.theme_config = OptionsConfigItem(
"Appearance",
"Theme",
theme_value,
OptionsValidator(theme_values)
)
def on_theme_changed(value):
self.settings.set("theme", value)
if value == "Dark":
setTheme(Theme.DARK)
else:
setTheme(Theme.LIGHT)
self.theme_config.valueChanged.connect(on_theme_changed)
self.theme_card = ComboBoxSettingCard(
self.theme_config,
FluentIcon.BRUSH,
"Theme",
"Selects the application color theme.",
theme_values,
group
)
self.theme_card.setObjectName("theme")
group.addSettingCard(self.theme_card)
return group
def create_update_settings_group(self):
group = SettingCardGroup("Updates & Downloads", self.scrollWidget)
self.auto_update_card = SwitchSettingCard(
FluentIcon.UPDATE,
"Check for updates on startup",
"Automatically checks for new OpCore Simplify updates when the application launches to keep you up to date",
configItem=None,
parent=group
)
self.auto_update_card.setObjectName("auto_update_check")
self.auto_update_card.switchButton.setChecked(self.settings.get_auto_update_check())
self.auto_update_card.switchButton.checkedChanged.connect(lambda checked: self.settings.set("auto_update_check", checked))
group.addSettingCard(self.auto_update_card)
return group
def create_advanced_group(self):
group = SettingCardGroup("Advanced Settings", self.scrollWidget)
self.debug_logging_card = SwitchSettingCard(
FluentIcon.DEVELOPER_TOOLS,
"Enable debug logging",
"Enables detailed debug logging throughout the application for advanced troubleshooting and diagnostics",
configItem=None,
parent=group
)
self.debug_logging_card.setObjectName("enable_debug_logging")
self.debug_logging_card.switchButton.setChecked(self.settings.get_enable_debug_logging())
self.debug_logging_card.switchButton.checkedChanged.connect(lambda checked: self.settings.set("enable_debug_logging", checked))
group.addSettingCard(self.debug_logging_card)
return group
def create_help_group(self):
group = SettingCardGroup("Help & Documentation", self.scrollWidget)
self.opencore_docs_card = HyperlinkCard(
"https://dortania.github.io/OpenCore-Install-Guide/",
"OpenCore Install Guide",
FluentIcon.BOOK_SHELF,
"OpenCore Documentation",
"Complete guide for installing macOS with OpenCore",
group
)
group.addSettingCard(self.opencore_docs_card)
self.troubleshoot_card = HyperlinkCard(
"https://dortania.github.io/OpenCore-Install-Guide/troubleshooting/troubleshooting.html",
"Troubleshooting",
FluentIcon.HELP,
"Troubleshooting Guide",
"Solutions to common OpenCore installation issues",
group
)
group.addSettingCard(self.troubleshoot_card)
self.github_card = HyperlinkCard(
"https://github.com/lzhoang2801/OpCore-Simplify",
"View on GitHub",
FluentIcon.GITHUB,
"OpCore-Simplify Repository",
"Report issues, contribute, or view the source code",
group
)
group.addSettingCard(self.github_card)
return group
def browse_output_directory(self):
folder = QFileDialog.getExistingDirectory(
self,
"Select Build Output Directory",
os.path.expanduser("~")
)
if folder:
self.settings.set("build_output_directory", folder)
self.output_dir_card.setContent(folder)
self.controller.update_status("Output directory updated successfully", "success")
def reset_to_defaults(self):
result = show_confirmation("Reset Settings", "Are you sure you want to reset all settings to their default values?")
if result:
self.settings.settings = self.settings.defaults.copy()
self.settings.save_settings()
for widget in self.findChildren(QWidget):
key = widget.objectName()
if key and key in self.settings.defaults:
default_value = self.settings.defaults.get(key)
self._update_widget_value(widget, default_value)
self.controller.update_status("All settings reset to defaults", "success")