Example #1
0
class SelectDialog(QDialog, Ui_SelectDialog):
    """description of class"""
    def __init__(self,
                 parent,
                 title,
                 text,
                 options,
                 enabledOptions=None,
                 headerOptions=[],
                 singleSelectionMode=True):
        QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(title)
        self.label_text.setText(text)
        self.headerOptions = headerOptions
        self.singleSelectionMode = singleSelectionMode
        self.options = options
        self.initUi(options, enabledOptions, headerOptions,
                    singleSelectionMode)
        self.pushButton_sellectAll.clicked.connect(
            self.pushButton_selectAll_clicked)
        self.pushButton_deSellectAll.clicked.connect(
            self.pushButton_deSelectAll_clicked)
        self.listView_options.clicked.connect(self.listView_options_clicked)

    def initUi(self, options, enabledOptions, headerOptions,
               singleSelectionMode):
        boldFont = QFont()
        boldFont.setBold(True)

        # set the selection mode
        if not singleSelectionMode:
            self.listView_options.setSelectionMode(
                QAbstractItemView.ExtendedSelection)

        # create enableItems if none
        if enabledOptions is None:
            enabledOptions = [True for idx in range(len(options))]

        # Insert the choices
        self.standaredItemModel = QStandardItemModel(self.listView_options)
        self.standaredItemModel.itemChanged.connect(self.onItemChanged)
        for idx in range(len(options)):
            standaredItem = QStandardItem(options[idx])
            standaredItem.setSelectable(enabledOptions[idx])
            if idx in headerOptions:
                standaredItem.setFont(boldFont)
            self.standaredItemModel.appendRow(standaredItem)

        self.listView_options.setModel(self.standaredItemModel)

        # disable select all / de select all buttons if in single selection
        # mode
        if singleSelectionMode:
            self.pushButton_sellectAll.setDisabled(True)
            self.pushButton_deSellectAll.setDisabled(True)

    def onItemChanged(self, item):
        QMessageBox.information(self, "Selec Dialog Message",
                                "Selected Item :" + item.text())

    def selection(self):
        return [
            index.row() for index in
            self.listView_options.selectionModel().selectedIndexes()
        ]

    def pushButton_selectAll_clicked(self):
        selectIndexes = [
            idx for idx in range(self.standaredItemModel.rowCount())
        ]
        self.setSelected(selectIndexes, QItemSelectionModel.Select)

    def pushButton_deSelectAll_clicked(self):
        selectIndexes = [
            idx for idx in range(self.standaredItemModel.rowCount())
        ]
        self.setSelected(selectIndexes, QItemSelectionModel.Deselect)

    def setSelected(self, selectIndexes, newStatus):
        modelIndexes = [
            self.standaredItemModel.createIndex(rowIndex, 0)
            for rowIndex in selectIndexes
        ]
        selectionModel = self.listView_options.selectionModel()
        for modelIndex in modelIndexes:
            selectionModel.select(modelIndex, newStatus)

    @pyqtSlot("QModelIndex")
    def listView_options_clicked(self, modelIndex):
        if self.singleSelectionMode:
            return

        row = modelIndex.row()
        if row in self.headerOptions:
            indexInHeaderOptions = self.headerOptions.index(row)
            if indexInHeaderOptions == len(self.headerOptions) - 1:
                selectedIndexes = [
                    idx for idx in range(row, len(self.options))
                ]
            else:
                selectedIndexes = [
                    idx for idx in range(
                        row, self.headerOptions[indexInHeaderOptions + 1])
                ]
            selectionModel = self.listView_options.selectionModel()
            if modelIndex in selectionModel.selectedIndexes():
                newStatus = QItemSelectionModel.Select
            else:
                newStatus = QItemSelectionModel.Deselect
            self.setSelected(selectedIndexes, newStatus)
