Example #1
0
    def _init_widgets(self):
        vertical_layout = QVBoxLayout()

        # dialog text
        self.label = QLabel(self)
        self.label.setWordWrap(True)
        self.label.setText(
            "<html><head/><body><p> Magic Sync is a one-time sync that attempts to sync <span style=\" "
            "font-weight:600;\">non-conflicting</span> data from all users on all functions, essentially a global "
            "knowledge merge. Would you like to preform this action? You may optionally select a user you would like "
            "prioritized for non-conflicting sync first.</p><p>Priority User:</p></body></html>",
        )

        # user selection
        items = self._get_users()
        self.comboBox = QComboBox(self)
        self.comboBox.addItems(items)

        # confirm button
        self.buttonBox = QDialogButtonBox(self)
        self.buttonBox.setStandardButtons(QDialogButtonBox.No
                                          | QDialogButtonBox.Yes)
        self.buttonBox.accepted.connect(self._on_yes_clicked)
        self.buttonBox.rejected.connect(self._on_no_clicked)

        vertical_layout.addWidget(self.label, 0, Qt.AlignBottom)
        vertical_layout.addWidget(self.comboBox, 0, Qt.AlignBottom)
        vertical_layout.addWidget(self.buttonBox, 0, Qt.AlignBottom)

        self._main_layout.addLayout(vertical_layout, 0, 0, 1, 1)
Example #2
0
class MagicSyncDialog(QDialog):
    def __init__(self, controller, parent=None):
        super().__init__(parent)
        self.controller = controller

        self.setWindowTitle("Magic Sync")
        self._main_layout = QGridLayout()
        self._init_widgets()
        self.setLayout(self._main_layout)

        self.should_sync = False
        self.preferred_user = None
        self.show()

    def _init_widgets(self):
        vertical_layout = QVBoxLayout()

        # dialog text
        self.label = QLabel(self)
        self.label.setWordWrap(True)
        self.label.setText(
            "<html><head/><body><p> Magic Sync is a one-time sync that attempts to sync <span style=\" "
            "font-weight:600;\">non-conflicting</span> data from all users on all functions, essentially a global "
            "knowledge merge. Would you like to preform this action? You may optionally select a user you would like "
            "prioritized for non-conflicting sync first.</p><p>Priority User:</p></body></html>",
        )

        # user selection
        items = self._get_users()
        self.comboBox = QComboBox(self)
        self.comboBox.addItems(items)

        # confirm button
        self.buttonBox = QDialogButtonBox(self)
        self.buttonBox.setStandardButtons(QDialogButtonBox.No
                                          | QDialogButtonBox.Yes)
        self.buttonBox.accepted.connect(self._on_yes_clicked)
        self.buttonBox.rejected.connect(self._on_no_clicked)

        vertical_layout.addWidget(self.label, 0, Qt.AlignBottom)
        vertical_layout.addWidget(self.comboBox, 0, Qt.AlignBottom)
        vertical_layout.addWidget(self.buttonBox, 0, Qt.AlignBottom)

        self._main_layout.addLayout(vertical_layout, 0, 0, 1, 1)

    def _get_users(self):
        return ["None"] + list(self.controller.usernames(priority=1))

    def _on_yes_clicked(self):
        self.should_sync = True
        combo_text = self.comboBox.currentText()
        self.preferred_user = combo_text if combo_text != "None" else None
        self.close()

    def _on_no_clicked(self):
        self.should_sync = False
        self.close()
