Example #1
0
class StatisticsPanel(QWidget):
    _MAX_STAT_ROW = 3

    def __init__(self, parent: QWidget = None):
        super().__init__(parent)
        self.spinner = QtWaitingSpinner(parent=self, centerOnParent=True,
                                        disableParentWhenSpinning=True)

        self.spinner.setInnerRadius(15)
        self.layout = QGridLayout()
        self.layout.setHorizontalSpacing(2)
        self.layout.setVerticalSpacing(4)
        self.setLayout(self.layout)

    def setStatistics(self, stat: Dict[str, object]) -> None:
        item: QLayoutItem = self.layout.takeAt(0)
        while item:
            item.widget().deleteLater()
            self.layout.removeItem(item)
            item = self.layout.takeAt(0)
        r: int = 0
        c: int = 0
        for k, v in stat.items():
            self.layout.addWidget(QLabel('{}:'.format(k), self), r, c, 1, 1,
                                  alignment=Qt.AlignLeft)
            self.layout.addWidget(QLabel('{}'.format(str(v)), self), r, c + 1, 1, 1,
                                  alignment=Qt.AlignLeft)
            r += 1
            if r % StatisticsPanel._MAX_STAT_ROW == 0:
                self.layout.setColumnMinimumWidth(c + 2, 5)  # separator
                c += 3
                r = 0
Example #2
0
class PageAllegroMonitored(QWidget):
    def __init__(self, parent=None, shared_dict=None):
        QWidget.__init__(self)
        parent.addWidget(self)
        self.shared_dict = shared_dict
        self.parent = parent
        self.setLayoutDirection(Qt.LeftToRight)

        self.gridLayout = QGridLayout(self)
        self.gridLayout.setContentsMargins(0, 0, 0, 0)

        self.scrollArea = QScrollArea(self)
        self.scrollArea.setStyleSheet("""QScrollArea{border: none;}""")
        self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.scrollArea.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored)
        self.scrollArea.setWidgetResizable(True)
        self.gridLayout.addWidget(self.scrollArea, 0, 0, 1, 1)

        self.scrollAreaWidgetContents = QWidget()
        self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 654, 479))
        self.scrollArea.setWidget(self.scrollAreaWidgetContents)

        self.gridLayout_scroll_area = QGridLayout(
            self.scrollAreaWidgetContents)
        self.gridLayout_scroll_area.setSpacing(0)
        self.gridLayout_scroll_area.setContentsMargins(40, 0, 40, -1)

        self.label_title = QLabel("List of your monitored objects",
                                  self.scrollAreaWidgetContents)
        self.label_title.setStyleSheet(styles.label_title)
        self.label_title.setAlignment(Qt.AlignCenter)
        self.gridLayout_scroll_area.addWidget(self.label_title)

        self.spacer = QSpacerItem(20, 40, QSizePolicy.Minimum,
                                  QSizePolicy.Expanding)

        self.load_list()

    def load_list(self):
        elements = data.read_monitored_elements()
        for element in elements:
            e = ElementAllegroMonitored(element['name'], element['link'],
                                        element['is_done'], element['price'],
                                        element['xpath'], element['time'],
                                        element['is_monitoring'],
                                        self.scrollAreaWidgetContents,
                                        self.shared_dict)
            self.gridLayout_scroll_area.addWidget(e)

        self.gridLayout_scroll_area.addItem(self.spacer)

    def add_to_list(self, name, link, is_done, price, xpath, time,
                    is_monitoring):
        self.gridLayout_scroll_area.removeItem(self.spacer)
        e = ElementAllegroMonitored(name, link, is_done, price, xpath, time,
                                    is_monitoring)
        self.gridLayout_scroll_area.addWidget(e)
        self.gridLayout_scroll_area.addItem(self.spacer)
