Exemplo n.º 1
0
 def _restore_window(self) -> None:
     """
     Restore window state and geometry from previous session if exists.
     :return None:
     """
     settings = QSettings(company_name, app_name)
     if not settings.contains(window_geometry):
         settings.setValue(window_geometry, self.saveGeometry())
     if not settings.contains(window_state):
         settings.setValue(window_state, self.saveState())
     self.restoreGeometry(settings.value(window_geometry))
     self.restoreState(settings.value(window_state))
Exemplo n.º 2
0
    def _is_url_scheme_registered_windows(self):

        reg_path = self.WIN_REG_PATH.format(self.URL_SCHEME)
        reg = QSettings(reg_path, QSettings.NativeFormat)

        if reg.contains("Default"):
            reg.beginGroup("shell")
            reg.beginGroup("open")
            reg.beginGroup("command")
            if reg.contains("Default"):
                return True, reg.value("Default")
        return False, None
Exemplo n.º 3
0
class Ui_MainWindow(SetupUIMixin, MatrixTabMixin, ValueScoreTabMixin):
    def __init__(self):
        # Make sure that mixins do not have an init method
        self.matrix = Matrix()
        self.settings = QSettings('twenty5151', 'decision_matrix_qt')
        self.cc_tab_page = None
        self.data_tab_page = DataTab(self)
        self.data_tab_groupboxes = {}

        if not self.settings.contains('confirm_delete'):
            self.settings.setValue('confirm_delete', True)
Exemplo n.º 4
0
 def restore_window(self) -> None:
     """
     Restore window state and geometry from previous session if exists.
     :return None:
     """
     self._logger.debug("running")
     settings = QSettings(settings_info[0], settings_info[1])
     settings.beginGroup(self._settings_group_ident)
     if not settings.contains(self._win_geo_ident):
         settings.setValue(self._win_geo_ident, self.frameGeometry())
     self.setGeometry(settings.value(self._win_geo_ident))
     settings.endGroup()
     self._logger.debug("done")