Example #3
0
    def _init_widgets(self):
        # status bar
        self._status_label = QLabel(self)
        self._status_label.setText(self.controller.status_string())
        self._status_bar = QContextStatusBar(self.controller, self)
        self._status_bar.addPermanentWidget(self._status_label)

        # control box
        control_layout = QVBoxLayout()

        # tabs for panel_tabs
        self.tabView = QTabWidget()

        # add panel_tabs to tabs
        self._ctx_table = QCTXTable(self.controller)
        self._func_table = QFunctionTable(self.controller)
        self._global_table = QGlobalsTable(self.controller)
        self._activity_table = QActivityTable(self.controller)
        self._utilities_panel = QUtilPanel(self.controller)

        self.tabView.addTab(self._ctx_table, "Context")
        self.tabView.addTab(self._func_table, "Functions")
        self.tabView.addTab(self._global_table, "Globals")
        self.tabView.addTab(self._activity_table, "Activity")
        self.tabView.addTab(self._utilities_panel, "Utilities")

        self.tables.update({
            "functions": self._func_table,
            "globals": self._global_table,
            "activity": self._activity_table
        })

        main_layout = QVBoxLayout()
        main_layout.addWidget(self.tabView)
        main_layout.addWidget(self._status_bar)
        main_layout.setSpacing(0)
        main_layout.setContentsMargins(0, 0, 0, 0)

        self.setLayout(main_layout)
Example #4
0
class ControlPanel(QWidget):
    update_ready = Signal()
    ctx_change = Signal()

    def __init__(self, controller, parent=None):
        super(ControlPanel, self).__init__(parent)
        self.controller = controller

        self.tables = {}
        self._init_widgets()

        # register controller callback
        self.update_ready.connect(self.reload)
        self.controller.ui_callback = self.update_callback

        self.ctx_change.connect(self._reload_ctx)
        self.controller.ctx_change_callback = self.ctx_callback

    def update_callback(self):
        """
        This function will be called in another thread, so the work
        done here is guaranteed to be thread safe.

        @return:
        """
        self._update_table_data()
        self.update_ready.emit()

    def ctx_callback(self):
        if isinstance(self.controller.last_ctx, binsync.data.Function):
            self._ctx_table.update_table(new_ctx=self.controller.last_ctx.addr)

        self.ctx_change.emit()

    def reload(self):
        # check if connected
        if self.controller and self.controller.check_client():
            self._reload_tables()

        # update status
        status = self.controller.status_string(
        ) if self.controller else "Disconnected"
        self._status_label.setText(status)

    def closeEvent(self, event):
        if self.controller is not None:
            self.controller.client_init_callback = None

    def _init_widgets(self):
        # status bar
        self._status_label = QLabel(self)
        self._status_label.setText(self.controller.status_string())
        self._status_bar = QContextStatusBar(self.controller, self)
        self._status_bar.addPermanentWidget(self._status_label)

        # control box
        control_layout = QVBoxLayout()

        # tabs for panel_tabs
        self.tabView = QTabWidget()

        # add panel_tabs to tabs
        self._ctx_table = QCTXTable(self.controller)
        self._func_table = QFunctionTable(self.controller)
        self._global_table = QGlobalsTable(self.controller)
        self._activity_table = QActivityTable(self.controller)
        self._utilities_panel = QUtilPanel(self.controller)

        self.tabView.addTab(self._ctx_table, "Context")
        self.tabView.addTab(self._func_table, "Functions")
        self.tabView.addTab(self._global_table, "Globals")
        self.tabView.addTab(self._activity_table, "Activity")
        self.tabView.addTab(self._utilities_panel, "Utilities")

        self.tables.update({
            "functions": self._func_table,
            "globals": self._global_table,
            "activity": self._activity_table
        })

        main_layout = QVBoxLayout()
        main_layout.addWidget(self.tabView)
        main_layout.addWidget(self._status_bar)
        main_layout.setSpacing(0)
        main_layout.setContentsMargins(0, 0, 0, 0)

        self.setLayout(main_layout)

    def _reload_ctx(self):
        ctx_name = self.controller.last_ctx.name or ""
        ctx_name = ctx_name[:12] + "..." if len(ctx_name) > 12 else ctx_name
        self._status_bar.showMessage(
            f"{ctx_name}@{hex(self.controller.last_ctx.addr)}")
        self._ctx_table.reload()

    def _reload_tables(self):
        for _, table in self.tables.items():
            table.reload()
        self._ctx_table.reload()

    def _update_table_data(self):
        for _, table in self.tables.items():
            table.update_table()

        self._ctx_table.update_table()