Example #3
0
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        w = QWidget()

        hb = QHBoxLayout()
        hb.setSizeConstraint(QLayout.SetFixedSize)

        self._timer = QTimer()
        self._timer.timeout.connect(self.update_timer)
        self._timer.start(1000)  # 1 second timer

        # tag::status[]
        self.mines = QLabel()
        self.mines.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)

        self.clock = QLabel()
        self.clock.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)

        f = self.mines.font()
        f.setPointSize(24)
        f.setWeight(75)
        self.mines.setFont(f)
        self.clock.setFont(f)

        self.clock.setText("000")

        self.button = QPushButton()
        self.button.setFixedSize(QSize(32, 32))
        self.button.setIconSize(QSize(32, 32))
        self.button.setIcon(QIcon("./icons/smiley.png"))
        self.button.setFlat(True)

        self.button.pressed.connect(self.button_pressed)

        self.statusBar()

        l = QLabel()
        l.setPixmap(QPixmap.fromImage(IMG_BOMB))
        l.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        hb.addWidget(l)

        hb.addWidget(self.mines)
        hb.addWidget(self.button)
        hb.addWidget(self.clock)

        l = QLabel()
        l.setPixmap(QPixmap.fromImage(IMG_CLOCK))
        l.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        hb.addWidget(l)

        vb = QVBoxLayout()
        vb.setSizeConstraint(QLayout.SetFixedSize)
        vb.addLayout(hb)
        # end::status[]

        # tag::grid[]
        self.grid = QGridLayout()
        self.grid.setSpacing(5)
        self.grid.setSizeConstraint(QLayout.SetFixedSize)
        # end::grid[]

        vb.addLayout(self.grid)
        w.setLayout(vb)

        self.setCentralWidget(w)

        self.menuBar().setNativeMenuBar(False)

        # tag::menuGame[]
        game_menu = self.menuBar().addMenu("&Game")

        new_game_action = QAction("New game", self)
        new_game_action.setStatusTip(
            "Start a new game (your current game will be lost)"
        )
        new_game_action.triggered.connect(self.reset_map)
        game_menu.addAction(new_game_action)

        levels = game_menu.addMenu("Levels")
        for n, level in enumerate(LEVELS):
            level_action = QAction(level[0], self)
            level_action.setStatusTip("{1}x{1} grid, with {2} mines".format(*level))
            level_action.triggered.connect(lambda _, n=n: self.set_level(n))
            levels.addAction(level_action)
        # end::menuGame[]

        # Start on easy
        self.set_level(0)
        self.show()

    # tag::levelsInit[]
    def set_level(self, level):
        self.level_name, self.b_size, self.n_mines = LEVELS[level]

        self.setWindowTitle("Moonsweeper - %s" % (self.level_name))
        self.mines.setText("%03d" % self.n_mines)

        self.clear_map()
        self.init_map()
        self.reset_map()

    # end::levelsInit[]

    # tag::clearMap[]
    def clear_map(self):
        # Remove all positions from the map, up to maximum size.
        for x in range(0, LEVELS[-1][1]):  # <1>
            for y in range(0, LEVELS[-1][1]):
                c = self.grid.itemAtPosition(y, x)
                if c:  # <2>
                    self.grid.removeItem(c)
                    c.widget().setParent(None)

    # end::clearMap[]

    # tag::initMap[]
    def init_map(self):
        # Add positions to the map
        for x in range(0, self.b_size):
            for y in range(0, self.b_size):
                w = Pos(x, y)
                self.grid.addWidget(w, y, x)
                # Connect signal to handle expansion.
                w.clicked.connect(self.trigger_start)
                w.revealed.connect(self.on_reveal)
                w.expandable.connect(self.expand_reveal)

        # Place resize on the event queue, giving control back to Qt before.
        QTimer.singleShot(0, lambda: self.resize(1, 1))  # <1>

    # end::initMap[]

    # tag::resetMap[]
    def reset_map(self):
        self._reset_position_data()
        self._reset_add_mines()
        self._reset_calculate_adjacency()
        self._reset_add_starting_marker()
        self.update_timer()

    # end::resetMap[]

    # tag::resetMap1[]
    def _reset_position_data(self):
        # Clear all mine positions
        for x in range(0, self.b_size):
            for y in range(0, self.b_size):
                w = self.grid.itemAtPosition(y, x).widget()
                w.reset()

    # end::resetMap1[]

    # tag::resetMap2[]
    def _reset_add_mines(self):
        # Add mine positions
        positions = []
        while len(positions) < self.n_mines:
            x, y = (
                random.randint(0, self.b_size - 1),
                random.randint(0, self.b_size - 1),
            )
            if (x, y) not in positions:
                w = self.grid.itemAtPosition(y, x).widget()
                w.is_mine = True
                positions.append((x, y))

        # Calculate end-game condition
        self.end_game_n = (self.b_size * self.b_size) - (self.n_mines + 1)
        return positions

    # end::resetMap2[]

    # tag::resetMap3[]
    def _reset_calculate_adjacency(self):
        def get_adjacency_n(x, y):
            positions = self.get_surrounding(x, y)
            return sum(1 for w in positions if w.is_mine)

        # Add adjacencies to the positions
        for x in range(0, self.b_size):
            for y in range(0, self.b_size):
                w = self.grid.itemAtPosition(y, x).widget()
                w.adjacent_n = get_adjacency_n(x, y)

    # end::resetMap3[]

    # tag::resetMap4[]
    def _reset_add_starting_marker(self):
        # Place starting marker.

        # Set initial status (needed for .click to function)
        self.update_status(STATUS_READY)

        while True:
            x, y = (
                random.randint(0, self.b_size - 1),
                random.randint(0, self.b_size - 1),
            )
            w = self.grid.itemAtPosition(y, x).widget()
            # We don't want to start on a mine.
            if not w.is_mine:
                w.is_start = True
                w.is_revealed = True
                w.update()

                # Reveal all positions around this, if they are not mines either.
                for w in self.get_surrounding(x, y):
                    if not w.is_mine:
                        w.click()
                break

        # Reset status to ready following initial clicks.
        self.update_status(STATUS_READY)

    # end::resetMap4[]

    # tag::surrounding[]
    def get_surrounding(self, x, y):
        positions = []

        for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
            for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
                if not (xi == x and yi == y):
                    positions.append(self.grid.itemAtPosition(yi, xi).widget())

        return positions

    # end::surrounding[]

    # tag::statusButton[]
    def button_pressed(self):
        if self.status == STATUS_PLAYING:
            self.game_over()

        elif self.status == STATUS_FAILED or self.status == STATUS_SUCCESS:
            self.reset_map()

    # end::statusButton[]

    def reveal_map(self):
        for x in range(0, self.b_size):
            for y in range(0, self.b_size):
                w = self.grid.itemAtPosition(y, x).widget()
                w.reveal(False)

    # tag::expandReveal[]
    def expand_reveal(self, x, y):
        """
        Iterate outwards from the initial point, adding new locations to the
        queue. This allows us to expand all in a single go, rather than
        relying on multiple callbacks.
        """
        to_expand = [(x, y)]
        to_reveal = []
        any_added = True

        while any_added:
            any_added = False
            to_expand, l = [], to_expand

            for x, y in l:
                positions = self.get_surrounding(x, y)
                for w in positions:
                    if not w.is_mine and w not in to_reveal:
                        to_reveal.append(w)
                        if w.adjacent_n == 0:
                            to_expand.append((w.x, w.y))
                            any_added = True

        # Iterate an reveal all the positions we have found.
        for w in to_reveal:
            w.reveal()

    # end::expandReveal[]

    def trigger_start(self, *args):
        if self.status == STATUS_READY:
            # First click.
            self.update_status(STATUS_PLAYING)
            # Start timer.
            self._timer_start_nsecs = int(time.time())

    # tag::updateStatus[]
    def update_status(self, status):
        self.status = status
        self.button.setIcon(QIcon(STATUS_ICONS[self.status]))

        if status == STATUS_READY:
            self.statusBar().showMessage("Ready")

    # end::updateStatus[]

    def update_timer(self):
        if self.status == STATUS_PLAYING:
            n_secs = int(time.time()) - self._timer_start_nsecs
            self.clock.setText("%03d" % n_secs)

        elif self.status == STATUS_READY:
            self.clock.setText("%03d" % 0)

    # tag::endGame[]
    def on_reveal(self, w):
        if w.is_mine:
            self.game_over()

        else:
            self.end_game_n -= 1  # decrement remaining empty spaces

            if self.end_game_n == 0:
                self.game_won()

    def game_over(self):
        self.reveal_map()
        self.update_status(STATUS_FAILED)

    def game_won(self):
        self.reveal_map()
        self.update_status(STATUS_SUCCESS)
