コード例 #1
0
class InstallPage(QWidget):
    """Settings page for the installed plugins"""
    def __init__(self, parent, repo):
        """QWidget Dictionary -> Void
        Consumes the parent and the repository dictionary and sets up the
        install page in the settings area"""
        QWidget.__init__(self, parent)
        self._userPlugins = helper.getPlugins()
        self._repo = repo

        # Add a scrollArea that if they are more plugins that fit into the
        # settings page
        scrollArea = QScrollArea(self)
        scrollArea.setWidgetResizable(True)
        scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        baseLayout = QVBoxLayout()
        baseLayout.setAlignment(Qt.AlignTop)
        self.setLayout(baseLayout)
        baseWidget = QWidget()
        scrollArea.setWidget(baseWidget)
        baseLayout.addWidget(scrollArea)

        self._vbox = QVBoxLayout()
        baseWidget.setLayout(self._vbox)

    def update(self, userPlugins):
        """ListOfUserpluginEntry -> Void
        Consume a list of UserpluginEntry and repopulates the install page"""
        for i in reversed(range(self._vbox.count())):
            try:
                self._vbox.itemAt(i).widget().setParent(None)
            except AttributeError as e:
                qWarning("Can't call setParent of None type")

        labelText = "<h2>Install Plugins</h2>"
        if (len(self._repo["plugins"]) < 1):
            labelText += "<p>It seems we could not load the plugin repository.</p>"
            labelText += "<p style='color:red'>Make shure your internet connection is working and restart Enki.</p><p></p>"
        self._vbox.addWidget(QLabel(labelText))

        for entry in self._repo["plugins"]:
            isInstalled = helper.isPluginInstalled(entry["name"], userPlugins)
            if isInstalled:
                self._vbox.addWidget(pluginspage.PluginTitlecard(isInstalled))
            else:
                self._vbox.addWidget(InstallableTitlecard(entry, self))

    def addPluginToUserPlugins(self, installableTitlecard):
        """InstallableTitlecard -> Void
        Consumes an InstallableTitlecard and insert an PluginTitleCard instead
        of itself"""
        index = self._vbox.indexOf(installableTitlecard)
        name = installableTitlecard.modulename()
        pluginEntry = helper.initPlugin(name)

        if pluginEntry:
            self._userPlugins.append(pluginEntry)
            self._vbox.insertWidget(index,
                                    pluginspage.PluginTitlecard(pluginEntry))
コード例 #2
0
ファイル: installpage.py プロジェクト: bjones1/enki
class InstallPage(QWidget):
    """Settings page for the installed plugins"""
    def __init__(self, parent, repo):
        """QWidget Dictionary -> Void
        Consumes the parent and the repository dictionary and sets up the
        install page in the settings area"""
        QWidget.__init__(self, parent)
        self._userPlugins = helper.getPlugins()
        self._repo = repo

        # Add a scrollArea that if they are more plugins that fit into the
        # settings page
        scrollArea = QScrollArea(self)
        scrollArea.setWidgetResizable(True)
        scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        baseLayout = QVBoxLayout()
        baseLayout.setAlignment(Qt.AlignTop)
        self.setLayout(baseLayout)
        baseWidget = QWidget()
        scrollArea.setWidget(baseWidget)
        baseLayout.addWidget(scrollArea)

        self._vbox = QVBoxLayout()
        baseWidget.setLayout(self._vbox)

    def update(self, userPlugins):
        """ListOfUserpluginEntry -> Void
        Consume a list of UserpluginEntry and repopulates the install page"""
        for i in reversed(range(self._vbox.count())):
            try:
                self._vbox.itemAt(i).widget().setParent(None)
            except AttributeError as e:
                qWarning("Can't call setParent of None type")

        labelText = "<h2>Install Plugins</h2>"
        if (len(self._repo["plugins"]) < 1):
            labelText += "<p>It seems we could not load the plugin repository.</p>"
            labelText += "<p style='color:red'>Make shure your internet connection is working and restart Enki.</p><p></p>"
        self._vbox.addWidget(QLabel(labelText))

        for entry in self._repo["plugins"]:
            isInstalled = helper.isPluginInstalled(entry["name"], userPlugins)
            if isInstalled:
                self._vbox.addWidget(pluginspage.PluginTitlecard(isInstalled))
            else:
                self._vbox.addWidget(InstallableTitlecard(entry, self))

    def addPluginToUserPlugins(self, installableTitlecard):
        """InstallableTitlecard -> Void
        Consumes an InstallableTitlecard and insert an PluginTitleCard instead
        of itself"""
        index = self._vbox.indexOf(installableTitlecard)
        name = installableTitlecard.modulename()
        pluginEntry = helper.initPlugin(name)

        if pluginEntry:
            self._userPlugins.append(pluginEntry)
            self._vbox.insertWidget(index,
                                    pluginspage.PluginTitlecard(pluginEntry))
コード例 #3
0
    def _create_rule_widget(self, layout: QVBoxLayout,
                            rule: Rule) -> RuleWidget:
        rule_widget = RuleWidget(rule)
        rule_widget.register_callbacks(
            lambda: layout.insertWidget(
                layout.indexOf(rule_widget),
                self._create_rule_widget(layout, Rule({"type": "app"}))),
            lambda: rule_widget.remove_from(layout))

        return rule_widget
コード例 #4
0
ファイル: QScrollableBox.py プロジェクト: lihaochen910/Candy
class QScrollableBox(QWidget):
    def __init__(self, parent):
        super(QScrollableBox, self).__init__(parent)
        mainLayout = QVBoxLayout()
        mainLayout.setContentsMargins(0, 0, 0, 0)
        self.scrollArea = QScrollArea(self)
        sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        self.scrollArea.setSizePolicy(sizePolicy)
        self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.scrollArea.setWidgetResizable(True)

        mainLayout.addWidget(self.scrollArea)
        self.setLayout(mainLayout)
        scrollContents = QWidget()
        self.m_layout = QVBoxLayout()
        self.m_layout.setContentsMargins(0, 0, 0, 0)
        self.m_layout.setSizeConstraint(QLayout.SetNoConstraint)
        scrollContents.setLayout(self.m_layout)

        self.scrollArea.setWidget(scrollContents)

    def addWidget(self, w):
        if not w:
            return
        count = self.m_layout.count()
        if count > 1:
            self.m_layout.removeItem(self.m_layout.itemAt(count - 1))
        self.m_layout.addWidget(w)
        w.show()
        self.m_layout.addStretch()
        self.scrollArea.update()

    def removeWidget(self, w):
        self.m_layout.removeWidget(w)
        self.scrollArea.update()

    def insertWidget(self, i, w):
        self.m_layout.insertWidget(i, w)
        self.scrollArea.update()

    def clearWidgets(self):
        item = self.m_layout.takeAt(0)
        while item != None:
            item.widget().deleteLater()
            self.m_layout.removeItem(item)
            del item

        self.scrollArea.update()

    def indexOf(self, w):
        return self.m_layout.indexOf(w)
コード例 #5
0
class BasePage(QWidget):
    finished = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self._pager = None
        self.nextEnabled = True
        self.previousEnabled = True
        self.layout = QVBoxLayout(self)
        self.layout.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.setSizePolicy(
            QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored))

        self.setStyleSheet("QLabel {font: 20pt}")

        self.labels = []
        self.picture = None

    @pyqtSlot()
    def onEnter(self):
        pass

    def setPager(self, pager):
        self._pager = pager

    def getButtonHeight(self):
        if self._pager is not None:
            return self._pager.getButtonHeight()
        else:
            return 100

    @pyqtSlot()
    def onExit(self):
        pass

    def getPictureSize(self):
        s = self.size() - QSize(0, self.getButtonHeight())
        for l in self.labels:
            s -= QSize(0, l.size().height())
        return QSize(max(400, s.width()), max(400, s.height()))

    def resizeEvent(self, event):
        if self.picture is not None:
            i = self.layout.indexOf(self.picture)
            self.layout.removeWidget(self.picture)
            self.picture.setParent(None)
            self.picture = QLabel(self)
            self.picture.setPixmap(
                self.pixMap.scaled(self.getPictureSize(), Qt.KeepAspectRatio))
            self.layout.insertWidget(i, self.picture)
コード例 #6
0
class QImageGrids(QWidget):

    # signals
    focusChanged = pyqtSignal(QPixmap)

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

        self.VBoxLayout = QVBoxLayout()
        self.setLayout(self.VBoxLayout)
        self.VBoxLayout.addStretch()

        self.setBackgroundRole(QPalette.Light)

        self._focusItemIndex = 0

    def add(self, imgPath, imgBasePath=None):
        self.insert(self.VBoxLayout.count() - 1, imgPath, imgBasePath)

    def insert(self, index, imgPath, imgBasePath=None):

        imgGrid = QImageGrid(imgPath)

        if imgBasePath is not None:
            imgGrid.baseImgPath = imgBasePath

        self.VBoxLayout.insertWidget(index, imgGrid)

        self.connectGridSignals(imgGrid)

    def count(self):
        return self.VBoxLayout.count() - 1

    def connectGridSignals(self, grid):
        for img in grid.children():
            if isinstance(img, QImageLabel):
                img.clicked.connect(self.imageClicked)

    def removeFocusedGrid(self):
        # TODO
        grid = self.getFocusedGrid(
        )  # .VBoxLayout.itemAt(self._focusItemIndex)
        if isinstance(grid, QImageGrid):
            self.VBoxLayout.removeWidget(grid)
            grid.deleteLater()
            try:
                self.moveGridFocusUp()
            except MoveGridFocusError:
                pass
            finally:
                newGrid = self.getFocusedGrid()
                if newGrid is not None:
                    newGrid.reFocus()
                    self.emitFocusChanged()

    def reloadFocusedGrid(self):

        index = self._focusItemIndex
        grid = self.getFocusedGrid()
        row = grid._focusItemRow
        col = grid._focusItemColumn
        baseImgPath = grid.baseImgPath

        self.removeFocusedGrid()
        self.getFocusedGrid().clearFocusItem()
        self.insert(index, baseImgPath)

        self._focusItemIndex = index
        self.getFocusedGrid().setFocusItem(row, col)
        self.emitFocusChanged()

    def getFocusedGrid(self) -> QImageGrid:
        imgGridItem = self.VBoxLayout.itemAt(self._focusItemIndex)
        if imgGridItem is None:
            return None
        else:
            return imgGridItem.widget()

    @pyqtSlot()
    def imageClicked(self):

        # clear the currently selected image
        oldGrid = self.getFocusedGrid()
        if oldGrid is not None:
            oldGrid.clearFocusItem()

        # focus on the new image
        imgLabel = self.sender()
        self._focusItemIndex = self.VBoxLayout.indexOf(imgLabel.parent())
        self.getFocusedGrid().setFocusWidget(imgLabel)
        imgLabel.clicked.connect(self.emitFocusChanged)

    def emitFocusChanged(self):
        pixmap = self.getFocusedGrid().getFocusWidget().pixmap()
        self.focusChanged.emit(pixmap)

    def moveGridFocusDown(self):
        # only shift if we're not already at the bottom
        if not self._focusItemIndex == self.VBoxLayout.count() - 2:
            self._focusItemIndex += 1
        else:
            raise MoveGridFocusError(
                self._focusItemIndex, self._focusItemIndex + 1,
                f'Grid {self._focusItemIndex + 1} does not exist')

    def moveGridFocusUp(self):
        # only shift if we're not already at the top
        if not self._focusItemIndex == 0:
            self._focusItemIndex -= 1
        else:
            raise MoveGridFocusError(self._focusItemIndex,
                                     self._focusItemIndex - 1,
                                     'Grid "-1" does not exist')

    def moveItemFocusDown(self):
        grid = self.getFocusedGrid()
        try:
            grid.moveFocusDown()
        except MoveGridItemFocusError as e:

            try:
                self.moveGridFocusDown()
            except MoveGridFocusError:
                pass
            else:
                newGrid = self.getFocusedGrid()
                newGrid.setFocusItem(0, e.column)
                grid.clearFocusItem()
                self.emitFocusChanged()

        else:
            self.emitFocusChanged()

    def moveItemFocusUp(self):
        grid = self.getFocusedGrid()
        try:
            grid.moveFocusUp()
        except MoveGridItemFocusError as e:

            try:
                self.moveGridFocusUp()
            except MoveGridFocusError:
                pass
            else:
                newGrid = self.getFocusedGrid()
                newGrid.setFocusItem(newGrid.rows - 1, e.column)
                grid.clearFocusItem()
                self.emitFocusChanged()

        else:
            self.emitFocusChanged()

    def moveItemFocusLeft(self):
        grid = self.getFocusedGrid()
        try:
            grid.moveFocusLeft()
        except MoveGridItemFocusError:
            pass
        else:
            self.emitFocusChanged()

    def moveItemFocusRight(self):
        grid = self.getFocusedGrid()
        try:
            grid.moveFocusRight()
        except MoveGridItemFocusError:
            pass
        else:
            self.emitFocusChanged()

    def moveFocusNext(self):
        grid = self.getFocusedGrid()
        try:
            grid.moveFocusNext()
        except MoveGridItemFocusError:

            try:
                self.moveGridFocusDown()
            except MoveGridFocusError:
                pass
            else:
                newGrid = self.getFocusedGrid()
                newGrid.setFocusItem(0, 0)
                grid.clearFocusItem()
                self.emitFocusChanged()

        else:
            self.emitFocusChanged()

    def moveFocusPrevious(self):
        grid = self.getFocusedGrid()
        try:
            grid.moveFocusPrevious()
        except MoveGridItemFocusError:

            try:
                self.moveGridFocusUp()
            except MoveGridFocusError:
                pass
            else:
                newGrid = self.getFocusedGrid()
                if newGrid.rows % 2 == 0:
                    newGrid.setFocusItem(newGrid.rows - 1, 0)
                else:
                    newGrid.setFocusItem(newGrid.rows - 1, newGrid.cols - 1)
                grid.clearFocusItem()
                self.emitFocusChanged()

        else:
            self.emitFocusChanged()
コード例 #7
0
class MainWindow(QWidget):
    def __init__(self):
        super(QWidget, self).__init__()
        self.settings = QSettings()
        geometry = self.settings.value('mainwindowgeometry', '')
        if geometry:
            self.restoreGeometry(geometry)
        self.vlayout = QVBoxLayout()
        self.layout = QHBoxLayout()
        self.left = QWidget()
        self.left.setLayout(self.vlayout)
        self.right = QWidget()
        self.layout.addWidget(self.left)
        self.layout.addWidget(self.right)
        self.setLayout(self.layout)
        self.vlayout.addWidget(QLabel("Choose profile to check:"))
        self.profilewidget = QComboBox()
        for p in CLI_PROFILES:
            self.profilewidget.addItem(p)
        last_used_profile = self.settings.value("last_used_profile", "")
        if last_used_profile:
            self.profilewidget.setCurrentText(last_used_profile)
        self.vlayout.addWidget(self.profilewidget)

        self.profilewidget.currentIndexChanged.connect(self.profile_changed)

        self.vlayout.addWidget(QLabel("Choose checks to run:"))
        self.checkwidget = CheckCombo(self.profilewidget.currentText())
        self.vlayout.addWidget(self.checkwidget)

        self.vlayout.addWidget(QLabel("Choose level of output:"))
        self.loglevelwidget = QComboBox()
        for l in log_levels.keys():
            self.loglevelwidget.addItem(l)
        self.loglevelwidget.setCurrentText("INFO")
        self.vlayout.addWidget(self.loglevelwidget)

        self.vlayout.addWidget(DragDropArea(self))

        self.progress = QProgressBar(self)
        self.progress.setMinimum(0)
        self.progress.setMaximum(100)
        self.progress.setValue(0)
        self.vlayout.addWidget(self.progress)
        self.vlayout.addStretch()

    def run_fontbakery(self, paths):
        self.progress.setValue(0)
        # Setup the worker object and the worker_thread.
        profilename = self.profilewidget.currentText()
        loglevel = log_levels[self.loglevelwidget.currentText()]
        self.settings.setValue('last_used_profile', profilename)
        print("checked_checks", self.checkwidget.checked_checks())
        self.worker = FontbakeryRunner(profilename, [loglevel], paths, checks=self.checkwidget.checked_checks())
        self.worker_thread = QThread()
        self.worker.moveToThread(self.worker_thread)
        self.worker_thread.started.connect(self.worker.start)
        self.worker.signalStatus.connect(self.show_results)
        self.worker.progressStatus.connect(self.update_progress)
        self.worker_thread.start()

    def update_progress(self, value):
        self.progress.setValue(int(value))

    def show_results(self, html, md):
        self.worker_thread.quit()
        self.layout.removeWidget(self.right)
        self.right.deleteLater()
        self.right = ResultsWidget(html, md)
        self.layout.addWidget(self.right)

    def profile_changed(self):
        index = self.vlayout.indexOf(self.checkwidget)
        self.vlayout.removeWidget(self.checkwidget)
        self.checkwidget.deleteLater()
        self.checkwidget = CheckCombo(self.profilewidget.currentText())
        self.vlayout.insertWidget(index, self.checkwidget)

    def closeEvent(self, event):
        geometry = self.saveGeometry()
        self.settings.setValue('mainwindowgeometry', geometry)
コード例 #8
0
class QtLayersList(QScrollArea):

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

        self.layers = layers
        self.setWidgetResizable(True)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scrollWidget = QWidget()
        self.setWidget(scrollWidget)
        self.vbox_layout = QVBoxLayout(scrollWidget)
        self.vbox_layout.addWidget(QtDivider())
        self.vbox_layout.addStretch(1)
        self.centers = []
        self.setAcceptDrops(True)
        self.setToolTip('Layer list')

        self.layers.changed.added.connect(self._add)
        self.layers.changed.removed.connect(self._remove)
        self.layers.changed.reordered.connect(self._reorder)

    def _add(self, event):
        """Inserts a layer widget at a specific index
        """
        layer = event.item
        index = event.index
        total = len(self.layers)
        if layer._qt_properties is not None:
            self.vbox_layout.insertWidget(2*(total - index)-1,
                                          layer._qt_properties)
            self.vbox_layout.insertWidget(2*(total - index), QtDivider())

    def _remove(self, event):
        """Removes a layer widget
        """
        layer = event.item
        if layer._qt_properties is not None:
            index = self.vbox_layout.indexOf(layer._qt_properties)
            divider = self.vbox_layout.itemAt(index+1).widget()
            self.vbox_layout.removeWidget(layer._qt_properties)
            layer._qt_properties.deleteLater()
            layer._qt_properties = None
            self.vbox_layout.removeWidget(divider)
            divider.deleteLater()
            divider = None

    def _reorder(self, event):
        """Reorders list of layer widgets by looping through all
        widgets in list sequentially removing them and inserting
        them into the correct place in final list.
        """
        total = len(self.layers)
        for i in range(total):
            layer = self.layers[i]
            if layer._qt_properties is not None:
                index = self.vbox_layout.indexOf(layer._qt_properties)
                divider = self.vbox_layout.itemAt(index+1).widget()
                self.vbox_layout.removeWidget(layer._qt_properties)
                self.vbox_layout.removeWidget(divider)
                self.vbox_layout.insertWidget(2*(total - i)-1,
                                              layer._qt_properties)
                self.vbox_layout.insertWidget(2*(total - i), divider)

    def mouseReleaseEvent(self, event):
        """Unselects all layer widgets
        """
        self.layers.unselect_all()

    def dragLeaveEvent(self, event):
        """Unselects layer dividers
        """
        event.ignore()
        for i in range(0, self.vbox_layout.count(), 2):
            self.vbox_layout.itemAt(i).widget().setSelected(False)

    def dragEnterEvent(self, event):
        event.accept()
        divs = []
        for i in range(0, self.vbox_layout.count(), 2):
            widget = self.vbox_layout.itemAt(i).widget()
            divs.append(widget.y()+widget.frameGeometry().height()/2)
        self.centers = [(divs[i+1]+divs[i])/2 for i in range(len(divs)-1)]

    def dragMoveEvent(self, event):
        cord = event.pos().y()
        center_list = (i for i, x in enumerate(self.centers) if x > cord)
        divider_index = next(center_list, len(self.centers))
        layerWidget = event.source()
        total = self.vbox_layout.count()//2 - 1
        index = total - self.vbox_layout.indexOf(layerWidget)//2 - 1
        insert = total - divider_index
        if not (insert == index) and not (insert-1 == index):
            state = True
        else:
            state = False
        for i in range(0, self.vbox_layout.count(), 2):
            if i == 2*divider_index:
                self.vbox_layout.itemAt(i).widget().setSelected(state)
            else:
                self.vbox_layout.itemAt(i).widget().setSelected(False)

    def dropEvent(self, event):
        for i in range(0, self.vbox_layout.count(), 2):
            self.vbox_layout.itemAt(i).widget().setSelected(False)
        cord = event.pos().y()
        center_list = (i for i, x in enumerate(self.centers) if x > cord)
        divider_index = next(center_list, len(self.centers))
        layerWidget = event.source()
        total = self.vbox_layout.count()//2 - 1
        index = total - self.vbox_layout.indexOf(layerWidget)//2 - 1
        insert = total - divider_index
        if index != insert and index+1 != insert:
            if not self.layers[index].selected:
                self.layers.unselect_all()
                self.layers[index].selected = True
            self.layers._move_layers(index, insert)
        event.accept()