Example #5
0
    def _init_widgets(self):
        upper_layout = QGridLayout()

        # user label
        user_label = QLabel(self)
        user_label.setText("User name")
        user_label.setToolTip(
            "The name your user will be saved as on remote. Can be anything other that 'root'. This name does not "
            "need to be the same as your Git username.")

        self._user_edit = QLineEdit(self)

        row = 0
        upper_layout.addWidget(user_label, row, 0)
        upper_layout.addWidget(self._user_edit, row, 1)
        row += 1

        # binsync label
        binsync_label = QLabel(self)
        binsync_label.setText("Git repo")
        binsync_label.setToolTip(
            "The path to a locally cloned Git repo. This can be ignored if you have not cloned down a remote "
            "repo yet, but you have the URL. The local repo can also be an empty folder that will be turned into "
            "a BinSync database by selecting 'init_remote'.")

        # repo path
        self._repo_edit = QLineEdit(self)
        self._repo_edit.textChanged.connect(self._on_repo_textchanged)
        #self._repo_edit.setFixedWidth(150)

        # repo path selection button
        repo_button = QPushButton(self)
        repo_button.setText("...")
        repo_button.clicked.connect(self._on_repo_clicked)
        repo_button.setFixedWidth(40)

        upper_layout.addWidget(binsync_label, row, 0)
        upper_layout.addWidget(self._repo_edit, row, 1)
        upper_layout.addWidget(repo_button, row, 2)
        row += 1

        # clone from a remote URL
        self.remote_label = QLabel(self)
        self.remote_label.setText("Remote URL")
        self.remote_label.setToolTip(
            "The URL to a remove Git repo. This is not required if you have already selected a locally cloned "
            "Git repo that has a remote. This repo will be cloned to the same location as your binary if a local "
            "repo does not exist yet.")

        self._remote_edit = QLineEdit(self)

        upper_layout.addWidget(self.remote_label, row, 0)
        upper_layout.addWidget(self._remote_edit, row, 1)
        row += 1

        # initialize repo checkbox
        self._initrepo_checkbox = QCheckBox(self)
        self._initrepo_checkbox.setText("Init Remote")
        self._initrepo_checkbox.setToolTip(
            "Ether inits the local folder, making it into a Git repo, or updates the remote references of the current "
            "Git repo to have the correct BinSync layout which includes the root branch and the new user. Should only "
            "ever be used when first creating a BinSync repo for all users.")
        self._initrepo_checkbox.setChecked(False)
        #self._initrepo_checkbox.setEnabled(False)

        upper_layout.addWidget(self._initrepo_checkbox, row, 1)
        row += 1

        # buttons
        self._ok_button = QPushButton(self)
        self._ok_button.setText("OK")
        self._ok_button.setDefault(True)
        self._ok_button.clicked.connect(self._on_ok_clicked)

        cancel_button = QPushButton(self)
        cancel_button.setText("Cancel")
        cancel_button.clicked.connect(self._on_cancel_clicked)

        buttons_layout = QHBoxLayout()
        buttons_layout.addWidget(self._ok_button)
        buttons_layout.addWidget(cancel_button)

        # main layout
        self._main_layout.addLayout(upper_layout)
        self._main_layout.addLayout(buttons_layout)

        # change the text if config exists
        if self.should_load_config:
            self.load_saved_config()
