From f3e2dfc4de63fb0daf4689db1d9b37b12a7d15df Mon Sep 17 00:00:00 2001 From: Mykola Grymalyuk Date: Sat, 6 May 2023 19:49:29 -0600 Subject: [PATCH] Push experimental build --- resources/constants.py | 1 + resources/main.py | 4 +- resources/wx_gui/gui_build.py | 112 ++++++++++++ resources/wx_gui/gui_entry.py | 70 ++++++++ resources/wx_gui/gui_install_oc.py | 277 +++++++++++++++++++++++++++++ resources/wx_gui/gui_main_menu.py | 99 +++++++++++ resources/wx_gui/gui_menubar.py | 14 ++ resources/wx_gui/gui_support.py | 139 +++++++++++++++ resources/wx_gui/gui_sys_patch.py | 202 +++++++++++++++++++++ 9 files changed, 917 insertions(+), 1 deletion(-) create mode 100644 resources/wx_gui/gui_build.py create mode 100644 resources/wx_gui/gui_entry.py create mode 100644 resources/wx_gui/gui_install_oc.py create mode 100644 resources/wx_gui/gui_main_menu.py create mode 100644 resources/wx_gui/gui_menubar.py create mode 100644 resources/wx_gui/gui_support.py create mode 100644 resources/wx_gui/gui_sys_patch.py diff --git a/resources/constants.py b/resources/constants.py index 642fbee5d..be3f90157 100644 --- a/resources/constants.py +++ b/resources/constants.py @@ -15,6 +15,7 @@ class Constants: self.patcher_version: str = "0.6.6" # OpenCore-Legacy-Patcher self.patcher_support_pkg_version: str = "1.0.0" # PatcherSupportPkg self.copyright_date: str = "Copyright © 2020-2023 Dortania" + self.patcher_name: str = "OpenCore Legacy Patcher" # URLs self.url_patcher_support_pkg: str = "https://github.com/dortania/PatcherSupportPkg/releases/download/" diff --git a/resources/main.py b/resources/main.py index 75daeb699..278d1ff33 100644 --- a/resources/main.py +++ b/resources/main.py @@ -7,6 +7,7 @@ import threading from pathlib import Path from resources.gui import gui_main +from resources.wx_gui import gui_entry from resources import ( constants, utilities, @@ -38,7 +39,8 @@ class OpenCoreLegacyPatcher: self._generate_base_data() if utilities.check_cli_args() is None: - gui_main.wx_python_gui(self.constants).main_menu(None) + # gui_main.wx_python_gui(self.constants).main_menu(None) + gui_entry.EntryPoint(self.constants).start() def _generate_base_data(self) -> None: diff --git a/resources/wx_gui/gui_build.py b/resources/wx_gui/gui_build.py new file mode 100644 index 000000000..e66b23da6 --- /dev/null +++ b/resources/wx_gui/gui_build.py @@ -0,0 +1,112 @@ +import wx +import logging +import threading + +from resources.wx_gui import gui_main_menu, gui_install_oc, gui_support +from resources.build import build +from resources import constants + + +class BuildFrame(wx.Frame): + """ + Create a frame for building OpenCore + Uses a Modal Dialog for smoother transition from other frames + """ + def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None): + super(BuildFrame, self).__init__(parent, title=title, size=(350, 200)) + + self.install_button: wx.Button = None + self.text_box: wx.TextCtrl = None + self.frame_modal: wx.Dialog = None + + self.constants: constants.Constants = global_constants + self.title: str = title + self.stock_output = logging.getLogger().handlers[0].stream + + self.frame_modal = wx.Dialog(self, title=title, size=(400, 200)) + + self._generate_elements(self.frame_modal) + + self.SetPosition(screen_location) if screen_location else self.Centre() + self.frame_modal.ShowWindowModal() + + self._invoke_build() + + + def _generate_elements(self, frame=None) -> None: + """ + Generate UI elements for build frame + + Format: + - Title label: Build and Install OpenCore + - Text: Model: {Build or Host Model} + - Button: Install OpenCore + - Read-only text box: {empty} + - Button: Return to Main Menu + """ + frame = self if not frame else frame + + title_label = wx.StaticText(frame, label="Build and Install OpenCore", pos=(-1,5)) + title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + title_label.Center(wx.HORIZONTAL) + + model_label = wx.StaticText(frame, label=f"Model: {self.constants.custom_model or self.constants.computer.real_model}", pos=(-1,30)) + model_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + model_label.Center(wx.HORIZONTAL) + + # Button: Install OpenCore + install_button = wx.Button(frame, label="🔩 Install OpenCore", pos=(-1, model_label.GetPosition()[1] + model_label.GetSize()[1]), size=(150, 30)) + install_button.Bind(wx.EVT_BUTTON, self.on_install) + install_button.Center(wx.HORIZONTAL) + install_button.Disable() + self.install_button = install_button + + # Read-only text box: {empty} + text_box = wx.TextCtrl(frame, value="", pos=(-1, install_button.GetPosition()[1] + install_button.GetSize()[1] + 10), size=(400, 350), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH2) + text_box.Center(wx.HORIZONTAL) + self.text_box = text_box + + # Button: Return to Main Menu + return_button = wx.Button(frame, label="Return to Main Menu", pos=(-1, text_box.GetPosition()[1] + text_box.GetSize()[1] + 5), size=(200, 30)) + return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu) + return_button.Center(wx.HORIZONTAL) + + # Adjust window size to fit all elements + frame.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40)) + + + def _invoke_build(self): + """ + Calls build function and redirects stdout to the text box + """ + logging.getLogger().handlers[0].stream = gui_support.RedirectText(self.text_box, False) + build.BuildOpenCore(self.constants.custom_model or self.constants.computer.real_model, self.constants) + logging.getLogger().handlers[0].stream = self.stock_output + self.install_button.Enable() + + + def on_return_to_main_menu(self, event): + self.frame_modal.Hide() + main_menu_frame = gui_main_menu.MainMenu( + None, + title=self.title, + global_constants=self.constants, + screen_location=self.GetScreenPosition() + ) + main_menu_frame.Show() + self.frame_modal.Destroy() + self.Destroy() + + + def on_install(self, event): + self.frame_modal.Destroy() + self.Destroy() + install_oc_frame = gui_install_oc.InstallOCFrame( + None, + title=self.title, + global_constants=self.constants, + screen_location=self.GetScreenPosition() + ) + install_oc_frame.Show() + + diff --git a/resources/wx_gui/gui_entry.py b/resources/wx_gui/gui_entry.py new file mode 100644 index 000000000..71ab18bbc --- /dev/null +++ b/resources/wx_gui/gui_entry.py @@ -0,0 +1,70 @@ +import wx +import sys +import logging +import atexit + +from resources import constants +from resources.wx_gui import ( + gui_main_menu, + gui_build, + gui_menubar, + gui_install_oc +) + +class SupportedEntryPoints: + """ + Enum for supported entry points + """ + MAIN_MENU = gui_main_menu.MainMenu + BUILD_OC = gui_build.BuildFrame + INSTALL_OC = gui_install_oc.InstallOCFrame + +class EntryPoint: + + def __init__(self, global_constants: constants.Constants) -> None: + self.app: wx.App = None + self.main_menu_frame: gui_main_menu.MainMenu = None + self.constants: constants.Constants = global_constants + + self.constants.gui_mode = True + + + def _generate_base_data(self) -> None: + self.app = wx.App() + + + def start(self, entry: SupportedEntryPoints = gui_main_menu.MainMenu) -> None: + """ + Launches entry point for the wxPython GUI + """ + self._generate_base_data() + self.frame: wx.Frame = entry( + None, + title=f"{self.constants.patcher_name} ({self.constants.patcher_version})", + global_constants=self.constants, + screen_location=None + ) + self.frame.SetMenuBar(gui_menubar.GenerateMenubar().generate()) + atexit.register(self.OnCloseFrame) + + self.app.MainLoop() + + + def OnCloseFrame(self, event=None): + """ + Closes the wxPython GUI + """ + + if not self.frame: + return + + logging.info("- Cleaning up wxPython GUI") + + self.frame.SetTransparent(0) + wx.GetApp().Yield() + + self.frame.DestroyChildren() + self.frame.Destroy() + self.app.ExitMainLoop() + + sys.exit() diff --git a/resources/wx_gui/gui_install_oc.py b/resources/wx_gui/gui_install_oc.py new file mode 100644 index 000000000..09540f69d --- /dev/null +++ b/resources/wx_gui/gui_install_oc.py @@ -0,0 +1,277 @@ +import wx +import threading +import logging + +from resources.wx_gui import gui_main_menu, gui_support +from resources import constants, install + + +class InstallOCFrame(wx.Frame): + """ + Create a frame for installing OpenCore to disk + """ + def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None): + super(InstallOCFrame, self).__init__(parent, title=title, size=(300, 120)) + + self.constants: constants.Constants = global_constants + self.title: str = title + + self.available_disks: dict = None + self.stock_output = logging.getLogger().handlers[0].stream + + self._generate_elements() + + self.SetPosition(screen_location) if screen_location else self.Centre() + self.Show() + + self._display_disks() + + + def _generate_elements(self) -> None: + """ + Display indeterminate progress bar while collecting disk information + + Format: + - Title label: Install OpenCore + - Text: Fetching information on local disks... + - Progress bar: {indeterminate} + """ + + # Title label: Install OpenCore + title_label = wx.StaticText(self, label="Install OpenCore", pos=(-1,5)) + title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + title_label.Center(wx.HORIZONTAL) + + # Text: Parsing local disks... + text_label = wx.StaticText(self, label="Fetching information on local disks...", pos=(-1,30)) + text_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + text_label.Center(wx.HORIZONTAL) + self.text_label = text_label + + # Progress bar: {indeterminate} + progress_bar = wx.Gauge(self, range=100, pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1]), size=(150, 30), style=wx.GA_HORIZONTAL | wx.GA_SMOOTH) + progress_bar.Center(wx.HORIZONTAL) + progress_bar.Pulse() + self.progress_bar = progress_bar + + + def _fetch_disks(self) -> None: + """ + Fetch information on local disks + """ + self.available_disks = install.tui_disk_installation(self.constants).list_disks() + + + def _display_disks(self) -> None: + """ + Display disk selection dialog + """ + thread = threading.Thread(target=self._fetch_disks) + thread.start() + + while thread.is_alive(): + wx.Yield() + continue + + self.progress_bar.Hide() + + # Create wxDialog for disk selection + dialog = wx.Dialog(self, title=self.title, size=(370, -1)) + + # Title label: Install OpenCore + title_label = wx.StaticText(dialog, label="Install OpenCore", pos=(-1,5)) + title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + title_label.Center(wx.HORIZONTAL) + + # Text: select disk to install OpenCore onto + text_label = wx.StaticText(dialog, label="Select disk to install OpenCore onto:", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5)) + text_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + text_label.Center(wx.HORIZONTAL) + + # Add note: "Missing disks? Ensure they're FAT32 or formatted as GUID/GPT" + gpt_note = wx.StaticText(dialog, label="Missing disks? Ensure they're FAT32 or formatted as GUID/GPT", pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + 5)) + gpt_note.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + gpt_note.Center(wx.HORIZONTAL) + + # Add buttons for each disk + if self.available_disks is None: + # Text: Failed to find any applicable disks + disk_label = wx.StaticText(dialog, label="Failed to find any applicable disks", pos=(-1, gpt_note.GetPosition()[1] + gpt_note.GetSize()[1] + 5)) + disk_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + disk_label.Center(wx.HORIZONTAL) + else: + disk_root = self.constants.booted_oc_disk + if disk_root: + # disk6s1 -> disk6 + disk_root = self.constants.booted_oc_disk.strip("disk") + disk_root = "disk" + disk_root.split("s")[0] + + # Add buttons for each disk + for disk in self.available_disks: + # Create a button for each disk + logging.info(f"- {self.available_disks[disk]['disk']} - {self.available_disks[disk]['name']} - {self.available_disks[disk]['size']}") + disk_button = wx.Button(dialog, label=f"{self.available_disks[disk]['disk']} - {self.available_disks[disk]['name']} - {self.available_disks[disk]['size']}", size=(300,30), pos=(-1, gpt_note.GetPosition()[1] + gpt_note.GetSize()[1] + 5)) + disk_button.Center(wx.HORIZONTAL) + disk_button.Bind(wx.EVT_BUTTON, lambda event, disk=disk: self._display_volumes(disk, self.available_disks)) + if disk_root == self.available_disks[disk]['disk']: + disk_button.SetForegroundColour((25, 179, 231)) + + + if disk_root: + # Add note: "Note: Blue represent the disk OpenCore is currently booted from" + disk_label = wx.StaticText(dialog, label="Note: Blue represent the disk OpenCore is currently booted from", pos=(-1, disk_button.GetPosition()[1] + disk_button.GetSize()[1] + 5)) + disk_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + disk_label.Center(wx.HORIZONTAL) + else: + disk_label = wx.StaticText(dialog, label="", pos=(-1, disk_button.GetPosition()[1] + 15)) + disk_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + + # Add button: Search for disks again + search_button = wx.Button(dialog, label="Search for disks again", size=(200,30), pos=(-1, disk_label.GetPosition()[1] + disk_label.GetSize()[1] + 5)) + search_button.Center(wx.HORIZONTAL) + search_button.Bind(wx.EVT_BUTTON, self._reload_frame) + + # Add button: Return to main menu + return_button = wx.Button(dialog, label="Return to main menu", size=(200,30), pos=(-1, search_button.GetPosition()[1] + 25)) + return_button.Center(wx.HORIZONTAL) + return_button.Bind(wx.EVT_BUTTON, self._return_to_main_menu) + + # Set size + dialog.SetSize((370, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40)) + dialog.ShowWindowModal() + self.dialog = dialog + + + def _display_volumes(self, disk: str, dataset: dict) -> None: + """ + List volumes on disk + """ + + self.dialog.Close() + + # Create dialog + dialog = wx.Dialog( + self, + title=f"Volumes on {disk}", + style=wx.CAPTION | wx.CLOSE_BOX, + size=(370, 300) + ) + + # Add text: "Volumes on {disk}" + text_label = wx.StaticText(dialog, label=f"Volumes on {disk}", pos=(-1, 10)) + text_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + text_label.Center(wx.HORIZONTAL) + + partitions = install.tui_disk_installation(self.constants).list_partitions(disk, dataset) + for partition in partitions: + logging.info(f"- {partitions[partition]['partition']} - {partitions[partition]['name']} - {partitions[partition]['size']}") + disk_button = wx.Button(dialog, label=f"{partitions[partition]['partition']} - {partitions[partition]['name']} - {partitions[partition]['size']}", size=(300,30), pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + 5)) + disk_button.Center(wx.HORIZONTAL) + disk_button.Bind(wx.EVT_BUTTON, lambda event, partition=partition: self._install_oc_process(partition)) + + # Add button: Return to main menu + return_button = wx.Button(dialog, label="Return to main menu", size=(200,30), pos=(-1, disk_button.GetPosition()[1] + disk_button.GetSize()[1])) + return_button.Center(wx.HORIZONTAL) + return_button.Bind(wx.EVT_BUTTON, self._return_to_main_menu) + + # Set size + dialog.SetSize((370, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40)) + + # Show dialog + dialog.ShowWindowModal() + self.dialog = dialog + + + def _install_oc_process(self, partition: dict) -> None: + """ + Install OpenCore to disk + """ + self.dialog.Close() + + # Create dialog + dialog = wx.Dialog( + self, + title=f"Installing OpenCore to {partition}", + style=wx.CAPTION | wx.CLOSE_BOX, + size=(370, 200) + ) + + # Add text: "Installing OpenCore to {partition}" + text_label = wx.StaticText(dialog, label=f"Installing OpenCore to {partition}", pos=(-1, 10)) + text_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + text_label.Center(wx.HORIZONTAL) + + # Read-only text box: {empty} + text_box = wx.TextCtrl(dialog, value="", pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + 10), size=(370, 200), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH2) + text_box.Center(wx.HORIZONTAL) + self.text_box = text_box + + # Add button: Return to main menu + return_button = wx.Button(dialog, label="Return to main menu", size=(200,30), pos=(-1, text_box.GetPosition()[1] + text_box.GetSize()[1] + 10)) + return_button.Center(wx.HORIZONTAL) + return_button.Bind(wx.EVT_BUTTON, self._return_to_main_menu) + return_button.Disable() + + # Set size + dialog.SetSize((370, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40)) + + # Show dialog + dialog.ShowWindowModal() + self.dialog = dialog + + # Install OpenCore + self._install_oc(partition) + return_button.Enable() + + + def _install_oc(self, partition: dict) -> None: + """ + Install OpenCore to disk + """ + logging.info(f"- Installing OpenCore to {partition}") + logging.getLogger().handlers[0].stream = gui_support.RedirectText(self.text_box, False) + result = install.tui_disk_installation(self.constants).install_opencore(partition) + logging.getLogger().handlers[0].stream = self.stock_output + + if result is True: + if not self.constants.custom_model: + gui_support.RestartHost(self).restart(message="OpenCore has finished installing to disk.\n\nYou will need to reboot and hold the Option key and select OpenCore/Boot EFI's option.\n\nWould you like to reboot?") + else: + popup_message = wx.MessageDialog( + self, + f"OpenCore has finished installing to disk.\n\nYou can eject the drive, insert it into the {self.constants.custom_model}, reboot, hold the Option key and select OpenCore/Boot EFI's option.", "Success", + wx.OK + ) + popup_message.ShowModal() + + + def _reload_frame(self, event) -> None: + """ + Reload frame + """ + self.Destroy() + frame = InstallOCFrame( + None, + title=self.title, + global_constants=self.constants, + screen_location=self.GetScreenPosition() + ) + frame.Show() + + + def _return_to_main_menu(self, event: wx.Event) -> None: + """ + Return to main menu + """ + main_menu_frame = gui_main_menu.MainMenu( + None, + title=self.title, + global_constants=self.constants, + screen_location=self.GetScreenPosition() + ) + main_menu_frame.Show() + self.Destroy() + + + + diff --git a/resources/wx_gui/gui_main_menu.py b/resources/wx_gui/gui_main_menu.py new file mode 100644 index 000000000..1751f8ad8 --- /dev/null +++ b/resources/wx_gui/gui_main_menu.py @@ -0,0 +1,99 @@ +import wx +from resources.wx_gui import ( + gui_build, + gui_sys_patch, + gui_support, +) +from resources import constants + +class MainMenu(wx.Frame): + def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None): + super(MainMenu, self).__init__(parent, title=title, size=(350, 300)) + + self.constants: constants.Constants = global_constants + self.title: str = title + + self._generate_elements() + + self.SetPosition(screen_location) if screen_location else self.Centre() + self.Show() + + + def _generate_elements(self) -> None: + """ + Generate UI elements for the main menu + + Format: + - Title label: OpenCore Legacy Patcher v{X.Y.Z} + - Text: Model: {Build or Host Model} + - Buttons: + - Build and Install OpenCore + - Post-Install Root Patch + - Create macOS Installer + - Settings + - Help + - Text: Copyright + """ + + # Title label: OpenCore Legacy Patcher v{X.Y.Z} + title_label = wx.StaticText(self, label=f"OpenCore Legacy Patcher v{self.constants.patcher_version}", pos=(-1,1)) + title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + title_label.Center(wx.HORIZONTAL) + + # Text: Model: {Build or Host Model} + model_label = wx.StaticText(self, label=f"Model: {self.constants.custom_model or self.constants.computer.real_model}", pos=(-1,30)) + model_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + model_label.Center(wx.HORIZONTAL) + + # Buttons: + menu_buttons = { + "Build and Install OpenCore": self.on_build_and_install, + "Post-Install Root Patch": self.on_post_install_root_patch, + "Create macOS Installer": self.on_create_macos_installer, + "Settings": self.on_settings, + "Help": self.on_help + } + button_y = model_label.GetPosition()[1] + 20 + for button_name, button_function in menu_buttons.items(): + button = wx.Button(self, label=button_name, pos=(-1, button_y), size=(200, 30)) + button.Bind(wx.EVT_BUTTON, button_function) + button.Center(wx.HORIZONTAL) + button_y += 30 + + # Text: Copyright + copy_label = wx.StaticText(self, label=self.constants.copyright_date, pos=(-1, button_y + 10)) + copy_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + copy_label.Center(wx.HORIZONTAL) + + # Set window size + self.SetSize((350, copy_label.GetPosition()[1] + 50)) + + + def on_build_and_install(self, event): + gui_build.BuildFrame( + parent=None, + title=self.title, + global_constants=self.constants, + screen_location=self.GetPosition() + ) + self.Destroy() + + + def on_post_install_root_patch(self, event): + gui_sys_patch.SysPatchMenu( + parent=None, + title=self.title, + global_constants=self.constants, + screen_location=self.GetPosition() + ) + self.Destroy() + + + def on_create_macos_installer(self, event): + pass + + def on_settings(self, event): + pass + + def on_help(self, event): + pass diff --git a/resources/wx_gui/gui_menubar.py b/resources/wx_gui/gui_menubar.py new file mode 100644 index 000000000..c1a1e9b19 --- /dev/null +++ b/resources/wx_gui/gui_menubar.py @@ -0,0 +1,14 @@ +# Generates menubar for wxPython to use + +import wx + +class GenerateMenubar: + + def __init__(self) -> None: + self.menubar: wx.MenuBar = None + + + def generate(self) -> wx.MenuBar: + self.menubar = wx.MenuBar() + + return self.menubar \ No newline at end of file diff --git a/resources/wx_gui/gui_support.py b/resources/wx_gui/gui_support.py new file mode 100644 index 000000000..8271fb8dc --- /dev/null +++ b/resources/wx_gui/gui_support.py @@ -0,0 +1,139 @@ +import wx +import time +import sys +import time +import logging + +import subprocess + +from resources import constants + +class RedirectText(object): + """ + Redirects stdout to a wxPython TextCtrl + """ + + def __init__(self, aWxTextCtrl: wx.TextCtrl, sleep: bool): + self.out = aWxTextCtrl + self.sleep = sleep + + def write(self,string): + self.out.WriteText(string) + wx.GetApp().Yield() + if self.sleep: + time.sleep(0.01) + + def fileno(self): + return 1 + + def flush(self): + pass + + +class RestartHost: + """ + Restarts the host machine + """ + + def __init__(self, frame) -> None: + self.frame: wx.Frame = frame + + + def restart(self, event: wx.Event = None, message: str = ""): + self.popup = wx.MessageDialog( + self.frame, + message, + "Reboot to apply?", + wx.YES_NO | wx.ICON_INFORMATION + ) + self.popup.SetYesNoLabels("Reboot", "Ignore") + answer = self.popup.ShowModal() + if answer == wx.ID_YES: + # Reboots with Count Down prompt (user can still dismiss if needed) + self.frame.Hide() + wx.GetApp().Yield() + subprocess.call(['osascript', '-e', 'tell app "loginwindow" to «event aevtrrst»']) + sys.exit(0) + + +class RelaunchApplicationAsRoot: + """ + Relaunches the application as root + """ + + def __init__(self, frame: wx.Frame, global_constants: constants.Constants) -> None: + self.constants = global_constants + self.frame: wx.Frame = frame + + def relaunch(self, event: wx.Event): + + self.dialog = wx.MessageDialog( + self.frame, + "OpenCore Legacy Patcher needs to relaunch as admin to continue. You will be prompted to enter your password.", + "Relaunch as root?", + wx.YES_NO | wx.ICON_QUESTION + ) + + # Show Dialog Box + if self.dialog.ShowModal() != wx.ID_YES: + logging.info("User cancelled relaunch") + return + + + timer: int = 5 + program_arguments: str = "" + + if event: + if event.GetEventObject() != wx.Menu: + try: + if event.GetEventObject().GetLabel() in ["Start Root Patching", "Reinstall Root Patches"]: + program_arguments = " --gui_patch" + elif event.GetEventObject().GetLabel() == "Revert Root Patches": + program_arguments = " --gui_unpatch" + except TypeError: + pass + + if self.constants.launcher_script is None: + program_arguments = f"'{self.constants.launcher_binary}'{program_arguments}" + else: + program_arguments = f"{self.constants.launcher_binary} {self.constants.launcher_script}{program_arguments}" + + # Relaunch as root + args = [ + "osascript", + "-e", + f'''do shell script "{program_arguments}"''' + ' with prompt "OpenCore Legacy Patcher needs administrator privileges to relaunch as admin."' + " with administrator privileges" + " without altering line endings", + ] + + self.frame.DestroyChildren() + self.frame.SetSize(400, 300) + + # Header + header = wx.StaticText(self.frame, label="Relaunching as root") + header.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + header.Centre(wx.HORIZONTAL) + + # Add count down label + countdown_label = wx.StaticText(self.frame, label=f"Closing old process in {timer} seconds", pos=(0, header.GetPosition().y + header.GetSize().height + 3)) + countdown_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + countdown_label.Center(wx.HORIZONTAL) + + # Set size of frame + self.frame.SetSize((-1, countdown_label.GetPosition().y + countdown_label.GetSize().height + 40)) + + wx.GetApp().Yield() + + logging.info(f"- Relaunching as root with command: {program_arguments}") + subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + while True: + wx.GetApp().Yield() + countdown_label.SetLabel(f"Closing old process in {timer} seconds") + time.sleep(1) + timer -= 1 + if timer == 0: + break + sys.exit(0) \ No newline at end of file diff --git a/resources/wx_gui/gui_sys_patch.py b/resources/wx_gui/gui_sys_patch.py new file mode 100644 index 000000000..86ba38666 --- /dev/null +++ b/resources/wx_gui/gui_sys_patch.py @@ -0,0 +1,202 @@ + +import wx +import logging +import plistlib +from pathlib import Path + +from resources import constants +from resources.sys_patch import ( + sys_patch, + sys_patch_detect +) + +from resources.wx_gui import ( + gui_main_menu +) + +class SysPatchMenu(wx.Frame): + """ + Create a frame for building OpenCore + Uses a Modal Dialog for smoother transition from other frames + """ + def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None): + super(SysPatchMenu, self).__init__(parent, title=title, size=(350, 250)) + + self.title = title + self.constants: constants.Constants = global_constants + self.frame_modal: wx.Dialog = None + + self.frame_modal = wx.Dialog(self, title=title, size=(370, 200)) + + self._generate_elements(self.frame_modal) + + self.SetPosition(screen_location) if screen_location else self.Centre() + self.frame_modal.ShowWindowModal() + + + + def _generate_elements(self, frame=None) -> None: + """ + Generate UI elements for root patching frame + + Format: + - Title label: Post-Install Menu + - Label: Available patches: + - Labels: {patch name} + - Button: Start Root Patching + - Button: Revert Root Patches + - Button: Return to Main Menu + """ + frame = self if not frame else frame + + title_label = wx.StaticText(frame, label="Post-Install Menu", pos=(-1, 10)) + title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + title_label.Center(wx.HORIZONTAL) + + # Label: Available patches: + available_label = wx.StaticText(frame, label="Available patches for your system:", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 10)) + available_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + available_label.Center(wx.HORIZONTAL) + + # Labels: {patch name} + patches: dict = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set() + can_unpatch: bool = patches["Validation: Unpatching Possible"] + + if not any(not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True for patch in patches): + logging.info("- No applicable patches available") + patches = [] + + # Check if OCLP has already applied the same patches + no_new_patches = not self._check_if_new_patches_needed(patches) if patches else False + + + if not patches: + # Prompt user with no patches found + patch_label = wx.StaticText(frame, label="No patches needed", pos=(-1, available_label.GetPosition()[1] + 20)) + patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + patch_label.Center(wx.HORIZONTAL) + + else: + # Add Label for each patch + i = 0 + if no_new_patches is True: + patch_label = wx.StaticText(frame, label="No new patches needed", pos=(-1, 50)) + patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + patch_label.Center(wx.HORIZONTAL) + i = i + 10 + else: + for patch in patches: + if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True): + logging.info(f"- Adding patch: {patch} - {patches[patch]}") + patch_label = wx.StaticText(frame, label=f"- {patch}", pos=(available_label.GetPosition()[0] + , available_label.GetPosition()[1] + 20 + i)) + patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + i = i + 5 + + + if patches["Validation: Patching Possible"] is False: + # Cannot patch due to the following reasons: + i = i + 10 + patch_label = wx.StaticText(frame, label="Cannot patch due to the following reasons:", pos=(-1, patch_label.GetPosition().y + i + 10)) + patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + patch_label.Center(wx.HORIZONTAL) + + + for patch in patches: + if not patch.startswith("Validation"): + continue + if patches[patch] is False: + continue + if patch == "Validation: Unpatching Possible": + continue + + patch_label = wx.StaticText(frame, label=f"- {patch.split('Validation: ')[1]}", pos=(available_label.GetPosition().x - 10, patch_label.GetPosition().y + 20)) + patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + # patch_label.Center(wx.HORIZONTAL) + + + # Button: Start Root Patching + start_button = wx.Button(frame, label="Start Root Patching", pos=(10, patch_label.GetPosition().y + 40), size=(170, 30)) + start_button.Bind(wx.EVT_BUTTON, lambda event: self._start_root_patching(frame, patches, no_new_patches)) + start_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + start_button.Center(wx.HORIZONTAL) + + # Button: Revert Root Patches + revert_button = wx.Button(frame, label="Revert Root Patches", pos=(10, start_button.GetPosition().y + start_button.GetSize().height - 5 + ), size=(170, 30)) + revert_button.Bind(wx.EVT_BUTTON, lambda event: self._revert_root_patching(frame, patches, can_unpatch)) + revert_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + revert_button.Center(wx.HORIZONTAL) + + # Button: Return to Main Menu + return_button = wx.Button(frame, label="Return to Main Menu", pos=(10, revert_button.GetPosition().y + revert_button.GetSize().height + 10), size=(150, 30)) + return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu) + return_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + return_button.Center(wx.HORIZONTAL) + + # Set frame size + frame.SetSize((-1, return_button.GetPosition().y + return_button.GetSize().height + 40)) + + + + def _start_root_patching(self, frame: wx.Frame, patches: dict, no_new_patches: bool): + pass + + + def _revert_root_patching(self, frame: wx.Frame, patches: dict, can_unpatch: bool): + pass + + def on_return_to_main_menu(self, event): + self.frame_modal.Hide() + main_menu_frame = gui_main_menu.MainMenu( + None, + title=self.title, + global_constants=self.constants, + screen_location=self.GetScreenPosition() + ) + main_menu_frame.Show() + self.frame_modal.Destroy() + self.Destroy() + + + + + def _check_if_new_patches_needed(self, patches: dict) -> bool: + """ + Checks if any new patches are needed for the user to install + Newer users will assume the root patch menu will present missing patches. + Thus we'll need to see if the exact same OCLP build was used already + """ + + if self.constants.commit_info[0] in ["Running from source", "Built from source"]: + return True + + if self.constants.computer.oclp_sys_url != self.constants.commit_info[2]: + # If commits are different, assume patches are as well + return True + + oclp_plist = "/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist" + if not Path(oclp_plist).exists(): + # If it doesn't exist, no patches were ever installed + # ie. all patches applicable + return True + + oclp_plist_data = plistlib.load(open(oclp_plist, "rb")) + for patch in patches: + if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True): + # Patches should share the same name as the plist key + # See sys_patch_dict.py for more info + patch_installed = False + for key in oclp_plist_data: + if "Display Name" not in oclp_plist_data[key]: + continue + if oclp_plist_data[key]["Display Name"] == patch: + patch_installed = True + break + + if patch_installed is False: + logging.info(f"- Patch {patch} not installed") + return True + + logging.info("- No new patches detected for system") + return False \ No newline at end of file