コード例 #9
0
class Plugin(Plugin_Base):
    '''
        call sequence:
            set vars like hintSignal, hintSignal
            onInit
            onWidget
            onUiInitDone
                send
                onReceived
    '''
    # vars set by caller
    send = None              # send(data_bytes=None, file_path=None)
    hintSignal = None       # hintSignal.emit(title, msg)
    configGlobal = {}
    # other vars
    connParent = "main"
    connChilds = []
    id = "dbg"
    name = _("Send Receive")
    #
    receiveUpdateSignal = pyqtSignal(str, list, str) # head, content, encoding
    receiveProgressStop = False
    receivedData = []
    lock = threading.Lock()
    sendRecord = []
    lastColor = None
    lastBg = None
    defaultColor = None
    defaultBg = None
    help = '''{}<br>
<b style="color:#ef5350;"><kbd>F11</kbd></b>: {}<br>
<b style="color:#ef5350;"><kbd>Ctrl+Enter</kbd></b>: {}<br>
<b style="color:#ef5350;"><kbd>Ctrl+L</kbd></b>: {}<br>
<b style="color:#ef5350;"><kbd>Ctrl+K</kbd></b>: {}<br>
'''.format(
        _('Shortcut:'),
        _('Full screen'),
        _('Send data'),
        _('Clear Send Area'),
        _('Clear Receive Area')
    )

    def onInit(self, config):
        super().onInit(config)
        self.keyControlPressed = False
        self.isScheduledSending = False
        self.config = config
        default = {
            "version": 1,
            "receiveAscii" : True,
            "receiveAutoLinefeed" : False,
            "receiveAutoLindefeedTime" : 200,
            "sendAscii" : True,
            "sendScheduled" : False,
            "sendScheduledTime" : 300,
            "sendAutoNewline": False,
            "useCRLF" : True,
            "showTimestamp" : False,
            "recordSend" : False,
            "saveLogPath" : "",
            "saveLog" : False,
            "color" : False,
            "sendEscape" : False,
            "customSendItems" : [],
            "sendHistoryList" : [],
        }
        for k in default:
            if not k in self.config:
                self.config[k] = default[k]

    def onWidgetMain(self, parent):
        self.mainWidget = QSplitter(Qt.Vertical)
        # widgets receive and send area
        self.receiveArea = QTextEdit()
        font = QFont('Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace, Microsoft YaHei', 10)
        self.receiveArea.setFont(font)
        self.receiveArea.setLineWrapMode(QTextEdit.NoWrap)
        self.sendArea = QTextEdit()
        self.sendArea.setLineWrapMode(QTextEdit.NoWrap)
        self.sendArea.setAcceptRichText(False)
        self.clearReceiveButtion = QPushButton("")
        utils_ui.setButtonIcon(self.clearReceiveButtion, "mdi6.broom")
        self.sendButton = QPushButton("")
        utils_ui.setButtonIcon(self.sendButton, "fa.send")
        self.sendHistory = ComboBox()
        sendWidget = QWidget()
        sendAreaWidgetsLayout = QHBoxLayout()
        sendAreaWidgetsLayout.setContentsMargins(0,4,0,0)
        sendWidget.setLayout(sendAreaWidgetsLayout)
        buttonLayout = QVBoxLayout()
        buttonLayout.addWidget(self.clearReceiveButtion)
        buttonLayout.addStretch(1)
        buttonLayout.addWidget(self.sendButton)
        sendAreaWidgetsLayout.addWidget(self.sendArea)
        sendAreaWidgetsLayout.addLayout(buttonLayout)
        self.mainWidget.addWidget(self.receiveArea)
        self.mainWidget.addWidget(sendWidget)
        self.mainWidget.addWidget(self.sendHistory)
        self.mainWidget.setStretchFactor(0, 7)
        self.mainWidget.setStretchFactor(1, 2)
        self.mainWidget.setStretchFactor(2, 1)
        # event
        self.sendButton.clicked.connect(self.onSendData)
        self.clearReceiveButtion.clicked.connect(self.clearReceiveBuffer)
        self.receiveUpdateSignal.connect(self.updateReceivedDataDisplay)
        self.sendHistory.activated.connect(self.onSendHistoryIndexChanged)

        return self.mainWidget

    def onWidgetSettings(self, parent):
        # serial receive settings
        layout = QVBoxLayout()
        serialReceiveSettingsLayout = QGridLayout()
        serialReceiveSettingsGroupBox = QGroupBox(_("Receive Settings"))
        self.receiveSettingsAscii = QRadioButton(_("ASCII"))
        self.receiveSettingsAscii.setToolTip(_("Show recived data as visible format, select decode method at top right corner"))
        self.receiveSettingsHex = QRadioButton(_("HEX"))
        self.receiveSettingsHex.setToolTip(_("Show recived data as hex format"))
        self.receiveSettingsAscii.setChecked(True)
        self.receiveSettingsAutoLinefeed = QCheckBox(_("Auto\nLinefeed\nms"))
        self.receiveSettingsAutoLinefeed.setToolTip(_("Auto linefeed after interval, unit: ms"))
        self.receiveSettingsAutoLinefeedTime = QLineEdit("200")
        self.receiveSettingsAutoLinefeedTime.setProperty("class", "smallInput")
        self.receiveSettingsAutoLinefeedTime.setToolTip(_("Auto linefeed after interval, unit: ms"))
        self.receiveSettingsAutoLinefeed.setMaximumWidth(75)
        self.receiveSettingsAutoLinefeedTime.setMaximumWidth(75)
        self.receiveSettingsTimestamp = QCheckBox(_("Timestamp"))
        self.receiveSettingsTimestamp.setToolTip(_("Add timestamp before received data, will automatically enable auto line feed"))
        self.receiveSettingsColor = QCheckBox(_("Color"))
        self.receiveSettingsColor.setToolTip(_("Enable unix terminal color support, e.g. \\33[31;43mhello\\33[0m"))
        serialReceiveSettingsLayout.addWidget(self.receiveSettingsAscii,1,0,1,1)
        serialReceiveSettingsLayout.addWidget(self.receiveSettingsHex,1,1,1,1)
        serialReceiveSettingsLayout.addWidget(self.receiveSettingsAutoLinefeed, 2, 0, 1, 1)
        serialReceiveSettingsLayout.addWidget(self.receiveSettingsAutoLinefeedTime, 2, 1, 1, 1)
        serialReceiveSettingsLayout.addWidget(self.receiveSettingsTimestamp, 3, 0, 1, 1)
        serialReceiveSettingsLayout.addWidget(self.receiveSettingsColor, 3, 1, 1, 1)
        serialReceiveSettingsGroupBox.setLayout(serialReceiveSettingsLayout)
        serialReceiveSettingsGroupBox.setAlignment(Qt.AlignHCenter)
        layout.addWidget(serialReceiveSettingsGroupBox)

        # serial send settings
        serialSendSettingsLayout = QGridLayout()
        serialSendSettingsGroupBox = QGroupBox(_("Send Settings"))
        self.sendSettingsAscii = QRadioButton(_("ASCII"))
        self.sendSettingsHex = QRadioButton(_("HEX"))
        self.sendSettingsAscii.setToolTip(_("Get send data as visible format, select encoding method at top right corner"))
        self.sendSettingsHex.setToolTip(_("Get send data as hex format, e.g. hex '31 32 33' equal to ascii '123'"))
        self.sendSettingsAscii.setChecked(True)
        self.sendSettingsScheduledCheckBox = QCheckBox(_("Timed Send\nms"))
        self.sendSettingsScheduledCheckBox.setToolTip(_("Timed send, unit: ms"))
        self.sendSettingsScheduled = QLineEdit("300")
        self.sendSettingsScheduled.setProperty("class", "smallInput")
        self.sendSettingsScheduled.setToolTip(_("Timed send, unit: ms"))
        self.sendSettingsScheduledCheckBox.setMaximumWidth(75)
        self.sendSettingsScheduled.setMaximumWidth(75)
        self.sendSettingsCRLF = QCheckBox(_("<CRLF>"))
        self.sendSettingsCRLF.setToolTip(_("Select to send \\r\\n instead of \\n"))
        self.sendSettingsCRLF.setChecked(False)
        self.sendSettingsRecord = QCheckBox(_("Record"))
        self.sendSettingsRecord.setToolTip(_("Record send data"))
        self.sendSettingsEscape= QCheckBox(_("Escape"))
        self.sendSettingsEscape.setToolTip(_("Enable escape characters support like \\t \\r \\n \\x01 \\001"))
        self.sendSettingsAppendNewLine= QCheckBox(_("Newline"))
        self.sendSettingsAppendNewLine.setToolTip(_("Auto add new line when send"))
        serialSendSettingsLayout.addWidget(self.sendSettingsAscii,1,0,1,1)
        serialSendSettingsLayout.addWidget(self.sendSettingsHex,1,1,1,1)
        serialSendSettingsLayout.addWidget(self.sendSettingsScheduledCheckBox, 2, 0, 1, 1)
        serialSendSettingsLayout.addWidget(self.sendSettingsScheduled, 2, 1, 1, 1)
        serialSendSettingsLayout.addWidget(self.sendSettingsCRLF, 3, 0, 1, 1)
        serialSendSettingsLayout.addWidget(self.sendSettingsAppendNewLine, 3, 1, 1, 1)
        serialSendSettingsLayout.addWidget(self.sendSettingsEscape, 4, 0, 1, 2)
        serialSendSettingsLayout.addWidget(self.sendSettingsEscape, 4, 0, 1, 2)
        serialSendSettingsLayout.addWidget(self.sendSettingsRecord, 4, 1, 1, 1)
        serialSendSettingsGroupBox.setLayout(serialSendSettingsLayout)
        layout.addWidget(serialSendSettingsGroupBox)

        widget = QWidget()
        widget.setLayout(layout)
        layout.setContentsMargins(0,0,0,0)
        # event
        self.receiveSettingsTimestamp.clicked.connect(self.onTimeStampClicked)
        self.receiveSettingsAutoLinefeed.clicked.connect(self.onAutoLinefeedClicked)
        self.receiveSettingsAscii.clicked.connect(lambda : self.bindVar(self.receiveSettingsAscii, self.config, "receiveAscii"))
        self.receiveSettingsHex.clicked.connect(lambda : self.bindVar(self.receiveSettingsHex, self.config, "receiveAscii", invert = True))
        self.sendSettingsHex.clicked.connect(self.onSendSettingsHexClicked)
        self.sendSettingsAscii.clicked.connect(self.onSendSettingsAsciiClicked)
        self.sendSettingsRecord.clicked.connect(self.onRecordSendClicked)
        self.sendSettingsAppendNewLine.clicked.connect(lambda: self.bindVar(self.sendSettingsAppendNewLine, self.config, "sendAutoNewline"))
        self.sendSettingsEscape.clicked.connect(lambda: self.bindVar(self.sendSettingsEscape, self.config, "sendEscape"))
        self.sendSettingsCRLF.clicked.connect(lambda: self.bindVar(self.sendSettingsCRLF, self.config, "useCRLF"))
        self.receiveSettingsColor.clicked.connect(self.onSetColorChanged)
        self.receiveSettingsAutoLinefeedTime.textChanged.connect(lambda: self.bindVar(self.receiveSettingsAutoLinefeedTime, self.config, "receiveAutoLindefeedTime", vtype=int, vErrorMsg=_("Auto line feed value error, must be integer"), emptyDefault = "200"))
        self.sendSettingsScheduled.textChanged.connect(lambda: self.bindVar(self.sendSettingsScheduled, self.config, "sendScheduledTime", vtype=int, vErrorMsg=_("Timed send value error, must be integer"), emptyDefault = "300"))
        self.sendSettingsScheduledCheckBox.clicked.connect(lambda: self.bindVar(self.sendSettingsScheduledCheckBox, self.config, "sendScheduled"))
        return widget


    def onWidgetFunctional(self, parent):
        sendFunctionalLayout = QVBoxLayout()
        sendFunctionalLayout.setContentsMargins(0,0,0,0)
        # right functional layout
        self.filePathWidget = QLineEdit()
        self.openFileButton = QPushButton(_("Open File"))
        self.sendFileButton = QPushButton(_("Send File"))
        self.clearHistoryButton = QPushButton(_("Clear History"))
        self.addButton = QPushButton("")
        utils_ui.setButtonIcon(self.addButton, "fa.plus")
        self.fileSendGroupBox = QGroupBox(_("Sendding File"))
        fileSendGridLayout = QGridLayout()
        fileSendGridLayout.addWidget(self.filePathWidget, 0, 0, 1, 1)
        fileSendGridLayout.addWidget(self.openFileButton, 0, 1, 1, 1)
        fileSendGridLayout.addWidget(self.sendFileButton, 1, 0, 1, 2)
        self.fileSendGroupBox.setLayout(fileSendGridLayout)
        self.logFileGroupBox = QGroupBox(_("Save log"))
        # cumtom send zone
        #   groupbox
        customSendGroupBox = QGroupBox(_("Cutom send"))
        customSendItemsLayout0 = QVBoxLayout()
        customSendItemsLayout0.setContentsMargins(0,8,0,0)
        customSendGroupBox.setLayout(customSendItemsLayout0)
        #   scroll

        self.customSendScroll = QScrollArea()
        self.customSendScroll.setMinimumHeight(parameters.customSendItemHeight + 20)
        self.customSendScroll.setWidgetResizable(True)
        self.customSendScroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        #   add scroll to groupbox
        customSendItemsLayout0.addWidget(self.customSendScroll)
        #   wrapper widget
        cutomSendItemsWraper = QWidget()
        customSendItemsLayoutWrapper = QVBoxLayout()
        customSendItemsLayoutWrapper.setContentsMargins(0,0,0,0)
        cutomSendItemsWraper.setLayout(customSendItemsLayoutWrapper)
        #    custom items
        customItems = QWidget()
        self.customSendItemsLayout = QVBoxLayout()
        self.customSendItemsLayout.setContentsMargins(0,0,0,0)
        customItems.setLayout(self.customSendItemsLayout)
        customSendItemsLayoutWrapper.addWidget(customItems)
        customSendItemsLayoutWrapper.addWidget(self.addButton)
        #   set wrapper widget
        self.customSendScroll.setWidget(cutomSendItemsWraper)
        self.customSendScroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        #
        logFileLayout = QHBoxLayout()
        self.saveLogCheckbox = QCheckBox()
        self.logFilePath = QLineEdit()
        self.logFileBtn = QPushButton(_("Log path"))
        logFileLayout.addWidget(self.saveLogCheckbox)
        logFileLayout.addWidget(self.logFilePath)
        logFileLayout.addWidget(self.logFileBtn)
        self.logFileGroupBox.setLayout(logFileLayout)
        sendFunctionalLayout.addWidget(self.logFileGroupBox)
        sendFunctionalLayout.addWidget(self.fileSendGroupBox)
        sendFunctionalLayout.addWidget(self.clearHistoryButton)
        sendFunctionalLayout.addWidget(customSendGroupBox)
        sendFunctionalLayout.addStretch(1)
        self.funcWidget = QWidget()
        self.funcWidget.setLayout(sendFunctionalLayout)
        # event
        self.sendFileButton.clicked.connect(self.sendFile)
        self.saveLogCheckbox.clicked.connect(self.setSaveLog)
        self.logFileBtn.clicked.connect(self.selectLogFile)
        self.openFileButton.clicked.connect(self.selectFile)
        self.addButton.clicked.connect(self.customSendAdd)
        self.clearHistoryButton.clicked.connect(self.clearHistory)
        self.funcParent = parent
        return self.funcWidget

    def onWidgetStatusBar(self, parent):
        self.statusBar = statusBar(rxTxCount=True)
        return self.statusBar

    def onUiInitDone(self):
        paramObj = self.config
        self.receiveSettingsHex.setChecked(not paramObj["receiveAscii"])
        self.receiveSettingsAutoLinefeed.setChecked(paramObj["receiveAutoLinefeed"])
        try:
            interval = int(paramObj["receiveAutoLindefeedTime"])
            paramObj["receiveAutoLindefeedTime"] = interval
        except Exception:
            interval = parameters.Parameters.receiveAutoLindefeedTime
        self.receiveSettingsAutoLinefeedTime.setText(str(interval) if interval > 0 else str(parameters.Parameters.receiveAutoLindefeedTime))
        self.receiveSettingsTimestamp.setChecked(paramObj["showTimestamp"])
        self.sendSettingsHex.setChecked(not paramObj["sendAscii"])
        self.sendSettingsScheduledCheckBox.setChecked(paramObj["sendScheduled"])
        try:
            interval = int(paramObj["sendScheduledTime"])
            paramObj["sendScheduledTime"] = interval
        except Exception:
            interval = parameters.Parameters.sendScheduledTime
        self.sendSettingsScheduled.setText(str(interval) if interval > 0 else str(parameters.Parameters.sendScheduledTime))
        self.sendSettingsCRLF.setChecked(paramObj["useCRLF"])
        self.sendSettingsAppendNewLine.setChecked(paramObj["sendAutoNewline"])
        self.sendSettingsRecord.setChecked(paramObj["recordSend"])
        self.sendSettingsEscape.setChecked(paramObj["sendEscape"])
        for i in range(0, len(paramObj["sendHistoryList"])):
            text = paramObj["sendHistoryList"][i]
            self.sendHistory.addItem(text)
        self.logFilePath.setText(paramObj["saveLogPath"])
        self.logFilePath.setToolTip(paramObj["saveLogPath"])
        self.saveLogCheckbox.setChecked(paramObj["saveLog"])
        self.receiveSettingsColor.setChecked(paramObj["color"])
        # send items
        for text in paramObj["customSendItems"]:
            self.insertSendItem(text, load=True)

        self.receiveProcess = threading.Thread(target=self.receiveDataProcess)
        self.receiveProcess.setDaemon(True)
        self.receiveProcess.start()

    def onSendSettingsHexClicked(self):
        self.config["sendAscii"] = False
        data = self.sendArea.toPlainText().replace("\n","\r\n")
        data = utils.bytes_to_hex_str(data.encode())
        self.sendArea.clear()
        self.sendArea.insertPlainText(data)

    def onSendSettingsAsciiClicked(self):
        self.config["sendAscii"] = True
        try:
            data = self.sendArea.toPlainText().replace("\n"," ").strip()
            self.sendArea.clear()
            if data != "":
                data = utils.hex_str_to_bytes(data).decode(self.configGlobal["encoding"],'ignore')
                self.sendArea.insertPlainText(data)
        except Exception as e:
            # QMessageBox.information(self,self.strings.strWriteFormatError,self.strings.strWriteFormatError)
            print("format error")

    def onAutoLinefeedClicked(self):
        if (self.config["showTimestamp"] or self.config["recordSend"]) and not self.receiveSettingsAutoLinefeed.isChecked():
            self.receiveSettingsAutoLinefeed.setChecked(True)
            self.hintSignal.emit("warning", _("Warning"), _("linefeed always on if timestamp or record send is on"))
        self.config["receiveAutoLinefeed"] = self.receiveSettingsAutoLinefeed.isChecked()

    def onTimeStampClicked(self):
        self.config["showTimestamp"] = self.receiveSettingsTimestamp.isChecked()
        if self.config["showTimestamp"]:
            self.config["receiveAutoLinefeed"] = True
            self.receiveSettingsAutoLinefeed.setChecked(True)

    def onRecordSendClicked(self):
        self.config["recordSend"] = self.sendSettingsRecord.isChecked()
        if self.config["recordSend"]:
            self.config["receiveAutoLinefeed"] = True
            self.receiveSettingsAutoLinefeed.setChecked(True)

    def onEscapeSendClicked(self):
        self.config["sendEscape"] = self.sendSettingsEscape.isChecked()

    def onSetColorChanged(self):
        self.config["color"] = self.receiveSettingsColor.isChecked()

    def onSendHistoryIndexChanged(self, idx):
        self.sendArea.clear()
        self.sendArea.insertPlainText(self.sendHistory.currentText())

    def clearHistory(self):
        self.config["sendHistoryList"].clear()
        self.sendHistory.clear()
        self.hintSignal.emit("info", _("OK"), _("History cleared!"))


    def onSent(self, ok, msg, length, path):
        if ok:
            self.statusBar.addTx(length)
        else:
            self.hintSignal.emit("error", _("Error"), _("Send data failed!") + " " + msg)

    def onSentFile(self, ok, msg, length, path):
        print("file sent {}, path: {}".format('ok' if ok else 'fail', path))
        if ok:
            self.sendFileButton.setText(_("Send file"))
            self.sendFileButton.setDisabled(False)
            self.statusBar.addTx(length)
        else:
            self.hintSignal.emit("error", _("Error"), _("Send file failed!") + " " + msg)

    def setSaveLog(self):
        if self.saveLogCheckbox.isChecked():
            self.config["saveLog"] = True
        else:
            self.config["saveLog"] = False

    def selectFile(self):
        oldPath = self.filePathWidget.text()
        if oldPath=="":
            oldPath = os.getcwd()
        fileName_choose, filetype = QFileDialog.getOpenFileName(self.mainWidget,
                                    _("Select file"),
                                    oldPath,
                                    _("All Files (*)"))

        if fileName_choose == "":
            return
        self.filePathWidget.setText(fileName_choose)
        self.filePathWidget.setToolTip(fileName_choose)

    def selectLogFile(self):
        oldPath = self.logFilePath.text()
        if oldPath=="":
            oldPath = os.getcwd()
        fileName_choose, filetype = QFileDialog.getSaveFileName(self.mainWidget,
                                    _("Select file"),
                                    os.path.join(oldPath, "comtool.log"),
                                    _("Log file (*.log);;txt file (*.txt);;All Files (*)"))

        if fileName_choose == "":
            return
        self.logFilePath.setText(fileName_choose)
        self.logFilePath.setToolTip(fileName_choose)
        self.config["saveLogPath"] = fileName_choose

    def onLog(self, text):
        if self.config["saveLogPath"]:
            with open(self.config["saveLogPath"], "a+", encoding=self.configGlobal["encoding"], newline="\n") as f:
                f.write(text)

    def onKeyPressEvent(self, event):
        if event.key() == Qt.Key_Control:
            self.keyControlPressed = True
        elif event.key() == Qt.Key_Return or event.key()==Qt.Key_Enter:
            if self.keyControlPressed:
                self.onSendData()
        elif event.key() == Qt.Key_L:
            if self.keyControlPressed:
                self.sendArea.clear()
        elif event.key() == Qt.Key_K:
            if self.keyControlPressed:
                self.receiveArea.clear()

    def onKeyReleaseEvent(self, event):
        if event.key() == Qt.Key_Control:
            self.keyControlPressed = False

    def insertSendItem(self, text="", load = False):
        itemsNum = self.customSendItemsLayout.count() + 1
        height = parameters.customSendItemHeight * (itemsNum + 1) + 20
        topHeight = self.fileSendGroupBox.height() + self.logFileGroupBox.height() + 100
        if height + topHeight > self.funcParent.height():
            height = self.funcParent.height() - topHeight
        if height < 0:
            height = self.funcParent.height() // 3
        self.customSendScroll.setMinimumHeight(height)
        item = QWidget()
        layout = QHBoxLayout()
        layout.setContentsMargins(0,0,0,0)
        item.setLayout(layout)
        cmd = QLineEdit(text)
        send = QPushButton("")
        utils_ui.setButtonIcon(send, "fa.send")
        cmd.setToolTip(text)
        send.setToolTip(text)
        cmd.textChanged.connect(lambda: self.onCustomItemChange(self.customSendItemsLayout.indexOf(item), cmd, send))
        send.setProperty("class", "smallBtn")
        send.clicked.connect(lambda: self.sendCustomItem(self.config["customSendItems"][self.customSendItemsLayout.indexOf(item)]))
        delete = QPushButton("")
        utils_ui.setButtonIcon(delete, "fa.close")
        delete.setProperty("class", "deleteBtn")
        layout.addWidget(cmd)
        layout.addWidget(send)
        layout.addWidget(delete)
        delete.clicked.connect(lambda: self.deleteSendItem(self.customSendItemsLayout.indexOf(item), item))
        self.customSendItemsLayout.addWidget(item)
        if not load:
            self.config["customSendItems"].append("")

    def deleteSendItem(self, idx, item):
        item.setParent(None)
        self.config["customSendItems"].pop(idx)
        itemsNum = self.customSendItemsLayout.count()
        height = parameters.customSendItemHeight * (itemsNum + 1) + 20
        topHeight = self.fileSendGroupBox.height() + self.logFileGroupBox.height() + 100
        if height + topHeight > self.funcParent.height():
            height = self.funcParent.height() - topHeight
        self.customSendScroll.setMinimumHeight(height)

    def onCustomItemChange(self, idx, edit, send):
        text = edit.text()
        edit.setToolTip(text)
        send.setToolTip(text)
        self.config["customSendItems"][idx] = text

    def sendCustomItem(self, text):
        self.onSendData(data = text)

    def customSendAdd(self):
        self.insertSendItem()

    def getSendData(self, data=None) -> bytes:
        if data is None:
            data = self.sendArea.toPlainText()
        return self.parseSendData(data, self.configGlobal["encoding"], self.config["useCRLF"], not self.config["sendAscii"], self.config["sendEscape"])

    def sendFile(self):
        filename = self.filePathWidget.text()
        if not os.path.exists(filename):
            self.hintSignal.emit("error", _("Error"), _("File path error\npath") + ":%s" %(filename))
            return
        if not self.isConnected():
            self.hintSignal.emit("warning", _("Warning"), _("Connect first please"))
        else:
            self.sendFileButton.setDisabled(True)
            self.sendFileButton.setText(_("Sending file"))
            self.send(file_path=filename, callback = lambda ok, msg, length, path: self.onSentFile(ok, msg, length, path))


    def scheduledSend(self):
        self.isScheduledSending = True
        while self.config["sendScheduled"]:
            self.onSendData()
            try:
                time.sleep(self.config["sendScheduledTime"]/1000)
            except Exception:
                self.hintSignal.emit("error", _("Error"), _("Time format error"))
        self.isScheduledSending = False

    def sendData(self, data_bytes = None):
        try:
            if self.isConnected():
                if not data_bytes or type(data_bytes) == str:
                    data = self.getSendData(data_bytes)
                else:
                    data = data_bytes
                if not data:
                    return
                if self.config["sendAutoNewline"]:
                    data += b"\r\n" if self.config["useCRLF"] else b"\n"
                # record send data
                if self.config["recordSend"]:
                    head = '=> '
                    if self.config["showTimestamp"]:
                        head += '[{}] '.format(utils.datetime_format_ms(datetime.now()))
                    isHexStr, sendStr, sendStrsColored = self.bytes2String(data, not self.config["receiveAscii"], encoding=self.configGlobal["encoding"])
                    if isHexStr:
                        sendStr = sendStr.upper()
                        sendStrsColored= sendStr
                        head += "[HEX] "
                    if self.config["useCRLF"]:
                        head = "\r\n" + head
                    else:
                        head = "\n" + head
                    if head.strip() != '=>':
                        head = '{}: '.format(head.rstrip())
                    self.receiveUpdateSignal.emit(head, [sendStrsColored], self.configGlobal["encoding"])
                    self.sendRecord.insert(0, head + sendStr)
                self.send(data_bytes=data, callback = self.onSent)
                if data_bytes:
                    data = str(data_bytes)
                else:
                    data = self.sendArea.toPlainText()
                self.sendHistoryFindDelete(data)
                self.sendHistory.insertItem(0,data)
                self.sendHistory.setCurrentIndex(0)
                try:
                    idx = self.config["sendHistoryList"].index(data)
                    self.config["sendHistoryList"].pop(idx)
                except Exception:
                     pass
                self.config["sendHistoryList"].insert(0, data)

                # scheduled send
                if self.config["sendScheduled"]:
                    if not self.isScheduledSending:
                        t = threading.Thread(target=self.scheduledSend)
                        t.setDaemon(True)
                        t.start()
        except Exception as e:
            import traceback
            traceback.print_exc()
            print("[Error] sendData: ", e)
            self.hintSignal.emit("error", _("Error"), _("Send Error") + str(e))
            # print(e)

    def onSendData(self, call=True, data=None):
        try:
            self.sendData(data)
        except Exception as e:
            print("[Error] onSendData: ", e)
            self.hintSignal.emit("error", _("Error"), _("get data error") + ": " + str(e))

    def updateReceivedDataDisplay(self, head : str, datas : list, encoding):
        if datas:
            curScrollValue = self.receiveArea.verticalScrollBar().value()
            self.receiveArea.moveCursor(QTextCursor.End)
            endScrollValue = self.receiveArea.verticalScrollBar().value()
            cursor = self.receiveArea.textCursor()
            format = cursor.charFormat()
            font = QFont('Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace, Microsoft YaHei', 10)
            format.setFont(font)
            if not self.defaultColor:
                self.defaultColor = format.foreground()
            if not self.defaultBg:
                self.defaultBg = format.background()
            if head:
                format.setForeground(self.defaultColor)
                cursor.setCharFormat(format)
                format.setBackground(self.defaultBg)
                cursor.setCharFormat(format)
                cursor.insertText(head)
            for data in datas:
                if type(data) == str:
                    self.receiveArea.insertPlainText(data)
                elif type(data) == list:
                    for color, bg, text in data:
                        if color:
                            format.setForeground(QColor(color))
                            cursor.setCharFormat(format)
                        else:
                            format.setForeground(self.defaultColor)
                            cursor.setCharFormat(format)
                        if bg:
                            format.setBackground(QColor(bg))
                            cursor.setCharFormat(format)
                        else:
                            format.setBackground(self.defaultBg)
                            cursor.setCharFormat(format)
                        cursor.insertText(text)
                else: # bytes
                    self.receiveArea.insertPlainText(data.decode(encoding=encoding, errors="ignore"))
            if curScrollValue < endScrollValue:
                self.receiveArea.verticalScrollBar().setValue(curScrollValue)
            else:
                self.receiveArea.moveCursor(QTextCursor.End)

    def sendHistoryFindDelete(self,str):
        self.sendHistory.removeItem(self.sendHistory.findText(str))

    def _getColorByfmt(self, fmt:bytes):
        colors = {
            b"0": None,
            b"30": "#000000",
            b"31": "#f44336",
            b"32": "#4caf50",
            b"33": "#ffa000",
            b"34": "#2196f3",
            b"35": "#e85aad",
            b"36": "#26c6da",
            b"37": "#a1887f",
        }
        bgs = {
            b"0": None,
            b"40": "#000000",
            b"41": "#f44336",
            b"42": "#4caf50",
            b"43": "#ffa000",
            b"44": "#2196f3",
            b"45": "#e85aad",
            b"46": "#26c6da",
            b"47": "#a1887f",
        }
        fmt = fmt[2:-1].split(b";")
        color = colors[b'0']
        bg = bgs[b'0']
        for cmd in fmt:
            if cmd in colors:
                color = colors[cmd]
            if cmd in bgs:
                bg = bgs[cmd]
        return color, bg

    def _texSplitByColor(self, text:bytes):
        remain = b''
        ignoreCodes = [rb'\x1b\[\?.*?h', rb'\x1b\[\?.*?l']
        text = text.replace(b"\x1b[K", b"")
        for code in ignoreCodes:
            colorFmt = re.findall(code, text)
            for fmt in colorFmt:
                text = text.replace(fmt, b"")
        colorFmt = re.findall(rb'\x1b\[.*?m', text)
        if text.endswith(b"\x1b"): # ***\x1b
            text = text[:-1]
            remain = b'\x1b'
        elif text.endswith(b"\x1b["): # ***\x1b[
            text = text[:-2]
            remain = b'\x1b['
        else: # ****\x1b[****, ****\x1b[****;****m
            idx = -2
            idx_remain = -1
            while 1:
                idx = text.find(b"\x1b[", len(text) - 10 + idx + 2) # \x1b[00;00m]
                if idx < 0:
                    break
                remain = text[idx:]
                idx_remain = idx
            if len(remain) > 0:
                match = re.findall(rb'\x1b\[.*?m', remain)  # ****\x1b[****;****m***
                if len(match) > 0: # have full color format
                    remain = b''
                else:
                    text = text[:idx_remain]
        plaintext = text
        for fmt in colorFmt:
            plaintext = plaintext.replace(fmt, b"")
        colorStrs = []
        if colorFmt:
            p = 0
            for fmt in colorFmt:
                idx = text[p:].index(fmt)
                if idx != 0:
                    colorStrs.append([self.lastColor, self.lastBg, text[p:p+idx]])
                    p += idx
                self.lastColor, self.lastBg = self._getColorByfmt(fmt)
                p += len(fmt)
            colorStrs.append([self.lastColor, self.lastBg, text[p:]])
        else:
            colorStrs = [[self.lastColor, self.lastBg, text]]
        return plaintext, colorStrs, remain

    def getColoredText(self, data_bytes, decoding=None):
        plainText, coloredText, remain = self._texSplitByColor(data_bytes)
        if decoding:
            plainText = plainText.decode(encoding=decoding, errors="ignore")
            decodedColoredText = []
            for color, bg, text in coloredText:
                decodedColoredText.append([color, bg, text.decode(encoding=decoding, errors="ignore")])
            coloredText = decodedColoredText
        return plainText, coloredText, remain

    def bytes2String(self, data : bytes, showAsHex : bool, encoding="utf-8"):
        isHexString = False
        dataColored = None
        if showAsHex:
            return True, utils.hexlify(data, ' ').decode(encoding=encoding), dataColored
        try:
            dataPlain, dataColore, remain = self.getColoredText(data, self.configGlobal["encoding"])
            if remain:
                dataPlain += remain.decode(encoding=self.configGlobal["encoding"], errors="ignore")
        except Exception:
            dataPlain = utils.hexlify(data, ' ').decode(encoding=encoding)
            isHexString = True
        return isHexString, dataPlain, dataColored

    def clearReceiveBuffer(self):
        self.receiveArea.clear()
        self.statusBar.clear()

    def onReceived(self, data : bytes):
        self.receivedData.append(data)
        self.statusBar.addRx(len(data))
        self.lock.release()

    def receiveDataProcess(self):
        self.receiveProgressStop = False
        timeLastReceive = 0
        new_line = True
        logData = None
        buffer = b''
        remain = b''
        while(not self.receiveProgressStop):
            logData = None
            head = ""
            self.lock.acquire()
            new = b"".join(self.receivedData)
            buffer += new
            self.receivedData = []
            # timeout, add new line
            if time.time() - timeLastReceive> self.config["receiveAutoLindefeedTime"]:
                if self.config["showTimestamp"] or self.config["receiveAutoLinefeed"]:
                    if self.config["useCRLF"]:
                        head += "\r\n"
                    else:
                        head += "\n"
                    new_line = True
            data = ""
            # have data in buffer
            if len(buffer) > 0:
                hexstr = False
                # show as hex, just show
                if not self.config["receiveAscii"]:
                    data = utils.bytes_to_hex_str(buffer)
                    colorData = data
                    buffer = b''
                    hexstr = True
                # show as string, and don't need to render color
                elif not self.config["color"]:
                    data = buffer.decode(encoding=self.configGlobal["encoding"], errors="ignore")
                    colorData = data
                    buffer = b''
                # show as string, and need to render color, wait for \n or until timeout to ensure color flag in buffer
                else:
                    if time.time() - timeLastReceive >  self.config["receiveAutoLindefeedTime"] or b'\n' in buffer:
                        data, colorData, remain = self.getColoredText(buffer, self.configGlobal["encoding"])
                        buffer = remain
                # add time receive head
                # get data from buffer, now render
                if data:
                    # add time header, head format(send receive '123' for example):
                    # '123'  '[2021-12-20 11:02:08.02.754]: 123' '=> 12' '<= 123'
                    # '=> [2021-12-20 11:02:34.02.291]: 123' '<= [2021-12-20 11:02:40.02.783]: 123'
                    # '<= [2021-12-20 11:03:25.03.320] [HEX]: 31 32 33 ' '=> [2021-12-20 11:03:27.03.319] [HEX]: 31 32 33'
                    if new_line:
                        timeNow = '[{}] '.format(utils.datetime_format_ms(datetime.now()))
                        if self.config["recordSend"]:
                            head += "<= "
                        if self.config["showTimestamp"]:
                            head += timeNow
                            head = '{} '.format(head.rstrip())
                        if hexstr:
                            head += "[HEX] "
                        if (self.config["recordSend"] or self.config["showTimestamp"]) and not head.endswith("<= "):
                            head = head[:-1] + ": "
                        new_line = False
                    self.receiveUpdateSignal.emit(head, [colorData], self.configGlobal["encoding"])
                    logData = head + data
            if len(new) > 0:
                timeLastReceive = time.time()

            while len(self.sendRecord) > 0:
                self.onLog(self.sendRecord.pop())
            if logData:
                self.onLog(logData)