Example #4
0
class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, ui_demo=False):
        super(MainWindow, self).__init__()
        self.setupUi(self)
        self.grid_layout = QGridLayout()

        self.btnDetectClient.clicked.connect(self.detectClients)
        self.lineEditIpRange.returnPressed.connect(self.detectClients)
        self.btnSelectAllClients.clicked.connect(self.selectAllCLients)

        shortcut = QShortcut(QKeySequence(self.tr("Ctrl+A")), self)
        shortcut.activated.connect(self.selectAllCLients)

        self.btnUnselectClients.clicked.connect(self.unselectAllCLients)
        self.btnSelectExam.clicked.connect(self.selectExamByWizard)
        self.btnSelectExam.setEnabled(True)

        self.btnPrepareExam.clicked.connect(self.prepareExam)
        self.btnPrepareExam.setEnabled(False)

        self.btnGetExams.clicked.connect(self.retrieveExamFilesByWizard)
        self.btnGetExams.setEnabled(True)
        self.btnSaveExamLog.clicked.connect(self.saveExamLog)
        # self.btnSaveExamLog.setEnabled(False)

        self.actionBearbeiten.triggered.connect(self.openConfigDialog)
        self.actionAlle_Benutzer_benachrichtigen.triggered.connect(
            self.sendMessage)
        self.actionAlle_Clients_zur_cksetzen.triggered.connect(
            self.resetClients)
        self.actionAlle_Clients_rebooten.triggered.connect(
            self.rebootAllClients)
        self.actionAlle_Clients_herunterfahren.triggered.connect(
            self.shutdownAllClients)
        self.actionOnlineHelp.triggered.connect(self.openHelpUrl)
        self.actionOfflineHelp.triggered.connect(self.openHelpUrlOffline)
        self.actionSortClientByCandidateName.triggered.connect(
            self.sortButtonsByCandidateName)
        self.actionSortClientByComputerName.triggered.connect(
            self.sortButtonsByComputerName)
        self.actionVersionInfo.triggered.connect(self.showVersionInfo)
        self.actionDisplayIPs.triggered.connect(self.toggleClientIpDisplay)

        self.btnApplyCandidateNames.clicked.connect(self.applyCandidateNames)
        self.btnNameClients.clicked.connect(self.activateNameTab)

        self.btnBlockUsb.clicked.connect(self.blockUsb)
        self.btnBlockWebAccess.clicked.connect(self.blockWebAccess)

        self.appTitle = 'ECMan - Exam Client Manager'
        self.setWindowTitle(self.appTitle)

        self.logger = Logger(self.textEditLog)

        self.lb_directory = None
        self.advancedUi = False
        self.configure()
        self.show()
        if ui_demo is False:
            self.detectClients()

        else:
            self.clientFrame.setLayout(self.grid_layout)
            for i in range(6):
                self.addTestClient(i + 100)

        self.statusBar = QStatusBar()
        self.setStatusBar(self.statusBar)

    def sortButtonsByCandidateName(self):
        '''
        sort LbClient-widgets by candidate name
        :return: nothing
        '''
        clients = [
            self.grid_layout.itemAt(i).widget()
            for i in range(self.grid_layout.count())
        ]
        clients = sorted(clients,
                         key=lambda client: client.computer.candidateName)
        self.arrangeClientButtons(clients)

    def sortButtonsByComputerName(self):
        '''
        supposed to sort LbClient-widgets by candidate name
        :return: nothing
        '''
        clients = [
            self.grid_layout.itemAt(i).widget()
            for i in range(self.grid_layout.count())
        ]
        clients = sorted(clients,
                         key=lambda client: client.computer.getHostName())
        self.arrangeClientButtons(clients)

    def toggleClientIpDisplay(self):
        clients = [
            self.grid_layout.itemAt(i).widget()
            for i in range(self.grid_layout.count())
        ]
        for client in clients:
            client.toggleShowIp()

    def arrangeClientButtons(self, clients):
        try:
            for i in reversed(range(self.grid_layout.count())):
                self.grid_layout.removeItem(self.grid_layout.itemAt(i))
        except:
            pass

        self.clientFrame.setLayout(self.grid_layout)
        for button in clients:
            self.grid_layout.addWidget(button,
                                       self.grid_layout.count() / 4,
                                       self.grid_layout.count() % 4)

        self.clientFrame.setLayout(self.grid_layout)

    def activateNameTab(self):
        if self.textEditCandidates.toPlainText() == "":
            candidates = "\n".join(
                str(x + 1) for x in range(self.grid_layout.count()))
            self.textEditCandidates.setText(candidates)

        self.tabs.setCurrentWidget(self.tab_candidates)

    def openHelpUrl(self):
        webbrowser.open(
            self.config.get(
                "General",
                "wikiurl",
                fallback="https://github.com/greenorca/ECMan/wiki"))

    def openHelpUrlOffline(self):
        webbrowser.open("file://" + os.getcwd().replace("\\", "/") +
                        "/help/Home.html")

    def checkOldLogFiles(self):
        """
        dummy,
        """
        pass

    def blockUsb(self):
        block = self.btnBlockUsb.text() == "USB blockieren"
        clients = [
            self.grid_layout.itemAt(i).widget()
            for i in range(self.grid_layout.count())
            if self.grid_layout.itemAt(i).widget().isSelected
        ]

        if block:
            for client in clients:
                client.blockUsbAccessThread()
            self.btnBlockUsb.setText("USB freigeben")
        else:
            for client in clients:
                client.allowUsbAccessThread()
            self.btnBlockUsb.setText("USB blockieren")

    def blockWebAccess(self):
        block = self.btnBlockWebAccess.text() == "Web blockieren"
        clients = [
            self.grid_layout.itemAt(i).widget()
            for i in range(self.grid_layout.count())
            if self.grid_layout.itemAt(i).widget().isSelected
        ]
        if block:
            for client in clients:
                client.blockInternetAccessThread()

            self.btnBlockWebAccess.setText("Web freigeben")

        else:
            for client in clients:
                client.allowInternetAccessThread()

            self.btnBlockWebAccess.setText("Web blockieren")

    def resetClients(self):
        """
        resets remote files and configuration for all connected clients
        """

        items = ["Ja, Namen zurücksetzen", "Nein, Namen beibehalten"]
        item, ok = QInputDialog().getItem(
            self, "LB-Status zurücksetzen",
            "USB-Sticks und Internet werden freigeben.\nDaten im Benutzerverzeichnis werden NICHT gelöscht.\nKandidaten-Namen zurücksetzen? ",
            items, 0, False)
        if ok is False:
            return

        resetCandidateName = True if item.startswith("Ja") else False

        clients = [
            self.grid_layout.itemAt(i).widget()
            for i in range(self.grid_layout.count())
        ]

        progressDialog = EcManProgressDialog(self, "Reset Clients")
        progressDialog.setMaxValue(self.grid_layout.count())
        progressDialog.resetValue()
        progressDialog.open()

        self.worker = ResetClientsWorker(clients, resetCandidateName)
        self.worker.updateProgressSignal.connect(progressDialog.incrementValue)
        self.worker.start()

    def closeEvent(self, event):
        """
        overrides closeEvent of base class, clean up

        """
        print("cleaning up...")
        #         try:* especially remove shares created on Windows hosts
        #             for share in self.sharenames:
        #                 Thread(target=self.runLocalPowerShellAsRoot("Remove-SmbShare -Name {} -Force".format(share))).start()
        #         except Exception:
        #             pass
        #
        print("done cleaning up...")
        super(MainWindow, self).closeEvent(event)

    def selectAllCLients(self):
        """
        marks / selects all connected client pcs
        """
        for i in range(self.grid_layout.count()):
            self.grid_layout.itemAt(i).widget().select()

    def unselectAllCLients(self):
        """
        unmarks / unselects all connected client pcs
        """
        for i in range(self.grid_layout.count()):
            self.grid_layout.itemAt(i).widget().unselect()

    def shutdownAllClients(self):
        for i in range(self.grid_layout.count()):
            self.grid_layout.itemAt(i).widget().shutdownClient()

    def rebootAllClients(self):
        for i in range(self.grid_layout.count()):
            self.grid_layout.itemAt(i).widget().computer.reboot()
            self.grid_layout.itemAt(i).widget().deleteLater()
        # TODO TEST sven
        #for i in range(self.grid_layout.count()):
        #    self.grid_layout.removeItem(self.grid_layout.itemAt(i))

    def sendMessage(self):

        message, ok = QInputDialog.getText(self, "Eingabe",
                                           "Nachricht an Kandidaten eingeben")
        if ok:
            for i in range(self.grid_layout.count()):
                QThreadPool.globalInstance().start(
                    SendMessageTask(
                        self.grid_layout.itemAt(i).widget(), message))

    def applyCandidateNames(self):
        """
        reads candidate names from respective textEditField (line by line)
        and applies these names to (random) client pcs
        """
        names = self.textEditCandidates.toPlainText().rstrip().splitlines()
        # cleanup and remove duplicate names
        names = [x.strip() for x in names]
        names = list(set(names))

        clients = [
            self.grid_layout.itemAt(i).widget()
            for i in range(self.grid_layout.count())
        ]

        # select only the computers without candidate name
        if self.checkBox_OverwriteExisitingNames.checkState(
        ) != Qt.CheckState.Checked:
            clients = [
                x for x in clients if x.computer.getCandidateName() == ""
                or x.computer.getCandidateName() is None
            ]

        if len(names) > len(clients):
            self.showMessageBox(
                "Fehler",
                "Nicht genug Prüfungs-PCs für alle {} Kandidaten".format(
                    str(len(names))),
                messageType=QMessageBox.Warning)
            return

        progressDialog = EcManProgressDialog(
            self, "Fortschritt Kandidatennamen setzen")
        progressDialog.setMaxValue(len(names))
        progressDialog.resetValue()

        self.worker = SetCandidateNamesWorker(clients, names)
        self.worker.updateProgressSignal.connect(progressDialog.incrementValue)
        self.worker.start()

        progressDialog.open()
        self.tabs.setCurrentWidget(self.tab_pcs)

    def configure(self):
        """
        sets inial values for app
        """
        self.port = 5986
        self.server = None

        self.configFile = Path(str(Path.home()) + "/.ecman.conf")

        if not (self.configFile.exists()):
            result = self.openConfigDialog()

        if self.configFile.exists() or result == 1:
            self.readConfigFile()

        self.result_directory = ""

        self.sharenames = [
        ]  # dump all eventually created local Windows-Shares in this array
        self.debug = True
        # fetch own ip adddress
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            s.connect(('9.9.9.9', 1))  # connect() for UDP doesn't send packets
            self.ip_address = s.getsockname()[0]
            parts = self.ip_address.split(".")[0:-1]
            self.ipRange = ".".join(parts) + ".*"
            self.lineEditIpRange.setText(self.ipRange)
            self.appTitle = self.windowTitle() + " on " + self.ip_address
            self.setWindowTitle(self.appTitle)
        except Exception as ex:
            self.ipRange, ok = QInputDialog.getText(
                self, "Keine Verbindung zum Internet",
                "Möglicherweise gelingt der Sichtflug. Bitte geben Sie die lokale IP-Adresse ein:"
            )

            self.log("no connection to internet:" + str(ex))

    def readConfigFile(self):
        """
        reads config file into class variables
        """
        self.config = ConfigParser()
        self.config.read_file(open(str(self.configFile)))

        self.port = self.config.get("General", "winrm_port", fallback=5986)

        self.client_lb_user = self.config.get("Client",
                                              "lb_user",
                                              fallback="student")
        self.user = self.config.get("Client", "user", fallback="")
        self.passwd = self.config.get("Client", "pwd", fallback="")
        self.maxFiles = int(
            self.config.get("Client", "max_files", fallback="1000"))
        self.maxFileSize = int(
            self.config.get("Client", "max_filesize",
                            fallback="1000")) * 1024 * 1024  # thats MB now...

        self.advancedUi = self.config.get('General',
                                          'advanced_ui',
                                          fallback="False") == "True"
        pass

    def openConfigDialog(self):
        """
        opens configuration dialog
        """
        config_dialog = EcManConfigDialog(self)
        result = config_dialog.exec_()
        if result == 1:
            config_dialog.saveConfig()
            self.readConfigFile()
        return result

    def log(self, message):
        """
        basic logging functionality
        TODO: improve...
        """
        self.logger.log(message)

    def updateProgressBar(self, value):
        self.progressBar.setValue(value)
        if (value == self.progressBar.maximum()):
            self.enableButtons(True)
            self.progressBar.setEnabled(False)

    def enableButtons(self, enable):

        if type(enable) != bool:
            raise Exception("Invalid parameter, must be boolean")
        self.btnNameClients.setEnabled(enable)
        self.btnSelectAllClients.setEnabled(enable)
        self.btnUnselectClients.setEnabled(enable)
        self.btnPrepareExam.setEnabled(enable)
        self.btnGetExams.setEnabled(enable)
        self.btnSaveExamLog.setEnabled(enable)
        self.btnDetectClient.setEnabled(enable)
        self.btnBlockWebAccess.setEnabled(enable)
        self.btnBlockUsb.setEnabled(enable)

    def retrieveExamFilesByWizard(self):
        """
        retrieve exam files for all clients
        """
        # find all suitable clients (required array for later threading)
        clients = [
            self.grid_layout.itemAt(i).widget()
            for i in range(self.grid_layout.count())
            if self.grid_layout.itemAt(i).widget().isSelected
            and self.grid_layout.itemAt(i).widget().computer.state in [
                Computer.State.STATE_DEPLOYED, Computer.State.STATE_FINISHED,
                Computer.State.STATE_RETRIVAL_FAIL
            ]
        ]

        if len(clients) == 0:
            self.showMessageBox("Achtung",
                                "Keine Clients ausgewählt bzw. deployed")
            return

        unknownClients = [
            c for c in clients if c.computer.getCandidateName() == ""
            or c.computer.getCandidateName() == None
        ]

        for unknown in unknownClients:
            unknown.computer.state = Computer.State.STATE_RETRIVAL_FAIL
            unknown._colorizeWidgetByClientState()

        if unknownClients != []:
            choice = QMessageBox.critical(
                self, "Achtung",
                "{} clients ohne gültigen Kandidatennamen.<br>Rückholung für alle anderen fortsetzen?"
                .format(str(len(unknownClients))), QMessageBox.Yes,
                QMessageBox.No)

            if choice == QMessageBox.No:
                return

        clients = [c for c in clients if c not in unknownClients]

        retVal = QMessageBox.StandardButton.Yes
        if self.result_directory != "":

            items = [
                "Ergebnispfad neu auswählen",
                "Weiter mit bisherigem Verzeichnis"
            ]
            item, ok = QInputDialog().getItem(
                self, "Achtung",
                "LB-Ergebnisverzeichnis ist bereits ausgewählt.\nErneutes Abholen kann existierende Ergebnisse überschreiben.\nWas möchten Sie tun?",
                items, 0, False)

            if ok is False:
                return

        if self.result_directory == "" or item == "Ergebnispfad neu auswählen":

            if self.server is None or self.server.connect() is not True:
                self.server = self.getServerCredentialsByWizard()
                if self.server is None:
                    return

            wizard = EcShareWizard(
                parent=self,
                server=self.server,
                wizardType=EcShareWizard.TYPE_RESULT_DESTINATION,
                advanced_Ui=self.advancedUi)

            wizard.setModal(True)
            result = wizard.exec_()
            print("I'm done, wizard result=" + str(result))
            if result == 1:
                print("selected values: %s - %s - %s" %
                      (wizard.field("username"), wizard.field("servername"),
                       wizard.selectedPath))

                self.result_directory = wizard.selectedPath

            else:
                print("Abbruch, kein Zielverzeichnis ausgewählt")
                return

        self.result_directory = self.result_directory.replace("/", "#")
        self.log("save result files into: " +
                 self.result_directory.replace("#", "\\"))

        progressDialog = EcManProgressDialog(
            self, "Fortschritt Ergebnisse kopieren")
        progressDialog.setMaxValue(len(clients))
        progressDialog.resetValue()
        progressDialog.open()

        self.log("starting to retrieve files")

        self.worker = RetrieveResultsWorker(clients, self.server.user,
                                            self.server.password,
                                            self.server.domain,
                                            self.result_directory,
                                            self.maxFiles, self.maxFileSize)
        self.worker.updateProgressSignal.connect(progressDialog.incrementValue)
        self.worker.start()

        self.btnSaveExamLog.setEnabled(True)

    def prepareExam(self):
        self.copyFilesToClient()
        pass

    def copyFilesToClient(self):
        """
        copies selected exam folder to all connected clients that are selected and not in STATE_DEPLOYED or STATE_FINISHED
        """
        if self.lb_directory == None or self.lb_directory == "":
            self.showMessageBox("Fehler", "Kein Prüfungsordner ausgewählt")
            return

        clients = [
            self.grid_layout.itemAt(i).widget()
            for i in range(self.grid_layout.count())
            if self.grid_layout.itemAt(i).widget().isSelected
            and self.grid_layout.itemAt(i).widget().computer.state not in
            [Computer.State.STATE_DEPLOYED, Computer.State.STATE_FINISHED]
        ]

        if len([x for x in clients if x.computer.candidateName == None]) > 0:
            self.showMessageBox("Warnung",
                                "Bitte Kandidatenname für alle PCs vergeben")
            return

        if len(clients) == 0:
            self.showMessageBox(
                "Warnung",
                "Keine Client-PCs ausgewählt oder Prüfungen bereits aktiv")
            return

        progressDialog = EcManProgressDialog(
            self, "Fortschritt LB-Client-Deployment")
        progressDialog.setMaxValue(len(clients))
        progressDialog.resetValue()
        progressDialog.open()

        self.worker = CopyExamsWorker(
            clients,
            self.server.user,
            self.server.password,
            self.server.domain,
            src=self.lb_directory,
            reset=(self.checkBoxWipeHomedir.checkState() ==
                   Qt.CheckState.Checked))
        self.worker.updateProgressSignal.connect(progressDialog.incrementValue)
        self.worker.start()

    def detectClients(self):
        """
        starts portscan to search for winrm enabled clients
        """
        ip_range = self.lineEditIpRange.text()
        if not (ip_range.endswith('*')):
            self.showMessageBox(
                'Eingabefehler',
                'Gültiger IP-V4 Bereich endet mit * (z.B. 192.168.0.*)')
            return

        try:
            self.worker.exit()
        except Exception as ex:
            print("crashed on stopping existing scanner thread: " + str(ex))

        self.ipRange = ip_range
        self.progressBar.setEnabled(True)
        self.progressBar.setValue(0)

        self.enableButtons(enable=False)
        self.clientCount = 0
        # clear previous client buttons
        try:
            for i in reversed(range(self.grid_layout.count())):
                self.grid_layout.itemAt(i).widget().close()
                self.grid_layout.itemAt(i).widget().deleteLater()
        except:
            pass
        self.clientFrame.setLayout(self.grid_layout)

        self.progressBar.setMaximum(253)
        self.worker = ScannerWorker(self.ipRange, self.ip_address)
        self.worker.updateProgressSignal.connect(self.updateProgressBar)
        self.worker.addClientSignal.connect(self.addClient)
        self.worker.start()

    def addClient(self, ip):
        """
        populate GUI with newly received client ips
        param ip: only last byte required
        param scan: wether or not scan the Client (set to False only for GUI testing)
        """
        self.log("new client signal received: " + str(ip))
        self.clientCount += 1
        self.statusBar.showMessage(
            str(self.clientCount) + " clients detected", 0)

        clientIp = self.ipRange.replace("*", str(ip))
        button = LbClient(clientIp,
                          remoteAdminUser=self.user,
                          passwd=self.passwd,
                          candidateLogin=self.client_lb_user,
                          parentApp=self)
        button.setMinimumHeight(50)
        # button.installEventFilter(self)
        self.grid_layout.addWidget(button,
                                   self.grid_layout.count() / 4,
                                   self.grid_layout.count() % 4)
        self.clientFrame.setLayout(self.grid_layout)
        # QtGui.qApp.processEvents()

    def addTestClient(self, ip):
        """
        populate GUI with dummy buttons
        param ip: only last byte required
        """
        self.log("new client signal received: " + str(ip))
        clientIp = self.ipRange.replace("*", str(ip))
        button = LbClient(clientIp,
                          remoteAdminUser=self.user,
                          passwd=self.passwd,
                          candidateLogin=self.client_lb_user,
                          parentApp=self,
                          test=True)
        button.setMinimumHeight(50)
        # button.installEventFilter(self)
        self.grid_layout.addWidget(button,
                                   self.grid_layout.count() / 4,
                                   self.grid_layout.count() % 4)
        self.clientFrame.setLayout(self.grid_layout)
        # QtGui.qApp.processEvents()

    def getExamPath(self):
        return self.lb_directory

    def getServerCredentialsByWizard(self):
        """
        open server config and login dialog, returns server object or None
        """
        wizard = EcLoginWizard(parent=self,
                               username=self.config.get("General",
                                                        "username",
                                                        fallback=""),
                               domain=self.config.get("General",
                                                      "domain",
                                                      fallback=""),
                               servername=self.config.get("General",
                                                          "lb_server",
                                                          fallback=""))

        wizard.setModal(True)
        result = wizard.exec_()
        print("I'm done, wizard result=" + str(result))
        if result == 1:
            self.config["General"]["username"] = wizard.field("username")
            self.config["General"]["domain"] = wizard.field("domainname")
            self.config["General"]["servername"] = wizard.server.serverName

            self.saveConfig()
            return wizard.server

        return None

    def selectExamByWizard(self):
        """
        provides ability to select serverName share plus logon credentials and lb directory using a wizard
        """

        if self.server == None or self.server.connect() is False:
            self.server = self.getServerCredentialsByWizard()

        wizard = EcShareWizard(parent=self,
                               server=self.server,
                               wizardType=EcShareWizard.TYPE_LB_SELECTION,
                               advanced_Ui=self.advancedUi)

        wizard.setModal(True)
        result = wizard.exec_()
        print("I'm done, wizard result=" + str(result))
        if result == 1:
            self.lb_directory = wizard.selectedPath
            self.setWindowTitle(self.appTitle + " - LB-Verzeichnis::" +
                                self.lb_directory.split("/")[-1])
            self.log("setup LB directory: " + self.lb_directory.split("/")[-1])
            self.btnPrepareExam.setEnabled(True)
            self.lblExamName.setText(self.lb_directory)
        else:
            self.log("no valid share selected")

    def saveExamLog(self):
        """
        on demand, store all client logs as PDF
        """
        if self.result_directory == None or len(self.result_directory) == 0:
            self.showMessageBox(
                "Fehler",
                "Ergebnispfad für Prüfungsdaten nicht gesetzt.<br>Bitte zuerst Prüfungsdaten abholen.",
                QMessageBox.Error)
            return

        clients = [
            self.grid_layout.itemAt(i).widget()
            for i in range(self.grid_layout.count())
        ]
        for client in clients:
            lb_dataDirectory = client.computer.lb_dataDirectory.split("#")[-1]
            pdfFileName = "protocol_" + date.today(
            ).__str__() + "_" + client.computer.getCandidateName().replace(
                " ", "_") + ".pdf"
            LogfileHandler(client.computer.logfile_name, client.computer.getCandidateName()). \
                createPdf(pdfFileName)
            if not (self.server.connect()):
                self.showMessageBox(
                    "Fehler",
                    "Verbindung zum Server kann nicht aufgebaut werden.",
                    QMessageBox.Error)
                return

            with open(pdfFileName, "rb") as file:
                sharename = self.result_directory.replace("##",
                                                          "").split("#")[1]
                destination = "/".join(
                    self.result_directory.replace(
                        "##",
                        "").split("#")[2:]) + "/" + lb_dataDirectory + "/"
                self.server.conn.storeFile(sharename,
                                           destination + pdfFileName, file)

    def __runLocalPowerShellAsRoot(self, command):
        """
        maybe useful at some stage again
        # see https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-shellexecutew#parameters
        :param command:
        :return:
        """

        retval = ctypes.windll.shell32.ShellExecuteW(
            None,
            "runas",  # runas admin,
            "C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\powershell.exe",
            # file to run
            command,  # actual powershell command
            None,
            0)  # last param disables popup powershell window...

        if retval != 42:
            self.log("ReturnCode after creating smbShare: " + str(retval))
            subprocess.run([
                "C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\powershell.exe",
                "Get-SmbShare"
            ])
            raise Exception("ReturnCode after running powershell: " +
                            str(retval))

    def __openFile(self):
        fname = QFileDialog.getOpenFileName(self, 'Open file', '/home')
        if fname[0]:
            f = open(fname[0], 'r')
        with f:
            data = f.read()
            doc = QTextDocument(data, None)
            self.textEditLog.setDocument(doc)

    def saveConfig(self):
        """
        write to file what's currently set in config
        """
        if not (self.configFile.exists()):
            self.configFile.touch()

        self.config.write(open(self.configFile, 'w'))

    def eventFilter(self, currentObject, event):
        """
        unused, define mouseover events (with tooltips) for LbClient widgets
        """
        if event.type() == QEvent.Enter:
            if isinstance(currentObject, LbClient):
                print("Mouseover event catched")
                currentObject.setOwnToolTip()
                return True
            else:
                self.log(str(type(currentObject)) + " not recognized")

                # elif event.type() == QEvent.Leave:
        #    pass
        return False

    def showMessageBox(self,
                       title,
                       message,
                       messageType=QMessageBox.Information):
        """
        convinence wrapper
        """
        msg = QMessageBox(messageType, title, message, parent=self)
        if messageType != QMessageBox.Information:
            msg.setStandardButtons(QMessageBox.Abort)
        return msg.exec_()

    def showVersionInfo(self):
        info = "<b>Offizielles Release:</b><br>" + version
        try:
            # latest release - make sure to save this file before building :-)
            modDate = time.localtime(os.path.getmtime(__file__))
            info = info + "<br><br><b>Diese Version:</b><br>" + time.strftime(
                "%Y-%m-%d", modDate)
        except:
            pass

        self.showMessageBox("ECMan - Version", info)
