示例#1
0
class MainWidget(QWidget):

    after_process_input_gci = pyqtSignal()

    def __init__(self):
        super().__init__()

        self.init_layout()
        self.output_filename_defaults = dict(
            gci='',
            replay_array='',
        )

        self.worker_thread = None
        self.after_process_input_gci.connect(self._after_process_input_gci)

        # Accept drag and drop; see event methods for details
        self.setAcceptDrops(True)

    def init_layout(self):

        self.error_label = QLabel("")
        self.error_label.setStyleSheet("QLabel { color: red; }")

        self.input_gci_label = QLabel("Drag and drop a .gci to get started")
        # Given that we have word wrap True, setting a fixed height prevents
        # the GUI from resizing (and prevents an associated window size warning
        # in the console window) when we switch between long and short
        # filepaths.
        # To help with reading long filepaths, we'll also set a tooltip each
        # time we set the filepath.
        self.input_gci_label.setWordWrap(True)
        self.input_gci_label.setMinimumWidth(150)
        self.input_gci_label.setFixedHeight(35)

        self.input_gci_checksum_label = QLabel("")

        self.gci_fields_widget = GCIFieldsWidget(self)

        self.output_button = QPushButton("Output", self)
        self.output_button.clicked.connect(self.on_output_button_click)

        self.output_type_combo_box = QComboBox(self)
        # The choices here only apply to replays. If we handle other types of
        # .gci's in the future, we might change this combo box to add its
        # choices dynamically.
        self.output_type_combo_box.addItems([".gci", "Replay array .bin"])
        self.output_type_combo_box.activated[str].connect(
            self.on_output_type_change)

        self.output_folder_select_dialog = QFileDialog(
            self, "Choose folder to save to", self.output_folder)
        self.output_folder_label = QLabel(self.output_folder)
        self.output_folder_label.setToolTip(self.output_folder)
        self.output_folder_label.setWordWrap(True)
        self.output_folder_label.setMinimumWidth(150)
        self.output_folder_label.setFixedHeight(50)
        self.output_folder_select_button = QPushButton("Choose", self)
        self.output_folder_select_button.clicked.connect(
            self.show_output_folder_select_dialog)

        self.output_filename_line_edit = QLineEdit()
        self.output_filename_line_edit.editingFinished.connect(
            self.on_output_filename_change)

        output_type_hbox = QHBoxLayout()
        output_type_hbox.addWidget(self.output_button)
        output_type_hbox.addWidget(self.output_type_combo_box)
        output_folder_hbox = QHBoxLayout()
        output_folder_hbox.addWidget(QLabel("to folder:"))
        output_folder_hbox.addWidget(self.output_folder_label)
        output_folder_hbox.addWidget(self.output_folder_select_button)
        output_filename_hbox = QHBoxLayout()
        output_filename_hbox.addWidget(QLabel("with filename:"))
        output_filename_hbox.addWidget(self.output_filename_line_edit)
        self.output_vbox = QVBoxLayout()
        self.output_vbox.addLayout(output_type_hbox)
        self.output_vbox.addLayout(output_folder_hbox)
        self.output_vbox.addLayout(output_filename_hbox)
        self.output_vbox_widget = QWidget()
        self.output_vbox_widget.setLayout(self.output_vbox)
        self.output_vbox_widget.hide()

        vbox = QVBoxLayout()
        vbox.addWidget(self.error_label)
        vbox.addWidget(self.input_gci_label)
        vbox.addWidget(self.input_gci_checksum_label)
        vbox.addWidget(self.gci_fields_widget)
        vbox.addWidget(self.output_vbox_widget)
        self.setLayout(vbox)

        self.setWindowTitle("F-Zero GX GCI info")
        self.show()

    def clear_error_display(self):
        self.error_label.setText("")

    def display_error(self, message):
        self.error_label.setText(message)

    def show_output_folder_select_dialog(self):
        folder = self.output_folder_select_dialog.getExistingDirectory(
            self, 'Choose folder')
        if folder:
            self.output_folder_label.setText(folder)
            self.output_folder_label.setToolTip(folder)
            config.set('output_folder', folder)

    def run_worker_job(self, job_func):
        if not self.worker_thread:
            # Create a thread.
            # Must store a reference to the thread in a non-local variable,
            # so the thread doesn't get garbage collected after returning
            # from this method
            # https://stackoverflow.com/a/15702922/
            self.worker_thread = WorkerThread()

        # This will emit 'started' and start running the thread
        self.worker_thread.run_job(job_func)

    def closeEvent(self, e):
        """Event handler: GUI window is closed"""
        config.save()

    def dragEnterEvent(self, e):
        """Event handler: Mouse enters the GUI window while dragging
        something"""
        e.accept()

    def dropEvent(self, e):
        """
        Event handler: Mouse is released on the GUI window after dragging.

        Check that we've dragged a single .gci file. If so, process it.
        """
        self.clear_error_display()

        mime_data = e.mimeData()
        dropped_uris = [uri.toString() for uri in mime_data.urls()]
        if len(dropped_uris) > 1:
            self.display_error("Please drag and drop a single file.")
            return

        uri = urllib.parse.urlparse(dropped_uris[0])
        uri_scheme = uri.scheme
        if uri_scheme != 'file':
            self.display_error(
                "Please drag and drop a file from your computer.")
            return

        # url2pathname() will:
        # - Replace URL-like escape codes such as %E3%83%BB
        #   with unescaped Unicode characters.
        # - Strip the beginning slash if it's a Windows filepath.
        #   (/D:/Games/... -> D:\Games\...)
        input_gci_filepath = Path(urllib.request.url2pathname(uri.path))
        if input_gci_filepath.suffix != '.gci':
            self.display_error("The dropped file doesn't seem to be a .gci.")
            return

        self.input_gci_filepath = input_gci_filepath
        self.process_input_gci()

    def process_input_gci(self):
        self.input_gci_label.setText("Working...")
        self.input_gci = gci(self.input_gci_filepath)
        input_data = self.input_gci.get_replay_data()

        Field.reset_field_structures()

        success = self.gci_fields_widget.read_fields_from_spec()
        if not success:
            self.input_gci_label.setText("Drag and drop a .gci to get started")
            return

        # Process the replay-specific GCI contents.
        # This call can take a while, especially with custom machines.
        self.run_worker_job(
            functools.partial(self.gci_fields_widget.read_values_from_gci,
                              input_data))

    @pyqtSlot()
    def _after_process_input_gci(self):
        self.gci_fields_widget.add_field_widgets()

        self.input_gci_label.setText(f"Input: {self.input_gci_filepath}")
        self.input_gci_label.setToolTip(f"Input: {self.input_gci_filepath}")
        self.input_gci_checksum_label.setText(
            f"\nChecksum: 0x{self.input_gci.get_checksum().hex()}")

        self.output_filename_defaults = dict(
            gci='output.gci',
            replay_array=f'{self.input_gci_filepath.stem}__replay_array.bin',
        )
        self.output_vbox_widget.show()
        self.on_output_type_change()

    @property
    def output_type(self):
        combo_box_text = self.output_type_combo_box.currentText()
        if combo_box_text == ".gci":
            return 'gci'
        elif combo_box_text == "Replay array .bin":
            return 'replay_array'

    @property
    def output_folder(self):
        return config.get('output_folder', '')

    @property
    def output_filename(self):
        fn = config.get(f'output_filename_{self.output_type}')
        if not fn:
            fn = self.output_filename_defaults[self.output_type]
        return fn

    @property
    def output_filepath(self):
        return Path(self.output_folder, self.output_filename)

    def on_output_type_change(self):
        output_filename = config.get(f'output_filename_{self.output_type}', '')
        self.output_filename_line_edit.setText(output_filename)
        self.output_filename_line_edit.setPlaceholderText(
            self.output_filename_defaults[self.output_type])

    def on_output_filename_change(self):
        config.set(f'output_filename_{self.output_type}',
                   self.output_filename_line_edit.text())

    def on_output_button_click(self):
        self.clear_error_display()

        if not self.output_folder:
            self.display_error("Please select an output folder.")
            return

        if self.output_type == 'gci':
            self.output_gci()
        elif self.output_type == 'replay_array':
            self.output_replay_array()

    def output_gci(self):
        # Re-encode the replay data
        new_replay_data = self.gci_fields_widget.output_values_to_gci()
        # Zero-pad to make the entire GCI a multiple of 0x2000 bytes + 0x40.
        # This replay data does not include the first 0x20A0 bytes of the GCI.
        gci_bytes_without_padding = len(new_replay_data) + 0x20A0
        gci_target_blocks = math.ceil(
            (gci_bytes_without_padding - 0x40) / 0x2000)
        gci_target_bytes = (gci_target_blocks * 0x2000) + 0x40
        zero_padding = [0] * (gci_target_bytes - gci_bytes_without_padding)
        new_replay_data.extend(zero_padding)

        # Concatenate new replay data to the rest of the original GCI
        self.input_gci.set_replay_data(new_replay_data)

        # Recompute the checksum of the whole GCI
        self.input_gci.recompute_checksum()

        # Write the new GCI to a file
        self.output_binary_file(self.input_gci.raw_bytes)

        # Success dialog
        button_box = QDialogButtonBox(QDialogButtonBox.Ok)
        vbox = QVBoxLayout()
        vbox.addWidget(
            QLabel(f"GCI written to: {self.output_filepath}"
                   f"\nChecksum: 0x{self.input_gci.get_checksum().hex()}"))
        vbox.addWidget(button_box)
        confirmed_dialog = QDialog(self)
        confirmed_dialog.setWindowTitle("Write success")
        confirmed_dialog.setLayout(vbox)
        # Make the OK button 'accept' the dialog
        button_box.accepted.connect(confirmed_dialog.accept)
        # Show the dialog
        confirmed_dialog.exec()

    def output_replay_array(self):
        self.display_error("Replay-array output mode isn't implemented yet.")

    def output_binary_file(self, bytes_to_write):
        with open(self.output_filepath, "wb") as output_file:
            output_file.write(bytes_to_write)