Example #6
0
class SyncConfig(QDialog):
    """
    The dialog that allows a user to config a BinSync client for:
    - initing a local repo
    - cloning a remote
    - using a locally pulled remote repo
    """
    def __init__(self,
                 controller,
                 open_magic_sync=True,
                 load_config=True,
                 parent=None):
        super().__init__(parent)
        self.controller = controller
        self.open_magic_sync = open_magic_sync
        self.should_load_config = load_config
        self.setWindowTitle("Configure BinSync")

        self._main_layout = QVBoxLayout()
        self._user_edit = None  # type:QLineEdit
        self._repo_edit = None  # type:QLineEdit
        self._remote_edit = None  # type:QLineEdit
        self._initrepo_checkbox = None  # type:QCheckBox

        self._init_widgets()
        self.setLayout(self._main_layout)
        self.show()

    def _init_widgets(self):
        upper_layout = QGridLayout()

        # user label
        user_label = QLabel(self)
        user_label.setText("User name")
        user_label.setToolTip(
            "The name your user will be saved as on remote. Can be anything other that 'root'. This name does not "
            "need to be the same as your Git username.")

        self._user_edit = QLineEdit(self)

        row = 0
        upper_layout.addWidget(user_label, row, 0)
        upper_layout.addWidget(self._user_edit, row, 1)
        row += 1

        # binsync label
        binsync_label = QLabel(self)
        binsync_label.setText("Git repo")
        binsync_label.setToolTip(
            "The path to a locally cloned Git repo. This can be ignored if you have not cloned down a remote "
            "repo yet, but you have the URL. The local repo can also be an empty folder that will be turned into "
            "a BinSync database by selecting 'init_remote'.")

        # repo path
        self._repo_edit = QLineEdit(self)
        self._repo_edit.textChanged.connect(self._on_repo_textchanged)
        #self._repo_edit.setFixedWidth(150)

        # repo path selection button
        repo_button = QPushButton(self)
        repo_button.setText("...")
        repo_button.clicked.connect(self._on_repo_clicked)
        repo_button.setFixedWidth(40)

        upper_layout.addWidget(binsync_label, row, 0)
        upper_layout.addWidget(self._repo_edit, row, 1)
        upper_layout.addWidget(repo_button, row, 2)
        row += 1

        # clone from a remote URL
        self.remote_label = QLabel(self)
        self.remote_label.setText("Remote URL")
        self.remote_label.setToolTip(
            "The URL to a remove Git repo. This is not required if you have already selected a locally cloned "
            "Git repo that has a remote. This repo will be cloned to the same location as your binary if a local "
            "repo does not exist yet.")

        self._remote_edit = QLineEdit(self)

        upper_layout.addWidget(self.remote_label, row, 0)
        upper_layout.addWidget(self._remote_edit, row, 1)
        row += 1

        # initialize repo checkbox
        self._initrepo_checkbox = QCheckBox(self)
        self._initrepo_checkbox.setText("Init Remote")
        self._initrepo_checkbox.setToolTip(
            "Ether inits the local folder, making it into a Git repo, or updates the remote references of the current "
            "Git repo to have the correct BinSync layout which includes the root branch and the new user. Should only "
            "ever be used when first creating a BinSync repo for all users.")
        self._initrepo_checkbox.setChecked(False)
        #self._initrepo_checkbox.setEnabled(False)

        upper_layout.addWidget(self._initrepo_checkbox, row, 1)
        row += 1

        # buttons
        self._ok_button = QPushButton(self)
        self._ok_button.setText("OK")
        self._ok_button.setDefault(True)
        self._ok_button.clicked.connect(self._on_ok_clicked)

        cancel_button = QPushButton(self)
        cancel_button.setText("Cancel")
        cancel_button.clicked.connect(self._on_cancel_clicked)

        buttons_layout = QHBoxLayout()
        buttons_layout.addWidget(self._ok_button)
        buttons_layout.addWidget(cancel_button)

        # main layout
        self._main_layout.addLayout(upper_layout)
        self._main_layout.addLayout(buttons_layout)

        # change the text if config exists
        if self.should_load_config:
            self.load_saved_config()

    #
    # Event handlers
    #

    def _on_ok_clicked(self):
        user = self._user_edit.text()
        path = self._repo_edit.text()
        remote_url = self._remote_edit.text()
        init_repo = self._initrepo_checkbox.isChecked()

        l.debug(
            "Attempting to connect to/init repo, user: %s | path: %s | init_repo? %r",
            user, path, init_repo)

        if not user:
            QMessageBox(self).critical(None, "Invalid user name",
                                       "User name cannot be empty.")
            return

        if user.lower() == "__root__":
            QMessageBox(self).critical(
                None, "Invalid user name",
                "User name cannot (and should not) be \'__root__\'.")
            return

        if not remote_url and not os.path.isdir(path) and not init_repo:
            QMessageBox(self).critical(
                None, "Repo does not exist",
                "The specified sync directory does not exist. "
                "Do you maybe want to initialize it?")
            return

        # convert to remote repo if no local is provided
        if self.is_git_repo(path):
            remote_url = None

        if remote_url and not path:
            path = os.path.join(
                os.path.dirname(self.controller.binary_path() or ""),
                os.path.basename(self.controller.binary_path() or "") + "_bs")

        try:
            connection_warnings = self.controller.connect(
                user, path, init_repo=init_repo, remote_url=remote_url)
            pass
        except Exception as e:
            l.critical("Error connecting to specified repository!")
            QMessageBox(self).critical(None, "Error connecting to repository",
                                       str(e))
            traceback.print_exc()
            return

        #
        # controller is now successfully connected to a real BinSync client. Everything from this point
        # onwards assumes that all normal client properties and functions work.
        #

        # warn user of anything that might look off
        self._parse_and_display_connection_warnings(connection_warnings)
        l.info(f"Client has connected to sync repo with user: {user}.")

        # create and save config if possible
        saved_config = self.save_config()
        if saved_config:
            l.debug(f"Configuration file was saved to {saved_config}.")

        self.close()

    def _on_repo_clicked(self):
        if 'SNAP' in os.environ:
            directory = QFileDialog.getExistingDirectory(
                self, "Select sync repo", "",
                QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
                | QFileDialog.DontUseNativeDialog)
        else:
            directory = QFileDialog.getExistingDirectory(
                self, "Select sync repo", "",
                QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)
        self._repo_edit.setText(QDir.toNativeSeparators(directory))

    def _on_repo_textchanged(self, new_text):
        path = new_text
        if pathlib.Path(path).exists() and self.is_git_repo(path):
            repo = git.Repo(path)
            try:
                url = repo.remote().url
            except Exception:
                url = None

            self._remote_edit.setText(url or "")
            self._remote_edit.setEnabled(False)
            self.remote_label.setStyleSheet('color: gray')

            is_binsync_inited = False
            try:
                is_binsync_inited = any(
                    ref.name.endswith(BINSYNC_ROOT_BRANCH)
                    for ref in repo.remote().refs)
            except Exception:
                pass

            if is_binsync_inited:
                self._initrepo_checkbox.setChecked(False)
                self._initrepo_checkbox.setStyleSheet(
                    'QCheckBox::unchecked { color: gray } '
                    'QCheckBox::checked { color: gray }')
                self._initrepo_checkbox.setEnabled(False)
            else:
                self._initrepo_checkbox.setEnabled(True)
                self._initrepo_checkbox.setStyleSheet(
                    'QCheckBox::unchecked { color: white } '
                    'QCheckBox::checked { color: white }')
        else:
            self._remote_edit.setEnabled(True)
            self._initrepo_checkbox.setEnabled(True)
            self.remote_label.setStyleSheet('color: white')
            self._initrepo_checkbox.setStyleSheet(
                'QCheckBox::unchecked { color: white } '
                'QCheckBox::checked { color: white }')

    def _on_cancel_clicked(self):
        self.close()

    #
    # Utils
    #

    def load_saved_config(self) -> bool:
        config = ProjectConfig.load_from_file(self.controller.binary_path()
                                              or "")
        if not config:
            return False

        user = config.user or ""
        repo = config.repo_path or ""
        remote = config.remote if config.remote and not config.repo_path else ""

        self._user_edit.setText(user)
        self._repo_edit.setText(repo)
        self._on_repo_textchanged(repo)
        self._remote_edit.setText(remote)
        return True

    def save_config(self) -> Optional[str]:
        user = self._user_edit.text()
        remote = self._remote_edit.text()
        repo = self._repo_edit.text()

        if remote and not repo:
            repo = str(
                pathlib.Path(self.controller.client.repo_root).absolute())

        config = ProjectConfig(self.controller.binary_path() or "",
                               user=user,
                               repo_path=repo,
                               remote=remote)
        if not config:
            return config

        return config.save()

    #
    # Static methods
    #

    @staticmethod
    def is_git_repo(path):
        return os.path.isdir(os.path.join(path, ".git"))

    @staticmethod
    def _parse_and_display_connection_warnings(warnings):
        warning_text = ""

        for warning in warnings:
            if warning == ConnectionWarnings.HASH_MISMATCH:
                warning_text += "Warning: the hash stored for this BinSync project does not match " \
                                "the hash of the binary you are attempting to analyze. It's possible " \
                                "you are working on a different binary.\n"

        if len(warning_text) > 0:
            QMessageBox.warning(
                None,
                "BinSync: Connection Warnings",
                warning_text,
                QMessageBox.Ok,
            )