Example #5
0
class SequenceRecordsWindow(QWidget):
    def __init__(self, parent):
        super(SequenceRecordsWindow, self).__init__(parent)
        self.grid_layout = QGridLayout()
        self.grid_layout.setContentsMargins(0, 0, 0, 0)
        self.grid_layout.setSpacing(0)
        self.setLayout(self.grid_layout)

        self.seq_font = QFont()
        self.seq_font.setFamily("Noto Sans Mono")
        self.seq_font.setPointSize(12)
        self.seq_font.setFixedPitch(True)
        self.seq_font.setStyleHint(QFont.Monospace)

        self.seq_h_scroll_bar = QScrollBar(self, self.parent())
        self.seq_h_scroll_bar.setOrientation(Qt.Horizontal)
        self.seq_h_scroll_bar.setMinimum(0)
        self.seq_h_scroll_bar.setMaximum(self.longest_seq_len - self.char_nb)
        self.seq_h_scroll_bar.valueChanged.connect(self.move_seqs)
        self.grid_layout.addWidget(self.seq_h_scroll_bar,
                                   self.grid_layout.rowCount(), 5)

        self.lower_spacer_item = QSpacerItem(1, 1, QSizePolicy.Minimum,
                                             QSizePolicy.MinimumExpanding)
        self.grid_layout.addItem(self.lower_spacer_item)

        self.seq_record_items = []

    def sizeHint(self):  # Workaroud QTBUG-70305
        return self.parent().parent().size()

    def populate(self, seq_records):
        self.grid_layout.removeWidget(self.seq_h_scroll_bar)
        self.grid_layout.removeItem(self.lower_spacer_item)

        for seq_record in seq_records:
            new_row = self.grid_layout.rowCount()
            self.seq_record_items.append(
                SequenceRecordItem(self, seq_record, self.seq_font))
            for widget_index in range(0,
                                      len(self.seq_record_items[-1].widgets)):
                col = widget_index
                self.seq_record_items[-1].seqLabel.installEventFilter(self)
                self.grid_layout.addWidget(
                    self.seq_record_items[-1].widgets[widget_index], new_row,
                    col)

            if len(seq_record) > self.longest_seq_len:
                self.longest_seq_len = len(seq_record)

        self.update_char_nb()
        self.grid_layout.addWidget(self.seq_h_scroll_bar,
                                   self.grid_layout.rowCount(), 5)
        self.grid_layout.addItem(self.lower_spacer_item)
        self.display_all_seq()

    def clear(self):
        # TODO
        pass

    def eventFilter(self, watched, event):
        if event.type() == QEvent.Resize:
            self.update_char_nb()
            self.update_scrollbar()
            self.display_all_seq()
        return super(SequenceRecordsWindow, self).eventFilter(watched, event)

    def display_all_seq(self):
        for seq_record_item in self.seq_record_items:
            seq_record_item.seqLabel.display_seq(
                seq_record_item.seq_record.seq, self.display_begin,
                self.char_nb)

    def update_char_nb(self):
        font_metrics = QFontMetrics(self.seq_font)
        px_wide_char = font_metrics.width("A")
        label_width = self.seq_record_items[0].seqLabel.width(
        )  # width of first seq label = all seq labels
        approx_char_nb = label_width // px_wide_char

        test_str = "A" * approx_char_nb

        while font_metrics.width(
                test_str) < label_width:  # fontMetrics not precise at all...
            test_str += "A"

        while font_metrics.width(
                test_str) >= label_width:  # fontMetrics not precise at all...
            test_str = test_str[:-1]

        self.char_nb = len(test_str)

    def update_scrollbar(self):
        self.seq_h_scroll_bar.setMaximum(self.longest_seq_len - self.char_nb +
                                         12)

    def move_seqs(self, value):
        print(value)
        self.display_begin = value
        self.display_all_seq()

    char_nb = 0
    longest_seq_len = 0
    display_begin = 0