Exemplo n.º 5
0
class Snippets(QDialog):

    def __init__(self, parent=None):
        super(Snippets, self).__init__(parent)
        # Create widgets
        self.setWindowModality(Qt.NonModal)
        self.title = QLabel(self.tr("Snippet Editor"))
        self.saveButton = QPushButton(self.tr("Save"))
        self.revertButton = QPushButton(self.tr("Revert"))
        self.clearHotkeyButton = QPushButton(self.tr("Clear Hotkey"))
        self.setWindowTitle(self.title.text())
        self.newFolderButton = QPushButton("New Folder")
        self.deleteSnippetButton = QPushButton("Delete")
        self.newSnippetButton = QPushButton("New Snippet")
        self.edit = QPlainTextEdit()
        self.resetting = False
        self.columns = 3

        self.keySequenceEdit = QKeySequenceEdit(self)
        self.currentHotkey = QKeySequence()
        self.currentHotkeyLabel = QLabel("")
        self.currentFileLabel = QLabel()
        self.currentFile = ""
        self.snippetDescription = QLineEdit()
        self.snippetEditsPending = False

        self.clearSelection()

        #Set Editbox Size
        font = getMonospaceFont(self)
        self.edit.setFont(font)
        font = QFontMetrics(font)
        self.edit.setTabStopWidth(4 * font.width(' ')); #TODO, replace with settings API

        #Files
        self.files = QFileSystemModel()
        self.files.setRootPath(snippetPath)
        self.files.setNameFilters(["*.py"])

        #Tree
        self.tree = QTreeView()
        self.tree.setModel(self.files)
        self.tree.setSortingEnabled(True)
        self.tree.hideColumn(2)
        self.tree.sortByColumn(0, Qt.AscendingOrder)
        self.tree.setRootIndex(self.files.index(snippetPath))
        for x in range(self.columns):
            #self.tree.resizeColumnToContents(x)
            self.tree.header().setSectionResizeMode(x, QHeaderView.ResizeToContents) 
        treeLayout = QVBoxLayout()
        treeLayout.addWidget(self.tree)
        treeButtons = QHBoxLayout()
        treeButtons.addWidget(self.newFolderButton)
        treeButtons.addWidget(self.newSnippetButton)
        treeButtons.addWidget(self.deleteSnippetButton)
        treeLayout.addLayout(treeButtons)
        treeWidget = QWidget()
        treeWidget.setLayout(treeLayout)

        # Create layout and add widgets
        buttons = QHBoxLayout()
        buttons.addWidget(self.clearHotkeyButton)
        buttons.addWidget(self.keySequenceEdit)
        buttons.addWidget(self.currentHotkeyLabel)
        buttons.addWidget(self.revertButton)
        buttons.addWidget(self.saveButton)

        description = QHBoxLayout()
        description.addWidget(QLabel(self.tr("Description: ")))
        description.addWidget(self.snippetDescription)

        vlayoutWidget = QWidget()
        vlayout = QVBoxLayout()
        vlayout.addWidget(self.currentFileLabel)
        vlayout.addWidget(self.edit)
        vlayout.addLayout(description)
        vlayout.addLayout(buttons)
        vlayoutWidget.setLayout(vlayout)

        hsplitter = QSplitter()
        hsplitter.addWidget(treeWidget)
        hsplitter.addWidget(vlayoutWidget)

        hlayout = QHBoxLayout()
        hlayout.addWidget(hsplitter)

        self.showNormal() #Fixes bug that maximized windows are "stuck"
        self.settings = QSettings("Vector 35", "Snippet Editor")
        if self.settings.contains("ui/snippeteditor/geometry"):
            self.restoreGeometry(self.settings.value("ui/snippeteditor/geometry"))
        else:
            self.edit.setMinimumWidth(80 * font.averageCharWidth())
            self.edit.setMinimumHeight(30 * font.lineSpacing())

        # Set dialog layout
        self.setLayout(hlayout)

        # Add signals
        self.saveButton.clicked.connect(self.save)
        self.revertButton.clicked.connect(self.loadSnippet)
        self.clearHotkeyButton.clicked.connect(self.clearHotkey)
        self.tree.selectionModel().selectionChanged.connect(self.selectFile)
        self.newSnippetButton.clicked.connect(self.newFileDialog)
        self.deleteSnippetButton.clicked.connect(self.deleteSnippet)
        self.newFolderButton.clicked.connect(self.newFolder)

    def registerAllSnippets(self):
        for action in list(filter(lambda x: x.startswith("Snippet\\"), UIAction.getAllRegisteredActions())):
            UIActionHandler.globalActions().unbindAction(action)
            UIAction.unregisterAction(action)

        for snippet in includeWalk(snippetPath, ".py"):
            (snippetDescription, snippetKey, snippetCode) = loadSnippetFromFile(snippet)
            if not snippetDescription:
                actionText = "Snippet\\" + snippet
            else:
                actionText = "Snippet\\" + snippetDescription
            UIAction.registerAction(actionText, snippetKey)
            UIActionHandler.globalActions().bindAction(actionText, UIAction(makeSnippetFunction(snippetCode)))

    def clearSelection(self):
        self.keySequenceEdit.clear()
        self.currentHotkey = QKeySequence()
        self.currentHotkeyLabel.setText("")
        self.currentFileLabel.setText("")
        self.snippetDescription.setText("")
        self.edit.setPlainText("")

    def reject(self):
        self.settings.setValue("ui/snippeteditor/geometry", self.saveGeometry())

        if self.snippetChanged():
            question = QMessageBox.question(self, self.tr("Discard"), self.tr("You have unsaved changes, quit anyway?"))
            if question != QMessageBox.StandardButton.Yes:
                return
        self.accept()

    def newFolder(self):
        (folderName, ok) = QInputDialog.getText(self, self.tr("Folder Name"), self.tr("Folder Name: "))
        if ok and folderName:
            index = self.tree.selectionModel().currentIndex()
            selection = self.files.filePath(index)
            if QFileInfo(selection).isDir():
                QDir(selection).mkdir(folderName)
            else:
                QDir(snippetPath).mkdir(folderName)    

    def selectFile(self, new, old):
        if (self.resetting):
            self.resetting = False
            return
        newSelection = self.files.filePath(new.indexes()[0])
        if QFileInfo(newSelection).isDir():
            self.clearSelection()
            return

        if old.length() > 0:
            oldSelection = self.files.filePath(old.indexes()[0])
            if not QFileInfo(oldSelection).isDir() and self.snippetChanged():
                question = QMessageBox.question(self, self.tr("Discard"), self.tr("Snippet changed. Discard changes?"))
                if question != QMessageBox.StandardButton.Yes:
                    self.resetting = True
                    self.tree.selectionModel().select(old, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
                    return False

        self.currentFile = newSelection
        self.loadSnippet()

    def loadSnippet(self):
        self.currentFileLabel.setText(QFileInfo(self.currentFile).baseName())
        log_debug("Loading %s as a snippet." % self.currentFile)
        (snippetDescription, snippetKey, snippetCode) = loadSnippetFromFile(self.currentFile)
        self.snippetDescription.setText(snippetDescription) if snippetDescription else self.snippetDescription.setText("")
        self.keySequenceEdit.setKeySequence(snippetKey[0]) if len(snippetKey) != 0 else self.keySequenceEdit.setKeySequence(QKeySequence(""))
        self.edit.setPlainText(snippetCode) if snippetCode else self.edit.setPlainText("")

    def newFileDialog(self):
        (snippetName, ok) = QInputDialog.getText(self, self.tr("Snippet Name"), self.tr("Snippet Name: "))
        if ok and snippetName:
            if not snippetName.endswith(".py"):
                snippetName += ".py"
            index = self.tree.selectionModel().currentIndex()
            selection = self.files.filePath(index)
            if QFileInfo(selection).isDir():
                open(os.path.join(selection, snippetName), "w").close()
            else:
                open(os.path.join(snippetPath, snippetName), "w").close()
            log_debug("Snippet %s created." % snippetName)

    def deleteSnippet(self):
        selection = self.tree.selectedIndexes()[::self.columns][0] #treeview returns each selected element in the row
        snippetName = self.files.fileName(selection)
        question = QMessageBox.question(self, self.tr("Confirm"), self.tr("Confirm deletion: ") + snippetName)
        if (question == QMessageBox.StandardButton.Yes):
            log_debug("Deleting snippet %s." % snippetName)
            self.clearSelection()
            self.files.remove(selection)

    def snippetChanged(self):
        if (self.currentFile == "" or QFileInfo(self.currentFile).isDir()):
            return False
        (snippetDescription, snippetKey, snippetCode) = loadSnippetFromFile(self.currentFile)
        if (not snippetCode):
            return False
        if len(snippetKey) == 0 and not self.keySequenceEdit.keySequence().isEmpty():
            return True
        if len(snippetKey) != 0 and snippetKey[0] != self.keySequenceEdit.keySequence():
            return True
        return self.edit.toPlainText() != snippetCode or \
               self.snippetDescription.text() != snippetDescription

    def save(self):
        log_debug("Saving snippet %s" % self.currentFile)
        outputSnippet = open(self.currentFile, "w")
        outputSnippet.write("#" + self.snippetDescription.text() + "\n")
        outputSnippet.write("#" + self.keySequenceEdit.keySequence().toString() + "\n")
        outputSnippet.write(self.edit.toPlainText())
        outputSnippet.close()
        self.registerAllSnippets()

    def clearHotkey(self):
        self.keySequenceEdit.clear()
Exemplo n.º 6
0
class CompanionController:
    """
    Main controller driving the companion app
        Controls UI and experiments
        Handles errors
        Creates device controllers
        Formats and saves device and experiment data to a file
    """
    def __init__(self) -> None:
        """
        Create elements of View and Controller
        :return None:
        """

        # initialize logging for app
        self.log_output = OutputWindow()
        self.settings = QSettings("Red Scientific", "Companion")

        self.settings.beginGroup("logging")
        if not self.settings.contains("loglevel"):
            self.settings.setValue("loglevel", "DEBUG")
        logginglevel = eval('logging.' + self.settings.value('loglevel'))
        self.settings.endGroup()

        logging.basicConfig(
            filename=self.__setup_log_output_file("companion_app_log.txt"),
            filemode='w',
            level=logginglevel,
            format='%(levelname)s - %(name)s - %(funcName)s: %(message)s')
        self.logger = logging.getLogger(__name__)

        self.formatter = logging.Formatter(
            '%(levelname)s - %(name)s - %(funcName)s: %(message)s')
        self.ch = logging.StreamHandler(self.log_output)
        self.ch.setLevel(logginglevel)
        self.ch.setFormatter(self.formatter)
        self.logger.addHandler(self.ch)

        self.logger.info("RS Companion app version: " + current_version_str)
        self.logger.debug("Initializing")

        # set up ui
        ui_min_size = QSize(950, 740)
        dock_size = QSize(850, 160)
        button_box_size = QSize(205, 120)
        info_box_size = QSize(230, 120)
        flag_box_size = QSize(80, 120)
        note_box_size = QSize(250, 120)
        tab_box_width_range = (350, 320)
        self.ui = CompanionWindow(ui_min_size, self.ch)
        self.menu_bar = MenuBar(self.ui, self.ch)
        self.control_dock = ControlDock(self.ui, dock_size, self.ch)
        self.button_box = ButtonBox(self.control_dock, button_box_size,
                                    self.ch)
        self.info_box = InfoBox(self.control_dock, info_box_size, self.ch)
        self.flag_box = FlagBox(self.control_dock, flag_box_size, self.ch)
        self.note_box = NoteBox(self.control_dock, note_box_size, self.ch)
        self.graph_box = DisplayContainer(self.ui, self.__refresh_all_graphs,
                                          self.ch)
        self.tab_box = TabContainer(self.ui, tab_box_width_range, self.ch)
        self.file_dialog = QFileDialog(self.ui)

        self.dev_con_manager = RSDeviceConnectionManager(self.ch)
        self.__setup_managers()
        self.settings.endGroup()
        # Initialize storage and state
        self.__controller_classes = dict()
        self.__controller_inits = dict()
        self.__graph_inits = dict()
        self.__populate_func_dicts()
        self.__graphs = dict()
        self.__exp_created = False
        self.__exp_running = False
        self.__num_drts = 0
        self.__num_vogs = 0
        self.__current_cond_name = ""
        self.__device_controllers = dict()
        self.__save_file_name = ""
        self.__save_dir = QDir().homePath()
        self.__device_spacers = dict()
        self.__devices_to_add = dict()

        # Assemble View objects
        self.__initialize_view()
        self.__init_controller_classes()

        self.logger.debug("Initialized")

    ########################################################################################
    # public functions
    ########################################################################################

    # TODO: Figure out how to show device added but not allow use until next experiment
    def add_device(self, device_name: str, port_name: str,
                   thread: PortWorker) -> None:
        """
        Handles an RS device being added during runtime
        If no experiment running, device added
        If experiment running, device put in a queue to add after experiment
        :param device_name: Type of RS device
        :param port_name: Specific device number
        :param thread: Device communication thread
        :return None:
        """

        self.logger.debug("running")
        if not self.__exp_created:
            self.__add_device((device_name, port_name), thread)
        else:
            self.__devices_to_add[(device_name, port_name)] = thread
        self.logger.debug("done")

    def remove_device(self, device_name: str, port_name: str) -> None:
        """
        Handles an RS device being removed during runtime
        :param device_name: Type of RS device
        :param port_name: Specific device number
        :return None:
        """

        self.logger.debug("running")
        if (device_name, port_name) in self.__devices_to_add:
            del self.__devices_to_add[(device_name, port_name)]
        self.__remove_device((device_name, port_name))
        self.logger.debug("done")

    def alert_device_connection_failure(self) -> None:
        """
        Handles a connection failure with a device
        Alerts user to connection failure
        :return None:

        """

        self.logger.debug("running")
        self.ui.show_help_window("Error", device_connection_error)
        self.logger.debug("done")

    def save_device_data(self,
                         device_name: Tuple[str, str],
                         device_line: str = '',
                         timestamp: datetime = None) -> None:
        """
        Saves experiment data from a device to a file
        :param device_name: Type of device and specific device number
        :param device_line: Data from device
        :param timestamp: Time of data received
        :return None:
        """

        self.logger.debug("running")
        if not timestamp:
            timestamp = get_current_time(device=True)
        spacer = ", "
        time = timestamp.strftime("%H:%M:%S.%f")
        date = timestamp.strftime("%Y-%m-%d")
        block_num = self.info_box.get_block_num()
        cond_name = self.button_box.get_condition_name()
        flag = self.flag_box.get_flag()
        main_block = time + spacer + date + spacer + block_num + spacer + cond_name + spacer + flag
        if device_name[0] == "Note":
            line = device_name[0] + spacer + main_block + spacer
            line = self.__get_device_note_spacers(line)
            line += device_line
        elif device_name[0] == "Keyflag":
            line = device_name[0] + spacer + main_block
            line = self.__get_device_note_spacers(line)
        else:
            line = device_name[0] + device_name[1] + spacer + main_block
            for key in self.__controller_classes:
                if device_name[0] == key:
                    line += device_line
                elif self.__controller_classes[key][1] > 0:
                    line += self.__controller_classes[key][0].get_note_spacer()
        write_line_to_file(self.__save_file_name, line)
        self.logger.debug("done")

    def __get_device_note_spacers(self, line: str):
        for val in self.__controller_classes.values():
            if val[1] > 0:
                line += val[0].get_note_spacer()
        return line

    ########################################################################################
    # initial setup
    ########################################################################################

    def __setup_managers(self) -> None:
        """
        Sets up device connection managers
        :return None:
        """

        self.logger.debug("running")
        self.dev_con_manager.signals.new_device_sig.connect(self.add_device)
        self.dev_con_manager.signals.disconnect_sig.connect(self.remove_device)
        self.dev_con_manager.signals.failed_con_sig.connect(
            self.alert_device_connection_failure)
        self.logger.debug("done")

    def __initialize_view(self) -> None:
        """
        Assembles the different View objects into a window. Initializes some handlers and controller functions
        :return None:
        """

        self.logger.debug("running")
        self.__setup_file_dialog()
        self.__setup_handlers()
        # self.__start_update_timer()
        self.control_dock.add_widget(self.button_box)
        self.control_dock.add_widget(self.flag_box)
        self.control_dock.add_widget(self.note_box)
        self.control_dock.add_widget(self.info_box)
        self.ui.add_menu_bar(self.menu_bar)
        self.ui.add_dock_widget(self.control_dock)
        self.ui.add_graph_container(self.graph_box)
        self.ui.add_tab_widget(self.tab_box)
        self.ui.show()
        self.logger.debug("done")

    # TODO: Add device controller destructors?
    def __populate_func_dicts(self) -> None:
        """
        creates dictionaries containing device controllers and device graphs functions
        :return None:
        """

        self.logger.debug("running")
        self.__controller_inits['drt'] = dict()
        self.__controller_inits['vog'] = dict()
        self.__graph_inits['drt'] = dict()
        self.__graph_inits['vog'] = dict()
        self.__controller_inits['drt'][
            'creator'] = self.__create_drt_controller
        self.__controller_inits['vog'][
            'creator'] = self.__create_vog_controller
        self.__graph_inits['drt']['creator'] = self.__make_drt_graph
        self.__graph_inits['drt']['destructor'] = self.__destroy_drt_graph
        self.__graph_inits['vog']['creator'] = self.__make_vog_graph
        self.__graph_inits['vog']['destructor'] = self.__destroy_vog_graph
        self.logger.debug("done")

    # TODO: move functionality to model, keep track of device count elsewhere
    def __init_controller_classes(self) -> None:
        """
        Creates a dictionary for static method references, and device count
        :return:
        """

        self.logger.debug("running")
        self.__controller_classes["DRT"] = [DRTController, 0]
        self.__controller_classes["VOG"] = [VOGController, 0]
        self.logger.debug("done")

    def __setup_file_dialog(self) -> None:
        """
        Sets up a file dialog for 1. saving data from experiments and 2. opening previous experiments.
        2. not implemented yet
        :return None:
        """

        self.logger.debug("running")
        # self.file_dialog.setViewMode(QFileDialog.Detail)
        self.file_dialog.setDirectory(QDir().homePath())
        # self.file_dialog.setFileMode(QFileDialog.Directory)
        self.logger.debug("done")

    def __setup_handlers(self) -> None:
        """
        Wire up buttons etc. in the view.
        :return None:
        """

        self.logger.debug("running")
        self.button_box.add_create_button_handler(self.__create_end_exp)
        self.button_box.add_start_button_handler(self.__start_stop_exp)
        self.note_box.add_note_box_changed_handler(
            self.__check_toggle_post_button)
        self.note_box.add_post_handler(self.__post_handler)
        self.menu_bar.add_open_last_save_dir_handler(self.__open_last_save_dir)
        self.menu_bar.add_about_app_handler(self.__about_app)
        self.menu_bar.add_about_company_handler(self.__about_company)
        self.menu_bar.add_update_handler(self.__check_for_updates_handler)
        self.menu_bar.add_log_window_handler(self.__log_window_handler)
        self.ui.keyPressEvent = self.__key_press_handler
        self.ui.add_close_handler(self.ui_close_event_handler)
        self.logger.debug("done")

    ########################################################################################
    # Experiment handling
    ########################################################################################

    def __create_end_exp(self) -> None:
        """
        Either begin or end an experiment. If beginning an experiment then get a dir path from the user to save
        experiment data and check output files. Path is required to continue.
        :return None:
        """

        self.logger.debug("running")
        if not self.__exp_created:
            self.logger.debug("creating experiment")
            if not self.__get_save_file_name():
                self.logger.debug(
                    "no save directory selected, done running __create_end_exp()"
                )
                return
            self.__create_exp()
            self.logger.debug("done")
        else:
            self.logger.debug("ending experiment")
            self.__end_exp()
            self.__save_file_name = ""
        self.logger.debug("done")

    def __create_exp(self) -> None:
        """
        Creates an experiment and updates the save file
        :return None:
        """

        self.logger.debug("running")
        date_time = get_current_time(device=True)
        self.__exp_created = True
        self.button_box.toggle_create_button()
        self.__add_hdr_to_output()
        devices_running = list()
        try:
            for controller in self.__device_controllers.values():
                if controller.active:
                    controller.start_exp()
                    devices_running.append(controller)
        except Exception as e:
            self.logger.exception("Failed trying to start_exp_all")
            self.button_box.toggle_create_button()
            self.__exp_created = False
            for controller in devices_running:
                controller.end_exp()
            return
        self.__check_toggle_post_button()
        self.info_box.set_start_time(
            get_current_time(time=True, date_time=date_time))
        self.logger.debug("done")

    def __end_exp(self) -> None:
        """
        Ends an experiment, and checks device backlog
        :return None:
        """

        self.logger.debug("running")
        self.__exp_created = False
        self.button_box.toggle_create_button()
        try:
            for controller in self.__device_controllers.values():
                controller.end_exp()
        except Exception as e:
            self.logger.exception("Failed trying to end_exp_all")
        self.__check_toggle_post_button()
        self.info_box.set_block_num(0)
        self.__check_device_backlog()
        self.logger.debug("done")

    def __start_stop_exp(self) -> None:
        """
        Handler method to either start or stop an experiment.
        :return None:
        """

        self.logger.debug("running")
        if self.__exp_running:
            self.logger.debug("stopping experiment")
            self.__stop_exp()
        else:
            self.logger.debug("starting experiment")
            self.__start_exp()
        self.logger.debug("done")

    def __start_exp(self) -> None:
        """
        Starts an experiment
        :return None:
        """

        self.logger.debug("running")
        self.__exp_running = True
        devices_running = list()
        try:
            for controller in self.__device_controllers.values():
                if controller.active:
                    controller.start_block()
                    devices_running.append(controller)
        except Exception as e:
            self.logger.exception("Failed trying to start_block_all")
            self.__exp_running = False
            for controller in devices_running:
                controller.end_exp()
            return
        self.info_box.set_block_num(
            str(int(self.info_box.get_block_num()) + 1))
        self.__current_cond_name = self.button_box.get_condition_name()
        self.__add_break_in_graph_lines()
        self.button_box.toggle_start_button()
        self.button_box.toggle_condition_name_box()
        self.logger.debug("done")

    def __stop_exp(self) -> None:
        """
        Stops an experiment
        :return None:
        """

        self.logger.debug("running")
        self.__exp_running = False
        try:
            for controller in self.__device_controllers.values():
                controller.end_block()
        except Exception as e:
            self.logger.exception("Failed trying to end_block_all")
        self.button_box.toggle_start_button()
        self.button_box.toggle_condition_name_box()
        self.logger.debug("done")

    # Depreciated
    def __add_vert_lines_to_graphs(self) -> None:
        """
        Adds to the Y-axis of a graph based on the current time
        :return None:
        """

        time = get_current_time(graph=True)
        for device_type in self.__graphs.values():
            device_type['frame'].get_graph().add_vert_lines(time)

    def __add_break_in_graph_lines(self) -> None:
        """
        Creates a break between times when the pause button is pressed
        :return None:
        """

        time = get_current_time(graph=True)
        for device_type in self.__graphs.values():
            device_type['frame'].get_graph().add_empty_point(time)

    def __check_toggle_post_button(self) -> None:
        """
        If an experiment is created and running and there is a note then allow user access to post button.
        :return None:
        """

        self.logger.debug("running")
        if self.__exp_created and len(
                self.note_box.get_note()) > 0:  # and self.__exp_running:
            self.logger.debug("button = true")
            self.note_box.toggle_post_button(True)
        else:
            self.logger.debug("button = false")
            self.note_box.toggle_post_button(False)
        self.logger.debug("done")

    ########################################################################################
    # Data saving
    ########################################################################################

    def __key_press_handler(self, event: QKeyEvent) -> None:
        """
        Only act on alphabetical key presses
        :return None:
        """

        self.logger.debug("running")
        if type(event) == QKeyEvent:
            if 0x41 <= event.key() <= 0x5a:
                self.flag_box.set_flag(chr(event.key()))
                if self.__exp_created:
                    self.save_device_data(('Keyflag', ''))
            event.accept()
        else:
            event.ignore()
        self.logger.debug("done")

    def __post_handler(self) -> None:
        """
        Write a given user note to all device output files.
        :return None:
        """

        self.logger.debug("running")
        note = self.note_box.get_note()
        self.note_box.clear_note()
        self.save_device_data(("Note", ''), note)
        self.logger.debug("done")

    def __get_save_file_name(self) -> bool:
        """
        Saves a save directory from a given file
        :return bool: True if the file name is longer than 1 character
        """

        self.logger.debug("running")
        self.__save_file_name = self.file_dialog.getSaveFileName(
            filter="*.txt")[0]
        self.logger.debug("1")
        valid = len(self.__save_file_name) > 1
        self.logger.debug("2")
        if valid:
            self.__save_dir = self.__get_save_dir_from_file_name(
                self.__save_file_name)
        self.logger.debug("done")
        return valid

    @staticmethod
    def __get_save_dir_from_file_name(file_name: str) -> str:
        """
        Returns the directory for where a file is saved
        :param file_name: Full file directory including file name
        :return str: Directory to the file
        """

        # possibly use for get last used directory
        end_index = file_name.rfind('/')
        dir_name = file_name[:end_index + 1]
        return dir_name

    def __check_for_updates_handler(self) -> None:
        """
        Ask VersionChecker if there is an update then alert user to result.
        :return None:
        """

        self.logger.debug("running")
        vc = VersionChecker()
        is_available = vc.check_version()
        if is_available == 1:
            self.ui.show_help_window("Update", update_available)
        elif is_available == 0:
            self.ui.show_help_window("Update", up_to_date)
        elif is_available == -1:
            self.ui.show_help_window("Error", error_checking_for_update)
        self.logger.debug("done")

    def __log_window_handler(self) -> None:
        """
        Handler for the output log
        :return None:
        """

        self.log_output.show()

    def __add_hdr_to_output(self) -> None:
        """
        Adds the header line to the output file
        :return None:
        """

        line = 'Device ID, Time, Date, Block, Condition, Key Flag, '
        for key in self.__controller_classes:
            if self.__controller_classes[key][1] > 0:
                line += self.__controller_classes[key][0].get_save_file_hdr()
        line += 'Note'
        write_line_to_file(self.__save_file_name, line, new=True)

    @staticmethod
    def __setup_log_output_file(file_name: str) -> str:
        """
        Create program output file to save log.
        :param file_name: Name of the save log
        :return str: full directory to the save log, including the save log name
        """

        fname = gettempdir() + "\\" + file_name
        with open(fname, "w") as temp:
            temp.write(program_output_hdr)
        return fname

    ########################################################################################
    # generic device handling
    ########################################################################################

    def __check_device_backlog(self) -> None:
        """
        Adds device backlog to devices
        :return None:
        """

        for device in self.__devices_to_add:
            self.__add_device(device, self.__devices_to_add[device])

    def __add_device(self, device: Tuple[str, str],
                     thread: PortWorker) -> None:
        """
        Handles when a new device is found by the device manager and added to the program's scope.
        Creates a device type specific graph if needed and connects device specific controller to device graph and
        device manager to let device controller handle device specific messages.
        Each device gets its own configure tab.
        :param device: Device to add, tuple of device type and name as strings
        :param thread: Device communication thread
        :return None:
        """

        self.logger.debug("running")
        if not check_device_tuple(device):
            self.logger.warning("expected tuple of two strings, got otherwise")
            return
        if device[0] not in self.__controller_inits.keys():
            self.logger.warning("Unknown device")
            return
        if not self.__controller_inits[device[0]]['creator'](device, thread):
            self.logger.debug("Failed to make controller")
            return
        self.__graphs[device[0]]['frame'].get_graph().add_device(device[1][3:])
        self.tab_box.add_tab(self.__device_controllers[device].get_tab_obj())
        self.__controller_classes[device[0].upper()][1] += 1
        self.logger.debug("done")

    def __remove_device(self, device: Tuple[str, str]) -> None:
        """
        Removes device tab, graph link and device information
        :param device: Device to be removed, tuple of device type and name as strings
        :return None:
        """

        self.logger.debug("running")
        if not check_device_tuple(device):
            self.logger.warning("expected tuple of two strings, got otherwise")
            return
        self.logger.debug("Removing " + device[0] + " " + device[1])
        if device in self.__device_controllers:
            self.__graphs[device[0]]['num_devices'] -= 1
        else:
            self.logger.debug("Unknown device or already disconnected: " +
                              device[0] + " " + device[1])
            return
        self.tab_box.remove_tab(
            self.__device_controllers[device].get_tab_obj().get_name())
        self.__graphs[device[0]]['frame'].get_graph().remove_device(
            device[1][3:])
        del self.__device_controllers[device]
        self.__check_num_devices()
        self.__controller_classes[device[0].upper()][1] -= 1
        self.logger.debug("done")

    # TODO: This and create drt controller functions could be merged. Perhaps different functions for the
    #  try except block.
    def __create_drt_controller(self, device: Tuple[str, str],
                                thread: PortWorker) -> bool:
        """
        Creates a controller for a DRT device
        :param device: Device to create a controller for, tuple of device type and name as strings
        :param thread: Device communication thread
        :return bool: Returns true if a DRT controller is created
        """

        self.logger.debug("running")
        self.logger.debug("Got " + device[0] + " " + device[1])
        if not device[0] in self.__graphs:
            self.logger.debug("Making graph for drt")
            if not self.__make_drt_graph():
                self.logger.warning("Failed to make_drt_graph")
                return False
        self.logger.debug("Making controller for drt")
        try:
            device_controller = DRTController(device, thread,
                                              self.add_data_to_graph, self.ch,
                                              self.save_device_data)
            device_controller.get_tab_obj().setParent(self.tab_box)
        except Exception as e:
            self.logger.exception("failed to make device_controller for drt" +
                                  str(device))
            self.__check_num_devices()
            return False
        self.logger.debug("Made controller for drt")
        self.__device_controllers[device] = device_controller
        self.__graphs[device[0]]['num_devices'] += 1
        self.logger.debug("done")
        return True

    def __create_vog_controller(self, device: Tuple[str, str],
                                thread: PortWorker) -> bool:
        """
        Creates a controller for a VOG device
        :param device: Device to create a controller for, tuple of device type and name as strings
        :param thread: Device communication thread
        :return bool: Returns true if a VOG controller is created
        """

        self.logger.debug("running")
        self.logger.debug("Got " + device[0] + " " + device[1])
        if not device[0] in self.__graphs:
            self.logger.debug("Making graph for vog")
            if not self.__make_vog_graph():
                self.logger.warning("Failed to make_vog_graph")
                return False
        self.logger.debug("Making controller for vog")
        try:
            device_controller = VOGController(device, thread,
                                              self.add_data_to_graph, self.ch,
                                              self.save_device_data)
            device_controller.get_tab_obj().setParent(self.tab_box)
        except Exception as e:
            self.logger.exception("failed to make device_controller for vog" +
                                  str(device))
            self.__check_num_devices()
            return False
        self.logger.debug("Made controller for vog")
        self.__device_controllers[device] = device_controller
        self.__graphs[device[0]]['num_devices'] += 1
        self.logger.debug("done")
        return True

    ########################################################################################
    # Other handlers
    ########################################################################################

    # TODO: Figure out why closing right after running app causes error.
    def ui_close_event_handler(self):
        """
        Shut down all devices before closing the app.
        :return:
        """

        self.logger.debug("running")
        self.dev_con_manager.cleanup()
        for controller in self.__device_controllers.values():
            controller.cleanup()
        self.log_output.close()
        self.logger.debug("done")

    def __open_last_save_dir(self):
        """
        Opens native file manager to the last directory used for an experiment.
        If no experiment has been performed, opens default home path.
        :return:
        """

        self.logger.debug("running")
        if self.__save_dir == "":
            QDesktopServices.openUrl(QUrl.fromLocalFile(QDir().homePath()))
        else:
            QDesktopServices.openUrl(QUrl.fromLocalFile(self.__save_dir))
        self.logger.debug("done")

    def __about_company(self):
        """ Display company information. """
        self.logger.debug("running")
        self.ui.show_help_window("About Red Scientific", about_RS_text)
        self.logger.debug("done")

    def __about_app(self):
        """ Display app information. """
        self.logger.debug("running")
        self.ui.show_help_window(
            "About Red Scientific Companion App",
            about_RS_app_text + "\n\n Version: " + current_version_str)
        self.logger.debug("done")

    ########################################################################################
    # graph handling
    ########################################################################################

    def __check_num_devices(self):
        self.logger.debug("running")
        to_remove = list()
        for device_type in self.__graphs:
            if self.__graphs[device_type]['num_devices'] == 0:
                to_remove.append(device_type)
        for item in to_remove:
            self.__graph_inits[item]['destructor']()
        self.logger.debug("done")

    def __make_drt_graph(self):
        """ Create a drt type graph and add it to the display area. """
        self.logger.debug("running")
        try:
            graph = DRTGraph(self.graph_box, self.ch)
        except Exception as e:
            self.logger.exception("Failed to make DRTGraph")
            return False
        graph_frame = GraphFrame(self.graph_box, graph)
        self.__graphs["drt"] = dict()
        self.__graphs["drt"]["frame"] = graph_frame
        self.__graphs["drt"]["num_devices"] = 0
        self.graph_box.add_display(graph_frame)
        self.logger.debug("done")
        return True

    def __destroy_drt_graph(self):
        """ Remove the drt graph. Typically called when all drt devices have been disconnected. """
        self.logger.debug("running")
        if "drt" in self.__graphs.keys():
            self.graph_box.remove_display(self.__graphs["drt"]['frame'])
            del self.__graphs["drt"]
        self.logger.debug("done")

    def __make_vog_graph(self):
        """ Create a vog type graph and add it to the display area. """
        self.logger.debug("running")
        try:
            graph = VOGGraph(self.graph_box, self.ch)
        except Exception as e:
            self.logger.exception("Failed to make VOGGraph")
            return False
        graph_frame = GraphFrame(self.graph_box, graph)
        graph_frame.set_graph_height(500)
        self.__graphs["vog"] = dict()
        self.__graphs["vog"]["frame"] = graph_frame
        self.__graphs["vog"]["num_devices"] = 0
        self.graph_box.add_display(graph_frame)
        self.logger.debug("done")
        return True

    def __destroy_vog_graph(self):
        """ Remove the vog graph. Typically called when all vog devices have been disconnected. """
        self.logger.debug("running")
        if "vog" in self.__graphs.keys():
            self.graph_box.remove_display(self.__graphs["vog"]['frame'])
            del self.__graphs["vog"]
        self.logger.debug("done")

    def __refresh_all_graphs(self):
        self.logger.debug("running")
        for device_type in self.__graphs:
            self.__graphs[device_type]['frame'].get_graph().refresh_self()
        self.logger.debug("done")

    def add_data_to_graph(self, device, data):
        """ Pass data to device type graph along with specific device id. """
        self.logger.debug("running")
        if not check_device_tuple(device):
            return
        if device[0] not in self.__graphs:
            self.logger.warning("device not found in self.__graphs")
            return
        self.__graphs[device[0]]['frame'].get_graph().add_data(
            device[1][3:], data)
        self.logger.debug("done")
Exemplo n.º 7
0
class Widget(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        self.row = 0
        self.coins_sum = 0
        self.settings = QSettings('NoneCompany', 'freebot')

        VBoxLayout = QVBoxLayout()  # Right
        HBoxLayout = QHBoxLayout()  # Left

        self.create_form()
        self.set_placeholders()
        self.define_table()

        FormLayout = QHBoxLayout()
        FormLayout.addWidget(self.form_login)
        FormLayout.addWidget(self.form_password)
        FormLayout.addWidget(self.form_proxy)

        self.create_buttons()
        FromButtonsLayout = QHBoxLayout()
        FromButtonsLayout.addWidget(self.add_button)
        FromButtonsLayout.addWidget(self.clear_button)

        self.series = QtCharts.QLineSeries()
        self.chart_view = QtCharts.QChartView()
        self.chart_view.setRenderHint(QPainter.Antialiasing)

        VBoxLayout.addWidget(self.form_key)
        VBoxLayout.addLayout(FormLayout)
        VBoxLayout.addLayout(FromButtonsLayout)
        VBoxLayout.addWidget(self.chart_view)
        VBoxLayout.addWidget(self.start_button)

        HBoxLayout.addWidget(self.table)
        HBoxLayout.addLayout(VBoxLayout)
        self.setLayout(HBoxLayout)

        self.update_chart([time(), 0], True)
        self.create_connections()

    def set_placeholders(self):
        self.form_login.setPlaceholderText('login')
        self.form_password.setPlaceholderText('password')
        self.form_proxy.setPlaceholderText('proxy')
        self.form_key.setPlaceholderText('API-key rucaptcha.com')

    def create_form(self):
        self.form_login = QLineEdit()
        self.form_password = QLineEdit()
        self.form_proxy = QLineEdit()
        self.form_key = QLineEdit()

    def create_buttons(self):
        self.add_button = QPushButton('add')
        self.clear_button = QPushButton('clear')
        self.start_button = QPushButton('start')

    def create_connections(self):
        self.add_button.clicked.connect(self.fillin_table)
        self.start_button.clicked.connect(self.init_threads)
        self.clear_button.clicked.connect(self.clear_table)

    def define_table(self):
        self.table = QTableWidget()
        self.table.setColumnCount(len(CONSTANTS.TABLE_COLUMNS))
        self.table.setHorizontalHeaderLabels(CONSTANTS.TABLE_COLUMNS)
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

        if self.settings.contains('key'):
            self.form_key.setText(self.get_settings('key'))

        if self.settings.contains('table'):
            for row in literal_eval(self.get_settings('table')):
                self.add_account(*row, False)

    def set_settings(self, key, value):
        if key == 'table':
            if self.settings.contains('table'):
                table = literal_eval(self.get_settings('table'))
                table.append(value)
                value = table
            else:
                value = [value]

        self.settings.setValue(key, str(value))

    def get_settings(self, key):
        return self.settings.value(key)

    @Slot(int, list)
    def change_column_color(self, row, RGB):
        for col in range(len(CONSTANTS.TABLE_COLUMNS)):
            self.table.item(row, col).setBackground(QColor(*RGB))

    @Slot(int, str)
    def change_column_text(self, row, text):
        self.table.item(row, 3).setText(text)

    @Slot()
    def clear_table(self):
        self.settings.clear()
        self.table.setRowCount(0)
        self.row = 0

    @Slot()
    def fillin_table(self, settings=False):
        if self.check_form_state():
            return

        self.add_account(self.form_login.text(), self.form_password.text(),
                         self.form_proxy.text())

        self.form_login.clear()
        self.form_proxy.clear()
        self.form_password.clear()

    @Slot(list)
    def update_chart(self, data, init=False):
        self.coins_sum += data[1]

        MStime_now = float(QDateTime.currentDateTime().toMSecsSinceEpoch())

        if init:
            self.series.append(MStime_now, self.coins_sum)
            for i in range(6):
                if i and not i % 2:
                    self.series.append(MStime_now + (i * 1), self.coins_sum)
                else:
                    self.series.append(MStime_now + (i * 1), 0.00000001)
        else:
            self.series.append(MStime_now, self.coins_sum)

        chart = QtCharts.QChart()
        chart.addSeries(self.series)
        if not init:
            chart.setAnimationOptions(QtCharts.QChart.SeriesAnimations)
        chart.setTitle('Progress')

        axisX = QtCharts.QDateTimeAxis()
        axisX.setTickCount(5)
        axisX.setFormat('HH:mm:ss')
        axisX.setTitleText('time')
        chart.addAxis(axisX, Qt.AlignBottom)
        self.series.attachAxis(axisX)

        axisY = QtCharts.QValueAxis()
        axisY.setTickCount(5)
        axisY.setLabelFormat('%.8f')
        axisY.setTitleText('coins')
        chart.addAxis(axisY, Qt.AlignLeft)
        self.series.attachAxis(axisY)

        self.chart_view.setChart(chart)

    @Slot()
    def init_threads(self):
        self.start_button.setEnabled(False)
        self.set_settings('key', self.form_key.text())

        self.threads = [
            Bridge(self, row,
                   self.table.item(row, 0).text(),
                   self.table.item(row, 1).text(),
                   self.table.item(row, 2).text(), self.form_key.text())
            for row in range(self.table.rowCount())
        ]

    def check_form_state(self):
        return not self.form_login.text() \
        or not self.form_password.text() \
        or not self.form_proxy.text()

    def add_account(self, login, password, proxy, record=True):
        self.table.insertRow(self.row)

        self.table.setItem(self.row, 0, QTableWidgetItem(login))
        self.table.setItem(self.row, 1, QTableWidgetItem(password))
        self.table.setItem(self.row, 2, QTableWidgetItem(proxy))
        self.table.setItem(self.row, 3, QTableWidgetItem('?'))

        if record:
            self.set_settings('key', self.form_key.text())
            self.set_settings('table', [login, password, proxy])

        self.row += 1
Exemplo n.º 8
0
class AppController:
    """ The main controller for this app. """
    def __init__(self):
        # App settings and logging.
        self._settings = QSettings(company_name, app_name)

        if not self._settings.contains("language"):
            self._settings.setValue("language", LangEnum.ENG)
        self._lang = self._settings.value("language")
        self._strings = strings[self._lang]

        self._settings.beginGroup("logging")
        if not self._settings.contains("level"):
            self._settings.setValue("level", DEBUG)
        log_level = self._settings.value('level')
        self._settings.endGroup()

        log_file = setup_log_file(self._strings[StringsEnum.LOG_OUT_NAME],
                                  self._strings[StringsEnum.PROG_OUT_HDR])
        logging.basicConfig(filename=log_file,
                            filemode='w',
                            level=log_level,
                            format=log_format)
        self._logger = logging.getLogger(__name__)
        self.log_output = OutputWindow(self._lang)
        self.formatter = logging.Formatter(log_format)
        self.app_lh = logging.StreamHandler(self.log_output)
        self.app_lh.setLevel(log_level)
        self.app_lh.setFormatter(self.formatter)
        self.stderr_lh = logging.StreamHandler()
        self.stderr_lh.setLevel(logging.WARNING)
        self.stderr_lh.setFormatter(self.formatter)
        self._logger.addHandler(self.app_lh)
        self._logger.addHandler(self.stderr_lh)
        self._logger.info(self._strings[StringsEnum.LOG_VER_ID] +
                          str(current_version))

        self._logger.debug("Initializing")

        # View
        ui_min_size = QSize(950, 740)
        button_box_size = QSize(205, 120)
        info_box_size = QSize(230, 120)
        flag_box_size = QSize(80, 120)
        note_box_size = QSize(250, 120)
        drive_info_box_size = QSize(200, 120)
        mdi_area_min_size = QSize(500, 300)
        self.main_window = AppMainWindow(ui_min_size,
                                         [self.app_lh, self.stderr_lh],
                                         self._lang)
        self.menu_bar = AppMenuBar(self.main_window,
                                   [self.app_lh, self.stderr_lh], self._lang)
        self.button_box = ButtonBox(self.main_window, button_box_size,
                                    [self.app_lh, self.stderr_lh], self._lang)
        self.info_box = InfoBox(self.main_window, info_box_size,
                                [self.app_lh, self.stderr_lh], self._lang)
        self.d_info_box = DriveInfoBox(self.main_window, drive_info_box_size,
                                       [self.app_lh, self.stderr_lh],
                                       self._lang)
        self.flag_box = FlagBox(self.main_window, flag_box_size,
                                [self.app_lh, self.stderr_lh], self._lang)
        self.note_box = NoteBox(self.main_window, note_box_size,
                                [self.app_lh, self.stderr_lh], self._lang)
        self.mdi_area = MDIArea(self.main_window, mdi_area_min_size,
                                [self.app_lh, self.stderr_lh])
        self._file_dialog = QFileDialog(self.main_window)

        # Model
        self._model = AppModel([self.app_lh, self.stderr_lh], self._lang)

        # from PySide2.QtWidgets import QMdiSubWindow
        # for i in range(6):
        #     window = QMdiSubWindow()
        #     window.setFixedSize(300, 200)
        #     self.mdi_area.add_window(window)

        self._save_file_name = str()
        self._save_dir = str()
        self._tasks = []
        self._setup_handlers()
        self._initialize_view()
        self._start()
        self._logger.debug("Initialized")
        self._drive_updater_task = None
        self._curr_cond_name = ""

    def language_change_handler(self, lang: LangEnum) -> None:
        """
        Sets the app language to the user selection.
        :return None:
        """
        self._logger.debug("running")
        self._settings.setValue("language", lang)
        self._strings = strings[lang]
        self.main_window.set_lang(lang)
        self.menu_bar.set_lang(lang)
        self.button_box.set_lang(lang)
        self.info_box.set_lang(lang)
        self.d_info_box.set_lang(lang)
        self.flag_box.set_lang(lang)
        self.note_box.set_lang(lang)
        self._model.change_lang(lang)
        self._logger.debug("done")

    def debug_change_handler(self, debug_level: str) -> None:
        """
        Sets the app debug level.
        :param debug_level: The debug level
        :return None:
        """
        self._settings.setValue("logging/level", debug_level)
        self.main_window.show_help_window(
            self._strings[StringsEnum.APP_NAME],
            self._strings[StringsEnum.RESTART_PROG])

    def create_end_exp_handler(self) -> None:
        """
        Handler for create/end button.
        :return None:
        """
        self._logger.debug("running")
        if not self._model.exp_created:
            self._logger.debug("creating experiment")
            if not self._get_save_file_name():
                self._logger.debug(
                    "no save directory selected, done running _create_end_exp_handler()"
                )
                return
            self._create_exp()
            self.main_window.set_close_check(True)
            self._logger.debug("done")
        else:
            self._logger.debug("ending experiment")
            self._end_exp()
            self.main_window.set_close_check(False)
            self._save_file_name = ""
        self._logger.debug("done")

    def start_stop_exp_handler(self) -> None:
        """
        Handler play/pause button.
        :return None:
        """
        self._logger.debug("running")
        if self._model.exp_running:
            self._logger.debug("stopping experiment")
            self._stop_exp()
        else:
            self._logger.debug("starting experiment")
            self._start_exp()
        self._logger.debug("done")

    async def new_device_view_handler(self) -> None:
        """
        Check for and handle any new device view objects from model.
        :return None:
        """
        self._logger.debug("running")
        dev_type: str
        dev_port: AioSerial
        while True:
            await self._model.await_new_view()
            ret, view = self._model.get_next_new_view()
            while ret:
                self.mdi_area.add_window(view)
                ret, view = self._model.get_next_new_view()

    async def remove_device_view_handler(self) -> None:
        """
        Check for and handle any device views to remove from model.
        :return None:
        """
        while True:
            await self._model.await_remove_view()
            ret, view = self._model.get_next_view_to_remove()
            while ret:
                self.mdi_area.remove_window(view)
                ret, view = self._model.get_next_view_to_remove()

    async def device_conn_error_handler(self) -> None:
        """
        Alert user to device connection error.
        :return None:
        """
        while True:
            await self._model.await_dev_con_err()
            self.main_window.show_help_window(
                "Error", self._strings[StringsEnum.DEV_CON_ERR])

    def post_handler(self) -> None:
        """
        Handler for post button.
        :return:
        """
        self._logger.debug("running")
        note = self.note_box.get_note()
        self.note_box.clear_note()
        self._model.save_note(note)
        self._logger.debug("done")

    def about_rs_handler(self) -> None:
        """
        Handler for about company button.
        :return None:
        """
        self._logger.debug("running")
        self.main_window.show_help_window(
            self._strings[StringsEnum.APP_NAME],
            self._strings[StringsEnum.ABOUT_COMPANY])
        self._logger.debug("done")

    def about_app_handler(self) -> None:
        """
        Handler for about app button.
        :return None:
        """
        self._logger.debug("running")
        self.main_window.show_help_window(self._strings[StringsEnum.APP_NAME],
                                          self._strings[StringsEnum.ABOUT_APP])
        self._logger.debug("done")

    def check_for_updates_handler(self) -> None:
        """
        Handler for update button.
        :return None:
        """
        self._logger.debug("running")
        ret = self._model.check_version()
        if ret == 1:
            self.main_window.show_help_window(
                self._strings[StringsEnum.UPDATE_HDR],
                self._strings[StringsEnum.UPDATE_AVAILABLE])
        elif ret == 0:
            self.main_window.show_help_window(
                self._strings[StringsEnum.UPDATE_HDR],
                self._strings[StringsEnum.NO_UPDATE])
        elif ret == -1:
            self.main_window.show_help_window(
                self._strings[StringsEnum.UPDATE_HDR_ERR],
                self._strings[StringsEnum.ERR_UPDATE_CHECK])
        self._logger.debug("done")

    def log_window_handler(self) -> None:
        """
        Handler for output log button.
        :return None:
        """
        self._logger.debug("running")
        self.log_output.show()
        self._logger.debug("done")

    def last_save_dir_handler(self) -> None:
        """
        Handler for last save dir button.
        :return None:
        """
        self._logger.debug("running")
        if self._save_dir == "":
            QDesktopServices.openUrl(QUrl.fromLocalFile(QDir().homePath()))
        else:
            QDesktopServices.openUrl(QUrl.fromLocalFile(self._save_dir))
        self._logger.debug("done")

    # TODO: Implement
    def toggle_cam_handler(self) -> None:
        """
        Handler for use cam button.
        :return None:
        """
        self._logger.debug("running")
        print("Implement handling for this button")
        self._logger.debug("done")

    async def _update_drive_info_box(self):
        while True:
            info = get_disk_usage_stats(self._drive_path)
            self.d_info_box.set_name_val(str(info[0]))
            self.d_info_box.set_perc_val(str(info[4]))
            self.d_info_box.set_gb_val(str(info[2]))
            self.d_info_box.set_mb_val(str(info[3]))
            await sleep(3)

    def _create_exp(self) -> None:
        """
        Create an experiment. Signal devices and update view.
        :return None:
        """
        self._logger.debug("running")
        self._model.signal_create_exp(self._save_file_name)
        if self._model.exp_created:
            self.button_box.set_start_button_enabled(True)
            self.button_box.set_create_button_state(1)
            self._check_toggle_post_button()
            if self._set_drive_updater():
                self._drive_updater_task = create_task(
                    self._update_drive_info_box())
            self.info_box.set_start_time(
                format_current_time(datetime.now(), time=True))
        else:
            self.button_box.set_create_button_state(0)
        self._logger.debug("done")

    def _end_exp(self, save: bool = True) -> None:
        """
        End an experiment. Stop experiment if running then signal devices and update view.
        :param save: Save exp data.
        :return None:
        """
        self._logger.debug("running")
        if self._model.exp_running:
            self._stop_exp()
        self._model.signal_end_exp(save)
        self._check_toggle_post_button()
        if self._drive_updater_task:
            self._drive_updater_task.cancel()
            self._drive_updater_task = None
        self.info_box.set_block_num(0)
        self.button_box.set_create_button_state(0)
        self.button_box.set_start_button_enabled(False)
        self.button_box.set_start_button_state(0)
        self._logger.debug("done")

    def _start_exp(self) -> None:
        """
        Start an experiment if one has been created. Signal devices and update view.
        :return None:
        """
        self._logger.debug("running")
        self._model.signal_start_exp()
        if not self._model.exp_running:
            return
        self.info_box.set_block_num(
            str(int(self.info_box.get_block_num()) + 1))
        self.button_box.set_start_button_state(1)
        self.button_box.set_condition_name_box_enabled(False)
        self._curr_cond_name = self.button_box.get_condition_name()
        self._logger.debug("done")

    def _stop_exp(self) -> None:
        """
        Stop an experiment. Signal devices and update view.
        :return None:
        """
        self._logger.debug("running")
        self._model.signal_stop_exp()
        self.button_box.set_start_button_state(2)
        self.button_box.set_condition_name_box_enabled(True)
        self._logger.debug("done")

    def _set_drive_updater(self, filename: str = None) -> bool:
        """
        Update drive info display with drive information.
        :param filename: The drive to look at.
        :return None:
        """
        ret = True
        if not filename and not self._save_dir == "":
            self._drive_path = self._save_dir
        elif len(filename) > 0:
            self._drive_path = filename
        else:
            ret = False
        return ret

    def _check_toggle_post_button(self) -> None:
        """
        If an experiment is created and running and there is a note then allow user access to post button.
        :return None:
        """
        self._logger.debug("running")
        if self._model.exp_created and len(self.note_box.get_note()) > 0:
            self._logger.debug("button = true")
            self.note_box.set_post_button_enabled(True)
        else:
            self._logger.debug("button = false")
            self.note_box.set_post_button_enabled(False)
        self._logger.debug("done")

    def _get_save_file_name(self) -> bool:
        """
        Gets a new filename from the user for the current experiment to be saved as.
        :return bool: True if the file name is longer than 1 character
        """
        self._logger.debug("running")
        self._save_file_name = self._file_dialog.getSaveFileName(
            filter="*.rs")[0]
        valid = len(self._save_file_name) > 1
        if valid:
            self._save_dir = self._dir_name_from_file_name(
                self._save_file_name)
        self._logger.debug("done")
        return valid

    def _dir_name_from_file_name(self, filename: str) -> str:
        """
        Get directory name from filename.
        :param filename: The absolute path filename.
        :return str: The resulting directory name.
        """
        self._logger.debug("running")
        dir_name = filename[:filename.rindex("/")]
        self._logger.debug("done")
        return dir_name

    def _keypress_handler(self, event: QKeyEvent) -> None:
        """
        Handle any keypress event and intercept alphabetical keypresses, then set flag_box to that key.
        :param event: The keypress event.
        :return None:
        """
        self._logger.debug("running")
        if 0x41 <= event.key() <= 0x5a:
            self.flag_box.set_flag(chr(event.key()))
            self._model.save_flag(self.flag_box.get_flag())
        event.accept()
        self._logger.debug("done")

    def _setup_handlers(self) -> None:
        """
        Attach events to handlers
        :return None:
        """
        self._logger.debug("running")
        # Experiment controls
        self.button_box.add_create_button_handler(self.create_end_exp_handler)
        self.button_box.add_start_button_handler(self.start_stop_exp_handler)
        self.note_box.add_note_box_changed_handler(
            self._check_toggle_post_button)
        self.note_box.add_post_handler(self.post_handler)
        self.main_window.keyPressEvent = self._keypress_handler

        # File menu
        self.menu_bar.add_open_last_save_dir_handler(
            self.last_save_dir_handler)
        # self.menu_bar.add_cam_bool_handler(self.toggle_cam_handler)

        # Settings menu
        self.menu_bar.add_lang_select_handler(self.language_change_handler)
        self.menu_bar.add_debug_select_handler(self.debug_change_handler)

        # Help menu
        self.menu_bar.add_about_company_handler(self.about_rs_handler)
        self.menu_bar.add_about_app_handler(self.about_app_handler)
        self.menu_bar.add_update_handler(self.check_for_updates_handler)
        self.menu_bar.add_log_window_handler(self.log_window_handler)

        # Close app button
        self.main_window.add_close_handler(self._cleanup)

        self._logger.debug("done")

    def _initialize_view(self) -> None:
        """
        Put the different components of the view together and then show the view.
        :return None:
        """
        self._logger.debug("running")
        self.menu_bar.set_debug_action(self._settings.value("logging/level"))
        self.main_window.add_menu_bar(self.menu_bar)
        self.main_window.add_control_bar_widget(self.button_box)
        self.main_window.add_control_bar_widget(self.flag_box)
        self.main_window.add_control_bar_widget(self.note_box)
        self.main_window.add_spacer_item(1)
        self.main_window.add_control_bar_widget(self.info_box)
        self.main_window.add_control_bar_widget(self.d_info_box)
        self.main_window.add_mdi_area(self.mdi_area)
        self.main_window.show()

    def _start(self) -> None:
        """
        Start all recurring functions.
        :return None:
        """
        self._tasks.append(create_task(self.new_device_view_handler()))
        self._tasks.append(create_task(self.device_conn_error_handler()))
        self._tasks.append(create_task(self.remove_device_view_handler()))
        self._model.start()

    def _cleanup(self) -> None:
        """
        Cleanup any code that would cause problems for shutdown and prep for app closure.
        :return None:
        """
        if self._model.exp_created:
            self._end_exp(False)
        self._model.cleanup()
        create_task(end_tasks(self._tasks))
        self.log_output.close()
Exemplo n.º 9
0
class DDMWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.ConfigSettings()
        self.init_themes_main()

        self.MainMenuItems()

        self.MenuCreate()

        self.WindowSettings()
        self.show()

    def download_Cache(self):
        self.list_widget.clear()
        for i in getData():
            items = QListWidgetItem(i[0], self.list_widget)

    def download_CacheDelete(self):
        try:
            name = self.list_widget.currentItem().text()
            #print(f"Removing {name}")
            for i in getData():
                if name in i:
                    fname = i
                    break
            delData(name, fname[1], fname[2])
            myfile = Path(f"{fname[1]}/{fname[2]}")
            print(myfile)
            if myfile.exists():
                if self.sel_lang == "tr":
                    dosya_silme = QMessageBox.warning(
                        self, "Dosyayı Sil",
                        "Dosyayı diskten silmeli miyiz?\n\nDiskten silmek için evete basın!",
                        QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
                elif self.sel_lang == "en":
                    dosya_silme = QMessageBox.warning(
                        self, "Delete File",
                        "Should we delete file from disk?\n\nTo delete file from disk press yes!",
                        QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
                if dosya_silme == QMessageBox.Yes:
                    try:
                        myfile.unlink()
                        if self.sel_lang == "tr":
                            dosya_silme = QMessageBox.information(
                                self, "Dosya Silindi",
                                "Dosya Başarıyla Silindi!", QMessageBox.Ok,
                                QMessageBox.Ok)
                        elif self.sel_lang == "en":
                            dosya_silme = QMessageBox.information(
                                self, "File Deleted",
                                "File Succesfuly Deleted!", QMessageBox.Ok,
                                QMessageBox.Ok)
                    except Exception as e:
                        print(e)
                elif dosya_silme == QMessageBox.No:
                    pass
            self.download_Cache()
        except Exception as e:
            print(e)

    def slideLeftMenu(self):
        width = self.left_side_menu.width()

        if width == 50:
            newWidth = 150
        else:
            newWidth = 50

        self.animation = QPropertyAnimation(self.left_side_menu,
                                            b"minimumWidth")
        self.animation.setDuration(250)
        self.animation.setStartValue(width)
        self.animation.setEndValue(newWidth)
        self.animation.setEasingCurve(QtCore.QEasingCurve.InOutQuart)
        self.animation.start()

    def Add_Download(self):
        self.add_download_dialog = QDialog()

        self.add_download_dialog.setWindowTitle("Add_Download")
        # self.setWindowIcon(QIcon("logo.png"))
        self.init_themes_add_dialog()
        self.add_download_dialog.setFixedSize(325, 275)
        self.add_download_dialog.setMinimumSize(325, 240)

        self.isim = QLabel("İndir", self.add_download_dialog)
        self.isim.setFont(QFont("Hack Nerd Font", 15))
        self.isim.setGeometry(124, 13, 125, 34)

        # İlerleme/Progress
        # self.progressBar =

        # URL KUTUSU
        self.urlbox = QLineEdit("", self.add_download_dialog)
        # self.urlbox.setFixedSize(100,4)
        self.urlbox.setGeometry(35, 60, 250, 34)
        self.urlbox.setPlaceholderText("URL Gir")
        self.urlbox.setFont(QFont("Hack Nerd Font", 11))

        # INDIRME KONUMU
        self.downdirectory = QLineEdit(str(Path.home()),
                                       self.add_download_dialog)
        self.downdirectory.setGeometry(35, 100, 210, 34)
        self.downdirectory.setPlaceholderText("İndirilecek Konum")
        self.downdirectory.setFont(QFont("Hack Nerd Font", 11))

        # Dosya İsmi
        self.enterfilename = QLineEdit("", self.add_download_dialog)
        # self.filename.setFixedSize(100,4)
        self.enterfilename.setGeometry(35, 140, 210, 34)
        self.enterfilename.setPlaceholderText("Dosya İsmi(Opsiyonel)")
        self.enterfilename.setFont(QFont("Hack Nerd Font", 11))

        def connectfilename():
            fnameloop = asyncio.get_event_loop()
            fnameloop.run_until_complete(self.detect_fname())

        self.connectfilename = QPushButton(".", self.add_download_dialog)
        self.connectfilename.setGeometry(249, 140, 36, 34)
        self.connectfilename.setFont(QFont("Hack Nerd Font", 11))
        self.connectfilename.clicked.connect(connectfilename)

        # KONUM SEÇ BUTONU
        def download_dir():
            if self.sel_lang == "tr":
                try:
                    self.dirselectdialog = QFileDialog.getExistingDirectory(
                        self.add_download_dialog, 'İndirilecek Konumu Seç')
                except Exception as e:
                    print(e)
            elif self.sel_lang == "en":
                try:
                    self.dirselectdialog = QFileDialog.getExistingDirectory(
                        self.add_download_dialog, 'Select Dir')
                except Exception as e:
                    print(e)
            if self.dirselectdialog == "":
                self.downdirectory.setText(str(Path.home()))
            else:
                self.downdirectory.setText(str(self.dirselectdialog))

        self.selectdirbutton = QPushButton("...", self.add_download_dialog)
        self.selectdirbutton.setGeometry(249, 100, 36, 34)
        self.selectdirbutton.setFont(QFont("Hack Nerd Font", 11))
        self.selectdirbutton.clicked.connect(download_dir)

        # ProgressBar/İlerleme
        self.progressbar = QProgressBar(self.add_download_dialog)
        self.progressbar.setGeometry(35, 180, 250, 34)
        self.progressbar.setValue(0)

        # self.progressbar.hide()

        # INDIR BUTONU

        def start_downloading_process():
            url = str(self.urlbox.text())
            if url == "":
                if self.sel_lang == "tr":
                    self.urlboxempty = QMessageBox.warning(
                        self.add_download_dialog, "URL Kutusu Boş",
                        "Lütfen bir url giriniz!", QMessageBox.Ok,
                        QMessageBox.Ok)
                elif self.sel_lang == "en":
                    self.urlboxempty = QMessageBox.warning(
                        self.add_download_dialog, "URL Box Empty",
                        "Please enter a URL!", QMessageBox.Ok, QMessageBox.Ok)
            else:
                # self.add_download_dialog.close()
                # self.downloading_file()
                # self.add_download_dialog.close()
                self.download()

        self.downloadbutton = QPushButton("İndir", self.add_download_dialog)
        self.downloadbutton.setGeometry(85, 220, 70, 40)
        self.downloadbutton.setFont(QFont("Hack Nerd Font", 11))
        self.downloadbutton.clicked.connect(start_downloading_process)
        # self.downloadbutton.setStyleSheet("background-color: #268bd2;")

        # ÇIKIŞ BUTONU
        self.iptalbutton = QPushButton("İptal", self.add_download_dialog)
        self.iptalbutton.setGeometry(160, 220, 70, 40)
        self.iptalbutton.setFont(QFont("Hack Nerd Font", 11))
        self.iptalbutton.clicked.connect(self.add_download_dialog.close)
        # self.iptalbutton.setStyleSheet("background-color: #ed0b0b;")
        self.reTranslateAddDownload()
        self.add_download_dialog.show()

    # def downloading_file(self):
    #    self.downloading_dialog = QDialog()
    #    self.init_themes_add_dialog()
    #    self.downloading_dialog.setFixedSize(240, 240)
    #    #self.downloading_dialog.setMinimumSize(325, 240)

    # self.downloading_dialog.exec()
    def reTranslateAddDownload(self):
        if self.sel_lang == "en":
            self.isim.setText("Download")
            self.isim.setGeometry(110, 13, 125, 34)

            self.downloadbutton.setText("Download")
            self.downloadbutton.setGeometry(65, 220, 90, 40)

            self.iptalbutton.setText("Cancel")

            self.enterfilename.setPlaceholderText("File Name(Optional)")
            self.downdirectory.setPlaceholderText("Enter Directory")
            self.urlbox.setPlaceholderText("Enter the URL")

    def eventFilter(self, source, event):
        try:
            if (event.type() == QtCore.QEvent.ContextMenu
                    and source is self.list_widget):
                try:
                    self.menu = QMenu()
                    self.menu.addAction('In Future')
                    if self.menu.exec_(event.globalPos()):
                        item = source.itemAt(event.pos())
                        print(item.text())
                    return True
                except Exception as e:
                    print(e)
            return super().eventFilter(source, event)
        except Exception as e:
            print(e)

    from mainUI import MainMenuItems
    from mainUI import reTranslateMain

    from download_script import detect_fname
    from download_script import detect_fsize
    from download_script import check_connection
    from download_script import download

    # MenuBar things
    from MenuBar import exitAction
    from MenuBar import MenuCreate

    from MenuBar import about
    from MenuBar import reTranslateAbout

    from MenuBar import SettingsMenu
    from MenuBar import SetSettings
    from MenuBar import restartforsettings
    from MenuBar import reTranslateSettings

    # Theming Things
    from theming import init_themes_main
    from theming import init_themes_add_dialog
    from theming import init_themes_settings_dialog
    from theming import init_themes_about_dialog

    def WindowSettings(self):
        self.setWindowTitle("Dream Download Manager")
        # self.setWindowIcon(QIcon("logo.png"))

        #self.setFixedSize(485, 375)
        #self.setMinimumSize(325, 240)

    def ConfigSettings(self):
        self.settings = QSettings("DDM", "DreamDownloadManager")
        print(self.settings.fileName())
        if self.settings.contains('theme_selection'):
            self.selected_theme = self.settings.value('theme_selection')
        else:
            self.settings.setValue('theme_selection', 'Dark')

        if self.settings.contains('selected_lang'):
            self.sel_lang = self.settings.value('selected_lang')
        else:
            self.settings.setValue('selected_lang', 'en')
Exemplo n.º 10
0
class Snippets(QDialog):

    def __init__(self, context, parent=None):
        super(Snippets, self).__init__(parent)
        # Create widgets
        self.setWindowModality(Qt.ApplicationModal)
        self.title = QLabel(self.tr("Snippet Editor"))
        self.saveButton = QPushButton(self.tr("&Save"))
        self.saveButton.setShortcut(QKeySequence(self.tr("Ctrl+S")))
        self.runButton = QPushButton(self.tr("&Run"))
        self.runButton.setShortcut(QKeySequence(self.tr("Ctrl+R")))
        self.closeButton = QPushButton(self.tr("Close"))
        self.clearHotkeyButton = QPushButton(self.tr("Clear Hotkey"))
        self.setWindowTitle(self.title.text())
        #self.newFolderButton = QPushButton("New Folder")
        self.browseButton = QPushButton("Browse Snippets")
        self.browseButton.setIcon(QIcon.fromTheme("edit-undo"))
        self.deleteSnippetButton = QPushButton("Delete")
        self.newSnippetButton = QPushButton("New Snippet")
        self.edit = QCodeEditor(HIGHLIGHT_CURRENT_LINE=False, SyntaxHighlighter=PythonHighlighter)
        self.edit.setPlaceholderText("python code")
        self.resetting = False
        self.columns = 3
        self.context = context

        self.keySequenceEdit = QKeySequenceEdit(self)
        self.currentHotkey = QKeySequence()
        self.currentHotkeyLabel = QLabel("")
        self.currentFileLabel = QLabel()
        self.currentFile = ""
        self.snippetDescription = QLineEdit()
        self.snippetDescription.setPlaceholderText("optional description")

        #Set Editbox Size
        font = getMonospaceFont(self)
        self.edit.setFont(font)
        font = QFontMetrics(font)
        self.edit.setTabStopWidth(4 * font.width(' ')); #TODO, replace with settings API

        #Files
        self.files = QFileSystemModel()
        self.files.setRootPath(snippetPath)
        self.files.setNameFilters(["*.py"])

        #Tree
        self.tree = QTreeView()
        self.tree.setModel(self.files)
        self.tree.setSortingEnabled(True)
        self.tree.hideColumn(2)
        self.tree.sortByColumn(0, Qt.AscendingOrder)
        self.tree.setRootIndex(self.files.index(snippetPath))
        for x in range(self.columns):
            #self.tree.resizeColumnToContents(x)
            self.tree.header().setSectionResizeMode(x, QHeaderView.ResizeToContents)
        treeLayout = QVBoxLayout()
        treeLayout.addWidget(self.tree)
        treeButtons = QHBoxLayout()
        #treeButtons.addWidget(self.newFolderButton)
        treeButtons.addWidget(self.browseButton)
        treeButtons.addWidget(self.newSnippetButton)
        treeButtons.addWidget(self.deleteSnippetButton)
        treeLayout.addLayout(treeButtons)
        treeWidget = QWidget()
        treeWidget.setLayout(treeLayout)

        # Create layout and add widgets
        buttons = QHBoxLayout()
        buttons.addWidget(self.clearHotkeyButton)
        buttons.addWidget(self.keySequenceEdit)
        buttons.addWidget(self.currentHotkeyLabel)
        buttons.addWidget(self.closeButton)
        buttons.addWidget(self.runButton)
        buttons.addWidget(self.saveButton)

        description = QHBoxLayout()
        description.addWidget(QLabel(self.tr("Description: ")))
        description.addWidget(self.snippetDescription)

        vlayoutWidget = QWidget()
        vlayout = QVBoxLayout()
        vlayout.addLayout(description)
        vlayout.addWidget(self.edit)
        vlayout.addLayout(buttons)
        vlayoutWidget.setLayout(vlayout)

        hsplitter = QSplitter()
        hsplitter.addWidget(treeWidget)
        hsplitter.addWidget(vlayoutWidget)

        hlayout = QHBoxLayout()
        hlayout.addWidget(hsplitter)

        self.showNormal() #Fixes bug that maximized windows are "stuck"
        #Because you can't trust QT to do the right thing here
        if (sys.platform == "darwin"):
            self.settings = QSettings("Vector35", "Snippet Editor")
        else:
            self.settings = QSettings("Vector 35", "Snippet Editor")
        if self.settings.contains("ui/snippeteditor/geometry"):
            self.restoreGeometry(self.settings.value("ui/snippeteditor/geometry"))
        else:
            self.edit.setMinimumWidth(80 * font.averageCharWidth())
            self.edit.setMinimumHeight(30 * font.lineSpacing())

        # Set dialog layout
        self.setLayout(hlayout)

        # Add signals
        self.saveButton.clicked.connect(self.save)
        self.closeButton.clicked.connect(self.close)
        self.runButton.clicked.connect(self.run)
        self.clearHotkeyButton.clicked.connect(self.clearHotkey)
        self.tree.selectionModel().selectionChanged.connect(self.selectFile)
        self.newSnippetButton.clicked.connect(self.newFileDialog)
        self.deleteSnippetButton.clicked.connect(self.deleteSnippet)
        #self.newFolderButton.clicked.connect(self.newFolder)
        self.browseButton.clicked.connect(self.browseSnippets)

        if self.settings.contains("ui/snippeteditor/selected"):
            selectedName = self.settings.value("ui/snippeteditor/selected")
            self.tree.selectionModel().select(self.files.index(selectedName), QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
            if self.tree.selectionModel().hasSelection():
                self.selectFile(self.tree.selectionModel().selection(), None)
                self.edit.setFocus()
                cursor = self.edit.textCursor()
                cursor.setPosition(self.edit.document().characterCount()-1)
                self.edit.setTextCursor(cursor)
            else:
                self.readOnly(True)
        else:
            self.readOnly(True)


    @staticmethod
    def registerAllSnippets():
        for action in list(filter(lambda x: x.startswith("Snippets\\"), UIAction.getAllRegisteredActions())):
            if action == "Snippets\\Snippet Editor...":
                continue
            UIActionHandler.globalActions().unbindAction(action)
            Menu.mainMenu("Tools").removeAction(action)
            UIAction.unregisterAction(action)

        for snippet in includeWalk(snippetPath, ".py"):
            snippetKeys = None
            (snippetDescription, snippetKeys, snippetCode) = loadSnippetFromFile(snippet)
            actionText = actionFromSnippet(snippet, snippetDescription)
            if snippetCode:
                if snippetKeys == None:
                    UIAction.registerAction(actionText)
                else:
                    UIAction.registerAction(actionText, snippetKeys)
                UIActionHandler.globalActions().bindAction(actionText, UIAction(makeSnippetFunction(snippetCode)))
                Menu.mainMenu("Tools").addAction(actionText, "Snippets")

    def clearSelection(self):
        self.keySequenceEdit.clear()
        self.currentHotkey = QKeySequence()
        self.currentHotkeyLabel.setText("")
        self.currentFileLabel.setText("")
        self.snippetDescription.setText("")
        self.edit.clear()
        self.tree.clearSelection()
        self.currentFile = ""

    def reject(self):
        self.settings.setValue("ui/snippeteditor/geometry", self.saveGeometry())

        if self.snippetChanged():
            question = QMessageBox.question(self, self.tr("Discard"), self.tr("You have unsaved changes, quit anyway?"))
            if question != QMessageBox.StandardButton.Yes:
                return
        self.accept()

    def browseSnippets(self):
        url = QUrl.fromLocalFile(snippetPath)
        QDesktopServices.openUrl(url);

    def newFolder(self):
        (folderName, ok) = QInputDialog.getText(self, self.tr("Folder Name"), self.tr("Folder Name: "))
        if ok and folderName:
            index = self.tree.selectionModel().currentIndex()
            selection = self.files.filePath(index)
            if QFileInfo(selection).isDir():
                QDir(selection).mkdir(folderName)
            else:
                QDir(snippetPath).mkdir(folderName)

    def selectFile(self, new, old):
        if (self.resetting):
            self.resetting = False
            return
        if len(new.indexes()) == 0:
            self.clearSelection()
            self.currentFile = ""
            self.readOnly(True)
            return
        newSelection = self.files.filePath(new.indexes()[0])
        self.settings.setValue("ui/snippeteditor/selected", newSelection)
        if QFileInfo(newSelection).isDir():
            self.readOnly(True)
            self.clearSelection()
            self.currentFile = ""
            return

        if old and old.length() > 0:
            oldSelection = self.files.filePath(old.indexes()[0])
            if not QFileInfo(oldSelection).isDir() and self.snippetChanged():
                question = QMessageBox.question(self, self.tr("Discard"), self.tr("Snippet changed. Discard changes?"))
                if question != QMessageBox.StandardButton.Yes:
                    self.resetting = True
                    self.tree.selectionModel().select(old, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
                    return False

        self.currentFile = newSelection
        self.loadSnippet()

    def loadSnippet(self):
        self.currentFileLabel.setText(QFileInfo(self.currentFile).baseName())
        (snippetDescription, snippetKeys, snippetCode) = loadSnippetFromFile(self.currentFile)
        self.snippetDescription.setText(snippetDescription) if snippetDescription else self.snippetDescription.setText("")
        self.keySequenceEdit.setKeySequence(snippetKeys) if snippetKeys else self.keySequenceEdit.setKeySequence(QKeySequence(""))
        self.edit.setPlainText(snippetCode) if snippetCode else self.edit.setPlainText("")
        self.readOnly(False)

    def newFileDialog(self):
        (snippetName, ok) = QInputDialog.getText(self, self.tr("Snippet Name"), self.tr("Snippet Name: "))
        if ok and snippetName:
            if not snippetName.endswith(".py"):
                snippetName += ".py"
            index = self.tree.selectionModel().currentIndex()
            selection = self.files.filePath(index)
            if QFileInfo(selection).isDir():
                path = os.path.join(selection, snippetName)
            else:
                path = os.path.join(snippetPath, snippetName)
                self.readOnly(False)
            open(path, "w").close()
            self.tree.setCurrentIndex(self.files.index(path))
            log_debug("Snippet %s created." % snippetName)

    def readOnly(self, flag):
        self.keySequenceEdit.setEnabled(not flag)
        self.snippetDescription.setReadOnly(flag)
        self.edit.setReadOnly(flag)
        if flag:
            self.snippetDescription.setDisabled(True)
            self.edit.setDisabled(True)
        else:
            self.snippetDescription.setEnabled(True)
            self.edit.setEnabled(True)

    def deleteSnippet(self):
        selection = self.tree.selectedIndexes()[::self.columns][0] #treeview returns each selected element in the row
        snippetName = self.files.fileName(selection)
        question = QMessageBox.question(self, self.tr("Confirm"), self.tr("Confirm deletion: ") + snippetName)
        if (question == QMessageBox.StandardButton.Yes):
            log_debug("Deleting snippet %s." % snippetName)
            self.clearSelection()
            self.files.remove(selection)
            self.registerAllSnippets()

    def snippetChanged(self):
        if (self.currentFile == "" or QFileInfo(self.currentFile).isDir()):
            return False
        (snippetDescription, snippetKeys, snippetCode) = loadSnippetFromFile(self.currentFile)
        if snippetKeys == None and not self.keySequenceEdit.keySequence().isEmpty():
            return True
        if snippetKeys != None and snippetKeys != self.keySequenceEdit.keySequence().toString():
            return True
        return self.edit.toPlainText() != snippetCode or \
               self.snippetDescription.text() != snippetDescription

    def save(self):
        log_debug("Saving snippet %s" % self.currentFile)
        outputSnippet = codecs.open(self.currentFile, "w", "utf-8")
        outputSnippet.write("#" + self.snippetDescription.text() + "\n")
        outputSnippet.write("#" + self.keySequenceEdit.keySequence().toString() + "\n")
        outputSnippet.write(self.edit.toPlainText())
        outputSnippet.close()
        self.registerAllSnippets()

    def run(self):
        if self.context == None:
            log_warn("Cannot run snippets outside of the UI at this time.")
            return
        if self.snippetChanged():
            question = QMessageBox.question(self, self.tr("Confirm"), self.tr("You have unsaved changes, must save first. Save?"))
            if (question == QMessageBox.StandardButton.No):
                return
            else:
                self.save()
        actionText = actionFromSnippet(self.currentFile, self.snippetDescription.text())
        UIActionHandler.globalActions().executeAction(actionText, self.context)

        log_debug("Saving snippet %s" % self.currentFile)
        outputSnippet = codecs.open(self.currentFile, "w", "utf-8")
        outputSnippet.write("#" + self.snippetDescription.text() + "\n")
        outputSnippet.write("#" + self.keySequenceEdit.keySequence().toString() + "\n")
        outputSnippet.write(self.edit.toPlainText())
        outputSnippet.close()
        self.registerAllSnippets()

    def clearHotkey(self):
        self.keySequenceEdit.clear()
Exemplo n.º 11
0
class MainWindow(QMainWindow):
    img = None
    processed_img = None
    pdf = None
    pdf_image_refs = []
    pdf_image_idx = -1
    pixmap = None
    qr_code_id = None
    variant = 0
    valid_answers = {}
    score = -1.0
    updateWidgetsState = Signal()
    updateImageView = Signal()
    updateSheetResults = Signal()
    settings = None
    template_filename = None
    grading_script_filename = None
    saved_grading_script_filename = None
    student_records_filename = None
    saved_student_records_filename = None
    student_records = []
    question_variants = []
    answers_variants = []
    answer_scores = []
    recent_files = []
    recent_files_actions = []
    max_recent_files = 5

    def __init__(self):
        super(MainWindow, self).__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.ui.action_Open_image.triggered.connect(self.openImage)
        self.ui.action_Open_PDF_collection.triggered.connect(self.openPDF)
        self.ui.action_Set_template_file.triggered.connect(
            self.setTemplateFile)
        self.ui.action_Set_grading_R_file.triggered.connect(
            self.setGradingScriptFile)
        self.ui.action_Set_student_records_file.triggered.connect(
            self.setStudentRecordsFile)
        self.ui.nextButton.clicked.connect(self.nextPdfImage)
        self.ui.prevButton.clicked.connect(self.prevPdfImage)
        self.updateWidgetsState.connect(self.onUpdateWidgetsState)
        self.updateImageView.connect(self.onUpdateImageView)
        self.updateSheetResults.connect(self.onUpdateSheetResults)
        self.ui.processButton.clicked.connect(self.processImage)
        self.ui.saveResultButton.clicked.connect(self.saveResult)
        self.settings = QSettings()
        if self.settings.contains("template_file"):
            self.template_filename = self.settings.value("template_file")
        if self.settings.contains("student_records_file"):
            self.saved_student_records_filename = self.settings.value(
                "student_records_file")
        if self.settings.contains("grading_script_file"):
            self.saved_grading_script_filename = self.settings.value(
                "grading_script_file")
        if self.settings.contains("recent_files"):
            try:
                self.recent_files = json.loads(
                    self.settings.value("recent_files"))
            except (ValueError, TypeError):
                self.recent_files = []
        self.updateWidgetsState.emit()

    def resizeEvent(self, event):
        self.updateImageView.emit()
        return super(MainWindow, self).resizeEvent(event)

    def resetSheetResults(self):
        self.processed_img = None
        self.pixmap = None
        self.qr_code_id = None
        self.variant = 0
        self.valid_answers = {}
        self.score = -1

    def updatePdfSelection(self):
        self.resetSheetResults()
        self.img = fitz_to_cv(self.pdf,
                              self.pdf_image_refs[self.pdf_image_idx])
        self.pixmap = cv_to_qpixmap(cv.cvtColor(self.img, cv.COLOR_BGR2RGB))
        self.updateWidgetsState.emit()
        self.updateImageView.emit()
        self.updateSheetResults.emit()

    def updateRecentFiles(self, filename):
        try:
            self.recent_files.remove(filename)
        except ValueError:
            pass
        if len(self.recent_files) > self.max_recent_files:
            self.recent_files = self.recent_files[1:]
        self.recent_files.append(filename)
        self.settings.setValue('recent_files', json.dumps(self.recent_files))

    def loadImage(self, filename):
        if os.path.isfile(filename):
            self.img = cv.imread(filename)
            self.updateRecentFiles(filename)
            self.pdf = None
            self.pdf_image_refs = []
            self.pdf_image_idx = -1
            self.resetSheetResults()
            self.updateWidgetsState.emit()
            self.pixmap = cv_to_qpixmap(cv.cvtColor(self.img,
                                                    cv.COLOR_BGR2RGB))
            self.updateImageView.emit()
            self.updateSheetResults.emit()

    def loadPDF(self, filename):
        if os.path.isfile(filename):
            self.pdf = fitz.open(filename)
            self.updateRecentFiles(filename)
            self.pdf_image_refs = []
            for p in range(len(self.pdf)):
                self.pdf_image_refs += [
                    obj[0] for obj in self.pdf.getPageImageList(p)
                ]
            self.pdf_image_idx = 0
            self.updatePdfSelection()

    def computeScore(self, variant, answers, question_variants,
                     answers_variants, answer_scores):
        choice_to_int = {"a": 0, "b": 1, "c": 2, "d": 3}
        score = 0.0
        for q, a in answers.items():
            orig_q = question_variants[variant - 1].index(q)
            orig_a = answers_variants[variant -
                                      1][orig_q][choice_to_int[a.lower()]] - 1
            score += answer_scores[orig_q][orig_a]
            if answer_scores[orig_q][orig_a] < 0.1:
                print("Wrong answer for question {:d} in variant {:d}".format(
                    q, variant))
        return score

    @Slot()
    def openImage(self):
        filename, _ = QFileDialog.getOpenFileName(
            self, "Open Image", "", "Image Files (*.png *.jpg *.bmp)")
        self.loadImage(filename)

    @Slot()
    def openPDF(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Open PDF collection",
                                                  "", "PDF Files (*.pdf)")
        self.loadPDF(filename)

    @Slot()
    def openRecent(self):
        action = self.sender()

        if action:
            filename = action.data()
            _, extension = os.path.splitext(filename)
            if extension.lower() == ".pdf":
                self.loadPDF(filename)
            else:
                self.loadImage(filename)

    @Slot()
    def setTemplateFile(self):
        filename, _ = QFileDialog.getOpenFileName(
            self, "Set answer sheet template file", "",
            "SVG Template Files (*.svg)")
        if os.path.isfile(filename):
            self.template_filename = filename
            self.settings.setValue("template_file", self.template_filename)
            self.updateWidgetsState.emit()

    @Slot()
    def setGradingScriptFile(self):
        filename, _ = QFileDialog.getOpenFileName(
            self, "Set R grading file", self.saved_grading_script_filename,
            "R Script Files (*.r)")
        if os.path.isfile(filename):
            self.grading_script_filename = filename
            self.saved_grading_script_filename = filename
            self.settings.setValue("grading_script_file",
                                   self.grading_script_filename)

    @Slot()
    def setStudentRecordsFile(self):
        filename, _ = QFileDialog.getOpenFileName(
            self, "Set student records file",
            self.saved_student_records_filename, "CSV Files (*.csv)")
        if os.path.isfile(filename):
            self.student_records_filename = filename
            self.saved_student_records_filename = filename
            self.settings.setValue("student_records_file",
                                   self.student_records_filename)
            with open(self.student_records_filename) as csvfile:
                records = csv.reader(csvfile, delimiter=",", quotechar='"')
                self.student_records = []
                for row in records:
                    while len(row) < 4:
                        row.append("")
                    if row[2] == "":
                        row[2] = "0"
                    if row[3] == "":
                        row[3] = "-1"
                    self.student_records.append(row)
                csvfile.close()
            self.updateWidgetsState.emit()

    @Slot()
    def nextPdfImage(self):
        self.pdf_image_idx += 1
        self.updatePdfSelection()

    @Slot()
    def prevPdfImage(self):
        self.pdf_image_idx -= 1
        self.updatePdfSelection()

    @Slot()
    def processImage(self):
        if self.img is not None:
            if self.grading_script_filename is None:
                self.setGradingScriptFile()
            if os.path.isfile(self.grading_script_filename):
                self.question_variants, \
                self.answers_variants, \
                self.answer_scores = get_testset_answers(self.grading_script_filename)
            self.processed_img, \
            self.qr_code_id, \
            self.variant, \
            self.valid_answers = process_answer_sheet(self.img, self.template_filename)
            self.pixmap = cv_to_qpixmap(
                cv.cvtColor(self.processed_img, cv.COLOR_BGR2RGB))
            if self.variant == 0:
                self.variant = SetVersionDialog.getManualVersion()
            if 0 < self.variant and len(self.question_variants) > 0 and \
                    len(self.answers_variants) > 0 and len(self.answer_scores) > 0:
                self.score = self.computeScore(self.variant,
                                               self.valid_answers,
                                               self.question_variants,
                                               self.answers_variants,
                                               self.answer_scores)
            self.updateWidgetsState.emit()
            self.updateImageView.emit()
            self.updateSheetResults.emit()

    @Slot()
    def saveResult(self):
        if self.score > -1.0:
            if self.student_records_filename is None or len(
                    self.student_records) == 0:
                self.setStudentRecordsFile()
            if len(self.student_records) > 0:
                stud_id = SaveResultDialog.getRecordId(self.student_records)
                if 0 <= stud_id < len(self.student_records):
                    self.student_records[stud_id][2] = "{:.0f}".format(
                        self.score)
                    if self.pdf_image_idx >= 0 and len(
                            self.pdf_image_refs) > 0:
                        self.student_records[stud_id][3] = "{:d}".format(
                            self.pdf_image_idx)
                        self.updateWidgetsState.emit()
                    with open(self.student_records_filename, "w") as csvfile:
                        records = csv.writer(csvfile,
                                             delimiter=",",
                                             quotechar='"')
                        records.writerows(self.student_records)
                        csvfile.close()

    @Slot()
    def onUpdateWidgetsState(self):
        self.ui.action_Open_PDF_collection.setEnabled(
            self.template_filename is not None)
        pdf_whats_this = "Open PDF collection of scanned answer sheets" if self.template_filename is not None \
            else "Must set template file"
        self.ui.action_Open_PDF_collection.setWhatsThis(pdf_whats_this)
        self.ui.action_Open_image.setEnabled(
            self.template_filename is not None)
        self.ui.menu_Open_recent.setEnabled(self.template_filename is not None
                                            and len(self.recent_files) > 0)
        self.ui.menu_Open_recent.clear()
        self.recent_files_actions = []
        self.recent_files = [f for f in self.recent_files if os.path.isfile(f)]
        for idx, filename in enumerate(self.recent_files[::-1]):
            action = QAction("&{:d} {:s}".format(idx + 1,
                                                 os.path.basename(filename)))
            action.setToolTip(filename)
            action.setVisible(True)
            action.setData(filename)
            action.triggered.connect(self.openRecent)
            self.recent_files_actions.append(action)
            self.ui.menu_Open_recent.addAction(action)
        self.ui.nextButton.setEnabled(
            0 <= self.pdf_image_idx < len(self.pdf_image_refs) - 1)
        self.ui.prevButton.setEnabled(self.pdf_image_idx > 0)
        self.ui.processButton.setEnabled(self.img is not None)
        self.ui.saveResultButton.setEnabled(len(self.valid_answers) > 0)
        self.ui.statusbar.clearMessage()
        status_message = ""
        if self.pdf_image_idx >= 0 and len(self.pdf_image_refs) > 0:
            score_message = ", no records file set"
            if self.student_records_filename is not None and len(
                    self.student_records) > 0:
                score_message = ", not found in records"
                for record in self.student_records:
                    try:
                        stud_id = int(record[3])
                        stud_score = float(record[2])
                        stud_name = record[1]
                        stud_group = record[0]
                        if stud_id == self.pdf_image_idx:
                            score_message = ", in records as '{:s}' from {:s} "\
                                            "with a score of {:.0f}".format(stud_name, stud_group, stud_score)
                            break
                    except ValueError:
                        pass
            status_message = "Answer sheet {:2d} of {:d}{:s}".format(
                self.pdf_image_idx + 1, len(self.pdf_image_refs),
                score_message)
        self.ui.statusbar.showMessage(status_message)

    @Slot()
    def onUpdateImageView(self):
        if self.pixmap is not None:
            self.ui.imageView.setPixmap(
                self.pixmap.scaled(self.ui.imageView.size(),
                                   Qt.KeepAspectRatio,
                                   Qt.SmoothTransformation))

    @Slot()
    def onUpdateSheetResults(self):
        self.ui.idText.setText(self.qr_code_id)
        self.ui.versionText.setText(
            '{:d}'.format(self.variant) if self.variant > 0 else '')
        scoreText = "{:.0f}".format(
            self.score
        ) if self.score >= 0.0 else 'Not scored yet.' if self.variant > 0 else ''
        self.ui.scoreText.setText(scoreText)
        self.ui.answerList.clear()
        for q in sorted(self.valid_answers, key=lambda x: int(x)):
            self.ui.answerList.addItem(
                QListWidgetItem("{:3d} -> {:s}".format(
                    int(q), self.valid_answers[q].upper())))
Exemplo n.º 12
0
class AppController:
    """ The main controller for this app. """
    def __init__(self):
        # App settings and logging.
        self._settings = QSettings(settings_info[0], settings_info[1])

        if not self._settings.contains("language"):
            self._settings.setValue("language", LangEnum.ENG)
        self._lang = self._settings.value("language")
        if self._lang not in strings.keys():
            self._lang = LangEnum.ENG
            self._settings.setValue("language", LangEnum.ENG)
        self._strings = strings[self._lang]

        self._settings.beginGroup("logging")
        if not self._settings.contains("level"):
            self._settings.setValue("level", ERROR)
        log_level = self._settings.value('level')
        self._settings.endGroup()

        log_file = setup_log_file(self._strings[StringsEnum.LOG_OUT_NAME],
                                  self._strings[StringsEnum.PROG_OUT_HDR])
        logging.basicConfig(filename=log_file,
                            filemode='w',
                            level=log_level,
                            format=log_format)
        self._logger = logging.getLogger(__name__)
        self.log_output = OutputWindow(self._lang)
        self.formatter = logging.Formatter(log_format)
        self.app_lh = logging.StreamHandler(self.log_output)
        self.app_lh.setLevel(log_level)
        self.app_lh.setFormatter(self.formatter)
        self.stderr_lh = logging.StreamHandler()
        self.stderr_lh.setLevel(logging.WARNING)
        self.stderr_lh.setFormatter(self.formatter)
        log_handlers = [
            self.app_lh
        ]  # , self.stderr_lh]  # TODO: Remove self.stderr_lh from list for builds.
        for h in log_handlers:
            self._logger.addHandler(h)
        self._logger.info(self._strings[StringsEnum.LOG_VER_ID] +
                          str(version_number))
        setup_logging_queue()

        self._logger.debug("Initializing")

        # View
        ui_min_size = QSize(1065, 165)
        button_box_size = QSize(205, 120)
        layout_box_size = QSize(200, 120)
        info_box_size = QSize(230, 120)
        flag_box_size = QSize(80, 120)
        note_box_size = QSize(250, 120)
        drive_info_box_size = QSize(200, 120)
        mdi_area_min_size = QSize(1, 1)
        self.main_window = AppMainWindow(ui_min_size, self._lang, log_handlers)
        self.menu_bar = AppMenuBar(self.main_window, self._lang, log_handlers)
        self.button_box = ButtonBox(self.main_window, button_box_size,
                                    self._lang, log_handlers)
        self.layout_box = LayoutBox(self.main_window, layout_box_size,
                                    self._lang, log_handlers)
        self.info_box = InfoBox(self.main_window, info_box_size, self._lang,
                                log_handlers)
        self.d_info_box = DriveInfoBox(self.main_window, drive_info_box_size,
                                       self._lang, log_handlers)
        self.flag_box = FlagBox(self.main_window, flag_box_size, self._lang,
                                log_handlers)
        self.note_box = NoteBox(self.main_window, note_box_size, self._lang,
                                log_handlers)
        self.mdi_area = MDIArea(self.main_window, mdi_area_min_size,
                                log_handlers)
        self._file_dialog = QFileDialog(self.main_window)

        if not self._settings.contains("cam_scanner/active"):
            self._settings.setValue("cam_scanner/active", "True")

        # Model
        self._model = AppModel(self._lang, log_handlers)
        self._model.set_cams_active(
            eval(self._settings.value("cam_scanner/active")))

        self._save_file_name = str()
        self._save_dir = str()
        self._tasks = []
        self._setup_handlers()
        self._initialize_view()
        self._start()
        self._drive_updater_task = None
        self._curr_cond_name = ""
        self._logger.debug("Initialized")

    def _window_layout_handler(self, layout: str) -> None:
        """
        Sets the subwindow layout to vertical or horizontal
        :param layout: string value of either "vertical" or "horizontal" to set layout
        :return None:
        """
        self._logger.debug("running")
        if layout == "horizontal":
            self.mdi_area.sort_windows_horizontal()
        elif layout == "vertical":
            self.mdi_area.sort_windows_vertical()
        elif layout == "tiled":
            self.mdi_area.sort_windows_tiled()
        elif layout == "cascade":
            self.mdi_area.sort_windows_cascade()
        self._logger.debug("done")

    def language_change_handler(self, lang: LangEnum) -> None:
        """
        Sets the app language to the user selection.
        :return None:
        """
        self._logger.debug("running")
        self._settings.setValue("language", lang)
        self._strings = strings[lang]
        self.main_window.set_lang(lang)
        self.menu_bar.set_lang(lang)
        self.button_box.set_lang(lang)
        self.layout_box.set_lang(lang)
        self.info_box.set_lang(lang)
        self.d_info_box.set_lang(lang)
        self.flag_box.set_lang(lang)
        self.note_box.set_lang(lang)
        self._model.change_lang(lang)
        self._logger.debug("done")

    def debug_change_handler(self, debug_level: str) -> None:
        """
        Sets the app debug level.
        :param debug_level: The debug level
        :return None:
        """
        self._settings.setValue("logging/level", debug_level)
        self.main_window.show_help_window(
            self._strings[StringsEnum.APP_NAME],
            self._strings[StringsEnum.RESTART_PROG])

    def create_end_exp_handler(self) -> None:
        """
        Handler for create/end button.
        :return None:
        """
        self._logger.debug("running")
        if not self._model.exp_created:
            self._logger.debug("creating experiment")
            if not self._get_save_file_name():
                self._logger.debug(
                    "no save directory selected, done running _create_end_exp_handler()"
                )
                return
            self._create_exp()
            self.main_window.set_close_check(True)
            self._logger.debug("done")
        else:
            self._logger.debug("ending experiment")
            self._end_exp()
            self.main_window.set_close_check(False)
            self._save_file_name = ""
        self._logger.debug("done")

    def start_stop_exp_handler(self) -> None:
        """
        Handler play/pause button.
        :return None:
        """
        self._logger.debug("running")
        if self._model.exp_running:
            self._logger.debug("stopping experiment")
            self._stop_exp()
        else:
            self._logger.debug("starting experiment")
            self._start_exp()
        self._logger.debug("done")

    async def new_device_view_handler(self) -> None:
        """
        Check for and handle any new device view objects from model.
        :return None:
        """
        self._logger.debug("running")
        dev_type: str
        dev_port: AioSerial
        while True:
            await self._model.await_new_view()
            ret, view = self._model.get_next_new_view()
            while ret:
                self.mdi_area.add_window(view)
                ret, view = self._model.get_next_new_view()

    async def remove_device_view_handler(self) -> None:
        """
        Check for and handle any device views to remove from model.
        :return None:
        """
        while True:
            await self._model.await_remove_view()
            ret, view = self._model.get_next_view_to_remove()
            while ret:
                self.mdi_area.remove_window(view)
                ret, view = self._model.get_next_view_to_remove()

    async def device_conn_error_handler(self) -> None:
        """
        Alert user to device connection error.
        :return None:
        """
        while True:
            await self._model.await_dev_con_err()
            self.main_window.show_help_window(
                "Error", self._strings[StringsEnum.DEV_CON_ERR])

    def post_handler(self) -> None:
        """
        Handler for post button.
        :return:
        """
        self._logger.debug("running")
        note = self.note_box.get_note()
        self.note_box.clear_note()
        self._model.save_note(note)
        self._logger.debug("done")

    def _cond_name_handler(self, text: str) -> None:
        """
        Handle when condition name is changed.
        :param text: The new condition name text.
        :return None:
        """
        self._logger.debug("running")
        self._model.send_cond_name_to_devs(text)
        self._logger.debug("done")

    def about_rs_handler(self) -> None:
        """
        Handler for about company button.
        :return None:
        """
        self._logger.debug("running")
        self.main_window.show_help_window(
            self._strings[StringsEnum.APP_NAME],
            self._strings[StringsEnum.ABOUT_COMPANY])
        self._logger.debug("done")

    def about_app_handler(self) -> None:
        """
        Handler for about app button.
        :return None:
        """
        self._logger.debug("running")
        self.main_window.show_help_window(self._strings[StringsEnum.APP_NAME],
                                          self._strings[StringsEnum.ABOUT_APP])
        self._logger.debug("done")

    def check_for_updates_handler(self) -> None:
        """
        Handler for update button.
        :return None:
        """
        self._logger.debug("running")
        ret = self._model.check_version()
        if ret == 1:
            self.main_window.show_help_window(
                self._strings[StringsEnum.UPDATE_HDR],
                self._strings[StringsEnum.UPDATE_AVAILABLE])
        elif ret == 0:
            self.main_window.show_help_window(
                self._strings[StringsEnum.UPDATE_HDR],
                self._strings[StringsEnum.NO_UPDATE])
        elif ret == -1:
            self.main_window.show_help_window(
                self._strings[StringsEnum.UPDATE_HDR_ERR],
                self._strings[StringsEnum.ERR_UPDATE_CHECK])
        self._logger.debug("done")

    def log_window_handler(self) -> None:
        """
        Handler for output log button.
        :return None:
        """
        self._logger.debug("running")
        self.log_output.show()
        self._logger.debug("done")

    def last_save_dir_handler(self) -> None:
        """
        Handler for last save dir button.
        :return None:
        """
        self._logger.debug("running")
        if self._save_dir == "":
            QDesktopServices.openUrl(QUrl.fromLocalFile(QDir().homePath()))
        else:
            QDesktopServices.openUrl(QUrl.fromLocalFile(self._save_dir))
        self._logger.debug("done")

    def exit_handler(self) -> None:
        """
        Handler for the exit button in the file menu.
        :return None:
        """
        self._logger.debug("running")
        self.main_window.close()
        self._logger.debug("done")

    def toggle_cam_handler(self) -> None:
        """
        Handler for use cam button.
        :return None:
        """
        self._logger.debug("running")
        self._settings.beginGroup("cam_scanner")
        if not self._settings.contains("active"):
            self._settings.setValue("active", "True")
            active = True
        else:
            active = not eval(self._settings.value("active"))
            if active:
                self._settings.setValue("active", "True")
            else:
                self._settings.setValue("active", "False")
        self._settings.endGroup()
        self.menu_bar.set_cam_bool_checked(active)
        self._model.set_cams_active(active)
        self._logger.debug("done")

    async def _update_drive_info_box(self) -> None:
        """
        Periodically get info about drive currently being used to save data and display info on view.
        :return None:
        """
        while True:
            info = get_disk_usage_stats(self._drive_path)
            self.d_info_box.set_name_val(str(info[0]))
            self.d_info_box.set_perc_val(str(info[4]))
            self.d_info_box.set_gb_val(str(info[2]))
            self.d_info_box.set_mb_val(str(info[3]))
            await sleep(3)

    def _create_exp(self) -> None:
        """
        Create an experiment. Signal devices and update view.
        :return None:
        """
        self._logger.debug("running")
        # TODO: switch these back when .rs file reading is implemented.
        # self._model.signal_create_exp(self._save_file_name, self.button_box.get_condition_name())
        self._model.signal_create_exp(self._save_dir,
                                      self.button_box.get_condition_name(),
                                      self.flag_box.get_flag())
        if self._model.exp_created:
            self.button_box.set_start_button_enabled(True)
            self.button_box.set_create_button_state(1)
            self._check_toggle_post_button()
            self.menu_bar.set_use_cams(False)
            if self._set_drive_updater():
                self._drive_updater_task = create_task(
                    self._update_drive_info_box())
            self.info_box.set_exp_start_time(
                format_current_time(datetime.now(), time=True))
        else:
            self.button_box.set_create_button_state(0)
        self._logger.debug("done")

    def _end_exp(self) -> None:
        """
        End an experiment. Stop experiment if running then signal devices and update view.
        :return None:
        """
        self._logger.debug("running")
        if self._model.exp_running:
            self._stop_exp()
        self._model.signal_end_exp()
        self._check_toggle_post_button()
        if self._drive_updater_task:
            self._drive_updater_task.cancel()
            self._drive_updater_task = None
        self.info_box.set_block_num('0')
        self.button_box.set_create_button_state(0)
        self.button_box.set_start_button_enabled(False)
        self.button_box.set_start_button_state(0)
        self.menu_bar.set_use_cams(True)
        self._logger.debug("done")

    def _start_exp(self) -> None:
        """
        Start an experiment if one has been created. Signal devices and update view.
        :return None:
        """
        self._logger.debug("running")
        self._model.signal_start_exp(self.button_box.get_condition_name())
        if not self._model.exp_running:
            return
        self.info_box.set_block_num(str(self._model.get_block_num()))
        self.button_box.set_start_button_state(1)
        self.button_box.set_condition_name_box_enabled(False)
        self._curr_cond_name = self.button_box.get_condition_name()
        self.info_box.set_block_start_time(
            format_current_time(datetime.now(), time=True))
        self._logger.debug("done")

    def _stop_exp(self) -> None:
        """
        Stop an experiment. Signal devices and update view.
        :return None:
        """
        self._logger.debug("running")
        self._model.signal_stop_exp()
        self.button_box.set_start_button_state(2)
        self.button_box.set_condition_name_box_enabled(True)
        self._logger.debug("done")

    def _set_drive_updater(self, filename: str = None) -> bool:
        """
        Update drive info display with drive information.
        :param filename: The drive to look at.
        :return None:
        """
        ret = True
        if not filename:
            if not self._save_dir == "":
                self._drive_path = self._save_dir
            else:
                ret = False
        elif len(filename) > 0:
            self._drive_path = filename
        else:
            ret = False
        return ret

    async def _keep_time(self) -> None:
        """
        Update time every tick.
        :return None:
        """
        self._logger.debug("running")
        while True:
            self.info_box.set_current_time(
                format_current_time(datetime.now(), time=True))
            await sleep(1)

    def _check_toggle_post_button(self) -> None:
        """
        If an experiment is created and running and there is a note then allow user access to post button.
        :return None:
        """
        self._logger.debug("running")
        if self._model.exp_created and len(self.note_box.get_note()) > 0:
            self._logger.debug("button = true")
            self.note_box.set_post_button_enabled(True)
        else:
            self._logger.debug("button = false")
            self.note_box.set_post_button_enabled(False)
        self._logger.debug("done")

    def _get_save_file_name(self) -> bool:
        """
        Gets a new filename from the user for the current experiment to be saved as.
        :return bool: True if the file name is longer than 1 character
        """
        self._logger.debug("running")
        # TODO: Use save_file_name again when reading .rs files is implemented.
        # self._save_file_name = self._file_dialog.getSaveFileName(filter="*.rs")[0]
        self._save_dir = self._file_dialog.getExistingDirectory(
        )  # filter="*.rs")[0]
        # valid = len(self._save_file_name) > 1
        # if valid:
        #     self._save_dir = self._dir_name_from_file_name(self._save_file_name)
        self._logger.debug("done")
        if len(self._save_dir) > 0:
            return True
        return False

    def _dir_name_from_file_name(self, filename: str) -> str:
        """
        Get directory name from filename.
        :param filename: The absolute path filename.
        :return str: The resulting directory name.
        """
        self._logger.debug("running")
        dir_name = filename[:filename.rindex("/")]
        self._logger.debug("done")
        return dir_name

    def _keypress_handler(self, event: QKeyEvent) -> None:
        """
        Handle any keypress event and intercept alphabetical keypresses, then set flag_box to that key.
        :param event: The keypress event.
        :return None:
        """
        self._logger.debug("running")
        if 0x41 <= event.key() <= 0x5a:
            self.flag_box.set_flag(chr(event.key()))
            flag = self.flag_box.get_flag()
            self._model.send_keyflag_to_devs(flag)
            self._model.save_keyflag(flag)
        event.accept()
        self._logger.debug("done")

    def _setup_handlers(self) -> None:
        """
        Attach events to handlers
        :return None:
        """
        self._logger.debug("running")
        # Experiment controls
        self.button_box.add_create_button_handler(self.create_end_exp_handler)
        self.button_box.add_start_button_handler(self.start_stop_exp_handler)
        self.button_box.add_cond_name_changed_handler(self._cond_name_handler)
        self.note_box.add_note_box_changed_handler(
            self._check_toggle_post_button)
        self.note_box.add_post_handler(self.post_handler)
        self.main_window.keyPressEvent = self._keypress_handler
        self.layout_box.add_window_layout_handler(self._window_layout_handler)

        # File menu
        self.menu_bar.add_open_last_save_dir_handler(
            self.last_save_dir_handler)
        self.menu_bar.add_cam_bool_handler(self.toggle_cam_handler)
        self.menu_bar.add_exit_handler(self.exit_handler)

        # Settings menu
        self.menu_bar.add_lang_select_handler(self.language_change_handler)
        self.menu_bar.add_debug_select_handler(self.debug_change_handler)
        # self.menu_bar.add_window_layout_handler(self.window_layout_handler)

        # Help menu
        self.menu_bar.add_about_company_handler(self.about_rs_handler)
        self.menu_bar.add_about_app_handler(self.about_app_handler)
        self.menu_bar.add_update_handler(self.check_for_updates_handler)
        self.menu_bar.add_log_window_handler(self.log_window_handler)

        # Close app button
        self.main_window.add_close_handler(self.cleanup)

        self._logger.debug("done")

    def _initialize_view(self) -> None:
        """
        Put the different components of the view together and then show the view.
        :return None:
        """
        self._logger.debug("running")
        self.menu_bar.set_debug_action(self._settings.value("logging/level"))
        self.menu_bar.set_cam_bool_checked(
            eval(self._settings.value("cam_scanner/active")))
        self.main_window.add_menu_bar(self.menu_bar)
        self.main_window.add_control_bar_widget(self.button_box)
        self.main_window.add_control_bar_widget(self.flag_box)
        self.main_window.add_control_bar_widget(self.note_box)
        self.main_window.add_control_bar_widget(self.layout_box)
        self.main_window.add_spacer_item(1)
        self.main_window.add_control_bar_widget(self.info_box)
        self.main_window.add_control_bar_widget(self.d_info_box)
        self.main_window.add_mdi_area(self.mdi_area)
        self.main_window.show()
        self._logger.debug("done")

    def _start(self) -> None:
        """
        Start all recurring functions.
        :return None:
        """
        self._tasks.append(create_task(self.new_device_view_handler()))
        self._tasks.append(create_task(self.device_conn_error_handler()))
        self._tasks.append(create_task(self.remove_device_view_handler()))
        self._tasks.append(create_task(self._keep_time()))
        self._model.start()

    def cleanup(self) -> None:
        """
        Handler for view close event.
        :return None:
        """
        self._logger.debug("running")
        create_task(self._cleanup())
        self._logger.debug("done")

    async def _cleanup(self) -> None:
        """
        Cleanup any code that would cause problems for shutdown and prep for app closure.
        :return None:
        """
        self._logger.debug("running")
        for task in self._tasks:
            task.cancel()
        await self._model.cleanup()
        if self._drive_updater_task:
            self._drive_updater_task.cancel()
        self.log_output.close()
        self.main_window.set_close_override(True)
        self._logger.debug("done")
        self.main_window.close()
Exemplo n.º 13
0
class MainWindow(QMainWindow):
    def __init__(self):
        QMainWindow.__init__(self, parent=None)
        self.settings = QSettings(ORGANIZATION_STR)
        self.toolbar = self.createToolBar()

        self.widget = QWidget(self)
        self.layout = QVBoxLayout(self.widget)
        self.layout.setContentsMargins(1, 1, 1, 1)
        self.currentWidget = None

        self.editor = QTextEdit()
        self.editor.setAcceptRichText(False)

        font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
        fs = int(self.settings.value("fontSize", 13))
        font.setPointSize(fs)

        self.editor.setFont(font)
        self.preview = MarkdownRenderer()

        self.layout.addWidget(self.toolbar)
        self.widget.setLayout(self.layout)

        self.showWidget(self.editor)

        self.addToolBar(self.toolbar)
        self.setCentralWidget(self.widget)

        self.setWindowTitle(BASIC_TITLE)

        self.nbManager = NotebookManager()

        self.currentNotebook = None
        self.currentNote = None
        self.dirty = False

        self.editor.document().modificationChanged.connect(self.editorModified)

        self.updateUI()

        if len(self.nbManager.notebooks()) > 0:
            self.switchNotebook(self.nbManager.notebooks()[0].name)

        self.createTrayIcon()

        self.readConfig()

    def closeEvent(self, event):
        self.nbManager.writeConfig()
        self.writeConfig()

        QMainWindow.closeEvent(self, event)

    def showWidget(self, w):
        if self.currentWidget is not None:
            self.currentWidget.hide()
            self.layout.removeWidget(self.currentWidget)

        self.layout.addWidget(w)
        #self.layout.update()
        self.currentWidget = w
        self.currentWidget.show()

    def createToolBar(self):
        toolbar = QToolBar()

        act = toolbar.addAction(QIcon.fromTheme("document-new"),
                                "Create a new note", self.newNote)
        act.setShortcut(QKeySequence.New)
        toolbar.addAction(QIcon.fromTheme("folder-new"),
                          "Create a new notebook", self.newNotebook)
        toolbar.addSeparator()

        act = toolbar.addAction(QIcon.fromTheme("document-save"),
                                "Save changes to current note", self.saveNote)
        act.setShortcut(QKeySequence.Save)
        toolbar.addAction(QIcon.fromTheme("document-send"),
                          "Export current note")
        toolbar.addSeparator()

        toolbar.addAction(QIcon.fromTheme("format-text-bold"), "Bold")
        toolbar.addAction(QIcon.fromTheme("format-text-italic"), "Italic")
        toolbar.addAction(QIcon.fromTheme("format-text-underline"),
                          "Underline")

        self.headingsCombo = QComboBox()
        self.headingsCombo.addItem("H1")
        self.headingsCombo.addItem("H2")
        self.headingsCombo.addItem("H3")
        self.headingsCombo.addItem("H4")
        self.headingsCombo.addItem("H5")
        self.headingsCombo.addItem("H6")

        toolbar.addWidget(self.headingsCombo)
        toolbar.addSeparator()

        toolbar.addAction(QIcon.fromTheme("format-list-unordered"),
                          "Insert bulleted list")
        toolbar.addAction(QIcon.fromTheme("format-list-ordered"),
                          "Insert numbered list")
        toolbar.addAction(QIcon.fromTheme("insert-link"), "Inserts a link")
        toolbar.addSeparator()

        self.editableAction = toolbar.addAction(
            QIcon.fromTheme("edit"), "Toggle edit or markdown visualizer")
        self.editableAction.setCheckable(True)
        self.editableAction.setChecked(True)
        self.editableAction.setShortcut(QKeySequence("Ctrl+E"))
        QObject.connect(self.editableAction, SIGNAL('triggered(bool)'),
                        self.editTriggered)

        toolbar.addAction(QIcon.fromTheme("system-search"), "Search")

        self.notebooksCombo = QComboBox()
        self.notebooksCombo.setSizePolicy(QSizePolicy.Expanding,
                                          QSizePolicy.Preferred)
        self.notebooksCombo.addItem(QIcon.fromTheme("folder"),
                                    "Create new notebook")

        self.notesCombo = QComboBox()
        self.notesCombo.setSizePolicy(QSizePolicy.Expanding,
                                      QSizePolicy.Preferred)

        QObject.connect(self.notebooksCombo, SIGNAL("activated(QString)"),
                        self.switchNotebook)
        QObject.connect(self.notesCombo, SIGNAL("activated(QString)"),
                        self.switchNote)

        self.spacer = QWidget()
        self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)

        toolbar.addWidget(self.spacer)
        toolbar.addWidget(self.notebooksCombo)
        toolbar.addWidget(self.notesCombo)

        return toolbar

    def createTrayIcon(self):
        path = os.path.join(
            pathlib.Path(__file__).parent.absolute(), "icons/notes.svg")
        icon = QIcon(QPixmap(path))

        self.trayIcon = QSystemTrayIcon(icon, self)
        self.trayMenu = None

        QObject.connect(self.trayIcon,
                        SIGNAL("activated(QSystemTrayIcon::ActivationReason)"),
                        self.trayIconActivated)

        self.populateTrayMenu()
        self.trayIcon.show()

    def trayIconActivated(self, reason):
        if reason != QSystemTrayIcon.Trigger:
            return

        if self.isVisible():
            self.savedGeometry = self.saveGeometry()
            self.hide()
        else:
            self.show()
            self.activateWindow()
            self.restoreGeometry(self.savedGeometry)

    def populateTrayMenu(self):
        del self.trayMenu
        self.trayMenu = QMenu()

        self.trayMenu.addAction(QIcon.fromTheme("folder-new"),
                                "Create a new notebook", self.newNotebook)
        self.trayMenu.addSeparator()

        for notebook in self.nbManager.notebooks():
            nbMenu = self.trayMenu.addMenu(QIcon.fromTheme("folder"),
                                           notebook.name)

            nbMenu.addAction(QIcon.fromTheme("document-new"),
                             "Create a new note", self.newNote)
            nbMenu.addSeparator()

            for note in notebook.notes():
                nbMenu.addAction(QIcon.fromTheme("text-x-generic"), note.name)

        self.trayMenu.addSeparator()
        self.trayMenu.addAction(QIcon.fromTheme("application-exit"), "&Quit",
                                QApplication.instance().quit)

        self.trayIcon.setContextMenu(self.trayMenu)

    def editTriggered(self, checked):
        if checked:
            self.showWidget(self.editor)
        else:
            self.preview.displayMarkdown(self.editor.toPlainText())
            self.showWidget(self.preview)

    def readConfig(self):
        if self.settings.contains("geometry"):
            geometry = self.settings.value("geometry")
            self.restoreGeometry(geometry)

    def writeConfig(self):
        geometry = self.saveGeometry()

        self.settings.setValue("geometry", geometry)

    def updateUI(self):
        self.notebooksCombo.clear()

        for notebook in self.nbManager.notebooks():
            self.notebooksCombo.addItem(QIcon.fromTheme("folder"),
                                        notebook.name)

        if self.currentNotebook is not None:
            self.notesCombo.clear()

            for note in self.currentNotebook.notes():
                self.notesCombo.addItem(QIcon.fromTheme("text-x-generic"),
                                        note.name)

        if self.currentNotebook is not None and self.currentNote is not None:
            title = BASIC_TITLE + " - {}/{}".format(self.currentNotebook.name,
                                                    self.currentNote.name)

            if self.dirty:
                title += " (Modified)"
        else:
            title = BASIC_TITLE

        self.setWindowTitle(title)

    def switchNotebook(self, notebookName):
        self.currentNotebook = self.nbManager.notebook(notebookName)
        self.updateUI()

        # If notebook not empty, switch to first note.
        if len(self.currentNotebook.notes()) > 0:
            self.switchNote(self.currentNotebook.notes()[0].name)
        # If not, request creation of a new note
        else:
            self.newNote()

        self.notebooksCombo.setCurrentText(notebookName)

    def switchNote(self, noteName):
        self.currentNote = self.currentNotebook.note(noteName)
        self.editor.setPlainText(self.currentNote.contents)
        self.preview.displayMarkdown(self.currentNote.contents)

        self.updateUI()

        self.notesCombo.setCurrentText(noteName)

    def newNotebook(self):
        name, ok = QInputDialog.getText(
            self, "Notebook name",
            "How would you like your notebook to be named?")

        if ok:
            storagePath = QDir.homePath() + "/.config/wikinotebook/" + name

            nb = self.nbManager.createNewNotebook(name, storagePath)
            self.switchNotebook(nb.name)

    def newNote(self):
        if self.currentNotebook is None:
            return

        name, ok = QInputDialog.getText(
            self, "Note name", "How would you like your note to be named?")

        if ok:
            note = self.currentNotebook.createNewNote(name)
            self.switchNote(note.name)

    def saveNote(self):
        self.currentNote.contents = self.editor.toPlainText()
        self.currentNote.save()

        self.dirty = False
        self.editor.document().setModified(False)
        self.updateUI()

    def editorModified(self, changed):
        self.dirty = changed
        self.updateUI()