Example #7
0
    def _init_widgets(self):

        #
        # Developer Options Group
        #

        dev_options_group = QGroupBox()
        dev_options_layout = QVBoxLayout()
        dev_options_group.setTitle("Developer Options")
        dev_options_group.setLayout(dev_options_layout)

        self._debug_log_toggle = QCheckBox("Toggle Debug Logging")
        self._debug_log_toggle.setToolTip(
            "Toggles the logging of events BinSync developers care about.")
        self._debug_log_toggle.stateChanged.connect(self._handle_debug_toggle)
        dev_options_layout.addWidget(self._debug_log_toggle)

        #
        # Sync Options Group
        #

        sync_options_group = QGroupBox()
        sync_options_layout = QVBoxLayout()
        sync_options_group.setTitle("Sync Options")
        sync_options_group.setLayout(sync_options_layout)

        self._merge_level_label = QLabel("Sync Merge Level")
        self._merge_level_label.setToolTip("""<html>
            <p>
            Defines which method is used to sync artifacts from another user.<br>
            <b>Non-Conflicting</b>: Only syncs artifacts that are not currently defined by you, so nothing is ever overwritten.<br>
            <b>Overwrite</b>: Syncs all artifacts regardless of your defined ones, overwriting everything.<br>
            <b>Merge</b>: You pick which artifacts are synced via the UI. <b>Unimplemented.</b>
            </p>
            </html>
            """)
        self._merge_level_label.setTextFormat(Qt.RichText)
        self._merge_level_combobox = QComboBox()
        self._merge_level_combobox.addItems(
            ["Non-Conflicting", "Overwrite", "Merge"])
        self._merge_level_combobox.currentIndexChanged.connect(
            self._handle_sync_level_change)

        sync_level_layout = QHBoxLayout()
        #sync_level_group.layout().setContentsMargins(0, 0, 0, 0)
        sync_level_layout.addWidget(self._merge_level_label)
        sync_level_layout.addWidget(self._merge_level_combobox)

        self._magic_sync_button = QPushButton("Initiate Magic Sync")
        self._magic_sync_button.clicked.connect(self._handle_magic_sync_button)
        self._magic_sync_button.setToolTip(
            "Performs a best effort merge of all existing user data to your state, "
            "but won't affect your existing state (this uses a non-conflicting merge)."
        )

        self._force_push_button = QPushButton("Force Push...")
        self._force_push_button.clicked.connect(self._handle_force_push_button)
        self._force_push_button.setToolTip(
            "Manually select function and globals you would like to be force committed "
            "and pushed to your user branch on Git.")

        sync_options_layout.addLayout(sync_level_layout)
        sync_options_group.layout().addWidget(self._magic_sync_button)
        sync_options_group.layout().addWidget(self._force_push_button)

        #
        # Final Layout
        #

        main_layout = QVBoxLayout()
        main_layout.setContentsMargins(10, 20, 10, 20)
        main_layout.setSpacing(18)
        main_layout.setAlignment(Qt.AlignTop)
        main_layout.addWidget(sync_options_group)
        main_layout.addWidget(dev_options_group)
        self.setLayout(main_layout)