コード例 #10
0
class textEditorWindow(QMainWindow):

	updateOpen = pyqtSignal(str)
	removeOpen = pyqtSignal(object)

	# Constructor
	def __init__(self):
		super(textEditorWindow, self).__init__()
		self.setGeometry(400, 400, 600, 500)
		self.textBoxList = []
		self.openFiles = []
		self.openDialog = QDialog(parent=self)
		self.openDialog.setModal(True)
		self.connFileMap = []
		self.onlineIndex = 0
		self.username = None
		self.fileList = []
		self.tabsNextIndex = 0

		self.tabs = QTabWidget()
		self.tabs.resize(300,200)
		self.tabs.setTabsClosable(True)
		self.initOpenDialog()
		self.layout = QVBoxLayout(self)
		self.tabs.tabBar().setTabsClosable(True)
		self.tabs.tabBar().tabCloseRequested.connect(self.closeRequestedTab)

		self.setCentralWidget(self.tabs)

		self.menu()
		self.setEditingMenu(False)

		self.docked = QDockWidget("Online Users", self)
		self.addDockWidget(Qt.LeftDockWidgetArea, self.docked)
		self.dockedWidget = QWidget(self)
		self.docked.setWidget(self.dockedWidget)

		self.docklayout = QVBoxLayout()
		self.docklayout.addStretch(1)
		self.dockedWidget.setLayout(self.docklayout)
		self.docked.hide()

		toolbar = QToolBar('Toolbar', self)
		self.addToolBar(toolbar)
		toolbar.setFixedHeight
		fontFamilySelect = QFontComboBox(toolbar)
		fontFamilySelect.setCurrentFont(QFont(fontName, fontSize))
		fontFamilySelect.setWritingSystem(QFontDatabase.WritingSystem.Any)
		# Monospaced fonts only
		fontFamilySelect.setFontFilters(QFontComboBox.MonospacedFonts)
		fontFamilySelect.currentFontChanged.connect(self.updateFontFamily)

		self.fontSizes = ['8','9','10','11','12','14','16','18','20','22','24','26','28','36','48','72']
		fontSizeSelect = QComboBox(toolbar)
		fontSizeSelect.setSizeAdjustPolicy(QComboBox.AdjustToContents)
		fontSizeSelect.move(150,0)
		fontSizeSelect.addItems(self.fontSizes)
		fontSizeSelect.setCurrentIndex(6)
		fontSizeSelect.currentIndexChanged.connect(self.updateFontSizeIndex)

	def closeRequestedTab(self, index):
		if index==False: # hardcoded to be from menu item
			index = self.tabs.currentIndex()
		textBox = self.getCurrentTextbox(index=index)
		textBox.stopEditingFunction(index)

	def updateFontFamily(self, qfont):
		global fontName
		fontName = qfont.family()
		if len(self.openFiles) > 0:
			self.updateFonts()

	def updateFontSizeIndex(self, qsize):
		global fontSize
		fontSize = int(self.fontSizes[qsize])
		if len(self.openFiles) > 0:
			self.updateFonts()

	def updateFonts(self):
		text = self.getCurrentTextbox()
		text.setFont(QFont(fontName, fontSize))

	def createNewTab(self):
		ip, port, fileName, fullName = self.readFromLists()
		print(ip + "|" + port + "|" + fileName + "|" + fullName)
		if (ip == ""):
			return
		clientSocket = self.createFileSocket(ip, port, fileName)
		if clientSocket is None:
			return
		self.setEditingMenu(True)
		newTab = QWidget()
		text = Textbox(clientSocket, fullName, self.tabsNextIndex, self.updateOnline)
		self.tabsNextIndex += 1
		text.stopEditing.connect(self.removeTab)
		self.tabs.addTab(newTab, fullName)
		newTab.layout = QVBoxLayout(self)
		self.tabs.setCurrentIndex(self.tabsNextIndex-1)
		newTab.layout.addWidget(text)
		newTab.setLayout(newTab.layout)
		self.textBoxList.append(text)
		self.openFiles.append(fullName)
		fileusers = QGridLayout()
		##Temporary
		i = 0
		y = 0
		for x in text.names:
			label = QLabel(x)
			fileusers.addWidget(label, i, y)
			i += 1
			if i == 2:
				y += 1
				i = 0

		onlineBox = QGroupBox()
		onlineBox.setMaximumHeight(120)
		onlineBox.setLayout(fileusers)
		onlineBox.setTitle(fileName)

		text.onlineBox = onlineBox
		#self.docklayout.addWidget(onlineBox)
		self.docklayout.insertWidget(self.onlineIndex, onlineBox)
		self.onlineIndex += 1
		self.openDialog.hide()


	def initOpenDialog(self):
		tabNew = self.openDialog
		# tabNew = QWidget()
		# self.tabs.addTab(tabNew, "+")
		lytTab = QVBoxLayout(self)
		############################
		## Connection Layout Area ##
		############################
		lytConnection = QHBoxLayout(self)
		## Left Side: Connection List
		self.qlwConnSelect = QListWidget(tabNew)
		self.qlwConnSelect.setFixedSize(300,150)
		self.qlwConnSelect.itemSelectionChanged.connect(self.refreshFileList)
		## Middle: +/- buttons
		lytConnSettings = QVBoxLayout(self)
		btnAddConnection = QPushButton("+")
		btnAddConnection.setFixedSize(25,25)
		btnAddConnection.clicked.connect(self.addConnection)
		btnRefreshConnection = QPushButton("↻")
		btnRefreshConnection.setFixedSize(25,25)
		btnRefreshConnection.clicked.connect(self.refreshFileList)
		btnRemoveConnection = QPushButton("-")
		btnRemoveConnection.setFixedSize(25,25)
		btnRemoveConnection.clicked.connect(self.removeConnection)
		lytConnSettings.addWidget(btnAddConnection)
		lytConnSettings.addWidget(btnRefreshConnection)
		lytConnSettings.addWidget(btnRemoveConnection)
		## Right: Host/Port Inputs
		lblHost = QLabel("Host", self)
		self.lneHost = QLineEdit(parent=self)
		self.lneHost.placeholderText = 'Hostname'
		lblPort = QLabel("Port", self)
		self.lnePort = QLineEdit(parent=self)
		self.lnePort.placeholderText = 'Port'
		lytNewConn = QVBoxLayout(self)
		lytNewConn.addWidget(lblHost)
		lytNewConn.addWidget(self.lneHost)
		lytNewConn.addWidget(lblPort)
		lytNewConn.addWidget(self.lnePort)
		### Adding to Connection Layout
		lytConnection.addWidget(self.qlwConnSelect)
		lytConnection.addLayout(lytConnSettings)
		lytConnection.addStretch(1)
		lytConnection.addLayout(lytNewConn)
		lytConnection.addStretch(1)
		####################
		## File Selection ##
		####################
		lytFileList = QHBoxLayout(self)
		## Left Side
		self.qlwFileSelect = QListWidget(tabNew)
		self.qlwFileSelect.setFixedSize(300,150)
		## Middle
		lytModifyFile = QVBoxLayout(self)
		btnCreateFile = QPushButton("Create File")
		btnRenameFile = QPushButton("Rename File")
		btnDeleteFile = QPushButton("Delete File")
		btnCreateFile.setFixedSize(130,50)
		btnRenameFile.setFixedSize(130,50)
		btnDeleteFile.setFixedSize(130,50)
		btnCreateFile.clicked.connect(self.createFile)
		btnRenameFile.clicked.connect(self.renameFile)
		btnDeleteFile.clicked.connect(self.deleteFile)
		lytModifyFile.addWidget(btnCreateFile)
		lytModifyFile.addWidget(btnRenameFile)
		lytModifyFile.addWidget(btnDeleteFile)

		### Adding to File List Layout
		lytFileList.addWidget(self.qlwFileSelect)
		lytFileList.addStretch(2)
		lytFileList.addLayout(lytModifyFile)
		lytFileList.addStretch(1)
		# Connection Button
		btnConnect = QPushButton("Open File")
		btnConnect.setFixedSize(150,50)
		btnConnect.clicked.connect(self.createNewTab)
		## Final Layout Settings
		lytTab.addLayout(lytConnection)
		lytTab.addLayout(lytFileList)
		lytTab.addWidget(btnConnect)
		tabNew.setLayout(lytTab)

	def addConnection(self):
		host = self.lneHost.text()
		port = self.lnePort.text()
		# print("host = %s, port = %s"% (host, port))
		# Check if the port number is valid
		if not port.isdigit() or host == "":
			showErrorMessage("Invalid host or port")
			return
		# Attempt to connect to the server
		combinedName = host + ":" + port
		# If this is already an existing connection
		if combinedName in self.connFileMap:
			return
		if self.refreshFileList(ip=host, port=port) != True:
			return
		self.connFileMap.append(combinedName)
		self.lneHost.setText('')
		self.lnePort.setText('')
		qliNewConn = QListWidgetItem()
		qliNewConn.setText(combinedName)
		self.qlwConnSelect.addItem(qliNewConn)
		self.qlwConnSelect.setCurrentItem(qliNewConn)

	def removeConnection(self):
		if len(self.qlwConnSelect.selectedItems()) == 0:
			return
		self.connFileMap.remove(self.qlwConnSelect.currentItem().text())
		self.qlwConnSelect.takeItem(self.qlwConnSelect.currentRow())
		self.qlwFileSelect.clear()

	# Called when a new connection is selected
	def refreshFileList(self, ip="", port=""):
		if ip == False:
			ip = ""
		print("here1")
		if ip=="" or port=="":
			if len(self.qlwConnSelect.selectedItems()) == 0:
				return
			ip, port = self.qlwConnSelect.currentItem().text().split(':')
		combinedName = ip + ":" + port
		print("here2")
		# If this is the first refresh
		clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		try:
			clientSocket.connect((ip, int(port)))
		except socket.error:
			showErrorMessage("Failed to connect")
			return
		print("here3")
		response = sendMessage(clientSocket, True, "getFiles")
		if "Err" in response["FilesListResp"]:
			self.showErrorMessage(response["FilesListResp"]["Err"])
			return
		print("here4")
		files = sorted(response["FilesListResp"]["Ok"])
		print("we made it here!")
		self.qlwFileSelect.clear()
		for file in files:
			self.qlwFileSelect.addItem(file)
		return True

	def removeTab(self, index, text):
		self.tabs.removeTab(index)
		self.textBoxList.remove(text)
		tList = []
		tList.append(text)
		self.removeOpen.emit(tList)
		self.tabsNextIndex += -1
		self.openFiles.remove(text.fullName)
		if len(self.openFiles) == 0:
			self.setEditingMenu(False)
		for file in self.openFiles:
			print(file)

	def setEditingMenu(self, enabled):
		for child in self.fileMenu.actions():
			if child.text() == '&Save and close':
				child.setEnabled(enabled)
		for child in self.editMenu.actions():
			child.setEnabled(enabled)

	def setFileList(self, fileList):
		self.fileList = fileList

	def closeEvent(self, event: QCloseEvent):
		print("Window {} closed".format(self))
		self.removeOpen.emit(self.textBoxList)
		for object in self.textBoxList:
			sendMessage(object.clientSocket, False, "close")
		super().closeEvent(event)

	def createServerSocket(self, ip, port):
		clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		try:
			clientSocket.connect((ip, int(port)))
		except socket.error:
			showErrorMessage("Failed to connect")
			return None
		return clientSocket

	def createFileSocket(self, ip, port, fileName):
		clientSocket = self.createServerSocket(ip, port)
		fullName = ip + ":" + port + " " + fileName
		if fullName in self.openFiles:
			showErrorMessage("File is already open!")
		else:
			response = sendMessage(clientSocket, True, "open", fileName, self.username)
			if "Err" in response["OpenResp"]:
				showErrorMessage(response["OpenResp"]["Err"])
				return None
			else:
				return clientSocket

	def createFile(self):
		ip, port, fileName, fullName = self.readFromLists(requireFileSelection=False)
		if ip == "":
			return
		clientSocket = self.createServerSocket(ip, port)
		if clientSocket is None:
			return
		newName, okPressed = QInputDialog.getText(self, "Create file", "Enter new file name:", QLineEdit.Normal, "")
		if not okPressed:
			return
		response = sendMessage(clientSocket, True, "create", newName)
		if "Err" in response["CreateResp"]:
			showErrorMessage(response["CreateResp"]["Err"])
		else:
			self.refreshFileList()

	def renameFile(self):
		ip, port, fileName, fullName = self.readFromLists()
		if ip == "":
			return
		clientSocket = self.createServerSocket(ip, port)
		if clientSocket is None:
			return
		newName, okPressed = QInputDialog.getText(self, "Rename file", "Enter new file name:", QLineEdit.Normal, "")
		if okPressed:
			if newName != '':
				response = sendMessage(clientSocket, True, "rename", fileName, newName)
				if "Err" in response["RenameResp"]:
					showErrorMessage(response["RenameResp"]["Err"])
				else:
					newFullName = ip + ":" + port + " " + newName
					self.refreshFileList()
			else:
				showErrorMessage("File name cannot be empty")

	def deleteFile(self):
		ip, port, fileName, fullName = self.readFromLists()
		if ip == "":
			return
		clientSocket = self.createServerSocket(ip, port)
		if clientSocket is None:
			return
		if fileName in self.openFiles:
			showErrorMessage("Target file is open! Please close it first")
		else:
			response = sendMessage(clientSocket, True, "delete", fileName)
			if "Err" in response["DeleteResp"]:
				showErrorMessage(response["DeleteResp"]["Err"])
			else:
				self.refreshFileList()

	def readFromLists(self, requireFileSelection=True):
		if len(self.qlwConnSelect.selectedItems()) == 0:
			return ("","","","")
		if requireFileSelection:
			if len(self.qlwFileSelect.selectedItems()) == 0:
				return ("","","","")
		ip, port = self.qlwConnSelect.currentItem().text().split(':')
		fileName = None
		if requireFileSelection:
			fileName = self.qlwFileSelect.currentItem().text()
		else:
			fileName = ""
		fullName = ip+":"+port+" "+fileName
		if fullName in self.openFiles:
			showErrorMessage("File is open, please close it first.")
			return ("","","","")
		return (ip, port, fileName, fullName)

	def menu(self):
		menuBar = self.menuBar()
		self.fileMenu = menuBar.addMenu('&File')
		self.editMenu = menuBar.addMenu('&Edit')
		nameMenu = menuBar.addMenu('&Name')
		users = menuBar.addMenu('&Users')

		openAct = QAction('&Open', self)
		openAct.setShortcut('Ctrl+O')
		openAct.triggered.connect(self.showOpenDialog)
		self.fileMenu.addAction(openAct)

		saveCloseAct = QAction('&Save and close', self)
		saveCloseAct.setShortcut('Ctrl+W')
		saveCloseAct.setStatusTip('Save the current tab and close it.')
		saveCloseAct.triggered.connect(self.closeRequestedTab)
		self.fileMenu.addAction(saveCloseAct)

		exitAct = QAction(QIcon('exit.png'), '&Exit', self)
		exitAct.setShortcut('Ctrl+Q')
		exitAct.setStatusTip('Exit application')
		exitAct.triggered.connect(qApp.quit)
		self.fileMenu.addAction(exitAct)

	 	# === Edit === #
		cutAct = QAction('&Cut', self)
		cutAct.setStatusTip('Cut the current selection')
		cutAct.setShortcut('Ctrl+X')
		cutAct.triggered.connect(self.cutWrapper)
		self.editMenu.addAction(cutAct)

		copyAct = QAction('&Copy', self)
		copyAct.setStatusTip('Copy the current selection')
		copyAct.setShortcut('Ctrl+C')
		copyAct.triggered.connect(self.copyWrapper)
		self.editMenu.addAction(copyAct)

		pasteAct = QAction('&Paste', self)
		pasteAct.setStatusTip('Paste the current selection')
		pasteAct.setShortcut('Ctrl+V')
		pasteAct.triggered.connect(self.pasteWrapper)
		self.editMenu.addAction(pasteAct)

		self.editMenu.addSeparator()

		undoAct = QAction('&Undo', self)
		undoAct.setStatusTip('Undo the current selection')
		undoAct.setShortcut('Ctrl+Z')
		undoAct.triggered.connect(self.undoWrapper)
		self.editMenu.addAction(undoAct)

		redoAct = QAction('&Redo', self)
		redoAct.setStatusTip('Redo the current selection')
		redoAct.setShortcut('Ctrl+Y')
		redoAct.triggered.connect(self.redoWrapper)
		self.editMenu.addAction(redoAct)

		nameAction = QAction('Set Username', self)
		nameAction.triggered.connect(self.setName)
		nameMenu.addAction(nameAction)

		##Will be used to add new connections

		userAction = QAction('View Online Users', self, checkable=True)
		userAction.setChecked(False)
		userAction.triggered.connect(self.toggleOnline)

		users.addAction(userAction)

	def cutWrapper(self):
		text = self.getCurrentTextbox()
		text.cut()
	def copyWrapper(self):
		text = self.getCurrentTextbox()
		text.copy()
	def pasteWrapper(self):
		text = self.getCurrentTextbox()
		text.paste()
	def undoWrapper(self):
		text = self.getCurrentTextbox()
		text.undo()
	def redoWrapper(self):
		text = self.getCurrentTextbox()
		text.redo()

	def getCurrentTextbox(self, index=None):
		if index==None:
			index = self.tabs.currentIndex()
		childrenList = self.tabs.widget(index).children()
		for child in childrenList:
			if child.metaObject().className() == "Textbox":
				return child

	def showOpenDialog(self):
		self.openDialog.show()

	def setName(self):
		if len(self.openFiles) > 0:
			showErrorMessage("Cant set name when files opened")
			return

		username, okPressed = QInputDialog.getText(self, "", "Set Username", QLineEdit.Normal, "")

		if okPressed and username != "":
			self.username = username

		if okPressed and username == "":
			showErrorMessage("Name cannot be blank")

	def toggleOnline(self, state):
		if state:
			self.docked.show()

		if not state:
			self.docked.hide()

	def updateOnline(self, textbox, userlist):
		userlist.sort()
		if userlist == textbox.names:
			return
		index = self.docklayout.indexOf(textbox.onlineBox)
		textbox.onlineBox.close()

		fileusers = QGridLayout()
		i = 0
		y = 0
		count = 0
		for x in userlist:
			if x == self.username and count == 0:
				count+=1
				continue

			label = QLabel(x)
			fileusers.addWidget(label, i, y)
			i += 1
			if i == 2:
				y += 1
				i = 0

		onlineBox = QGroupBox()
		onlineBox.setMaximumHeight(120)
		onlineBox.setLayout(fileusers)
		onlineBox.setTitle(textbox.fullName)
		textbox.onlineBox = onlineBox

		self.docklayout.insertWidget(index, onlineBox)
		textbox.names = userlist
