Пример #1
0
    def view_fit_grains_results(self):
        for result in self.fit_grains_results:
            print(result)

        # Build grains table
        num_grains = len(self.fit_grains_results)
        shape = (num_grains, 21)
        grains_table = np.empty(shape)
        gw = instrument.GrainDataWriter(array=grains_table)
        for result in self.fit_grains_results:
            gw.dump_grain(*result)
        gw.close()

        # Display grains table in popup dialog
        dialog = QDialog(self.parent)
        dialog.setWindowTitle('Fit Grains Results')

        model = FitGrainsResultsModel(grains_table, dialog)
        view = QTableView(dialog)
        view.setModel(model)
        view.verticalHeader().hide()
        view.resizeColumnToContents(0)

        layout = QVBoxLayout(dialog)
        layout.addWidget(view)
        dialog.setLayout(layout)
        dialog.resize(960, 320)
        dialog.exec_()
Пример #2
0
 def quit_message(self) -> QDialog:
     """Displays a window while SCOUTS is exiting"""
     message = QDialog(self)
     message.setWindowTitle('Exiting SCOUTS')
     message.resize(300, 50)
     label = QLabel('SCOUTS is exiting, please wait...', message)
     label.setStyleSheet(self.style['label'])
     label.adjustSize()
     label.setAlignment(Qt.AlignCenter)
     label.move(int((message.width() - label.width()) / 2),
                int((message.height() - label.height()) / 2))
     return message
Пример #3
0
 def loading_message(self) -> QDialog:
     """Returns the message box to be displayed while the user waits for the input data to load."""
     message = QDialog(self)
     message.setWindowTitle('Loading')
     message.resize(300, 50)
     label = QLabel('loading DataFrame into memory...', message)
     label.setStyleSheet(self.style['label'])
     label.adjustSize()
     label.setAlignment(Qt.AlignCenter)
     label.move(int((message.width() - label.width()) / 2),
                int((message.height() - label.height()) / 2))
     return message
def demo_combine_meshes():
    """
    Demonstrates combining the mesh of two scene nodes
    Prompt user to select two nodes to be merged and merge them.
    """
    dialog = QDialog(GetQMaxMainWindow())
    dialog.resize(250, 100)
    dialog.setWindowTitle('DEMO - Combine 2 Nodes')

    main_layout = QVBoxLayout()
    label = QLabel("Combine 2 Nodes")
    main_layout.addWidget(label)

    combine_btn = QPushButton("Combine")
    combine_btn.clicked.connect(combine_two_meshes)
    main_layout.addWidget(combine_btn)

    dialog.setLayout(main_layout)
    dialog.show()
class batch_file_viewer(QTableWidget):
    def __init__(self, nf_settings_path):
        super(batch_file_viewer,self).__init__(parent = None)
        self.nf_settings_parser = custom_config_parser()
        self.nf_settings_parser.load(nf_settings_path)
        self.setRowCount(20)
        self.setColumnCount(2)
        # Fill all places so there are no "None" types in the table
        for row in range(self.rowCount()):
            for column in range(self.columnCount()):
                item = QTableWidgetItem()
                item.setText('')
                self.setItem(row, column, item)

        self.original_background = item.background()
        self.clipboard = QGuiApplication.clipboard()

        self.cellChanged.connect(self.check_cell)  # Needs to be after "filling for loop" above
        self.header = self.horizontalHeader()
        self.header.setSectionResizeMode(0, QHeaderView.Stretch)
        self.setHorizontalHeaderLabels(["MS files", "Label"])
        self.header.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.header.customContextMenuRequested.connect( self.right_click_menu )
        self.saved_text = ''
        self.store_re_text = ''

    def right_click_menu(self, point):
        column = self.header.logicalIndexAt(point.x())
        # show menu about the column if column 1
        if column == 1:
            menu = QMenu(self)
            menu.addAction('Auto label (experimental)', self.auto_label)
            menu.popup(self.header.mapToGlobal(point))
        elif column == 0:
            menu = QMenu(self)
            menu.addAction('Remove empty rows', self.remove_empty_rows)
            menu.popup(self.header.mapToGlobal(point))

    def keyPressEvent(self, event):
        """Add functionallity to keyboard"""
        # Mac OS specifics
        is_mac_os_delete = (event.key() == QtCore.Qt.Key_H and event.modifiers() == QtCore.Qt.ControlModifier)
        if event.key() == QtCore.Qt.Key_Delete or is_mac_os_delete:  # 16777223
            for item in self.selectedItems():
                item.setText('')
        elif event.key() == 16777221 or event.key() == 16777220:  # *.221 is right enter
            if len(self.selectedIndexes()) == 0:  # Quick check if anything is selected
                pass
            else:
                index = self.selectedIndexes()[0]  # Take last
                if index.row() + 1 > self.rowCount() - 1:
                    self.addRow_with_items()
                self.setCurrentCell(index.row() + 1, index.column())
        else:
            modifiers = QGuiApplication.keyboardModifiers()
            if modifiers == QtCore.Qt.ControlModifier and event.key() == 67:  # Copy
                self.saved_text = ''
                new_row = False
                for index in self.selectedIndexes():
                    if index.column() == 0:
                        if new_row:
                            self.saved_text += '\n'
                        new_row = True
                        self.saved_text += self.item(index.row(), index.column()).text()
                        self.saved_text += '\t'
                    elif index.column() == 1:
                        self.saved_text += self.item(index.row(), index.column()).text()
                        self.saved_text += '\n'
                        new_row = False

                self.clipboard.setText(self.saved_text)
            elif modifiers == QtCore.Qt.ControlModifier and event.key() == 86:  # Paste
                clipboard_text = self.clipboard.text()
                clipboard_text = clipboard_text.split('\n')
                paste_index = self.selectedIndexes()[0]
                row = paste_index.row()
                for text, index in zip(clipboard_text, range(len(clipboard_text))):
                    text = text.split('\t')
                    column = paste_index.column()
                    for input in text:
                        if input == '':
                            continue
                        if column > self.columnCount() - 1:
                            pass
                        else:
                            if self.item(row, column) is None:
                                self.addRow_with_items()
                            self.item(row, column).setText(input)
                        column += 1
                    row += 1
            else:
                super().keyPressEvent(event)  # Propagate to built in methods
                # This interferes with copy and paste if it is not else

    def check_cell(self, row, column):
        """Triggered when cell is changed"""
        self.blockSignals(True)
        self.pick_color(row, column)
        self.blockSignals(False)

    def addRow_with_items(self):
        item = QTableWidgetItem()
        item.setText('')
        self.setRowCount(self.rowCount() + 1)
        self.setItem(self.rowCount() - 1, 0, item)    # Note: new rowcount here
        label = QTableWidgetItem()
        label.setText('')
        self.setItem(self.rowCount() - 1, 1, label)

    def pick_color(self, row, column):
        """Triggered by check_cell"""
        msfile = self.item(row, 0)
        label = self.item(row, 1)
        if label is None or msfile is None:  # NOTE: item == None will give NotImplementedError. Must use "is"
            return  # This might remove some weird errors in the future

        # Fix for adding empty spaces
        if msfile.text() == ' ':
            msfile.setText('')

        # Ms file
        if not os.path.isfile(msfile.text()):
            msfile.setForeground(QColor('red'))
        elif msfile.text().split('.')[-1] == "mzML":
            msfile.setForeground(QColor(0,255,150))
        elif msfile.text().split('.')[-1] != "mzML":
            msfile.setForeground(QColor(30,150,255))

        workflow = self.nf_settings_parser.get('params.workflow')
        if msfile.text() == '':
            label.setBackground(self.original_background)
            label.setForeground(QColor('white'))
        elif label.text() == '' and os.path.isfile(msfile.text()) and workflow == "Full":
            label.setBackground(QColor('red'))
        elif os.path.isfile(msfile.text()) and label.text() != '':
            label.setBackground(self.original_background)
            label.setForeground(QColor(0,255,150))
        else:
            label.setBackground(self.original_background)
            label.setForeground(QColor('white'))

    def update(self):
        for row in range(self.rowCount()):
            for column in range(self.columnCount()):
                self.pick_color(row, column)

    def auto_assign(self, file):
        """Triggered when using file chooser button"""
        for row in range(self.rowCount()):
            item = self.item(row, 0)
            if item.text() == '':
                self.item(row, 0).setText(file)
                return

        # If we get here, add more rows
        self.blockSignals(True)
        self.addRow_with_items()
        self.item(self.rowCount() - 1, 0).setText(file)
        self.pick_color(self.rowCount() - 1, 0)
        self.blockSignals(False)

    def auto_label(self):
        all_labels = []
        # Get current labels, we want unique
        for row in range(self.rowCount()):
            label = self.item(row, 1).text()
            all_labels.append(label)

        self.dialog = QDialog()
        lay = QVBoxLayout()
        self.dialog.setLayout(lay)
        self.line = QLineEdit()
        self.line.setText(self.store_re_text)
        bbox = QDialogButtonBox()
        bbox.addButton("Help", QDialogButtonBox.HelpRole)
        bbox.addButton("Apply", QDialogButtonBox.AcceptRole)
        bbox.addButton("Cancel", QDialogButtonBox.RejectRole)
        bbox.accepted.connect(self.apply_auto)
        bbox.rejected.connect(self.cancel_auto)
        bbox.helpRequested.connect(self.help_auto)
        lay.addWidget(self.line)
        lay.addWidget(bbox)
        self.dialog.resize(300, 100)
        ans = self.dialog.exec_()  # This will block until the dialog closes
        if ans != 1:
            return
        re_text = self.line.text()
        self.store_re_text = re_text

        # Assign labels
        c = 0
        for row in range(self.rowCount()):
            label = self.item(row, 1).text()
            file = self.item(row, 0).text()
            if label == '' and file != '':
                file_name = os.path.basename(file)
                search = re.search(re_text, file_name)
                if search is not None:
                    c += 1
                    # idk how to check how many captured groups, so lets do this a hacky way
                    for i in range(10,-1,-1):
                        try:
                            added_label = search.group(i)
                            break
                        except Exception as e:
                            pass
                else:
                    added_label = ''  # Do not add label if you don't know what to add
                self.item(row, 1).setText(added_label)
        if c == 0:
            ERROR("No files matched the regex pattern")
            self.auto_label()

    def apply_auto(self):
        re_text = self.line.text()
        test_string = "This is a string to check if you put in a correct regex code"
        try:
            match = re.search(re_text, test_string)
        except Exception as e:
            ERROR("This is not a valid regex string. Error is: \n" + str(e))
            return
        self.dialog.accept()

    def cancel_auto(self):
        self.dialog.reject()

    def help_auto(self):
        help_dialog = QDialog()
        lay = QVBoxLayout()
        help_string = '''<html>
        <center>
        Auto labeler uses regular expressions to determine which files belong to a certain groupself. <br>
        Write the regular expression in the box and press apply. <br>
        The labeler will then label your file depending on the naming convention you applied. <br>

        Example: You have 100s of files named like this <br>
        patient_X_<date>.RAW <br>
        Solution can be found <a href=\"https://regex101.com/r/dpCBo9/1/\">here</a> <br>

        NOTE: The auto labeler will always take the LAST capturing group
        </html>
        '''
        help_label = QLabel(help_string)
        help_label.setOpenExternalLinks(True)
        lay.addWidget(help_label)
        help_dialog.setLayout(lay)
        ans = help_dialog.exec_()  # This will block until the dialog closes

    def remove_empty_rows(self):
        row = 0
        for _ in range(5000):
            file = self.item(row, 0)
            label = self.item(row, 1)
            if file is None or label is None:
                self.removeRow(row)
            elif file.text() == '' and label.text() == '':
                self.removeRow(row)
            else:
                row += 1
        if self.rowCount() == 0:
            item = QTableWidgetItem()
            self.setRowCount(self.rowCount() + 1)
            row = self.rowCount() - 1
            self.setItem(row, 0, item)    # Note: new rowcount here
            label = QTableWidgetItem()
            label.setText('')
            self.setItem(row, 1, label)