Example #6
0
class JointStatePublisherGui(QWidget):
    sliderUpdateTrigger = Signal()

    def __init__(self, title, jsp, num_rows=0):
        super(JointStatePublisherGui, self).__init__()
        self.jsp = jsp
        self.joint_map = {}
        self.vlayout = QVBoxLayout(self)
        self.scrollable = QWidget()
        self.gridlayout = QGridLayout()
        self.scroll = QScrollArea()
        self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scroll.setWidgetResizable(True)

        font = QFont("Helvetica", 9, QFont.Bold)

        ### Generate sliders ###
        sliders = []
        for name in self.jsp.joint_list:
            if name not in self.jsp.free_joints:
                continue
            joint = self.jsp.free_joints[name]

            if joint['min'] == joint['max']:
                continue

            joint_layout = QVBoxLayout()
            row_layout = QHBoxLayout()

            label = QLabel(name)
            label.setFont(font)
            row_layout.addWidget(label)
            display = QLineEdit("0.00")
            display.setAlignment(Qt.AlignRight)
            display.setFont(font)
            display.setReadOnly(True)
            row_layout.addWidget(display)

            joint_layout.addLayout(row_layout)

            slider = QSlider(Qt.Horizontal)

            slider.setFont(font)
            slider.setRange(0, RANGE)
            slider.setValue(RANGE / 2)

            joint_layout.addWidget(slider)

            self.joint_map[name] = {
                'slidervalue': 0,
                'display': display,
                'slider': slider,
                'joint': joint
            }
            # Connect to the signal provided by QSignal
            slider.valueChanged.connect(self.onValueChanged)
            sliders.append(joint_layout)

        # Determine number of rows to be used in grid
        self.num_rows = num_rows
        # if desired num of rows wasn't set, default behaviour is a vertical layout
        if self.num_rows == 0:
            self.num_rows = len(sliders)  # equals VBoxLayout
        # Generate positions in grid and place sliders there
        self.positions = self.generate_grid_positions(len(sliders),
                                                      self.num_rows)
        for item, pos in zip(sliders, self.positions):
            self.gridlayout.addLayout(item, *pos)

        # Set zero positions read from parameters
        self.center()

        # Synchronize slider and displayed value
        self.sliderUpdate(None)

        # Set up a signal for updating the sliders based on external joint info
        self.sliderUpdateTrigger.connect(self.updateSliders)

        self.scrollable.setLayout(self.gridlayout)
        self.scroll.setWidget(self.scrollable)
        self.vlayout.addWidget(self.scroll)

        # Buttons for randomizing and centering sliders and
        # Spinbox for on-the-fly selecting number of rows
        self.randbutton = QPushButton('Randomize', self)
        self.randbutton.clicked.connect(self.randomize_event)
        self.vlayout.addWidget(self.randbutton)
        self.ctrbutton = QPushButton('Center', self)
        self.ctrbutton.clicked.connect(self.center_event)
        self.vlayout.addWidget(self.ctrbutton)
        self.maxrowsupdown = QSpinBox()
        self.maxrowsupdown.setMinimum(1)
        self.maxrowsupdown.setMaximum(len(sliders))
        self.maxrowsupdown.setValue(self.num_rows)
        self.maxrowsupdown.lineEdit().setReadOnly(
            True)  # don't edit it by hand to avoid weird resizing of window
        self.maxrowsupdown.valueChanged.connect(self.reorggrid_event)
        self.vlayout.addWidget(self.maxrowsupdown)
        self.setLayout(self.vlayout)

    @pyqtSlot(int)
    def onValueChanged(self, event):
        # A slider value was changed, but we need to change the joint_info metadata.
        for name, joint_info in self.joint_map.items():
            joint_info['slidervalue'] = joint_info['slider'].value()
            joint = joint_info['joint']
            joint['position'] = self.sliderToValue(joint_info['slidervalue'],
                                                   joint)
            joint_info['display'].setText("%.2f" % joint['position'])

    @pyqtSlot()
    def updateSliders(self):
        self.update_sliders()

    def update_sliders(self):
        for name, joint_info in self.joint_map.items():
            joint = joint_info['joint']
            joint_info['slidervalue'] = self.valueToSlider(
                joint['position'], joint)
            joint_info['slider'].setValue(joint_info['slidervalue'])

    def center_event(self, event):
        self.center()

    def center(self):
        rospy.loginfo("Centering")
        for name, joint_info in self.joint_map.items():
            joint = joint_info['joint']
            joint_info['slider'].setValue(
                self.valueToSlider(joint['zero'], joint))

    def reorggrid_event(self, event):
        self.reorganize_grid(event)

    def reorganize_grid(self, number_of_rows):
        self.num_rows = number_of_rows

        # Remove items from layout (won't destroy them!)
        items = []
        for pos in self.positions:
            item = self.gridlayout.itemAtPosition(*pos)
            items.append(item)
            self.gridlayout.removeItem(item)

        # Generate new positions for sliders and place them in their new spots
        self.positions = self.generate_grid_positions(len(items),
                                                      self.num_rows)
        for item, pos in zip(items, self.positions):
            self.gridlayout.addLayout(item, *pos)

    def generate_grid_positions(self, num_items, num_rows):
        if num_rows == 0:
            return []
        positions = [
            (y, x)
            for x in range(int((math.ceil(float(num_items) / num_rows))))
            for y in range(num_rows)
        ]
        positions = positions[:num_items]
        return positions

    def randomize_event(self, event):
        self.randomize()

    def randomize(self):
        rospy.loginfo("Randomizing")
        for name, joint_info in self.joint_map.items():
            joint = joint_info['joint']
            joint_info['slider'].setValue(
                self.valueToSlider(random.uniform(joint['min'], joint['max']),
                                   joint))

    def sliderUpdate(self, event):
        for name, joint_info in self.joint_map.items():
            joint_info['slidervalue'] = joint_info['slider'].value()
        self.update_sliders()

    def valueToSlider(self, value, joint):
        return (value - joint['min']) * float(RANGE) / (joint['max'] -
                                                        joint['min'])

    def sliderToValue(self, slider, joint):
        pctvalue = slider / float(RANGE)
        return joint['min'] + (joint['max'] - joint['min']) * pctvalue