Example #2
0
class SelectDialog(QDialog, Ui_SelectDialog):
    """description of class"""

    def __init__(self, parent, title, text, options, enabledOptions=None, headerOptions=[], singleSelectionMode=True):
        QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(title)
        self.label_text.setText(text)
        self.headerOptions = headerOptions
        self.singleSelectionMode = singleSelectionMode
        self.options = options
        self.initUi(options, enabledOptions, headerOptions, singleSelectionMode)
        self.pushButton_sellectAll.clicked.connect(self.pushButton_selectAll_clicked)
        self.pushButton_deSellectAll.clicked.connect(self.pushButton_deSelectAll_clicked)
        self.listView_options.clicked.connect(self.listView_options_clicked)

    def initUi(self, options, enabledOptions, headerOptions, singleSelectionMode):
        boldFont = QFont()
        boldFont.setBold(True)

        # set the selection mode
        if not singleSelectionMode:
            self.listView_options.setSelectionMode(QAbstractItemView.ExtendedSelection)

        # create enableItems if none
        if enabledOptions is None:
            enabledOptions = [True for idx in range(len(options))]

        # Insert the choices
        self.standaredItemModel = QStandardItemModel(self.listView_options)
        self.standaredItemModel.itemChanged.connect(self.onItemChanged)
        for idx in range(len(options)):
            standaredItem = QStandardItem(options[idx])
            standaredItem.setSelectable(enabledOptions[idx])
            if idx in headerOptions:
                standaredItem.setFont(boldFont)
            self.standaredItemModel.appendRow(standaredItem)

        self.listView_options.setModel(self.standaredItemModel)

        # disable select all / de select all buttons if in single selection
        # mode
        if singleSelectionMode:
            self.pushButton_sellectAll.setDisabled(True)
            self.pushButton_deSellectAll.setDisabled(True)

    def onItemChanged(self, item):
        QMessageBox.information(self, "Selec Dialog Message", "Selected Item :" + item.text())

    def selection(self):
        return [index.row() for index in self.listView_options.selectionModel().selectedIndexes()]

    def pushButton_selectAll_clicked(self):
        selectIndexes = [idx for idx in range(self.standaredItemModel.rowCount())]
        self.setSelected(selectIndexes, QItemSelectionModel.Select)

    def pushButton_deSelectAll_clicked(self):
        selectIndexes = [idx for idx in range(self.standaredItemModel.rowCount())]
        self.setSelected(selectIndexes, QItemSelectionModel.Deselect)

    def setSelected(self, selectIndexes, newStatus):
        modelIndexes = [self.standaredItemModel.createIndex(rowIndex, 0) for rowIndex in selectIndexes]
        selectionModel = self.listView_options.selectionModel()
        for modelIndex in modelIndexes:
            selectionModel.select(modelIndex, newStatus)

    @pyqtSlot("QModelIndex")
    def listView_options_clicked(self, modelIndex):
        if self.singleSelectionMode:
            return

        row = modelIndex.row()
        if row in self.headerOptions:
            indexInHeaderOptions = self.headerOptions.index(row)
            if indexInHeaderOptions == len(self.headerOptions) - 1:
                selectedIndexes = [idx for idx in range(row, len(self.options))]
            else:
                selectedIndexes = [idx for idx in range(row, self.headerOptions[indexInHeaderOptions + 1])]
            selectionModel = self.listView_options.selectionModel()
            if modelIndex in selectionModel.selectedIndexes():
                newStatus = QItemSelectionModel.Select
            else:
                newStatus = QItemSelectionModel.Deselect
            self.setSelected(selectedIndexes, newStatus)