Пример #6
0
class Slot():
    """Slot class for handling signal of Qt objects
    """
    def __init__(self, parent):
        self.parent = parent

    # decorator
    def display(func):
        def wrapper(self, *args, **kwargs):
            try:
                self.parent.is_display = False
                self.parent.camera.is_reading = False
                self.parent.stop_timer()
                func(self, *args, **kwargs)
            finally:
                self.parent.is_display = True
                self.parent.camera.is_reading = True
                self.parent.start_timer()

        return wrapper

    def switch_theme(self):
        """
        Toggle the stylesheet to use the desired path in the Qt resource
        system (prefixed by `:/`) or generically (a path to a file on
        system). This is quoted : https://stackoverflow.com/questions/48256772/dark-theme-for-qt-widgets
        """
        # get the QApplication instance,  or crash if not set
        app = QApplication.instance()
        if app is None:
            raise RuntimeError("No Qt Application found.")

        text = ""
        if self.parent.style_theme == "light":
            self.parent.style_theme = "dark"
            self.parent.style_theme_sheet = ":/dark.qss"
            self.update_statusbar()
            text = "Light"
        elif self.parent.style_theme == "dark":
            self.parent.style_theme = "light"
            self.parent.style_theme_sheet = ":/light.qss"
            self.update_statusbar()
            text = "Dark"

        file = QFile(self.parent.style_theme_sheet)
        self.parent.theme_button.setText(text)
        file.open(QFile.ReadOnly | QFile.Text)
        stream = QTextStream(file)
        app.setStyleSheet(stream.readAll())

    def set_fontsize(self, text: str):
        """Change the font-size of all widgets.

        Args:
            text (str): font-size
        """
        size = int(text)
        font = self.parent.font()
        family = str(font.family())
        font_css = str(self.parent.parent_dir / "font.qss")
        """
        with open(font_css, "w") as f:
            f.write("* {\n")
            f.write('    font-family: "{}";\n'.format(family))
            f.write('    font-size: {}px;\n'.format(size))
            f.write("}")
        self.parent.setStyleSheet("")
        with open(font_css, "r") as f:
            self.parent.setStyleSheet(f.read())
        """
        self.parent.setStyleSheet('font-family: "{}"; font-size: {}px;'.format(
            family, size))

    def switch_paramlist(self) -> list:
        """Change the number of sliders shown on the window.

        User can select which parameter is shown on the window with Checkbox.

        Returns:
            list: List of selected paramters.
        """
        self.dialog = QDialog(self.parent)
        self.vbox2 = QVBoxLayout()

        self.check_boxes = []
        self.button_box = QGroupBox("params")
        self.button_box.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        for label in self.parent.support_params:
            self.check_box = QCheckBox(label)
            if label in self.parent.current_params.keys():
                self.check_box.setChecked(True)
            self.check_boxes.append(self.check_box)
            self.vbox2.addWidget(self.check_box)
        self.vbox2.addStretch(1)
        self.button_box.setLayout(self.vbox2)

        self.check_all = QCheckBox("Check all")
        self.check_all.setChecked(False)
        self.check_all.stateChanged.connect(self.ALLCheck)

        label = QLabel("Select parameters to create slider")

        self.qdbox = QDialogButtonBox(QDialogButtonBox.Ok
                                      | QDialogButtonBox.Cancel)
        self.qdbox.accepted.connect(self.dialog.accept)
        self.qdbox.rejected.connect(self.dialog.reject)

        self.hbox = QHBoxLayout()
        self.hbox.addWidget(self.check_all)
        self.hbox.addWidget(self.qdbox)

        self.vbox = QVBoxLayout()
        self.vbox.addWidget(label)
        self.vbox.addWidget(self.button_box)
        self.vbox.addLayout(self.hbox)

        self.dialog.setLayout(self.vbox)

        self.dialog.resize(400, 300)
        if self.dialog.exec_():
            ret = []
            for button, key in zip(self.check_boxes,
                                   self.parent.support_params):
                if button.isChecked():
                    ret.append(key)
            self.parent.update_params(ret)

    def ALLCheck(self):
        """Checkes all chekebox.
        """
        if self.check_all.isChecked():
            for cb in self.check_boxes:
                cb.setChecked(True)
        else:
            for cb in self.check_boxes:
                cb.setChecked(False)

    @display
    def about(self):
        """Show the about message on message box.
        """
        msg = QMessageBox(self.parent)
        #msg.setTextFormat(Qt.MarkdownText)
        msg.setIcon(msg.Information)
        msg.setWindowTitle("About this tool")
        msg.setText(MessageText.about_text)
        ret = msg.exec_()

    @display
    def show_shortcut(self):
        """Show the list of valid keyboard shortcut.
        """
        self.parent.dialog = QDialog(self.parent)
        table = QTableWidget()
        vbox = QVBoxLayout()
        self.parent.dialog.setLayout(vbox)
        self.parent.dialog.setWindowTitle("Keyboard shortcut")

        header = ["key", "description"]
        keys = MessageText.keylist
        table.setColumnCount(len(header))
        table.setRowCount(len(keys))
        table.setHorizontalHeaderLabels(header)
        table.verticalHeader().setVisible(False)
        table.setAlternatingRowColors(True)
        table.horizontalHeader().setStretchLastSection(True)
        table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        table.setFocusPolicy(Qt.NoFocus)

        for row, content in enumerate(keys):
            for col, elem in enumerate(content):
                item = QTableWidgetItem(elem)
                item.setFlags(Qt.ItemIsDragEnabled | Qt.ItemIsUserCheckable
                              | Qt.ItemIsEnabled)
                table.setItem(row, col, item)

        button = QPushButton("&Ok")
        button.clicked.connect(self.close)
        button.setAutoDefault(True)

        vbox.addWidget(table)
        vbox.addWidget(button)

        self.parent.dialog.resize(640, 480)
        self.parent.dialog.exec_()

    @display
    def usage(self):
        """Show usage of the program.
        """

        msg = QMessageBox(self.parent)
        msg.setWindowTitle("Usage")
        #msg.setTitle("Usage of this GUI")
        text = QLabel(MessageText.usage_text)
        msg.setIcon(QMessageBox.Information)
        scroll = QScrollArea(msg)
        scroll.setWidgetResizable(True)
        grid = msg.findChild(QGridLayout)
        text.setWordWrap(True)
        scroll.setWidget(text)
        scroll.setMinimumSize(800, 400)
        scroll.setStyleSheet("""
            border: 1.5px solid black;
            padding: 15px;
            """)
        grid.addWidget(scroll, 0, 1)
        msg.exec_()

    def quit(self):
        """Quit the program.
        """
        QApplication.quit()

    @display
    def change_frame_prop(self):
        """Change the properties of camera.
        """
        self.dialog = QDialog(self.parent)
        self.dialog.setWindowTitle("Change frame properties")

        text = QLabel()
        text.setText("Select fourcc, size and FPS.")

        fourcc, width, height, fps = self.parent.get_properties()
        size = "{}x{}".format(width, height)
        self.parent.fourcc_label = QLabel("Fourcc")
        self.parent.size_label = QLabel("Size")
        self.parent.fps_label = QLabel("FPS")
        self.parent.fourcc_result = QLabel(str(fourcc))
        self.parent.fourcc_result.setFrameShape(QFrame.StyledPanel)
        self.parent.size_result = QLabel(size)
        self.parent.size_result.setFrameShape(QFrame.Box)
        self.parent.size_result.setFrameStyle(QFrame.Panel | QFrame.Sunken)
        self.parent.fps_result = QLabel(str(fps))
        self.parent.fps_result.setFrameStyle(QFrame.Panel | QFrame.Sunken)

        fourcc_button = QPushButton("...")
        fourcc_button.clicked.connect(self.select_fourcc)
        size_button = QPushButton("...")
        size_button.clicked.connect(self.select_size)
        fps_button = QPushButton("...")
        fps_button.clicked.connect(self.select_fps)

        grid = QGridLayout()
        grid.addWidget(self.parent.fourcc_label, 0, 0)
        grid.addWidget(self.parent.fourcc_result, 0, 1)
        grid.addWidget(fourcc_button, 0, 2)
        grid.addWidget(self.parent.size_label, 1, 0)
        grid.addWidget(self.parent.size_result, 1, 1)
        grid.addWidget(size_button, 1, 2)
        grid.addWidget(self.parent.fps_label, 2, 0)
        grid.addWidget(self.parent.fps_result, 2, 1)
        grid.addWidget(fps_button, 2, 2)
        grid.setSpacing(5)

        self.button_box = QDialogButtonBox(QDialogButtonBox.Ok
                                           | QDialogButtonBox.Cancel)
        self.button_box.accepted.connect(self.dialog.accept)
        self.button_box.rejected.connect(self.dialog.reject)

        vbox = QVBoxLayout()
        vbox.addLayout(grid, 3)
        vbox.addWidget(self.button_box, 1)

        self.dialog.setLayout(vbox)
        self.dialog.resize(480, 270)
        if self.dialog.exec_():
            self.set_param()
            self.close()
        else:
            self.close()

    def select_fourcc(self):
        items = self.parent.camera.get_supported_fourcc()
        item, ok = QInputDialog.getItem(self.dialog, "Select", "Select Fourcc",
                                        items, 0, False)
        if ok:
            self.parent.fourcc_result.setText(item)
        else:
            return None

    def select_size(self):
        if not self.parent.fourcc_result.text():
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setText("Error")
            msg.setInformativeText("Select fourcc")
            msg.setWindowTitle("Error")
            msg.exec_()
            self.select_fourcc()
            return True

        if self.parent.camtype == "usb_cam":
            items = self.parent.camera.get_supported_size(
                self.parent.fourcc_result.text())
        #elif self.camtype == "raspi":
        else:
            items = self.parent.camera.raspicam_img_format()

        item, ok = QInputDialog.getItem(self.dialog, "Select", "Select Size",
                                        items, 0, False)
        if ok:
            self.parent.size_result.setText(item)
        else:
            return None

    def select_fps(self):
        if not self.parent.size_result.text():
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setText("Error")
            msg.setInformativeText("Select size")
            msg.setWindowTitle("Error")
            msg.exec_()
            self.select_size()
            return True

        if self.parent.camtype == "usb_cam":
            width, height = map(str, self.parent.size_result.text().split("x"))
            items = self.parent.camera.get_supported_fps(
                self.parent.fourcc_result.text(), width, height)
        #elif self.camtype == "raspi":
        else:
            items = self.parent.raspicam_fps()

        item, ok = QInputDialog.getItem(self.dialog, "Select", "Select FPS",
                                        items, 0, False)
        if ok:
            self.parent.fps_result.setText(item)
        else:
            return None

    def search_size(self, *args):
        lst = []
        for fourcc in args:
            lst.extend(
                [i for i in self.parent.v4l2.vidcap_format if fourcc in i])

        size_lst = []
        for i in lst:
            size = "{}x{}".format(i[1], i[2])
            if size not in size_lst:
                size_lst.append(size)
        return size_lst

    def search_fps(self, fourcc, size):
        width, height = map(int, size.split("x"))
        match = [fourcc, width, height]
        fps_lst = []
        for i in self.parent.v4l2.vidcap_format:
            if set(i) >= set(match):
                fps = i[-1]
                if fps not in fps_lst:
                    fps_lst.append(str(fps))
        return fps_lst

    def set_param(self):
        fourcc = self.parent.fourcc_result.text()
        size = self.parent.size_result.text()
        width, height = map(int, size.split("x"))
        fps = self.parent.fps_result.text()
        self.parent.camera.set_properties(fourcc, width, height, float(fps))
        self.parent.scene.setSceneRect(0, 0, width, height)
        if fps:
            self.parent.msec = 1 / float(fps) * 1000
        else:
            self.parent.msec = 1 / 30.0 * 1000
        self.parent.update_prop_table()

    def close(self):
        """Close the dialog.
        """
        try:
            self.parent.dialog.close()
            return True
        except:
            return False

    @display
    def show_paramlist(self):
        """Show the list of currently set parameters.
        """
        self.parent.dialog = QDialog(self.parent)
        table = QTableWidget()
        vbox = QVBoxLayout()
        self.parent.dialog.setLayout(vbox)
        self.parent.dialog.setWindowTitle("Parameters list")

        header = ["param", "min", "max", "step", "default"]
        lst = []
        for key, val in self.parent.current_params.items():
            sub = []
            sub.append(key)
            for head in header:
                if head in val:
                    sub.append(val[head])
            lst.append(sub)

        table.setColumnCount(len(header))
        table.setRowCount(len(self.parent.current_params))
        table.setHorizontalHeaderLabels(header)
        table.verticalHeader().setVisible(False)
        table.setAlternatingRowColors(True)
        table.horizontalHeader().setStretchLastSection(True)
        table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        table.setFocusPolicy(Qt.NoFocus)

        for row, content in enumerate(lst):
            for col, elem in enumerate(content):
                item = QTableWidgetItem(str(elem))
                item.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)
                item.setFlags(Qt.ItemIsDragEnabled | Qt.ItemIsUserCheckable
                              | Qt.ItemIsEnabled)
                table.setItem(row, col, item)

        table.resizeColumnsToContents()
        table.resizeRowsToContents()
        table.setColumnWidth(0, 250)
        table.setColumnWidth(1, 80)
        table.setColumnWidth(2, 80)
        table.setColumnWidth(3, 80)
        table.setColumnWidth(4, 80)

        button = QPushButton("&Ok")
        button.clicked.connect(self.close)
        button.setAutoDefault(True)

        vbox.addWidget(table)
        vbox.addWidget(button)

        self.parent.dialog.resize(640, 480)
        ret = self.parent.dialog.exec_()

    @display
    def set_font(self):
        """Change the font of all widgets through QFontDialog.
        """
        self.parent.dialog = QFontDialog(self.parent)
        self.parent.dialog.setOption(QFontDialog.DontUseNativeDialog)
        self.parent.dialog.resize(800, 600)
        ret = self.parent.dialog.exec_()
        if ret:
            font = self.parent.dialog.selectedFont()
            family = str(font.family())
            size = str(font.pointSize())
            font_css = str(self.parent.parent_dir / "font.qss")
            with open(font_css, "w") as f:
                f.write("* {\n")
                f.write('    font-family: "{}";\n'.format(family))
                f.write('    font-size: {}px;\n'.format(size))
                f.write("}")
            self.parent.setStyleSheet("")
            with open(font_css, "r") as f:
                self.parent.setStyleSheet(f.read())

    def update_statusbar(self):
        """Update statubar's style

        This method will be called when swtitching the color theme.

        """
        if self.parent.style_theme == "light":
            if self.parent.colorspace == "rgb":
                self.parent.stat_css = {
                    "postion": "color: black;",
                    "R": "color: red;",
                    "G": "color: green;",
                    "B": "color: blue;",
                    "alpha": "color: black;",
                }
            else:
                self.parent.stat_css = {
                    "postion": "color: black;",
                    "gray": "color: black;"
                }
        elif self.parent.style_theme == "dark":
            if self.parent.colorspace == "rgb":
                self.parent.stat_css = {
                    "postion": "color: white;",
                    "R": "color: white;",
                    "G": "color: white;",
                    "B": "color: white;",
                    "alpha": "color: white;",
                }
            else:
                self.parent.stat_css = {
                    "postion": "color: white;",
                    "gray": "color: white;"
                }
        for stat, st in zip(self.parent.statbar_list,
                            self.parent.stat_css.values()):
            stat.setStyleSheet(st)

    def set_file_rule(self):
        """Change the style of naming when saving frame as an image.
        """
        self.parent.filename_rule = next(self.parent.filename_rule_lst)
        self.parent.prop_table[5][1] = self.parent.filename_rule
        self.parent.update_prop_table()
        self.parent.write_text("change: {}".format(self.parent.filename_rule))
