Exemplo n.º 1
0
class MainWindow(QMainWindow):
    """MainWindow class

    Attributes:
        trace_data (TraceData): TraceData object
        filtered_trace (list): Filtered trace
    """
    def __init__(self, parent=None):
        """Inits MainWindow, UI and plugins"""
        super(MainWindow, self).__init__(parent)
        self.api = Api(self)
        self.trace_data = TraceData()
        self.filtered_trace = []
        self.init_plugins()
        self.init_ui()
        if len(sys.argv) > 1:
            self.open_trace(sys.argv[1])

    def dragEnterEvent(self, event):
        """QMainWindow method reimplementation for file drag."""
        event.setDropAction(Qt.MoveAction)
        super().dragEnterEvent(event)
        event.accept()

    def dropEvent(self, event):
        """QMainWindow method reimplementation for file drop."""
        super().dropEvent(event)
        if event.mimeData().hasUrls():
            for url in event.mimeData().urls():
                local_file = url.toLocalFile()
                if os.path.isfile(local_file):
                    self.open_trace(local_file)

    def init_ui(self):
        """Inits UI"""
        uic.loadUi("gui/mainwindow.ui", self)

        title = prefs.PACKAGE_NAME + " " + prefs.PACKAGE_VERSION
        self.setWindowTitle(title)

        # accept file drops
        self.setAcceptDrops(True)

        # make trace table wider than regs&mem
        self.splitter1.setSizes([1400, 100])
        self.splitter2.setSizes([600, 100])

        # Init trace table
        self.trace_table.itemSelectionChanged.connect(
            self.on_trace_table_row_changed)
        self.trace_table.setColumnCount(len(prefs.TRACE_LABELS))
        self.trace_table.setHorizontalHeaderLabels(prefs.TRACE_LABELS)
        self.trace_table.horizontalHeader().setStretchLastSection(True)
        self.trace_table.bookmarkCreated.connect(self.add_bookmark)
        self.trace_table.commentEdited.connect(self.set_comment)
        self.trace_table.printer = self.print

        if prefs.USE_SYNTAX_HIGHLIGHT_IN_TRACE:
            dark_text = False
            if not prefs.USE_DARK_THEME:
                dark_text = True
            self.trace_table.init_syntax_highlight(dark_text)

        # trace pagination
        if prefs.PAGINATION_ENABLED:
            self.trace_pagination = PaginationWidget()
            self.trace_pagination.pageChanged.connect(self.trace_table.update)
            self.horizontalLayout.addWidget(self.trace_pagination)
            self.trace_pagination.set_enabled(True)
            self.trace_pagination.rows_per_page = prefs.PAGINATION_ROWS_PER_PAGE

            self.trace_table.pagination = self.trace_pagination
            self.horizontalLayout.setAlignment(self.trace_pagination,
                                               Qt.AlignLeft)

        # these are used to remember current pages & scroll values for both traces
        self.trace_current_pages = [1, 1]
        self.trace_scroll_values = [0, 0]

        self.reg_table.setColumnCount(len(prefs.REG_LABELS))
        self.reg_table.setHorizontalHeaderLabels(prefs.REG_LABELS)
        self.reg_table.horizontalHeader().setStretchLastSection(True)

        if prefs.REG_FILTER_ENABLED:
            self.reg_table.filtered_regs = prefs.REG_FILTER

        # Init memory table
        self.mem_table.setColumnCount(len(prefs.MEM_LABELS))
        self.mem_table.setHorizontalHeaderLabels(prefs.MEM_LABELS)
        self.mem_table.horizontalHeader().setStretchLastSection(True)

        # Init bookmark table
        self.bookmark_table.setColumnCount(len(prefs.BOOKMARK_LABELS))
        self.bookmark_table.setHorizontalHeaderLabels(prefs.BOOKMARK_LABELS)
        self.bookmark_table.setContextMenuPolicy(Qt.CustomContextMenu)
        self.bookmark_table.customContextMenuRequested.connect(
            self.bookmark_table_context_menu_event)

        self.bookmark_menu = QMenu(self)

        go_action = QAction("Go to bookmark", self)
        go_action.triggered.connect(self.go_to_bookmark_in_trace)
        self.bookmark_menu.addAction(go_action)

        delete_bookmarks_action = QAction("Delete bookmark(s)", self)
        delete_bookmarks_action.triggered.connect(self.delete_bookmarks)
        self.bookmark_menu.addAction(delete_bookmarks_action)

        # Menu
        exit_action = QAction("&Exit", self)
        exit_action.setShortcut("Ctrl+Q")
        exit_action.setStatusTip("Exit application")
        exit_action.triggered.connect(self.close)

        open_trace_action = QAction("&Open trace..", self)
        open_trace_action.setStatusTip("Open trace")
        open_trace_action.triggered.connect(self.dialog_open_trace)

        self.save_trace_action = QAction("&Save trace", self)
        self.save_trace_action.setStatusTip("Save trace")
        self.save_trace_action.triggered.connect(self.save_trace)
        self.save_trace_action.setEnabled(False)

        save_trace_as_action = QAction("&Save trace as..", self)
        save_trace_as_action.setStatusTip("Save trace as..")
        save_trace_as_action.triggered.connect(self.dialog_save_trace_as)

        save_trace_as_json_action = QAction("&Save trace as JSON..", self)
        save_trace_as_json_action.setStatusTip("Save trace as JSON..")
        save_trace_as_json_action.triggered.connect(
            self.dialog_save_trace_as_json)

        file_menu = self.menu_bar.addMenu("&File")
        file_menu.addAction(open_trace_action)
        file_menu.addAction(self.save_trace_action)
        file_menu.addAction(save_trace_as_action)
        file_menu.addAction(save_trace_as_json_action)
        file_menu.addAction(exit_action)

        self.plugins_topmenu = self.menu_bar.addMenu("&Plugins")

        clear_bookmarks_action = QAction("&Clear bookmarks", self)
        clear_bookmarks_action.setStatusTip("Clear bookmarks")
        clear_bookmarks_action.triggered.connect(self.clear_bookmarks)

        bookmarks_menu = self.menu_bar.addMenu("&Bookmarks")
        bookmarks_menu.addAction(clear_bookmarks_action)

        # Init right click menu for trace table
        self.init_trace_table_menu()
        # Init plugins menu on menu bar
        self.init_plugins_menu()

        about_action = QAction("&About", self)
        about_action.triggered.connect(self.show_about_dialog)

        about_menu = self.menu_bar.addMenu("&About")
        about_menu.addAction(about_action)

        if prefs.USE_SYNTAX_HIGHLIGHT_IN_LOG:
            self.highlight = AsmHighlighter(self.log_text_edit.document())

        # trace select
        self.select_trace_combo_box.addItem("Full trace")
        self.select_trace_combo_box.addItem("Filtered trace")
        self.select_trace_combo_box.currentIndexChanged.connect(
            self.trace_combo_box_index_changed)

        self.filter_widget = FilterWidget()
        self.filter_widget.filterBtnClicked.connect(self.on_filter_btn_clicked)
        self.horizontalLayout.addWidget(self.filter_widget)
        if prefs.SHOW_SAMPLE_FILTERS:
            self.filter_widget.set_sample_filters(prefs.SAMPLE_FILTERS)

        self.find_widget = FindWidget()
        self.find_widget.findBtnClicked.connect(self.on_find_btn_clicked)
        self.find_widget.set_fields(prefs.FIND_FIELDS)
        self.horizontalLayout.addWidget(self.find_widget)

        self.show()

    def init_plugins(self):
        """Inits plugins"""
        self.manager = PluginManager()
        self.manager.setPluginPlaces(["plugins"])
        self.manager.collectPlugins()
        for plugin in self.manager.getAllPlugins():
            print_debug(f"Plugin found: {plugin.name}")

    def init_plugins_menu(self):
        """Inits plugins menu"""
        self.plugins_topmenu.clear()

        reload_action = QAction("Reload plugins", self)
        reload_action.setShortcut("Ctrl+R")
        func = functools.partial(self.reload_plugins)
        reload_action.triggered.connect(func)
        self.plugins_topmenu.addAction(reload_action)
        self.plugins_topmenu.addSeparator()

        plugins_menu = QMenu("Run plugin", self)

        for plugin in self.manager.getAllPlugins():
            action = QAction(plugin.name, self)
            func = functools.partial(self.execute_plugin, plugin)
            action.triggered.connect(func)
            plugins_menu.addAction(action)
        self.plugins_topmenu.addMenu(plugins_menu)

    def init_trace_table_menu(self):
        """Initializes right click menu for trace table"""
        self.trace_table_menu = QMenu(self)

        copy_action = QAction("Print selected cells", self)
        copy_action.triggered.connect(self.trace_table.print_selected_cells)
        self.trace_table_menu.addAction(copy_action)

        add_bookmark_action = QAction("Add Bookmark", self)
        add_bookmark_action.triggered.connect(self.trace_table.create_bookmark)
        self.trace_table_menu.addAction(add_bookmark_action)

        plugins_menu = QMenu("Plugins", self)

        for plugin in self.manager.getAllPlugins():
            action = QAction(plugin.name, self)
            func = functools.partial(self.execute_plugin, plugin)
            action.triggered.connect(func)
            plugins_menu.addAction(action)
        self.trace_table_menu.addMenu(plugins_menu)
        self.trace_table.menu = self.trace_table_menu

    def reload_plugins(self):
        """Reloads plugins"""
        self.init_plugins()
        self.init_trace_table_menu()
        self.init_plugins_menu()

    def on_trace_table_row_changed(self):
        """Called when selected row changes"""
        selected_row_ids = self.get_selected_row_ids(self.trace_table)
        if not selected_row_ids:
            return
        row_id = selected_row_ids[0]
        regs = self.trace_data.get_regs_and_values(row_id)
        modified_regs = []
        if prefs.HIGHLIGHT_MODIFIED_REGS:
            modified_regs = self.trace_data.get_modified_regs(row_id)
        self.reg_table.set_data(regs, modified_regs)
        mem = []
        if "mem" in self.trace_data.trace[row_id]:
            mem = self.trace_data.trace[row_id]["mem"]
        self.mem_table.set_data(mem)
        self.update_status_bar()

    def on_filter_btn_clicked(self, filter_text: str):
        if self.trace_data is None:
            return
        try:
            filtered_trace = filter_trace(
                self.trace_data.trace,  # get_visible_trace(),
                self.trace_data.get_regs(),
                filter_text,
            )
        except Exception as exc:
            self.show_messagebox("Filter error", f"{exc}")
            # print(traceback.format_exc())
        else:
            self.filtered_trace = filtered_trace
            self.show_filtered_trace()

    def on_find_btn_clicked(self, keyword: str, field_index: int,
                            direction: int):
        """Find next or prev button clicked"""
        current_row = self.trace_table.currentRow()
        if current_row < 0:
            current_row = 0

        if self.trace_table.pagination is not None:
            pagination = self.trace_table.pagination
            page = pagination.current_page
            rows_per_page = pagination.rows_per_page
            current_row += (page - 1) * rows_per_page

        if field_index == 0:
            field = TraceField.DISASM
        elif field_index == 1:
            field = TraceField.REGS
        elif field_index == 2:
            field = TraceField.MEM
        elif field_index == 3:
            field = TraceField.MEM_ADDR
        elif field_index == 4:
            field = TraceField.MEM_VALUE
        elif field_index == 5:
            field = TraceField.COMMENT
        elif field_index == 6:
            field = TraceField.ANY

        try:
            row_number = find(
                trace=self.get_visible_trace(),
                field=field,
                keyword=keyword,
                start_row=current_row + direction,
                direction=direction,
            )
        except Exception as exc:
            self.show_messagebox("Find error", f"{exc}")
            print(traceback.format_exc())
            self.print(traceback.format_exc())
            return

        if row_number is not None:
            self.trace_table.go_to_row(row_number)
        else:
            print_debug(
                f"{keyword} not found (row: {current_row}, direction: {direction})"
            )

    def get_visible_trace(self):
        """Returns the trace that is currently shown on trace table"""
        index = self.select_trace_combo_box.currentIndex()
        if self.trace_data is not None:
            if index == 0:
                return self.trace_data.trace
            else:
                return self.filtered_trace
        return None

    def bookmark_table_context_menu_event(self):
        """Context menu for bookmark table right click"""
        self.bookmark_menu.popup(QCursor.pos())

    def dialog_open_trace(self):
        """Shows dialog to open trace file"""
        all_traces = "All traces (*.tvt *.trace32 *.trace64)"
        all_files = "All files (*.*)"
        filename = QFileDialog.getOpenFileName(
            self, "Open trace", "", all_traces + ";; " + all_files)[0]
        if filename:
            self.open_trace(filename)
            if self.trace_data:
                self.save_trace_action.setEnabled(True)

    def dialog_save_trace_as(self):
        """Shows a dialog to select a save file"""
        filename = QFileDialog.getSaveFileName(
            self, "Save trace as", "",
            "Trace Viewer traces (*.tvt);; All files (*.*)")[0]
        print_debug("Save trace as: " + filename)
        if filename and trace_files.save_as_tv_trace(self.trace_data,
                                                     filename):
            self.trace_data.filename = filename
            self.save_trace_action.setEnabled(True)

    def dialog_save_trace_as_json(self):
        """Shows a dialog to save trace to JSON file"""
        filename = QFileDialog.getSaveFileName(
            self, "Save as JSON", "",
            "JSON files (*.txt);; All files (*.*)")[0]
        print_debug("Save trace as: " + filename)
        if filename:
            trace_files.save_as_json(self.trace_data, filename)

    def execute_plugin(self, plugin):
        """Executes a plugin and updates tables"""
        print_debug(f"Executing a plugin: {plugin.name}")
        try:
            plugin.plugin_object.execute(self.api)
        except Exception:
            print("Error in plugin:")
            print(traceback.format_exc())
            self.print("Error in plugin:")
            self.print(traceback.format_exc())
        finally:
            if prefs.USE_SYNTAX_HIGHLIGHT_IN_LOG:
                self.highlight.rehighlight()

    def show_filtered_trace(self):
        """Shows filtered_trace on trace_table"""
        if self.select_trace_combo_box.currentIndex() == 0:
            self.select_trace_combo_box.setCurrentIndex(1)
        else:
            self.trace_table.set_data(self.filtered_trace)
            self.trace_table.update()

    def set_comment(self, row_id, comment):
        """Sets comment to row on full trace"""
        self.trace_data.set_comment(row_id, comment)

    def on_bookmark_table_cell_edited(self, item):
        """Called when any cell is edited on bookmark table"""
        cell_type = item.whatsThis()
        bookmarks = self.trace_data.get_bookmarks()
        row = self.bookmark_table.currentRow()
        if row < 0:
            print_debug("Error, could not edit bookmark.")
            return
        if cell_type == "startrow":
            bookmarks[row].startrow = int(item.text())
        elif cell_type == "endrow":
            bookmarks[row].endrow = int(item.text())
        elif cell_type == "address":
            bookmarks[row].addr = item.text()
        elif cell_type == "disasm":
            bookmarks[row].disasm = item.text()
        elif cell_type == "comment":
            bookmarks[row].comment = item.text()
        else:
            print_debug("Unknown field edited on bookmark table...")

    def open_trace(self, filename):
        """Opens and reads a trace file"""
        print_debug(f"Opening trace file: {filename}")
        self.close_trace()
        self.trace_data = trace_files.open_trace(filename)
        if self.trace_data is None:
            print_debug(f"Error, couldn't open trace file: {filename}")
        else:
            if prefs.PAGINATION_ENABLED:
                self.trace_pagination.set_current_page(1, True)
            self.trace_table.set_data(self.trace_data.trace)
            self.trace_table.update()
        self.update_bookmark_table()
        self.trace_table.update_column_widths()

    def close_trace(self):
        """Clears trace and updates UI"""
        self.trace_data = None
        self.filtered_trace = []
        self.trace_table.set_data([])
        self.update_ui()

    def update_ui(self):
        """Updates tables and status bar"""
        self.trace_table.update()
        self.update_bookmark_table()
        self.update_status_bar()

    def save_trace(self):
        """Saves a trace file"""
        filename = self.trace_data.filename
        print_debug("Save trace: " + filename)
        if filename:
            trace_files.save_as_tv_trace(self.trace_data, filename)

    def show_about_dialog(self):
        """Shows an about dialog"""
        title = "About"
        name = prefs.PACKAGE_NAME
        version = prefs.PACKAGE_VERSION
        copyrights = prefs.PACKAGE_COPYRIGHTS
        url = prefs.PACKAGE_URL
        text = f"{name} {version} \n {copyrights} \n {url}"
        QMessageBox().about(self, title, text)

    def update_column_widths(self, table):
        """Updates column widths of a TableWidget to match the content"""
        table.setVisible(False)  # fix ui glitch with column widths
        table.resizeColumnsToContents()
        table.horizontalHeader().setStretchLastSection(True)
        table.setVisible(True)

    def update_status_bar(self):
        """Updates status bar"""
        if self.trace_data is None:
            return
        table = self.trace_table
        row = table.currentRow()

        row_count = table.rowCount()
        row_info = f"{row}/{row_count - 1}"
        filename = self.trace_data.filename.split("/")[-1]
        msg = f"File: {filename} | Row: {row_info} "

        selected_row_id = 0
        row_ids = self.trace_table.get_selected_row_ids()
        if row_ids:
            selected_row_id = row_ids[0]

        msg += f" | {len(self.trace_data.trace)} rows in full trace."
        msg += f" | {len(self.filtered_trace)} rows in filtered trace."

        bookmark = self.trace_data.get_bookmark_from_row(selected_row_id)
        if bookmark:
            msg += f" | Bookmark: {bookmark.disasm}   ; {bookmark.comment}"

        self.status_bar.showMessage(msg)

    def get_selected_row_ids(self, table):
        """Returns IDs of all selected rows of TableWidget.

        Args:
            table: PyQt TableWidget
        returns:
            list: Ordered list of row ids
        """
        # use a set so we don't get duplicate ids
        row_ids_set = set(
            table.item(index.row(), 0).text()
            for index in table.selectedIndexes())
        try:
            row_ids_list = [int(i) for i in row_ids_set]
        except ValueError:
            print_debug("Error. Values in the first column must be integers.")
            return []
        return sorted(row_ids_list)

    def trace_combo_box_index_changed(self, index):
        """Trace selection combo box index changed"""
        self.trace_table.set_data(self.get_visible_trace())

        other_index = index ^ 1
        if prefs.PAGINATION_ENABLED:
            # save current page
            self.trace_current_pages[
                other_index] = self.trace_pagination.current_page
            self.trace_pagination.set_current_page(
                self.trace_current_pages[index], True)

        # save scrollbar value
        current_scroll = self.trace_table.verticalScrollBar().value()
        self.trace_scroll_values[other_index] = current_scroll
        next_value = self.trace_scroll_values[index]

        self.trace_table.update()
        QApplication.processEvents()  # this is needed to update the scrollbar
        self.trace_table.verticalScrollBar().setValue(next_value)

    def go_to_row_in_visible_trace(self, row):
        """Goes to given row in currently visible trace"""
        self.trace_table.go_to_row(row)
        self.tab_widget.setCurrentIndex(0)

    def go_to_row_in_full_trace(self, row_id):
        """Switches to full trace and goes to given row"""
        # make sure we are shown full trace, not filtered
        if self.select_trace_combo_box.currentIndex() == 1:
            self.select_trace_combo_box.setCurrentIndex(0)
        self.go_to_row_in_visible_trace(row_id)

    def go_to_bookmark_in_trace(self):
        """Goes to trace row of selected bookmark"""
        selected_row_ids = self.get_selected_row_ids(self.bookmark_table)
        if not selected_row_ids:
            print_debug("Error. No bookmark selected.")
            return
        self.go_to_row_in_full_trace(selected_row_ids[0])

    def clear_bookmarks(self):
        """Clears all bookmarks"""
        self.trace_data.clear_bookmarks()
        self.update_bookmark_table()

    def delete_bookmarks(self):
        """Deletes selected bookmarks"""
        selected = self.bookmark_table.selectedItems()
        if not selected:
            print_debug("Could not delete a bookmark. Nothing selected.")
            return
        selected_rows = sorted(set({sel.row() for sel in selected}))
        for row in reversed(selected_rows):
            self.trace_data.delete_bookmark(row)
            self.bookmark_table.removeRow(row)

    def get_selected_bookmarks(self):
        """Returns selected bookmarks"""
        selected = self.bookmark_table.selectedItems()
        if not selected:
            print_debug("No bookmarks selected.")
            return []
        selected_rows = sorted(set({sel.row() for sel in selected}))
        all_bookmarks = self.trace_data.get_bookmarks()
        return [all_bookmarks[i] for i in selected_rows]

    def add_bookmark(self, bookmark):
        if prefs.ASK_FOR_BOOKMARK_COMMENT:
            comment = self.get_string_from_user(
                "Bookmark comment", "Give a comment for bookmark:")
            if comment:
                bookmark.comment = comment
        self.trace_data.add_bookmark(bookmark)
        self.update_bookmark_table()

    def update_bookmark_table(self):
        """Updates bookmarks table from trace_data"""
        if self.trace_data is None:
            return
        table = self.bookmark_table
        try:
            table.itemChanged.disconnect()
        except Exception:
            pass
        bookmarks = self.trace_data.get_bookmarks()
        table.setRowCount(len(bookmarks))

        for i, bookmark in enumerate(bookmarks):
            startrow = QTableWidgetItem(bookmark.startrow)
            startrow.setData(Qt.DisplayRole, int(bookmark.startrow))
            startrow.setWhatsThis("startrow")
            table.setItem(i, 0, startrow)

            endrow = QTableWidgetItem(bookmark.endrow)
            endrow.setData(Qt.DisplayRole, int(bookmark.endrow))
            endrow.setWhatsThis("endrow")
            table.setItem(i, 1, endrow)

            address = QTableWidgetItem(bookmark.addr)
            address.setWhatsThis("address")
            table.setItem(i, 2, address)

            disasm = QTableWidgetItem(bookmark.disasm)
            disasm.setWhatsThis("disasm")
            table.setItem(i, 3, disasm)

            comment = QTableWidgetItem(bookmark.comment)
            comment.setWhatsThis("comment")
            table.setItem(i, 4, comment)

        table.itemChanged.connect(self.on_bookmark_table_cell_edited)
        self.update_column_widths(table)

    def print(self, text):
        """Prints text to TextEdit on log tab"""
        self.log_text_edit.appendPlainText(str(text))

    def go_to_row(self, table, row):
        """Scrolls a table to the specified row"""
        table.scrollToItem(table.item(row, 3),
                           QAbstractItemView.PositionAtCenter)

    def ask_user(self, title, question):
        """Shows a messagebox with yes/no question

        Args:
            title (str): MessageBox title
            question (str): MessageBox qustion label
        Returns:
            bool: True if user clicked yes, False otherwise
        """
        answer = QMessageBox.question(
            self,
            title,
            question,
            QMessageBox.StandardButtons(QMessageBox.Yes | QMessageBox.No),
        )
        return bool(answer == QMessageBox.Yes)

    def get_string_from_user(self, title, label):
        """Gets a string from user

        Args:
            title (str): Input dialog title
            label (str): Input dialog label
        Returns:
            string: String given by user, empty string if user clicked cancel
        """
        answer, ok_clicked = QInputDialog.getText(self, title, label,
                                                  QLineEdit.Normal, "")
        if ok_clicked:
            return answer
        return ""

    def get_values_from_user(self, title, data, on_ok_clicked=None):
        """Gets values from user

        Args:
            title (str): Input dialog title
            data (list): List of dicts
            on_ok_clicked (method): Callback function to e.g. check the input
        Returns:
            list: List of values given by user, empty list if user canceled
        """
        input_dlg = InputDialog(self, title, data, on_ok_clicked)
        input_dlg.exec_()
        return input_dlg.get_data()

    def show_messagebox(self, title, msg):
        """Shows a messagebox"""
        alert = QMessageBox()
        alert.setWindowTitle(title)
        alert.setText(msg)
        alert.exec_()