コード例 #11
0
ファイル: MainWindow.py プロジェクト: Sikorskyyy/nice_backup
class MainWindow(QMainWindow):
    
    def __init__(self, parent=None):

        super(MainWindow, self).__init__(parent)
        centralWidget = QWidget()
        self.mainLayout = QVBoxLayout()
        buttonLayout = QHBoxLayout()
        foldersLayout = QHBoxLayout()
        backupFolderLayout = QHBoxLayout()
        self.usbDevicesChooser = QComboBox()
        
        usb_util = Utils()
        self.usbDevices = usb_util.get_storage_device()

        for device in self.usbDevices:
            self.usbDevicesChooser.addItem(device.__str__())

        self.pathToFolders = QLineEdit()
        self.pathToFolders.setPlaceholderText("Path to folders you want to backup")
        self.pathToBackupFolder = QLineEdit()
        self.pathToBackupFolder.setPlaceholderText("Path to backup destination folder")

        self.browseFoldersButton = QPushButton("Browse...")
        self.browseFoldersButton.clicked.connect(self.on_browseFolders_clicked)
        self.browseBackupFolderButton = QPushButton("Browse...")
        self.browseBackupFolderButton.clicked.connect(self.on_browseBackupFolder_clicked)

        foldersLayout.addWidget(self.pathToFolders)
        foldersLayout.addWidget(self.browseFoldersButton)
        foldersLayout.setSpacing(5)

        backupFolderLayout.addWidget(self.pathToBackupFolder)
        backupFolderLayout.addWidget(self.browseBackupFolderButton)
        backupFolderLayout.setSpacing(5)

        self.startBackupButton = QPushButton("Start Backup")
        self.startBackupButton.clicked.connect(self.on_startBackup_clicked)

        self.exitButton = QPushButton("Exit")
        self.exitButton.clicked.connect(self.on_exit_clicked)

        buttonLayout.addWidget(self.startBackupButton)
        buttonLayout.addWidget(self.exitButton)
        buttonLayout.setSpacing(10)

        self.mainLayout.addWidget(self.usbDevicesChooser)
        self.mainLayout.addLayout(backupFolderLayout)
        self.mainLayout.addLayout(foldersLayout)
        self.mainLayout.addLayout(buttonLayout)

        centralWidget.setLayout(self.mainLayout)
        self.setCentralWidget(centralWidget)
        self.setWindowTitle("Nice Backup")
        self.setMinimumSize(600, 200)

    def on_exit_clicked(self, widget):
        sys.exit()

    def on_browseFolders_clicked(self, widget):
        index = self.usbDevicesChooser.currentIndex()
        file_dialog = QFileDialog()
        file_dialog.setFileMode(QFileDialog.DirectoryOnly)
        file_dialog.setOption(QFileDialog.DontUseNativeDialog, True)
        file_view = file_dialog.findChild(QListView, 'listView')

        if file_view:
            file_view.setSelectionMode(QAbstractItemView.MultiSelection)

        ftree_view = file_dialog.findChild(QTreeView)

        if ftree_view:
            ftree_view.setSelectionMode(QAbstractItemView.MultiSelection)

        file_dialog.setDirectory(self.usbDevices[index].getPath())

        if file_dialog.exec():
            paths = file_dialog.selectedFiles()

        self.pathToFolders.setText(";".join(paths))

    def on_browseBackupFolder_clicked(self, widget):
        fname = QFileDialog.getExistingDirectory(self.window(), 'Open file', '/home/{}'.format(getpass.getuser()))
        self.pathToBackupFolder.setText(fname)

    def on_startBackup_clicked(self, widget):
        print("Starting backup")
        backuper = Backuper.Backuper()
        sources = self.pathToFolders.text().split(";")
        destination = self.pathToBackupFolder.text()

        progress = QProgressBar()
        progress.setMinimum(0)
        progress.setMaximum(len(sources))
        self.mainLayout.addWidget(progress)

        for source in sources:
            backuper.make_backup(source, destination)
            progress.setValue(sources.index(source))
            QApplication.processEvents()
        doneDialog = QMessageBox()

        doneDialog.setIcon(QMessageBox.Information)
        doneDialog.setText("Copying data is completed")
        doneDialog.setWindowTitle("Copy event")
        doneDialog.setStandardButtons(QMessageBox.Ok)

        index = self.mainLayout.indexOf(progress)
        self.mainLayout.itemAt(index).widget().setParent(None)

        doneDialog.exec()