Пример #7
0
class ADBHelper(object):
    # 构造函数
    def __init__(self):
        # 加载UI文件
        self.ui = QUiLoader().load('adbwindow.ui')
        # 设置窗口背景
        palette = QPalette()
        pix = QPixmap('bg.jpg')
        #pix = pix.scaled(self.ui.width(), self.ui.height())
        palette.setBrush(self.ui.backgroundRole(), QBrush(pix))
        self.ui.setAutoFillBackground(True)
        self.ui.setPalette(palette)

        self.ui.setWindowIcon(QIcon('adb.png'))
        # self.ui.setWindowFlags(qcore.Qt.WindowStaysOnTopHint)

        # 绑定保存参数菜单项点击事件
        self.ui.save_action.triggered.connect(self.save_params)
        # 绑定加载参数菜单项点击事件
        self.ui.load_action.triggered.connect(self.load_params)
        # 绑定退出菜单项点击事件
        self.ui.exit_action.triggered.connect(self.exit)
        # 绑定断开连接菜单项点击事件
        self.ui.disconnect_action.triggered.connect(self.disconnect)
        # 绑定清空输出信息菜单项点击事件
        self.ui.clear_action.triggered.connect(self.clear)
        # 绑定坐标工具菜单项点击事件
        self.ui.tool_action.triggered.connect(self.tool)
        # 绑定关于菜单项点击事件
        self.ui.about_action.triggered.connect(self.about)

        # 绑定启动ADB按钮点击事件
        self.ui.openadb_pushButton.clicked.connect(self.open_adb)
        # 绑定连接按钮点击事件
        self.ui.connect_pushButton.clicked.connect(self.connect)
        # 绑定显示设备列表按钮点击事件
        self.ui.show_devices_pushButton.clicked.connect(self.show_devices)
        # 绑定开始点击按钮点击事件
        self.ui.tap_pushButton.clicked.connect(self.tap)
        # 绑定开始滑屏按钮点击事件
        self.ui.swipe_pushButton.clicked.connect(self.swipe)
        # 绑定停止按钮点击事件
        self.ui.stop_pushButton.clicked.connect(self.stop)
        # 绑定坐标工具按钮点击事件
        self.ui.tool_pushButton.clicked.connect(self.tool)

        # 限制数值范围
        self.ui.swipe_x1_min_spinBox.valueChanged.connect(self.x1_min)
        self.ui.swipe_x1_max_spinBox.valueChanged.connect(self.x1_max)
        self.ui.swipe_y1_min_spinBox.valueChanged.connect(self.y1_min)
        self.ui.swipe_y1_max_spinBox.valueChanged.connect(self.y1_max)
        self.ui.swipe_x2_min_spinBox.valueChanged.connect(self.x2_min)
        self.ui.swipe_x2_max_spinBox.valueChanged.connect(self.x2_max)
        self.ui.swipe_y2_min_spinBox.valueChanged.connect(self.y2_min)
        self.ui.swipe_y2_max_spinBox.valueChanged.connect(self.y2_max)

        # ADB工具
        self.adbtool = None
        # 连接标志
        self.connected = False
        # 坐标工具窗口
        self.tool_dialog = None
        # 截图文件
        self.screen_cap_file = None

    # 当前时间
    def nowtime(self):
        return str(
            _time.strftime('%Y-%m-%d %H:%M:%S', _time.localtime(_time.time())))

    # 输出信息
    def output_message(self, message):
        mess = "<font color='orange'>[</font><font color='blue'>"+self.nowtime() + \
            "</font><font color='orange'>]</font><font color='green'>"+message+"</font>"
        self.ui.output_textEdit.append(mess)
        # 移动光标到最底
        self.ui.output_textEdit.moveCursor(QTextCursor.End)

    # 输出结果和错误信息
    def output_result_error(self, result, error):
        # 输出信息
        self.output_message(result)
        # 判断错误信息是否为空
        if len(error.strip()) != 0:
            self.output_message(error)

    # 保存参数
    def save_params(self):
        # 弹出文件选择器
        filepath, filetype = QFileDialog.getSaveFileName(
            self.ui, "保存参数", os.getcwd(), "Json File (*.json)")

        # 判断
        if filepath != "":
            # IP地址
            ip = self.ui.ip_lineEdit.text()
            # 端口号
            port = self.ui.port_spinBox.value()
            # 模拟点击
            tap_x = self.ui.tap_x_spinBox.value()
            tap_y = self.ui.tap_y_spinBox.value()
            tap_times = self.ui.tap_times_spinBox.value()
            tap_interval = round(self.ui.tap_interval_doubleSpinBox.value(), 1)
            tap_random_interval = self.ui.tap_random_interval_checkBox.isChecked(
            )
            # 模拟滑屏
            swipe_x1_min = self.ui.swipe_x1_min_spinBox.value()
            swipe_x1_max = self.ui.swipe_x1_max_spinBox.value()
            swipe_y1_min = self.ui.swipe_y1_min_spinBox.value()
            swipe_y1_max = self.ui.swipe_y1_max_spinBox.value()
            swipe_x2_min = self.ui.swipe_x2_min_spinBox.value()
            swipe_x2_max = self.ui.swipe_x2_max_spinBox.value()
            swipe_y2_min = self.ui.swipe_y2_min_spinBox.value()
            swipe_y2_max = self.ui.swipe_y2_max_spinBox.value()
            swipe_times = self.ui.swipe_times_spinBox.value()
            # 浮点数取整
            swipe_interval = round(
                self.ui.swipe_interval_doubleSpinBox.value(), 1)
            swipe_random_interval = self.ui.swipe_random_interval_checkBox.isChecked(
            )
            swipe_60 = self.ui.swipe_60_checkBox.isChecked()
            # 组成字典
            params = {
                'ip': ip,
                'port': port,
                'tap_x': tap_x,
                'tap_y': tap_y,
                'tap_times': tap_times,
                'tap_interval': tap_interval,
                'tap_random_interval': tap_random_interval,
                'swipe_x1_min': swipe_x1_min,
                'swipe_x1_max': swipe_x1_max,
                'swipe_y1_min': swipe_y1_min,
                'swipe_y1_max': swipe_y1_max,
                'swipe_x2_min': swipe_x2_min,
                'swipe_x2_max': swipe_x2_max,
                'swipe_y2_min': swipe_y2_min,
                'swipe_y2_max': swipe_y2_max,
                'swipe_times': swipe_times,
                'swipe_interval': swipe_interval,
                'swipe_random_interval': swipe_random_interval,
                'swipe_60': swipe_60
            }
            # 写入文件
            if not filepath.endswith('.json'):
                filepath = filepath + '.json'
            with open(filepath, 'w', encoding='utf-8') as f:
                json.dump(params, f)
            # 输出信息
            self.output_message('保存参数完成!文件路径:' + os.path.abspath(filepath))

    # 加载参数
    def load_params(self):
        # 弹出文件选择器
        filepath, filetype = QFileDialog.getOpenFileName(
            self.ui, "请选择文件", os.getcwd(), "Json File (*.json)")
        # 判断
        if filepath != "":
            # 读取文件
            with open(filepath, 'r', encoding='utf-8') as f:
                params = json.load(f)
                # IP和端口
                self.ui.ip_lineEdit.setText(params['ip'])
                self.ui.port_spinBox.setValue(params['port'])
                # 模拟点击
                self.ui.tap_x_spinBox.setValue(params['tap_x'])
                self.ui.tap_y_spinBox.setValue(params['tap_y'])
                self.ui.tap_times_spinBox.setValue(params['tap_times'])
                self.ui.tap_interval_doubleSpinBox.setValue(
                    params['tap_interval'])
                self.ui.tap_random_interval_checkBox.setChecked(
                    params['tap_random_interval'])
                # 模拟滑屏
                self.ui.swipe_x1_max_spinBox.setValue(params['swipe_x1_max'])
                self.ui.swipe_x1_min_spinBox.setValue(params['swipe_x1_min'])
                self.ui.swipe_y1_max_spinBox.setValue(params['swipe_y1_max'])
                self.ui.swipe_y1_min_spinBox.setValue(params['swipe_y1_min'])
                self.ui.swipe_x2_max_spinBox.setValue(params['swipe_x2_max'])
                self.ui.swipe_x2_min_spinBox.setValue(params['swipe_x2_min'])
                self.ui.swipe_y2_max_spinBox.setValue(params['swipe_y2_max'])
                self.ui.swipe_y2_min_spinBox.setValue(params['swipe_y2_min'])
                self.ui.swipe_times_spinBox.setValue(params['swipe_times'])
                self.ui.swipe_interval_doubleSpinBox.setValue(
                    params['swipe_interval'])
                self.ui.swipe_random_interval_checkBox.setChecked(
                    params['swipe_random_interval'])
                self.ui.swipe_60_checkBox.setChecked(params['swipe_60'])
            # 输出信息
            self.output_message('加载参数完成!文件路径:' + os.path.abspath(filepath))

    # 退出
    def exit(self):
        # 保存日志
        # 断开连接
        self.disconnect()
        # 退出
        self.ui.close()

    # 断开连接
    def disconnect(self):
        # 停止
        self.stop()
        # 断开连接
        result, error = ADBTool().disconnect()
        # 输出信息
        self.output_result_error(result, error)
        self.connected = False
        self.output_message('已经断开连接')
        # 按钮可点
        self.ui.tap_pushButton.setEnabled(True)
        self.ui.swipe_pushButton.setEnabled(True)

    # 清空输出信息
    def clear(self):
        self.ui.output_textEdit.setText('')

    # 关于
    def about(self):
        # 显示弹窗
        QMessageBox.about(
            self.ui, '关于',
            'ADB助手\n© Copyright 2020\n作者:ordinary-student\n版本:v1.0.0')

    # 打开ADB
    def open_adb(self):
        # 获取IP地址
        ip = self.ui.ip_lineEdit.text()
        # IP校验
        if len(ip.strip()) == 0:
            self.ui.ip_lineEdit.setText('')
            # 显示弹窗
            QMessageBox.information(self.ui, '提示', '请填写IP地址!')
            return
        # 获取端口号
        port = self.ui.port_spinBox.value()
        # 创建ADB工具
        adbtool = ADBTool(ip, port)
        # 启动ADB
        result, error = adbtool.open()
        # 输出信息
        self.output_result_error(result, error)
        # 判断是否已经启动
        if ('restarting' in result) and ('error' not in error):
            self.adbtool = adbtool
            self.output_message('ADB已经启动')

    # 连接设备
    def connect(self):
        # 判断
        if self.adbtool == None:
            # 显示弹窗
            QMessageBox.information(self.ui, '提示', 'ADB尚未启动!')
            return
        # 连接
        try:
            result, error = self.adbtool.connect()
            # 输出信息
            self.output_result_error(result, error)
            # 判断是否已经启动
            if ('connected' in result) and ('error' not in error):
                self.connected = True
                self.output_message('已经连接设备')
                self.adbtool.tap(700, 1860)
        except:
            self.output_message('设备连接失败')
            # 显示弹窗
            QMessageBox.information(
                self.ui, '提示',
                '设备连接失败!\n请检查设备和电脑是否处于同一局域网,\n检查设备的USB调试模式是否已经打开。\n若还是不行,请重启软件再尝试。'
            )
            return

    # 显示设备列表
    def show_devices(self):
        # 显示设备列表
        result, error = ADBTool().show_devices()
        # 输出信息
        self.output_result_error(result, error)

    # 开始点击
    def tap(self):
        # 判断ADB是否启动
        if self.adbtool == None:
            # 显示弹窗
            QMessageBox.information(self.ui, '提示', 'ADB尚未启动!')
            return
        # 判断是否连接
        if self.connected:
            tap_thread = Thread(target=self.tap2)
            tap_thread.start()
        else:
            # 显示弹窗
            QMessageBox.information(self.ui, '提示', '尚未连接设备!')
            return

    # 开始点击
    def tap2(self):
        # 按钮不可点
        self.ui.tap_pushButton.setEnabled(False)
        # 获取参数
        tap_x = self.ui.tap_x_spinBox.value()
        tap_y = self.ui.tap_y_spinBox.value()
        tap_times = self.ui.tap_times_spinBox.value()
        tap_interval = round(self.ui.tap_interval_doubleSpinBox.value(), 1)
        tap_random_interval = self.ui.tap_random_interval_checkBox.isChecked()
        if tap_random_interval:
            message = '点击坐标({},{})处,{}次,时间间隔随机'.format(tap_x, tap_y, tap_times)
        else:
            message = '点击坐标({},{})处,{}次,时间间隔{}秒'.format(
                tap_x, tap_y, tap_times, tap_interval)
        self.output_message(message)
        # 模拟点击
        if self.adbtool.continuous_tap(tap_x, tap_y, tap_times, tap_interval,
                                       tap_random_interval):
            self.output_message('模拟点击已完成')
        else:
            self.output_message('模拟点击已停止')
        # 按钮可点
        self.ui.tap_pushButton.setEnabled(True)

    # 开始滑屏
    def swipe(self):
        # 判断ADB是否启动
        if self.adbtool == None:
            # 显示弹窗
            QMessageBox.information(self.ui, '提示', 'ADB尚未启动!')
            return
        # 判断是否连接
        if self.connected:
            swipe_thread = Thread(target=self.swipe2)
            swipe_thread.start()
        else:
            # 显示弹窗
            QMessageBox.information(self.ui, '提示', '尚未连接设备!')
            return

    # 开始滑屏
    def swipe2(self):
        # 按钮不可点
        self.ui.swipe_pushButton.setEnabled(False)
        # 获取参数
        swipe_x1_min = self.ui.swipe_x1_min_spinBox.value()
        swipe_x1_max = self.ui.swipe_x1_max_spinBox.value()
        swipe_y1_min = self.ui.swipe_y1_min_spinBox.value()
        swipe_y1_max = self.ui.swipe_y1_max_spinBox.value()
        swipe_x2_min = self.ui.swipe_x2_min_spinBox.value()
        swipe_x2_max = self.ui.swipe_x2_max_spinBox.value()
        swipe_y2_min = self.ui.swipe_y2_min_spinBox.value()
        swipe_y2_max = self.ui.swipe_y2_max_spinBox.value()
        swipe_times = self.ui.swipe_times_spinBox.value()
        swipe_interval = round(self.ui.swipe_interval_doubleSpinBox.value(), 1)
        swipe_random_interval = self.ui.swipe_random_interval_checkBox.isChecked(
        )
        swipe_60 = self.ui.swipe_60_checkBox.isChecked()
        # 定时60秒
        if swipe_60:
            message = '从坐标({}~{},{}~{})滑到({}~{},{}~{})处,连续滑屏60秒,时间间隔{}秒'.format(
                swipe_x1_min, swipe_x1_max, swipe_y1_min, swipe_y1_max,
                swipe_x2_min, swipe_x2_max, swipe_y2_min, swipe_y2_max,
                swipe_interval)
            self.output_message(message)
            # 模拟连续滑屏60秒
            if self.adbtool.timing_swipe(
                (swipe_x1_min, swipe_x1_max), (swipe_y1_min, swipe_y1_max),
                (swipe_x2_min, swipe_x2_max), (swipe_y2_min, swipe_y2_max),
                    swipe_interval, swipe_random_interval):
                self.output_message('模拟滑屏已完成')
            else:
                self.output_message('模拟滑屏已停止')
        else:
            if swipe_random_interval:
                message = '从坐标({}~{},{}~{})滑到({}~{},{}~{})处,{}次,时间间隔随机'.format(
                    swipe_x1_min, swipe_x1_max, swipe_y1_min, swipe_y1_max,
                    swipe_x2_min, swipe_x2_max, swipe_y2_min, swipe_y2_max,
                    swipe_times)
            else:
                message = '从坐标({}~{},{}~{})滑到({}~{},{}~{})处,{}次,时间间隔{}秒'.format(
                    swipe_x1_min, swipe_x1_max, swipe_y1_min, swipe_y1_max,
                    swipe_x2_min, swipe_x2_max, swipe_y2_min, swipe_y2_max,
                    swipe_times, swipe_interval)
            self.output_message(message)
            # 模拟滑屏
            if self.adbtool.random_swipe(
                (swipe_x1_min, swipe_x1_max), (swipe_y1_min, swipe_y1_max),
                (swipe_x2_min, swipe_x2_max), (swipe_y2_min, swipe_y2_max),
                    swipe_times, swipe_interval, swipe_random_interval):
                self.output_message('模拟滑屏已完成')
            else:
                self.output_message('模拟滑屏已停止')
        # 按钮可点
        self.ui.swipe_pushButton.setEnabled(True)

    # 停止
    def stop(self):
        if self.adbtool != None:
            self.adbtool.stop()
            # 按钮可点
            self.ui.tap_pushButton.setEnabled(True)
            self.ui.swipe_pushButton.setEnabled(True)

    # 截图
    def screen_cap(self):
        if self.adbtool != None:
            # 截图
            photo_path = self.adbtool.screen_cap()
            # 获取图片到本地
            result, error = self.adbtool.pull_file(photo_path)
            self.output_result_error(result, error)
            # 判断
            if ('png' in result) and ('error' not in error):
                path = photo_path[(photo_path.rindex('/') + 1):]
                self.output_message('截图成功!图片路径:' + os.path.abspath(path))
                return os.path.abspath(path)
            else:
                self.output_message('截图失败!')
                return None
        else:
            return None

    # 坐标工具
    def tool(self):
        # 判断ADB是否启动
        if self.adbtool == None:
            # 显示弹窗
            QMessageBox.information(self.ui, '提示', 'ADB尚未启动!')
            return
        # 判断是否连接
        if self.connected:
            self.axis_tool()
        else:
            # 显示弹窗
            QMessageBox.information(self.ui, '提示', '尚未连接设备!')
            return

    # 坐标工具
    def axis_tool(self):
        w = 300
        h = 600
        # 判断
        if self.tool_dialog == None:
            # 创建坐标工具窗
            self.tool_dialog = QDialog(self.ui)
            self.tool_dialog.setWindowTitle('坐标工具')
            self.tool_dialog.resize(w, h)
        # 截图
        photo_path = self.screen_cap()
        if photo_path != None:
            if self.screen_cap_file != None:
                os.remove(self.screen_cap_file)
            self.screen_cap_file = photo_path
            pix = QPixmap(photo_path)
            w = pix.width()
            h = pix.height()
            # 设置面板大小
            self.tool_dialog.setFixedSize(w / 4, h / 4)
            # 调色板
            palette = QPalette()
            # 缩小图片
            pix = pix.scaled(w / 4, h / 4)
            palette.setBrush(self.tool_dialog.backgroundRole(), QBrush(pix))
            self.tool_dialog.setAutoFillBackground(True)
            self.tool_dialog.setPalette(palette)
            self.tool_dialog.setMouseTracking(True)
            # 绑定鼠标移动事件
            self.tool_dialog.mouseMoveEvent = self.mouse_move
        # 显示窗口
        self.tool_dialog.show()
        # 十字光标
        self.tool_dialog.setCursor(Qt.CrossCursor)

    # 鼠标移动
    def mouse_move(self, event):
        x = event.pos().x()
        y = event.pos().y()
        #print(x, y)
        self.tool_dialog.setWindowTitle('X:{},Y:{}'.format(x * 4, y * 4))

    # 限制数值范围
    def x1_min(self):
        if self.ui.swipe_x1_min_spinBox.value(
        ) >= self.ui.swipe_x1_max_spinBox.value():
            self.ui.swipe_x1_min_spinBox.setValue(
                self.ui.swipe_x1_max_spinBox.value())

    #
    def x1_max(self):
        if self.ui.swipe_x1_max_spinBox.value(
        ) <= self.ui.swipe_x1_min_spinBox.value():
            self.ui.swipe_x1_max_spinBox.setValue(
                self.ui.swipe_x1_min_spinBox.value())

    #
    def y1_min(self):
        if self.ui.swipe_y1_min_spinBox.value(
        ) >= self.ui.swipe_y1_max_spinBox.value():
            self.ui.swipe_y1_min_spinBox.setValue(
                self.ui.swipe_y1_max_spinBox.value())

    #
    def y1_max(self):
        if self.ui.swipe_y1_max_spinBox.value(
        ) <= self.ui.swipe_y1_min_spinBox.value():
            self.ui.swipe_y1_max_spinBox.setValue(
                self.ui.swipe_y1_min_spinBox.value())

    #
    def x2_min(self):
        if self.ui.swipe_x2_min_spinBox.value(
        ) >= self.ui.swipe_x2_max_spinBox.value():
            self.ui.swipe_x2_min_spinBox.setValue(
                self.ui.swipe_x2_max_spinBox.value())

    #
    def x2_max(self):
        if self.ui.swipe_x2_max_spinBox.value(
        ) <= self.ui.swipe_x2_min_spinBox.value():
            self.ui.swipe_x2_max_spinBox.setValue(
                self.ui.swipe_x2_min_spinBox.value())

    #
    def y2_min(self):
        if self.ui.swipe_y2_min_spinBox.value(
        ) >= self.ui.swipe_y2_max_spinBox.value():
            self.ui.swipe_y2_min_spinBox.setValue(
                self.ui.swipe_y2_max_spinBox.value())

    #
    def y2_max(self):
        if self.ui.swipe_y2_max_spinBox.value(
        ) <= self.ui.swipe_y2_min_spinBox.value():
            self.ui.swipe_y2_max_spinBox.setValue(
                self.ui.swipe_y2_min_spinBox.value())