Exemplo n.º 2
0
class MainWindow(QtWidgets.QMainWindow):
    """MainWindow class

    Attributes:
        trace_data (TraceData): TraceData object
        filtered_trace (list): Filtered trace
    """
    def __init__(self):
        """Inits MainWindow, UI and plugins"""
        super(MainWindow, self).__init__()
        self.api = Api(self)
        self.trace_data = TraceData()
        self.filtered_trace = None
        self.init_plugins()
        self.init_ui()
        if len(sys.argv) > 1:
            self.open_trace(sys.argv[1])

    def dragEnterEvent(self, event):
        """QMainWindow method reimplementation for file drag."""
        event.accept()

    def dropEvent(self, event):
        """QMainWindow method reimplementation for file drop."""
        if event.mimeData().hasUrls():
            for url in event.mimeData().urls():
                local_file = url.toLocalFile()
                if os.path.isfile(local_file):
                    self.open_trace(local_file)

    def init_plugins(self):
        """Inits plugins"""
        self.manager = PluginManager()
        self.manager.setPluginPlaces(["plugins"])
        self.manager.collectPlugins()
        for plugin in self.manager.getAllPlugins():
            print_debug("Plugin found: %s" % plugin.name)

    def init_plugins_menu(self):
        """Inits plugins menu"""
        self.plugins_topmenu.clear()
        reload_action = QtWidgets.QAction("Reload plugins", self)
        func = functools.partial(self.reload_plugins)
        reload_action.triggered.connect(func)
        self.plugins_topmenu.addAction(reload_action)
        self.plugins_topmenu.addSeparator()

        for plugin in self.manager.getAllPlugins():
            action = QtWidgets.QAction(plugin.name, self)
            func = functools.partial(self.execute_plugin, plugin)
            action.triggered.connect(func)
            self.plugins_topmenu.addAction(action)

    def reload_plugins(self):
        """Reloads plugins"""
        self.init_plugins()
        self.init_trace_table_menu()
        self.init_plugins_menu()

    def init_ui(self):
        """Inits UI"""
        uic.loadUi("gui/mainwindow.ui", self)

        title = prefs.PACKAGE_NAME + " " + prefs.PACKAGE_VERSION
        self.setWindowTitle(title)

        self.filter_button.clicked.connect(self.on_filter_clicked)
        self.filter_check_box.stateChanged.connect(
            self.on_filter_check_box_state_changed)

        self.find_next_button.clicked.connect(lambda: self.on_find_clicked(1))
        self.find_prev_button.clicked.connect(lambda: self.on_find_clicked(-1))

        # accept file drops
        self.setAcceptDrops(True)

        # make trace table wider than regs&mem
        self.splitter1.setSizes([1400, 100])
        self.splitter2.setSizes([600, 100])

        # Init trace table
        self.trace_table.setColumnCount(len(prefs.TRACE_LABELS))
        self.trace_table.setHorizontalHeaderLabels(prefs.TRACE_LABELS)
        self.trace_table.itemSelectionChanged.connect(
            self.on_trace_table_selection_changed)
        self.trace_table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.trace_table.customContextMenuRequested.connect(
            self.trace_table_context_menu_event)

        # Init register table
        self.reg_table.setColumnCount(len(prefs.REG_LABELS))
        self.reg_table.setHorizontalHeaderLabels(prefs.REG_LABELS)
        self.reg_table.horizontalHeader().setStretchLastSection(True)

        # Init memory table
        self.mem_table.setColumnCount(len(prefs.MEM_LABELS))
        self.mem_table.setHorizontalHeaderLabels(prefs.MEM_LABELS)
        self.mem_table.horizontalHeader().setStretchLastSection(True)

        # Init bookmark table
        self.bookmark_table.setColumnCount(len(prefs.BOOKMARK_LABELS))
        self.bookmark_table.setHorizontalHeaderLabels(prefs.BOOKMARK_LABELS)
        self.bookmark_table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.bookmark_table.customContextMenuRequested.connect(
            self.bookmark_table_context_menu_event)

        self.bookmark_menu = QtWidgets.QMenu(self)

        go_action = QtWidgets.QAction("Go to bookmark", self)
        go_action.triggered.connect(self.go_to_bookmark)
        self.bookmark_menu.addAction(go_action)

        delete_bookmarks_action = QtWidgets.QAction("Delete bookmark(s)", self)
        delete_bookmarks_action.triggered.connect(self.delete_bookmarks)
        self.bookmark_menu.addAction(delete_bookmarks_action)

        # Menu
        exit_action = QtWidgets.QAction("&Exit", self)
        exit_action.setShortcut("Ctrl+Q")
        exit_action.setStatusTip("Exit application")
        exit_action.triggered.connect(self.close)

        open_trace_action = QtWidgets.QAction("&Open trace..", self)
        open_trace_action.setStatusTip("Open trace")
        open_trace_action.triggered.connect(self.dialog_open_trace)

        self.save_trace_action = QtWidgets.QAction("&Save trace", self)
        # self.save_trace_action.setShortcut("Ctrl+S")
        self.save_trace_action.setStatusTip("Save trace")
        self.save_trace_action.triggered.connect(self.save_trace)
        self.save_trace_action.setEnabled(False)

        save_trace_as_action = QtWidgets.QAction("&Save trace as..", self)
        save_trace_as_action.setStatusTip("Save trace as..")
        save_trace_as_action.triggered.connect(self.dialog_save_trace_as)

        save_trace_as_json_action = QtWidgets.QAction("&Save trace as JSON..",
                                                      self)
        save_trace_as_json_action.setStatusTip("Save trace as JSON..")
        save_trace_as_json_action.triggered.connect(
            self.dialog_save_trace_as_json)

        file_menu = self.menu_bar.addMenu("&File")
        file_menu.addAction(open_trace_action)
        file_menu.addAction(self.save_trace_action)
        file_menu.addAction(save_trace_as_action)
        file_menu.addAction(save_trace_as_json_action)
        file_menu.addAction(exit_action)

        self.plugins_topmenu = self.menu_bar.addMenu("&Plugins")

        clear_bookmarks_action = QtWidgets.QAction("&Clear bookmarks", self)
        clear_bookmarks_action.setStatusTip("Clear bookmarks")
        clear_bookmarks_action.triggered.connect(self.clear_bookmarks)

        bookmarks_menu = self.menu_bar.addMenu("&Bookmarks")
        bookmarks_menu.addAction(clear_bookmarks_action)

        # Init right click menu for trace table
        self.init_trace_table_menu()
        self.init_plugins_menu()

        about_action = QtWidgets.QAction("&About", self)
        about_action.triggered.connect(self.show_about_dialog)

        about_menu = self.menu_bar.addMenu("&About")
        about_menu.addAction(about_action)

        if prefs.USE_SYNTAX_HIGHLIGHT:
            self.highlight = AsmHighlighter(self.log_text_edit.document())

        for field in prefs.FIND_FIELDS:
            self.find_combo_box.addItem(field)

        if prefs.SHOW_SAMPLE_FILTERS:
            for sample_filter in prefs.SAMPLE_FILTERS:
                self.filter_edit.addItem(sample_filter)

        self.filter_edit.keyPressEvent = self.on_filter_edit_key_pressed

        self.show()

    def init_trace_table_menu(self):
        """Initializes right click menu for trace table"""
        self.trace_table_menu = QtWidgets.QMenu(self)

        copy_action = QtWidgets.QAction("Print selected cells", self)
        copy_action.triggered.connect(self.trace_table_print_cells)
        self.trace_table_menu.addAction(copy_action)

        add_bookmark_action = QtWidgets.QAction("Add Bookmark", self)
        add_bookmark_action.triggered.connect(self.trace_table_create_bookmark)
        self.trace_table_menu.addAction(add_bookmark_action)

        plugins_menu = QtWidgets.QMenu("Plugins", self)

        for plugin in self.manager.getAllPlugins():
            action = QtWidgets.QAction(plugin.name, self)
            func = functools.partial(self.execute_plugin, plugin)
            action.triggered.connect(func)
            plugins_menu.addAction(action)
        self.trace_table_menu.addMenu(plugins_menu)

    def set_filter(self, filter_text):
        """Sets a a new filter for trace and filters trace"""
        try:
            self.filtered_trace = filter_trace(self.trace_data.trace,
                                               self.trace_data.regs,
                                               filter_text)
        except Exception as exc:
            print("Error on filter: " + str(exc))
            print(traceback.format_exc())

    def get_visible_trace(self):
        """Returns the trace that is currently shown on trace table"""
        if self.filter_check_box.isChecked(
        ) and self.filtered_trace is not None:
            return self.filtered_trace
        return self.trace_data.trace

    def bookmark_table_context_menu_event(self):
        """Context menu for bookmark table right click"""
        self.bookmark_menu.popup(QtGui.QCursor.pos())

    def dialog_open_trace(self):
        """Shows dialog to open trace file"""
        all_traces = "All traces (*.tvt *.trace32 *.trace64)"
        all_files = "All files (*.*)"
        filename = QtWidgets.QFileDialog.getOpenFileName(
            self, "Open trace", "", all_traces + ";; " + all_files)[0]
        if filename:
            self.open_trace(filename)
            if self.trace_data:
                self.save_trace_action.setEnabled(True)

    def dialog_save_trace_as(self):
        """Shows a dialog to select a save file"""
        filename = QtWidgets.QFileDialog.getSaveFileName(
            self, "Save trace as", "",
            "Trace Viewer traces (*.tvt);; All files (*.*)")[0]
        print_debug("Save trace as: " + filename)
        if filename and trace_files.save_as_tv_trace(self.trace_data,
                                                     filename):
            self.trace_data.filename = filename
            self.save_trace_action.setEnabled(True)

    def dialog_save_trace_as_json(self):
        """Shows a dialog to save trace to JSON file"""
        filename = QtWidgets.QFileDialog.getSaveFileName(
            self, "Save as JSON", "",
            "JSON files (*.txt);; All files (*.*)")[0]
        print_debug("Save trace as: " + filename)
        if filename:
            trace_files.save_as_json(self.trace_data, filename)

    def execute_plugin(self, plugin):
        """Executes a plugin and updates tables"""
        print_debug("Executing a plugin: %s" % plugin.name)
        try:
            plugin.plugin_object.execute(self.api)
        except Exception:
            print_debug("Error in plugin:")
            print_debug(traceback.format_exc())
            self.print("Error in plugin:")
            self.print(traceback.format_exc())
        finally:
            if prefs.USE_SYNTAX_HIGHLIGHT:
                self.highlight.rehighlight()

    def on_filter_edit_key_pressed(self, event):
        """Checks if enter is pressed on filterEdit"""
        key = event.key()
        if key == QtCore.Qt.Key_Return:
            self.on_filter_clicked()
        QtWidgets.QComboBox.keyPressEvent(self.filter_edit, event)

    def show_filtered_trace(self):
        """Shows filtered_trace on trace_table"""
        if not self.filter_check_box.isChecked():
            self.filter_check_box.setChecked(
                True)  # this will also update trace_table
        else:
            self.update_trace_table()

    def on_filter_check_box_state_changed(self):
        """Callback function for state change of filter checkbox"""
        self.update_trace_table()

    def on_find_clicked(self, direction):
        """Find next or prev button clicked"""

        row = self.trace_table.currentRow()
        if row < 0:
            row = 0

        keyword = self.search_edit.text()
        index = self.find_combo_box.currentIndex()
        if index == 0:
            field = TraceField.DISASM
        elif index == 1:
            field = TraceField.REGS
        elif index == 2:
            field = TraceField.MEM
        elif index == 3:
            field = TraceField.MEM_ADDR
        elif index == 4:
            field = TraceField.MEM_VALUE
        elif index == 5:
            field = TraceField.COMMENT
        elif index == 6:
            field = TraceField.ANY

        try:
            row_number = find(
                trace=self.get_visible_trace(),
                field=field,
                keyword=keyword,
                start_row=row + direction,
                direction=direction,
            )
        except Exception as exc:
            print("Error on find: " + str(exc))
            print(traceback.format_exc())
            self.print(traceback.format_exc())
            return

        if row_number is not None:
            self.goto_row(self.trace_table, row_number)
            self.select_row(self.trace_table, row_number)
        else:
            print_debug("%s not found (row %d, direction %d)" %
                        (keyword, row, direction))

    def on_filter_clicked(self):
        """Sets a filter and filters trace data"""
        filter_text = self.filter_edit.currentText()
        print_debug("Set filter: %s" % filter_text)
        self.set_filter(filter_text)
        if not self.filter_check_box.isChecked():
            self.filter_check_box.setChecked(True)
        else:
            self.update_trace_table()

    def on_trace_table_cell_edited(self, item):
        """Called when any cell is edited on trace table"""
        table = self.trace_table
        cell_type = item.whatsThis()
        if cell_type == "comment":
            row = table.currentRow()
            if row < 0:
                print_debug("Error, could not edit trace.")
                return
            row_id = int(table.item(row, 0).text())
            self.trace_data.set_comment(item.text(), row_id)
        else:
            print_debug("Only comment editing allowed for now...")

    def on_bookmark_table_cell_edited(self, item):
        """Called when any cell is edited on bookmark table"""
        cell_type = item.whatsThis()
        bookmarks = self.trace_data.get_bookmarks()
        row = self.bookmark_table.currentRow()
        if row < 0:
            print_debug("Error, could not edit bookmark.")
            return
        if cell_type == "startrow":
            bookmarks[row].startrow = int(item.text())
        elif cell_type == "endrow":
            bookmarks[row].endrow = int(item.text())
        elif cell_type == "address":
            bookmarks[row].addr = item.text()
        elif cell_type == "disasm":
            bookmarks[row].disasm = item.text()
        elif cell_type == "comment":
            bookmarks[row].comment = item.text()
        else:
            print_debug("Unknown field edited in bookmark table...")

    def open_trace(self, filename):
        """Opens and reads a trace file"""
        print_debug("Opening trace file: %s" % filename)
        self.close_trace()
        self.trace_data = trace_files.open_trace(filename)
        if self.trace_data is None:
            print_debug("Error, couldn't open trace file: %s" % filename)
        self.update_ui()
        self.update_column_widths(self.trace_table)

    def close_trace(self):
        """Clears trace and updates UI"""
        self.trace_data = None
        self.filtered_trace = None
        self.update_ui()

    def update_ui(self):
        """Updates tables and status bar"""
        self.update_trace_table()
        self.update_bookmark_table()
        self.update_status_bar()

    def save_trace(self):
        """Saves a trace file"""
        filename = self.trace_data.filename
        print_debug("Save trace: " + filename)
        if filename:
            trace_files.save_as_tv_trace(self.trace_data, filename)

    def show_about_dialog(self):
        """Shows an about dialog"""
        title = "About"
        name = prefs.PACKAGE_NAME
        version = prefs.PACKAGE_VERSION
        copyrights = prefs.PACKAGE_COPYRIGHTS
        url = prefs.PACKAGE_URL
        text = "%s %s \n %s \n %s" % (name, version, copyrights, url)
        QtWidgets.QMessageBox().about(self, title, text)

    def update_column_widths(self, table):
        """Updates column widths of a TableWidget to match the content"""
        table.setVisible(False)  # fix ui glitch with column widths
        table.resizeColumnsToContents()
        table.horizontalHeader().setStretchLastSection(True)
        table.setVisible(True)

    def update_trace_table(self):
        """Updates trace table"""
        table = self.trace_table

        if self.trace_data is None:
            table.setRowCount(0)
            return
        try:
            table.itemChanged.disconnect()
        except Exception:
            pass

        trace = self.get_visible_trace()
        row_count = len(trace)
        print_debug("Updating trace table: %d rows." % row_count)
        table.setRowCount(row_count)
        if row_count == 0:
            return

        ip_name = self.trace_data.get_instruction_pointer_name()
        if ip_name:
            ip_reg_index = self.trace_data.regs[ip_name]

        for i in range(0, row_count):
            row_id = str(trace[i]["id"])
            if ip_name:
                address = trace[i]["regs"][ip_reg_index]
                table.setItem(i, 1, QtWidgets.QTableWidgetItem(hex(address)))
            opcodes = trace[i]["opcodes"]
            disasm = trace[i]["disasm"]
            comment = str(trace[i]["comment"])
            comment_item = QtWidgets.QTableWidgetItem(comment)
            comment_item.setWhatsThis("comment")
            table.setItem(i, 0, QtWidgets.QTableWidgetItem(row_id))
            table.setItem(i, 2, QtWidgets.QTableWidgetItem(opcodes))
            table.setItem(i, 3, QtWidgets.QTableWidgetItem(disasm))
            table.setItem(i, 4, comment_item)
        table.itemChanged.connect(self.on_trace_table_cell_edited)

    def update_regs_and_mem(self):
        """Updates register and memory tables"""

        # clear mem_table
        self.mem_table.setRowCount(0)

        if self.trace_data is None:
            return

        table = self.trace_table
        row_ids = self.get_selected_row_ids(table)
        if not row_ids:
            return
        row_id = row_ids[0]
        trace_row = self.trace_data.trace[row_id]

        if "regs" in trace_row:
            registers = []
            flags = None
            reg_values = trace_row["regs"]

            for reg_name, reg_index in self.trace_data.regs.items():
                if (self.trace_data.arch in ('x86', 'x64')
                        and prefs.REG_FILTER_ENABLED
                        and reg_name not in prefs.REG_FILTER):
                    continue  # don't show this register

                reg_value = reg_values[reg_index]

                reg = {}
                reg["name"] = reg_name
                reg["value"] = reg_value
                registers.append(reg)

                if reg_name == "eflags":
                    eflags = reg_value
                    flags = {
                        "c": eflags & 1,  # carry
                        "p": (eflags >> 2) & 1,  # parity
                        # "a": (eflags >> 4) & 1,  # aux_carry
                        "z": (eflags >> 6) & 1,  # zero
                        "s": (eflags >> 7) & 1,  # sign
                        # "d": (eflags >> 10) & 1, # direction
                        # "o":  (eflags >> 11) & 1 # overflow
                    }

            if self.reg_table.rowCount() != len(registers):
                self.reg_table.setRowCount(len(registers))

            modified_regs = []
            if prefs.HIGHLIGHT_MODIFIED_REGS:
                modified_regs = self.trace_data.get_modified_regs(row_id)

            # fill register table
            for i, reg in enumerate(registers):
                self.reg_table.setItem(i, 0,
                                       QtWidgets.QTableWidgetItem(reg["name"]))
                self.reg_table.setItem(
                    i, 1, QtWidgets.QTableWidgetItem(hex(reg["value"])))
                self.reg_table.setItem(
                    i, 2, QtWidgets.QTableWidgetItem(str(reg["value"])))

                if reg["name"] in modified_regs:
                    self.reg_table.item(i, 0).setBackground(
                        QtGui.QColor(100, 100, 150))
                    self.reg_table.item(i, 1).setBackground(
                        QtGui.QColor(100, 100, 150))
                    self.reg_table.item(i, 2).setBackground(
                        QtGui.QColor(100, 100, 150))

            if flags:
                flags_text = f"C:{flags['c']} P:{flags['p']} Z:{flags['z']} S:{flags['s']}"
                row_count = self.reg_table.rowCount()
                self.reg_table.setRowCount(row_count + 1)
                self.reg_table.setItem(row_count, 0,
                                       QtWidgets.QTableWidgetItem("flags"))
                self.reg_table.setItem(row_count, 1,
                                       QtWidgets.QTableWidgetItem(flags_text))

        if "mem" in trace_row:
            mems = trace_row["mem"]
            self.mem_table.setRowCount(len(mems))
            for i, mem in enumerate(mems):
                self.mem_table.setItem(
                    i, 0, QtWidgets.QTableWidgetItem(mem["access"]))
                self.mem_table.setItem(
                    i, 1, QtWidgets.QTableWidgetItem(hex(mem["addr"])))
                self.mem_table.setItem(
                    i, 2, QtWidgets.QTableWidgetItem(hex(mem["value"])))
            self.update_column_widths(self.mem_table)

    def update_status_bar(self):
        """Updates status bar"""
        if self.trace_data is None:
            return
        table = self.trace_table
        row = table.currentRow()

        row_count = table.rowCount()
        row_info = "%d/%d" % (row, row_count - 1)
        filename = self.trace_data.filename.split("/")[-1]
        msg = "File: %s | Row: %s " % (filename, row_info)

        selected_row_id = 0
        row_ids = self.get_selected_row_ids(table)
        if row_ids:
            selected_row_id = row_ids[0]

        bookmark = self.trace_data.get_bookmark_from_row(selected_row_id)
        if bookmark:
            msg += " | Bookmark: %s   ; %s" % (bookmark.disasm,
                                               bookmark.comment)
        self.status_bar.showMessage(msg)

    def get_selected_row_ids(self, table):
        """Returns IDs of all selected rows of TableWidget.

        Args:
            table: PyQt TableWidget
        returns:
            list: Ordered list of row ids
        """
        # use a set so we don't get duplicate ids
        row_ids_set = set(
            table.item(index.row(), 0).text()
            for index in table.selectedIndexes())
        try:
            row_ids_list = [int(i) for i in row_ids_set]
        except ValueError:
            print_debug("Error. Values in the first column must be integers.")
            return None
        return sorted(row_ids_list)

    def trace_table_create_bookmark(self):
        """Context menu action for creating a bookmark"""
        table = self.trace_table

        selected_rows = table.selectedItems()
        if not selected_rows:
            print_debug("Could not create a bookmark. Nothing selected.")
            return
        addr = table.item(selected_rows[0].row(), 1).text()
        disasm = table.item(selected_rows[0].row(), 3).text()
        comment = ""
        if prefs.ASK_FOR_BOOKMARK_COMMENT:
            comment = self.get_string_from_user(
                "Bookmark comment", "Give a comment for bookmark:")
        if not comment:
            comment = table.item(selected_rows[0].row(), 4).text()

        selected_row_ids = self.get_selected_row_ids(table)
        first_row_id = selected_row_ids[0]
        last_row_id = selected_row_ids[-1]

        bookmark = Bookmark(startrow=first_row_id,
                            endrow=last_row_id,
                            addr=addr,
                            disasm=disasm,
                            comment=comment)
        self.trace_data.add_bookmark(bookmark)
        self.update_bookmark_table()

    def trace_table_print_cells(self):
        """Context menu action for trace table print cells"""
        items = self.trace_table.selectedItems()
        for item in items:
            self.print(item.text())

    def trace_table_context_menu_event(self):
        """Context menu for trace table right click"""
        self.trace_table_menu.popup(QtGui.QCursor.pos())

    def go_to_bookmark(self):
        """Goes to selected bookmark"""
        selected_row_ids = self.get_selected_row_ids(self.bookmark_table)
        if not selected_row_ids:
            print_debug("Error. No bookmark selected.")
            return
        row_id = selected_row_ids[0]
        if self.filter_check_box.isChecked():
            self.filter_check_box.setChecked(False)
        self.goto_row(self.trace_table, row_id)
        self.select_row(self.trace_table, row_id)
        self.tab_widget.setCurrentIndex(0)

    def clear_bookmarks(self):
        """Clears all bookmarks"""
        self.trace_data.clear_bookmarks()
        self.update_bookmark_table()

    def delete_bookmarks(self):
        """Deletes selected bookmarks"""
        selected = self.bookmark_table.selectedItems()
        if not selected:
            print_debug("Could not delete a bookmark. Nothing selected.")
            return
        selected_rows = sorted(set({sel.row() for sel in selected}))
        for row in reversed(selected_rows):
            self.trace_data.delete_bookmark(row)
            self.bookmark_table.removeRow(row)

    def get_selected_bookmarks(self):
        """Returns selected bookmarks"""
        selected = self.bookmark_table.selectedItems()
        if not selected:
            print_debug("No bookmarks selected.")
            return []
        selected_rows = sorted(set({sel.row() for sel in selected}))
        all_bookmarks = self.trace_data.get_bookmarks()
        return [all_bookmarks[i] for i in selected_rows]

    def update_bookmark_table(self):
        """Updates bookmarks table from trace_data"""
        if self.trace_data is None:
            return
        table = self.bookmark_table
        try:
            table.itemChanged.disconnect()
        except Exception:
            pass
        bookmarks = self.trace_data.get_bookmarks()
        table.setRowCount(len(bookmarks))

        for i, bookmark in enumerate(bookmarks):
            startrow = QtWidgets.QTableWidgetItem(bookmark.startrow)
            startrow.setData(QtCore.Qt.DisplayRole, int(bookmark.startrow))
            startrow.setWhatsThis("startrow")
            table.setItem(i, 0, startrow)

            endrow = QtWidgets.QTableWidgetItem(bookmark.endrow)
            endrow.setData(QtCore.Qt.DisplayRole, int(bookmark.endrow))
            endrow.setWhatsThis("endrow")
            table.setItem(i, 1, endrow)

            address = QtWidgets.QTableWidgetItem(bookmark.addr)
            address.setWhatsThis("address")
            table.setItem(i, 2, address)

            disasm = QtWidgets.QTableWidgetItem(bookmark.disasm)
            disasm.setWhatsThis("disasm")
            table.setItem(i, 3, disasm)

            comment = QtWidgets.QTableWidgetItem(bookmark.comment)
            comment.setWhatsThis("comment")
            table.setItem(i, 4, comment)

        # print_debug("Updating bookmark table: %d rows." % len(bookmarks))
        table.itemChanged.connect(self.on_bookmark_table_cell_edited)
        self.update_column_widths(table)

    def on_trace_table_selection_changed(self):
        """Callback function for trace table selection change"""
        self.update_regs_and_mem()
        self.update_status_bar()

    def print(self, text):
        """Prints text to TextEdit on log tab"""
        self.log_text_edit.appendPlainText(str(text))

    def goto_row(self, table, row):
        """Scrolls a table to the specified row"""
        table.scrollToItem(table.item(row, 3),
                           QtWidgets.QAbstractItemView.PositionAtCenter)

    def select_row(self, table, row):
        """Selects a row in a table"""
        table.clearSelection()
        item = table.item(row, 0)
        table.setCurrentItem(
            item,
            QtCore.QItemSelectionModel.Select
            | QtCore.QItemSelectionModel.Rows
            | QtCore.QItemSelectionModel.Current,
        )

    def ask_user(self, title, question):
        """Shows a messagebox with yes/no question

        Args:
            title (str): MessageBox title
            question (str): MessageBox qustion label
        Returns:
            bool: True if user clicked yes, False otherwise
        """
        answer = QtWidgets.QMessageBox.question(
            self, title, question,
            QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes
                                                  | QtWidgets.QMessageBox.No))
        return bool(answer == QtWidgets.QMessageBox.Yes)

    def get_string_from_user(self, title, label):
        """Gets a string from user

        Args:
            title (str): Input dialog title
            label (str): Input dialog label
        Returns:
            string: String given by user, empty string if user pressed cancel
        """
        answer, ok_pressed = QtWidgets.QInputDialog.getText(
            self, title, label, QtWidgets.QLineEdit.Normal, "")
        if ok_pressed:
            return answer
        return ""

    def show_messagebox(self, title, msg):
        """Shows a messagebox"""
        alert = QtWidgets.QMessageBox()
        alert.setWindowTitle(title)
        alert.setText(msg)
        alert.exec_()