コード例 #12
0
class QtFigure(BaseFigure, QWidget):
    """The VTK render window embedded into a PyQt5 QWidget. This can be
    embedded into a GUI the same way all other QWidgets are used.

    :param name: The window title of the figure, only applicable is **parent** is None, defaults to 'qt vtk figure'.
    :type name: str, optional

    :param parent: Parent window, defaults to None.
    :type parent: PyQt5.QtWidgets.QWidget, optional


    .. note::

        If you are new to Qt then this is a rather poor place to start. Whilst
        many libraries in Python are intuitive enough to be able to just dive
        straight in, Qt is not one of them. Preferably familiarise yourself
        with some basic Qt before coming here.


    This class inherits both from :class:`PyQt5.QtWidgets.QWidget` and a
    vtkplotlib BaseFigure class. Therefore it can be used exactly the same as you
    would normally use either a `QWidget` or a :class:`vtkplotlib.figure`.

    Care must be taken when using Qt to ensure you have **exactly one**
    QApplication. To make this class quicker to use the qapp is created
    automatically but is wrapped in a

    .. code-block:: python

        if QApplication.instance() is None:
            self.qapp = QApplication(sys.argv)
        else:
            self.qapp = QApplication.instance()

    This prevents multiple QApplication instances from being created (which
    causes an instant crash) whilst also preventing a QWidget from being
    created without a qapp (which also causes a crash).


    On ``self.show()``, ``self.qapp.exec_()`` is called automatically if
    ``self.parent() is None`` (unless specified otherwise). If the QFigure is part
    of a larger window then ``larger_window.show()`` must also explicitly show
    the figure. It won't begin interactive mode until ``qapp.exec_()`` is
    called.


    If the figure is not to be part of a larger window then it behaves exactly
    like a regular figure. You just need to explicitly create it first.

    .. code-block:: python

        import vtkplotlib as vpl

        # Create the figure. This automatically sets itself as the current
        # working figure. The qapp is created automatically if one doesn't
        # already exist.
        vpl.QtFigure("Exciting Window Title")

        # Everything from here on should be exactly the same as normal.

        vpl.quick_test_plot()

        # Automatically calls ``qapp.exec_()``. If you don't want it to then
        # use ``vpl.show(False)``.
        vpl.show()


    However this isn't particularly helpful. A more realistic example would
    require the figure be part of a larger window. In this case, treat the
    figure as you would any other QWidget. You must explicitly call
    ``figure.show()`` however. (Not sure why.)

    .. code-block:: python

        import vtkplotlib as vpl
        from PyQt5 import QtWidgets
        import numpy as np
        import sys

        # python 2 compatibility
        from builtins import super


        class FigureAndButton(QtWidgets.QWidget):
            def __init__(self):
                super().__init__()

                # Go for a vertical stack layout.
                vbox = QtWidgets.QVBoxLayout()
                self.setLayout(vbox)

                # Create the figure
                self.figure = vpl.QtFigure()

                # Create a button and attach a callback.
                self.button = QtWidgets.QPushButton("Make a Ball")
                self.button.released.connect(self.button_pressed_cb)

                # QtFigures are QWidgets and are added to layouts with `addWidget`
                vbox.addWidget(self.figure)
                vbox.addWidget(self.button)


            def button_pressed_cb(self):
                \"""Plot commands can be called in callbacks. The current working
                figure is still self.figure and will remain so until a new
                figure is created explicitly. So the ``fig=self.figure``
                arguments below aren't necessary but are recommended for
                larger, more complex scenarios.
                \"""

                # Randomly place a ball.
                vpl.scatter(np.random.uniform(-30, 30, 3),
                            color=np.random.rand(3),
                            fig=self.figure)

                # Reposition the camera to better fit to the balls.
                vpl.reset_camera(self.figure)

                # Without this the figure will not redraw unless you click on it.
                self.figure.update()


            def show(self):
                # The order of these two are interchangeable.
                super().show()
                self.figure.show()


            def closeEvent(self, event):
                \"""This isn't essential. VTK, OpenGL, Qt and Python's garbage
                collect all get in the way of each other so that VTK can't
                clean up properly which causes an annoying VTK error window to
                pop up. Explicitly calling QtFigure's `closeEvent()` ensures
                everything gets deleted in the right order.
                \"""
                self.figure.closeEvent(event)




        qapp = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)

        window = FigureAndButton()
        window.show()
        qapp.exec_()


    .. note::  QtFigures are not reshow-able if the figure has a parent.

    .. seealso:: :class:`vtkplotlib.QtFigure2` is an extension of this to provide some standard GUI elements, ready-made.

    """

    def __init__(self, name="qt vtk figure", parent=None):

        self.qapp = QApplication.instance() or QApplication(sys.argv)
        QWidget.__init__(self, parent)
        BaseFigure.__init__(self, name)

        self.vl = QVBoxLayout()
        self.setLayout(self.vl)

        self._vtkWidget = QVTKRenderWindowInteractor(self)
        self.vl.addWidget(self.vtkWidget)

        self.renWin
        self.iren

    def _re_init(self):
        debug("re init")
        name = self.window_name
        QWidget.__init__(self, self.parent())
        self.window_name = name
        self.setLayout(self.vl)
        self.setWindowTitle(self.window_name)

        self.vtkWidget = QVTKRenderWindowInteractor(self)
        self.vl.insertWidget(self._vtkWidget_replace_index, self.vtkWidget)

        self.renWin, self.iren

    @property
    def vtkWidget(self):
        if not hasattr(self, "_vtkWidget"):
            self._re_init()
        return self._vtkWidget

    @vtkWidget.setter
    def vtkWidget(self, widget):
        self._vtkWidget = widget

    @vtkWidget.deleter
    def vtkWidget(self):
        if hasattr(self, "_vtkWidget"):
            del self._vtkWidget

    def _base_show_wrap(QWidget_show_name):
        """Wrap all the ``QWidget.show()``, ``QWidget.showMaximized()`` etc
        methods so they can all be used as expected. Just in case Qt has changed
        and some ``show...()`` methods aren't present, this defaults to just
        ``show()``.
        """

        QWidget_show = getattr(QWidget, QWidget_show_name, QWidget.show)

        def show(self, block=None):
            if not hasattr(self, "vtkWidget"):
                self._re_init()
            self._connect_renderer()

            QWidget_show(self)

            self.iren.Initialize()
            self.renWin.Render()
            self.iren.Start()

            if block is None:
                block = self.parent() is None
            if block:
                self._flush_stdout()
                self.qapp.exec_()
            BaseFigure.show(self, block)

        show.__name__ = QWidget_show.__name__
        try:
            show.__qualname__ = QWidget_show.__qualname__
        except (AttributeError, TypeError):
            pass

        return show

    show = _base_show_wrap("show")
    showMaximized = _base_show_wrap("showMaximized")
    showMinimized = _base_show_wrap("showMinimized")
    showFullScreen = _base_show_wrap("showFullScreen")
    showNormal = _base_show_wrap("showNormal")

    @nuts_and_bolts.init_when_called
    def renWin(self):
        if not hasattr(self, "vtkWidget"):
            self._re_init()
        renWin = self.vtkWidget.GetRenderWindow()
        return renWin

    @nuts_and_bolts.init_when_called
    def iren(self):
        iren = self.renWin.GetInteractor()
        iren.SetInteractorStyle(self.style)
        return iren

    def update(self):
        BaseFigure.update(self)
        QWidget.update(self)
        self.qapp.processEvents()

#    def close(self):
#        BaseFigure.close(self)
#        QWidget.close(self)
#        self._clean_up()

    def on_close(self):
        debug("cleaning up")
        if hasattr(self, "_renWin"):
            # These prevent error dialogs popping up.
            self._disconnect_renderer()
            self.renWin.MakeCurrent()
            self.renWin.Finalize()

        if hasattr(self, "_vtkWidget"):
            self._vtkWidget_replace_index = self.vl.indexOf(self.vtkWidget)
            self.vl.removeWidget(self.vtkWidget)


#        self.renderer.RemoveAllViewProps()

        del self.vtkWidget, self.iren, self.renWin

    def closeEvent(self, event):
        self.on_close()

    window_name = property(QWidget.windowTitle, QWidget.setWindowTitle)

    def __del__(self):
        try:
            self.renderer.RemoveAllViewProps()
        except (AttributeError, TypeError):
            # In Python2, RemoveAllViewProps is already None
            pass

    def _prep_for_screenshot(self, off_screen=False):
        BaseFigure._prep_for_screenshot(self, off_screen)
        if off_screen:
            print("Off screen rendering can't be done using QtFigures.")
        self.show(block=False)

    def close(self):
        QWidget.close(self)
        # closeEvent seems to be called anyway but call this just to be sure.
        self.on_close()
        BaseFigure.close(self)
コード例 #13
0
ファイル: _layerList.py プロジェクト: pep8speaks/napari-gui
class QtLayerList(QScrollArea):
    def __init__(self, layers):
        super().__init__()

        self.layers = layers
        self.setWidgetResizable(True)
        #self.setFixedWidth(315)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scrollWidget = QWidget()
        self.setWidget(scrollWidget)
        self.layersLayout = QVBoxLayout(scrollWidget)
        self.layersLayout.addWidget(QtDivider())
        self.layersLayout.addStretch(1)
        self.setAcceptDrops(True)
        self.setToolTip('Layer list')

    def insert(self, index, total, layer):
        """Inserts a layer widget at a specific index
        """
        if layer._qt is not None:
            self.layersLayout.insertWidget(2 * (total - index) - 1, layer._qt)
            self.layersLayout.insertWidget(2 * (total - index), QtDivider())
        self.layers.viewer._update_active_layers()
        self.layers.viewer.controlBars.climSliderUpdate()

    def remove(self, layer):
        """Removes a layer widget
        """
        if layer._qt is not None:
            index = self.layersLayout.indexOf(layer._qt)
            divider = self.layersLayout.itemAt(index + 1).widget()
            self.layersLayout.removeWidget(layer._qt)
            layer._qt.deleteLater()
            layer._qt = None
            self.layersLayout.removeWidget(divider)
            divider.deleteLater()
            divider = None
        self.layers.viewer._update_active_layers()
        self.layers.viewer.controlBars.climSliderUpdate()

    def reorder(self):
        """Reorders list of layer widgets by looping through all
        widgets in list sequentially removing them and inserting
        them into the correct place in final list.
        """
        total = len(self.layers)
        for i in range(total):
            layer = self.layers[i]
            if layer._qt is not None:
                index = self.layersLayout.indexOf(layer._qt)
                divider = self.layersLayout.itemAt(index + 1).widget()
                self.layersLayout.removeWidget(layer._qt)
                self.layersLayout.removeWidget(divider)
                self.layersLayout.insertWidget(2 * (total - i) - 1, layer._qt)
                self.layersLayout.insertWidget(2 * (total - i), divider)
        self.layers.viewer._update_active_layers()
        self.layers.viewer.controlBars.climSliderUpdate()

    def mouseReleaseEvent(self, event):
        """Unselects all layer widgets
        """
        if self.layersLayout.count() > 1:
            self.layersLayout.itemAt(1).widget().unselectAll()
        self.layers.viewer._update_active_layers()
        self.layers.viewer._set_annotation_mode(self.layers.viewer.annotation)
        self.layers.viewer.controlBars.climSliderUpdate()
        self.layers.viewer._status = 'Ready'
        self.layers.viewer.emitStatus()

    def dragLeaveEvent(self, event):
        event.ignore()
        for i in range(0, self.layersLayout.count(), 2):
            self.layersLayout.itemAt(i).widget().setSelected(False)

    def dragEnterEvent(self, event):
        event.accept()
        dividers = []
        for i in range(0, self.layersLayout.count(), 2):
            widget = self.layersLayout.itemAt(i).widget()
            dividers.append(widget.y() + widget.frameGeometry().height() / 2)
        self.centers = [(dividers[i + 1] + dividers[i]) / 2
                        for i in range(len(dividers) - 1)]

    def dragMoveEvent(self, event):
        cord = event.pos().y()
        divider_index = next(
            (i for i, x in enumerate(self.centers) if x > cord),
            len(self.centers))
        layerWidget = event.source()
        layers = layerWidget.layer.viewer.layers
        index = layers.index(layerWidget.layer)
        total = len(layers)
        insert_index = total - divider_index
        if not (insert_index == index) and not (insert_index - 1 == index):
            state = True
        else:
            state = False
        for i in range(0, self.layersLayout.count(), 2):
            if i == 2 * divider_index:
                self.layersLayout.itemAt(i).widget().setSelected(state)
            else:
                self.layersLayout.itemAt(i).widget().setSelected(False)

    def dropEvent(self, event):
        for i in range(0, self.layersLayout.count(), 2):
            self.layersLayout.itemAt(i).widget().setSelected(False)
        cord = event.pos().y()
        divider_index = next(
            (i for i, x in enumerate(self.centers) if x > cord),
            len(self.centers))
        layerWidget = event.source()
        layers = layerWidget.layer.viewer.layers
        index = layers.index(layerWidget.layer)
        total = len(layers)
        insert_index = total - divider_index
        indices = [i for i in range(total)]
        if layerWidget.layer.selected:
            selected = []
            for i in range(total):
                if layers[i].selected:
                    selected.append(i)
        else:
            selected = [index]
        for i in selected:
            indices.remove(i)
        offset = sum([i < insert_index for i in selected])
        j = insert_index - offset
        for i in selected:
            indices.insert(j, i)
            j = j + 1
        if not indices == [i for i in range(total)]:
            layers.reorder(indices)
            event.accept()
        else:
            event.ignore()
        if not layerWidget.layer.selected:
            layerWidget.unselectAll()
            layerWidget.setSelected(True)
コード例 #14
0
class Overview(QWidget):
    ''' Dock Widget
        This widget shows an overview of all created frames.
        Supports:   Change to a frame with a click.
                    The active frame is heighlighted.
    '''
    def __init__(self,parent=None):
        super().__init__(parent)
        self.initUI()

    def initUI(self):
        self.scrollArea = QScrollArea(self)
        self.scrollArea.setWidgetResizable(True)
        self.scrollAreaWidgetContents = QWidget(self.scrollArea)
        self.vLayout = QVBoxLayout(self.scrollAreaWidgetContents)
        self.vLayout.setSpacing(20)
        self.scrollArea.setWidget(self.scrollAreaWidgetContents)
        self.scrollArea.setMinimumSize(150,self.parent().parent().frames.size().height())
        self.addFrameWidget()

        self.parent().parent().frames.resized.connect(lambda x:self.adaptFrameViewerSize(x))



    def addFrameWidget(self,frameID = 0):
        for i in reversed(range(self.vLayout.count())):
            widget = self.vLayout.takeAt(i).widget()
            if widget is not None:
                widget = widget.setParent(None)

        for i,frame in enumerate(self.parent().parent().frames.sceneCollection):
            label = OverviewLabel(self)
            view = SingleFrameViewer(label,self.parent().parent().frames)
            view.setScene(frame)
            view.scale(0.2,0.2)
            view.resize(130,130)
            label.clicked.connect(lambda x: self.labelClicked(x))
            self.vLayout.insertWidget(i,label)
        self.activeFrameChanged(self.parent().parent().frames.activeFrameID)

    def removeFrameWidget(self, frameID = 0):
        try:
            widget = self.vLayout.takeAt(frameID).widget()
            if widget is not None:
                widget = widget.setParent(None)
        except:
            print("Tried to remove overview frame {}, but it could not be found.".format(frameID))

    def activeFrameChanged(self,activeFrameID):
        for i in reversed(range(self.vLayout.count())):
            widget = self.vLayout.itemAt(i).widget()
            if widget is not None:
                if widget.active:
                    widget.toggleActive()
        activeFrame = self.vLayout.itemAt(activeFrameID)
        if activeFrame is not None:
            activeFrame.widget().toggleActive()

    def labelClicked(self,widget):
        self.parent().parent().frames.selectFrameById(self.vLayout.indexOf(widget))

    def adaptFrameViewerSize(self,size):
        self.scrollArea.setMinimumSize(self.scrollArea.minimumSize().width(),size.height()-7)
