Ejemplo n.º 1
0
class CoilSnakeGui(object):
    def __init__(self):
        self.preferences = CoilSnakePreferences()
        self.preferences.load()
        self.components = []
        self.progress_bar = None

    # Preferences functions

    def refresh_debug_logging(self):
        if self.preferences["debug mode"]:
            logging.root.setLevel(logging.DEBUG)
        else:
            logging.root.setLevel(logging.INFO)

    def refresh_debug_mode_command_label(self):
        # The "Debug Mode" command is the 5th in the Preferences menu (starting counting at 0, including separators)
        self.pref_menu.entryconfig(5,
                                   label=self.get_debug_mode_command_label())

    def get_debug_mode_command_label(self):
        return 'Disable Debug Mode' if self.preferences[
            "debug mode"] else 'Enable Debug Mode'

    def set_debug_mode(self):
        if self.preferences["debug mode"]:
            confirm = tkinter.messagebox.askquestion(
                "Disable Debug Mode?",
                "Would you like to disable Debug mode?",
                icon="question")

            if confirm == "yes":
                self.preferences["debug mode"] = False
        else:
            confirm = tkinter.messagebox.askquestion(
                "Enable Debug Mode?",
                "Would you like to enable Debug mode? Debug mode will provide you with more detailed output while "
                + "CoilSnake is running.\n\n" +
                "This is generally only needed by advanced users.",
                icon="question")

            if confirm == "yes":
                self.preferences["debug mode"] = True

        self.preferences.save()

        self.refresh_debug_logging()
        self.refresh_debug_mode_command_label()

    def set_emulator_exe(self):
        tkinter.messagebox.showinfo(
            "Select the Emulator Executable",
            "Select an emulator executable for CoilSnake to use.\n\n"
            "Hint: It is probably named either zsnesw.exe, snes9x.exe, or higan-accuracy.exe"
        )

        emulator_exe = tkinter.filedialog.askopenfilename(
            parent=self.root,
            initialdir=os.path.expanduser("~"),
            title="Select an Emulator Executable")
        if emulator_exe:
            self.preferences["emulator"] = emulator_exe
            self.preferences.save()

    def set_ccscript_offset(self):
        ccscript_offset_str = tkinter.simpledialog.askstring(
            title="Input CCScript Offset",
            prompt=
            ("Specify the hexidecimal offset to which CCScript should compile text.\n"
             + "(The default value is F10000)\n\n" +
             "You should leave this setting alone unless if you really know what you are doing."
             ),
            initialvalue="{:x}".format(
                self.preferences.get_ccscript_offset()).upper())

        if ccscript_offset_str:
            try:
                ccscript_offset = int(ccscript_offset_str, 16)
            except:
                tkinter.messagebox.showerror(
                    parent=self.root,
                    title="Error",
                    message="{} is not a valid hexidecimal number.".format(
                        ccscript_offset_str))
                return

            self.preferences.set_ccscript_offset(ccscript_offset)
            self.preferences.save()

    def get_java_exe(self):
        return self.preferences["java"] or find_system_java_exe()

    def set_java_exe(self):
        system_java_exe = find_system_java_exe()

        if system_java_exe:
            confirm = tkinter.messagebox.askquestion(
                "Configure Java",
                "CoilSnake has detected Java at the following location:\n\n" +
                system_java_exe + "\n\n" +
                "To use this installation of Java, select \"Yes\".\n\n" +
                "To override and instead use a different version of Java, select \"No\".",
                icon="question")
            if confirm == "yes":
                self.preferences["java"] = None
                self.preferences.save()
                return

        tkinter.messagebox.showinfo(
            "Select the Java Executable",
            "Select a Java executable for CoilSnake to use.\n\n"
            "On Windows, it might be called \"javaw.exe\" or \"java.exe\".")

        java_exe = tkinter.filedialog.askopenfilename(
            parent=self.root,
            title="Select the Java Executable",
            initialfile=(self.preferences["java"] or system_java_exe))
        if java_exe:
            self.preferences["java"] = java_exe
            self.preferences.save()

    def save_default_tab(self):
        tab_number = self.notebook.index(self.notebook.select())
        self.preferences.set_default_tab(tab_number)
        self.preferences.save()

    def save_geometry_and_close(self, e=None):
        self.preferences['width'] = self.root.winfo_width()
        self.preferences['height'] = self.root.winfo_height()
        self.preferences['xpos'] = self.root.winfo_rootx()
        self.preferences['ypos'] = self.root.winfo_rooty()
        self.preferences.save()
        self.root.destroy()

    def load_geometry(self):
        self.root.update_idletasks()
        width = self.preferences['width'] or self.root.winfo_width()
        height = self.preferences['height'] or self.root.winfo_height()
        xpos = self.preferences['xpos'] or self.root.winfo_rootx()
        ypos = self.preferences['ypos'] or self.root.winfo_rooty()

        if platform.system() != "Windows" and platform.system() != "Darwin":
            # Workaround - On X11, the window coordinates refer to the window border rather than the content
            self.root.geometry('{}x{}+0+0'.format(width, height))
            self.root.update_idletasks()
            xpos -= self.root.winfo_rootx()
            ypos -= self.root.winfo_rooty()

        self.root.geometry('{}x{}+{}+{}'.format(width, height, xpos, ypos))
        self.root.update_idletasks()

    # GUI update functions
    def disable_all_components(self):
        for component in self.components:
            component["state"] = DISABLED

    def enable_all_components(self):
        for component in self.components:
            component["state"] = NORMAL

    # GUI popup functions

    def run_rom(self, entry):
        rom_filename = entry.get()
        if not self.preferences["emulator"]:
            tkinter.messagebox.showerror(
                parent=self.root,
                title="Error",
                message="""CoilSnake could not find an emulator.
Please configure your emulator in the Settings menu.""")
        elif rom_filename:
            Popen([self.preferences["emulator"], rom_filename])

    def open_ebprojedit(self, entry=None):
        if entry:
            project_path = entry.get()
        else:
            project_path = None

        java_exe = self.get_java_exe()
        if not java_exe:
            tkinter.messagebox.showerror(
                parent=self.root,
                title="Error",
                message="""CoilSnake could not find Java.
Please configure Java in the Settings menu.""")
            return

        command = [java_exe, "-jar", asset_path(["bin", "EbProjEdit.jar"])]
        if project_path:
            command.append(os.path.join(project_path, PROJECT_FILENAME))

        Popen(command)

    # Actions

    def do_decompile(self, rom_entry, project_entry):
        rom = rom_entry.get()
        project = project_entry.get()

        if rom and project:
            if os.path.isdir(project):
                confirm = tkinter.messagebox.askquestion(
                    "Are You Sure?",
                    "Are you sure you would like to permanently overwrite the "
                    + "contents of the selected output directory?",
                    icon='warning')
                if confirm != "yes":
                    return

            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()

            self.progress_bar.clear()
            thread = Thread(target=self._do_decompile_help,
                            args=(rom, project))
            thread.start()

    def _do_decompile_help(self, rom, project):
        try:
            decompile_rom(rom_filename=rom,
                          project_path=project,
                          progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.clear()
        self.enable_all_components()

    def do_compile(self, project_entry, base_rom_entry, rom_entry):
        base_rom = base_rom_entry.get()
        rom = rom_entry.get()
        project = project_entry.get()

        if base_rom and rom and project:
            self.save_default_tab()

            base_rom_rom = Rom()
            base_rom_rom.from_file(base_rom)
            if base_rom_rom.type == "Earthbound" and len(
                    base_rom_rom) == 0x300000:
                confirm = tkinter.messagebox.askquestion(
                    "Expand Your Base ROM?",
                    "You are attempting to compile using a base ROM which is "
                    "unexpanded. It is likely that this will not succeed, as CoilSnake "
                    "needs the extra space in an expanded ROM to store additional data."
                    "\n\n"
                    "Would you like to expand this base ROM before proceeding? This "
                    "will permanently overwrite your base ROM.",
                    icon='warning')
                if confirm == "yes":
                    base_rom_rom.expand(0x400000)
                    base_rom_rom.to_file(base_rom)
            del base_rom_rom

            # Update the GUI
            self.console.clear()
            self.disable_all_components()

            self.progress_bar.clear()

            log.info("Starting compilation...")

            thread = Thread(target=self._do_compile_help,
                            args=(project, base_rom, rom))
            thread.start()

    def _do_compile_help(self, project, base_rom, rom):
        try:
            compile_project(
                project,
                base_rom,
                rom,
                ccscript_offset=self.preferences.get_ccscript_offset(),
                progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.clear()
        self.enable_all_components()

    def do_upgrade(self, rom_entry, project_entry):
        rom = rom_entry.get()
        project = project_entry.get()

        if rom and project:
            confirm = tkinter.messagebox.askquestion(
                "Are You Sure?",
                "Are you sure you would like to upgrade this project? This operation "
                + "cannot be undone.\n\n" +
                "It is recommended that you backup your project before proceeding.",
                icon='warning')
            if confirm != "yes":
                return

            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()

            self.progress_bar.clear()
            thread = Thread(target=self._do_upgrade_help, args=(rom, project))
            thread.start()

    def _do_upgrade_help(self, rom, project):
        try:
            upgrade_project(project_path=project,
                            base_rom_filename=rom,
                            progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.clear()
        self.enable_all_components()

    def do_decompile_script(self, rom_entry, project_entry):
        rom = rom_entry.get()
        project = project_entry.get()

        if rom and project:
            confirm = tkinter.messagebox.askquestion(
                "Are You Sure?",
                "Are you sure you would like to decompile the script into this "
                "project? This operation cannot be undone.\n\n" +
                "It is recommended that you backup your project before proceeding.",
                icon='warning')
            if confirm != "yes":
                return

            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()
            self.progress_bar.cycle_animation_start()

            thread = Thread(target=self._do_decompile_script_help,
                            args=(rom, project))
            thread.start()

    def _do_decompile_script_help(self, rom, project):
        try:
            decompile_script(rom_filename=rom,
                             project_path=project,
                             progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.cycle_animation_stop()
        self.enable_all_components()

    def do_patch_rom(self, clean_rom_entry, patched_rom_entry, patch_entry,
                     headered_var):
        clean_rom = clean_rom_entry.get()
        patched_rom = patched_rom_entry.get()
        patch = patch_entry.get()
        headered = headered_var.get()

        if clean_rom and patched_rom and patch:
            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()
            self.progress_bar.cycle_animation_start()

            thread = Thread(target=self._do_patch_rom_help,
                            args=(clean_rom, patched_rom, patch, headered))
            thread.start()

    def _do_patch_rom_help(self, clean_rom, patched_rom, patch, headered):
        try:
            patch_rom(clean_rom,
                      patched_rom,
                      patch,
                      headered,
                      progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.cycle_animation_stop()
        self.enable_all_components()

    def do_create_patch(self, clean_rom_entry, hacked_rom_entry,
                        patch_path_entry, author, description, title):
        clean_rom = clean_rom_entry.get()
        hacked_rom = hacked_rom_entry.get()
        patch_path = patch_path_entry.get()

        if clean_rom and hacked_rom and patch_path:
            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()
            self.progress_bar.cycle_animation_start()

            thread = Thread(target=self._do_create_patch_help,
                            args=(clean_rom, hacked_rom, patch_path, author,
                                  description, title))
            thread.start()

    def _do_create_patch_help(self, clean_rom, hacked_rom, patch_path, author,
                              description, title):
        try:
            if patch_path.endswith(".ebp"):
                create_patch(clean_rom,
                             hacked_rom,
                             patch_path,
                             author,
                             description,
                             title,
                             progress_bar=self.progress_bar)
            elif patch_path.endswith(".ips"):
                create_patch(clean_rom,
                             hacked_rom,
                             patch_path,
                             "",
                             "",
                             "",
                             progress_bar=self.progress_bar)
            else:
                log.info(
                    "Could not patch ROM: Invalid patch format. Please end patchfile with either .ebp or .ips."
                )
                return
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.cycle_animation_stop()
        self.enable_all_components()

    def main(self):
        self.create_gui()
        self.root.mainloop()

    def create_gui(self):
        self.root = Tk()
        self.root.wm_title("CoilSnake " + information.VERSION)

        if platform.system() == "Windows":
            self.root.tk.call("wm", "iconbitmap", self.root._w,
                              asset_path(["images", "CoilSnake.ico"]))
        elif platform.system() == "Darwin":
            # Workaround - Raise the window
            from Cocoa import NSRunningApplication, NSApplicationActivateIgnoringOtherApps

            app = NSRunningApplication.runningApplicationWithProcessIdentifier_(
                os.getpid())
            app.activateWithOptions_(NSApplicationActivateIgnoringOtherApps)
        else:
            self.iconphoto_params = (
                True,
                ImageTk.PhotoImage(file=asset_path(["images", "16.png"])),
                ImageTk.PhotoImage(file=asset_path(["images", "22.png"])),
                ImageTk.PhotoImage(file=asset_path(["images", "24.png"])),
                ImageTk.PhotoImage(file=asset_path(["images", "32.png"])),
                ImageTk.PhotoImage(file=asset_path(["images", "48.png"])),
                ImageTk.PhotoImage(file=asset_path(["images", "64.png"])),
                ImageTk.PhotoImage(file=asset_path(["images", "128.png"])),
                ImageTk.PhotoImage(file=asset_path(["images", "256.png"])))
            self.root.wm_iconphoto(*self.iconphoto_params)

        self.create_menubar()

        self.notebook = tkinter.ttk.Notebook(self.root)

        decompile_frame = self.create_decompile_frame(self.notebook)
        self.notebook.add(decompile_frame, text="Decompile")

        compile_frame = self.create_compile_frame(self.notebook)
        self.notebook.add(compile_frame, text="Compile")

        upgrade_frame = self.create_upgrade_frame(self.notebook)
        self.notebook.add(upgrade_frame, text="Upgrade")

        decompile_script_frame = self.create_decompile_script_frame(
            self.notebook)
        self.notebook.add(decompile_script_frame, text="Decompile Script")

        patcher_patch_frame = self.create_apply_patch_frame(self.notebook)
        self.notebook.add(patcher_patch_frame, text="Apply Patch")

        patcher_create_frame = self.create_create_patch_frame(self.notebook)
        self.notebook.add(patcher_create_frame, text="Create Patch")

        self.notebook.pack(fill=X)
        self.notebook.select(self.preferences.get_default_tab())

        self.progress_bar = CoilSnakeGuiProgressBar(self.root,
                                                    orient=HORIZONTAL,
                                                    mode='determinate')
        self.progress_bar.pack(fill=X)

        console_frame = Frame(self.root)

        scrollbar = Scrollbar(console_frame)
        scrollbar.pack(side=RIGHT, fill=Y)

        self.console = ThreadSafeConsole(console_frame, width=80, height=8)
        self.console.pack(fill=BOTH, expand=1)
        scrollbar.config(command=self.console.yview)
        self.console.config(yscrollcommand=scrollbar.set)
        console_frame.pack(fill=BOTH, expand=1)

        def selectall_text(event):
            event.widget.tag_add("sel", "1.0", "end")

        self.root.bind_class("Text", "<Control-a>", selectall_text)

        def selectall_entry(event):
            event.widget.selection_range(0, END)

        self.root.bind_class("Entry", "<Control-a>", selectall_entry)

        def tab_changed(event):
            # Do this so some random element in the tab isn't selected upon tab change
            self.notebook.focus()

            ## Recalculate the height of the notebook depending on the contents of the new tab

            # Ensure the dimensions of the widgets are up to date
            self.notebook.update_idletasks()

            # Get the width and height of the window, so we can reset it later
            width = self.root.winfo_width()
            height = self.root.winfo_height()

            # Set the notebook height to the selected tab's requested height
            tab_window_name = self.notebook.select()
            tab = self.notebook.nametowidget(tab_window_name)
            tab_height = tab.winfo_reqheight()
            self.notebook.configure(height=tab_height)

            # Keeps the window from changing size
            self.root.geometry("{}x{}".format(width, height))

        self.notebook.bind("<<NotebookTabChanged>>", tab_changed)

        self.console_stream = self.console

        setup_logging(quiet=False, verbose=False, stream=self.console_stream)
        self.refresh_debug_logging()
        self.load_geometry()
        self.root.protocol("WM_DELETE_WINDOW", self.save_geometry_and_close)

    def create_about_window(self):
        self.about_menu = Toplevel(self.root, takefocus=True)

        if platform.system() == "Windows":
            self.about_menu.tk.call("wm", "iconbitmap", self.about_menu._w,
                                    asset_path(["images", "CoilSnake.ico"]))
        elif platform.system() != "Darwin":
            self.about_menu.wm_iconphoto(*self.iconphoto_params)

        photo_header = ImageTk.PhotoImage(
            file=asset_path(["images", "CS4_logo.png"]))
        about_header = Label(self.about_menu,
                             image=photo_header,
                             anchor=CENTER)
        about_header.photo = photo_header
        about_header.pack(side=TOP, fill=BOTH, expand=1)

        photo = ImageTk.PhotoImage(file=asset_path(["images", "logo.png"]))
        about_label = Label(self.about_menu, image=photo, justify=RIGHT)
        about_label.photo = photo
        about_label.pack(side=LEFT, fill=BOTH, expand=1)

        about_right_frame = tkinter.ttk.Frame(self.about_menu)
        Label(about_right_frame,
              text=coilsnake_about(),
              font=("Courier", 10),
              anchor=CENTER,
              justify=LEFT).pack(fill=BOTH, expand=1, side=TOP)

        about_right_frame.pack(side=LEFT, fill=BOTH, expand=1)

        self.about_menu.resizable(False, False)
        self.about_menu.title("About CoilSnake {}".format(information.VERSION))
        self.about_menu.withdraw()
        self.about_menu.transient(self.root)

        self.about_menu.protocol('WM_DELETE_WINDOW', self.about_menu.withdraw)

    def create_menubar(self):
        menubar = Menu(self.root)

        # Add 'About CoilSnake' to the app menu on macOS
        self.create_about_window()

        def show_about_window():
            self.about_menu.deiconify()
            self.about_menu.lift()

        if platform.system() == "Darwin":
            app_menu = Menu(menubar, name='apple')
            menubar.add_cascade(menu=app_menu)
            app_menu.add_command(label="About CoilSnake",
                                 command=show_about_window)

        # Tools pulldown menu
        tools_menu = Menu(menubar, tearoff=0)
        tools_menu.add_command(label="EB Project Editor",
                               command=self.open_ebprojedit)
        tools_menu.add_separator()
        tools_menu.add_command(label="Expand ROM to 32 MBit",
                               command=partial(gui_util.expand_rom, self.root))
        tools_menu.add_command(label="Expand ROM to 48 MBit",
                               command=partial(gui_util.expand_rom_ex,
                                               self.root))
        tools_menu.add_separator()
        tools_menu.add_command(label="Add Header to ROM",
                               command=partial(gui_util.add_header_to_rom,
                                               self.root))
        tools_menu.add_command(label="Remove Header from ROM",
                               command=partial(gui_util.strip_header_from_rom,
                                               self.root))
        menubar.add_cascade(label="Tools", menu=tools_menu)

        # Preferences pulldown menu
        self.pref_menu = Menu(menubar, tearoff=0)
        self.pref_menu.add_command(label="Configure Emulator",
                                   command=self.set_emulator_exe)
        self.pref_menu.add_command(label="Configure Java",
                                   command=self.set_java_exe)
        self.pref_menu.add_separator()
        self.pref_menu.add_command(label="Configure CCScript",
                                   command=self.set_ccscript_offset)
        self.pref_menu.add_separator()
        self.pref_menu.add_command(label=self.get_debug_mode_command_label(),
                                   command=self.set_debug_mode)
        menubar.add_cascade(label="Settings", menu=self.pref_menu)

        # Help menu
        help_menu = Menu(menubar, tearoff=0)

        def open_coilsnake_website():
            webbrowser.open(information.WEBSITE, 2)

        if platform.system() != "Darwin":
            help_menu.add_command(label="About CoilSnake",
                                  command=show_about_window)

        help_menu.add_command(label="CoilSnake Website",
                              command=open_coilsnake_website)

        menubar.add_cascade(label="Help", menu=help_menu)

        self.root.config(menu=menubar)

    def create_decompile_frame(self, notebook):
        self.decompile_fields = dict()

        decompile_frame = tkinter.ttk.Frame(notebook)
        self.add_title_label_to_frame(
            text="Decompile a ROM to create a new project.",
            frame=decompile_frame)

        profile_selector_init = self.add_profile_selector_to_frame(
            frame=decompile_frame,
            tab="decompile",
            fields=self.decompile_fields)

        input_rom_entry = self.add_rom_fields_to_frame(name="ROM",
                                                       frame=decompile_frame)
        self.decompile_fields["rom"] = input_rom_entry
        project_entry = self.add_project_fields_to_frame(
            name="Output Directory", frame=decompile_frame)
        self.decompile_fields["output_directory"] = project_entry

        profile_selector_init()

        def decompile_tmp():
            self.do_decompile(input_rom_entry, project_entry)

        decompile_button = Button(decompile_frame,
                                  text="Decompile",
                                  command=decompile_tmp)
        decompile_button.pack(fill=X, expand=1)
        self.components.append(decompile_button)

        return decompile_frame

    def create_compile_frame(self, notebook):
        self.compile_fields = dict()

        compile_frame = tkinter.ttk.Frame(notebook)
        self.add_title_label_to_frame(
            text="Compile a project to create a new ROM.", frame=compile_frame)

        profile_selector_init = self.add_profile_selector_to_frame(
            frame=compile_frame, tab="compile", fields=self.compile_fields)

        base_rom_entry = self.add_rom_fields_to_frame(name="Base ROM",
                                                      frame=compile_frame)
        self.compile_fields["base_rom"] = base_rom_entry
        project_entry = self.add_project_fields_to_frame(name="Project",
                                                         frame=compile_frame)
        self.compile_fields["project"] = project_entry
        output_rom_entry = self.add_rom_fields_to_frame(name="Output ROM",
                                                        frame=compile_frame,
                                                        save=True)
        self.compile_fields["output_rom"] = output_rom_entry

        profile_selector_init()

        def compile_tmp():
            self.do_compile(project_entry, base_rom_entry, output_rom_entry)

        compile_button = Button(compile_frame,
                                text="Compile",
                                command=compile_tmp)
        compile_button.pack(fill=X, expand=1)
        self.components.append(compile_button)

        return compile_frame

    def create_upgrade_frame(self, notebook):
        upgrade_frame = tkinter.ttk.Frame(notebook)
        self.add_title_label_to_frame(
            text=
            "Upgrade a project created using an older version of CoilSnake.",
            frame=upgrade_frame)

        rom_entry = self.add_rom_fields_to_frame(name="Clean ROM",
                                                 frame=upgrade_frame)
        project_entry = self.add_project_fields_to_frame(name="Project",
                                                         frame=upgrade_frame)

        def upgrade_tmp():
            self.preferences["default upgrade rom"] = rom_entry.get()
            self.preferences.save()
            self.do_upgrade(rom_entry, project_entry)

        self.upgrade_button = Button(upgrade_frame,
                                     text="Upgrade",
                                     command=upgrade_tmp)
        self.upgrade_button.pack(fill=X, expand=1)
        self.components.append(self.upgrade_button)

        if self.preferences["default upgrade rom"]:
            set_entry_text(entry=rom_entry,
                           text=self.preferences["default upgrade rom"])

        return upgrade_frame

    def create_decompile_script_frame(self, notebook):
        decompile_script_frame = tkinter.ttk.Frame(notebook)
        self.add_title_label_to_frame(
            text="Decompile a ROM's script to an already existing project.",
            frame=decompile_script_frame)

        input_rom_entry = self.add_rom_fields_to_frame(
            name="ROM", frame=decompile_script_frame)
        project_entry = self.add_project_fields_to_frame(
            name="Project", frame=decompile_script_frame)

        def decompile_script_tmp():
            self.preferences[
                "default decompile script rom"] = input_rom_entry.get()
            self.preferences.save()
            self.do_decompile_script(input_rom_entry, project_entry)

        button = Button(decompile_script_frame,
                        text="Decompile Script",
                        command=decompile_script_tmp)
        button.pack(fill=X, expand=1)
        self.components.append(button)

        if self.preferences["default decompile script rom"]:
            set_entry_text(
                entry=input_rom_entry,
                text=self.preferences["default decompile script rom"])

        return decompile_script_frame

    def create_apply_patch_frame(self, notebook):
        patcher_patch_frame = tkinter.ttk.Frame(notebook)
        self.add_title_label_to_frame("Apply an EBP or IPS patch to a ROM",
                                      patcher_patch_frame)

        clean_rom_entry = self.add_rom_fields_to_frame(
            name="Clean ROM", frame=patcher_patch_frame, padding_buttons=0)
        patched_rom_entry = self.add_rom_fields_to_frame(
            name="Patched ROM",
            frame=patcher_patch_frame,
            save=True,
            padding_buttons=0)
        patch_entry = self.add_patch_fields_to_frame(name="Patch",
                                                     frame=patcher_patch_frame)
        headered_var = self.add_headered_field_to_frame(
            name="ROM Header (IPS only)", frame=patcher_patch_frame)

        def patch_rom_tmp():
            self.preferences["default clean rom"] = clean_rom_entry.get()
            self.preferences["default patched rom"] = patched_rom_entry.get()
            self.preferences["default patch"] = patch_entry.get()
            self.preferences.save()
            self.do_patch_rom(clean_rom_entry, patched_rom_entry, patch_entry,
                              headered_var)

        button = Button(patcher_patch_frame,
                        text="Patch ROM",
                        command=patch_rom_tmp)
        button.pack(fill=X, expand=1)
        self.components.append(button)

        if self.preferences["default clean rom"]:
            set_entry_text(entry=clean_rom_entry,
                           text=self.preferences["default clean rom"])
        if self.preferences["default patched rom"]:
            set_entry_text(entry=patched_rom_entry,
                           text=self.preferences["default patched rom"])
        if self.preferences["default patch"]:
            set_entry_text(entry=patch_entry,
                           text=self.preferences["default patch"])

        return patcher_patch_frame

    def create_create_patch_frame(self, notebook):
        patcher_create_frame = tkinter.ttk.Frame(notebook)
        self.add_title_label_to_frame("Create EBP patch from a ROM",
                                      patcher_create_frame)

        clean_rom_entry = self.add_rom_fields_to_frame(
            name="Clean ROM", frame=patcher_create_frame, padding_buttons=0)
        hacked_rom_entry = self.add_rom_fields_to_frame(
            name="Modified ROM", frame=patcher_create_frame, padding_buttons=0)
        patch_entry = self.add_patch_fields_to_frame(
            name="Patch", frame=patcher_create_frame, save=True)

        def create_patch_tmp(author, description, title):
            self.preferences["default clean rom"] = clean_rom_entry.get()
            self.preferences["default hacked rom"] = hacked_rom_entry.get()
            self.preferences["default created patch"] = patch_entry.get()
            self.preferences.save()
            self.do_create_patch(clean_rom_entry, hacked_rom_entry,
                                 patch_entry, author, description, title)

        def create_patch_do_first():
            if patch_entry.get().endswith(".ebp"):
                popup_ebp_patch_info(self, notebook)
            elif patch_entry.get().endswith(".ips"):
                create_patch_tmp("", "", "")
            else:
                exc = Exception(
                    "Could not create patch because patch does not end in .ips or .ebp"
                )
                log.error(exc)

        def popup_ebp_patch_info(self, notebook):

            if self.preferences["default author"] is None:
                self.preferences["default author"] = "Author"

            author = self.preferences["default author"]

            if self.preferences["default description"] is None:
                self.preferences["default description"] = "Description"

            description = self.preferences["default description"]

            if self.preferences["default title"] is None:
                self.preferences["default title"] = "Title"

            title = self.preferences["default title"]

            top = self.top = Toplevel(notebook)
            top.wm_title("EBP Patch")
            l = Label(top, text="Input EBP Patch Info.")
            l.pack()
            auth = Entry(top)
            auth.delete(0, )
            auth.insert(0, author)
            auth.pack()
            desc = Entry(top)
            desc.delete(0, )
            desc.insert(0, description)
            desc.pack()
            titl = Entry(top)
            titl.delete(0, )
            titl.insert(0, title)
            titl.pack()

            def cleanup():
                author = auth.get()
                self.preferences["default author"] = author
                description = desc.get()
                self.preferences["default description"] = description
                title = titl.get()
                self.preferences["default title"] = title
                self.top.destroy()
                create_patch_tmp(author, description, title)

            self.b = Button(top, text='OK', command=cleanup)
            self.b.pack()

        button = Button(patcher_create_frame,
                        text="Create Patch",
                        command=create_patch_do_first)
        button.pack(fill=X, expand=1)
        self.components.append(button)

        if self.preferences["default clean rom"]:
            set_entry_text(entry=clean_rom_entry,
                           text=self.preferences["default clean rom"])
        if self.preferences["default hacked rom"]:
            set_entry_text(entry=hacked_rom_entry,
                           text=self.preferences["default hacked rom"])
        if self.preferences["default created patch"]:
            set_entry_text(entry=patch_entry,
                           text=self.preferences["default created patch"])

        return patcher_create_frame

    def add_title_label_to_frame(self, text, frame):
        Label(frame, text=text, justify=CENTER).pack(fill=BOTH, expand=1)

    def add_profile_selector_to_frame(self, frame, tab, fields):
        profile_frame = tkinter.ttk.Frame(frame)

        Label(profile_frame, text="Profile:",
              width=LABEL_WIDTH).pack(side=LEFT)

        def tmp_select(profile_name):
            for field_id in fields:
                set_entry_text(entry=fields[field_id],
                               text=self.preferences.get_profile_value(
                                   tab, profile_name, field_id))
            self.preferences.set_default_profile(tab, profile_name)
            self.preferences.save()

        profile_var = StringVar(profile_frame)

        profile = OptionMenu(profile_frame,
                             profile_var,
                             "",
                             command=tmp_select)
        profile.pack(side=LEFT, fill=BOTH, expand=1, ipadx=1)

        self.components.append(profile)

        def tmp_reload_options(selected_profile_name=None):
            profile["menu"].delete(0, END)
            for profile_name in sorted(self.preferences.get_profiles(tab)):
                if not selected_profile_name:
                    selected_profile_name = profile_name
                profile["menu"].add_command(label=profile_name,
                                            command=tkinter._setit(
                                                profile_var, profile_name,
                                                tmp_select))
            profile_var.set(selected_profile_name)
            tmp_select(selected_profile_name)

        def tmp_new():
            profile_name = tkinter.simpledialog.askstring(
                "New Profile Name", "Specify the name of the new profile.")
            if profile_name:
                profile_name = profile_name.strip()
                if self.preferences.has_profile(tab, profile_name):
                    tkinter.messagebox.showerror(
                        parent=self.root,
                        title="Error",
                        message="A profile with that name already exists.")
                    return

                self.preferences.add_profile(tab, profile_name)
                tmp_reload_options(profile_name)
                self.preferences.save()

        def tmp_save():
            profile_name = profile_var.get()
            for field_id in fields:
                self.preferences.set_profile_value(tab, profile_name, field_id,
                                                   fields[field_id].get())
            self.preferences.save()

        def tmp_delete():
            if self.preferences.count_profiles(tab) <= 1:
                tkinter.messagebox.showerror(
                    parent=self.root,
                    title="Error",
                    message="Cannot delete the only profile.")
            else:
                self.preferences.delete_profile(tab, profile_var.get())
                tmp_reload_options()
                self.preferences.save()

        button = Button(profile_frame,
                        text="Save",
                        width=BUTTON_WIDTH,
                        command=tmp_save)
        button.pack(side=LEFT)
        self.components.append(button)

        button = Button(profile_frame,
                        text="Delete",
                        width=BUTTON_WIDTH,
                        command=tmp_delete)
        button.pack(side=LEFT)
        self.components.append(button)

        button = Button(profile_frame,
                        text="New",
                        width=BUTTON_WIDTH,
                        command=tmp_new)
        button.pack(side=LEFT)
        self.components.append(button)

        profile_frame.pack(fill=X, expand=1)

        def tmp_reload_options_and_select_default():
            tmp_reload_options(selected_profile_name=self.preferences.
                               get_default_profile(tab))

        return tmp_reload_options_and_select_default

    def add_rom_fields_to_frame(self,
                                name,
                                frame,
                                save=False,
                                padding_buttons=1):
        rom_frame = tkinter.ttk.Frame(frame)

        Label(rom_frame,
              text="{}:".format(name),
              width=LABEL_WIDTH,
              justify=RIGHT).pack(side=LEFT)
        rom_entry = Entry(rom_frame)
        rom_entry.pack(side=LEFT, fill=BOTH, expand=1, padx=1)
        self.components.append(rom_entry)

        def browse_tmp():
            browse_for_rom(self.root, rom_entry, save)

        def run_tmp():
            self.run_rom(rom_entry)

        button = Button(rom_frame,
                        text="Browse...",
                        command=browse_tmp,
                        width=BUTTON_WIDTH)
        button.pack(side=LEFT)
        self.components.append(button)

        button = Button(rom_frame,
                        text="Run",
                        command=run_tmp,
                        width=BUTTON_WIDTH)
        button.pack(side=LEFT)
        self.components.append(button)

        for i in range(padding_buttons):
            button = Button(rom_frame,
                            text="",
                            width=BUTTON_WIDTH,
                            state=DISABLED,
                            takefocus=False)
            button.pack(side=LEFT)
            button.lower()

        rom_frame.pack(fill=X)

        return rom_entry

    def add_project_fields_to_frame(self, name, frame):
        project_frame = tkinter.ttk.Frame(frame)

        Label(project_frame,
              text="{}:".format(name),
              width=LABEL_WIDTH,
              justify=RIGHT).pack(side=LEFT)
        project_entry = Entry(project_frame)
        project_entry.pack(side=LEFT, fill=BOTH, expand=1, padx=1)
        self.components.append(project_entry)

        def browse_tmp():
            browse_for_project(self.root, project_entry, save=True)

        def open_tmp():
            open_folder(project_entry)

        def edit_tmp():
            self.open_ebprojedit(project_entry)

        button = Button(project_frame,
                        text="Browse...",
                        command=browse_tmp,
                        width=BUTTON_WIDTH)
        button.pack(side=LEFT)
        self.components.append(button)

        button = Button(project_frame,
                        text="Open",
                        command=open_tmp,
                        width=BUTTON_WIDTH)
        button.pack(side=LEFT)
        self.components.append(button)

        button = Button(project_frame,
                        text="Edit",
                        command=edit_tmp,
                        width=BUTTON_WIDTH)
        button.pack(side=LEFT)
        self.components.append(button)

        project_frame.pack(fill=X, expand=1)

        return project_entry

    def add_patch_fields_to_frame(self, name, frame, save=False):
        patch_frame = tkinter.ttk.Frame(frame)

        Label(patch_frame,
              text="{}:".format(name),
              width=LABEL_WIDTH,
              justify=RIGHT).pack(side=LEFT)
        patch_entry = Entry(patch_frame)
        patch_entry.pack(side=LEFT, fill=BOTH, expand=1, padx=1)
        self.components.append(patch_entry)

        def browse_tmp():
            browse_for_patch(self.root, patch_entry, save)

        button = Button(patch_frame,
                        text="Browse...",
                        command=browse_tmp,
                        width=BUTTON_WIDTH)
        button.pack(side=LEFT)
        self.components.append(button)

        button = Button(patch_frame,
                        text="",
                        width=BUTTON_WIDTH,
                        state=DISABLED,
                        takefocus=False)
        button.pack(side=LEFT)
        button.lower()

        patch_frame.pack(fill=BOTH, expand=1)

        return patch_entry

    def add_headered_field_to_frame(self, name, frame):
        patch_frame = tkinter.ttk.Frame(frame)

        headered_var = BooleanVar()
        headered_check = Checkbutton(patch_frame,
                                     text=name,
                                     variable=headered_var)
        headered_check.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(headered_check)
        patch_frame.pack(fill=BOTH, expand=1)

        return headered_var
Ejemplo n.º 2
0
class CoilSnakeGui(object):
    def __init__(self):
        self.preferences = CoilSnakePreferences()
        self.preferences.load()
        self.components = []
        self.progress_bar = None

    # Preferences functions

    def refresh_debug_logging(self):
        if self.preferences["debug mode"]:
            logging.root.setLevel(logging.DEBUG)
        else:
            logging.root.setLevel(logging.INFO)

    def set_debug_mode(self):
        confirm = tkMessageBox.askquestion(
            "Enable Debug Mode?",
            "Would you like to enable Debug mode? Debug mode will provide you with more detailed output while "
            + "CoilSnake is running.\n\n" +
            "This is generally only needed by advanced users.",
            icon="question")

        self.preferences["debug mode"] = (confirm == "yes")
        self.preferences.save()

        self.refresh_debug_logging()

    def set_emulator_exe(self):
        tkMessageBox.showinfo(
            "Select the Emulator Executable",
            "Select an emulator executable for CoilSnake to use.\n\n"
            "Hint: It is probably named either zsnesw.exe, snes9x.exe, or higan-accuracy.exe"
        )

        emulator_exe = tkFileDialog.askopenfilename(
            parent=self.root,
            initialdir=os.path.expanduser("~"),
            title="Select an Emulator Executable")
        if emulator_exe:
            self.preferences["emulator"] = emulator_exe
            self.preferences.save()

    def set_ccscript_offset(self):
        ccscript_offset_str = tkSimpleDialog.askstring(
            title="Input CCScript Offset",
            prompt=
            ("Specify the hexidecimal offset to which CCScript should compile text.\n"
             + "(The default value is F10000)\n\n" +
             "You should leave this setting alone unless if you really know what you are doing."
             ),
            initialvalue="{:x}".format(
                self.preferences.get_ccscript_offset()).upper())

        if ccscript_offset_str:
            try:
                ccscript_offset = int(ccscript_offset_str, 16)
            except:
                tkMessageBox.showerror(
                    parent=self.root,
                    title="Error",
                    message="{} is not a valid hexidecimal number.".format(
                        ccscript_offset_str))
                return

            self.preferences.set_ccscript_offset(ccscript_offset)
            self.preferences.save()

    def get_java_exe(self):
        return self.preferences["java"] or find_system_java_exe()

    def set_java_exe(self):
        system_java_exe = find_system_java_exe()

        if system_java_exe:
            confirm = tkMessageBox.askquestion(
                "Configure Java",
                "CoilSnake has detected Java at the following location:\n\n" +
                system_java_exe + "\n\n" +
                "To use this installation of Java, select \"Yes\".\n\n" +
                "To override and instead use a different version of Java, select \"No\".",
                icon="question")
            if confirm == "yes":
                self.preferences["java"] = None
                self.preferences.save()
                return

        tkMessageBox.showinfo(
            "Select the Java Executable",
            "Select a Java executable for CoilSnake to use.\n\n"
            "On Windows, it might be called \"javaw.exe\" or \"java.exe\".")

        java_exe = tkFileDialog.askopenfilename(
            parent=self.root,
            title="Select the Java Executable",
            initialfile=(self.preferences["java"] or system_java_exe))
        if java_exe:
            self.preferences["java"] = java_exe
            self.preferences.save()

    def save_default_tab(self):
        tab_number = self.notebook.index(self.notebook.select())
        self.preferences.set_default_tab(tab_number)
        self.preferences.save()

    # GUI update functions
    def disable_all_components(self):
        for component in self.components:
            component["state"] = DISABLED

    def enable_all_components(self):
        for component in self.components:
            component["state"] = NORMAL

    # GUI popup functions

    def run_rom(self, entry):
        rom_filename = entry.get()
        if not self.preferences["emulator"]:
            tkMessageBox.showerror(
                parent=self.root,
                title="Error",
                message="""CoilSnake could not find an emulator.
Please configure your emulator in the Settings menu.""")
        elif rom_filename:
            Popen([self.preferences["emulator"], rom_filename])

    def open_ebprojedit(self, entry=None):
        if entry:
            project_path = entry.get()
        else:
            project_path = None

        java_exe = self.get_java_exe()
        if not java_exe:
            tkMessageBox.showerror(parent=self.root,
                                   title="Error",
                                   message="""CoilSnake could not find Java.
Please configure Java in the Settings menu.""")
            return

        command = [java_exe, "-jar", asset_path(["bin", "EbProjEdit.jar"])]
        if project_path:
            command.append(os.path.join(project_path, PROJECT_FILENAME))

        Popen(command)

    # Actions

    def do_decompile(self, rom_entry, project_entry):
        rom = rom_entry.get()
        project = project_entry.get()

        if rom and project:
            if os.path.isdir(project):
                confirm = tkMessageBox.askquestion(
                    "Are You Sure?",
                    "Are you sure you would like to permanently overwrite the "
                    + "contents of the selected output directory?",
                    icon='warning')
                if confirm != "yes":
                    return

            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()

            self.progress_bar.clear()
            thread = Thread(target=self._do_decompile_help,
                            args=(rom, project))
            thread.start()

    def _do_decompile_help(self, rom, project):
        try:
            decompile_rom(rom_filename=rom,
                          project_path=project,
                          progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.clear()
        self.enable_all_components()

    def do_compile(self, project_entry, base_rom_entry, rom_entry):
        base_rom = base_rom_entry.get()
        rom = rom_entry.get()
        project = project_entry.get()

        if base_rom and rom and project:
            self.save_default_tab()

            base_rom_rom = Rom()
            base_rom_rom.from_file(base_rom)
            if base_rom_rom.type == "Earthbound" and len(
                    base_rom_rom) == 0x300000:
                confirm = tkMessageBox.askquestion(
                    "Expand Your Base ROM?",
                    "You are attempting to compile using a base ROM which is "
                    "unexpanded. It is likely that this will not succeed, as CoilSnake "
                    "needs the extra space in an expanded ROM to store additional data."
                    "\n\n"
                    "Would you like to expand this base ROM before proceeding? This "
                    "will permanently overwrite your base ROM.",
                    icon='warning')
                if confirm == "yes":
                    base_rom_rom.expand(0x400000)
                    base_rom_rom.to_file(base_rom)
            del base_rom_rom

            # Update the GUI
            self.console.clear()
            self.disable_all_components()

            self.progress_bar.clear()

            log.info("Starting compilation...")

            thread = Thread(target=self._do_compile_help,
                            args=(project, base_rom, rom))
            thread.start()

    def _do_compile_help(self, project, base_rom, rom):
        try:
            compile_project(
                project,
                base_rom,
                rom,
                ccscript_offset=self.preferences.get_ccscript_offset(),
                progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.clear()
        self.enable_all_components()

    def do_upgrade(self, rom_entry, project_entry):
        rom = rom_entry.get()
        project = project_entry.get()

        if rom and project:
            confirm = tkMessageBox.askquestion(
                "Are You Sure?",
                "Are you sure you would like to upgrade this project? This operation "
                + "cannot be undone.\n\n" +
                "It is recommended that you backup your project before proceeding.",
                icon='warning')
            if confirm != "yes":
                return

            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()

            self.progress_bar.clear()
            thread = Thread(target=self._do_upgrade_help, args=(rom, project))
            thread.start()

    def _do_upgrade_help(self, rom, project):
        try:
            upgrade_project(project_path=project,
                            base_rom_filename=rom,
                            progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.clear()
        self.enable_all_components()

    def do_decompile_script(self, rom_entry, project_entry):
        rom = rom_entry.get()
        project = project_entry.get()

        if rom and project:
            confirm = tkMessageBox.askquestion(
                "Are You Sure?",
                "Are you sure you would like to decompile the script into this "
                "project? This operation cannot be undone.\n\n" +
                "It is recommended that you backup your project before proceeding.",
                icon='warning')
            if confirm != "yes":
                return

            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()
            self.progress_bar.cycle_animation_start()

            thread = Thread(target=self._do_decompile_script_help,
                            args=(rom, project))
            thread.start()

    def _do_decompile_script_help(self, rom, project):
        try:
            decompile_script(rom_filename=rom,
                             project_path=project,
                             progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.cycle_animation_stop()
        self.enable_all_components()

    def do_patch_rom(self, clean_rom_entry, patched_rom_entry, patch_entry,
                     headered_var):
        clean_rom = clean_rom_entry.get()
        patched_rom = patched_rom_entry.get()
        patch = patch_entry.get()
        headered = headered_var.get()

        if clean_rom and patched_rom and patch:
            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()
            self.progress_bar.cycle_animation_start()

            thread = Thread(target=self._do_patch_rom_help,
                            args=(clean_rom, patched_rom, patch, headered))
            thread.start()

    def _do_patch_rom_help(self, clean_rom, patched_rom, patch, headered):
        try:
            patch_rom(clean_rom,
                      patched_rom,
                      patch,
                      headered,
                      progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.cycle_animation_stop()
        self.enable_all_components()

    def main(self):
        self.create_gui()
        self.root.mainloop()

    def create_gui(self):
        self.root = Tk()
        self.root.wm_title("CoilSnake " + information.VERSION)

        self.icon = ImageTk.PhotoImage(file=asset_path(["images", "icon.png"]))
        self.root.tk.call('wm', 'iconphoto', self.root._w, self.icon)

        self.create_menubar()

        self.notebook = ttk.Notebook(self.root)

        decompile_frame = self.create_decompile_frame(self.notebook)
        self.notebook.add(decompile_frame, text="Decompile")

        compile_frame = self.create_compile_frame(self.notebook)
        self.notebook.add(compile_frame, text="Compile")

        upgrade_frame = self.create_upgrade_frame(self.notebook)
        self.notebook.add(upgrade_frame, text="Upgrade")

        decompile_script_frame = self.create_decompile_script_frame(
            self.notebook)
        self.notebook.add(decompile_script_frame, text="Decompile Script")

        patcher_patch_frame = self.create_apply_patch_frame(self.notebook)
        self.notebook.add(patcher_patch_frame, text="Apply Patch")

        #patcher_create_frame = self.create_create_patch_frame(self.notebook)
        #self.notebook.add(patcher_create_frame, text="Create Patch")

        self.notebook.pack(fill=BOTH, expand=1)
        self.notebook.select(self.preferences.get_default_tab())

        self.progress_bar = CoilSnakeGuiProgressBar(self.root,
                                                    orient=HORIZONTAL,
                                                    mode='determinate')
        self.progress_bar.pack(fill=BOTH, expand=1)

        console_frame = Frame(self.root)

        scrollbar = Scrollbar(console_frame)
        scrollbar.pack(side=RIGHT, fill=Y)

        self.console = ThreadSafeConsole(console_frame, width=80, height=8)
        self.console.pack(fill=X)
        scrollbar.config(command=self.console.yview)
        self.console.config(yscrollcommand=scrollbar.set)
        console_frame.pack(fill=X, expand=1)

        def selectall_text(event):
            event.widget.tag_add("sel", "1.0", "end")

        self.root.bind_class("Text", "<Control-a>", selectall_text)

        def selectall_entry(event):
            event.widget.selection_range(0, END)

        self.root.bind_class("Entry", "<Control-a>", selectall_entry)

        def tab_changed(event):
            # Do this so some random element in the tab isn't selected upon tab change
            self.notebook.focus()

        self.notebook.bind("<<NotebookTabChanged>>", tab_changed)

        self.console_stream = self.console

        setup_logging(quiet=False, verbose=False, stream=self.console_stream)
        self.refresh_debug_logging()

    def create_about_window(self):
        self.about_menu = Toplevel(self.root, takefocus=True)
        self.about_menu.tk.call('wm', 'iconphoto', self.about_menu._w,
                                self.icon)

        photo = ImageTk.PhotoImage(file=asset_path(["images", "logo.png"]))
        about_label = Label(self.about_menu, image=photo)
        about_label.photo = photo
        about_label.pack(side=LEFT, expand=1)

        about_right_frame = ttk.Frame(self.about_menu)
        Label(about_right_frame,
              text=coilsnake_about(),
              font=("Courier", 11),
              anchor="w",
              justify="left",
              borderwidth=5,
              relief=GROOVE).pack(fill=BOTH, expand=1, side=TOP)

        about_right_frame.pack(side=LEFT, fill=BOTH, expand=1)

        self.about_menu.resizable(False, False)
        self.about_menu.title("About CoilSnake {}".format(information.VERSION))
        self.about_menu.withdraw()
        self.about_menu.transient(self.root)

        self.about_menu.protocol('WM_DELETE_WINDOW', self.about_menu.withdraw)

    def create_menubar(self):
        menubar = Menu(self.root)

        # Tools pulldown menu
        tools_menu = Menu(menubar, tearoff=0)
        tools_menu.add_command(label="EB Project Editor",
                               command=self.open_ebprojedit)
        tools_menu.add_separator()
        tools_menu.add_command(label="Expand ROM to 32 MBit",
                               command=partial(gui_util.expand_rom, self.root))
        tools_menu.add_command(label="Expand ROM to 48 MBit",
                               command=partial(gui_util.expand_rom_ex,
                                               self.root))
        tools_menu.add_separator()
        tools_menu.add_command(label="Add Header to ROM",
                               command=partial(gui_util.add_header_to_rom,
                                               self.root))
        tools_menu.add_command(label="Remove Header from ROM",
                               command=partial(gui_util.strip_header_from_rom,
                                               self.root))
        menubar.add_cascade(label="Tools", menu=tools_menu)

        # Preferences pulldown menu
        pref_menu = Menu(menubar, tearoff=0)
        pref_menu.add_command(label="Configure Emulator",
                              command=self.set_emulator_exe)
        pref_menu.add_command(label="Configure Java",
                              command=self.set_java_exe)
        pref_menu.add_separator()
        pref_menu.add_command(label="Configure CCScript",
                              command=self.set_ccscript_offset)
        pref_menu.add_separator()
        pref_menu.add_command(label="Debug Mode", command=self.set_debug_mode)
        menubar.add_cascade(label="Settings", menu=pref_menu)

        # Help menu
        help_menu = Menu(menubar, tearoff=0)

        self.create_about_window()

        def show_about_window():
            self.about_menu.deiconify()
            self.about_menu.lift()

        def open_coilsnake_website():
            webbrowser.open(information.WEBSITE, 2)

        help_menu.add_command(label="About CoilSnake",
                              command=show_about_window)
        help_menu.add_command(label="CoilSnake Website",
                              command=open_coilsnake_website)

        menubar.add_cascade(label="Help", menu=help_menu)

        self.root.config(menu=menubar)

    def create_decompile_frame(self, notebook):
        self.decompile_fields = dict()

        decompile_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame(
            text="Decompile a ROM to create a new project.",
            frame=decompile_frame)

        profile_selector_init = self.add_profile_selector_to_frame(
            frame=decompile_frame,
            tab="decompile",
            fields=self.decompile_fields)

        input_rom_entry = self.add_rom_fields_to_frame(name="ROM",
                                                       frame=decompile_frame)
        self.decompile_fields["rom"] = input_rom_entry
        project_entry = self.add_project_fields_to_frame(
            name="Output Directory", frame=decompile_frame)
        self.decompile_fields["output_directory"] = project_entry

        profile_selector_init()

        def decompile_tmp():
            self.do_decompile(input_rom_entry, project_entry)

        decompile_button = Button(decompile_frame,
                                  text="Decompile",
                                  command=decompile_tmp)
        decompile_button.pack(fill=BOTH, expand=1)
        self.components.append(decompile_button)

        return decompile_frame

    def create_compile_frame(self, notebook):
        self.compile_fields = dict()

        compile_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame(
            text="Compile a project to create a new ROM.", frame=compile_frame)

        profile_selector_init = self.add_profile_selector_to_frame(
            frame=compile_frame, tab="compile", fields=self.compile_fields)

        base_rom_entry = self.add_rom_fields_to_frame(name="Base ROM",
                                                      frame=compile_frame)
        self.compile_fields["base_rom"] = base_rom_entry
        project_entry = self.add_project_fields_to_frame(name="Project",
                                                         frame=compile_frame)
        self.compile_fields["project"] = project_entry
        output_rom_entry = self.add_rom_fields_to_frame(name="Output ROM",
                                                        frame=compile_frame,
                                                        save=True)
        self.compile_fields["output_rom"] = output_rom_entry

        profile_selector_init()

        def compile_tmp():
            self.do_compile(project_entry, base_rom_entry, output_rom_entry)

        compile_button = Button(compile_frame,
                                text="Compile",
                                command=compile_tmp)
        compile_button.pack(fill=BOTH, expand=1)
        self.components.append(compile_button)

        return compile_frame

    def create_upgrade_frame(self, notebook):
        upgrade_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame(
            text=
            "Upgrade a project created using an older version of CoilSnake.",
            frame=upgrade_frame)

        rom_entry = self.add_rom_fields_to_frame(name="Clean ROM",
                                                 frame=upgrade_frame)
        project_entry = self.add_project_fields_to_frame(name="Project",
                                                         frame=upgrade_frame)

        def upgrade_tmp():
            self.preferences["default upgrade rom"] = rom_entry.get()
            self.preferences.save()
            self.do_upgrade(rom_entry, project_entry)

        self.upgrade_button = Button(upgrade_frame,
                                     text="Upgrade",
                                     command=upgrade_tmp)
        self.upgrade_button.pack(fill=BOTH, expand=1)
        self.components.append(self.upgrade_button)

        if self.preferences["default upgrade rom"]:
            set_entry_text(entry=rom_entry,
                           text=self.preferences["default upgrade rom"])

        return upgrade_frame

    def create_decompile_script_frame(self, notebook):
        decompile_script_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame(
            text="Decompile a ROM's script to an already existing project.",
            frame=decompile_script_frame)

        input_rom_entry = self.add_rom_fields_to_frame(
            name="ROM", frame=decompile_script_frame)
        project_entry = self.add_project_fields_to_frame(
            name="Project", frame=decompile_script_frame)

        def decompile_script_tmp():
            self.preferences[
                "default decompile script rom"] = input_rom_entry.get()
            self.preferences.save()
            self.do_decompile_script(input_rom_entry, project_entry)

        button = Button(decompile_script_frame,
                        text="Decompile Script",
                        command=decompile_script_tmp)
        button.pack(fill=BOTH, expand=1)
        self.components.append(button)

        if self.preferences["default decompile script rom"]:
            set_entry_text(
                entry=input_rom_entry,
                text=self.preferences["default decompile script rom"])

        return decompile_script_frame

    def create_apply_patch_frame(self, notebook):
        patcher_patch_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame("Apply an EBP or IPS patch to a ROM",
                                      patcher_patch_frame)

        clean_rom_entry = self.add_rom_fields_to_frame(
            name="Clean ROM", frame=patcher_patch_frame, padding_buttons=0)
        patched_rom_entry = self.add_rom_fields_to_frame(
            name="Patched ROM",
            frame=patcher_patch_frame,
            save=True,
            padding_buttons=0)
        patch_entry = self.add_patch_fields_to_frame(name="Patch",
                                                     frame=patcher_patch_frame)
        headered_var = self.add_headered_field_to_frame(
            name="ROM Header (IPS only)", frame=patcher_patch_frame)

        def patch_rom_tmp():
            self.preferences["default clean rom"] = clean_rom_entry.get()
            self.preferences["default patched rom"] = patched_rom_entry.get()
            self.preferences["default patch"] = patch_entry.get()
            self.preferences.save()
            self.do_patch_rom(clean_rom_entry, patched_rom_entry, patch_entry,
                              headered_var)

        button = Button(patcher_patch_frame,
                        text="Patch ROM",
                        command=patch_rom_tmp)
        button.pack(fill=BOTH, expand=1)
        self.components.append(button)

        if self.preferences["default clean rom"]:
            set_entry_text(entry=clean_rom_entry,
                           text=self.preferences["default clean rom"])
        if self.preferences["default patched rom"]:
            set_entry_text(entry=patched_rom_entry,
                           text=self.preferences["default patched rom"])
        if self.preferences["default patch"]:
            set_entry_text(entry=patch_entry,
                           text=self.preferences["default patch"])

        return patcher_patch_frame

    def create_create_patch_frame(self, notebook):
        patcher_create_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame("Create EBP patch from a ROM",
                                      patcher_create_frame)

        clean_rom_entry = self.add_rom_fields_to_frame(
            name="Clean ROM", frame=patcher_create_frame, padding_buttons=0)
        modified_rom_entry = self.add_rom_fields_to_frame(
            name="Modified ROM", frame=patcher_create_frame, padding_buttons=0)
        patch_entry = self.add_patch_fields_to_frame(
            name="Patch", frame=patcher_create_frame, save=True)

        def create_patch_tmp():
            self.preferences["default clean rom"] = clean_rom_entry.get()
            self.preferences["default modified rom"] = modified_rom_entry.get()
            self.preferences.save()
            self.do_create_patch(clean_rom_entry, modified_rom_entry,
                                 patch_entry)

        button = Button(patcher_create_frame,
                        text="Create Patch",
                        command=create_patch_tmp)
        button.pack(fill=BOTH, expand=1)
        self.components.append(button)

        if self.preferences["default clean rom"]:
            set_entry_text(entry=clean_rom_entry,
                           text=self.preferences["default clean rom"])
        if self.preferences["default modified rom"]:
            set_entry_text(entry=modified_rom_entry,
                           text=self.preferences["default patched rom"])

        return patcher_create_frame

    def add_title_label_to_frame(self, text, frame):
        Label(frame, text=text, justify=CENTER).pack(fill=BOTH, expand=1)

    def add_profile_selector_to_frame(self, frame, tab, fields):
        profile_frame = ttk.Frame(frame)

        Label(profile_frame, text="Profile:", width=13).pack(side=LEFT,
                                                             fill=BOTH,
                                                             expand=1)

        def tmp_select(profile_name):
            for field_id in fields:
                set_entry_text(entry=fields[field_id],
                               text=self.preferences.get_profile_value(
                                   tab, profile_name, field_id))
            self.preferences.set_default_profile(tab, profile_name)
            self.preferences.save()

        profile_var = StringVar(profile_frame)

        profile = OptionMenu(profile_frame,
                             profile_var,
                             "",
                             command=tmp_select)
        profile["width"] = 26
        profile.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(profile)

        def tmp_reload_options(selected_profile_name=None):
            profile["menu"].delete(0, END)
            for profile_name in sorted(self.preferences.get_profiles(tab)):
                if not selected_profile_name:
                    selected_profile_name = profile_name
                profile["menu"].add_command(label=profile_name,
                                            command=Tkinter._setit(
                                                profile_var, profile_name,
                                                tmp_select))
            profile_var.set(selected_profile_name)
            tmp_select(selected_profile_name)

        def tmp_new():
            profile_name = tkSimpleDialog.askstring(
                "New Profile Name", "Specify the name of the new profile.")
            if profile_name:
                profile_name = profile_name.strip()
                if self.preferences.has_profile(tab, profile_name):
                    tkMessageBox.showerror(
                        parent=self.root,
                        title="Error",
                        message="A profile with that name already exists.")
                    return

                self.preferences.add_profile(tab, profile_name)
                tmp_reload_options(profile_name)
                self.preferences.save()

        def tmp_save():
            profile_name = profile_var.get()
            for field_id in fields:
                self.preferences.set_profile_value(tab, profile_name, field_id,
                                                   fields[field_id].get())
            self.preferences.save()

        def tmp_delete():
            if self.preferences.count_profiles(tab) <= 1:
                tkMessageBox.showerror(
                    parent=self.root,
                    title="Error",
                    message="Cannot delete the only profile.")
            else:
                self.preferences.delete_profile(tab, profile_var.get())
                tmp_reload_options()
                self.preferences.save()

        button = Button(profile_frame, text="Save", width=5, command=tmp_save)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(profile_frame,
                        text="Delete",
                        width=5,
                        command=tmp_delete)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(profile_frame, text="New", width=5, command=tmp_new)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        profile_frame.pack(fill=BOTH, expand=1)

        def tmp_reload_options_and_select_default():
            tmp_reload_options(selected_profile_name=self.preferences.
                               get_default_profile(tab))

        return tmp_reload_options_and_select_default

    def add_rom_fields_to_frame(self,
                                name,
                                frame,
                                save=False,
                                padding_buttons=1):
        rom_frame = ttk.Frame(frame)

        Label(rom_frame, text="{}:".format(name), width=13,
              justify=RIGHT).pack(side=LEFT, fill=BOTH, expand=1)
        rom_entry = Entry(rom_frame, width=30)
        rom_entry.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(rom_entry)

        def browse_tmp():
            browse_for_rom(self.root, rom_entry, save)

        def run_tmp():
            self.run_rom(rom_entry)

        button = Button(rom_frame,
                        text="Browse...",
                        command=browse_tmp,
                        width=6)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(rom_frame, text="Run", command=run_tmp, width=5)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        for i in range(padding_buttons):
            button = Button(rom_frame,
                            text="",
                            width=5,
                            state=DISABLED,
                            takefocus=False)
            button.pack(side=LEFT, fill=BOTH, expand=1)
            button.lower()

        rom_frame.pack(fill=BOTH, expand=1)

        return rom_entry

    def add_project_fields_to_frame(self, name, frame):
        project_frame = ttk.Frame(frame)

        Label(project_frame, text="{}:".format(name), width=13,
              justify=RIGHT).pack(side=LEFT, fill=BOTH, expand=1)
        project_entry = Entry(project_frame, width=30)
        project_entry.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(project_entry)

        def browse_tmp():
            browse_for_project(self.root, project_entry, save=True)

        def open_tmp():
            open_folder(project_entry)

        def edit_tmp():
            self.open_ebprojedit(project_entry)

        button = Button(project_frame,
                        text="Browse...",
                        command=browse_tmp,
                        width=6)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(project_frame, text="Open", command=open_tmp, width=5)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(project_frame, text="Edit", command=edit_tmp, width=5)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        project_frame.pack(fill=BOTH, expand=1)

        return project_entry

    def add_patch_fields_to_frame(self, name, frame, save=False):
        patch_frame = ttk.Frame(frame)

        Label(patch_frame, text="{}:".format(name), width=13,
              justify=RIGHT).pack(side=LEFT, fill=BOTH, expand=1)
        patch_entry = Entry(patch_frame, width=30)
        patch_entry.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(patch_entry)

        def browse_tmp():
            browse_for_patch(self.root, patch_entry, save)

        button = Button(patch_frame,
                        text="Browse...",
                        command=browse_tmp,
                        width=6)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(patch_frame,
                        text="",
                        width=5,
                        state=DISABLED,
                        takefocus=False)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        button.lower()

        patch_frame.pack(fill=BOTH, expand=1)

        return patch_entry

    def add_headered_field_to_frame(self, name, frame):
        patch_frame = ttk.Frame(frame)

        headered_var = BooleanVar()
        headered_check = Checkbutton(patch_frame,
                                     text=name,
                                     variable=headered_var)
        headered_check.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(headered_check)
        patch_frame.pack(fill=BOTH, expand=1)

        return headered_var
Ejemplo n.º 3
0
class CoilSnakeGui(object):
    def __init__(self):
        self.preferences = CoilSnakePreferences()
        self.preferences.load()
        self.components = []
        self.progress_bar = None

    # Preferences functions

    def refresh_debug_logging(self):
        if self.preferences["debug mode"]:
            logging.root.setLevel(logging.DEBUG)
        else:
            logging.root.setLevel(logging.INFO)

    def set_debug_mode(self):
        confirm = tkMessageBox.askquestion(
            "Enable Debug Mode?",
            "Would you like to enable Debug mode? Debug mode will provide you with more detailed output while "
            + "CoilSnake is running.\n\n"
            + "This is generally only needed by advanced users.",
            icon="question"
        )

        self.preferences["debug mode"] = (confirm == "yes")
        self.preferences.save()

        self.refresh_debug_logging()

    def set_emulator_exe(self):
        tkMessageBox.showinfo(
            "Select the Emulator Executable",
            "Select an emulator executable for CoilSnake to use.\n\n"
            "Hint: It is probably named either zsnesw.exe, snes9x.exe, or higan-accuracy.exe"
        )

        emulator_exe = tkFileDialog.askopenfilename(
            parent=self.root,
            initialdir=os.path.expanduser("~"),
            title="Select an Emulator Executable")
        if emulator_exe:
            self.preferences["emulator"] = emulator_exe
            self.preferences.save()

    def set_ccscript_offset(self):
        ccscript_offset_str = tkSimpleDialog.askstring(
            title="Input CCScript Offset",
            prompt=("Specify the hexidecimal offset to which CCScript should compile text.\n"
                    + "(The default value is F10000)\n\n"
                    + "You should leave this setting alone unless if you really know what you are doing."),
            initialvalue="{:x}".format(self.preferences.get_ccscript_offset()).upper())

        if ccscript_offset_str:
            try:
                ccscript_offset = int(ccscript_offset_str, 16)
            except:
                tkMessageBox.showerror(parent=self.root,
                                       title="Error",
                                       message="{} is not a valid hexidecimal number.".format(ccscript_offset_str))
                return

            self.preferences.set_ccscript_offset(ccscript_offset)
            self.preferences.save()

    def get_java_exe(self):
        return self.preferences["java"] or find_system_java_exe()

    def set_java_exe(self):
        system_java_exe = find_system_java_exe()

        if system_java_exe:
            confirm = tkMessageBox.askquestion(
                "Configure Java",
                "CoilSnake has detected Java at the following location:\n\n"
                + system_java_exe + "\n\n"
                + "To use this installation of Java, select \"Yes\".\n\n"
                + "To override and instead use a different version of Java, select \"No\".",
                icon="question"
            )
            if confirm == "yes":
                self.preferences["java"] = None
                self.preferences.save()
                return

        tkMessageBox.showinfo(
            "Select the Java Executable",
            "Select a Java executable for CoilSnake to use.\n\n"
            "On Windows, it might be called \"javaw.exe\" or \"java.exe\"."
        )

        java_exe = tkFileDialog.askopenfilename(
            parent=self.root,
            title="Select the Java Executable",
            initialfile=(self.preferences["java"] or system_java_exe))
        if java_exe:
            self.preferences["java"] = java_exe
            self.preferences.save()

    def save_default_tab(self):
        tab_number = self.notebook.index(self.notebook.select())
        self.preferences.set_default_tab(tab_number)
        self.preferences.save()

    # GUI update functions
    def disable_all_components(self):
        for component in self.components:
            component["state"] = DISABLED

    def enable_all_components(self):
        for component in self.components:
            component["state"] = NORMAL

    # GUI popup functions

    def run_rom(self, entry):
        rom_filename = entry.get()
        if not self.preferences["emulator"]:
            tkMessageBox.showerror(parent=self.root,
                                   title="Error",
                                   message="""CoilSnake could not find an emulator.
Please configure your emulator in the Settings menu.""")
        elif rom_filename:
            Popen([self.preferences["emulator"], rom_filename])
    
    def open_ebprojedit(self, entry=None):
        if entry:
            project_path = entry.get()
        else:
            project_path = None

        java_exe = self.get_java_exe()
        if not java_exe:
            tkMessageBox.showerror(parent=self.root,
                                   title="Error",
                                   message="""CoilSnake could not find Java.
Please configure Java in the Settings menu.""")
            return

        command = [java_exe, "-jar", asset_path(["bin", "EbProjEdit.jar"])]
        if project_path:
            command.append(os.path.join(project_path, PROJECT_FILENAME))

        Popen(command)

    # Actions

    def do_decompile(self, rom_entry, project_entry):
        rom = rom_entry.get()
        project = project_entry.get()

        if rom and project:
            if os.path.isdir(project):
                confirm = tkMessageBox.askquestion("Are You Sure?",
                                                   "Are you sure you would like to permanently overwrite the "
                                                   + "contents of the selected output directory?",
                                                   icon='warning')
                if confirm != "yes":
                    return

            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()

            self.progress_bar.clear()
            thread = Thread(target=self._do_decompile_help, args=(rom, project))
            thread.start()

    def _do_decompile_help(self, rom, project):
        try:
            decompile_rom(rom_filename=rom, project_path=project, progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.clear()
        self.enable_all_components()

    def do_compile(self, project_entry, base_rom_entry, rom_entry):
        base_rom = base_rom_entry.get()
        rom = rom_entry.get()
        project = project_entry.get()

        if base_rom and rom and project:
            self.save_default_tab()

            base_rom_rom = Rom()
            base_rom_rom.from_file(base_rom)
            if base_rom_rom.type == "Earthbound" and len(base_rom_rom) == 0x300000:
                confirm = tkMessageBox.askquestion("Expand Your Base ROM?",
                                                   "You are attempting to compile using a base ROM which is "
                                                   "unexpanded. It is likely that this will not succeed, as CoilSnake "
                                                   "needs the extra space in an expanded ROM to store additional data."
                                                   "\n\n"
                                                   "Would you like to expand this base ROM before proceeding? This "
                                                   "will permanently overwrite your base ROM.",
                                                   icon='warning')
                if confirm == "yes":
                    base_rom_rom.expand(0x400000)
                    base_rom_rom.to_file(base_rom)
            del base_rom_rom

            # Update the GUI
            self.console.clear()
            self.disable_all_components()

            self.progress_bar.clear()

            log.info("Starting compilation...")

            thread = Thread(target=self._do_compile_help, args=(project, base_rom, rom))
            thread.start()

    def _do_compile_help(self, project, base_rom, rom):
        try:
            compile_project(project, base_rom, rom,
                            ccscript_offset=self.preferences.get_ccscript_offset(),
                            progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.clear()
        self.enable_all_components()

    def do_upgrade(self, rom_entry, project_entry):
        rom = rom_entry.get()
        project = project_entry.get()

        if rom and project:
            confirm = tkMessageBox.askquestion("Are You Sure?",
                                               "Are you sure you would like to upgrade this project? This operation "
                                               + "cannot be undone.\n\n"
                                               + "It is recommended that you backup your project before proceeding.",
                                               icon='warning')
            if confirm != "yes":
                return

            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()

            self.progress_bar.clear()
            thread = Thread(target=self._do_upgrade_help, args=(rom, project))
            thread.start()

    def _do_upgrade_help(self, rom, project):
        try:
            upgrade_project(project_path=project, base_rom_filename=rom, progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.clear()
        self.enable_all_components()

    def do_decompile_script(self, rom_entry, project_entry):
        rom = rom_entry.get()
        project = project_entry.get()

        if rom and project:
            confirm = tkMessageBox.askquestion("Are You Sure?",
                                               "Are you sure you would like to decompile the script into this "
                                               "project? This operation cannot be undone.\n\n"
                                               + "It is recommended that you backup your project before proceeding.",
                                               icon='warning')
            if confirm != "yes":
                return

            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()
            self.progress_bar.cycle_animation_start()

            thread = Thread(target=self._do_decompile_script_help, args=(rom, project))
            thread.start()

    def _do_decompile_script_help(self, rom, project):
        try:
            decompile_script(rom_filename=rom, project_path=project, progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.cycle_animation_stop()
        self.enable_all_components()

    def do_patch_rom(self, clean_rom_entry, patched_rom_entry, patch_entry, headered_var):
        clean_rom = clean_rom_entry.get()
        patched_rom = patched_rom_entry.get()
        patch = patch_entry.get()
        headered = headered_var.get()

        if clean_rom and patched_rom and patch:
            self.save_default_tab()

            # Update the GUI
            self.console.clear()
            self.disable_all_components()
            self.progress_bar.cycle_animation_start()

            thread = Thread(target=self._do_patch_rom_help, args=(clean_rom, patched_rom, patch, headered))
            thread.start()

    def _do_patch_rom_help(self, clean_rom, patched_rom, patch, headered):
        try:
            patch_rom(clean_rom, patched_rom, patch, headered, progress_bar=self.progress_bar)
        except Exception as inst:
            log.debug(format_exc())
            log.error(inst)

        self.progress_bar.cycle_animation_stop()
        self.enable_all_components()

    def main(self):
        self.create_gui()
        self.root.mainloop()

    def create_gui(self):
        self.root = Tk()
        self.root.wm_title("CoilSnake " + information.VERSION)

        self.icon = ImageTk.PhotoImage(file=asset_path(["images", "icon.png"]))
        self.root.tk.call('wm', 'iconphoto', self.root._w, self.icon)

        self.create_menubar()

        self.notebook = ttk.Notebook(self.root)

        decompile_frame = self.create_decompile_frame(self.notebook)
        self.notebook.add(decompile_frame, text="Decompile")

        compile_frame = self.create_compile_frame(self.notebook)
        self.notebook.add(compile_frame, text="Compile")

        upgrade_frame = self.create_upgrade_frame(self.notebook)
        self.notebook.add(upgrade_frame, text="Upgrade")

        decompile_script_frame = self.create_decompile_script_frame(self.notebook)
        self.notebook.add(decompile_script_frame, text="Decompile Script")

        patcher_patch_frame = self.create_apply_patch_frame(self.notebook)
        self.notebook.add(patcher_patch_frame, text="Apply Patch")

        #patcher_create_frame = self.create_create_patch_frame(self.notebook)
        #self.notebook.add(patcher_create_frame, text="Create Patch")

        self.notebook.pack(fill=BOTH, expand=1)
        self.notebook.select(self.preferences.get_default_tab())

        self.progress_bar = CoilSnakeGuiProgressBar(self.root, orient=HORIZONTAL, mode='determinate')
        self.progress_bar.pack(fill=BOTH, expand=1)

        console_frame = Frame(self.root)

        scrollbar = Scrollbar(console_frame)
        scrollbar.pack(side=RIGHT, fill=Y)

        self.console = ThreadSafeConsole(console_frame, width=80, height=8)
        self.console.pack(fill=X)
        scrollbar.config(command=self.console.yview)
        self.console.config(yscrollcommand=scrollbar.set)
        console_frame.pack(fill=X, expand=1)

        def selectall_text(event):
            event.widget.tag_add("sel", "1.0", "end")
        self.root.bind_class("Text", "<Control-a>", selectall_text)

        def selectall_entry(event):
            event.widget.selection_range(0, END)
        self.root.bind_class("Entry", "<Control-a>", selectall_entry)

        def tab_changed(event):
            # Do this so some random element in the tab isn't selected upon tab change
            self.notebook.focus()
        self.notebook.bind("<<NotebookTabChanged>>", tab_changed)

        self.console_stream = self.console

        setup_logging(quiet=False, verbose=False, stream=self.console_stream)
        self.refresh_debug_logging()

    def create_about_window(self):
        self.about_menu = Toplevel(self.root, takefocus=True)
        self.about_menu.tk.call('wm', 'iconphoto', self.about_menu._w, self.icon)

        photo = ImageTk.PhotoImage(file=asset_path(["images", "logo.png"]))
        about_label = Label(self.about_menu, image=photo)
        about_label.photo = photo
        about_label.pack(side=LEFT, expand=1)

        about_right_frame = ttk.Frame(self.about_menu)
        Label(about_right_frame,
              text=coilsnake_about(),
              font=("Courier", 11),
              anchor="w",
              justify="left",
              borderwidth=5,
              relief=GROOVE).pack(fill=BOTH, expand=1, side=TOP)

        about_right_frame.pack(side=LEFT, fill=BOTH, expand=1)

        self.about_menu.resizable(False, False)
        self.about_menu.title("About CoilSnake {}".format(information.VERSION))
        self.about_menu.withdraw()
        self.about_menu.transient(self.root)

        self.about_menu.protocol('WM_DELETE_WINDOW', self.about_menu.withdraw)

    def create_menubar(self):
        menubar = Menu(self.root)

        # Tools pulldown menu
        tools_menu = Menu(menubar, tearoff=0)
        tools_menu.add_command(label="EB Project Editor",
                               command=self.open_ebprojedit)
        tools_menu.add_separator()
        tools_menu.add_command(label="Expand ROM to 32 MBit",
                               command=partial(gui_util.expand_rom, self.root))
        tools_menu.add_command(label="Expand ROM to 48 MBit",
                               command=partial(gui_util.expand_rom_ex, self.root))
        tools_menu.add_separator()
        tools_menu.add_command(label="Add Header to ROM",
                               command=partial(gui_util.add_header_to_rom, self.root))
        tools_menu.add_command(label="Remove Header from ROM",
                               command=partial(gui_util.strip_header_from_rom, self.root))
        menubar.add_cascade(label="Tools", menu=tools_menu)

        # Preferences pulldown menu
        pref_menu = Menu(menubar, tearoff=0)
        pref_menu.add_command(label="Configure Emulator",
                              command=self.set_emulator_exe)
        pref_menu.add_command(label="Configure Java",
                              command=self.set_java_exe)
        pref_menu.add_separator()
        pref_menu.add_command(label="Configure CCScript",
                              command=self.set_ccscript_offset)
        pref_menu.add_separator()
        pref_menu.add_command(label="Debug Mode",
                              command=self.set_debug_mode)
        menubar.add_cascade(label="Settings", menu=pref_menu)

        # Help menu
        help_menu = Menu(menubar, tearoff=0)

        self.create_about_window()

        def show_about_window():
            self.about_menu.deiconify()
            self.about_menu.lift()

        def open_coilsnake_website():
            webbrowser.open(information.WEBSITE, 2)

        help_menu.add_command(label="About CoilSnake", command=show_about_window)
        help_menu.add_command(label="CoilSnake Website", command=open_coilsnake_website)

        menubar.add_cascade(label="Help", menu=help_menu)

        self.root.config(menu=menubar)

    def create_decompile_frame(self, notebook):
        self.decompile_fields = dict()

        decompile_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame(text="Decompile a ROM to create a new project.", frame=decompile_frame)

        profile_selector_init = self.add_profile_selector_to_frame(frame=decompile_frame,
                                                                   tab="decompile",
                                                                   fields=self.decompile_fields)

        input_rom_entry = self.add_rom_fields_to_frame(name="ROM", frame=decompile_frame)
        self.decompile_fields["rom"] = input_rom_entry
        project_entry = self.add_project_fields_to_frame(name="Output Directory", frame=decompile_frame)
        self.decompile_fields["output_directory"] = project_entry

        profile_selector_init()

        def decompile_tmp():
            self.do_decompile(input_rom_entry, project_entry)

        decompile_button = Button(decompile_frame, text="Decompile", command=decompile_tmp)
        decompile_button.pack(fill=BOTH, expand=1)
        self.components.append(decompile_button)

        return decompile_frame

    def create_compile_frame(self, notebook):
        self.compile_fields = dict()

        compile_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame(text="Compile a project to create a new ROM.", frame=compile_frame)

        profile_selector_init = self.add_profile_selector_to_frame(frame=compile_frame,
                                                                   tab="compile",
                                                                   fields=self.compile_fields)

        base_rom_entry = self.add_rom_fields_to_frame(name="Base ROM", frame=compile_frame)
        self.compile_fields["base_rom"] = base_rom_entry
        project_entry = self.add_project_fields_to_frame(name="Project", frame=compile_frame)
        self.compile_fields["project"] = project_entry
        output_rom_entry = self.add_rom_fields_to_frame(name="Output ROM", frame=compile_frame, save=True)
        self.compile_fields["output_rom"] = output_rom_entry

        profile_selector_init()

        def compile_tmp():
            self.do_compile(project_entry, base_rom_entry, output_rom_entry)

        compile_button = Button(compile_frame, text="Compile", command=compile_tmp)
        compile_button.pack(fill=BOTH, expand=1)
        self.components.append(compile_button)

        return compile_frame

    def create_upgrade_frame(self, notebook):
        upgrade_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame(text="Upgrade a project created using an older version of CoilSnake.",
                                      frame=upgrade_frame)

        rom_entry = self.add_rom_fields_to_frame(name="Clean ROM", frame=upgrade_frame)
        project_entry = self.add_project_fields_to_frame(name="Project", frame=upgrade_frame)

        def upgrade_tmp():
            self.preferences["default upgrade rom"] = rom_entry.get()
            self.preferences.save()
            self.do_upgrade(rom_entry, project_entry)

        self.upgrade_button = Button(upgrade_frame, text="Upgrade", command=upgrade_tmp)
        self.upgrade_button.pack(fill=BOTH, expand=1)
        self.components.append(self.upgrade_button)

        if self.preferences["default upgrade rom"]:
            set_entry_text(entry=rom_entry,
                           text=self.preferences["default upgrade rom"])

        return upgrade_frame

    def create_decompile_script_frame(self, notebook):
        decompile_script_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame(text="Decompile a ROM's script to an already existing project.",
                                      frame=decompile_script_frame)

        input_rom_entry = self.add_rom_fields_to_frame(name="ROM", frame=decompile_script_frame)
        project_entry = self.add_project_fields_to_frame(name="Project", frame=decompile_script_frame)

        def decompile_script_tmp():
            self.preferences["default decompile script rom"] = input_rom_entry.get()
            self.preferences.save()
            self.do_decompile_script(input_rom_entry, project_entry)

        button = Button(decompile_script_frame, text="Decompile Script", command=decompile_script_tmp)
        button.pack(fill=BOTH, expand=1)
        self.components.append(button)

        if self.preferences["default decompile script rom"]:
            set_entry_text(entry=input_rom_entry,
                           text=self.preferences["default decompile script rom"])

        return decompile_script_frame

    def create_apply_patch_frame(self, notebook):
        patcher_patch_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame("Apply an EBP or IPS patch to a ROM", patcher_patch_frame)

        clean_rom_entry = self.add_rom_fields_to_frame(name="Clean ROM", frame=patcher_patch_frame, padding_buttons=0)
        patched_rom_entry = self.add_rom_fields_to_frame(name="Patched ROM", frame=patcher_patch_frame, save=True,
                                                         padding_buttons=0)
        patch_entry = self.add_patch_fields_to_frame(name="Patch", frame=patcher_patch_frame)
        headered_var = self.add_headered_field_to_frame(name="ROM Header (IPS only)", frame=patcher_patch_frame)

        def patch_rom_tmp():
            self.preferences["default clean rom"] = clean_rom_entry.get()
            self.preferences["default patched rom"] = patched_rom_entry.get()
            self.preferences["default patch"] = patch_entry.get()
            self.preferences.save()
            self.do_patch_rom(clean_rom_entry, patched_rom_entry, patch_entry, headered_var)

        button = Button(patcher_patch_frame, text="Patch ROM", command=patch_rom_tmp)
        button.pack(fill=BOTH, expand=1)
        self.components.append(button)

        if self.preferences["default clean rom"]:
            set_entry_text(entry=clean_rom_entry,
                           text=self.preferences["default clean rom"])
        if self.preferences["default patched rom"]:
            set_entry_text(entry=patched_rom_entry,
                           text=self.preferences["default patched rom"])
        if self.preferences["default patch"]:
            set_entry_text(entry=patch_entry,
                           text=self.preferences["default patch"])

        return patcher_patch_frame

    def create_create_patch_frame(self, notebook):
        patcher_create_frame = ttk.Frame(notebook)
        self.add_title_label_to_frame("Create EBP patch from a ROM", patcher_create_frame)

        clean_rom_entry = self.add_rom_fields_to_frame(name="Clean ROM", frame=patcher_create_frame, padding_buttons=0)
        modified_rom_entry = self.add_rom_fields_to_frame(name="Modified ROM", frame=patcher_create_frame,
                                                          padding_buttons=0)
        patch_entry = self.add_patch_fields_to_frame(name="Patch", frame=patcher_create_frame, save=True)

        def create_patch_tmp():
            self.preferences["default clean rom"] = clean_rom_entry.get()
            self.preferences["default modified rom"] = modified_rom_entry.get()
            self.preferences.save()
            self.do_create_patch(clean_rom_entry, modified_rom_entry, patch_entry)

        button = Button(patcher_create_frame, text="Create Patch", command=create_patch_tmp)
        button.pack(fill=BOTH, expand=1)
        self.components.append(button)

        if self.preferences["default clean rom"]:
            set_entry_text(entry=clean_rom_entry,
                           text=self.preferences["default clean rom"])
        if self.preferences["default modified rom"]:
            set_entry_text(entry=modified_rom_entry,
                           text=self.preferences["default patched rom"])

        return patcher_create_frame

    def add_title_label_to_frame(self, text, frame):
        Label(frame, text=text, justify=CENTER).pack(fill=BOTH, expand=1)

    def add_profile_selector_to_frame(self, frame, tab, fields):
        profile_frame = ttk.Frame(frame)

        Label(profile_frame, text="Profile:", width=13).pack(side=LEFT, fill=BOTH, expand=1)

        def tmp_select(profile_name):
            for field_id in fields:
                set_entry_text(entry=fields[field_id],
                               text=self.preferences.get_profile_value(tab, profile_name, field_id))
            self.preferences.set_default_profile(tab, profile_name)
            self.preferences.save()

        profile_var = StringVar(profile_frame)

        profile = OptionMenu(profile_frame, profile_var, "", command=tmp_select)
        profile["width"] = 26
        profile.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(profile)

        def tmp_reload_options(selected_profile_name=None):
            profile["menu"].delete(0, END)
            for profile_name in sorted(self.preferences.get_profiles(tab)):
                if not selected_profile_name:
                    selected_profile_name = profile_name
                profile["menu"].add_command(label=profile_name,
                                            command=Tkinter._setit(profile_var, profile_name, tmp_select))
            profile_var.set(selected_profile_name)
            tmp_select(selected_profile_name)

        def tmp_new():
            profile_name = tkSimpleDialog.askstring("New Profile Name", "Specify the name of the new profile.")
            if profile_name:
                profile_name = profile_name.strip()
                if self.preferences.has_profile(tab, profile_name):
                    tkMessageBox.showerror(parent=self.root,
                                           title="Error",
                                           message="A profile with that name already exists.")
                    return

                self.preferences.add_profile(tab, profile_name)
                tmp_reload_options(profile_name)
                self.preferences.save()

        def tmp_save():
            profile_name = profile_var.get()
            for field_id in fields:
                self.preferences.set_profile_value(tab, profile_name, field_id, fields[field_id].get())
            self.preferences.save()

        def tmp_delete():
            if self.preferences.count_profiles(tab) <= 1:
                tkMessageBox.showerror(parent=self.root,
                                       title="Error",
                                       message="Cannot delete the only profile.")
            else:
                self.preferences.delete_profile(tab, profile_var.get())
                tmp_reload_options()
                self.preferences.save()

        button = Button(profile_frame, text="Save", width=5, command=tmp_save)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(profile_frame, text="Delete", width=5, command=tmp_delete)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(profile_frame, text="New", width=5, command=tmp_new)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        profile_frame.pack(fill=BOTH, expand=1)

        def tmp_reload_options_and_select_default():
            tmp_reload_options(selected_profile_name=self.preferences.get_default_profile(tab))

        return tmp_reload_options_and_select_default

    def add_rom_fields_to_frame(self, name, frame, save=False, padding_buttons=1):
        rom_frame = ttk.Frame(frame)

        Label(rom_frame, text="{}:".format(name), width=13, justify=RIGHT).pack(side=LEFT, fill=BOTH, expand=1)
        rom_entry = Entry(rom_frame, width=30)
        rom_entry.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(rom_entry)

        def browse_tmp():
            browse_for_rom(self.root, rom_entry, save)

        def run_tmp():
            self.run_rom(rom_entry)

        button = Button(rom_frame, text="Browse...", command=browse_tmp, width=6)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(rom_frame, text="Run", command=run_tmp, width=5)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        for i in range(padding_buttons):
            button = Button(rom_frame, text="", width=5, state=DISABLED, takefocus=False)
            button.pack(side=LEFT, fill=BOTH, expand=1)
            button.lower()

        rom_frame.pack(fill=BOTH, expand=1)

        return rom_entry

    def add_project_fields_to_frame(self, name, frame):
        project_frame = ttk.Frame(frame)

        Label(project_frame, text="{}:".format(name), width=13, justify=RIGHT).pack(side=LEFT, fill=BOTH, expand=1)
        project_entry = Entry(project_frame, width=30)
        project_entry.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(project_entry)

        def browse_tmp():
            browse_for_project(self.root, project_entry, save=True)

        def open_tmp():
            open_folder(project_entry)

        def edit_tmp():
            self.open_ebprojedit(project_entry)

        button = Button(project_frame, text="Browse...", command=browse_tmp, width=6)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(project_frame, text="Open", command=open_tmp, width=5)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(project_frame, text="Edit", command=edit_tmp, width=5)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        project_frame.pack(fill=BOTH, expand=1)

        return project_entry

    def add_patch_fields_to_frame(self, name, frame, save=False):
        patch_frame = ttk.Frame(frame)

        Label(
            patch_frame, text="{}:".format(name), width=13, justify=RIGHT
        ).pack(side=LEFT, fill=BOTH, expand=1)
        patch_entry = Entry(patch_frame, width=30)
        patch_entry.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(patch_entry)

        def browse_tmp():
            browse_for_patch(self.root, patch_entry, save)

        button = Button(patch_frame, text="Browse...", command=browse_tmp, width=6)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        self.components.append(button)

        button = Button(patch_frame, text="", width=5, state=DISABLED, takefocus=False)
        button.pack(side=LEFT, fill=BOTH, expand=1)
        button.lower()

        patch_frame.pack(fill=BOTH, expand=1)

        return patch_entry

    def add_headered_field_to_frame(self, name, frame):
        patch_frame = ttk.Frame(frame)

        headered_var = BooleanVar()
        headered_check = Checkbutton(patch_frame, text=name, variable=headered_var)
        headered_check.pack(
            side=LEFT, fill=BOTH, expand=1
        )
        self.components.append(headered_check)
        patch_frame.pack(fill=BOTH, expand=1)

        return headered_var