class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setupUi(self)
        self.setAttribute(Qt.WA_QuitOnClose)

        geometry = Settings().retrieve_geometry("main")
        if geometry:
            self.restoreGeometry(geometry)
        geometry = Settings().retrieve_geometry("localPanel")
        if geometry:
            self.localFilesTreeView.header().restoreState(geometry)

        self._connection_scanner = ConnectionScanner()
        self._connection = None
        self._root_dir = Settings().root_dir
        self._mcu_files_model = None
        self._mcu_filesize_model = None
        self._terminal = Terminal()
        self._terminal_dialog = None
        self._code_editor = None
        self._flash_dialog = None
        self._settings_dialog = None
        self._about_dialog = None
        self._preset_password = None

        self.actionNavigate.triggered.connect(self.navigate_directory)
        self.actionTerminal.triggered.connect(self.open_terminal)
        self.actionCode_Editor.triggered.connect(self.open_code_editor)
        self.actionUpload.triggered.connect(self.upload_transfer_scripts)
        self.actionFlash.triggered.connect(self.open_flash_dialog)
        self.actionSettings.triggered.connect(self.open_settings_dialog)
        self.actionAbout.triggered.connect(self.open_about_dialog)

        self.lastSelectedConnection = None
        self.connectionComboBox.currentIndexChanged.connect(
            self.connection_changed)
        self.refreshButton.clicked.connect(self.refresh_ports)

        # Populate baud speed combo box and select default
        self.baudComboBox.clear()
        for speed in BaudOptions.speeds:
            self.baudComboBox.addItem(str(speed))
        self.baudComboBox.setCurrentIndex(BaudOptions.speeds.index(115200))

        self.presetButton.clicked.connect(self.show_presets)
        self.connectButton.clicked.connect(self.connect_pressed)

        self.update_file_tree()

        self.listButton.clicked.connect(self.list_mcu_files)
        self.mcuFilesTreeView.clicked.connect(self.mcu_file_selection_changed)
        self.mcuFilesTreeView.doubleClicked.connect(self.read_mcu_file)
        self.mcuFilesTreeView.setRootIsDecorated(False)
        self.mcuFilesTreeView.setSortingEnabled(True)
        self.executeButton.clicked.connect(self.execute_mcu_code)
        self.removeButton.clicked.connect(self.remove_file)
        self.localPathEdit.setText(self._root_dir)

        local_selection_model = self.localFilesTreeView.selectionModel()
        local_selection_model.selectionChanged.connect(
            self.local_file_selection_changed)
        self.localFilesTreeView.doubleClicked.connect(self.open_local_file)

        self.compileButton.clicked.connect(self.compile_files)
        self.update_compile_button()
        self.autoTransferCheckBox.setChecked(Settings().auto_transfer)

        self.transferToMcuButton.clicked.connect(self.transfer_to_mcu)
        self.transferToPcButton.clicked.connect(self.transfer_to_pc)

        self.disconnected()

    def closeEvent(self, event):
        Settings().root_dir = self._root_dir
        Settings().auto_transfer = self.autoTransferCheckBox.isChecked()
        Settings().update_geometry("main", self.saveGeometry())
        Settings().update_geometry(
            "localPanel",
            self.localFilesTreeView.header().saveState())
        Settings().save()
        if self._connection is not None and self._connection.is_connected():
            self.end_connection()
        if self._terminal_dialog:
            self._terminal_dialog.close()
        if self._code_editor:
            self._code_editor.close()
        event.accept()

    def connection_changed(self):
        connection = self._connection_scanner.port_list[
            self.connectionComboBox.currentIndex()]
        self.connectionStackedWidget.setCurrentIndex(1 if connection ==
                                                     "wifi" else 0)
        self.lastSelectedConnection = connection

    def refresh_ports(self):
        # Cache value of last selected connection because it might change when manipulating combobox
        last_selected_connection = self.lastSelectedConnection

        self._connection_scanner.scan_connections(with_wifi=True)
        self.connectionComboBox.clear()

        # Test if there are any available ports
        if self._connection_scanner.port_list:
            selected_port_idx = -1
            pref_port = Settings().preferred_port

            # Populate port combo box and get index of preferred port if available
            for i, port in enumerate(self._connection_scanner.port_list):
                self.connectionComboBox.addItem(port)
                if pref_port and port.upper() == pref_port.upper():
                    selected_port_idx = i

            # Override preferred port if user made selection and this port is still available
            if last_selected_connection and last_selected_connection in self._connection_scanner.port_list:
                selected_port_idx = self._connection_scanner.port_list.index(
                    last_selected_connection)
            # Set current port
            self.connectionComboBox.setCurrentIndex(
                selected_port_idx if selected_port_idx >= 0 else 0)
            self.connectButton.setEnabled(True)
        else:
            self.connectButton.setEnabled(False)

    def set_status(self, status):
        if status == "Connected":
            self.statusLabel.setStyleSheet(
                "QLabel { background-color : none; color : green; font : bold;}"
            )
        elif status == "Disconnected":
            self.statusLabel.setStyleSheet(
                "QLabel { background-color : none; color : red; }")
        elif status == "Connecting...":
            self.statusLabel.setStyleSheet(
                "QLabel { background-color : none; color : blue; }")
        elif status == "Error":
            self.statusLabel.setStyleSheet(
                "QLabel { background-color : red; color : white; }")
        elif status == "Password":
            self.statusLabel.setStyleSheet(
                "QLabel { background-color : red; color : white; }")
            status = "Wrong Password"
        elif status == "Host":
            self.statusLabel.setStyleSheet(
                "QLabel { background-color : red; color : white; }")
            status = "Invalid IP or domain"
        else:
            self.statusLabel.setStyleSheet(
                "QLabel { background-color : red; color : white; }")
        self.statusLabel.setText(status)
        QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)

    def update_compile_button(self):
        self.compileButton.setEnabled(
            bool(Settings().mpy_cross_path)
            and len(self.get_local_file_selection()) > 0)

    def disconnected(self):
        self.connectButton.setText("Connect")
        self.set_status("Disconnected")
        self.listButton.setEnabled(False)
        self.connectionComboBox.setEnabled(True)
        self.baudComboBox.setEnabled(True)
        self.refreshButton.setEnabled(True)
        self.mcuFilesTreeView.setEnabled(False)
        self.executeButton.setEnabled(False)
        self.removeButton.setEnabled(False)
        self.actionTerminal.setEnabled(False)
        self.actionUpload.setEnabled(False)
        self.transferToMcuButton.setEnabled(False)
        self.transferToPcButton.setEnabled(False)
        # Clear terminal on disconnect
        self._terminal.clear()
        if self._terminal_dialog:
            self._terminal_dialog.close()
        if self._code_editor:
            self._code_editor.disconnected()
        self.refresh_ports()

    def connected(self):
        self.connectButton.setText("Disconnect")
        self.set_status("Connected")
        self.listButton.setEnabled(True)
        self.connectionComboBox.setEnabled(False)
        self.baudComboBox.setEnabled(False)
        self.refreshButton.setEnabled(False)
        self.mcuFilesTreeView.setEnabled(True)
        self.actionTerminal.setEnabled(True)
        if isinstance(self._connection, SerialConnection):
            self.actionUpload.setEnabled(True)
        self.transferToMcuButton.setEnabled(True)
        if self._code_editor:
            self._code_editor.connected(self._connection)
        self.list_mcu_files()

    def navigate_directory(self):
        dialog = QFileDialog()
        dialog.setDirectory(self._root_dir)
        dialog.setFileMode(QFileDialog.Directory)
        dialog.setOption(QFileDialog.ShowDirsOnly)
        dialog.exec()
        path = dialog.selectedFiles()
        if path and path[0]:
            self._root_dir = path[0]
            self.localPathEdit.setText(self._root_dir)
            self.update_file_tree()

    def update_file_tree(self):
        model = QFileSystemModel()
        model.setRootPath(self._root_dir)
        self.localFilesTreeView.setModel(model)
        local_selection_model = self.localFilesTreeView.selectionModel()
        local_selection_model.selectionChanged.connect(
            self.local_file_selection_changed)
        self.localFilesTreeView.setRootIndex(model.index(self._root_dir))

    def serial_mcu_connection_valid(self):
        try:
            self._connection.list_files()
            return True
        except OperationError:
            return False

    def list_mcu_files(self):
        file_list = []
        try:
            file_list = self._connection.list_files()
        except OperationError:
            QMessageBox().critical(self, "Operation failed",
                                   "Could not list files.", QMessageBox.Ok)
            return
        try:
            file_sizes = self._connection.list_file_sizes()
        except OperationError:
            QMessageBox().critical(self, "Operation failed",
                                   "Could not list files.", QMessageBox.Ok)
            return
        lmax = 0
        for fn in file_list:
            if len(fn) > lmax:
                lmax = len(fn)
        print(lmax)
        print(file_sizes)
        numFiles = len(file_sizes)
        self._mcu_files_model = QStandardItemModel()
        self._mcu_filesize_model = QStandardItemModel()

        self._mcu_files_model.setHorizontalHeaderLabels(
            ['name', 'size', 'type'])

        for (file, fs, i) in zip(file_list, file_sizes, range(numFiles)):
            QApplication.processEvents()
            idx = self._mcu_files_model.rowCount()
            item0 = QStandardItem(file)
            item1 = QStandardItem(str('{:10}'.format(fs)))
            fields = file.split('.')
            if len(fields) > 0:
                item2 = QStandardItem(fields[-1])
            else:
                item2 = QStandardItem(' ')
            item1.setTextAlignment(Qt.AlignRight)
            self._mcu_files_model.setItem(idx, 0, item0)
            self._mcu_files_model.setItem(idx, 1, item1)
            self._mcu_files_model.setItem(idx, 2, item2)
            self._mcu_files_model.setData(
                self._mcu_files_model.createIndex(idx, 1), Qt.AlignRight,
                Qt.TextAlignmentRole)
            self._mcu_files_model.setData(
                self._mcu_files_model.createIndex(idx, 1), Qt.AlignLeft,
                Qt.TextAlignmentRole)
            # self._mcu_files_model.insertRow(idx)
            # self._mcu_filesize_model.insertRow(idx)
            # self._mcu_files_model.setData(self._mcu_files_model.index(idx), file)
            # self._mcu_filesize_model.setData(self._mcu_filesize_model.index(idx),str(fs))

        self.mcuFilesTreeView.setModel(self._mcu_files_model)
        self.mcuFilesTreeView.setAllColumnsShowFocus(True)
        self.mcuFilesTreeView.header().setStretchLastSection(False)
        self.mcuFilesTreeView.resizeColumnToContents(0)
        self.mcuFilesTreeView.resizeColumnToContents(1)
        self.mcuFilesTreeView.resizeColumnToContents(2)
        # self.mcuFilesTreeView.horizontalHeader.setDefaultAlignment(Qt.AlignRight)
        self.mcuFilesTreeView.setSelectionMode(
            QAbstractItemView.MultiSelection)
        # self.size_view.setModel(self._mcu_filesize_model)
        self.mcu_file_selection_changed()

    def execute_mcu_code(self):
        idx = self.mcuFilesTreeView.currentIndex()
        assert isinstance(idx, QModelIndex)
        model = self.mcuFilesTreeView.model()
        # assert isinstance(model, QStringListModel)
        file_name = model.data(idx, 0)
        print(file_name)
        self._connection.run_file(file_name)

    def remove_file(self):
        file_names = self.get_remote_file_selection()
        for fn in file_names:
            try:
                self._connection.remove_file(fn)
            except OperationError:
                QMessageBox().critical(self, "Operation failed",
                                       "Could not remove the file.",
                                       QMessageBox.Ok)
                return
        self.list_mcu_files()

    def ask_for_password(self, title, label="Password"):
        if self._preset_password is not None:
            return self._preset_password

        input_dlg = QInputDialog(parent=self, flags=Qt.Dialog)
        input_dlg.setTextEchoMode(QLineEdit.Password)
        input_dlg.setWindowTitle(title)
        input_dlg.setLabelText(label)
        input_dlg.resize(500, 100)
        input_dlg.exec()
        return input_dlg.textValue()

    def start_connection(self):
        self.set_status("Connecting...")

        connection = self._connection_scanner.port_list[
            self.connectionComboBox.currentIndex()]

        if connection == "wifi":
            host = self.addressLineEdit.text()
            port = self.portSpinBox.value()

            try:
                self._connection = WifiConnection(host, port, self._terminal,
                                                  self.ask_for_password)
            except ConnectionError:
                # Do nothing, _connection will be None and code
                # at the end of function will handle this
                pass
            except PasswordException:
                self.set_status("Password")
                return
            except HostnameResolutionError:
                self.set_status("Host")
                return
            except NewPasswordException:
                QMessageBox().information(
                    self, "Password set",
                    "WebREPL password was not previously configured, so it was set to "
                    "\"passw\" (without quotes). "
                    "You can change it in port_config.py (will require reboot to take effect). "
                    "Caution: Passwords longer than 9 characters will be truncated.\n\n"
                    "Continue by connecting again.", QMessageBox.Ok)
                return
        else:
            baud_rate = BaudOptions.speeds[self.baudComboBox.currentIndex()]
            self._connection = SerialConnection(
                connection, baud_rate, self._terminal,
                self.serialResetCheckBox.isChecked())
            if self._connection.is_connected():
                if not self.serial_mcu_connection_valid():
                    self._connection.disconnect()
                    self._connection = None
            else:
                # serial connection didn't work, so likely the unplugged the serial device and COM value is stale
                self.refresh_ports()

        if self._connection is not None and self._connection.is_connected():
            self.connected()
            if isinstance(self._connection, SerialConnection):
                if Settings(
                ).use_transfer_scripts and not self._connection.check_transfer_scripts_version(
                ):
                    QMessageBox.warning(
                        self, "Transfer scripts problem",
                        "Transfer scripts for UART are either"
                        " missing or have wrong version.\nPlease use 'File->Init transfer files' to"
                        " fix this issue.")
        else:
            self._connection = None
            self.set_status("Error")
            self.refresh_ports()

    def end_connection(self):
        self._connection.disconnect()
        self._connection = None

        self.disconnected()

    def show_presets(self):
        dialog = WiFiPresetDialog()
        dialog.accepted.connect(
            lambda: self.use_preset(dialog.selected_ip, dialog.selected_port,
                                    dialog.selected_password))
        dialog.exec()

    def use_preset(self, ip, port, password):
        self.addressLineEdit.setText(ip)
        self.portSpinBox.setValue(port)
        self._preset_password = password

    def connect_pressed(self):
        if self._connection is not None and self._connection.is_connected():
            self.end_connection()
        else:
            self.start_connection()

    def run_file(self):
        content = self.codeEdit.toPlainText()
        self._connection.send_block(content)

    def open_local_file(self, idx):
        assert isinstance(idx, QModelIndex)
        model = self.localFilesTreeView.model()
        assert isinstance(model, QFileSystemModel)

        if model.isDir(idx):
            return

        local_path = model.filePath(idx)
        remote_path = local_path.rsplit("/", 1)[1]

        if Settings().external_editor_path:
            self.open_external_editor(local_path)
        else:
            if FileInfo.is_file_binary(local_path):
                QMessageBox.information(
                    self, "Binary file detected",
                    "Editor doesn't support binary files.")
                return
            with open(local_path) as f:
                text = "".join(f.readlines())
                self.open_code_editor()
                self._code_editor.set_code(local_path, remote_path, text)

    def mcu_file_selection_changed(self):
        idx = self.mcuFilesTreeView.currentIndex()
        assert isinstance(idx, QModelIndex)
        if idx.row() >= 0:
            self.executeButton.setEnabled(True)
            self.removeButton.setEnabled(True)
            self.transferToPcButton.setEnabled(True)
        else:
            self.executeButton.setEnabled(False)
            self.removeButton.setEnabled(False)
            self.transferToPcButton.setEnabled(False)

    def get_local_file_selection(self):
        """Returns absolute paths for selected local files"""
        indices = self.localFilesTreeView.selectedIndexes()
        model = self.localFilesTreeView.model()
        assert isinstance(model, QFileSystemModel)

        def filter_indices(x):
            return x.column() == 0 and not model.isDir(x)

        # Filter out all but first column (file name) and
        # don't include directories
        indices = [x for x in indices if filter_indices(x)]

        # Return absolute paths
        return [model.filePath(idx) for idx in indices]

    def local_file_selection_changed(self):
        self.update_compile_button()
        local_file_paths = self.get_local_file_selection()
        if len(local_file_paths) == 1:
            self.remoteNameEdit.setText(local_file_paths[0].rsplit("/", 1)[1])
        else:
            self.remoteNameEdit.setText("")

    def compile_files(self):
        local_file_paths = self.get_local_file_selection()
        compiled_file_paths = []

        for local_path in local_file_paths:
            split = os.path.splitext(local_path)
            if split[1] == ".mpy":
                title = "COMPILE WARNING!! " + os.path.basename(local_path)
                QMessageBox.warning(
                    self, title, "Can't compile .mpy files, already bytecode")
                continue
            mpy_path = split[0] + ".mpy"

            try:
                os.remove(mpy_path)
                # Force view to update itself so that it sees file removed
                self.localFilesTreeView.repaint()
                QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
            except OSError:
                pass

            try:
                with subprocess.Popen(
                    [Settings().mpy_cross_path,
                     os.path.basename(local_path)],
                        cwd=os.path.dirname(local_path),
                        stdout=subprocess.PIPE,
                        stdin=subprocess.PIPE,
                        stderr=subprocess.PIPE) as proc:
                    proc.wait()  # Wait for process to finish
                    out = proc.stderr.read()
                    if out:
                        QMessageBox.warning(self, "Compilation error",
                                            out.decode("utf-8"))
                        continue

            except OSError:
                QMessageBox.warning(self, "Compilation error",
                                    "Failed to run mpy-cross")
                continue

            compiled_file_paths += [mpy_path]

        # Force view to update so that it sees compiled files added
        self.localFilesTreeView.repaint()
        QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)

        # Force view to update last time so that it resorts the content
        # Without this, the new file will be placed at the bottom of the view no matter what
        header = self.localFilesTreeView.header()
        column = header.sortIndicatorSection()
        order = header.sortIndicatorOrder()
        # This is necessary so that view actually sorts anything again
        self.localFilesTreeView.sortByColumn(-1, Qt.AscendingOrder)
        QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
        self.localFilesTreeView.sortByColumn(column, order)

        selection_model = self.localFilesTreeView.selectionModel()
        if compiled_file_paths:
            assert isinstance(selection_model, QItemSelectionModel)
            selection_model.clearSelection()

        for mpy_path in compiled_file_paths:
            idx = self.localFilesTreeView.model().index(mpy_path)
            selection_model.select(
                idx, QItemSelectionModel.Select | QItemSelectionModel.Rows)

        if (self.autoTransferCheckBox.isChecked() and self._connection
                and self._connection.is_connected() and compiled_file_paths):
            self.transfer_to_mcu()

    def finished_read_mcu_file(self, file_name, transfer):
        assert isinstance(transfer, FileTransfer)
        result = transfer.read_result

        if result.binary_data:
            try:
                text = result.binary_data.decode("utf-8", "strict")
            except UnicodeDecodeError:
                QMessageBox.information(
                    self, "Binary file detected",
                    "Editor doesn't support binary files, "
                    "but these can still be transferred.")
                return
        else:
            text = "! Failed to read file !"

        self.open_code_editor()
        self._code_editor.set_code(None, file_name, text)

    def read_mcu_file(self, idx):
        assert isinstance(idx, QModelIndex)
        model = self.mcuFilesTreeView.model()
        # assert isinstance(model, QStringListModel)
        # file_name = model.data(idx, 0)
        file_name = model.itemFromIndex(model.index(idx.row(), 0)).text()
        progress_dlg = FileTransferDialog(FileTransferDialog.DOWNLOAD)
        progress_dlg.finished.connect(lambda: self.finished_read_mcu_file(
            file_name, progress_dlg.transfer))
        progress_dlg.show()
        self._connection.read_file(file_name, progress_dlg.transfer)

    def upload_transfer_scripts(self):
        progress_dlg = FileTransferDialog(FileTransferDialog.UPLOAD)
        progress_dlg.finished.connect(self.list_mcu_files)
        progress_dlg.show()
        self._connection.upload_transfer_files(progress_dlg.transfer)

    def transfer_to_mcu(self):
        local_file_paths = self.get_local_file_selection()

        progress_dlg = FileTransferDialog(FileTransferDialog.UPLOAD)
        progress_dlg.finished.connect(self.list_mcu_files)
        progress_dlg.show()

        # Handle single file transfer
        if len(local_file_paths) == 1:
            local_path = local_file_paths[0]
            remote_path = self.remoteNameEdit.text()
            with open(local_path, "rb") as f:
                content = f.read()
            self._connection.write_file(remote_path, content,
                                        progress_dlg.transfer)
            return

        # Batch file transfer
        progress_dlg.enable_cancel()
        progress_dlg.transfer.set_file_count(len(local_file_paths))
        self._connection.write_files(local_file_paths, progress_dlg.transfer)

    def finished_transfer_to_pc(self, file_path, transfer):
        if not transfer.read_result.binary_data:
            return

        try:
            with open(file_path, "wb") as file:
                file.write(transfer.read_result.binary_data)
        except IOError:
            QMessageBox.critical(
                self, "Save operation failed",
                "Couldn't save the file. Check path and permissions.")

    def get_remote_file_selection(self):
        selected_indices = self.mcuFilesTreeView.selectedIndexes()
        file_names = []
        local_paths = []
        for idx in selected_indices:
            # idx = self.mcuFilesTreeView.currentIndex()
            assert isinstance(idx, QModelIndex)
            model = self.mcuFilesTreeView.model()
            # assert isinstance(model, QStringListModel)
            if (idx.column() == 0):
                fn = model.itemFromIndex(idx).text()
                file_names.append(fn)
        return file_names

    def transfer_to_pc(self):
        file_names = self.get_remote_file_selection()
        local_paths = []
        for fn in file_names:
            local_paths.append(self.localPathEdit.text() + "/" + fn)

        callback = self.finished_transfer_to_pc

        progress_dlg = FileTransferDialog(FileTransferDialog.DOWNLOAD)
        # progress_dlg.finished.connect(self.list_mcu_files)
        progress_dlg.show()

        if len(file_names) == 1:
            local_path = self.localPathEdit.text() + "/" + file_names[0]
            progress_dlg.finished.connect(lambda: self.finished_transfer_to_pc(
                local_path, progress_dlg.transfer))
            self._connection.read_file(file_names[0], progress_dlg.transfer)
            return

        # Batch file transfer
        progress_dlg.enable_cancel()
        print(len(file_names))
        progress_dlg.transfer.set_file_count(len(file_names))
        self._connection.read_files(file_names, local_paths,
                                    progress_dlg.transfer, callback)

        # for idx in selected_indices:
        #     idx = self.mcuFilesTreeView.currentIndex()
        #     assert isinstance(idx, QModelIndex)
        #     model = self.mcuFilesTreeView.model()
        #     # assert isinstance(model, QStringListModel)
        #     remote_path = model.data(idx,0)
        #     print(remote_path)
        #     local_path = self.localPathEdit.text() + "/" + remote_path

        #     progress_dlg = FileTransferDialog(FileTransferDialog.DOWNLOAD)
        #     progress_dlg.finished.connect(lambda: self.finished_transfer_to_pc(local_path, progress_dlg.transfer))
        #     progress_dlg.show()
        #     self._connection.read_file(remote_path, progress_dlg.transfer)

    def open_terminal(self):
        if self._terminal_dialog is not None:
            return
        self._terminal_dialog = TerminalDialog(self, self._connection,
                                               self._terminal)
        self._terminal_dialog.finished.connect(self.close_terminal)
        self._terminal_dialog.show()

    def close_terminal(self):
        self._terminal_dialog = None

    def open_external_editor(self, file_path):
        ext_path = Settings().external_editor_path
        ext_args = []
        if Settings().external_editor_args:

            def wildcard_replace(s):
                s = s.replace("%f", file_path)
                return s

            ext_args = [
                wildcard_replace(x.strip())
                for x in Settings().external_editor_args.split(";")
            ]

        subprocess.Popen([ext_path] + ext_args)

    def open_code_editor(self):
        if self._code_editor is not None:
            return

        self._code_editor = CodeEditDialog(self, self._connection)
        self._code_editor.mcu_file_saved.connect(self.list_mcu_files)
        self._code_editor.finished.connect(self.close_code_editor)
        self._code_editor.show()

    def close_code_editor(self):
        self._code_editor = None

    def open_flash_dialog(self):
        if self._connection is not None and self._connection.is_connected():
            self.end_connection()

        self._flash_dialog = FlashDialog(self)
        self._flash_dialog.finished.connect(self.close_flash_dialog)
        self._flash_dialog.show()

    def close_flash_dialog(self):
        self._flash_dialog = None

    def open_settings_dialog(self):
        if self._settings_dialog is not None:
            return
        self._settings_dialog = SettingsDialog(self)
        self._settings_dialog.finished.connect(self.close_settings_dialog)
        self._settings_dialog.show()

    def close_settings_dialog(self):
        self._settings_dialog = None
        # Update compile button as mpy-cross path might have been set
        self.update_compile_button()

    def open_about_dialog(self):
        if self._about_dialog is not None:
            return
        self._settings_dialog = AboutDialog(self)
        self._settings_dialog.finished.connect(self.close_about_dialog)
        self._settings_dialog.show()

    def close_about_dialog(self):
        self._about_dialog = None