コード例 #15
0
ファイル: main.py プロジェクト: dammhannes/gospel-pdf-viewer
class Window(QMainWindow, Ui_window):
    renderRequested = QtCore.pyqtSignal(int, float)
    loadFileRequested = QtCore.pyqtSignal(str, str)
    findTextRequested = QtCore.pyqtSignal(str, int, bool)

    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.setupUi(self)
        self.dockSearch.hide()
        self.dockWidget.hide()
        self.dockWidget.setMinimumWidth(310)
        self.findTextEdit.setFocusPolicy(QtCore.Qt.StrongFocus)
        self.treeView.setAlternatingRowColors(True)
        self.treeView.clicked.connect(self.onOutlineClick)
        # resizing pages requires some time to take effect
        self.resize_page_timer = QtCore.QTimer(self)
        self.resize_page_timer.setSingleShot(True)
        self.resize_page_timer.timeout.connect(self.onWindowResize)
        # Add shortcut actions
        self.gotoPageAction = QAction(QIcon(":/goto.png"), "GoTo Page", self)
        self.gotoPageAction.triggered.connect(self.gotoPage)
        self.copyTextAction = QAction(QIcon(":/copy.png"), "Copy Text", self)
        self.copyTextAction.setCheckable(True)
        self.copyTextAction.triggered.connect(self.toggleCopyText)
        self.findTextAction = QAction(QIcon(":/search.png"), "Find Text", self)
        self.findTextAction.setShortcut('Ctrl+F')
        self.findTextAction.triggered.connect(self.dockSearch.show)
        # connect menu actions signals
        self.openFileAction.triggered.connect(self.openFile)
        self.lockUnlockAction.triggered.connect(self.lockUnlock)
        self.printAction.triggered.connect(self.printFile)
        self.quitAction.triggered.connect(self.close)
        self.toPSAction.triggered.connect(self.exportToPS)
        self.pageToImageAction.triggered.connect(self.exportPageToImage)
        self.docInfoAction.triggered.connect(self.docInfo)
        self.zoominAction.triggered.connect(self.zoomIn)
        self.zoomoutAction.triggered.connect(self.zoomOut)
        self.undoJumpAction.triggered.connect(self.undoJump)
        self.prevPageAction.triggered.connect(self.goPrevPage)
        self.nextPageAction.triggered.connect(self.goNextPage)
        self.firstPageAction.triggered.connect(self.goFirstPage)
        self.lastPageAction.triggered.connect(self.goLastPage)
        # Create widgets for menubar / toolbar
        self.gotoPageEdit = QLineEdit(self)
        self.gotoPageEdit.setPlaceholderText("Jump to page...")
        self.gotoPageEdit.setMaximumWidth(120)
        self.gotoPageEdit.returnPressed.connect(self.gotoPage)
        self.gotoPageValidator = QIntValidator(1, 1, self.gotoPageEdit)
        self.gotoPageEdit.setValidator(self.gotoPageValidator)
        self.pageNoLabel = QLabel(self)
        self.pageNoLabel.setFrameShape(QFrame.StyledPanel)
        spacer = QWidget(self)
        spacer.setSizePolicy(1 | 2 | 4, 1 | 4)
        self.zoomLevelCombo = QComboBox(self)
        self.zoomLevelCombo.addItems([
            "Fixed Width", "75%", "90%", "100%", "110%", "121%", "133%",
            "146%", "175%", "200%"
        ])
        self.zoomLevelCombo.activated.connect(self.setZoom)
        self.zoom_levels = [0, 75, 90, 100, 110, 121, 133, 146, 175, 200]
        # Add toolbar actions
        self.toolBar.addAction(self.openFileAction)
        self.toolBar.addAction(self.printAction)
        self.toolBar.addSeparator()
        self.toolBar.addAction(self.docInfoAction)
        self.toolBar.addSeparator()
        self.toolBar.addAction(self.zoomoutAction)
        self.toolBar.addWidget(self.zoomLevelCombo)
        self.toolBar.addAction(self.zoominAction)
        self.toolBar.addSeparator()
        self.toolBar.addAction(self.firstPageAction)
        self.toolBar.addAction(self.prevPageAction)
        self.toolBar.addWidget(self.pageNoLabel)
        self.toolBar.addAction(self.nextPageAction)
        self.toolBar.addAction(self.lastPageAction)
        self.toolBar.addAction(self.undoJumpAction)
        self.toolBar.addWidget(self.gotoPageEdit)
        self.toolBar.addAction(self.gotoPageAction)
        self.toolBar.addSeparator()
        self.toolBar.addAction(self.copyTextAction)
        self.toolBar.addAction(self.findTextAction)
        #self.toolBar.addAction(self.saveUnlockedAction)
        self.toolBar.addWidget(spacer)
        self.toolBar.addSeparator()
        self.toolBar.addAction(self.quitAction)
        # Add widgets
        self.statusbar = QLabel(self)
        self.statusbar.setStyleSheet(
            "QLabel { font-size: 12px; border-radius: 2px; padding: 2px; background: palette(highlight); color: palette(highlighted-text); }"
        )
        self.statusbar.setMaximumHeight(16)
        self.statusbar.hide()
        # Impoort settings
        desktop = QApplication.desktop()
        self.settings = QtCore.QSettings("gospel-pdf", "main", self)
        self.recent_files = self.settings.value("RecentFiles", [])
        self.history_filenames = self.settings.value("HistoryFileNameList", [])
        self.history_page_no = self.settings.value("HistoryPageNoList", [])
        self.offset_x = int(self.settings.value("OffsetX", 4))
        self.offset_y = int(self.settings.value("OffsetY", 26))
        self.available_area = [
            desktop.availableGeometry().width(),
            desktop.availableGeometry().height()
        ]
        self.zoomLevelCombo.setCurrentIndex(
            int(self.settings.value("ZoomLevel", 2)))
        # Connect Signals
        self.scrollArea.verticalScrollBar().valueChanged.connect(
            self.onMouseScroll)
        self.scrollArea.verticalScrollBar().sliderReleased.connect(
            self.onSliderRelease)
        self.findTextEdit.returnPressed.connect(self.findNext)
        self.findNextButton.clicked.connect(self.findNext)
        self.findBackButton.clicked.connect(self.findBack)
        self.findCloseButton.clicked.connect(self.dockSearch.hide)
        self.dockSearch.visibilityChanged.connect(self.toggleFindMode)
        # Create separate thread and move renderer to it
        self.thread1 = QtCore.QThread(self)
        self.renderer1 = Renderer(0)
        self.renderer1.moveToThread(
            self.thread1)  # this must be moved before connecting signals
        self.renderRequested.connect(self.renderer1.render)
        self.loadFileRequested.connect(self.renderer1.loadDocument)
        self.findTextRequested.connect(self.renderer1.findText)
        self.renderer1.rendered.connect(self.setRenderedImage)
        self.renderer1.textFound.connect(self.onTextFound)
        self.thread1.start()
        self.thread2 = QtCore.QThread(self)
        self.renderer2 = Renderer(1)
        self.renderer2.moveToThread(self.thread2)
        self.renderRequested.connect(self.renderer2.render)
        self.loadFileRequested.connect(self.renderer2.loadDocument)
        self.renderer2.rendered.connect(self.setRenderedImage)
        self.thread2.start()
        # Initialize Variables
        self.doc = None
        self.filename = ''
        self.passwd = ''
        self.pages = []
        self.jumped_from = None
        self.max_preload = 1
        self.recent_files_actions = []
        self.addRecentFiles()
        # Show Window
        width = int(self.settings.value("WindowWidth", 1040))
        height = int(self.settings.value("WindowHeight", 717))
        self.resize(width, height)
        self.show()

    def addRecentFiles(self):
        self.recent_files_actions[:] = []  # pythonic way to clear list
        self.menuRecentFiles.clear()
        for each in self.recent_files:
            name = elideMiddle(os.path.basename(each), 60)
            action = self.menuRecentFiles.addAction(name, self.openRecentFile)
            self.recent_files_actions.append(action)
        self.menuRecentFiles.addSeparator()
        self.menuRecentFiles.addAction(QIcon(':/edit-clear.png'),
                                       'Clear Recents', self.clearRecents)

    def openRecentFile(self):
        action = self.sender()
        index = self.recent_files_actions.index(action)
        self.loadPDFfile(self.recent_files[index])

    def clearRecents(self):
        self.recent_files_actions[:] = []
        self.menuRecentFiles.clear()
        self.recent_files[:] = []

    def removeOldDoc(self):
        if not self.doc:
            return
        # Save current page number
        self.saveFileData()
        # Remove old document
        for i in range(len(self.pages)):
            self.verticalLayout.removeWidget(self.pages[-1])
        for i in range(len(self.pages)):
            self.pages.pop().deleteLater()
        self.frame.deleteLater()
        self.jumped_from = None
        self.addRecentFiles()

    def loadPDFfile(self, filename):
        """ Loads pdf document in all threads """
        filename = os.path.expanduser(filename)
        doc = Poppler.Document.load(filename)
        if not doc: return
        password = ''
        if doc.isLocked():
            password = QInputDialog.getText(self, 'This PDF is locked',
                                            'Enter Password :'******'':
                if self.doc == None: sys.exit(1)  #exif if first document
                else: return
            locked = doc.unlock(password.encode(), password.encode())
            if locked:
                return QMessageBox.critical(self, "Failed !",
                                            "Incorrect Password")
            self.passwd = password
            self.lockUnlockAction.setText("Save Unlocked")
        else:
            self.lockUnlockAction.setText("Encrypt PDF")
        self.removeOldDoc()
        doc.setRenderHint(Poppler.Document.TextAntialiasing
                          | Poppler.Document.TextHinting
                          | Poppler.Document.Antialiasing
                          | Poppler.Document.ThinLineSolid)
        self.doc = doc
        self.filename = filename
        self.pages_count = self.doc.numPages()
        self.current_page = 1
        self.rendered_pages = []
        self.getOutlines(self.doc)
        # Load Document in other threads
        self.loadFileRequested.emit(self.filename, password)
        if collapseUser(self.filename) in self.history_filenames:
            self.current_page = int(
                self.history_page_no[self.history_filenames.index(
                    collapseUser(self.filename))])
        self.current_page = min(self.current_page, self.pages_count)
        self.scroll_render_lock = False
        # Add widgets
        self.frame = Frame(self.scrollAreaWidgetContents, self.scrollArea)
        self.verticalLayout = QVBoxLayout(self.frame)
        self.horizontalLayout_2.addWidget(self.frame)
        self.scrollArea.verticalScrollBar().setValue(0)
        self.frame.jumpToRequested.connect(self.jumpToPage)
        self.frame.copyTextRequested.connect(self.copyText)
        self.frame.showStatusRequested.connect(self.showStatus)

        # Render 4 pages, (Preload 3 pages)
        self.max_preload = min(4, self.pages_count)
        # Add pages
        for i in range(self.pages_count):
            page = PageWidget(i + 1, self.frame)
            self.verticalLayout.addWidget(page, 0, QtCore.Qt.AlignCenter)
            self.pages.append(page)
        self.resizePages()
        self.pageNoLabel.setText('<b>%i/%i</b>' %
                                 (self.current_page, self.pages_count))
        self.gotoPageValidator.setTop(self.pages_count)
        self.setWindowTitle(
            os.path.basename(self.filename) + " - Gospel PDF " + __version__)
        if self.current_page != 1:
            QtCore.QTimer.singleShot(150 + self.pages_count // 3,
                                     self.jumpToCurrentPage)

    def setRenderedImage(self, page_no, image):
        """ takes a QImage and sets pixmap of the specified page
            when number of rendered pages exceeds a certain number, old page image is
            deleted to save memory """
        debug("Set Rendered Image :", page_no)
        self.pages[page_no - 1].setPageData(page_no, QPixmap.fromImage(image),
                                            self.doc.page(page_no - 1))
        # Request to render next page
        if self.current_page <= page_no < (self.current_page +
                                           self.max_preload - 2):
            if (page_no + 2 not in self.rendered_pages) and (page_no + 2 <=
                                                             self.pages_count):
                self.rendered_pages.append(page_no + 2)
                self.renderRequested.emit(page_no + 2,
                                          self.pages[page_no + 1].dpi)
        # Replace old rendered pages with blank image
        if len(self.rendered_pages) > 10:
            cleared_page_no = self.rendered_pages.pop(0)
            debug("Clear Page :", cleared_page_no)
            self.pages[cleared_page_no - 1].clear()
        debug("Rendered Pages :", self.rendered_pages)

    def renderCurrentPage(self):
        """ Requests to render current page. if it is already rendered, then request
            to render next unrendered page """
        requested = 0
        for page_no in range(self.current_page,
                             self.current_page + self.max_preload):
            if (page_no not in self.rendered_pages) and (page_no <=
                                                         self.pages_count):
                self.rendered_pages.append(page_no)
                self.renderRequested.emit(page_no, self.pages[page_no - 1].dpi)
                requested += 1
                debug("Render Requested :", page_no)
                if requested == 2: return

    def onMouseScroll(self, pos):
        """ It is called when vertical scrollbar value is changed.
            Get the current page number on scrolling, then requests to render"""
        index = self.verticalLayout.indexOf(
            self.frame.childAt(self.frame.width() / 2, pos))
        if index == -1: return
        self.pageNoLabel.setText('<b>%i/%i</b>' %
                                 (index + 1, self.pages_count))
        if self.scrollArea.verticalScrollBar().isSliderDown(
        ) or self.scroll_render_lock:
            return
        self.current_page = index + 1
        self.renderCurrentPage()

    def onSliderRelease(self):
        self.onMouseScroll(self.scrollArea.verticalScrollBar().value())

    def openFile(self):
        filename, sel_filter = QFileDialog.getOpenFileName(
            self, "Select Document to Open", "",
            "Portable Document Format (*.pdf);;All Files (*)")
        if filename != "":
            self.loadPDFfile(filename)

    def lockUnlock(self):
        if which("qpdf") == None:
            self.lockUnlockAction.setEnabled(False)
            QMessageBox.warning(
                self, "qpdf Required",
                "qpdf command not found.\nInstall qpdf program.")
            return
        if self.lockUnlockAction.text() == "Encrypt PDF":
            self.encryptPDF()
            return
        filename, ext = os.path.splitext(self.filename)
        new_name = filename + "-unlocked.pdf"
        proc = Popen([
            "qpdf", "--decrypt", "--password="******"File Saved",
                                    "Successfully saved as\n" + basename)
        else:
            QMessageBox.warning(self, "Failed !", "Failed to save as unlocked")

    def encryptPDF(self):
        password, ok = QInputDialog.getText(self, "Lock PDF",
                                            "Enter Password :"******"":
            return
        filename, ext = os.path.splitext(self.filename)
        new_name = filename + "-locked.pdf"
        proc = Popen([
            "qpdf", "--encrypt", password, password, '128', '--',
            self.filename, new_name
        ])
        stdout, stderr = proc.communicate()
        if proc.returncode == 0:
            basename = os.path.basename(new_name)
            QMessageBox.information(self, "File Saved",
                                    "Successfully saved as\n" + basename)
        else:
            QMessageBox.warning(self, "Failed !",
                                "Failed to save as Encrypted")

    def printFile(self):
        if which("quikprint") == None:
            QMessageBox.warning(self, "QuikPrint Required",
                                "Install QuikPrint program.")
            return
        Popen(["quikprint", self.filename])

    def exportToPS(self):
        width = self.doc.page(self.current_page - 1).pageSizeF().width()
        height = self.doc.page(self.current_page - 1).pageSizeF().height()
        filename, sel_filter = QFileDialog.getSaveFileName(
            self, "Select File to Save",
            os.path.splitext(self.filename)[0] + '.ps',
            "Adobe Postscript Format (*.ps)")
        if filename == '': return
        conv = self.doc.psConverter()
        conv.setPaperWidth(width)
        conv.setPaperHeight(height)
        conv.setOutputFileName(filename)
        conv.setPageList([i + 1 for i in range(self.pages_count)])
        ok = conv.convert()
        if ok:
            QMessageBox.information(self, "Successful !",
                                    "File has been successfully exported")
        else:
            QMessageBox.warning(self, "Failed !",
                                "Failed to export to Postscript")

    def exportPageToImage(self):
        dialog = ExportToImageDialog(self.current_page, self.pages_count, self)
        if dialog.exec_() == QDialog.Accepted:
            try:
                dpi = int(dialog.dpiEdit.text())
                page_no = dialog.pageNoSpin.value()
                filename = os.path.splitext(
                    self.filename)[0] + '-' + str(page_no) + '.jpg'
                page = self.doc.page(page_no - 1)
                if not page: return
                img = page.renderToImage(dpi, dpi)
                img.save(filename)
                QMessageBox.information(self, "Successful !",
                                        "Page has been successfully exported")
            except:
                QMessageBox.warning(self, "Failed !",
                                    "Failed to export to Image")

    def docInfo(self):
        info_keys = list(self.doc.infoKeys())
        values = [self.doc.info(key) for key in info_keys]
        page_size = self.doc.page(self.current_page - 1).pageSizeF()
        page_size = "%s x %s pts" % (page_size.width(), page_size.height())
        info_keys += ['Embedded FIles', 'Page Size']
        values += [str(self.doc.hasEmbeddedFiles()), page_size]
        dialog = DocInfoDialog(self)
        dialog.setInfo(info_keys, values)
        dialog.exec_()

    def jumpToCurrentPage(self):
        """ this is used as a slot, to connect with a timer"""
        self.jumpToPage(self.current_page)

    def jumpToPage(self, page_num, top=0.0):
        """ scrolls to a particular page and position """
        if page_num < 1: page_num = 1
        elif page_num > self.pages_count: page_num = self.pages_count
        if not (0 < top < 1.0): top = 0
        self.jumped_from = self.current_page
        self.current_page = page_num
        scrollbar_pos = self.pages[page_num - 1].pos().y()
        scrollbar_pos += top * self.pages[page_num - 1].height()
        self.scrollArea.verticalScrollBar().setValue(scrollbar_pos)

    def undoJump(self):
        if self.jumped_from == None: return
        self.jumpToPage(self.jumped_from)

    def goNextPage(self):
        if self.current_page == self.pages_count: return
        self.jumpToPage(self.current_page + 1)

    def goPrevPage(self):
        if self.current_page == 1: return
        self.jumpToPage(self.current_page - 1)

    def goFirstPage(self):
        self.jumpToPage(1)

    def goLastPage(self):
        self.jumpToPage(self.pages_count)

    def gotoPage(self):
        text = self.gotoPageEdit.text()
        if text == "": return
        self.jumpToPage(int(text))
        self.gotoPageEdit.clear()
        self.gotoPageEdit.clearFocus()

######################  Zoom and Size Management  ##########################

    def availableWidth(self):
        """ Returns available width for rendering a page """
        dock_width = 0 if self.dockWidget.isHidden(
        ) else self.dockWidget.width()
        return self.width() - dock_width - 50

    def resizePages(self):
        '''Resize all pages according to zoom level '''
        page_dpi = self.zoom_levels[
            self.zoomLevelCombo.currentIndex()] * SCREEN_DPI / 100
        fixed_width = self.availableWidth()
        for i in range(self.pages_count):
            pg_width = self.doc.page(i).pageSizeF().width()  # width in points
            pg_height = self.doc.page(i).pageSizeF().height()
            if self.zoomLevelCombo.currentIndex() == 0:  # if fixed width
                dpi = 72.0 * fixed_width / pg_width
            else:
                dpi = page_dpi
            self.pages[i].dpi = dpi
            self.pages[i].setFixedSize(pg_width * dpi / 72.0,
                                       pg_height * dpi / 72.0)
        for page_no in self.rendered_pages:
            self.pages[page_no - 1].clear()
        self.rendered_pages = []
        self.renderCurrentPage()

    def setZoom(self, index):
        """ Gets called when zoom level is changed"""
        self.scroll_render_lock = True  # rendering on scroll is locked as set scroll position
        self.resizePages()
        QtCore.QTimer.singleShot(300, self.afterZoom)

    def zoomIn(self):
        index = self.zoomLevelCombo.currentIndex()
        if index == len(self.zoom_levels) - 1: return
        if index == 0: index = 3
        self.zoomLevelCombo.setCurrentIndex(index + 1)
        self.setZoom(index + 1)

    def zoomOut(self):
        index = self.zoomLevelCombo.currentIndex()
        if index == 1: return
        if index == 0: index = 4
        self.zoomLevelCombo.setCurrentIndex(index - 1)
        self.setZoom(index - 1)

    def afterZoom(self):
        scrolbar_pos = self.pages[self.current_page - 1].pos().y()
        self.scrollArea.verticalScrollBar().setValue(scrolbar_pos)
        self.scroll_render_lock = False
#########            Search Text            #########

    def toggleFindMode(self, enable):
        if enable:
            self.findTextEdit.setText('')
            self.findTextEdit.setFocus()
            self.search_text = ''
            self.search_result_page = 0
        elif self.search_result_page != 0:
            self.pages[self.search_result_page - 1].highlight_area = None
            self.pages[self.search_result_page - 1].updateImage()

    def findNext(self):
        """ search text in current page and next pages """
        text = self.findTextEdit.text()
        if text == "": return
        # search from current page when text changed
        if self.search_text != text or self.search_result_page == 0:
            search_from_page = self.current_page
        else:
            search_from_page = self.search_result_page + 1
        self.findTextRequested.emit(text, search_from_page, False)
        if self.search_result_page != 0:  # clear previous highlights
            self.pages[self.search_result_page - 1].highlight_area = None
            self.pages[self.search_result_page - 1].updateImage()
            self.search_result_page = 0
        self.search_text = text

    def findBack(self):
        """ search text in pages before current page """
        text = self.findTextEdit.text()
        if text == "": return
        if self.search_text != text or self.search_result_page == 0:
            search_from_page = self.current_page
        else:
            search_from_page = self.search_result_page - 1
        self.findTextRequested.emit(text, search_from_page, True)
        if self.search_result_page != 0:
            self.pages[self.search_result_page - 1].highlight_area = None
            self.pages[self.search_result_page - 1].updateImage()
            self.search_result_page = 0
        self.search_text = text

    def onTextFound(self, page_no, areas):
        self.pages[page_no - 1].highlight_area = areas
        self.search_result_page = page_no
        if self.pages[page_no - 1].pixmap():
            self.pages[page_no - 1].updateImage()
        first_result_pos = areas[0].y() / self.doc.page(page_no -
                                                        1).pageSize().height()
        self.jumpToPage(page_no, first_result_pos)

#########      Cpoy Text to Clip Board      #########

    def toggleCopyText(self, checked):
        self.frame.enableCopyTextMode(checked)

    def copyText(self, page_no, top_left, bottom_right):
        zoom = self.pages[page_no -
                          1].height() / self.doc.page(page_no -
                                                      1).pageSize().height()
        # Copy text to clipboard
        text = self.doc.page(page_no - 1).text(
            QtCore.QRectF(top_left / zoom, bottom_right / zoom))
        QApplication.clipboard().setText(text)
        self.copyTextAction.setChecked(False)
        self.toggleCopyText(False)


##########      Other Functions      ##########

    def getOutlines(self, doc):
        toc = doc.toc()
        if not toc:
            self.dockWidget.hide()
            return
        self.dockWidget.show()
        outline_model = QStandardItemModel(self)
        parent_item = outline_model.invisibleRootItem()
        node = toc.firstChild()
        loadOutline(doc, node, parent_item)
        self.treeView.setModel(outline_model)
        if parent_item.rowCount() < 4:
            self.treeView.expandToDepth(0)
        self.treeView.setHeaderHidden(True)
        self.treeView.header().setSectionResizeMode(0, 1)
        self.treeView.header().setSectionResizeMode(1, 3)
        self.treeView.header().setStretchLastSection(False)

    def onOutlineClick(self, m_index):
        page_num = self.treeView.model().data(m_index, QtCore.Qt.UserRole + 1)
        top = self.treeView.model().data(m_index, QtCore.Qt.UserRole + 2)
        if not page_num: return
        self.jumpToPage(page_num, top)

    def showStatus(self, url):
        if url == "":
            self.statusbar.hide()
            return
        self.statusbar.setText(url)
        self.statusbar.adjustSize()
        self.statusbar.move(0, self.height() - self.statusbar.height())
        self.statusbar.show()

    def resizeEvent(self, ev):
        QMainWindow.resizeEvent(self, ev)
        if self.filename == '': return
        if self.zoomLevelCombo.currentIndex() == 0:
            self.resize_page_timer.start(200)

    def onWindowResize(self):
        for i in range(self.pages_count):
            self.pages[
                i].annots_listed = False  # Clears prev link annotation positions
        self.resizePages()
        wait(300)
        self.jumpToCurrentPage()
        if not self.isMaximized():
            self.settings.setValue("WindowWidth", self.width())
            self.settings.setValue("WindowHeight", self.height())

    def saveFileData(self):
        if self.filename != '':
            filename = collapseUser(self.filename)
            if filename in self.history_filenames:
                index = self.history_filenames.index(filename)
                self.history_page_no[index] = self.current_page
            else:
                self.history_filenames.insert(0, filename)
                self.history_page_no.insert(0, self.current_page)
            if filename in self.recent_files:
                self.recent_files.remove(filename)
            self.recent_files.insert(0, filename)

    def closeEvent(self, ev):
        """ Save all settings on window close """
        self.saveFileData()
        self.settings.setValue("OffsetX", self.geometry().x() - self.x())
        self.settings.setValue("OffsetY", self.geometry().y() - self.y())
        self.settings.setValue("ZoomLevel", self.zoomLevelCombo.currentIndex())
        self.settings.setValue("HistoryFileNameList",
                               self.history_filenames[:100])
        self.settings.setValue("HistoryPageNoList", self.history_page_no[:100])
        self.settings.setValue("RecentFiles", self.recent_files[:10])
        return QMainWindow.closeEvent(self, ev)

    def onAppQuit(self):
        """ Close running threads """
        loop1 = QtCore.QEventLoop()
        loop2 = QtCore.QEventLoop()
        self.thread1.finished.connect(loop1.quit)
        self.thread2.finished.connect(loop2.quit)
        self.thread1.quit()
        loop1.exec_()
        self.thread2.quit()
        loop2.exec_()
コード例 #16
0
ファイル: padulator.py プロジェクト: sparsile/padulator
class PadCalc(QWidget):
    def __init__(self):
        super().__init__()
        load_data()

        card_tags = ['leader','sub1','sub2','sub3','sub4','friend']
        self.cards = { t: CardIcon() for t in card_tags }

        self.vlayout = QVBoxLayout(self)
        self.vlayout.setSpacing(0)
        self.setLayout(self.vlayout)

        self.userbox = QHBoxLayout()
        userfield = QLineEdit()
        userbutton = QPushButton('Load')
        userbutton.clicked.connect(lambda: self.set_user(userfield.text()))
        self.userbox.addWidget(userfield)
        self.userbox.addWidget(userbutton)
        userfield.returnPressed.connect(userbutton.click)
        self.vlayout.addLayout(self.userbox)

        maxcheckbox = QCheckBox('Use maxed stats?')
        maxcheckbox.stateChanged[int].connect(self.setMaxed)
        self.vlayout.addWidget(maxcheckbox)


        self.teamchooser = QComboBox(self)
        self.teamchooser.currentIndexChanged[int].connect(self.set_team)
        self.vlayout.addWidget(self.teamchooser)

        teambox = QHBoxLayout()
        teambox.addStretch(1)
        for card in card_tags:
            teambox.addWidget(self.cards[card])

        teambox.setSpacing(0)
        teambox.addStretch(1)
        teambox.setAlignment(Qt.AlignCenter)
        self.vlayout.addLayout(teambox)

        self.board = Board()
        self.vlayout.addWidget(self.board)
        self.vlayout.itemAt(self.vlayout.indexOf(self.board)).setAlignment(Qt.AlignCenter)

        self.orbchooser = QHBoxLayout()
        b = OrbButton(value = 0)
        b.clicked.connect(functools.partial(self.setPaintOrb,Orb.Null))
        self.orbchooser.addWidget(b)
        for i in ORBS:
            b = OrbButton(value=i)
            #print('Setting click value of button %s to %s' % (id(b),i))
            b.clicked.connect(functools.partial(self.setPaintOrb,i))
            self.orbchooser.addWidget(b)

        self.vlayout.addLayout(self.orbchooser)

        self.damagereadout = QLabel()
        font = QFont()
        font.setPointSize(30)
        self.damagereadout.setAlignment(Qt.AlignCenter)
        self.damagereadout.setFont(font)
        self.vlayout.addWidget(self.damagereadout)
        self.board.valueChanged.connect(self.update_damage)

        labels = ['atk','combos','leaders','enhance','prongs','rows']
        lfont = QFont()
        lfont.setPointSize(9)
        vfont = QFont()
        vfont.setPointSize(12)
        self.details = {key: QVBoxLayout() for key in labels}
        for i in labels:
            label = QLabel(i)
            label.setFont(lfont)
            label.setAlignment(Qt.AlignCenter)
            label.setMargin(0)
            label.setContentsMargins(0,0,0,0)
            label.setIndent(0)
            self.details[i].label = label
            self.details[i].addWidget(self.details[i].label)
            value = QLabel('1')
            value.setFont(vfont)
            value.setAlignment(Qt.AlignCenter)
            value.setMargin(0)
            value.setIndent(0)
            value.setContentsMargins(0,0,0,0)
            self.details[i].value = value
            self.details[i].addWidget(self.details[i].value)
            self.details[i].setContentsMargins(1,1,1,1)

        self.detailreadout = QHBoxLayout()
        for i in labels:
            self.detailreadout.addLayout(self.details[i])
            timeslabel = QLabel('\u00d7')
            timeslabel.setMargin(0)
            timeslabel.setIndent(0)
            timeslabel.setAlignment(Qt.AlignCenter)
            timeslabel.setContentsMargins(0,0,0,0)
            self.detailreadout.addWidget(timeslabel)

        self.detailreadout.takeAt(self.detailreadout.count()-1)
        self.vlayout.addLayout(self.detailreadout)

        self.vlayout.addStretch(1000)
        self.skillbox = QHBoxLayout()
        self.vlayout.addLayout(self.skillbox)
        #self.set_user('korora')

    def setMaxed(self,state):
        Card.use_max_stats = (state == Qt.Checked)
        self.drawui()
        self.update_damage()
        self.set_team(self.teamchooser.currentIndex())

    def setPaintOrb(self,orb):
        global paintOrb
        paintOrb = orb

    def set_user(self,username):
        newuser = User(username)

        if hasattr(self,'user'):
            olduser = self.user.username
        else:
            olduser = ''

        if hasattr(newuser,'teams') and len(newuser.teams) > 0:
            teamchooser = self.teamchooser
            self.user = newuser
            index = teamchooser.currentIndex()
            try:
                teamchooser.currentIndexChanged[int].disconnect()
            except:
                return

            teamchooser.clear()
            for team in self.user.teams:
                teamchooser.addItem('%s' % (team['name']))

            if newuser.username != olduser:
                self.set_team(0)
            else:
                teamchooser.setCurrentIndex(index)
                self.set_team(index)

            teamchooser.currentIndexChanged[int].connect(self.set_team)

    def update_damage(self):
        (match,enhanced,row) = self.board.match()
        nmatch = sum(len(v) for v in match.values())
        nrow = sum(v for v in row.values())
        (dmg, multipliers) = compute_damage((match,enhanced,row),self.team)
        self.damagereadout.setText('{:,}'.format(round(sum([sum(i) for i in dmg.values()]))))
        for i in multipliers:
            if i is not 'atk':
                self.details[i].value.setText('%.2f' % multipliers[i])
            else:
                self.details[i].value.setText('%d' % multipliers[i])

        for card in self.cards.values():
            # add a damage label
            dam = dmg[card.card]
            card.main_attack = dam[0]
            card.sub_attack = dam[1]
            card.repaint()

    def set_team(self,index):
        teamdata = self.user.teams[index]
        team = []
        for i in ['leader','sub1','sub2','sub3','sub4']:
            team += [Card().load_from_id(self.user,teamdata[i])]
        friend = {
                'monster': teamdata['friend_leader'],
                'plus_atk': teamdata['friend_atk'],
                'plus_hp': teamdata['friend_hp'],
                'plus_rcv': teamdata['friend_rcv'],
                'current_awakening': teamdata['friend_awakening'],
                'lv': teamdata['friend_level'],
                'current_skill': teamdata['friend_skill']
                }
        team += [Card().load_from_card(friend)]
        self.team = Team(team)
        #print('|'+self.teamchooser.itemText(index)+'|')
        #print(len(self.teamchooser.itemText(index)))
        self.teamchooser.setMinimumContentsLength(len(self.teamchooser.itemText(index))-3)
        for i in range(self.skillbox.count()):
            w = self.skillbox.takeAt(i)
            w.widget().deleteLater()
            
        #svc = SkillViewController(skill=self.team.lskills[0])
        #svc.skillsChanged.connect(self.update_damage)
        #self.skillbox.addWidget(svc)
        self.drawui()
        self.update_damage()

    def drawui(self):
        self.cards['leader'].card = (self.team.cards[0])
        self.cards['sub1'].card = (self.team.cards[1])
        self.cards['sub2'].card = (self.team.cards[2])
        self.cards['sub3'].card = (self.team.cards[3])
        self.cards['sub4'].card = (self.team.cards[4])
        self.cards['friend'].card = (self.team.cards[5])
        for card in self.cards.values():
            card.load_icon()
コード例 #17
0
ファイル: protocol.py プロジェクト: Neutree/COMTool
class Plugin(Plugin_Base):
    '''
        call sequence:
            set vars like hintSignal, hintSignal
            onInit
            onWidget
            onUiInitDone
                send
                onReceived
            getConfig
    '''
    # vars set by caller
    isConnected = lambda: False
    send = lambda x, y: None  # send(data_bytes=None, file_path=None, callback=lambda ok,msg:None)
    hintSignal = None  # hintSignal.emit(type(error, warning, info), title, msg)
    configGlobal = {}
    # other vars
    connParent = "dbg"  # parent id
    connChilds = []  # children ids
    id = "protocol"
    name = _("protocol")

    enabled = False  # user enabled this plugin
    active = False  # using this plugin

    showReceiveDataSignal = pyqtSignal(str)

    def __init__(self):
        super().__init__()
        if not self.id:
            raise ValueError(f"var id of Plugin {self} should be set")

    def onInit(self, config):
        '''
            init params, DO NOT take too long time in this func
        '''
        default = {
            "version":
            1,
            "sendAscii":
            True,
            "useCRLF":
            False,
            "sendEscape":
            True,
            "code":
            defaultProtocols.copy(),
            "currCode":
            "default",
            "customSendItems": [{
                "text": "hello",
                "remark": "hello",
                "icon": "fa5.hand-paper"
            }, {
                "text": "\\x01\\x03\\x03\\x03\\x03\\x01",
                "remark": "pre",
                "icon": "ei.arrow-left",
                "shortcut": [[16777234, "Left"]]
            }, {
                "text": "\\x01\\x04\\x04\\x04\\x04\\x01",
                "remark": "next",
                "icon": "ei.arrow-right",
                "shortcut": [[16777236, "Right"]]
            }, {
                "text": "\\x01\\x01\\x01\\x01\\x01\\x01",
                "remark": "ok",
                "icon": "fa.circle-o",
                "shortcut": [[16777220, "Return"]]
            }, {
                "text": "\\x01\\x02\\x02\\x02\\x02\\x01",
                "remark": "ret",
                "icon": "ei.return-key",
                "shortcut": [[16777216, "Esc"]]
            }]
        }
        self.config = config
        for k in default:
            if not k in self.config:
                self.config[k] = default[k]
        self.editingDefaults = False
        self.codeGlobals = {
            "unpack": unpack,
            "pack": pack,
            "crc": crc,
            "encoding": self.configGlobal["encoding"],
            "print": self.print
        }
        self.encodeMethod = lambda x: x
        self.decodeMethod = lambda x: x
        self.pressedKeys = []
        self.keyModeClickTime = 0

    def print(self, *args, **kw_args):
        end = "\n"
        start = "[MSG]: "
        if "end" in kw_args:
            end = kw_args["end"]
        if "start" in kw_args:
            start = kw_args["start"]
        string = start + " ".join(map(str, args)) + end
        self.showReceiveDataSignal.emit(string)

    class ModeButton(QPushButton):
        onFocusIn = pyqtSignal(QFocusEvent)
        onFocusOut = pyqtSignal(QFocusEvent)

        def __init__(self, text, eventFilter, parent=None) -> None:
            super().__init__(text, parent)
            self.installEventFilter(eventFilter)

        def focusInEvent(self, event):
            self.onFocusIn.emit(event)

        def focusOutEvent(self, event):
            self.onFocusOut.emit(event)

    def onWidgetMain(self, parent):
        self.mainWidget = QSplitter(Qt.Vertical)
        self.receiveWidget = TextEdit()
        font = QFont(
            'Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace, Microsoft YaHei',
            10)
        self.receiveWidget.setFont(font)
        self.receiveWidget.setLineWrapMode(TextEdit.NoWrap)
        self.clearBtn = QPushButton("")
        self.keyBtneventFilter = self.ModeButtonEventFilter(
            self.onModeBtnKeyPressEvent, self.onModeBtnKeyReleaseEvent)
        self.keyModeBtn = self.ModeButton(_("Key mode"),
                                          self.keyBtneventFilter)
        layoutClearMode = QHBoxLayout()
        layoutClearMode.addWidget(self.clearBtn)
        layoutClearMode.addWidget(self.keyModeBtn)
        clearModeWidget = QWidget()
        clearModeWidget.setLayout(layoutClearMode)
        utils_ui.setButtonIcon(self.clearBtn, "mdi6.broom")
        self.addButton = QPushButton("")
        utils_ui.setButtonIcon(self.addButton, "fa.plus")
        self.customSendScroll = QScrollArea()
        self.customSendScroll.setMinimumHeight(
            parameters.customSendItemHeight + 20)
        self.customSendScroll.setWidgetResizable(True)
        self.customSendScroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.customSendScroll.setHorizontalScrollBarPolicy(
            Qt.ScrollBarAlwaysOff)
        cutomSendItemsWraper = QWidget()
        self.customSendScroll.setWidget(cutomSendItemsWraper)
        #   wrapper widget
        cutomSendItemsWraper0 = QWidget()
        cutomSendItemsWraper0.setProperty("class", "scrollbar2")
        layout0 = QVBoxLayout()
        layout0.setContentsMargins(0, 8, 0, 0)
        cutomSendItemsWraper0.setLayout(layout0)
        layout0.addWidget(self.customSendScroll)
        customSendItemsLayoutWrapper = QVBoxLayout()
        customSendItemsLayoutWrapper.setContentsMargins(0, 0, 0, 0)
        cutomSendItemsWraper.setLayout(customSendItemsLayoutWrapper)
        # items container
        self.customItems = QWidget()
        self.customSendItemsLayout = QVBoxLayout()
        self.customSendItemsLayout.setContentsMargins(0, 0, 0, 0)
        self.customItems.setLayout(self.customSendItemsLayout)

        customSendItemsLayoutWrapper.addWidget(self.customItems)
        customSendItemsLayoutWrapper.addWidget(self.addButton)
        customSendItemsLayoutWrapper.addStretch(0)

        self.mainWidget.addWidget(self.receiveWidget)
        self.mainWidget.addWidget(clearModeWidget)
        self.mainWidget.addWidget(cutomSendItemsWraper0)
        self.mainWidget.setStretchFactor(0, 2)
        self.mainWidget.setStretchFactor(1, 1)
        self.mainWidget.setStretchFactor(2, 11)
        # event
        self.addButton.clicked.connect(lambda: self.insertSendItem())

        def clearReceived():
            self.receiveWidget.clear()
            self.statusBar.clear()

        self.clearBtn.clicked.connect(clearReceived)

        def keyModeOn(event):
            self.keyModeBtn.setProperty("class", "deleteBtn")
            utils_ui.updateStyle(self.mainWidget, self.keyModeBtn)
            self.keyModeClickTime = time.time()
            # show all shortcut
            widgets = self.customItems.findChildren(QPushButton, "editRemark")
            for i, w in enumerate(widgets):
                shortcut = "+".join(
                    (name for v, name in self.config["customSendItems"][i]
                     ["shortcut"]))
                w.setText(shortcut)
                utils_ui.updateStyle(self.mainWidget, w)

        def keyModeOff(event):
            self.keyModeBtn.setProperty("class", "")
            utils_ui.updateStyle(self.mainWidget, self.keyModeBtn)
            self.keyModeClickTime = 0
            # remove all preesed keys even them not release actually, to avoid window swith by ALT+TAB bug
            self.pressedKeys = []
            # hide all shortcut
            widgets = self.customItems.findChildren(QPushButton, "editRemark")
            for w in widgets:
                w.setText("")

        def keyModeTuggle():
            if self.keyModeBtn.property("class") == "deleteBtn":
                if time.time() - self.keyModeClickTime < 0.2:
                    return
                else:
                    self.keyModeBtn.clearFocus()

        self.keyModeBtn.onFocusIn.connect(keyModeOn)
        self.keyModeBtn.onFocusOut.connect(keyModeOff)
        self.keyModeBtn.clicked.connect(keyModeTuggle)
        return self.mainWidget

    def onWidgetSettings(self, parent):
        root = QWidget()
        rootLayout = QVBoxLayout()
        rootLayout.setContentsMargins(0, 0, 0, 0)
        root.setLayout(rootLayout)
        setingGroup = QGroupBox(_("En-decoding settings"))
        layout = QGridLayout()
        setingGroup.setLayout(layout)
        self.codeItems = ComboBox()
        self.codeItemCustomStr = _("Custom, input name")
        self.codeItemLoadDefaultsStr = _("Load defaults")
        self.codeItems.setEditable(True)
        self.codeWidget = PlainTextEdit()
        self.saveCodeBtn = QPushButton(_("Save"))
        self.saveCodeBtn.setEnabled(False)
        self.deleteCodeBtn = QPushButton(_("Delete"))
        btnLayout = QHBoxLayout()
        btnLayout.addWidget(self.saveCodeBtn)
        btnLayout.addWidget(self.deleteCodeBtn)
        layout.addWidget(QLabel(_("Defaults")), 0, 0, 1, 1)
        layout.addWidget(self.codeItems, 0, 1, 1, 1)
        layout.addWidget(QLabel(_("Code")), 1, 0, 1, 1)
        layout.addWidget(self.codeWidget, 1, 1, 1, 1)
        layout.addLayout(btnLayout, 2, 1, 1, 1)
        serialSendSettingsLayout = QGridLayout()
        sendGroup = QGroupBox(_("Send settings"))
        sendGroup.setLayout(serialSendSettingsLayout)
        self.sendSettingsAscii = QRadioButton(_("ASCII"))
        self.sendSettingsHex = QRadioButton(_("HEX"))
        self.sendSettingsAscii.setToolTip(
            _("Get send data as visible format, select encoding method at top right corner"
              ))
        self.sendSettingsHex.setToolTip(
            _("Get send data as hex format, e.g. hex '31 32 33' equal to ascii '123'"
              ))
        self.sendSettingsAscii.setChecked(True)
        self.sendSettingsCRLF = QCheckBox(_("<CRLF>"))
        self.sendSettingsCRLF.setToolTip(
            _("Select to send \\r\\n instead of \\n"))
        self.sendSettingsCRLF.setChecked(False)
        self.sendSettingsEscape = QCheckBox(_("Escape"))
        self.sendSettingsEscape.setToolTip(
            _("Enable escape characters support like \\t \\r \\n \\x01 \\001"))
        serialSendSettingsLayout.addWidget(self.sendSettingsAscii, 0, 0, 1, 1)
        serialSendSettingsLayout.addWidget(self.sendSettingsHex, 0, 1, 1, 1)
        serialSendSettingsLayout.addWidget(self.sendSettingsCRLF, 1, 0, 1, 1)
        serialSendSettingsLayout.addWidget(self.sendSettingsEscape, 1, 1, 1, 1)

        rootLayout.addWidget(sendGroup)
        rootLayout.addWidget(setingGroup)
        # event
        self.sendSettingsAscii.clicked.connect(lambda: self.bindVar(
            self.sendSettingsAscii, self.config, "sendAscii", bool))
        self.sendSettingsHex.clicked.connect(lambda: self.bindVar(
            self.sendSettingsHex, self.config, "sendAscii", bool, invert=True))
        self.sendSettingsCRLF.clicked.connect(lambda: self.bindVar(
            self.sendSettingsCRLF, self.config, "useCRLF", bool))
        self.sendSettingsEscape.clicked.connect(lambda: self.bindVar(
            self.sendSettingsEscape, self.config, "sendEscape", bool))
        self.saveCodeBtn.clicked.connect(self.saveCode)
        self.deleteCodeBtn.clicked.connect(self.deleteCode)
        self.codeWidget.onSave = self.saveCode
        return root

    def onWidgetStatusBar(self, parent):
        self.statusBar = statusBar(rxTxCount=True)
        return self.statusBar

    def onUiInitDone(self):
        '''
            UI init done, you can update your widget here
            this method runs in UI thread, do not block too long
        '''
        newItems = []
        for item in self.config["customSendItems"]:
            item = self.insertSendItem(item, load=True)
            newItems.append(item)
        self.config["customSendItems"] = newItems
        self.sendSettingsAscii.setChecked(self.config["sendAscii"])
        self.sendSettingsHex.setChecked(not self.config["sendAscii"])
        self.sendSettingsCRLF.setChecked(self.config["useCRLF"])
        self.sendSettingsEscape.setChecked(self.config["sendEscape"])
        self.showReceiveDataSignal.connect(self.showReceivedData)
        # init decoder and encoder
        for k in self.config["code"]:
            self.codeItems.addItem(k)
        self.codeItems.addItem(self.codeItemCustomStr)
        self.codeItems.addItem(self.codeItemLoadDefaultsStr)
        name = self.config["currCode"]
        idx = self.codeItems.findText(self.config["currCode"])
        if idx < 0:
            idx = 0
            name = "default"
        self.codeItems.setCurrentIndex(idx)
        self.selectCode(name)
        self.codeItems.currentIndexChanged.connect(
            self.onCodeItemChanged
        )  # add here to avoid self.selectCode trigger
        self.codeWidget.textChanged.connect(self.onCodeChanged)

    class ModeButtonEventFilter(QObject):
        def __init__(self, keyPressCb, keyReleaseCb) -> None:
            super().__init__()
            self.keyPressCb = keyPressCb
            self.keyReleaseCb = keyReleaseCb

        def eventFilter(self, obj, evt):
            if evt.type() == QEvent.KeyPress:
                # prevent default key events
                self.keyPressCb(evt)
                return True
            elif evt.type() == QEvent.KeyRelease:
                self.keyReleaseCb(evt)
                return True
            return False

    def onModeBtnKeyPressEvent(self, event):
        # send by shortcut
        key = event.key()
        self.pressedKeys.append(key)
        for item in self.config["customSendItems"]:
            if not "shortcut" in item:
                continue
            shortcut = item["shortcut"]
            if len(shortcut) == len(self.pressedKeys):
                same = True
                for i in range(len(shortcut)):
                    if shortcut[i][0] != self.pressedKeys[i]:
                        same = False
                        break
                if same:
                    self.sendCustomItem(item)

    def onModeBtnKeyReleaseEvent(self, event):
        key = event.key()
        if key in self.pressedKeys:
            self.pressedKeys.remove(key)

    def onKeyPressEvent(self, event):
        pass

    def onKeyReleaseEvent(self, event):
        pass

    def insertSendItem(self, item=None, load=False):
        # itemsNum = self.customSendItemsLayout.count() + 1
        # height = parameters.customSendItemHeight * (itemsNum + 1) + 20
        # topHeight = self.receiveWidget.height() + 100
        # if height + topHeight > self.funcParent.height():
        #     height = self.funcParent.height() - topHeight
        # if height < 0:
        #     height = self.funcParent.height() // 3
        # self.customSendScroll.setMinimumHeight(height)
        if not item:
            item = {"text": "", "remark": None, "icon": None}
        if type(item) == str:
            item = {"text": item, "remark": None}
        text = item["text"]
        remark = item["remark"]
        itemWidget = QWidget()
        layout = QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        itemWidget.setLayout(layout)
        cmd = QLineEdit(text)
        if remark:
            send = QPushButton(remark)
        else:
            send = QPushButton("")
        if (not "icon" in item) or not item["icon"]:
            item["icon"] = "fa.send"
        if not "shortcut" in item:
            item["shortcut"] = []
        utils_ui.setButtonIcon(send, item["icon"])
        editRemark = QPushButton("")
        editRemark.setObjectName("editRemark")
        utils_ui.setButtonIcon(editRemark, "ei.pencil")
        editRemark.setProperty("class", "remark")
        cmd.setToolTip(text)
        send.setToolTip(text)
        cmd.textChanged.connect(lambda: self.onCustomItemChange(
            self.customSendItemsLayout.indexOf(itemWidget), cmd, send))
        send.setProperty("class", "smallBtn")

        def sendCustomData(idx):
            self.sendCustomItem(self.config["customSendItems"][idx])

        send.clicked.connect(lambda: sendCustomData(
            self.customSendItemsLayout.indexOf(itemWidget)))
        delete = QPushButton("")
        utils_ui.setButtonIcon(delete, "fa.close")
        delete.setProperty("class", "deleteBtn")
        layout.addWidget(cmd)
        layout.addWidget(send)
        layout.addWidget(editRemark)
        layout.addWidget(delete)
        delete.clicked.connect(lambda: self.deleteSendItem(
            self.customSendItemsLayout.indexOf(itemWidget), itemWidget,
            [send, editRemark, delete]))

        def changeRemark(idx, obj):
            if not "icon" in self.config["customSendItems"][idx]:
                self.config["customSendItems"][idx]["icon"] = None
            shortcut = []
            if "shortcut" in self.config["customSendItems"][idx]:
                shortcut = self.config["customSendItems"][idx]["shortcut"]
            ok, remark, icon, shortcut = EditRemarDialog(
                obj.text(), self.config["customSendItems"][idx]["icon"],
                shortcut).exec()
            if ok:
                obj.setText(remark)
                if icon:
                    utils_ui.setButtonIcon(obj, icon)
                else:
                    obj.setIcon(QIcon())
                self.config["customSendItems"][idx]["remark"] = remark
                self.config["customSendItems"][idx]["icon"] = icon
                self.config["customSendItems"][idx]["shortcut"] = shortcut

        editRemark.clicked.connect(lambda: changeRemark(
            self.customSendItemsLayout.indexOf(itemWidget), send))
        self.customSendItemsLayout.addWidget(itemWidget)
        if not load:
            self.config["customSendItems"].append(item)
        return item

    def deleteSendItem(self, idx, item, iconItems=[]):
        for obj in iconItems:
            utils_ui.clearButtonIcon(obj)
        item.setParent(None)
        self.config["customSendItems"].pop(idx)
        # itemsNum = self.customSendItemsLayout.count()
        # height = parameters.customSendItemHeight * (itemsNum + 1) + 20
        # topHeight = self.receiveWidget.height() + 100
        # if height + topHeight > self.funcParent.height():
        #     height = self.funcParent.height() - topHeight
        # self.customSendScroll.setMinimumHeight(height)

    def showReceivedData(self, text: str):
        curScrollValue = self.receiveWidget.verticalScrollBar().value()
        self.receiveWidget.moveCursor(QTextCursor.End)
        endScrollValue = self.receiveWidget.verticalScrollBar().value()
        self.receiveWidget.insertPlainText(text)
        if curScrollValue < endScrollValue:
            self.receiveWidget.verticalScrollBar().setValue(curScrollValue)
        else:
            self.receiveWidget.moveCursor(QTextCursor.End)

    def onReceived(self, data: bytes):
        self.statusBar.addRx(len(data))
        try:
            data = self.decodeMethod(data)
        except Exception as e:
            self.hintSignal.emit("error", _("Error"),
                                 _("Run decode error") + " " + str(e))
            return
        if not data:
            return
        for plugin in self.connChilds:
            plugin.onReceived(data)
        if type(data) != str:
            data = self.decodeReceivedData(data, self.configGlobal["encoding"],
                                           not self.config["sendAscii"],
                                           self.config["sendEscape"])
        self.showReceiveDataSignal.emit(data + "\n")

    def sendData(self, data_bytes=None):
        try:
            data_bytes = self.encodeMethod(data_bytes)
        except Exception as e:
            self.hintSignal.emit("error", _("Error"),
                                 _("Run encode error") + " " + str(e))
            return
        if data_bytes:
            self.send(data_bytes, callback=self.onSent)

    def onSent(self, ok, msg, length, path):
        if ok:
            self.statusBar.addTx(length)
        else:
            self.hintSignal.emit("error", _("Error"),
                                 _("Send data failed!") + " " + msg)

    def sendCustomItem(self, item):
        text = item["text"]
        dateBytes = self.parseSendData(text, self.configGlobal["encoding"],
                                       self.config["useCRLF"],
                                       not self.config["sendAscii"],
                                       self.config["sendEscape"])
        if dateBytes:
            self.sendData(data_bytes=dateBytes)

    def onCustomItemChange(self, idx, edit, send):
        text = edit.text()
        edit.setToolTip(text)
        send.setToolTip(text)
        self.config["customSendItems"][idx].update({
            "text": text,
            "remark": send.text()
        })

    def onCodeItemChanged(self):
        if self.editingDefaults:
            return
        self.editingDefaults = True
        if self.codeItems.currentText() == self.codeItemCustomStr:
            self.codeItems.clearEditText()
            self.editingDefaults = False
            return
        if self.codeItems.currentText() == self.codeItemLoadDefaultsStr:
            for name in defaultProtocols:
                idx = self.codeItems.findText(name)
                if idx >= 0:
                    self.codeItems.removeItem(idx)
                self.codeItems.insertItem(self.codeItems.count() - 2, name)
                self.config["code"][name] = defaultProtocols[name]
            self.codeItems.setCurrentIndex(0)
            self.selectCode(self.codeItems.currentText())
            self.editingDefaults = False
            return
        # update code from defaults
        self.selectCode(self.codeItems.currentText())
        self.editingDefaults = False

    def selectCode(self, name):
        if name in [self.codeItemCustomStr, self.codeItemLoadDefaultsStr
                    ] or not name or not name in self.config["code"]:
            print(f"name {name} invalid")
            return
        self.config["currCode"] = name
        self.codeWidget.clear()
        self.codeWidget.insertPlainText(self.config["code"][name])
        ok, e, d = self.getEnDecodeMethod(self.codeWidget.toPlainText())
        if ok:
            self.encodeMethod = e
            self.decodeMethod = d
        self.saveCodeBtn.setText(_("Save"))
        self.saveCodeBtn.setEnabled(False)

    def getEnDecodeMethod(self, code):
        func = lambda x: x
        try:
            exec(code, self.codeGlobals)
            if (not "decode"
                    in self.codeGlobals) or not "encode" in self.codeGlobals:
                raise ValueError(
                    _("decode and encode method should be in code"))
            return True, self.codeGlobals["encode"], self.codeGlobals["decode"]
        except Exception as e:
            msg = _("Method error") + "\n" + str(e)
            self.hintSignal.emit("error", _("Error"), msg)
        return False, func, func

    def onCodeChanged(self):
        changed = True
        name = self.codeItems.currentText()
        if name in self.config["code"]:
            codeSaved = self.config["code"][name]
            code = self.codeWidget.toPlainText()
            if code == codeSaved:
                changed = False
        if changed:
            self.saveCodeBtn.setText(_("Save") + " *")
            self.saveCodeBtn.setEnabled(True)
        else:
            self.saveCodeBtn.setText(_("Save"))
            self.saveCodeBtn.setEnabled(False)

    def saveCode(self):
        self.editingDefaults = True
        name = self.codeItems.currentText()
        if name in [self.codeItemCustomStr, self.codeItemLoadDefaultsStr
                    ] or not name:
            self.hintSignal.emit("warning", _("Warning"),
                                 _("Please input code profile name first"))
            self.editingDefaults = False
            return
        idx = self.codeItems.findText(name)
        if idx < 0:
            self.codeItems.insertItem(self.codeItems.count() - 2, name)
        self.editingDefaults = False
        code = self.codeWidget.toPlainText()
        ok, e, d = self.getEnDecodeMethod(code)
        if ok:
            self.encodeMethod = e
            self.decodeMethod = d
            self.config["code"][name] = code
            self.saveCodeBtn.setText(_("Save"))
            self.saveCodeBtn.setEnabled(False)

    def deleteCode(self):
        self.editingDefaults = True
        name = self.codeItems.currentText()
        itemsConfig = [self.codeItemCustomStr, self.codeItemLoadDefaultsStr]
        # QMessageBox.infomation()
        if name in itemsConfig or not name:
            self.hintSignal.emit(
                "warning", _("Warning"),
                _("Please select a code profile name first to delete"))
            self.editingDefaults = False
            return
        idx = self.codeItems.findText(name)
        if idx < 0:
            self.editingDefaults = False
            return
        self.codeItems.removeItem(idx)
        self.config["code"].pop(name)
        name = list(self.config["code"].keys())
        if len(name) > 0:
            name = name[0]
            self.codeItems.setCurrentText(name)
            self.selectCode(name)
        self.editingDefaults = False