Пример #8
0
class DeviceListDialog(QObject):
    show_tray_notification = Signal(str)
    management_action = Signal(
        str,  # action name
        str,  # action type
        str,  # node id
        bool)  # is_itself
    start_transfers = Signal()
    _update = Signal(list)

    def __init__(self,
                 parent=None,
                 initial_data=(),
                 disk_usage=0,
                 node_status=SS_STATUS_SYNCED,
                 node_substatus=None,
                 dp=1,
                 nodes_actions=(),
                 license_type=None):
        QObject.__init__(self)
        self._update.connect(self._update_data, Qt.QueuedConnection)
        self._dialog = QDialog(parent)
        self._dialog.setWindowIcon(QIcon(':/images/icon.png'))
        self._ui = Ui_Dialog()
        self._ui.setupUi(self._dialog)
        self._dialog.setAttribute(Qt.WA_MacFrameworkScaled)
        self._ui.device_list_view.setFont(QFont('Nano', 10 * dp))
        self._license_type = license_type

        self._model = TableModel(disk_usage, node_status, node_substatus)
        QTimer.singleShot(100, lambda: self.update(initial_data))

        self._view = self._ui.device_list_view
        self._view.setModel(self._model)
        self._view.setSelectionMode(QAbstractItemView.NoSelection)

        self._ui.centralWidget.setFrameShape(QFrame.NoFrame)
        self._ui.centralWidget.setLineWidth(1)

        self._nodes_actions = nodes_actions

    def show(self, on_finished):
        def finished():
            self._dialog.finished.disconnect(finished)
            self._view.resizeRowsToContents()
            self._model.beginResetModel()
            on_finished()

        screen_width = QApplication.desktop().width()
        parent_x = self._dialog.parent().x()
        parent_width = self._dialog.parent().width()
        width = self._dialog.width()
        offset = 16
        if parent_x + parent_width / 2 > screen_width / 2:
            x = parent_x - width - offset
            if x < 0:
                x = 0
        else:
            x = parent_x + parent_width + offset
            diff = x + width - screen_width
            if diff > 0:
                x -= diff
        self._dialog.move(x, self._dialog.parent().y())
        if width > screen_width - offset:
            self._dialog.resize(screen_width - offset, self._dialog.height())

        self._view.setMouseTracking(True)
        self._old_mouse_move_event = self._view.mouseMoveEvent
        self._view.mouseMoveEvent = self._mouse_moved
        self._old_mouse_release_event = self._view.mouseMoveEvent
        self._view.mouseReleaseEvent = self._mouse_released

        logger.info("Opening device list dialog...")
        # Execute dialog
        self._dialog.finished.connect(finished)
        self._dialog.raise_()
        self._dialog.show()

    def update(self, nodes_info):
        self._update.emit(nodes_info)

    def _update_data(self, nodes_info):
        changed_nodes, deleted_nodes = self._model.update(nodes_info)
        for node_id in changed_nodes | deleted_nodes:
            self._nodes_actions.pop(node_id, None)
        self._view.resizeRowsToContents()

    def update_download_speed(self, value):
        self._model.update_node_download_speed(value)
        self._view.resizeRowsToContents()

    def update_upload_speed(self, value):
        self._model.update_node_upload_speed(value)
        self._view.resizeRowsToContents()

    def update_sync_dir_size(self, value):
        self._model.update_node_sync_dir_size(int(value))
        self._view.resizeRowsToContents()

    def update_node_status(self, value, substatus):
        self._model.update_node_status(value, substatus)
        self._view.resizeRowsToContents()

    def close(self):
        self._dialog.reject()

    def set_license_type(self, license_type):
        self._license_type = license_type

    def _mouse_moved(self, event):
        pos = event.pos()
        index = self._view.indexAt(pos)
        if index.isValid():
            if self._model.to_manage(index) and \
                    not self._pos_is_in_scrollbar_header(pos):
                self._view.setCursor(Qt.PointingHandCursor)
            else:
                self._view.setCursor(Qt.ArrowCursor)
        else:
            self._view.setCursor(Qt.ArrowCursor)
        self._old_mouse_move_event(event)

    def _mouse_released(self, event):
        pos = event.pos()
        index = self._view.indexAt(pos)
        if index.isValid():
            if self._model.to_manage(index) and \
                    not self._pos_is_in_scrollbar_header(pos):
                self._show_menu(index, pos)
        self._old_mouse_release_event(event)

    def _pos_is_in_scrollbar_header(self, pos):
        # mouse is not tracked as in view when in header or scrollbar area
        # so pretend we are there if we are near
        pos_in_header = pos.y() < 10
        if pos_in_header:
            return True

        scrollbars = self._view.findChildren(QScrollBar)
        if not scrollbars:
            return False

        pos_x = self._view.mapToGlobal(pos).x()
        for scrollbar in scrollbars:
            if not scrollbar.isVisible():
                continue

            scrollbar_x = scrollbar.mapToGlobal(QPoint(0, 0)).x()
            if scrollbar_x - 10 <= pos_x <= scrollbar_x + scrollbar.width():
                return True

        return False

    def _show_menu(self, index, pos):
        node_id, \
        node_name, \
        is_online, \
        is_itself, \
        is_wiped = self._model.get_node_id_online_itself(index)
        if not node_id:
            return

        license_free = self._license_type == FREE_LICENSE

        menu = QMenu(self._view)
        menu.setStyleSheet("background-color: #EFEFF4; ")
        menu.setToolTipsVisible(license_free)
        if license_free:
            menu.setStyleSheet(
                'QToolTip {{background-color: #222222; color: white;}}')
            menu.hovered.connect(lambda a: self._on_menu_hovered(a, menu))

        def add_menu_item(caption,
                          index=None,
                          action_name=None,
                          action_type="",
                          start_transfers=False,
                          disabled=False,
                          tooltip=""):
            action = menu.addAction(caption)
            action.setEnabled(not disabled)
            action.tooltip = tooltip if tooltip else ""
            if not start_transfers:
                action.triggered.connect(lambda: self._on_menu_clicked(
                    index, action_name, action_type))
            else:
                action.triggered.connect(self.start_transfers.emit)

        tooltip = tr("Not available for free license") \
            if license_free and not is_itself else ""
        if not is_online:
            action_in_progress = ("hideNode", "") in \
                                 self._nodes_actions.get(node_id, set())
            item_text = tr("Remove node") if not action_in_progress \
                else tr("Remove node in progress...")
            add_menu_item(item_text,
                          index,
                          "hideNode",
                          disabled=action_in_progress)
        elif is_itself:
            add_menu_item(tr("Transfers..."), start_transfers=True)
        if not is_wiped:
            wipe_in_progress = ("execute_remote_action", "wipe") in \
                                 self._nodes_actions.get(node_id, set())
            if not wipe_in_progress:
                action_in_progress = ("execute_remote_action", "logout") in \
                                     self._nodes_actions.get(node_id, set())
                item_text = tr("Log out") if not action_in_progress \
                    else tr("Log out in progress...")
                add_menu_item(item_text,
                              index,
                              "execute_remote_action",
                              "logout",
                              disabled=action_in_progress
                              or license_free and not is_itself,
                              tooltip=tooltip)
            item_text = tr("Log out && wipe") if not wipe_in_progress \
                else tr("Wipe in progress...")
            add_menu_item(item_text,
                          index,
                          "execute_remote_action",
                          "wipe",
                          disabled=wipe_in_progress
                          or license_free and not is_itself,
                          tooltip=tooltip)

        pos_to_show = QPoint(pos.x(), pos.y() + 20)
        menu.exec_(self._view.mapToGlobal(pos_to_show))

    def _on_menu_clicked(self, index, action_name, action_type):
        node_id, \
        node_name, \
        is_online, \
        is_itself, \
        is_wiped = self._model.get_node_id_online_itself(index)
        if action_name == "hideNode" and is_online:
            self.show_tray_notification.emit(
                tr("Action unavailable for online node"))
            return

        if (action_name == "hideNode" or action_type == "wipe"):
            if action_name == "hideNode":
                alert_str = tr(
                    '"{}" node will be removed '
                    'from list of devices. Files will not be wiped.'.format(
                        node_name))
            else:
                alert_str = tr(
                    'All files from "{}" node\'s '
                    'pvtbox secured folder will be wiped. '.format(node_name))
            if not self._user_confirmed_action(alert_str):
                return

        if not is_itself:
            self._nodes_actions[node_id].add((action_name, action_type))
        self.management_action.emit(action_name, action_type, node_id,
                                    is_itself)

    def _on_menu_hovered(self, action, menu):
        if not action.tooltip:
            return

        a_geometry = menu.actionGeometry(action)
        point = menu.mapToGlobal(
            QPoint(a_geometry.x() + 30,
                   a_geometry.y() + 5))
        QToolTip.showText(point, action.tooltip, menu, a_geometry,
                          60 * 60 * 1000)

    def _user_confirmed_action(self, alert_str):
        msg = tr("<b>Are</b> you <b>sure</b>?<br><br>{}".format(alert_str))
        userAnswer = msgbox(msg,
                            title=' ',
                            buttons=[
                                (tr('Cancel'), 'Cancel'),
                                (tr('Yes'), 'Yes'),
                            ],
                            parent=self._dialog,
                            default_index=0,
                            enable_close_button=True)

        return userAnswer == 'Yes'

    def on_management_action_in_progress(self, action_name, action_type,
                                         node_id):
        self._nodes_actions[node_id].add((action_name, action_type))