mirror of
https://github.com/outbackdingo/OpCore-Simplify.git
synced 2026-08-25 14:53:06 +00:00
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:
@@ -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",
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user