Example #7
0
class CodePreview_Widget(QWidget):
    def __init__(self):
        super(CodePreview_Widget, self).__init__()

        self.text_edit = CodePreview_TextEdit()
        self.node_instance = None
        self.buttons_obj_dict = {}
        self.active_class_index = -1
        self.edited_codes = {}

        settings_layout = QHBoxLayout()

        info_and_SH_layout = QVBoxLayout()

        # info label
        info_label = QLabel('Click on edit for more info!')
        info_label.setFont(QFont('Poppins', 8))
        info_and_SH_layout.addWidget(info_label)

        # syntax highlighting
        self.syntax_highlighting_check_box = QCheckBox('syntax highlighting (alpha)')
        self.syntax_highlighting_check_box.toggled.connect(self.syntax_highlighting_toggled)
        self.syntax_highlighting_check_box.setChecked(True)
        info_and_SH_layout.addWidget(self.syntax_highlighting_check_box)

        settings_layout.addLayout(info_and_SH_layout)

        # class radio buttons widget
        self.class_selection_layout = QGridLayout()

        settings_layout.addLayout(self.class_selection_layout)
        settings_layout.setAlignment(self.class_selection_layout, Qt.AlignRight)

        # edit source code buttons
        edit_buttons_layout = QVBoxLayout()
        self.edit_code_button = QPushButton('edit')
        self.edit_code_button.setMaximumWidth(100)
        self.edit_code_button.clicked.connect(self.edit_code_button_clicked)
        self.override_code_button = QPushButton('override')
        self.override_code_button.setMaximumWidth(100)
        self.override_code_button.setEnabled(False)
        self.override_code_button.clicked.connect(self.override_code_button_clicked)
        self.reset_code_button = QPushButton('reset')
        self.reset_code_button.setMaximumWidth(206)
        self.reset_code_button.setEnabled(False)
        self.reset_code_button.clicked.connect(self.reset_code_button_clicked)
        edit_buttons_top_layout = QHBoxLayout()
        edit_buttons_top_layout.addWidget(self.edit_code_button)
        edit_buttons_top_layout.addWidget(self.override_code_button)
        edit_buttons_layout.addLayout(edit_buttons_top_layout)
        edit_buttons_layout.addWidget(self.reset_code_button)

        settings_layout.addLayout(edit_buttons_layout)


        main_layout = QVBoxLayout()
        main_layout.addLayout(settings_layout)
        main_layout.addWidget(self.text_edit)
        self.setLayout(main_layout)

        self.set_new_NI(None)

    def set_new_NI(self, ni):
        self.disable_editing()

        self.rebuild_class_selection(ni)
        self.update_edit_status()

        self.node_instance = ni

        if ni is None:  # no NI selected
            self.text_edit.set_code('')
            self.edit_code_button.setEnabled(False)
            self.override_code_button.setEnabled(False)
            self.reset_code_button.setEnabled(False)
            return
        self.edit_code_button.setEnabled(True)

        self.update_code()

    def update_code(self):

        self.disable_editing()

        if self.active_class_index == -1 or self.node_instance is None:
            return

        if self.get_current_code_obj() not in self.edited_codes:
            self.text_edit.set_code(inspect.getsource(self.get_current_code_class()))
            self.reset_code_button.setEnabled(False)
        else:
            self.text_edit.set_code(self.edited_codes[self.get_current_code_obj()])
            self.reset_code_button.setEnabled(True)

    def get_current_code_class(self):
        return self.get_current_code_obj().__class__

    def get_current_code_obj(self):
        return list(self.buttons_obj_dict.values())[self.active_class_index]

    def rebuild_class_selection(self, obj):
        # clear layout
        for i in range(self.class_selection_layout.count()):
            item = self.class_selection_layout.itemAt(0)
            widget = item.widget()
            widget.hide()
            self.class_selection_layout.removeItem(item)

        self.buttons_obj_dict = {}
        self.active_class_index = -1

        if find_type_in_object(obj, NodeInstance):
            # NI class
            node_inst_class_RB = QRadioButton('NodeInstance')
            node_inst_class_RB.toggled.connect(self.class_RB_toggled)
            self.buttons_obj_dict[node_inst_class_RB] = obj
            self.class_selection_layout.addWidget(node_inst_class_RB, 0, 0)

            # main_widget class
            if obj.main_widget is not None:
                main_widget_class_RB = QRadioButton('MainWidget')
                main_widget_class_RB.toggled.connect(self.class_RB_toggled)
                self.buttons_obj_dict[main_widget_class_RB] = obj.main_widget
                self.class_selection_layout.addWidget(main_widget_class_RB, 1, 0)

            # data input widgets
            row_count = 0
            for inp in obj.inputs:
                if inp.widget is not None:
                    inp_widget_class_RB = QRadioButton('Input '+str(obj.inputs.index(inp)))
                    inp_widget_class_RB.toggled.connect(self.class_RB_toggled)
                    self.buttons_obj_dict[inp_widget_class_RB] = inp.widget
                    self.class_selection_layout.addWidget(inp_widget_class_RB, row_count, 1)
                    row_count += 1

            node_inst_class_RB.setChecked(True)

    def update_edit_status(self):
        for o in list(self.buttons_obj_dict.keys()):
            if self.edited_codes.keys().__contains__(self.buttons_obj_dict[o]):
                o.setStyleSheet('color: #3B9CD9;')
                f = o.font()
                f.setBold(True)
                o.setFont(f)
            else:
                o.setStyleSheet('color: white;')
                f = o.font()
                f.setBold(False)
                o.setFont(f)

    def class_RB_toggled(self, checked):
        if checked:
            self.active_class_index = list(self.buttons_obj_dict.keys()).index(self.sender())
            self.update_code()

    def syntax_highlighting_toggled(self):
        if self.syntax_highlighting_check_box.isChecked():
            self.text_edit.enable_highlighting()
        else:
            self.text_edit.disable_highlighting()

    def edit_code_button_clicked(self):
        info_dialog = EditSourceCode_Dialog(self)
        accepted = info_dialog.exec_()
        if accepted:
            self.enable_editing()

    def enable_editing(self):
        self.text_edit.enable_editing()
        self.override_code_button.setEnabled(True)

    def disable_editing(self):
        self.text_edit.disable_editing()
        self.override_code_button.setEnabled(False)

    def override_code_button_clicked(self):
        new_code = self.text_edit.get_code()
        override_code(self.get_current_code_obj(), new_code)
        self.disable_editing()

        self.edited_codes[self.get_current_code_obj()] = new_code
        self.reset_code_button.setEnabled(True)
        self.update_edit_status()

    def reset_code_button_clicked(self):
        code = inspect.getsource(self.get_current_code_class())
        override_code(self.get_current_code_obj(), code)
        del self.edited_codes[self.get_current_code_obj()]
        self.update_code()
        self.update_edit_status()