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))
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
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)
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")
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()
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")
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
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()
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')
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()
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())))
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()
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()