Example #8
0
class QUtilPanel(QWidget):
    def __init__(self, controller: BinSyncController, parent=None):
        super().__init__(parent)
        self.controller = controller
        self._init_widgets()

    def _init_widgets(self):

        #
        # Developer Options Group
        #

        dev_options_group = QGroupBox()
        dev_options_layout = QVBoxLayout()
        dev_options_group.setTitle("Developer Options")
        dev_options_group.setLayout(dev_options_layout)

        self._debug_log_toggle = QCheckBox("Toggle Debug Logging")
        self._debug_log_toggle.setToolTip(
            "Toggles the logging of events BinSync developers care about.")
        self._debug_log_toggle.stateChanged.connect(self._handle_debug_toggle)
        dev_options_layout.addWidget(self._debug_log_toggle)

        #
        # Sync Options Group
        #

        sync_options_group = QGroupBox()
        sync_options_layout = QVBoxLayout()
        sync_options_group.setTitle("Sync Options")
        sync_options_group.setLayout(sync_options_layout)

        self._merge_level_label = QLabel("Sync Merge Level")
        self._merge_level_label.setToolTip("""<html>
            <p>
            Defines which method is used to sync artifacts from another user.<br>
            <b>Non-Conflicting</b>: Only syncs artifacts that are not currently defined by you, so nothing is ever overwritten.<br>
            <b>Overwrite</b>: Syncs all artifacts regardless of your defined ones, overwriting everything.<br>
            <b>Merge</b>: You pick which artifacts are synced via the UI. <b>Unimplemented.</b>
            </p>
            </html>
            """)
        self._merge_level_label.setTextFormat(Qt.RichText)
        self._merge_level_combobox = QComboBox()
        self._merge_level_combobox.addItems(
            ["Non-Conflicting", "Overwrite", "Merge"])
        self._merge_level_combobox.currentIndexChanged.connect(
            self._handle_sync_level_change)

        sync_level_layout = QHBoxLayout()
        #sync_level_group.layout().setContentsMargins(0, 0, 0, 0)
        sync_level_layout.addWidget(self._merge_level_label)
        sync_level_layout.addWidget(self._merge_level_combobox)

        self._magic_sync_button = QPushButton("Initiate Magic Sync")
        self._magic_sync_button.clicked.connect(self._handle_magic_sync_button)
        self._magic_sync_button.setToolTip(
            "Performs a best effort merge of all existing user data to your state, "
            "but won't affect your existing state (this uses a non-conflicting merge)."
        )

        self._force_push_button = QPushButton("Force Push...")
        self._force_push_button.clicked.connect(self._handle_force_push_button)
        self._force_push_button.setToolTip(
            "Manually select function and globals you would like to be force committed "
            "and pushed to your user branch on Git.")

        sync_options_layout.addLayout(sync_level_layout)
        sync_options_group.layout().addWidget(self._magic_sync_button)
        sync_options_group.layout().addWidget(self._force_push_button)

        #
        # Final Layout
        #

        main_layout = QVBoxLayout()
        main_layout.setContentsMargins(10, 20, 10, 20)
        main_layout.setSpacing(18)
        main_layout.setAlignment(Qt.AlignTop)
        main_layout.addWidget(sync_options_group)
        main_layout.addWidget(dev_options_group)
        self.setLayout(main_layout)

    #
    # Event Handlers
    #

    def _handle_debug_toggle(self):
        if self._debug_log_toggle.isChecked():
            logging.getLogger("binsync").setLevel("DEBUG")
            logging.getLogger("ida_binsync").setLevel("DEBUG")
            l.info("Logger has been set to level: DEBUG")
        else:
            logging.getLogger("binsync").setLevel("INFO")
            logging.getLogger("ida_binsync").setLevel("INFO")
            l.info("Logger has been set to level: INFO")

    def _handle_sync_level_change(self, index):
        selected_opt = self._merge_level_combobox.itemText(index)
        if selected_opt == "Non-Conflicting":
            self.controller.merge_level = MergeLevel.NON_CONFLICTING
        elif selected_opt == "Overwrite":
            self.controller.merge_level = MergeLevel.OVERWRITE
        elif selected_opt == "Merge":
            self.controller.merge_level = MergeLevel.MERGE
        else:
            return
        l.debug(f"Sync level changed to: {selected_opt}")

    def _handle_magic_sync_button(self):
        dialog = MagicSyncDialog(self.controller)
        dialog.exec_()

        if not dialog.should_sync:
            return

        self.controller.magic_fill(preference_user=dialog.preferred_user)

    def _handle_force_push_button(self):
        self.popup = ForcePushUI(self.controller)
        self.popup.show()