class OrderBookDialog(QtWidgets.QDialog, Ui_OrderBookDialog):
    # Class data
    cutoff = None  # Number of asks and bids to display
    width = None  # Width of data fields
    btcusdWorker = None  # Worker for updating btcusd data
    btcusdThread = QThread()  # Thread for worker
    ethusdWorker = None  # Worker for updating ethusd data
    ethusdThread = QThread()  # Thread for worker
    ethbtcWorker = None  # Worker for updating ethbtc data
    ethbtcThread = QThread()  # Thread for worker
    btcusdModel = None  # Model for displaying btcusd data
    btcusdModelIndex = None  # Model index for centering data around spread
    ethusdModel = None  # Model for displaying ethusd data
    ethusdModelIndex = None  # Model index for centering data around spread
    ethbtcModel = None  # Model for displaying ethbtc data
    ethbtcModelIndex = None  # Model index for centering data around spread

    # Initializer
    def __init__(self, parent, cutoff=9, width=15):
        super(OrderBookDialog, self).__init__(parent)
        self.cutoff = cutoff
        self.width = width
        self.initUI()
        self.buildThreads()
        self.startThreads()

    # Initialize UI
    def initUI(self):
        self.setupUi(self)
        self.btcusdModel = QStandardItemModel(self.btcusdListView)
        self.btcusdModelIndex = QModelIndex()
        self.btcusdModelIndex = self.btcusdModel.createIndex(
            self.cutoff + 1, 0)
        self.btcusdListView.setModel(self.btcusdModel)
        self.ethusdModel = QStandardItemModel(self.ethusdListView)
        self.ethusdModelIndex = QModelIndex()
        self.ethusdModelIndex = self.ethusdModel.createIndex(
            self.cutoff + 1, 0)
        self.ethusdListView.setModel(self.ethusdModel)
        self.ethbtcModel = QStandardItemModel(self.ethbtcListView)
        self.ethbtcModelIndex = QModelIndex()
        self.ethbtcModelIndex = self.ethbtcModel.createIndex(
            self.cutoff + 1, 0)
        self.ethbtcListView.setModel(self.ethbtcModel)

        # Set fixed-width font
        fixedFont = QFontDatabase.systemFont(1)
        self.btcusdListView.setFont(fixedFont)
        self.ethusdListView.setFont(fixedFont)
        self.ethbtcListView.setFont(fixedFont)

        # Connect actions
        self.closeButton.clicked.connect(self.close)

    # Build threads
    ############################################################################
    def buildThreads(self):
        self.btcusdWorker = Worker('BTCUSD', self.cutoff, self.width)
        self.btcusdWorker.moveToThread(self.btcusdThread)
        self.btcusdWorker.dataReady.connect(self.updateGui)
        self.btcusdThread.started.connect(self.btcusdWorker.work)

        self.ethusdWorker = Worker('ETHUSD', self.cutoff, self.width)
        self.ethusdWorker.moveToThread(self.ethusdThread)
        self.ethusdWorker.dataReady.connect(self.updateGui)
        self.ethusdThread.started.connect(self.ethusdWorker.work)

        self.ethbtcWorker = Worker('ETHBTC', self.cutoff, self.width)
        self.ethbtcWorker.moveToThread(self.ethbtcThread)
        self.ethbtcWorker.dataReady.connect(self.updateGui)
        self.ethbtcThread.started.connect(self.ethbtcWorker.work)

    # Start threads
    ############################################################################
    def startThreads(self):
        self.btcusdThread.start()
        self.ethusdThread.start()
        self.ethbtcThread.start()

    # Updates QListViews
    ############################################################################
    @pyqtSlot(str, list)
    def updateGui(self, flag: str, stringList: list):
        if flag == 'BTCUSD':
            self.btcusdModel.clear()
            for string in stringList:
                item = QStandardItem()
                item.setText(string)
                self.btcusdModel.appendRow(item)
            self.btcusdListView.setCurrentIndex(self.btcusdModelIndex)
            self.btcusdListView.scrollTo(self.btcusdModelIndex, 3)
        elif flag == 'ETHUSD':
            self.ethusdModel.clear()
            for string in stringList:
                item = QStandardItem()
                item.setText(string)
                self.ethusdModel.appendRow(item)
            self.ethusdListView.setCurrentIndex(self.ethusdModelIndex)
            self.ethusdListView.scrollTo(self.ethusdModelIndex, 3)
        elif flag == 'ETHBTC':
            self.ethbtcModel.clear()
            for string in stringList:
                item = QStandardItem()
                item.setText(string)
                self.ethbtcModel.appendRow(item)
            self.ethbtcListView.setCurrentIndex(self.ethbtcModelIndex)
            self.ethbtcListView.scrollTo(self.ethbtcModelIndex, 3)

    # When user closes order book, stop threads
    ############################################################################
    def closeEvent(self, event):
        self.btcusdWorker.stopWork()
        self.ethusdWorker.stopWork()
        self.ethbtcWorker.stopWork()
        self.btcusdThread.quit()
        self.ethusdThread.quit()
        self.ethbtcThread.quit()