def current_selected_uuids(self, lbox: QTreeView = None): lbox = self.current_set_listbox if lbox is None else lbox if lbox is None: LOG.error('not sure which list box is active! oh pooh.') return for q in lbox.selectedIndexes(): yield self.doc.uuid_for_current_layer(q.row())
class FileSystemView(QWidget): def __init__(self, dir_path): super().__init__() appWidth = 800 appHeight = 300 self.setWindowTitle('File System Viewer') self.setGeometry(300, 300, appWidth, appHeight) self.model = QFileSystemModel() self.model.setRootPath(dir_path) self.tree = QTreeView() self.tree.setModel(self.model) self.tree.setRootIndex(self.model.index(dirPath)) self.tree.setColumnWidth(0, 250) self.tree.setAlternatingRowColors(True) layout = QVBoxLayout() self.photo = QLabel('Hola' + chr(10) + 'Caracola') layout.addWidget(self.tree) layout.addWidget(self.photo) self.setLayout(layout) self.tree.selectionModel().selectionChanged.connect(self.select) def keyPressEvent(self, event): if event.key() == Qt.Key_Space or event.key() == Qt.Key_Return: index = self.tree.selectedIndexes()[0] crawler = index.model().filePath(index) print(crawler) #self.tree.keyPressEvent(self, event) def select(self, index1): index = self.tree.selectedIndexes()[0] file_sel = index.model().filePath(index) if os.path.isfile(file_sel) and file_sel[-3:].upper() in 'JPG PNG': pixmap = QPixmap(file_sel) self.photo.setPixmap(pixmap) print(file_sel)
class Window(QWidget): def __init__(self): QWidget.__init__(self) self.treeView = QTreeView() self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) self.treeView.customContextMenuRequested.connect(self.openMenu) self.model = QStandardItemModel() self.addItems(self.model, data) self.treeView.setModel(self.model) self.model.setHorizontalHeaderLabels([self.tr("Object")]) layout = QVBoxLayout() layout.addWidget(self.treeView) self.setLayout(layout) def addItems(self, parent, elements): for text, children in elements: item = QStandardItem(text) parent.appendRow(item) if children: self.addItems(item, children) def openMenu(self, position): indexes = self.treeView.selectedIndexes() if len(indexes) > 0: level = 0 index = indexes[0] while index.parent().isValid(): index = index.parent() level += 1 menu = QMenu() if level == 0: menu.addAction(self.tr("Edit person")) elif level == 1: menu.addAction(self.tr("Edit object/container")) elif level == 2: menu.addAction(self.tr("Edit object")) menu.exec_(self.treeView.viewport().mapToGlobal(position))
class Widget(QWidget): def __init__(self, panel): super(Widget, self).__init__(panel) layout = QVBoxLayout() self.setLayout(layout) layout.setSpacing(0) self.searchEntry = SearchLineEdit() self.treeView = QTreeView(contextMenuPolicy=Qt.CustomContextMenu) self.textView = QTextBrowser() applyButton = QToolButton(autoRaise=True) editButton = QToolButton(autoRaise=True) addButton = QToolButton(autoRaise=True) self.menuButton = QPushButton(flat=True) menu = QMenu(self.menuButton) self.menuButton.setMenu(menu) splitter = QSplitter(Qt.Vertical) top = QHBoxLayout() layout.addLayout(top) splitter.addWidget(self.treeView) splitter.addWidget(self.textView) layout.addWidget(splitter) splitter.setSizes([200, 100]) splitter.setCollapsible(0, False) top.addWidget(self.searchEntry) top.addWidget(applyButton) top.addSpacing(10) top.addWidget(addButton) top.addWidget(editButton) top.addWidget(self.menuButton) # action generator for actions added to search entry def act(slot, icon=None): a = QAction(self, triggered=slot) self.addAction(a) a.setShortcutContext(Qt.WidgetWithChildrenShortcut) icon and a.setIcon(icons.get(icon)) return a # hide if ESC pressed in lineedit a = act(self.slotEscapePressed) a.setShortcut(QKeySequence(Qt.Key_Escape)) # import action a = self.importAction = act(self.slotImport, 'document-open') menu.addAction(a) # export action a = self.exportAction = act(self.slotExport, 'document-save-as') menu.addAction(a) # apply button a = self.applyAction = act(self.slotApply, 'edit-paste') applyButton.setDefaultAction(a) menu.addSeparator() menu.addAction(a) # add button a = self.addAction_ = act(self.slotAdd, 'list-add') a.setShortcut(QKeySequence(Qt.Key_Insert)) addButton.setDefaultAction(a) menu.addSeparator() menu.addAction(a) # edit button a = self.editAction = act(self.slotEdit, 'document-edit') a.setShortcut(QKeySequence(Qt.Key_F2)) editButton.setDefaultAction(a) menu.addAction(a) # set shortcut action a = self.shortcutAction = act( self.slotShortcut, 'preferences-desktop-keyboard-shortcuts') menu.addAction(a) # delete action a = self.deleteAction = act(self.slotDelete, 'list-remove') a.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Delete)) menu.addAction(a) # restore action a = self.restoreAction = act(self.slotRestore) menu.addSeparator() menu.addAction(a) # help button a = self.helpAction = act(self.slotHelp, 'help-contents') menu.addSeparator() menu.addAction(a) self.treeView.setSelectionBehavior(QTreeView.SelectRows) self.treeView.setSelectionMode(QTreeView.ExtendedSelection) self.treeView.setRootIsDecorated(False) self.treeView.setAllColumnsShowFocus(True) self.treeView.setModel(model.model()) self.treeView.setCurrentIndex(QModelIndex()) # signals self.searchEntry.returnPressed.connect(self.slotReturnPressed) self.searchEntry.textChanged.connect(self.updateFilter) self.treeView.doubleClicked.connect(self.slotDoubleClicked) self.treeView.customContextMenuRequested.connect(self.showContextMenu) self.treeView.selectionModel().currentChanged.connect(self.updateText) self.treeView.model().dataChanged.connect(self.updateFilter) # highlight text self.highlighter = highlight.Highlighter(self.textView.document()) # complete on snippet variables self.searchEntry.setCompleter( QCompleter([ ':icon', ':indent', ':menu', ':name', ':python', ':selection', ':set', ':symbol', ':template', ':template-run' ], self.searchEntry)) self.readSettings() app.settingsChanged.connect(self.readSettings) app.translateUI(self) self.updateColumnSizes() self.setAcceptDrops(True) def dropEvent(self, ev): if not ev.source() and ev.mimeData().hasUrls(): filename = ev.mimeData().urls()[0].toLocalFile() if filename: ev.accept() from . import import_export import_export.load(filename, self) def dragEnterEvent(self, ev): if not ev.source() and ev.mimeData().hasUrls(): ev.accept() def translateUI(self): try: self.searchEntry.setPlaceholderText(_("Search...")) except AttributeError: pass # not in Qt 4.6 shortcut = lambda a: a.shortcut().toString(QKeySequence.NativeText) self.menuButton.setText(_("&Menu")) self.addAction_.setText(_("&Add...")) self.addAction_.setToolTip( _("Add a new snippet. ({key})").format( key=shortcut(self.addAction_))) self.editAction.setText(_("&Edit...")) self.editAction.setToolTip( _("Edit the current snippet. ({key})").format( key=shortcut(self.editAction))) self.shortcutAction.setText(_("Configure Keyboard &Shortcut...")) self.deleteAction.setText(_("&Remove")) self.deleteAction.setToolTip(_("Remove the selected snippets.")) self.applyAction.setText(_("A&pply")) self.applyAction.setToolTip(_("Apply the current snippet.")) self.importAction.setText(_("&Import...")) self.importAction.setToolTip(_("Import snippets from a file.")) self.exportAction.setText(_("E&xport...")) self.exportAction.setToolTip(_("Export snippets to a file.")) self.restoreAction.setText(_("Restore &Built-in Snippets...")) self.restoreAction.setToolTip( _("Restore deleted or changed built-in snippets.")) self.helpAction.setText(_("&Help")) self.searchEntry.setToolTip( _("Enter text to search in the snippets list.\n" "See \"What's This\" for more information.")) self.searchEntry.setWhatsThis(''.join( map("<p>{0}</p>\n".format, ( _("Enter text to search in the snippets list, and " "press Enter to apply the currently selected snippet."), _("If the search text fully matches the value of the '{name}' variable " "of a snippet, that snippet is selected.").format( name="name"), _("If the search text starts with a colon ':', the rest of the " "search text filters snippets that define the given variable. " "After a space a value can also be entered, snippets will then " "match if the value of the given variable contains the text after " "the space."), _("E.g. entering {menu} will show all snippets that are displayed " "in the insert menu.").format(menu="<code>:menu</code>"), )))) def sizeHint(self): return self.parent().mainwindow().size() / 4 def readSettings(self): data = textformats.formatData('editor') self.textView.setFont(data.font) self.textView.setPalette(data.palette()) def showContextMenu(self, pos): """Called when the user right-clicks the tree view.""" self.menuButton.menu().popup(self.treeView.viewport().mapToGlobal(pos)) def slotReturnPressed(self): """Called when the user presses Return in the search entry. Applies current snippet.""" name = self.currentSnippet() if name: view = self.parent().mainwindow().currentView() insert.insert(name, view) self.parent().hide() # make configurable? view.setFocus() def slotEscapePressed(self): """Called when the user presses ESC in the search entry. Hides the panel.""" self.parent().hide() self.parent().mainwindow().currentView().setFocus() def slotDoubleClicked(self, index): name = self.treeView.model().name(index) view = self.parent().mainwindow().currentView() insert.insert(name, view) def slotAdd(self): """Called when the user wants to add a new snippet.""" edit.Edit(self, None) def slotEdit(self): """Called when the user wants to edit a snippet.""" name = self.currentSnippet() if name: edit.Edit(self, name) def slotShortcut(self): """Called when the user selects the Configure Shortcut action.""" from widgets import shortcuteditdialog name = self.currentSnippet() if name: collection = self.parent().snippetActions action = actions.action(name, None, collection) default = collection.defaults().get(name) mgr = actioncollectionmanager.manager(self.parent().mainwindow()) cb = mgr.findShortcutConflict dlg = shortcuteditdialog.ShortcutEditDialog( self, cb, (collection, name)) if dlg.editAction(action, default): mgr.removeShortcuts(action.shortcuts()) collection.setShortcuts(name, action.shortcuts()) self.treeView.update() def slotDelete(self): """Called when the user wants to delete the selected rows.""" rows = sorted(set(i.row() for i in self.treeView.selectedIndexes()), reverse=True) if rows: for row in rows: name = self.treeView.model().names()[row] self.parent().snippetActions.setShortcuts(name, []) self.treeView.model().removeRow(row) self.updateFilter() def slotApply(self): """Called when the user clicks the apply button. Applies current snippet.""" name = self.currentSnippet() if name: view = self.parent().mainwindow().currentView() insert.insert(name, view) def slotImport(self): """Called when the user activates the import action.""" filetypes = "{0} (*.xml);;{1} (*)".format(_("XML Files"), _("All Files")) caption = app.caption(_("dialog title", "Import Snippets")) filename = None filename = QFileDialog.getOpenFileName(self, caption, filename, filetypes)[0] if filename: from . import import_export import_export.load(filename, self) def slotExport(self): """Called when the user activates the export action.""" allrows = [ row for row in range(model.model().rowCount()) if not self.treeView.isRowHidden(row, QModelIndex()) ] selectedrows = [ i.row() for i in self.treeView.selectedIndexes() if i.column() == 0 and i.row() in allrows ] names = self.treeView.model().names() names = [names[row] for row in selectedrows or allrows] filetypes = "{0} (*.xml);;{1} (*)".format(_("XML Files"), _("All Files")) n = len(names) caption = app.caption( _("dialog title", "Export {num} Snippet", "Export {num} Snippets", n).format(num=n)) filename = QFileDialog.getSaveFileName(self, caption, None, filetypes)[0] if filename: from . import import_export try: import_export.save(names, filename) except (IOError, OSError) as e: QMessageBox.critical( self, _("Error"), _("Can't write to destination:\n\n{url}\n\n{error}"). format(url=filename, error=e.strerror)) def slotRestore(self): """Called when the user activates the Restore action.""" from . import restore dlg = restore.RestoreDialog(self) dlg.setWindowModality(Qt.WindowModal) dlg.populate() dlg.show() dlg.finished.connect(dlg.deleteLater) def slotHelp(self): """Called when the user clicks the small help button.""" userguide.show("snippets") def currentSnippet(self): """Returns the name of the current snippet if it is visible.""" row = self.treeView.currentIndex().row() if row != -1 and not self.treeView.isRowHidden(row, QModelIndex()): return self.treeView.model().names()[row] def updateFilter(self): """Called when the text in the entry changes, updates search results.""" text = self.searchEntry.text() ltext = text.lower() filterVars = text.startswith(':') if filterVars: try: fvar, fval = text[1:].split(None, 1) fhide = lambda v: v.get(fvar) in (True, None ) or fval not in v.get(fvar) except ValueError: fvar = text[1:].strip() fhide = lambda v: not v.get(fvar) for row in range(self.treeView.model().rowCount()): name = self.treeView.model().names()[row] nameid = snippets.get(name).variables.get('name', '') if filterVars: hide = fhide(snippets.get(name).variables) elif nameid == text: i = self.treeView.model().createIndex(row, 0) self.treeView.selectionModel().setCurrentIndex( i, QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows) hide = False elif nameid.lower().startswith(ltext): hide = False elif ltext in snippets.title(name).lower(): hide = False else: hide = True self.treeView.setRowHidden(row, QModelIndex(), hide) self.updateText() def updateText(self): """Called when the current snippet changes.""" name = self.currentSnippet() self.textView.clear() if name: s = snippets.get(name) self.highlighter.setPython('python' in s.variables) self.textView.setPlainText(s.text) def updateColumnSizes(self): self.treeView.resizeColumnToContents(0) self.treeView.resizeColumnToContents(1)
class DirectoriesDialog(QMainWindow): def __init__(self, app, **kwargs): super().__init__(None, **kwargs) self.app = app self.specific_actions = set() self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS self.recentFolders = Recent(self.app, "recentFolders") self._setupUi() self._updateScanTypeList() self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView) self.directoriesDelegate = DirectoriesDelegate() self.treeView.setItemDelegate(self.directoriesDelegate) self._setupColumns() self.app.recentResults.addMenu(self.menuLoadRecent) self.app.recentResults.addMenu(self.menuRecentResults) self.recentFolders.addMenu(self.menuRecentFolders) self._updateAddButton() self._updateRemoveButton() self._updateLoadResultsButton() self._updateActionsState() self._setupBindings() def _setupBindings(self): self.appModeRadioBox.itemSelected.connect(self.appModeButtonSelected) self.showPreferencesButton.clicked.connect( self.app.actionPreferences.trigger) self.scanButton.clicked.connect(self.scanButtonClicked) self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger) self.addFolderButton.clicked.connect(self.actionAddFolder.trigger) self.removeFolderButton.clicked.connect(self.removeFolderButtonClicked) self.treeView.selectionModel().selectionChanged.connect( self.selectionChanged) self.app.recentResults.itemsChanged.connect( self._updateLoadResultsButton) self.recentFolders.itemsChanged.connect(self._updateAddButton) self.recentFolders.mustOpenItem.connect(self.app.model.add_directory) self.directoriesModel.foldersAdded.connect( self.directoriesModelAddedFolders) self.app.willSavePrefs.connect(self.appWillSavePrefs) def _setupActions(self): # (name, shortcut, icon, desc, func) ACTIONS = [ ( "actionLoadResults", "Ctrl+L", "", tr("Load Results..."), self.loadResultsTriggered, ), ( "actionShowResultsWindow", "", "", tr("Scan Results"), self.app.showResultsWindow, ), ("actionAddFolder", "", "", tr("Add Folder..."), self.addFolderTriggered), ("actionLoadDirectories", "", "", tr("Load Directories..."), self.loadDirectoriesTriggered), ("actionSaveDirectories", "", "", tr("Save Directories..."), self.saveDirectoriesTriggered), ] createActions(ACTIONS, self) if self.app.use_tabs: # Keep track of actions which should only be accessible from this window self.specific_actions.add(self.actionLoadDirectories) self.specific_actions.add(self.actionSaveDirectories) def _setupMenu(self): if not self.app.use_tabs: # we are our own QMainWindow, we need our own menu bar self.menubar = QMenuBar(self) self.menubar.setGeometry(QRect(0, 0, 42, 22)) self.menuFile = QMenu(self.menubar) self.menuFile.setTitle(tr("File")) self.menuView = QMenu(self.menubar) self.menuView.setTitle(tr("View")) self.menuHelp = QMenu(self.menubar) self.menuHelp.setTitle(tr("Help")) self.setMenuBar(self.menubar) menubar = self.menubar else: # we are part of a tab widget, we populate its window's menubar instead self.menuFile = self.app.main_window.menuFile self.menuView = self.app.main_window.menuView self.menuHelp = self.app.main_window.menuHelp menubar = self.app.main_window.menubar self.menuLoadRecent = QMenu(self.menuFile) self.menuLoadRecent.setTitle(tr("Load Recent Results")) self.menuFile.addAction(self.actionLoadResults) self.menuFile.addAction(self.menuLoadRecent.menuAction()) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionClearPictureCache) self.menuFile.addSeparator() self.menuFile.addAction(self.actionLoadDirectories) self.menuFile.addAction(self.actionSaveDirectories) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionQuit) self.menuView.addAction(self.app.actionDirectoriesWindow) self.menuView.addAction(self.actionShowResultsWindow) self.menuView.addAction(self.app.actionIgnoreList) self.menuView.addSeparator() self.menuView.addAction(self.app.actionPreferences) self.menuHelp.addAction(self.app.actionShowHelp) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) menubar.addAction(self.menuFile.menuAction()) menubar.addAction(self.menuView.menuAction()) menubar.addAction(self.menuHelp.menuAction()) # Recent folders menu self.menuRecentFolders = QMenu() self.menuRecentFolders.addAction(self.actionAddFolder) self.menuRecentFolders.addSeparator() # Recent results menu self.menuRecentResults = QMenu() self.menuRecentResults.addAction(self.actionLoadResults) self.menuRecentResults.addSeparator() def _setupUi(self): self.setWindowTitle(self.app.NAME) self.resize(420, 338) self.centralwidget = QWidget(self) self.verticalLayout = QVBoxLayout(self.centralwidget) self.verticalLayout.setContentsMargins(4, 0, 4, 0) self.verticalLayout.setSpacing(0) hl = QHBoxLayout() label = QLabel(tr("Application Mode:"), self) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) hl.addWidget(label) self.appModeRadioBox = RadioBox( self, items=[tr("Standard"), tr("Music"), tr("Picture")], spread=False) hl.addWidget(self.appModeRadioBox) self.verticalLayout.addLayout(hl) hl = QHBoxLayout() hl.setAlignment(Qt.AlignLeft) label = QLabel(tr("Scan Type:"), self) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) hl.addWidget(label) self.scanTypeComboBox = QComboBox(self) self.scanTypeComboBox.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.scanTypeComboBox.setMaximumWidth(400) hl.addWidget(self.scanTypeComboBox) self.showPreferencesButton = QPushButton(tr("More Options"), self.centralwidget) self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) hl.addWidget(self.showPreferencesButton) self.verticalLayout.addLayout(hl) self.promptLabel = QLabel( tr('Select folders to scan and press "Scan".'), self.centralwidget) self.verticalLayout.addWidget(self.promptLabel) self.treeView = QTreeView(self.centralwidget) self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows) self.treeView.setAcceptDrops(True) triggers = (QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked) self.treeView.setEditTriggers(triggers) self.treeView.setDragDropOverwriteMode(True) self.treeView.setDragDropMode(QAbstractItemView.DropOnly) self.treeView.setUniformRowHeights(True) self.verticalLayout.addWidget(self.treeView) self.horizontalLayout = QHBoxLayout() self.removeFolderButton = QPushButton(self.centralwidget) self.removeFolderButton.setIcon(QIcon(QPixmap(":/minus"))) self.removeFolderButton.setShortcut("Del") self.horizontalLayout.addWidget(self.removeFolderButton) self.addFolderButton = QPushButton(self.centralwidget) self.addFolderButton.setIcon(QIcon(QPixmap(":/plus"))) self.horizontalLayout.addWidget(self.addFolderButton) spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem1) self.loadResultsButton = QPushButton(self.centralwidget) self.loadResultsButton.setText(tr("Load Results")) self.horizontalLayout.addWidget(self.loadResultsButton) self.scanButton = QPushButton(self.centralwidget) self.scanButton.setText(tr("Scan")) self.scanButton.setDefault(True) self.horizontalLayout.addWidget(self.scanButton) self.verticalLayout.addLayout(self.horizontalLayout) self.setCentralWidget(self.centralwidget) self._setupActions() self._setupMenu() if self.app.prefs.directoriesWindowRect is not None: self.setGeometry(self.app.prefs.directoriesWindowRect) else: moveToScreenCenter(self) def _setupColumns(self): header = self.treeView.header() header.setStretchLastSection(False) header.setSectionResizeMode(0, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.Fixed) header.resizeSection(1, 100) def _updateActionsState(self): self.actionShowResultsWindow.setEnabled( self.app.resultWindow is not None) def _updateAddButton(self): if self.recentFolders.isEmpty(): self.addFolderButton.setMenu(None) else: self.addFolderButton.setMenu(self.menuRecentFolders) def _updateRemoveButton(self): indexes = self.treeView.selectedIndexes() if not indexes: self.removeFolderButton.setEnabled(False) return self.removeFolderButton.setEnabled(True) def _updateLoadResultsButton(self): if self.app.recentResults.isEmpty(): self.loadResultsButton.setMenu(None) else: self.loadResultsButton.setMenu(self.menuRecentResults) def _updateScanTypeList(self): try: self.scanTypeComboBox.currentIndexChanged[int].disconnect( self.scanTypeChanged) except TypeError: # Not connected, ignore pass self.scanTypeComboBox.clear() scan_options = self.app.model.SCANNER_CLASS.get_scan_options() for scan_option in scan_options: self.scanTypeComboBox.addItem(scan_option.label) SCAN_TYPE_ORDER = [so.scan_type for so in scan_options] selected_scan_type = self.app.prefs.get_scan_type( self.app.model.app_mode) scan_type_index = SCAN_TYPE_ORDER.index(selected_scan_type) self.scanTypeComboBox.setCurrentIndex(scan_type_index) self.scanTypeComboBox.currentIndexChanged[int].connect( self.scanTypeChanged) self.app._update_options() # --- QWidget overrides def closeEvent(self, event): event.accept() if self.app.model.results.is_modified: title = tr("Unsaved results") msg = tr("You have unsaved results, do you really want to quit?") if not self.app.confirm(title, msg): event.ignore() if event.isAccepted(): self.app.shutdown() # --- Events def addFolderTriggered(self): title = tr("Select a folder to add to the scanning list") flags = QFileDialog.ShowDirsOnly dirpath = str( QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags)) if not dirpath: return self.lastAddedFolder = dirpath self.app.model.add_directory(dirpath) self.recentFolders.insertItem(dirpath) def appModeButtonSelected(self, index): if index == 2: mode = AppMode.Picture elif index == 1: mode = AppMode.Music else: mode = AppMode.Standard self.app.model.app_mode = mode self._updateScanTypeList() def appWillSavePrefs(self): self.app.prefs.directoriesWindowRect = self.geometry() def directoriesModelAddedFolders(self, folders): for folder in folders: self.recentFolders.insertItem(folder) def loadResultsTriggered(self): title = tr("Select a results file to load") files = ";;".join( [tr("dupeGuru Results (*.dupeguru)"), tr("All Files (*.*)")]) destination = QFileDialog.getOpenFileName(self, title, "", files)[0] if destination: self.app.model.load_from(destination) self.app.recentResults.insertItem(destination) def loadDirectoriesTriggered(self): title = tr("Select a directories file to load") files = ";;".join( [tr("dupeGuru Results (*.dupegurudirs)"), tr("All Files (*.*)")]) destination = QFileDialog.getOpenFileName(self, title, "", files)[0] if destination: self.app.model.load_directories(destination) def removeFolderButtonClicked(self): self.directoriesModel.model.remove_selected() def saveDirectoriesTriggered(self): title = tr("Select a file to save your directories to") files = tr("dupeGuru Directories (*.dupegurudirs)") destination, chosen_filter = QFileDialog.getSaveFileName( self, title, "", files) if destination: if not destination.endswith(".dupegurudirs"): destination = "{}.dupegurudirs".format(destination) self.app.model.save_directories_as(destination) def scanButtonClicked(self): if self.app.model.results.is_modified: title = tr("Start a new scan") msg = tr( "You have unsaved results, do you really want to continue?") if not self.app.confirm(title, msg): return self.app.model.start_scanning() def scanTypeChanged(self, index): scan_options = self.app.model.SCANNER_CLASS.get_scan_options() self.app.prefs.set_scan_type(self.app.model.app_mode, scan_options[index].scan_type) self.app._update_options() def selectionChanged(self, selected, deselected): self._updateRemoveButton()
class LogInspectorWindow(QMainWindow): def __init__(self, configFilePath, parent=None): super(LogInspectorWindow, self).__init__(parent) self.initMatPlotLib() self.configFilePath = configFilePath folder = os.path.dirname(self.configFilePath) if not os.path.exists(folder): os.makedirs(folder) if os.path.exists(self.configFilePath): # config.yaml found. Read from file. file = open(self.configFilePath, 'r') self.config = yaml.safe_load(file) file.close() else: # config.yaml not found. Create new file. self.config = {} self.config['logs_directory'] = os.path.join( os.path.expanduser("~"), "Documents", "Inertial_Sense", "Logs") self.config['directory'] = "" self.config['serials'] = ["ALL"] file = open(self.configFilePath, 'w') yaml.dump(self.config, file) file.close() self.currently_selected = 'posNEDMap' self.downsample = 5 self.plotargs = None self.log = None self.plotter = None def initMatPlotLib(self): self.figure = plt.figure() self.canvas = FigureCanvas(self.figure) self.toolbar = NavigationToolbar(self.canvas, self) self.figure.subplots_adjust(left=0.05, right=0.99, bottom=0.05, top=0.91, wspace=0.2, hspace=0.2) def addButton(self, name, function, multithreaded=True, layout=None): setattr(self, name + "button", QPushButton(name)) # if multithreaded: # setattr(self, name+"buttonThread", Thread(target=function)) # getattr(self, name+"button").pressed.connect(self.startLoadingIndicator) # getattr(self, name+"button").released.connect(getattr(self, name+"buttonThread").start) # else: getattr(self, name + "button").clicked.connect(function) # getattr(self, name + "button").setMinimumWidth(220) if layout is None: if self.buttonLayoutRightCol.count( ) < self.buttonLayoutMiddleCol.count(): self.buttonLayoutRightCol.addWidget( getattr(self, name + "button")) elif self.buttonLayoutMiddleCol.count( ) < self.buttonLayoutLeftCol.count(): self.buttonLayoutMiddleCol.addWidget( getattr(self, name + "button")) else: self.buttonLayoutLeftCol.addWidget( getattr(self, name + "button")) else: layout.addWidget(getattr(self, name + "button")) def updatePlot(self): self.plot(self.currently_selected, self.plotargs) def updateWindowTitle(self): if np.shape(self.log.data[0, DID_DEV_INFO])[0] != 0: info = self.log.data[0, DID_DEV_INFO][0] infoStr = 'SN' + str(info[1]) + ', H:' + verArrayToString( info[2]) + ', F:' + verArrayToString( info[3]) + ' build ' + str( info[4]) + ', ' + dateTimeArrayToString( info[8], info[9]) + ', ' + info[10].decode('UTF-8') self.setWindowTitle("LogInspector - " + infoStr) def choose_directory(self): log_dir = config['logs_directory'] directory = QFileDialog.getExistingDirectory( parent=self, caption='Choose Log Directory', directory=log_dir) if directory != '': try: self.load(directory) except Exception as e: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("Unable to load log: " + e.__str__()) msg.setDetailedText(traceback.format_exc()) msg.exec() def load(self, directory): print("loading files from " + directory) # if self.log is None: self.log = Log() self.log.load(directory) print("done loading") self.plotter = logPlot(False, False, 'svg', self.log) self.plotter.setDownSample(self.downsample) # str = '' # if self.log.navMode: # str += 'NAV ' # if self.log.rtk: # str += 'RTK ' # if self.log.compassing: # str += 'Comp ' # self.statusLabel.setText(str) self.updatePlot() self.updateWindowTitle() self.stopLoadingIndicator() def setupUi(self): self.setObjectName("LogInspector") self.setWindowTitle("LogInspector") self.resize(1280, 900) self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMinMaxButtonsHint) self.setWindowIcon(QIcon("assets/Magnifying_glass_icon.png")) # MainWindow.showMaximized() self.createFileTree() self.createButtonColumn() self.formatButtonColumn() self.createBottomToolbar() self.figureLayout = QVBoxLayout() self.figureLayout.addWidget(self.canvas) self.figureLayout.addLayout(self.toolLayout) self.figureLayout.setStretchFactor(self.canvas, 1) layout = QHBoxLayout() layout.addLayout(self.controlLayout) layout.addLayout(self.figureLayout) layout.setStretch(1, 1) widget = QWidget() widget.setLayout(layout) self.setCentralWidget(widget) # self.resize(1280, 900) self.resize(1450, 1000) self.setAcceptDrops(True) def createButtonColumn(self): self.controlLayout = QVBoxLayout() self.buttonLayoutLeftCol = QVBoxLayout() self.buttonLayoutMiddleCol = QVBoxLayout() self.buttonLayoutRightCol = QVBoxLayout() self.addButton('Pos NED Map', lambda: self.plot('posNEDMap')) self.addButton('Pos NED', lambda: self.plot('posNED')) self.addButton('Pos LLA', lambda: self.plot('posLLA')) self.addButton('GPS LLA', lambda: self.plot('llaGps')) self.addButton('Vel NED', lambda: self.plot('velNED')) self.addButton('Vel UVW', lambda: self.plot('velUVW')) self.addButton('Attitude', lambda: self.plot('attitude')) self.addButton('Heading', lambda: self.plot('heading')) self.addButton('INS Status', lambda: self.plot('insStatus')) self.addButton('HDW Status', lambda: self.plot('hdwStatus')) self.addButton('GPS 1 Stats', lambda: self.plot('gpsStats')) self.addButton('GPS 2 Stats', lambda: self.plot('gps2Stats')) self.addButton('RTK Pos Stats', lambda: self.plot('rtkPosStats')) self.addButton('RTK Cmp Stats', lambda: self.plot('rtkCmpStats')) self.addButton('Flash Config', lambda: self.showFlashConfig()) self.addButton('Device Info', lambda: self.showDeviceInfo()) self.addButton('IMU PQR', lambda: self.plot('imuPQR')) self.addButton('IMU Accel', lambda: self.plot('imuAcc')) self.addButton('PSD PQR', lambda: self.plot('gyroPSD')) self.addButton('PSD Accel', lambda: self.plot('accelPSD')) self.addButton('Magnetometer', lambda: self.plot('magnetometer')) self.addButton('Temp', lambda: self.plot('temp')) def formatButtonColumn(self): self.buttonLayoutLeftCol.setAlignment(QtCore.Qt.AlignTop) self.buttonLayoutMiddleCol.setAlignment(QtCore.Qt.AlignTop) self.buttonLayoutRightCol.setAlignment(QtCore.Qt.AlignTop) self.buttonColumnLayout = QHBoxLayout() self.buttonColumnLayout.addLayout(self.buttonLayoutLeftCol) self.buttonColumnLayout.addLayout(self.buttonLayoutMiddleCol) self.buttonColumnLayout.addLayout(self.buttonLayoutRightCol) self.controlLayout.addLayout(self.buttonColumnLayout) self.controlDirLayout = QHBoxLayout() self.controlDirLayout.addWidget(self.dirLineEdit) self.controlLayout.addLayout(self.controlDirLayout) self.controlLayout.addWidget(self.fileTree) # self.buttonLayout.addItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)) # self.addButton('load', self.choose_directory, multithreaded=False) def createBottomToolbar(self): self.toolLayout = QHBoxLayout() self.toolLayout.addWidget(self.toolbar) self.loadingIndictator = QLabel() self.loadingMovie = QMovie('assets/loader.gif') self.emptyLoadingPicture = QPicture() self.emptyLoadingPicture.load('assets/empty_loader.png') self.stopLoadingIndicator() self.toolLayout.addWidget(self.loadingIndictator) self.toolLayout.addItem( QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)) # self.toolLayout.addWidget(QSpacerItem(150, 10, QSizePolicy.Expanding)) self.copyImagePushButton = QPushButton() # self.copyImagePushButton.setText("Copy") # self.copyImagePushButton.setMinimumWidth(1) # self.copyImagePushButton.style().standardIcon(QStyle.SP_DialogOpenButton) self.copyImagePushButton.setIcon(self.style().standardIcon( QStyle.SP_DialogSaveButton)) self.toolLayout.addWidget(self.copyImagePushButton) self.copyImagePushButton.clicked.connect(self.copyPlotToClipboard) downsampleLabel = QLabel() downsampleLabel.setText("DS") self.downSampleInput = QSpinBox() self.downSampleInput.setValue(self.downsample) self.toolLayout.addWidget(downsampleLabel) self.toolLayout.addWidget(self.downSampleInput) self.downSampleInput.valueChanged.connect(self.changeDownSample) self.statusLabel = QLabel() self.toolLayout.addWidget(self.statusLabel) def changeDownSample(self, val): self.downsample = val self.plotter.setDownSample(self.downsample) self.updatePlot() def copyPlotToClipboard(self): # pixmap = QPixmap.grabWidget(self.canvas) # QApplication.clipboard().setPixmap(pixmap) # pixmap.save('test.png') # store the image in a buffer using savefig(), this has the # advantage of applying all the default savefig parameters # such as background color; those would be ignored if you simply # grab the canvas using Qt buf = io.BytesIO() self.figure.savefig(buf) QApplication.clipboard().setImage(QImage.fromData(buf.getvalue())) buf.close() def startLoadingIndicator(self): self.loadingIndictator.setMovie(self.loadingMovie) self.loadingMovie.start() def dragEnterEvent(self, e): if (e.mimeData().hasUrls()): e.acceptProposedAction() def dropEvent(self, e): try: directory = e.mimeData().urls()[0].toLocalFile() self.load(directory) except Exception as e: self.showError(e) def showError(self, e): msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("Unable to load log: " + e.__str__()) msg.setDetailedText(traceback.format_exc()) msg.exec() def createFileTree(self): self.dirModel = QFileSystemModel() self.dirModel.setRootPath(self.config["logs_directory"]) self.dirModel.setFilter(QtCore.QDir.Dirs | QtCore.QDir.NoDotAndDotDot) self.dirLineEdit = QLineEdit() self.dirLineEdit.setText(self.config["logs_directory"]) self.dirLineEdit.setFixedHeight(25) self.dirLineEdit.returnPressed.connect(self.handleTreeDirChange) self.fileTree = QTreeView() self.fileTree.setModel(self.dirModel) self.fileTree.setRootIndex( self.dirModel.index(self.config['logs_directory'])) self.fileTree.setColumnHidden(1, True) self.fileTree.setColumnHidden(2, True) self.fileTree.setColumnHidden(3, True) self.fileTree.setMinimumWidth(300) self.fileTree.clicked.connect(self.handleTreeViewClick) self.fileTree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.fileTree.setSelectionMode(QAbstractItemView.SingleSelection) self.fileTree.customContextMenuRequested.connect( self.handleTreeViewRightClick) # self.populateRMSCheck(self.config['logs_directory']) def updateFileTree(self): self.dirModel.setRootPath(self.config["logs_directory"]) self.fileTree.setRootIndex( self.dirModel.index(self.config['logs_directory'])) def populateRMSCheck(self, directory): for subdir in os.listdir(directory): path = os.path.join(directory, subdir) if os.path.isdir(path): self.populateRMSCheck(path) elif 'RMS' in subdir: f = open(path) rms_report = f.read() p = re.compile(r'(?<=^PASS/FAIL).*\n', re.M) line = re.search(p, rms_report).group() failed = True if "FAIL" in line else False if failed: pass else: pass def handleTreeDirChange(self): self.config["logs_directory"] = self.dirLineEdit.text() self.updateFileTree() file = open(self.configFilePath, 'w') yaml.dump(self.config, file) file.close() def handleTreeViewClick(self): selected_directory = self.fileTree.model().filePath( self.fileTree.selectedIndexes()[0]) for fname in os.listdir(selected_directory): if fname.endswith('.dat'): try: self.load(selected_directory) except Exception as e: self.showError(e) break def handleTreeViewRightClick(self, event): selected_directory = os.path.normpath(self.fileTree.model().filePath( self.fileTree.selectedIndexes()[0])) menu = QMenu(self) copyAction = menu.addAction("Copy path") nppActionHot = menu.addAction("Run NPP, HOT start") nppActionCold = menu.addAction("Run NPP, COLD start") nppActionFactory = menu.addAction("Run NPP, FACTORY start") setDataInfoDirHotAction = menu.addAction( "Set dataInfo.json directory, HOT start") setDataInfoDirColdAction = menu.addAction( "Set dataInfo.json directory, COLD start") setDataInfoDirFactoryAction = menu.addAction( "Set dataInfo.json directory, FACTORY start") exploreAction = menu.addAction("Explore folder") cleanFolderAction = menu.addAction("Clean folder") deleteFolderAction = menu.addAction("Delete folder") action = menu.exec_(self.fileTree.viewport().mapToGlobal(event)) if action == copyAction: cb = QApplication.clipboard() cb.clear(mode=cb.Clipboard) cb.setText(selected_directory, mode=cb.Clipboard) if action == nppActionHot: cleanFolder(selected_directory) setDataInformationDirectory(selected_directory, startMode=START_MODE_HOT) sys.path.insert(1, '../../../../python/src') from supernpp.supernpp import SuperNPP spp = SuperNPP(selected_directory, self.config['serials']) spp.run() if action == nppActionCold: cleanFolder(selected_directory) setDataInformationDirectory(selected_directory, startMode=START_MODE_COLD) sys.path.insert(1, '../../../../python/src') from supernpp.supernpp import SuperNPP spp = SuperNPP(selected_directory, self.config['serials'], startMode=START_MODE_COLD) spp.run() if action == nppActionFactory: cleanFolder(selected_directory) setDataInformationDirectory(selected_directory, startMode=START_MODE_FACTORY) sys.path.insert(1, '../../../../python/src') from supernpp.supernpp import SuperNPP spp = SuperNPP(selected_directory, self.config['serials'], startMode=START_MODE_FACTORY) spp.run() if action == setDataInfoDirHotAction: setDataInformationDirectory(selected_directory, startMode=START_MODE_HOT) if action == setDataInfoDirColdAction: setDataInformationDirectory(selected_directory, startMode=START_MODE_COLD) if action == setDataInfoDirFactoryAction: setDataInformationDirectory(selected_directory, startMode=START_MODE_FACTORY) if action == exploreAction: openFolderWithFileBrowser(selected_directory) if action == cleanFolderAction: cleanFolder(selected_directory) if action == deleteFolderAction: msg = QMessageBox(self) msg.setIcon(QMessageBox.Question) msg.setText("Are you sure you want to delete this folder?\n\n" + selected_directory) msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) result = msg.exec() if result == QMessageBox.Yes: removeDirectory(selected_directory) def stopLoadingIndicator(self): self.loadingMovie.stop() self.loadingIndictator.clear() self.loadingIndictator.setPicture(self.emptyLoadingPicture) def showDeviceInfo(self): dlg = DeviceInfoDialog(self.log, self) dlg.show() dlg.exec_() def showFlashConfig(self): dlg = FlashConfigDialog(self.log, self) dlg.show() dlg.exec_() def plot(self, func, args=None): print("plotting " + func) self.currently_selected = func self.plotargs = args self.figure.clear() if hasattr(self, 'plotter'): if args is not None: getattr(self.plotter, func)(*args, self.figure) else: getattr(self.plotter, func)(self.figure) self.canvas.draw() self.stopLoadingIndicator() print("done plotting")
class DirectoriesDialog(QMainWindow): def __init__(self, app, **kwargs): super().__init__(None, **kwargs) self.app = app self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS self.recentFolders = Recent(self.app, 'recentFolders') self._setupUi() self._updateScanTypeList() self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView) self.directoriesDelegate = DirectoriesDelegate() self.treeView.setItemDelegate(self.directoriesDelegate) self._setupColumns() self.app.recentResults.addMenu(self.menuLoadRecent) self.app.recentResults.addMenu(self.menuRecentResults) self.recentFolders.addMenu(self.menuRecentFolders) self._updateAddButton() self._updateRemoveButton() self._updateLoadResultsButton() self._updateActionsState() self._setupBindings() def _setupBindings(self): self.appModeRadioBox.itemSelected.connect(self.appModeButtonSelected) self.showPreferencesButton.clicked.connect(self.app.actionPreferences.trigger) self.scanButton.clicked.connect(self.scanButtonClicked) self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger) self.addFolderButton.clicked.connect(self.actionAddFolder.trigger) self.removeFolderButton.clicked.connect(self.removeFolderButtonClicked) self.treeView.selectionModel().selectionChanged.connect(self.selectionChanged) self.app.recentResults.itemsChanged.connect(self._updateLoadResultsButton) self.recentFolders.itemsChanged.connect(self._updateAddButton) self.recentFolders.mustOpenItem.connect(self.app.model.add_directory) self.directoriesModel.foldersAdded.connect(self.directoriesModelAddedFolders) self.app.willSavePrefs.connect(self.appWillSavePrefs) def _setupActions(self): # (name, shortcut, icon, desc, func) ACTIONS = [ ('actionLoadResults', 'Ctrl+L', '', tr("Load Results..."), self.loadResultsTriggered), ('actionShowResultsWindow', '', '', tr("Results Window"), self.app.showResultsWindow), ('actionAddFolder', '', '', tr("Add Folder..."), self.addFolderTriggered), ] createActions(ACTIONS, self) def _setupMenu(self): self.menubar = QMenuBar(self) self.menubar.setGeometry(QRect(0, 0, 42, 22)) self.menuFile = QMenu(self.menubar) self.menuFile.setTitle(tr("File")) self.menuView = QMenu(self.menubar) self.menuView.setTitle(tr("View")) self.menuHelp = QMenu(self.menubar) self.menuHelp.setTitle(tr("Help")) self.menuLoadRecent = QMenu(self.menuFile) self.menuLoadRecent.setTitle(tr("Load Recent Results")) self.setMenuBar(self.menubar) self.menuFile.addAction(self.actionLoadResults) self.menuFile.addAction(self.menuLoadRecent.menuAction()) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionClearPictureCache) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionQuit) self.menuView.addAction(self.app.actionPreferences) self.menuView.addAction(self.actionShowResultsWindow) self.menuView.addAction(self.app.actionIgnoreList) self.menuHelp.addAction(self.app.actionShowHelp) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuView.menuAction()) self.menubar.addAction(self.menuHelp.menuAction()) # Recent folders menu self.menuRecentFolders = QMenu() self.menuRecentFolders.addAction(self.actionAddFolder) self.menuRecentFolders.addSeparator() # Recent results menu self.menuRecentResults = QMenu() self.menuRecentResults.addAction(self.actionLoadResults) self.menuRecentResults.addSeparator() def _setupUi(self): self.setWindowTitle(self.app.NAME) self.resize(420, 338) self.centralwidget = QWidget(self) self.verticalLayout = QVBoxLayout(self.centralwidget) hl = QHBoxLayout() label = QLabel(tr("Application Mode:"), self) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) hl.addWidget(label) self.appModeRadioBox = RadioBox( self, items=[tr("Standard"), tr("Music"), tr("Picture")], spread=False ) hl.addWidget(self.appModeRadioBox) self.verticalLayout.addLayout(hl) hl = QHBoxLayout() hl.setAlignment(Qt.AlignLeft) label = QLabel(tr("Scan Type:"), self) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) hl.addWidget(label) self.scanTypeComboBox = QComboBox(self) self.scanTypeComboBox.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.scanTypeComboBox.setMaximumWidth(400) hl.addWidget(self.scanTypeComboBox) self.showPreferencesButton = QPushButton(tr("More Options"), self.centralwidget) self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) hl.addWidget(self.showPreferencesButton) self.verticalLayout.addLayout(hl) self.promptLabel = QLabel(tr("Select folders to scan and press \"Scan\"."), self.centralwidget) self.verticalLayout.addWidget(self.promptLabel) self.treeView = QTreeView(self.centralwidget) self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows) self.treeView.setAcceptDrops(True) triggers = QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed\ | QAbstractItemView.SelectedClicked self.treeView.setEditTriggers(triggers) self.treeView.setDragDropOverwriteMode(True) self.treeView.setDragDropMode(QAbstractItemView.DropOnly) self.treeView.setUniformRowHeights(True) self.verticalLayout.addWidget(self.treeView) self.horizontalLayout = QHBoxLayout() self.removeFolderButton = QPushButton(self.centralwidget) self.removeFolderButton.setIcon(QIcon(QPixmap(":/minus"))) self.removeFolderButton.setShortcut("Del") self.horizontalLayout.addWidget(self.removeFolderButton) self.addFolderButton = QPushButton(self.centralwidget) self.addFolderButton.setIcon(QIcon(QPixmap(":/plus"))) self.horizontalLayout.addWidget(self.addFolderButton) spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem1) self.loadResultsButton = QPushButton(self.centralwidget) self.loadResultsButton.setText(tr("Load Results")) self.horizontalLayout.addWidget(self.loadResultsButton) self.scanButton = QPushButton(self.centralwidget) self.scanButton.setText(tr("Scan")) self.scanButton.setDefault(True) self.horizontalLayout.addWidget(self.scanButton) self.verticalLayout.addLayout(self.horizontalLayout) self.setCentralWidget(self.centralwidget) self._setupActions() self._setupMenu() if self.app.prefs.directoriesWindowRect is not None: self.setGeometry(self.app.prefs.directoriesWindowRect) else: moveToScreenCenter(self) def _setupColumns(self): header = self.treeView.header() header.setStretchLastSection(False) header.setSectionResizeMode(0, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.Fixed) header.resizeSection(1, 100) def _updateActionsState(self): self.actionShowResultsWindow.setEnabled(self.app.resultWindow is not None) def _updateAddButton(self): if self.recentFolders.isEmpty(): self.addFolderButton.setMenu(None) else: self.addFolderButton.setMenu(self.menuRecentFolders) def _updateRemoveButton(self): indexes = self.treeView.selectedIndexes() if not indexes: self.removeFolderButton.setEnabled(False) return self.removeFolderButton.setEnabled(True) def _updateLoadResultsButton(self): if self.app.recentResults.isEmpty(): self.loadResultsButton.setMenu(None) else: self.loadResultsButton.setMenu(self.menuRecentResults) def _updateScanTypeList(self): try: self.scanTypeComboBox.currentIndexChanged[int].disconnect(self.scanTypeChanged) except TypeError: # Not connected, ignore pass self.scanTypeComboBox.clear() scan_options = self.app.model.SCANNER_CLASS.get_scan_options() for scan_option in scan_options: self.scanTypeComboBox.addItem(scan_option.label) SCAN_TYPE_ORDER = [so.scan_type for so in scan_options] selected_scan_type = self.app.prefs.get_scan_type(self.app.model.app_mode) scan_type_index = SCAN_TYPE_ORDER.index(selected_scan_type) self.scanTypeComboBox.setCurrentIndex(scan_type_index) self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged) self.app._update_options() #--- QWidget overrides def closeEvent(self, event): event.accept() if self.app.model.results.is_modified: title = tr("Unsaved results") msg = tr("You have unsaved results, do you really want to quit?") if not self.app.confirm(title, msg): event.ignore() if event.isAccepted(): QApplication.quit() #--- Events def addFolderTriggered(self): title = tr("Select a folder to add to the scanning list") flags = QFileDialog.ShowDirsOnly dirpath = str(QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags)) if not dirpath: return self.lastAddedFolder = dirpath self.app.model.add_directory(dirpath) self.recentFolders.insertItem(dirpath) def appModeButtonSelected(self, index): if index == 2: mode = AppMode.Picture elif index == 1: mode = AppMode.Music else: mode = AppMode.Standard self.app.model.app_mode = mode self._updateScanTypeList() def appWillSavePrefs(self): self.app.prefs.directoriesWindowRect = self.geometry() def directoriesModelAddedFolders(self, folders): for folder in folders: self.recentFolders.insertItem(folder) def loadResultsTriggered(self): title = tr("Select a results file to load") files = ';;'.join([tr("dupeGuru Results (*.dupeguru)"), tr("All Files (*.*)")]) destination = QFileDialog.getOpenFileName(self, title, '', files) if destination: self.app.model.load_from(destination) self.app.recentResults.insertItem(destination) def removeFolderButtonClicked(self): self.directoriesModel.model.remove_selected() def scanButtonClicked(self): if self.app.model.results.is_modified: title = tr("Start a new scan") msg = tr("You have unsaved results, do you really want to continue?") if not self.app.confirm(title, msg): return self.app.model.start_scanning() def scanTypeChanged(self, index): scan_options = self.app.model.SCANNER_CLASS.get_scan_options() self.app.prefs.set_scan_type(self.app.model.app_mode, scan_options[index].scan_type) self.app._update_options() def selectionChanged(self, selected, deselected): self._updateRemoveButton()
class Navigation(QWidget): """ Navigation class definition. Provide a combobox to switch on each opened directories and display it into a tree view Provide 2 useful function (to use in alter module): - add_action(name, shortcut, callback) - callback take 2 arguments : file_info and parent - add_separator() """ SETTINGS_DIRECTORIES = 'navigation_dirs' SETTINGS_CURRENT_DIR = 'navigation_current_dir' onFileItemActivated = pyqtSignal(QFileInfo, name="onFileItemActivated") onDirItemActivated = pyqtSignal(QFileInfo, name="onDirItemActivated") def __init__(self, parent=None): super(Navigation, self).__init__(parent) self.setObjectName("Navigation") self.layout = QVBoxLayout(self) self.layout.setSpacing(0) self.layout.setContentsMargins(0,0,0,0) self.menu_button = QPushButton('Select directory', self) self.menu_button.setFlat(True) # self.menu_button.clicked.connect(self.on_menu_button_clicked) self.menu = QMenu(self) self.menu_button.setMenu(self.menu) self.menu_directories = QMenu(self) self.menu_directories.setTitle('Directories') self.menu_add_action( 'Open directory', self.open_directory, None, QKeySequence.Open) self.menu_add_separator() self.menu_add_action('Refresh', self.reset, None, QKeySequence.Refresh) # @TODO invoke_all self.menu_add_separator() self.menu.addMenu(self.menu_directories) self.tree = QTreeView(self) self.model = FileSystemModel(self) self.tree.setModel(self.model) self.tree.setColumnHidden(1, True) self.tree.setColumnHidden(2, True) self.tree.setColumnHidden(3, True) self.tree.setHeaderHidden(True) # only to expand directory or activated with one click self.tree.clicked.connect(self.on_item_clicked) # else, for file use activated signal self.tree.activated.connect(self.on_item_activated) self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self.on_context_menu) self.widgets = collections.OrderedDict() self.widgets['menu_button'] = self.menu_button self.widgets['tree'] = self.tree # @ToDo: Alter.invoke_all('add_widget', self.widgets) for name, widget in self.widgets.items(): if name == 'menu_button': self.layout.addWidget(widget, 0, Qt.AlignLeft) else: self.layout.addWidget(widget) self.context_menu = QMenu(self) self.add_action('New file', QKeySequence.New, FileSystemHelper.new_file) self.add_action('New Directory', '', FileSystemHelper.new_directory) self.add_separator() self.add_action('Rename', '', FileSystemHelper.rename) self.add_action('Copy', QKeySequence.Copy, FileSystemHelper.copy) self.add_action('Cut', QKeySequence.Cut, FileSystemHelper.cut) self.add_action('Paste', QKeySequence.Paste, FileSystemHelper.paste) self.add_separator() self.add_action('Delete', QKeySequence.Delete, FileSystemHelper.delete) # @ToDo Alter.invoke_all('navigation_add_action', self) #restore previous session and data dirs = ModuleManager.core['settings'].Settings.value( self.SETTINGS_DIRECTORIES, None, True) for directory_path in dirs: name = os.path.basename(directory_path) self.menu_add_directory(name, directory_path) current_dir = ModuleManager.core['settings'].Settings.value( self.SETTINGS_CURRENT_DIR, '') if current_dir: for action in self.menu_directories.actions(): if action.data() == current_dir: action.trigger() self.menu_button.setFocusPolicy(Qt.NoFocus) self.menu_button.setFocusProxy(self.tree) def reset(self, file_info): self.model.beginResetModel() current_dir = ModuleManager.core['settings'].Settings.value( self.SETTINGS_CURRENT_DIR, '') if current_dir: for action in self.menu_directories.actions(): if action.data() == current_dir: action.trigger() def on_menu_button_clicked(self): pos = self.mapToGlobal(self.menu_button.pos()) menu_width = self.menu.sizeHint().width() pos.setY(pos.y() + self.menu_button.height()) # pos.setX(pos.x() + self.menu_button.width() - menu_width) if len(self.menu.actions()) > 0: self.menu.exec(pos) def menu_add_action(self, name, callback, data=None, shortcut=None, icon=None): action = QAction(name, self) if icon: action.setIcon(icon) if shortcut: action.setShortcut(shortcut) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) if data: action.setData(data) action.triggered.connect(callback) self.addAction(action) self.menu.addAction(action) def menu_add_directory(self, name, data): action = QAction(name, self) action.setData(data) action.triggered.connect(self.on_menu_action_triggered) self.menu_directories.addAction(action) return action def menu_add_separator(self): self.menu.addSeparator() def add_action(self, name, shortcut, callback, icon = None): """ Ajoute une action au context menu et au widget navigation lui même. Créer une fonction à la volé pour fournir des arguments aux fonctions associé aux actions. """ action = QAction(name, self) if icon: action.setIcon(icon) action.setShortcut(shortcut) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.__wrapper(callback)) self.addAction(action) self.context_menu.addAction(action) def add_separator(self): """Simple abstraction of self.context_menu.addSeparator()""" self.context_menu.addSeparator() def __wrapper(self, callback): def __new_function(): """ __new_function représente la forme de tous les callbacks connecté à une action pour pouvoir utiliser les raccourcis en même temps que le menu contextuel. """ action = self.sender() file_info = action.data() if not file_info: indexes = self.tree.selectedIndexes() if indexes: model_index = indexes[0] file_info = self.model.fileInfo(model_index) callback(file_info, self) elif action.shortcut() == QKeySequence.New: file_info = self.model.fileInfo(self.tree.rootIndex()) callback(file_info, self) else: callback(file_info, self) action.setData(None) return __new_function def question(self, text, informative_text = None): message_box = QMessageBox(self) message_box.setText(text) if informative_text: message_box.setInformativeText(informative_text) message_box.setStandardButtons( QMessageBox.No | QMessageBox.Yes) message_box.setDefaultButton(QMessageBox.No) return message_box.exec() def on_context_menu(self, point): model_index = self.tree.indexAt(point) file_info = self.model.fileInfo(model_index) # pour chaque action on met a jour les data (file_info) # puis on altère les actions (ex enabled) for action in self.context_menu.actions(): if not action.isSeparator(): action.setData(file_info) action.setEnabled(model_index.isValid()) if action.shortcut() == QKeySequence.New: action.setEnabled(True) if not model_index.isValid(): file_info = self.model.fileInfo(self.tree.rootIndex()) action.setData(file_info) if action.shortcut() == QKeySequence.Paste: enable = FileSystemHelper.ready() and model_index.isValid() action.setEnabled(enable) if action.shortcut() == QKeySequence.Delete: # remove directory only if is an empty directory if model_index.isValid() and file_info.isDir(): path = file_info.absoluteFilePath() # QDir(path).count() always contains '.' and '..' action.setEnabled(QDir(path).count() == 2) # @ToDo #Alter.invoke_all( # 'navigation_on_menu_action', # model_index, file_info, action, self) if len(self.context_menu.actions()) > 0: self.context_menu.exec(self.tree.mapToGlobal(point)) # reset action data, sinon y a des problèmes dans _new_function for action in self.context_menu.actions(): action.setData(None) def on_item_activated(self, index): qFileInfo = self.model.fileInfo(index) if qFileInfo.isDir(): self.onDirItemActivated.emit(qFileInfo) else: self.onFileItemActivated.emit(qFileInfo) def on_item_clicked(self, index): qFileInfo = self.model.fileInfo(index) if qFileInfo.isDir(): self.onDirItemActivated.emit(qFileInfo) self.tree.setExpanded(index, not self.tree.isExpanded(index)) else: self.onFileItemActivated.emit(qFileInfo) def open_directory(self): project = ModuleManager.core['settings'].Settings.value( self.SETTINGS_CURRENT_DIR, '') path = QFileDialog.getExistingDirectory(self, "Open Directory", project) if path: name = os.path.basename(path) action = self.menu_add_directory(name, path) self.save_directories_path() action.trigger() def on_menu_action_triggered(self): action = self.sender() path = action.data() if path: self.model.setRootPath(path) self.tree.setRootIndex(self.model.index(path)) self.menu_button.setText(os.path.basename(path)) self.save_current_dir(path) def save_directories_path(self): ModuleManager.core['settings'].Settings.set_value( self.SETTINGS_DIRECTORIES, [action.data() for action in self.menu_directories.actions()] ) def save_current_dir(self, path): ModuleManager.core['settings'].Settings.set_value( self.SETTINGS_CURRENT_DIR, path )
class PYCFScape(QMainWindow): def __init__(self): super().__init__() # We determine where the script is placed, for acessing files we use (such as the icon) self.we_live_in = sys.argv[0] self.we_live_in = os.path.split(self.we_live_in)[0] self.build_interface() self.opened_file = None # stores the filepath self.opened_vpk = None # stores the opened vpk self.internal_directory_understanding = { } # a dictionary version of the paths self.vpk_loaded = False # whether or not we have a vpk file open self.export_paths = [ ] # Paths we want to export when file->export is selected def build_interface(self): self.setWindowTitle("PYCFScape") self.setWindowIcon( QIcon(os.path.join(self.we_live_in, 'res/Icon64.ico'))) self.main_content = QWidget() self.main_content_layout = QVBoxLayout() self.main_content.setLayout(self.main_content_layout) # set up the interface parts self.output_console = QTextEdit() # Will basically just copy stdout self.output_console.setReadOnly(True) sys.stdout = bob_logger(self.output_console, False, sys.stdout) sys.stderr = bob_logger(self.output_console, True, sys.stderr) self.vpk_tree = QTreeView() # Displays the tree of the vpk's content self.vpk_tree_model = QStandardItemModel(self.vpk_tree) self.vpk_tree.setModel(self.vpk_tree_model) self.vpk_tree._mousePressEvent = self.vpk_tree.mousePressEvent # store it so we can call it self.vpk_tree.mousePressEvent = self.vpk_tree_item_clicked self.vpk_tree.setHeaderHidden(True) # We use a QTreeView to also display headers. # We however, still treat it as a list view. self.dir_list = QTreeView( ) # Displays the list of the current vpk's directory's content self.dir_list_model = QStandardItemModel(self.dir_list) self.dir_list.setModel(self.dir_list_model) self.dir_list.doubleClicked.connect(self.dir_list_item_double_clicked) self.dir_list_model.itemChanged.connect(self.dir_list_item_updated) self.dir_list.setContextMenuPolicy(Qt.CustomContextMenu) self.dir_list.customContextMenuRequested.connect( self.dir_list_context_menu) self.dir_list_model.setColumnCount(2) self.dir_list_model.setHorizontalHeaderLabels(["Name", "Type"]) self.dir_list.header().resizeSection(0, 250) # The tool bar - WARNING: MESSY CODE! self.actions_toolbar = self.addToolBar("") # OPEN BUTTON self.open_button = self.actions_toolbar.addAction( QIcon.fromTheme("document-open"), "Open File") self.open_button.triggered.connect(self.open_file) self.actions_toolbar.addSeparator() self.back_button = QToolButton() # GO BACK BUTTON self.back_button.setIcon(QIcon.fromTheme("go-previous")) self.back_button.setDisabled(True) self.actions_toolbar.addWidget(self.back_button) self.forward_button = QToolButton() # GO FORWARD BUTTON self.forward_button.setIcon(QIcon.fromTheme("go-next")) self.forward_button.setDisabled(True) self.actions_toolbar.addWidget(self.forward_button) self.up_button = QToolButton() # GO UP BUTTON self.up_button.setIcon(QIcon.fromTheme("go-up")) self.up_button.setDisabled(True) self.actions_toolbar.addWidget(self.up_button) self.actions_toolbar.addSeparator() # FIND BUTTON self.search_button = self.actions_toolbar.addAction( QIcon.fromTheme("system-search"), "Find in file") self.search_button.setDisabled(True) self.search_button.triggered.connect(self.search_for_file) self.actions_toolbar.addSeparator() # EXPORT BUTTON self.export_button = self.actions_toolbar.addAction( QIcon.fromTheme("extract-archive"), "Export Selection") self.export_button.setDisabled(False) self.export_button.triggered.connect(self.export_selection) self.main_content_layout.addWidget(self.actions_toolbar) # now we want the menubar self.menu_bar = self.menuBar() self.file_menu = self.menu_bar.addMenu("&File") self.edit_menu = self.menu_bar.addMenu("&Edit") self.menu_bar.addSeparator() self.help_menu = self.menu_bar.addMenu("&Help") self.file_menu.addActions([self.open_button]) self.close_button = self.file_menu.addAction( QIcon.fromTheme("document-close"), "Close File" # bit redundant, i actually see no use ) self.close_button.triggered.connect(self.close_vpk) self.file_menu.addSeparator() self.file_menu.addAction(QIcon.fromTheme("application-exit"), "Exit").triggered.connect(self.close) self.edit_menu.addActions([self.search_button]) self.help_menu.addAction(QIcon.fromTheme("help-about"), "About && License").triggered.connect( self.about) # the statusbar self.status_bar = self.statusBar() # set up the splitters # horizontal self.horz_splitter_container = QWidget() self.horz_splitter_container_layout = QVBoxLayout() self.horz_splitter_container.setLayout( self.horz_splitter_container_layout) self.horz_splitter = QSplitter(Qt.Horizontal) self.horz_splitter.addWidget(self.vpk_tree) self.horz_splitter.addWidget(self.dir_list) self.horz_splitter.setSizes([50, 200]) self.horz_splitter_container_layout.addWidget(self.horz_splitter) # vertical self.vert_splitter = QSplitter(Qt.Vertical) self.vert_splitter.addWidget(self.horz_splitter_container) self.vert_splitter.addWidget(self.output_console) self.vert_splitter.setSizes([200, 50]) self.main_content_layout.addWidget(self.vert_splitter) self.setCentralWidget(self.main_content) self.show() ## # Update Functions ## def update_console(self, text, is_stdout): colour = Qt.Red if not is_stdout else Qt.black self.output_console.setTextColor(colour) self.output_console.moveCursor(QTextCursor.End) self.output_console.insertPlainText(text) def update_interface(self): # update the tree view self.update_vpk_tree() # update the list view self.update_dir_list() self.search_button.setDisabled(not self.vpk_loaded) def update_vpk_tree(self): self.vpk_tree_model.removeRows(0, self.vpk_tree_model.rowCount()) if self.opened_vpk: self.tree_magic(self.internal_directory_understanding) def update_dir_list(self): self.dir_list_model.removeRows(0, self.dir_list_model.rowCount( )) # remove anyway (for instances such as opening a new file) selected = self.vpk_tree.selectedIndexes() if not selected: return selected = selected[0] selected_item = self.vpk_tree_model.itemFromIndex(selected) if not selected_item: return if not selected_item.is_dir: return path = selected_item.path understanding = self.get_understanding_from( self.internal_directory_understanding, path) self.list_magic(understanding, path) ## # Events ## def vpk_tree_item_clicked(self, event): self.vpk_tree._mousePressEvent(event) index = self.vpk_tree.indexAt(event.pos()) # We can rather safely assume any items in the vpk tree will have OUR special attributes if index.isValid(): item = self.vpk_tree_model.itemFromIndex(index) print("selected", item.path) if item.is_dir: self.update_dir_list() def dir_list_item_double_clicked(self, index): item = self.dir_list_model.itemFromIndex(index) if not item.column() == 0: return print("double clicked", item.path) if item.is_dir: print("is dir") # this is probably a REALLY **REALLY** awful way of doing this, but you're welcome to PR a better way. :) index_in_tree = self.find_in_model(self.vpk_tree_model, item.path) if index_in_tree.isValid(): self.vpk_tree.setCurrentIndex(index_in_tree) self.update_dir_list() else: self.status_bar.showMessage(MSG_EXPORT) # we clearly wanna export the file and open it bits = self.opened_vpk[item.path[1:]].read() path = compat.write_to_temp(bits, os.path.split(item.path)[1]) print("Wrote to", path) compat.tell_os_open(path) self.status_bar.clearMessage() def dir_list_item_updated(self, item): if item.checkState() == Qt.Checked: if not item.is_dir: self.export_paths.append(item.path) else: index_in_tree = self.find_in_model(self.vpk_tree_model, item.path) if index_in_tree.isValid(): paths = self.recursively_get_paths_from_dir_index_item( index_in_tree, self.vpk_tree_model) self.export_paths += paths elif item.checkState() == Qt.Unchecked: if not item.is_dir: if item.path in self.export_paths: self.export_paths.remove(item.path) else: index_in_tree = self.find_in_model(self.vpk_tree_model, item.path) if index_in_tree.isValid(): paths = self.recursively_get_paths_from_dir_index_item( index_in_tree, self.vpk_tree_model) for path in paths: if path in self.export_paths: self.export_paths.remove(path) ## # The next 3 functions are the original pathtodir but merged with the program ## def nested_dict(self): return defaultdict(self.nested_dict) def nested_dict_to_regular(self, d): if not isinstance(d, defaultdict): return d return {k: self.nested_dict_to_regular(v) for k, v in d.items()} def understand_directories(self, list_of_paths): use_dict = defaultdict(self.nested_dict) for path in list_of_paths: parts = path.split('/') if parts: marcher = use_dict for key in parts[:-1]: marcher = marcher[key] marcher[parts[-1]] = parts[-1] return dict(use_dict) def get_understanding_from(self, understanding, path): keys = path.split('/') if keys[0] == '': keys = keys[1:] if keys: marcher = understanding for key in keys: marcher = marcher[key] # we can now assume marcher is the understanding we want return marcher ## # Utility ## def tree_magic(self, dict_of_things, parent=None, root=''): if not parent: parent = self.vpk_tree_model # Stack overflow 14478170 for thing in sorted(dict_of_things, key=lambda f: os.path.splitext(f)): path = root + '/{}'.format(thing) thing_item = QStandardItem() thing_item.setText(thing) thing_item.setEditable(False) thing_item.path = path thing_item.is_dir = False icon, _ = self.get_info_from_path(path) thing_item.setIcon(icon) if isinstance(dict_of_things[thing], dict): thing_item.setIcon(QIcon.fromTheme("folder")) self.tree_magic(dict_of_things[thing], thing_item, path) thing_item.is_dir = True parent.appendRow(thing_item) def list_magic(self, dict_of_things, root=''): # like tree_magic but operates on dir_list for thing in sorted(dict_of_things, key=lambda f: os.path.splitext(f)): path = root + '/{}'.format(thing) thing_item = QStandardItem() thing_item.setText(thing) thing_item.setEditable(False) thing_item.setCheckable(True) thing_item_type = QStandardItem( ) # This doesn't actually do anything but convey more information to the user thing_item_type.setEditable(False) if path in self.export_paths: thing_item.setCheckState(Qt.Checked) thing_item.path = path thing_item.is_dir = False icon, desc = self.get_info_from_path(path) thing_item.setIcon(icon) thing_item_type.setText(desc) if isinstance(dict_of_things[thing], dict): thing_item.setIcon(QIcon.fromTheme("folder")) thing_item.is_dir = True thing_item_type.setText("Directory") self.dir_list_model.appendRow([thing_item, thing_item_type]) def get_info_from_path(self, path): # returns the icon AND description string icon = None desc = None # first we test against mimetype # probably bad code, but it works! thing_mimetype = mimetypes.guess_type(path)[0] if thing_mimetype: if thing_mimetype[:6] == "audio/": icon = QIcon.fromTheme("audio-x-generic") elif thing_mimetype[:12] == "application/": icon = QIcon.fromTheme("application-x-generic") elif thing_mimetype[:5] == "text/": icon = QIcon.fromTheme("text-x-generic") elif thing_mimetype[:6] == "image/": icon = QIcon.fromTheme("image-x-generic") elif thing_mimetype[:6] == "video/": icon = QIcon.fromTheme("video-x-generic") desc = thing_mimetype # well ok, maybe that didn't work, let's test the filepath ourselves. file_ext = os.path.splitext(path)[1] if file_ext: if file_ext in [".vtf"]: icon = QIcon.fromTheme("image-x-generic") desc = "Valve Texture File" elif file_ext in [".vmt"]: icon = QIcon.fromTheme("text-x-generic") desc = "Valve Material File" elif file_ext in [ ".pcf" ]: # we can safely assume they are not fonts in this context, but rather icon = QIcon.fromTheme("text-x-script") desc = "Valve DMX Implementation" # TODO: is there a better name elif file_ext in [".bsp"]: icon = QIcon.fromTheme("text-x-generic") desc = "Binary Space Partition" elif file_ext in [".res"]: icon = QIcon.fromTheme("text-x-generic") desc = "Valve Key Value" elif file_ext in [".vcd"]: icon = QIcon.fromTheme("text-x-generic") desc = "Valve Choreography Data" if not icon: # If all else fails, display SOMETHING icon = QIcon.fromTheme("text-x-generic") if not desc: desc = "File" return icon, desc def find_in_model(self, model: QStandardItemModel, path): for i in range(0, model.rowCount()): index_in_tree = model.index(i, 0) if model.itemFromIndex(index_in_tree).path == path: return index_in_tree if model.itemFromIndex(index_in_tree).is_dir: index_in_tree = self.find_in_model_parent(model, path, parent=index_in_tree) if not index_in_tree.isValid(): continue if model.itemFromIndex(index_in_tree).path == path: return index_in_tree def find_in_model_parent(self, model: QStandardItemModel, path, parent): for i in range(0, model.rowCount(parent)): index_in_tree = model.index(i, 0, parent) if model.itemFromIndex(index_in_tree).path == path: return index_in_tree if model.itemFromIndex(index_in_tree).is_dir: index_in_tree = self.find_in_model_parent(model, path, parent=index_in_tree) if not index_in_tree.isValid(): continue if model.itemFromIndex(index_in_tree).path == path: return index_in_tree return QModelIndex() def export_file(self, path, out_dir): filepath = os.path.split(path)[0] if not os.path.isdir('{}{}'.format(out_dir, filepath)): os.makedirs('{}{}'.format(out_dir, filepath)) print("Attempting to export to", "{}{}".format(out_dir, filepath), "from", path[1:], "in the vpk") outcontents = self.opened_vpk.get_file(path[1:]).read() outfile = open('{}{}'.format(out_dir, path), 'wb') outfile.write(outcontents) outfile.close() def recursively_get_paths_from_dir_index_item(self, index_in_tree, model): paths = [] for i in range( self.vpk_tree_model.itemFromIndex(index_in_tree).rowCount()): index = self.vpk_tree_model.index(i, 0, index_in_tree) index_item = self.vpk_tree_model.itemFromIndex(index) if not index_item.is_dir: paths.append(index_item.path) else: paths += self.recursively_get_paths_from_dir_index_item( index, model) return paths def close_vpk(self): # We trash everything! self.vpk_loaded = False self.opened_file = None self.opened_vpk = {} self.export_paths = [] self.internal_directory_understanding = {} self.status_bar.showMessage(MSG_UPDATE_UI) self.update_interface() self.status_bar.clearMessage() def open_vpk(self, vpk_path): if self.vpk_loaded: # if we already have a file open, close it. self.close_vpk() self.status_bar.showMessage(MSG_OPEN_VPK) if not os.path.exists(vpk_path): print( "Attempted to open {}, which doesn't exist.".format(vpk_path)) return self.opened_file = vpk_path try: self.opened_vpk = vpk.open(vpk_path) except Exception as e: print("Ran into an error from the VPK Library.") sys.stdout.write(str(e)) self.error_box(str(e)) return self.vpk_loaded = True self.status_bar.showMessage(MSG_UNDERSTAND_VPK) # Now we attempt to understand the vpk self.internal_directory_understanding = self.understand_directories( self.opened_vpk) self.status_bar.showMessage(MSG_UPDATE_UI) self.update_interface() self.status_bar.clearMessage() ## # Dialogs ## def open_dialog(self): fn = QFileDialog.getOpenFileName(None, "Open Package", str(pathlib.Path.home()), filter=("Valve Pak Files (*.vpk)")) return fn def open_dir_dialog(self, title="Open Directory"): fn = QFileDialog.getExistingDirectory(None, title, str(pathlib.Path.home())) return fn def error_box(self, text="...", title="Error"): box = QMessageBox() box.setIcon(QMessageBox.Critical) box.setText(text) box.setWindowTitle(title) box.setStandardButtons(QMessageBox.Ok) return box.exec() def info_box(self, text="...", title="Info"): box = QMessageBox() box.setIcon(QMessageBox.Information) box.setText(text) box.setWindowTitle(title) box.setStandardButtons(QMessageBox.Ok) return box.exec() def dir_list_context_menu(self, event): menu = QMenu(self) selected = self.dir_list.selectedIndexes() if not selected: return selected = selected[0] selected_item = self.dir_list_model.itemFromIndex(selected) path = selected_item.path extract = menu.addAction(QIcon.fromTheme("extract-archive"), "Extract") validate = menu.addAction(QIcon.fromTheme("view-certificate"), "Validate") # TODO: better validation icon menu.addSeparator() gotodirectory = menu.addAction(QIcon.fromTheme("folder"), "Go To Directory") menu.addSeparator() properties = menu.addAction(QIcon.fromTheme("settings-configure"), "Properties") extract.setDisabled(selected_item.is_dir) validate.setDisabled(selected_item.is_dir) action = menu.exec_(self.dir_list.mapToGlobal(event)) if action == extract: self.export_selected_file(path) elif action == validate: self.validate_file(path) elif action in [gotodirectory, properties]: self.info_box( "I'm not sure what this does in the original GCFScape.\nIf you know, please make an issue on github!" ) def about(self): box = QMessageBox() box.setWindowTitle( "About PYCFScape") # TODO: what do we version and how box.setText("""PYCFScape Version 0 MIT LICENSE V1.00 OR LATER Python {} QT {} AUTHORS (Current Version): ACBob - https://acbob.gitlab.io Project Homepage https://github.com/acbob/pycfscape""".format(sys.version, QT_VERSION_STR)) box.setIcon(QMessageBox.Information) box.exec() ## # Actions ## def open_file(self): self.status_bar.showMessage(MSG_USER_WAIT) fn = self.open_dialog()[0] if not fn: self.status_bar.clearMessage() return self.open_vpk(fn) def search_for_file(self, event): print(event) def export_selection(self): if not self.export_paths: self.info_box( "You can't export nothing!\n(Please select some items to export.)" ) return self.status_bar.showMessage(MSG_USER_WAIT) output_dir = self.open_dir_dialog("Export to...") if output_dir: self.status_bar.showMessage(MSG_EXPORT) print("attempting export to", output_dir) for file in self.export_paths: self.export_file(file, output_dir) self.status_bar.clearMessage() def export_selected_file(self, file): self.status_bar.showMessage(MSG_USER_WAIT) output_dir = self.open_dir_dialog("Export to...") if output_dir: self.status_bar.showMessage(MSG_EXPORT) print("attempting export to", output_dir) self.export_file(file, output_dir) self.status_bar.clearMessage() def validate_file(self, file): filetoverify = self.opened_vpk.get_file(file[1:]) if filetoverify: verified = filetoverify.verify() if verified: self.info_box("{} is a perfectly healthy file.".format(file), "All's good.") else: self.error_box("{} is not valid!".format(file), "Uh oh.") else: print("What? file doesn't exist? HOW IS THIS MAN")
class ZeroConfExplorer(QWidget): """ create a zeroconf qgroubbox with a qlist view """ def __init__(self, name): super(ZeroConfExplorer, self).__init__() # init explorer to None self.oscquery_device = None if not name: name = 'OSCJSON thru TCP Explorer' # create the view self.explorer = QTreeView() # Hide Useless Header self.explorer.header().hide() self.panel = Panel() # create right-click menu self.explorer.setContextMenuPolicy(Qt.CustomContextMenu) self.explorer.customContextMenuRequested.connect(self.contextual_menu) # create the model self.devices_model = QStandardItemModel() # link model to the view self.explorer.setModel(self.devices_model) self.explorer.selectionModel().selectionChanged.connect( self.selection_updated) # set selection self.device_selection_model = self.explorer.selectionModel() # set layout and group Layout = QGridLayout() # add the view to the layout Layout.addWidget(self.explorer, 0, 0) Layout.addWidget(self.panel, 0, 1) # add the layout to the GroupBox self.setLayout(Layout) #self.setMinimumSize(300, 300) self.explorer.setFixedSize(300, 300) # start zeroconf services zeroconf = Zeroconf() # start the callback, it will create items listener = ZeroConfListener(self.devices_model) listener.add_device.connect(self.connect_device) browser = ServiceBrowser(zeroconf, "_oscjson._tcp.local.", listener) self.current_remote = None def connect_device(self, device): self.panel.device = device def contextual_menu(self, position): indexes = self.explorer.selectedIndexes() if len(indexes) > 0: level = 0 index = indexes[0] while index.parent().isValid(): index = index.parent() level += 1 node = self.devices_model.itemFromIndex(index) menu = QMenu() if level == 0: menu.addAction("Refresh Device Namespace", node.update) elif level > 0: menu.addAction("Refresh Node", node.update) menu.exec_(self.explorer.viewport().mapToGlobal(position)) def selection_updated(self, *args, **kwargs): """ called when device selection is updated we will disconnect our ossia.OSCQueryDevice from the previous device if there was one and then we will reconnect it to the current instance of the Device model """ index = self.device_selection_model.selectedIndexes() # we consider unique selection modelIndex = index[0] if modelIndex: if self.current_remote: self.panel.layout.removeWidget(self.current_remote) self.current_remote.deleteLater() del self.current_remote self.current_remote = None node = self.devices_model.itemFromIndex(modelIndex).node if node.__class__.__name__ == 'OSCQueryDevice': print('Device') else: if node.parameter: self.current_remote = self.panel.add_remote(node.parameter) else: self.current_remote = self.panel.add_inspector(node) else: print('no node selected')
class App(QMainWindow): def __init__(self): super().__init__() self.initUI() def initUI(self): self.srvList = ["", "opcUA", "modbusTCP"] if (os.name == 'nt'): self.appPath = os.path.abspath(sys.argv[0]).replace( os.path.basename(__file__), '') self.imgPath = self.appPath + "img\\" self.dbPath = self.appPath + "db\\webDb.db" #import self.appPath+srvconf else: self.appPath = os.path.abspath(sys.argv[0]).replace( os.path.basename(__file__), '') self.imgPath = self.appPath + "img/" self.dbPath = self.appPath + "db/webDb.db" self.versionPr = 'ScadaPy Web JSON Сервер v.3.14' self.setGeometry(300, 100, 1500, 820) self.setWindowTitle(self.versionPr) self.setWindowIcon(QIcon(self.imgPath + 'Globe.png')) self.h = self.frameGeometry().height() self.w = self.frameGeometry().width() self.setStyleSheet("background-color: #FFF8E7;") font = QFont() font.setPointSize(12) self.label0 = QLabel(self) self.label0.setFont(font) self.label0.move(400, 60) self.label0.resize(300, 25) self.label0.setText("ID сервера") self.label00 = QLabel(self) self.label00.setFont(font) self.label00.move(550, 60) self.label00.resize(300, 25) self.label00.setText("") self.label1 = QLabel(self) self.label1.setFont(font) self.label1.move(400, 90) self.label1.resize(140, 25) self.label1.setText("Название сервера") self.srvName = QLineEdit(self) self.srvName.setToolTip('Пример: Сервер Маш.Зала №1') self.srvName.move(550, 90) self.srvName.setFont(font) self.srvName.resize(300, 25) self.label2 = QLabel(self) self.label2.setFont(font) self.label2.move(400, 120) self.label2.resize(140, 25) self.label2.setText("Http IP address") self.slaveIP = QLineEdit(self) self.slaveIP.setToolTip('Пример: 192.168.0.111') self.slaveIP.move(550, 120) self.slaveIP.setFont(font) self.slaveIP.resize(300, 25) self.label2 = QLabel(self) self.label2.setFont(font) self.label2.move(400, 150) self.label2.resize(140, 25) self.label2.setText("Http Port") self.slavePort = QLineEdit(self) self.slavePort.setToolTip('Пример : 8080') self.slavePort.move(550, 150) self.slavePort.setFont(font) self.slavePort.resize(100, 25) self.label7 = QLabel(self) self.label7.setFont(font) self.label7.move(680, 150) self.label7.resize(140, 25) self.label7.setText("Timeout") self.serverTimeout = QLineEdit(self) self.serverTimeout.setToolTip('Пример ms: 1 ') self.serverTimeout.move(750, 150) self.serverTimeout.setFont(font) self.serverTimeout.resize(100, 25) self.label8 = QLabel(self) self.label8.setFont(font) self.label8.move(400, 180) self.label8.resize(140, 25) self.label8.setText("Login") self.serverLogin = QLineEdit(self) self.serverLogin.setToolTip('Имя пользователя') self.serverLogin.move(550, 180) self.serverLogin.setFont(font) self.serverLogin.resize(100, 25) self.label9 = QLabel(self) self.label9.setFont(font) self.label9.move(680, 180) self.label9.resize(140, 25) self.label9.setText("Password") self.serverPassword = QLineEdit(self) self.serverPassword.setToolTip('Пароль') self.serverPassword.move(750, 180) self.serverPassword.setFont(font) self.serverPassword.resize(100, 25) exitAction = QAction(QIcon(self.imgPath + 'exit.png'), '&Выход', self) exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Выход из программы') exitAction.triggered.connect(qApp.quit) addServerAction = QAction(QIcon(self.imgPath + 'add.png'), '&Добавить', self) addServerAction.setStatusTip('Добавить сервер') addServerAction.triggered.connect(self.addNewServer) delServerAction = QAction(QIcon(self.imgPath + 'button_cancel.png'), '&Удалить', self) delServerAction.setStatusTip('Удалить сервер') delServerAction.triggered.connect(self.delServer) saveServerAction = QAction(QIcon(self.imgPath + 'filesave.png'), '&Сохранить', self) saveServerAction.setStatusTip('Сохранить сервер') saveServerAction.triggered.connect(self.saveServer) saveScr = QAction(QIcon(self.imgPath + 'bottom.png'), '&Сохранить скрипт', self) saveScr.setStatusTip('Сохранить скрипт') saveScr.triggered.connect(self.saveScr) runScr = QAction(QIcon(self.imgPath + 'run.png'), '&Запустить скрипт', self) runScr.setStatusTip('Запустить скрипт') runScr.triggered.connect(self.runScr) runConf = QAction(QIcon(self.imgPath + 'app.png'), '&Запустить конфигуратор', self) runConf.setStatusTip('Запустить конфигуратор') runConf.triggered.connect(self.runConf) menubar = self.menuBar() fileMenu = menubar.addMenu('&Команды') fileMenu.addAction(addServerAction) fileMenu.addAction(delServerAction) fileMenu.addAction(saveServerAction) fileMenu.addAction(saveScr) fileMenu.addAction(runScr) fileMenu.addAction(exitAction) self.toolbar = self.addToolBar('Выход') self.toolbar.addAction(exitAction) self.toolbar.addAction(addServerAction) self.toolbar.addAction(delServerAction) self.toolbar.addAction(saveServerAction) self.toolbar.addAction(saveScr) self.toolbar.addAction(runScr) self.toolbar.addAction(runConf) # self.statusBar().showMessage('Загрузка данных') self.treeView = QTreeView(self) self.treeView.setFont(font) self.treeView.setObjectName("treeView") self.model = QStandardItemModel() self.treeView.setModel(self.model) self.treeView.setStyleSheet("background-color: white;") self.header = ['Название сервера'] self.model.setHorizontalHeaderLabels(self.header) self.sqlLoad() self.treeView.clicked.connect(self.onClickItem) self.frameTable = QFrame(self) self.frameTable.setVisible(True) self.addRow = QPushButton(self.frameTable) self.addRow.setIcon(QIcon(self.imgPath + 'add.png')) self.addRow.move(10, 10) self.addRow.resize(30, 30) self.addRow.clicked.connect(self.addItemTree) self.saveTable = QPushButton(self.frameTable) self.saveTable.setIcon(QIcon(self.imgPath + 'filesave.png')) self.saveTable.resize(30, 30) self.saveTable.move(50, 10) self.saveTable.clicked.connect(self.saveData) #################################################################### self.treeTable = QTableWidget(self.frameTable) self.treeTable.setStyleSheet("background-color: white;") fontTable = QFont() fontTable.setPointSize(10) self.treeTable.setFont(fontTable) # self.show() self.showMaximized() def addItemTree(self): items = ("modbus_tcp", "opc_ua") item, okPressed = QInputDialog.getItem(self, "Добавить тип сервера", "Тип:", items, 0, False) if okPressed and item: self.treeTable.insertRow(self.treeTable.rowCount()) self.treeTable.setItem(self.treeTable.rowCount() - 1, 0, QTableWidgetItem(item)) def getData(self): self.treeTable.clear() self.treeTable.setColumnCount(10) #self.treeTable.setRowCount(1) self.treeTable.setHorizontalHeaderLabels([ 'Тип сервера', 'Имя переменной', 'Имя параметра', 'IP адрес', 'Порт', 'Логин', 'Пароль', 'Timeout', 'Адрес ячейки', 'Количество' ]) self.treeTable.resizeColumnsToContents() self.treeTable.setColumnWidth(0, 120) self.treeTable.setColumnWidth(1, 170) self.treeTable.setColumnWidth(2, 170) self.treeTable.setColumnWidth(3, 140) self.treeTable.setColumnWidth(4, 80) self.treeTable.setColumnWidth(5, 120) self.treeTable.setColumnWidth(6, 120) self.treeTable.setColumnWidth(7, 80) self.treeTable.setColumnWidth(8, 120) self.treeTable.setColumnWidth(9, 80) connDb = sqlite3.connect(self.dbPath) cursor = connDb.cursor() cursor.execute( "select type,var,param,ip,port,login,password,t,regadr,regcount from master where serverId = " + self.label00.text()) dt = cursor.fetchall() self.treeTable.setRowCount(len(dt)) for i in range(0, len(dt)): self.treeTable.setItem(i, 0, QTableWidgetItem(dt[i][0])) self.treeTable.setItem(i, 1, QTableWidgetItem(dt[i][1])) self.treeTable.setItem(i, 2, QTableWidgetItem(dt[i][2])) self.treeTable.setItem(i, 3, QTableWidgetItem(dt[i][3])) self.treeTable.setItem(i, 4, QTableWidgetItem(dt[i][4])) self.treeTable.setItem(i, 5, QTableWidgetItem(dt[i][5])) self.treeTable.setItem(i, 6, QTableWidgetItem(dt[i][6])) self.treeTable.setItem(i, 7, QTableWidgetItem(dt[i][7])) self.treeTable.setItem(i, 8, QTableWidgetItem(dt[i][8])) self.treeTable.setItem(i, 9, QTableWidgetItem(dt[i][9])) i += 1 def saveData(self): connDb = sqlite3.connect(self.dbPath) cursor = connDb.cursor() cursor.execute("delete from master where serverId= '" + self.label00.text() + "'") connDb.commit() for i in range(0, self.treeTable.rowCount()): try: if (len(self.treeTable.item(i, 0).text()) > 0 and len(self.treeTable.item(i, 1).text()) > 0 and len(self.treeTable.item(i, 2).text()) > 0 and len(self.treeTable.item(i, 3).text()) > 0 and len(self.treeTable.item(i, 4).text()) > 0 and len(self.treeTable.item(i, 5).text()) > 0 and len(self.treeTable.item(i, 6).text()) > 0 and len(self.treeTable.item(i, 7).text()) > 0 and len(self.treeTable.item(i, 8).text()) > 0): cursor.execute( "INSERT INTO master(serverId,type,var,param,ip,port,login,password,t,regadr,regcount,valid)\ VALUES('" + self.label00.text() + "',\ '" + self.treeTable.item(i, 0).text() + "',\ '" + self.treeTable.item(i, 1).text() + "',\ '" + self.treeTable.item(i, 2).text() + "',\ '" + self.treeTable.item(i, 3).text() + "',\ '" + self.treeTable.item(i, 4).text() + "',\ '" + self.treeTable.item(i, 5).text() + "',\ '" + self.treeTable.item(i, 6).text() + "',\ '" + self.treeTable.item(i, 7).text() + "',\ '" + self.treeTable.item(i, 8).text() + "',\ '" + self.treeTable.item(i, 9).text() + "',\ 1)") connDb.commit() except Exception as e: print(e) pass i += 1 self.getData() def truePanel(self): self.getData() def onClickItem(self): self.treeTable.clear() self.treeTable.setRowCount(0) self.srvName.setText("") self.label00.setText("") self.slaveIP.setText("") self.slavePort.setText("") self.serverTimeout.setText("") self.serverLogin.setText("") self.serverPassword.setText("") try: self.slaveIP.setEnabled(True) self.slavePort.setEnabled(True) index_list = [i.data() for i in self.treeView.selectedIndexes()] s = index_list[0].split(':') self.srvName.setText(s[1]) self.label00.setText(s[0]) connDb = sqlite3.connect(self.dbPath) cursor = connDb.cursor() cursor.execute( "select ip, port,timeout,login,password from servers where id = " + s[0]) dt = cursor.fetchone() self.slaveIP.setText(dt[0]) self.slavePort.setText(dt[1]) self.serverTimeout.setText(dt[2]) self.serverLogin.setText(dt[3]) self.serverPassword.setText(dt[4]) self.truePanel() except Exception as e: index_list = [i.data() for i in self.treeView.selectedIndexes()] self.srvName.setText(index_list[0]) self.slaveIP.setEnabled(False) self.slavePort.setEnabled(False) print(e) def addNewServer(self): sender = self.sender() self.statusBar().showMessage('Добавление нового сервера') self.model.appendRow(QStandardItem("Новый Сервер ")) def closeEvent(self, event): reply = QMessageBox.question(self, 'Сообщение', "Вы уверены, что хотите выйти?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: event.accept() else: event.ignore() def resizeEvent(self, event): self.h = self.frameGeometry().height() self.treeView.setGeometry(QtCore.QRect(0, 60, 350, self.h - 120)) self.treeTable.setGeometry( QtCore.QRect(10, 50, self.w - 270, self.h - 320)) self.frameTable.setGeometry( QtCore.QRect(350, 210, self.w - 250, self.h - 260)) def sqlLoad(self): connDb = sqlite3.connect(self.dbPath) cursor = connDb.cursor() for row in cursor.execute( 'SELECT name, id FROM servers where valid=1 ORDER BY id'): self.model.appendRow(QStandardItem(str(row[1]) + ":" + row[0])) def saveServer(self): self.treeTable.clear() connDb = sqlite3.connect(self.dbPath) cursor = connDb.cursor() if (len(self.label00.text()) > 0 and int(self.label00.text()) > 0): if (len(self.srvName.text()) > 1): connDb.execute("update servers set name ='" + self.srvName.text() + "', ip = '" + self.slaveIP.text() + "', port = '" + self.slavePort.text() + "'\ ,timeout='" + self.serverTimeout.text() + "', login='******',password='******' where id = " + self.label00.text() + " ") connDb.commit() cursor.execute( "select ip,port,timeout,login,password from servers where id = " + self.label00.text()) dt = cursor.fetchone() self.slaveIP.setText(dt[0]) self.slavePort.setText(dt[1]) self.serverTimeout.setText(dt[2]) self.serverLogin.setText(dt[3]) self.serverPassword.setText(dt[4]) self.truePanel() else: try: if (len(self.srvName.text()) > 1): cursor.execute( "INSERT INTO servers(name,valid) VALUES( '" + self.srvName.text() + "',1)") connDb.commit() except Exception as e: #print(e) pass self.model.clear() self.model.setHorizontalHeaderLabels(self.header) cursor = connDb.cursor() for row in cursor.execute( 'SELECT name, id FROM servers where valid=1 ORDER BY id'): self.model.appendRow(QStandardItem(str(row[1]) + ":" + row[0])) # self.truePanel() def saveScr(self): try: pathFolder = self.appPath if (os.name == 'nt'): slash = '\\' bat = '.bat' rem = 'rem ' command = 'start ' else: slash = '/' bat = '.sh' rem = '# ' command = '' f = open( pathFolder + 'scr' + slash + 'web_' + self.label00.text() + bat, 'w') print(pathFolder + 'scr' + slash + 'web_' + self.label00.text() + bat) f.write(rem + 'Скрипт создан в программе \'' + self.versionPr + '\'\n') f.write(rem + 'Сервер Web \'' + self.srvName.text() + '\'\n') f.write(rem + 'Http адрес \'' + self.slaveIP.text() + '\'\n') f.write(rem + 'Http порт \'' + self.slavePort.text() + '\'\n') f.write(command + sys.executable + ' ' + pathFolder + 'source' + slash + 'websrv.py ' + self.label00.text() + ' ' + pathFolder + 'db' + slash + 'webDb.db') f.close() ret = pathFolder + '' + slash + 'scr' + slash + 'web_' + self.label00.text( ) + bat except Exception as e: print(e) return ret def runScr(self): if (os.name == 'nt'): os.system(self.saveScr()) else: os.system('chmod 777 ' + self.saveScr()) os.system('xfce4-terminal --command=\'sudo ' + self.saveScr() + '\'') def runConf(self): if (os.name == 'nt'): self.appPath = os.path.abspath(sys.argv[0]).replace( os.path.basename(__file__), '') os.system(sys.executable + " " + self.appPath + "srvconf.py") else: self.appPath = os.path.abspath(sys.argv[0]).replace( os.path.basename(__file__), '') os.system(sys.executable + " " + self.appPath + "srvconf.py") def delServer(self): self.treeTable.clear() self.treeTable.setRowCount(0) connDb = sqlite3.connect(self.dbPath) if (len(self.label00.text()) > 0 and int(self.label00.text()) > 0): if (len(self.srvName.text()) > 1): connDb.execute("update servers set valid=0 where id = " + self.label00.text() + " ") connDb.commit() self.model.clear() self.model.setHorizontalHeaderLabels(self.header) cursor = connDb.cursor() for row in cursor.execute( 'SELECT name, id FROM servers where valid=1 ORDER BY id'): self.model.appendRow(QStandardItem(str(row[1]) + ":" + row[0])) self.slaveIP.setText("") self.slavePort.setText("") # self.label3.setText("") self.srvName.setText("")
class Editor(QMainWindow): """This is the main class. """ FORMATS = ("Aiken (*.txt);;Cloze (*.cloze);;GIFT (*.gift);;JSON (*.json)" ";;LaTex (*.tex);;Markdown (*.md);;PDF (*.pdf);;XML (*.xml)") SHORTCUTS = { "Create file": Qt.CTRL + Qt.Key_N, "Find questions": Qt.CTRL + Qt.Key_F, "Read file": Qt.CTRL + Qt.Key_O, "Read folder": Qt.CTRL + Qt.SHIFT + Qt.Key_O, "Save": Qt.CTRL + Qt.Key_S, "Save as": Qt.CTRL + Qt.SHIFT + Qt.Key_S, "Add hint": Qt.CTRL + Qt.SHIFT + Qt.Key_H, "Remove hint": Qt.CTRL + Qt.SHIFT + Qt.Key_Y, "Add answer": Qt.CTRL + Qt.SHIFT + Qt.Key_A, "Remove answer": Qt.CTRL + Qt.SHIFT + Qt.Key_Q, "Open datasets": Qt.CTRL + Qt.SHIFT + Qt.Key_D } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setWindowTitle("QAS Editor GUI") self._items: List[QWidget] = [] self._main_editor = None self.path: str = None self.top_quiz = Category() self.cxt_menu = QMenu(self) self.cxt_item: QStandardItem = None self.cxt_data: _Question | Category= None self.cur_question: _Question = None self.tagbar: GTagBar = None self.main_editor: GTextEditor = None self.is_open_find = self.is_open_dataset = False with resources.open_text("qas_editor.gui", "stylesheet.css") as ifile: self.setStyleSheet(ifile.read()) self._add_menu_bars() # Left side self.data_view = QTreeView() self.data_view.setIconSize(QSize(18, 18)) xframe_vbox = self._block_datatree() left = QWidget() left.setLayout(xframe_vbox) # Right side self.cframe_vbox = QVBoxLayout() self._block_general_data() self._block_answer() self._block_hints() self._block_units() self._block_zones() self._block_solution() self._block_template() self.cframe_vbox.addStretch() self.cframe_vbox.setSpacing(5) for value in self._items: value.setEnabled(False) frame = QFrame() frame.setLineWidth(2) frame.setLayout(self.cframe_vbox) right = QScrollArea() right.setWidget(frame) right.setWidgetResizable(True) # Create main window divider for the splitter splitter = QSplitter() splitter.addWidget(left) splitter.addWidget(right) splitter.setStretchFactor(1, 1) splitter.setSizes([250, 100]) self.setCentralWidget(splitter) # Create lower status bar. status = QStatusBar() self.setStatusBar(status) self.cat_name = QLabel() status.addWidget(self.cat_name) self._update_tree_item(self.top_quiz, self.root_item) self.data_view.expandAll() self.setGeometry(50, 50, 1200, 650) self.show() def _debug_me(self): self.path = "./test_lib/datasets/moodle/all.xml" self.top_quiz = Category.read_files(["./test_lib/datasets/moodle/all.xml"]) gtags = {} self.top_quiz.get_tags(gtags) self.tagbar.set_gtags(gtags) self.root_item.clear() self._update_tree_item(self.top_quiz, self.root_item) self.data_view.expandAll() def _add_menu_bars(self): file_menu = self.menuBar().addMenu("&File") tmp = QAction("New file", self) tmp.setStatusTip("New file") tmp.triggered.connect(self._create_file) tmp.setShortcut(self.SHORTCUTS["Create file"]) file_menu.addAction(tmp) tmp = QAction("Open file", self) tmp.setStatusTip("Open file") tmp.triggered.connect(self._read_file) tmp.setShortcut(self.SHORTCUTS["Read file"]) file_menu.addAction(tmp) tmp = QAction("Open folder", self) tmp.setStatusTip("Open folder") tmp.triggered.connect(self._read_folder) tmp.setShortcut(self.SHORTCUTS["Read folder"]) file_menu.addAction(tmp) tmp = QAction("Save", self) tmp.setStatusTip("Save top category to specified file on disk") tmp.triggered.connect(lambda: self._write_file(False)) tmp.setShortcut(self.SHORTCUTS["Save"]) file_menu.addAction(tmp) tmp = QAction("Save As...", self) tmp.setStatusTip("Save top category to specified file on disk") tmp.triggered.connect(lambda: self._write_file(True)) tmp.setShortcut(self.SHORTCUTS["Save as"]) file_menu.addAction(tmp) file_menu = self.menuBar().addMenu("&Edit") tmp = QAction("Shortcuts", self) #tmp.setShortcut(self.SHORTCUTS["Read file"]) file_menu.addAction(tmp) tmp = QAction("Datasets", self) tmp.triggered.connect(self._open_dataset_popup) tmp.setShortcut(self.SHORTCUTS["Open datasets"]) file_menu.addAction(tmp) tmp = QAction("Find Question", self) tmp.triggered.connect(self._open_find_popup) tmp.setShortcut(self.SHORTCUTS["Find questions"]) file_menu.addAction(tmp) self.toolbar = GTextToolbar(self) self.addToolBar(Qt.TopToolBarArea, self.toolbar) def _add_new_category(self): popup = PopupName(self, True) popup.show() if not popup.exec(): return self.cxt_data.add_subcat(popup.data) self._new_item(popup.data, self.cxt_item, "question") def _add_new_question(self): popup = PopupQuestion(self, self.cxt_data) popup.show() if not popup.exec(): return self._new_item(popup.question, self.cxt_item, "question") @action_handler def _append_category(self): path, _ = QFileDialog.getOpenFileName(self, "Open file", "", self.FORMATS) if not path: return quiz = Category.read_files([path], path.rsplit("/", 1)[-1]) self.cxt_data[quiz.name] = quiz self._update_tree_item(quiz, self.cxt_item) def _block_answer(self) -> None: frame = GCollapsible(self, "Answers") self.cframe_vbox.addLayout(frame) self._items.append(GOptions(self.toolbar, self.main_editor)) _shortcut = QShortcut(self.SHORTCUTS["Add answer"], self) _shortcut.activated.connect(self._items[-1].add) _shortcut = QShortcut(self.SHORTCUTS["Remove answer"], self) _shortcut.activated.connect(self._items[-1].pop) frame.setLayout(self._items[-1]) def _block_datatree(self) -> QVBoxLayout: self.data_view.setStyleSheet("margin: 5px 5px 0px 5px") self.data_view.setHeaderHidden(True) self.data_view.doubleClicked.connect(self._update_item) self.data_view.setContextMenuPolicy(Qt.CustomContextMenu) self.data_view.customContextMenuRequested.connect(self._data_view_cxt) self.data_view.setDragEnabled(True) self.data_view.setAcceptDrops(True) self.data_view.setDropIndicatorShown(True) self.data_view.setDragDropMode(QAbstractItemView.InternalMove) self.data_view.original_dropEvent = self.data_view.dropEvent self.data_view.dropEvent = self._dataview_dropevent self.root_item = QStandardItemModel(0, 1) self.root_item.setHeaderData(0, Qt.Horizontal, "Classification") self.data_view.setModel(self.root_item) xframe_vbox = QVBoxLayout() xframe_vbox.addWidget(self.data_view) return xframe_vbox def _block_general_data(self) -> None: clayout = GCollapsible(self, "Question Header") self.cframe_vbox.addLayout(clayout, 1) grid = QVBoxLayout() # No need of parent. It's inside GCollapsible grid.setSpacing(2) self.main_editor = GTextEditor(self.toolbar, "question") self._items.append(self.main_editor) self._items[-1].setToolTip("Question's description text") self._items[-1].setMinimumHeight(200) grid.addWidget(self._items[-1], 1) self.tagbar = GTagBar(self) self.tagbar.setToolTip("List of tags used by the question.") self._items.append(self.tagbar) grid.addWidget(self._items[-1], 0) others = QHBoxLayout() # No need of parent. It's inside GCollapsible grid.addLayout(others, 0) group_box = QGroupBox("General", self) _content = QVBoxLayout(group_box) _content.setSpacing(5) _content.setContentsMargins(5, 3, 5, 3) self._items.append(GField("dbid", self, int)) self._items[-1].setToolTip("Optional ID for the question.") self._items[-1].setFixedWidth(50) _content.addWidget(self._items[-1], 0) self._items.append(GField("default_grade", self, int)) self._items[-1].setToolTip("Default grade.") self._items[-1].setFixedWidth(50) self._items[-1].setText("1.0") _content.addWidget(self._items[-1], 0) self._items.append(GField("penalty", self, str)) self._items[-1].setToolTip("Penalty") self._items[-1].setFixedWidth(50) self._items[-1].setText("0.0") _content.addWidget(self._items[-1], 0) _content.addStretch() others.addWidget(group_box, 0) group_box = QGroupBox("Unit Handling", self) _content = QVBoxLayout(group_box) _content.setSpacing(5) _content.setContentsMargins(5, 3, 5, 3) self._items.append(GDropbox("grading_type", self, Grading)) self._items[-1].setToolTip("Grading") self._items[-1].setMinimumWidth(80) _content.addWidget(self._items[-1], 0) self._items.append(GDropbox("show_units", self, ShowUnits)) self._items[-1].setToolTip("Show units") _content.addWidget(self._items[-1], 0) self._items.append(GField("unit_penalty", self, float)) self._items[-1].setToolTip("Unit Penalty") self._items[-1].setText("0.0") _content.addWidget(self._items[-1], 0) self._items.append(GCheckBox("left", "Left side", self)) _content.addWidget(self._items[-1], 0) others.addWidget(group_box, 1) group_box = QGroupBox("Multichoices", self) _content = QVBoxLayout(group_box) _content.setSpacing(5) _content.setContentsMargins(5, 3, 5, 3) self._items.append(GDropbox("numbering", self, Numbering)) self._items[-1].setToolTip("How options will be enumerated") _content.addWidget(self._items[-1], 0) self._items.append(GCheckBox("show_instr", "Instructions", self)) self._items[-1].setToolTip("If the structions 'select one (or more " " options)' should be shown") _content.addWidget(self._items[-1], 0) self._items.append(GCheckBox("single", "Multi answer", self)) self._items[-1].setToolTip("If there is just a single or multiple " "valid answers") _content.addWidget(self._items[-1], 0) self._items.append(GCheckBox("shuffle", "Shuffle", self)) self._items[-1].setToolTip("If answers should be shuffled (e.g. order " "of options will change each time)") _content.addWidget(self._items[-1], 0) others.addWidget(group_box, 1) group_box = QGroupBox("Documents", self) _content = QGridLayout(group_box) _content.setSpacing(5) _content.setContentsMargins(5, 3, 5, 3) self._items.append(GDropbox("rsp_format", self, ResponseFormat)) self._items[-1].setToolTip("The format to be used in the reponse.") _content.addWidget(self._items[-1], 0, 0, 1, 2) self._items.append(GCheckBox("rsp_required", "Required", self)) self._items[-1].setToolTip("Require the student to enter some text.") _content.addWidget(self._items[-1], 0, 2) self._items.append(GField("min_words", self, int)) self._items[-1].setToolTip("Minimum word limit") self._items[-1].setText("0") _content.addWidget(self._items[-1], 1, 0) self._items.append(GField("max_words", self, int)) self._items[-1].setToolTip("Maximum word limit") self._items[-1].setText("10000") _content.addWidget(self._items[-1], 2, 0) self._items.append(GField("attachments", self, int)) self._items[-1].setToolTip("Number of attachments allowed. 0 is none." " -1 is unlimited. Should be bigger than " "field below.") self._items[-1].setText("-1") _content.addWidget(self._items[-1], 1, 1) self._items.append(GField("atts_required", self, int)) self._items[-1].setToolTip("Number of attachments required. 0 is none." " -1 is unlimited. Should be smaller than " "field above.") self._items[-1].setText("0") _content.addWidget(self._items[-1], 2, 1) self._items.append(GField("lines", self, int)) self._items[-1].setToolTip("Input box size.") self._items[-1].setText("15") _content.addWidget(self._items[-1], 1, 2) self._items.append(GField("max_bytes", self, int)) self._items[-1].setToolTip("Maximum file size.") self._items[-1].setText("1Mb") _content.addWidget(self._items[-1], 2, 2) self._items.append(GField("file_types", self, str)) self._items[-1].setToolTip("Accepted file types (comma separeted).") self._items[-1].setText(".txt, .pdf") _content.addWidget(self._items[-1], 3, 0, 1, 3) others.addWidget(group_box, 1) _wrapper = QVBoxLayout() # No need of parent. It's inside GCollapsible group_box = QGroupBox("Random", self) _wrapper.addWidget(group_box) _content = QVBoxLayout(group_box) _content.setSpacing(5) _content.setContentsMargins(5, 3, 5, 3) self._items.append(GCheckBox("subcats", "Subcats", self)) self._items[-1].setToolTip("If questions wshould be choosen from " "subcategories too.") _content.addWidget(self._items[-1]) self._items.append(GField("choose", self, int)) self._items[-1].setToolTip("Number of questions to select.") self._items[-1].setText("5") self._items[-1].setFixedWidth(85) _content.addWidget(self._items[-1]) group_box = QGroupBox("Fill-in", self) _wrapper.addWidget(group_box) _content = QVBoxLayout(group_box) _content.setContentsMargins(5, 3, 5, 3) self._items.append(GCheckBox("use_case", "Match case", self)) self._items[-1].setToolTip("If text is case sensitive.") _content.addWidget(self._items[-1]) others.addLayout(_wrapper, 0) group_box = QGroupBox("Datasets", self) _content = QGridLayout(group_box) _content.setSpacing(5) _content.setContentsMargins(5, 3, 5, 3) self._items.append(GList("datasets", self)) self._items[-1].setFixedHeight(70) self._items[-1].setToolTip("List of datasets used by this question.") _content.addWidget(self._items[-1], 0, 0, 1, 2) self._items.append(GDropbox("synchronize", self, Synchronise)) self._items[-1].setToolTip("How should the databases be synchronized.") self._items[-1].setMinimumWidth(70) _content.addWidget(self._items[-1], 1, 0) _gen = QPushButton("Gen", self) _gen.setToolTip("Generate new items based on the max, min and decimal " "values of the datasets, and the current solution.") _gen.clicked.connect(self._gen_items) _content.addWidget(_gen, 1, 1) others.addWidget(group_box, 2) others.addStretch() clayout.setLayout(grid) clayout._toggle() def _block_hints(self) -> None: clayout = GCollapsible(self, "Hints") self.cframe_vbox.addLayout(clayout) self._items.append(GHintsList(None, self.toolbar)) _shortcut = QShortcut(self.SHORTCUTS["Add hint"], self) _shortcut.activated.connect(self._items[-1].add) _shortcut = QShortcut(self.SHORTCUTS["Remove hint"], self) _shortcut.activated.connect(self._items[-1].pop) clayout.setLayout(self._items[-1]) def _block_solution(self) -> None: collapsible = GCollapsible(self, "Solution and Feedback") self.cframe_vbox.addLayout(collapsible) layout = QVBoxLayout() collapsible.setLayout(layout) self._items.append(GTextEditor(self.toolbar, "feedback")) self._items[-1].setMinimumHeight(100) self._items[-1].setToolTip("General feedback for the question. May " "also be used to describe solutions.") layout.addWidget(self._items[-1]) sframe = QFrame(self) sframe.setStyleSheet(".QFrame{border:1px solid rgb(41, 41, 41);" "background-color: #e4ebb7}") layout.addWidget(sframe) _content = QGridLayout(sframe) self._items.append(GTextEditor(self.toolbar, "if_correct")) self._items[-1].setToolTip("Feedback for correct answer") _content.addWidget(self._items[-1], 0, 0) self._items.append(GTextEditor(self.toolbar, "if_incomplete")) self._items[-1].setToolTip("Feedback for incomplete answer") _content.addWidget(self._items[-1], 0, 1) self._items.append(GTextEditor(self.toolbar, "if_incorrect")) self._items[-1].setToolTip("Feedback for incorrect answer") _content.addWidget(self._items[-1], 0, 2) self._items.append(GCheckBox("show_num", "Show the number of correct " "responses once the question has finished" , self)) _content.addWidget(self._items[-1], 2, 0, 1, 3) _content.setColumnStretch(3, 1) def _block_template(self) -> None: collapsible = GCollapsible(self, "Templates") self.cframe_vbox.addLayout(collapsible) layout = QVBoxLayout() collapsible.setLayout(layout) self._items.append(GTextEditor(self.toolbar, "template")) self._items[-1].setMinimumHeight(70) self._items[-1].setToolTip("Text displayed in the response input box " "when a new attempet is started.") layout.addWidget(self._items[-1]) self._items.append(GTextEditor(self.toolbar, "grader_info")) self._items[-1].setMinimumHeight(50) self._items[-1].setToolTip("Information for graders.") layout.addWidget(self._items[-1]) def _block_units(self): collapsible = GCollapsible(self, "Units") self.cframe_vbox.addLayout(collapsible) def _block_zones(self): collapsible = GCollapsible(self, "Background and Zones") self.cframe_vbox.addLayout(collapsible) @action_handler def _clone_shallow(self) -> None: new_data = copy.copy(self.cxt_data) self._new_item(new_data, self.cxt_item.parent(), "question") @action_handler def _clone_deep(self) -> None: new_data = copy.deepcopy(self.cxt_data) self._new_item(new_data, self.cxt_itemparent(), "question") @action_handler def _create_file(self, *_): self.top_quiz = Category() self.path = None self.root_item.clear() self._update_tree_item(self.top_quiz, self.root_item) @action_handler def _dataview_dropevent(self, event: QDropEvent): from_obj = self.data_view.selectedIndexes()[0].data(257) to_obj = self.data_view.indexAt(event.pos()).data(257) if isinstance(to_obj, Category): if isinstance(from_obj, _Question): to_obj.add_subcat(from_obj) else: to_obj.add_question(from_obj) else: event.ignore() self.data_view.original_dropEvent(event) def _data_view_cxt(self, event): model_idx = self.data_view.indexAt(event) self.cxt_item = self.root_item.itemFromIndex(model_idx) self.cxt_data = model_idx.data(257) self.cxt_menu.clear() rename = QAction("Rename", self) rename.triggered.connect(self._rename_category) self.cxt_menu.addAction(rename) if self.cxt_item != self.root_item.item(0): tmp = QAction("Delete", self) tmp.triggered.connect(self._delete_item) self.cxt_menu.addAction(tmp) tmp = QAction("Clone (Shallow)", self) tmp.triggered.connect(self._clone_shallow) self.cxt_menu.addAction(tmp) tmp = QAction("Clone (Deep)", self) tmp.triggered.connect(self._clone_deep) self.cxt_menu.addAction(tmp) if isinstance(self.cxt_data, Category): tmp = QAction("Save as", self) tmp.triggered.connect(lambda: self._write_quiz(self.cxt_data, True)) self.cxt_menu.addAction(tmp) tmp = QAction("Append", self) tmp.triggered.connect(self._append_category) self.cxt_menu.addAction(tmp) tmp = QAction("Sort", self) #tmp.triggered.connect(self._add_new_category) self.cxt_menu.addAction(tmp) tmp = QAction("New Question", self) tmp.triggered.connect(self._add_new_question) self.cxt_menu.addAction(tmp) tmp = QAction("New Category", self) tmp.triggered.connect(self._add_new_category) self.cxt_menu.addAction(tmp) self.cxt_menu.popup(self.data_view.mapToGlobal(event)) @action_handler def _delete_item(self, *_): self.cxt_item.parent().removeRow(self.cxt_item.index().row()) cat = self.cxt_data.parent if isinstance(self.cxt_data, _Question): cat.pop_question(self.cxt_data) elif isinstance(self.cxt_data, Category): cat.pop_subcat(self.cxt_data) @action_handler def _gen_items(self, _): pass def _new_item(self, data: Category, parent: QStandardItem, title: str): name = f"{data.__class__.__name__}_icon.png".lower() item = None with resources.path("qas_editor.images", name) as path: item = QStandardItem(QIcon(path.as_posix()), data.name) item.setEditable(False) item.setData(QVariant(data)) parent.appendRow(item) return item @action_handler def _open_dataset_popup(self, _): if not self.is_open_dataset: popup = PopupDataset(self, self.top_quiz) popup.show() self.is_open_dataset = True @action_handler def _open_find_popup(self, _): if not self.is_open_find: popup = PopupFind(self, self.top_quiz, self.tagbar.cat_tags) popup.show() self.is_open_find = True @action_handler def _read_file(self, _): files, _ = QFileDialog.getOpenFileNames(self, "Open file", "", self.FORMATS) if not files: return if len(files) == 1: self.path = files[0] self.top_quiz = Category.read_files(files) gtags = {} self.top_quiz.get_tags(gtags) self.tagbar.set_gtags(gtags) self.root_item.clear() self._update_tree_item(self.top_quiz, self.root_item) self.data_view.expandAll() @action_handler def _read_folder(self, _): dialog = QFileDialog(self) dialog.setFileMode(QFileDialog.FileMode.Directory) if not dialog.exec(): return self.top_quiz = Category() self.path = None for folder in dialog.selectedFiles(): cat = folder.rsplit("/", 1)[-1] quiz = Category.read_files(glob.glob(f"{folder}/*"), cat) self.top_quiz.add_subcat(quiz) gtags = {} self.top_quiz.get_tags(gtags) self.tagbar.set_gtags(gtags) self.root_item.clear() self._update_tree_item(self.top_quiz, self.root_item) self.data_view.expandAll() @action_handler def _rename_category(self, *_): popup = PopupName(self, False) popup.show() if not popup.exec(): return self.cxt_data.name = popup.data self.cxt_item.setText(popup.data) @action_handler def _update_item(self, model_index: QModelIndex) -> None: item = model_index.data(257) if isinstance(item, _Question): for key in self._items: attr = key.get_attr() if attr in item.__dict__: key.setEnabled(True) key.from_obj(item) else: key.setEnabled(False) self.cur_question = item path = [f" ({item.__class__.__name__})"] while item.parent: path.append(item.name) item = item.parent path.append(item.name) path.reverse() self.cat_name.setText(" > ".join(path[:-1]) + path[-1]) def _update_tree_item(self, data: Category, parent: QStandardItem) -> None: item = self._new_item(data, parent, "category") for k in data.questions: self._new_item(k, item, "question") for k in data: self._update_tree_item(data[k], item) @action_handler def _write_quiz(self, quiz: Category, save_as: bool): if save_as or self.path is None: path, _ = QFileDialog.getSaveFileName(self, "Save file", "", self.FORMATS) if not path: return None else: path = self.path ext = path.rsplit('.', 1)[-1] getattr(quiz, quiz.SERIALIZERS[ext][1])(path) return path def _write_file(self, save_as: bool) -> None: path = self._write_quiz(self.top_quiz, save_as) if path: self.path = path
class TapeWidget(QWidget): def __init__(self, parent = None): super().__init__(parent) self._main_layout = QVBoxLayout(self) self._search_box = QLineEdit(self) self._button_layout = QHBoxLayout() self._view = QTreeView() self._view.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self._view.setSelectionMode(QAbstractItemView.ExtendedSelection) self._view.setHeaderHidden(True) self._add_note_button = QPushButton(self) self._add_note_button.setText("New note") self._add_child_button = QPushButton(self) self._add_child_button.setText("New child") self._add_sibling_button = QPushButton(self) self._add_sibling_button.setText("New sibling") self._delete_note_button = QPushButton(self) self._delete_note_button.setText("Delete note") self._button_layout.addWidget(self._add_note_button) self._button_layout.addWidget(self._add_sibling_button) self._button_layout.addWidget(self._add_child_button) self._button_layout.addWidget(self._delete_note_button) self._button_layout.addStretch() self._main_layout.addWidget(self._search_box) self._main_layout.addLayout(self._button_layout) self._main_layout.addWidget(self._view) self._tape_filter_proxy_model = TapeFilterProxyModel() self._note_delegate = NoteDelegate() self._tape_model = QStandardItemModel() self.set_model(self._tape_model) self._view.setItemDelegate(self._note_delegate) self._view.setModel(self._tape_filter_proxy_model) self._add_note_button.clicked.connect(lambda checked: self.add_and_focus_note()) self._add_sibling_button.clicked.connect(self._new_sibling_handler) self._add_child_button.clicked.connect(self._new_child_handler) self._delete_note_button.clicked.connect(self.delete_selected_notes) self._search_box.textChanged.connect(self._tape_filter_proxy_model.setFilterFixedString) def model(self): """ Returns the model that contains all notes managed by the tape. The model should be treated as read-only. You can only modify it indirectly through the methods provided by TapeWidget. """ return self._tape_model def proxy_model(self): """ Returns the model that contains notes matching current filter. The model should be treated as read-only. You can only modify it indirectly through the methods provided by TapeWidget. """ return self._tape_filter_proxy_model def set_model(self, model): assert ( len(set([item_to_id(item) for item in all_items(model) if item_to_id(item) != None])) == len( [item_to_id(item) for item in all_items(model) if item_to_id(item) != None]) ) # NOTE: If there's an exception in setSourceModel(), we can hope that the source model # remains unchanged. That's why we assing to _tape_model only if that instruction succeeds. self._tape_filter_proxy_model.setSourceModel(model) self._tape_model = model def notes(self): return all_notes(self._tape_model) def assign_ids(self): assign_note_ids(self._tape_model) def create_empty_note(self): return Note( body = "", tags = [], created_at = datetime.utcnow() ) def add_note(self, note = None, parent_index = None): # NOTE: Remember to use indexes from _tape_model, not _tape_filter_proxy_model here assert parent_index == None or self._tape_model.itemFromIndex(parent_index) != None and parent_index.isValid() root_item = self._tape_model.invisibleRootItem() if parent_index == None: parent_item = root_item else: parent_item = self._tape_model.itemFromIndex(parent_index) if note != None: assert note not in self.notes() else: note = self.create_empty_note() item = QStandardItem() set_item_note(item, note) parent_item.appendRow(item) def add_and_focus_note(self, parent_proxy_index = None): if parent_proxy_index != None: parent_index = self._tape_filter_proxy_model.mapToSource(parent_proxy_index) else: parent_index = None self.add_note(parent_index = parent_index) if parent_proxy_index != None: self._view.expand(parent_proxy_index) parent_item = self._tape_model.itemFromIndex(parent_index) else: parent_item = self._tape_model.invisibleRootItem() # NOTE: It's likely that the new note does not match the filter and won't not be present # in the proxy model. We want to select it and focus on it so the filter must be cleared. # And it must be cleared before taking the index in the proxy because changing the filter # may change the set of notes present in the proxy and invalidate the index. self.set_filter('') new_note_index = parent_item.child(parent_item.rowCount() - 1).index() new_note_proxy_index = self._tape_filter_proxy_model.mapFromSource(new_note_index) self.clear_selection() self.set_note_selection(new_note_proxy_index, True) self._view.scrollTo(new_note_proxy_index) def remove_notes(self, indexes): remove_items(self._tape_model, indexes) def clear(self): self._tape_model.clear() def set_filter(self, text): # NOTE: This triggers textChanged() signal which applies the filter self._search_box.setText(text) def get_filter(self): return self._search_box.text() def selected_proxy_indexes(self): return self._view.selectedIndexes() def selected_indexes(self): return [self._tape_filter_proxy_model.mapToSource(proxy_index) for proxy_index in self.selected_proxy_indexes()] def set_note_selection(self, proxy_index, select): assert proxy_index != None and proxy_index.isValid() assert self._tape_model.itemFromIndex(self._tape_filter_proxy_model.mapToSource(proxy_index)) != None self._view.selectionModel().select( QItemSelection(proxy_index, proxy_index), QItemSelectionModel.Select if select else QItemSelectionModel.Deselect ) def clear_selection(self): self._view.selectionModel().clear() def delete_selected_notes(self): self.remove_notes(self.selected_indexes()) def _new_sibling_handler(self): selected_proxy_indexes = self._view.selectedIndexes() if len(selected_proxy_indexes) > 1: self.clear_selection() selected_proxy_indexes = [] if len(selected_proxy_indexes) == 0 or selected_proxy_indexes[0].parent() == QModelIndex(): self.add_and_focus_note() else: self.add_and_focus_note(selected_proxy_indexes[0].parent()) def add_child_to_selected_element(self): selected_proxy_indexes = self._view.selectedIndexes() if len(selected_proxy_indexes) != 1: return False else: self.add_and_focus_note(selected_proxy_indexes[0]) return True def _new_child_handler(self): added = self.add_child_to_selected_element() if not added: QMessageBox.warning(self, "Can't add note", "To be able to add a new child note select exactly one parent")
class TreeView(QWidget): def __init__(self, table, parent): QWidget.__init__(self, parent) self.window = parent self.tree = QTreeView(self) indent = self.tree.indentation() self.tree.setIndentation(indent / 2) self.model = DataModel(table) self.sorter = sorter = FilterModel(self) sorter.setSourceModel(self.model) self.tree.setModel(sorter) for col in range(3, 9): self.tree.setItemDelegateForColumn(col, PercentDelegate(self)) self.tree.header().setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tree.header().customContextMenuRequested.connect( self._on_header_menu) self.tree.setSortingEnabled(True) self.tree.setAutoExpandDelay(0) self.tree.resizeColumnToContents(0) self.tree.resizeColumnToContents(NAME_COLUMN) self.tree.expand(self.sorter.index(0, 0)) #self.tree.expandAll() self.tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self._on_tree_menu) searchbox = QHBoxLayout() self.search = QLineEdit(self) searchbox.addWidget(self.search) self.search_type = QComboBox(self) self.search_type.addItem("Contains", SEARCH_CONTAINS) self.search_type.addItem("Exact", SEARCH_EXACT) self.search_type.addItem("Reg.Exp", SEARCH_REGEXP) searchbox.addWidget(self.search_type) btn = QPushButton("&Search", self) searchbox.addWidget(btn) btn.clicked.connect(self._on_search) btn = QPushButton("&Next", self) searchbox.addWidget(btn) btn.clicked.connect(self._on_search_next) filterbox = QHBoxLayout() label = QLabel("Time Individual", self) filterbox.addWidget(label) self.individual_time = QSpinBox(self) self.individual_time.setMinimum(0) self.individual_time.setMaximum(100) self.individual_time.setSuffix(" %") filterbox.addWidget(self.individual_time) label = QLabel("Alloc Individual", self) filterbox.addWidget(label) self.individual_alloc = QSpinBox(self) self.individual_alloc.setMinimum(0) self.individual_alloc.setMaximum(100) self.individual_alloc.setSuffix(" %") filterbox.addWidget(self.individual_alloc) label = QLabel("Time Inherited", self) filterbox.addWidget(label) self.inherited_time = QSpinBox(self) self.inherited_time.setMinimum(0) self.inherited_time.setMaximum(100) self.inherited_time.setSuffix(" %") filterbox.addWidget(self.inherited_time) label = QLabel("Alloc Inherited", self) filterbox.addWidget(label) self.inherited_alloc = QSpinBox(self) self.inherited_alloc.setMinimum(0) self.inherited_alloc.setMaximum(100) self.inherited_alloc.setSuffix(" %") filterbox.addWidget(self.inherited_alloc) btn = QPushButton("&Filter", self) btn.clicked.connect(self._on_filter) filterbox.addWidget(btn) btn = QPushButton("&Reset", self) filterbox.addWidget(btn) btn.clicked.connect(self._on_reset_filter) vbox = QVBoxLayout() vbox.addLayout(searchbox) vbox.addLayout(filterbox) vbox.addWidget(self.tree) self.setLayout(vbox) self._search_idxs = None self._search_idx_no = 0 def _expand_to(self, idx): idxs = [idx] parent = idx while parent and parent.isValid(): parent = self.sorter.parent(parent) idxs.append(parent) #print(idxs) for idx in reversed(idxs[:-1]): data = self.sorter.data(idx, QtCore.Qt.DisplayRole) #print(data) self.tree.expand(idx) def _on_search(self): text = self.search.text() selected = self.tree.selectedIndexes() # if selected: # start = selected[0] # else: start = self.sorter.index(0, NAME_COLUMN) search_type = self.search_type.currentData() if search_type == SEARCH_EXACT: method = QtCore.Qt.MatchFixedString elif search_type == SEARCH_CONTAINS: method = QtCore.Qt.MatchContains else: method = QtCore.Qt.MatchRegExp self._search_idxs = idxs = self.sorter.search(start, text, search_type) if idxs: self.window.statusBar().showMessage( "Found: {} occurence(s)".format(len(idxs))) self._search_idx_no = 0 idx = idxs[0] self._locate(idx) else: self.window.statusBar().showMessage("Not found") def _locate(self, idx): self.tree.resizeColumnToContents(0) self.tree.resizeColumnToContents(NAME_COLUMN) self._expand_to(idx) self.tree.setCurrentIndex(idx) #self.tree.selectionModel().select(idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Current | QItemSelectionModel.Rows) #self.tree.scrollTo(idx, QAbstractItemView.PositionAtCenter) def _on_search_next(self): if self._search_idxs: n = len(self._search_idxs) self._search_idx_no = (self._search_idx_no + 1) % n idx = self._search_idxs[self._search_idx_no] self.window.statusBar().showMessage("Occurence {} of {}".format( self._search_idx_no, n)) self._locate(idx) else: self.window.statusBar().showMessage("No search results") def _on_filter(self): self.sorter.setFilter(self.search.text(), self.individual_time.value(), self.individual_alloc.value(), self.inherited_time.value(), self.inherited_alloc.value()) def _on_reset_filter(self): self.sorter.reset() def _on_header_menu(self, pos): menu = make_header_menu(self.tree) menu.exec_(self.mapToGlobal(pos)) def _on_tree_menu(self, pos): index = self.tree.indexAt(pos) #print("index: {}".format(index)) if index.isValid(): record = self.sorter.data(index, QtCore.Qt.UserRole + 1) #print("okay?..") #print("context: {}".format(record)) menu = self.window.make_item_menu(self.model, record) menu.exec_(self.tree.viewport().mapToGlobal(pos))
class RedditScraperWindow(QWidget): """The main window of the program.""" ########### Setup ################################## def __init__(self): super().__init__() self.read_user_config() self.load_assets() self.initialize_ui() def load_assets(self): script_folder = os.path.dirname( os.path.dirname(os.path.abspath(__file__))) asset_folder = os.path.join(script_folder, "assets") if os.path.isdir(asset_folder): self.download_icon = QIcon( os.path.join(asset_folder, 'download_icon.png')) self.stop_icon = QIcon(os.path.join(asset_folder, 'stop_icon.png')) self.reddit_icon = QIcon( os.path.join(asset_folder, 'reddit_icon.png')) else: self.download_icon = QIcon( os.path.join("assets", 'download_icon.png')) self.stop_icon = QIcon(os.path.join("assets", 'stop_icon.png')) self.reddit_icon = QIcon(os.path.join("assets", 'reddit_icon.png')) def initialize_ui(self): """sets up the user interface, connects all the signals and shows the window. """ self.init_components() self.read_settings_config() self.connect_signals() self.setGeometry(100, 100, 1000, 800) self.setWindowTitle('Reddit Image Scraper') self.setWindowIcon(self.reddit_icon) self.show() def init_components(self): """initializes all components and sets up the layout""" internalWidgetInput = QWidget() internalWidgetTree = QWidget() ############ define components #################### self.subredditInput = QComboBox() self.subredditInput.setEditable(True) self.subredditInput.addItems(self.get_downloaded_subreddits()) self.numInput = QLineEdit() self.onlyInt = QIntValidator() self.numInput.setValidator(self.onlyInt) subredditLabel = QLabel('subreddit') numLabel = QLabel('number of images') self.dirLabel = QLabel('choose a directory') scale_label = QLabel("Scale images?") self.imgView = QLabel() self.outputText = QTextEdit('') self.outputText.setReadOnly(True) self.scale_cb = QCheckBox() self.sortingCb = QComboBox() self.sortingCb.addItems([ "Hot", "Top all time", "Top this month", "Top past year", "New", "Controversial" ]) sortingLabel = QLabel('sorting method') self.runButton = QPushButton('Download') self.runButton.setIcon(self.download_icon) self.chooseDirButton = QPushButton('Save dir') self.stopButton = QPushButton('Stop') self.stopButton.setIcon(self.stop_icon) self.fileModel = QFileSystemModel() self.tree = QTreeView() self.tree.setModel(self.fileModel) self.tree.setColumnHidden(1, True) self.tree.setColumnHidden(2, True) self.tree.setColumnHidden(3, True) ############## Menu stuff ################### menu_bar = QMenuBar() file_menu = menu_bar.addMenu('File') help_menu = menu_bar.addMenu('Help') self.exit_action = QAction('Exit', self) file_menu.addAction(self.exit_action) self.help_action = QAction('Help', self) help_menu.addAction(self.help_action) menu_bar.setFixedHeight(30) ############# Setup the grid layout############################### grid = QGridLayout() # grid.addWidget(menu_bar, 1, 0, 1, 4) grid.setSpacing(4) grid.addWidget(subredditLabel, 1, 0) grid.addWidget(self.subredditInput, 1, 1) grid.addWidget(numLabel, 2, 0) grid.addWidget(self.numInput, 2, 1) grid.addWidget(sortingLabel, 3, 0) grid.addWidget(self.sortingCb, 3, 1) grid.addWidget(self.chooseDirButton, 4, 0) grid.addWidget(self.dirLabel, 4, 1) grid.addWidget(self.stopButton, 5, 0) grid.addWidget(self.runButton, 5, 1) grid.addWidget(self.outputText, 7, 0, 7, 2) # grid.addWidget(self.tree,1,2, 11,7) grid.addWidget(scale_label, 6, 0) grid.addWidget(self.scale_cb, 6, 1) hboxTree = QVBoxLayout() hboxTree.addWidget(self.tree) #the image viewer, setting how it behaves under resizing. self.imgView.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.imgView.setMaximumHeight(MAX_IMAGE_HEIGHT) self.imgView.setAlignment(Qt.AlignmentFlag.AlignCenter) img_scroll_area = QScrollArea() img_scroll_area.setMinimumHeight(MAX_IMAGE_HEIGHT) img_scroll_area.setMinimumWidth(MAX_IMAGE_HEIGHT) img_scroll_area.setWidget(self.imgView) internalWidgetInput.setLayout(grid) # internalWidgetInput.setMinimumWidth(300) internalWidgetInput.setFixedWidth(300) internalWidgetInput.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)) internalWidgetTree.setLayout(hboxTree) internalWidgetTree.setFixedWidth(360) internalWidgetTree.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)) #construct layout of main window. hbox = QHBoxLayout() hbox.setSpacing(0) hbox.setContentsMargins(0, 0, 0, 0) hbox.setMenuBar(menu_bar) hbox.addWidget(internalWidgetInput) hbox.addWidget(internalWidgetTree) hbox.addWidget(img_scroll_area) self.setLayout(hbox) def connect_signals(self): """connects all the signals to the right functions""" self.chooseDirButton.clicked.connect(self.show_dir_dialog) self.runButton.clicked.connect(self.run_download_threaded) self.tree.clicked.connect(self.on_treeView_clicked) self.stopButton.clicked.connect(self.stop_download) self.scale_cb.clicked.connect(self.refresh_image) self.exit_action.triggered.connect(exit) #self.edit_login_action.triggered.connect(self.edit_login_info) self.help_action.triggered.connect(self.show_help) self.tree.selectionModel().selectionChanged.connect( self.on_selection_change) def read_user_config(self): """reads in the users username and password from the config file, or if there is no config file, shows an input dialog. Also tests so that only valid login information gets saved to the config file. """ config = configparser.ConfigParser() self.redditScraper = redditScraper() if os.path.exists('redditScraper.ini'): config.read('redditScraper.ini') if 'DIR' in config: self.folder = config['DIR']['root_folder'] else: config['REDDIT'] = { 'subreddit': "wallpapers", 'num': 10, 'sorting': "Hot", 'downloaded_subreddits': "" } with open('redditScraper.ini', 'w') as configfile: config.write(configfile) self.config = config def read_settings_config(self): """reads the saved settings from the config file, if they're there.""" if 'DIR' in self.config: self.folder = self.config['DIR']['root_folder'] idx = self.fileModel.setRootPath(str(self.folder)) self.tree.setRootIndex(idx) self.dirLabel.setText(self.folder) if 'REDDIT' in self.config: self.subredditInput.setCurrentText( self.config['REDDIT']['subreddit']) self.numInput.setText(self.config['REDDIT']['num']) self.sortingCb.setCurrentText(self.config['REDDIT']['sorting']) ################### Actions: ################## @pyqtSlot(QModelIndex) def on_treeView_clicked(self, index): """triggers when the user clicks on a file item shown in the treeView, and shows that file in the picture viewer.""" index = self.fileModel.index(index.row(), 0, index.parent()) self.show_image(index) def on_selection_change(self, selected: QItemSelection, deselected: QItemSelection): """ Triggers when the selected item in the treeview changes, and updates the shown picture. """ self.refresh_image() def refresh_image(self): selected_image_index = self.tree.selectedIndexes()[0] self.show_image(selected_image_index) def show_image(self, index: QModelIndex): filePath = self.fileModel.filePath(index) if os.path.isfile(filePath) and filePath.split(".")[-1] in [ "jpg", "gif", "png", "jpeg" ]: pixmap = QPixmap(filePath) if self.scale_cb.isChecked(): self.imgView.setFixedHeight(MAX_IMAGE_HEIGHT) scaled_img = pixmap.scaledToHeight(MAX_IMAGE_HEIGHT) self.imgView.setFixedWidth(scaled_img.width()) self.imgView.setPixmap(scaled_img) else: self.imgView.setFixedHeight(pixmap.height()) self.imgView.setFixedWidth(pixmap.width()) self.imgView.setPixmap(pixmap) def show_dir_dialog(self): """lets the user select the root folder, and saves the choice to the config file.""" self.folder = QFileDialog.getExistingDirectory( self, 'Choose base directory', '/home') self.dirLabel.setText(self.folder) self.config['DIR'] = {'root_folder': self.folder} with open('redditScraper.ini', 'w') as configfile: self.config.write(configfile) idx = self.fileModel.setRootPath(self.folder) self.tree.setRootIndex(idx) return self.folder @pyqtSlot(str) def update_output_text(self, message: str): """updates the output text area, to show progress on downloads.""" self.outputText.setText(message + self.outputText.toPlainText()) def save_subreddit(self, subreddit: str, num: int, sorting: str): """helper function to save the current settings to the config file.""" downloaded_subreddits = self.get_downloaded_subreddits() if subreddit not in downloaded_subreddits: downloaded_subreddits.append(subreddit) self.subredditInput.addItem(subreddit) downloaded_subreddits = ','.join(downloaded_subreddits) self.config['REDDIT'] = { 'subreddit': subreddit, 'num': str(num), 'sorting': sorting, 'downloaded_subreddits': downloaded_subreddits } with open('redditScraper.ini', 'w') as configfile: self.config.write(configfile) def get_downloaded_subreddits(self) -> list: if 'downloaded_subreddits' in self.config['REDDIT'].keys(): subreddits = self.config['REDDIT']['downloaded_subreddits'].split( ',') return subreddits return [] def run_download_threaded(self): """downloads the pictures. Runs in a QThread, so that the program does not freeze. Also checks whether the specified subreddit exists. """ subreddit = self.subredditInput.currentText() num = int(self.numInput.text()) sorting = self.sortingCb.currentText() if self.redditScraper.sub_exists(subreddit): if not hasattr(self, "folder"): msgBox = QMessageBox() msgBox.setText('You need to set a download folder!') msgBox.setWindowTitle("Pick a download folder") msgBox.exec_() return self.save_subreddit(subreddit, num, sorting) self.get_thread = RedditDownloadThread( self.redditScraper, subreddit, num, num * LOOKUP_LIMIT_MULTIPLIER, sorting, self.folder) self.get_thread.changeText.connect(self.update_output_text) self.get_thread.start() else: msgBox = QMessageBox() msgBox.setText('That subreddit does not exist, please try again') msgBox.setWindowTitle("Invalid subreddit") msgBox.exec_() pass def stop_download(self): """Stops the download thread and prints a message to the output.""" try: if self.get_thread.isRunning(): self.get_thread.terminate() self.outputText.setText(self.outputText.toPlainText() + ' Aborted!\n') except Exception: pass ############### Menu actions: ############### def show_help(self): msgBox = QMessageBox() msgBox.setWindowIcon(self.reddit_icon) msgBox.setText( 'This program downloads images posted to reddit.com, or more specifically, to subreddits' + '(i.e. sub-forums). To use it, one needs a valid reddit account, which one can sign up for ' + 'on reddit.com. One also needs to know some names of subreddits. \n Some suggestions for subreddits: ' + 'wallpapers , earthporn , nature , and pics . \n \n ' + "This program will download up to the specified number of images. It can handle png, jpg and gif," + "so links to gyf-files, videos or anything else will be ignored. Therefore the number of images actually " + "downloaded will usually be less than the specified number. So if you want a lot of images, just put" + " a large limit.\n The images will be placed in a subfolder of the chosen base folder, named after the subreddit. This folder will be created if it does not exist.\n \n " + "To view the images, click on them in the tree-view, and they will appear on the right" ) msgBox.setWindowTitle("Help") msgBox.exec_()
class App(QWidget): ITEM, ITEM2 = range(2) def __init__(self): super(App, self).__init__() self.title = 'Example GUI' self.initUI() def initUI(self): self.setWindowTitle(self.title) # Edit Box self.textbox = QLineEdit(self) # Push button self.button = QPushButton('see list', self) self.button.setToolTip('Show List of items') self.button.clicked.connect(self.on_click) # Search and button Layout grouping self.horizontalGroupBox = QGroupBox() layout = QHBoxLayout() layout.addWidget(self.textbox) layout.addWidget(self.button) self.horizontalGroupBox.setLayout(layout) # Tree View self.dataGroupBox = QGroupBox("Android Directory") self.dataView = QTreeView() self.dataView.setRootIsDecorated(False) self.dataView.setAlternatingRowColors(True) # self.dataView.setDisabled(True) self.dataView.setSortingEnabled(True) self.dataView.clicked.connect(self.on_click) dataLayout = QHBoxLayout() dataLayout.addWidget(self.dataView) self.dataGroupBox.setLayout(dataLayout) mainLayout = QVBoxLayout() mainLayout.addWidget(self.horizontalGroupBox) mainLayout.addWidget(self.dataGroupBox) self.setLayout(mainLayout) # Example for Set the model process = subprocess.Popen(["adb shell ls"], shell=True, stdout=subprocess.PIPE) stdout = process.communicate()[0] dirList = [i for i in format(stdout.decode("utf-8")).split("\n")] model = self.createListModel(self) self.dataView.setModel(model) for i in dirList: self.addItem(model, str(i)) self.show() # Model def createListModel(self, parent): model = QStandardItemModel(0, 1, parent) model.setHeaderData(self.ITEM, Qt.Horizontal, "DIRectory") return model # Add data def addItem(self, model, item): model.insertRow(0) model.setData(model.index(0, self.ITEM), item) @pyqtSlot() def on_click(self): path = [str(data.data()) for data in self.dataView.selectedIndexes()] print(self.dataView.selectedIndexes()[0].row()) self.textbox.setText(path[0]) self.textbox.setFocus()
class DataWindow(QMdiSubWindow): def __init__(self, app, OAuth_token, parent): super().__init__() self.app = app self.token = OAuth_token self.parent = parent self.__threads = [] self.initUI() def initUI(self): # Format the window self.format_window() # Create a horizontal layout to hold the widgets hbox = QHBoxLayout() # Add the widgets hbox.addWidget(self.set_directory_btn()) hbox.addWidget(self.create_file_browser()) hbox.addWidget(self.add_to_selection_btn()) # Create a central widget for the local data window window_widget = QWidget() # Add the vertical box layout window_widget.setLayout(hbox) # Set the projects window widget self.setWidget(window_widget) def format_window(self): """ Form the local data window :return: """ # Gets the QRect of the main window geom = self.parent.geometry() # Gets the Qrect of the sections window section_geom = self.parent.section_geom # Define geometries for the projects window x0 = section_geom.x() + section_geom.width() y0 = section_geom.y() w = geom.width() - x0 h = ((geom.height() - y0) / 3) self.setGeometry(x0, y0, w, h) # Remove frame from projects window self.setWindowFlags(Qt.FramelessWindowHint) ##### # Window Widgets ##### def set_directory_btn(self): """ Creates a QPushButton that can be used to set the current root directory :return: QPushButton """ btn = QPushButton() btn.setIcon( QIcon(os.path.normpath(__file__ + '/../../img/Folder-48.png'))) press_button(self.app, btn) # Format button btn.setToolTip("Select local directory") btn.setToolTipDuration(1) btn.pressed.connect(self.on_set_directory_pressed) return btn def create_file_browser(self): """ Creates a QTreeView with a QFileSystemModel that is used as a file browser :return: QTreeview """ self.browser = QTreeView() # Set the model of the QTreeView self.model = QFileSystemModel() home_dir = os.path.expanduser("~") # Define the initial root directory self.model.setRootPath(home_dir) self.browser.setModel(self.model) # Resize the first column self.browser.setColumnWidth(0, self.geometry().width() / 3) # Control how selection of items works #self.browser.setSelectionBehavior(QAbstractItemView.SelectItems) # Allow for only single item selection self.browser.setSelectionMode( QAbstractItemView.ExtendedSelection ) # Alow for multiple rows to be selected return self.browser def add_to_selection_btn(self): """ Creates a QPushButton that can be used to open the metadata window for the selected items in the file browser :return: QPushButton """ btn = QPushButton() btn.setIcon( QIcon( os.path.normpath(__file__ + '/../../img/Insert Row Below-48.png'))) press_button(self.app, btn) # Format button btn.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) btn.setToolTip("Add selected items to articles list") btn.setToolTipDuration(1) btn.pressed.connect(self.on_open_selection_clicked) return btn ##### # Widget Actions ##### def on_set_directory_pressed(self): """ Called when the set root directory button is pressed :return: """ dir_name = self.user_set_dir() self.browser.setRootIndex(self.model.index(dir_name)) def user_set_dir(self): """ Creates a QFileDialog that prompts the user to choose a root directory :return: Sting. directory path """ return str(QFileDialog.getExistingDirectory(self, "Select Directory")) def on_open_selection_clicked(self): """ Called when the open selection button is clicked. Either open or closes the metadata window :return: """ # Retrieve a set of file paths file_paths = self.get_selection_set() # Locally reference the Article List Widget article_tree = self.parent.data_articles_window.article_tree # Prevent the user from editting, searching, etc. while new articles are created. article_tree.disable_fields() # Create an article creation worker worker = ArticleCreationWorker(self.token, self.parent, file_paths) # Create the thread. load_articles_thread = QThread() load_articles_thread.setObjectName('local_articles_thread') self.__threads.append((load_articles_thread, worker)) # Add the worker to the thread worker.moveToThread(load_articles_thread) # Connect signals from worker worker.sig_step.connect(article_tree.add_to_tree) worker.sig_step.connect( lambda article_id: article_tree.add_to_articles(article_id)) worker.sig_done.connect(article_tree.enable_fields) worker.sig_done.connect(article_tree.update_search_field) worker.sig_done.connect(self.parent.data_articles_window.check_edit) load_articles_thread.started.connect(worker.work) # Begin worker thread load_articles_thread.start() def get_selection_set(self): """ Creates a set of selected item file paths. :return: """ # Get a list of selected items from the QTreeview items = self.browser.selectedIndexes() # Create an empty set to add file paths to file_paths = set() for item in items: # For items that are not directories if not self.model.isDir(item): file_paths.add( self.model.filePath(item)) # Add the item file path else: # Combine the current set with a set of files contained within the directory. Does not recursively # open contained directories contained_files = self.get_child_files( self.model.filePath(item)) if contained_files is not None: file_paths |= contained_files return file_paths @staticmethod def get_child_files(path): """ given a path to a directory will return a set of file paths contained within. Does not recursively open internal directories :param path: string. path to directory :return: set. Containing file paths """ dir = os.path.normpath(path) if os.path.isdir(dir): dir_contents = os.listdir(dir) file_set = set() for item in dir_contents: if not os.path.isdir(item): file_set.add(os.path.join(dir, item)) return file_set else: return None
class ParamEditor(QTabWidget): jsonData = {} isParseError = False error = pyqtSignal(str, str) format_data = [] horizontal_tab = 0 vertical_tab = 1 def __init__(self, mode=0): super().__init__() self.setObjectName("ParamEditor") self.json_edit = JSONEditor() self.table_edit = QTreeView() self.model = QStandardItemModel() self.table_edit.setAlternatingRowColors(True) self.table_edit.setModel(self.model) self.addTab(self.table_edit, "UI") self.addTab(self.json_edit, "JSON") self.table_edit.setContextMenuPolicy(Qt.CustomContextMenu) self.table_edit.customContextMenuRequested.connect(self.open_menu) self.model.itemChanged.connect(self.data_change) self.currentChanged.connect(self.tab_selected) if mode == self.vertical_tab: self.setTabPosition(QTabWidget.West) def set_json_data(self, param): self.jsonData = param self.update() def update(self): self.load_param_2_table(self.jsonData) self.load_param_2_json_editor(self.jsonData) def load_param_2_table(self, param): self.model.clear() self.model.setHorizontalHeaderLabels(['key', 'value']) header = self.table_edit.header() header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) self.create_param_ui(self.model.invisibleRootItem(), param, 0, "", []) self.table_edit.expandAll() def create_param_ui(self, parent, obj, level, parent_name="", item_link=None): if item_link is None: item_link = [] if isinstance(obj, list): for i in range(len(obj)): if isinstance(obj[i], dict) or isinstance(obj[i], list): item = QStandardItem(str(i) + ": ") item.setEditable(False) data = { "key": ":" + str(i), "parent": parent_name, 'level': level, 'link': item_link } item.setData(data) parent.appendRow([item]) link = item_link.copy() link.append(":" + str(i)) self.create_param_ui(item, obj[i], level, parent_name, link) else: item = QStandardItem(str(i) + ": ") item.setEditable(False) value = QStandardItem(str(obj[i])) value.setEditable(True) link = item_link.copy() link.append(":" + str(i)) data = { "key": ":" + str(i), "parent": parent_name, 'level': level, 'link': link } item.setData(data) value.setData(data) parent.appendRow([item, value]) elif isinstance(obj, dict): for key, value in obj.items(): if isinstance(value, dict) or isinstance(value, list): item = QStandardItem(key) item.setEditable(False) data = { "key": key, "parent": parent_name, 'level': level, "link": item_link } item.setData(data) parent.appendRow([item]) link = item_link.copy() link.append(key) self.create_param_ui(item, value, level + 1, key, link) else: self.add_object(parent, obj[key], level, key, item_link) else: self.add_object(parent, obj, level, parent_name, item_link) def add_object(self, parent, obj, level, parent_name="", item_link=None): if item_link is None: item_link = [] item = QStandardItem(parent_name) item.setEditable(False) value = QStandardItem(str(obj)) value.setEditable(True) data = { "key": parent_name, "parent": "", 'level': level, 'link': item_link } item.setData(data) value.setData(data) parent.appendRow([item, value]) def load_param_2_json_editor(self, param): self.json_edit.setDocument(color_json(param)) def sync(self): if self.currentIndex() == 0: self.load_param_2_json_editor(self.jsonData) elif self.currentIndex() == 1: self.try_sync_table() def try_sync_table(self): self.isParseError = False str_param = self.json_edit.toPlainText() try: if str_param == "" or str_param.isspace(): self.jsonData = {} else: self.jsonData = json.loads(str_param) self.load_param_2_json_editor(self.jsonData) self.isParseError = False except Exception as ex: print(ex) self.error.emit("JSON Param", str(ex)) self.isParseError = True finally: if self.isParseError: self.load_param_2_json_editor(str_param) else: self.load_param_2_json_editor(self.jsonData) self.load_param_2_table(self.jsonData) return self.isParseError def data_change(self, item: QStandardItem): data = item.data() if data is not None and "link" in data: item_link = data['link'] link = item_link.copy() link.append(data['key']) self.update_data(self.jsonData, link, item.text(), 0, len(link)) def update_data(self, obj, link, new, index, end): i = link[index] if i.startswith(":"): i = int(i.replace(":", "")) if index == end - 1: obj[i] = new else: self.update_data(obj[i], link, new, index + 1, end) def tab_selected(self, arg=None): if arg is not None: if arg == 0: self.try_sync_table() else: if not self.isParseError: self.load_param_2_json_editor(self.jsonData) def open_menu(self, position): indexes = self.table_edit.selectedIndexes() level = 0 data_link = [] link = [] item = None if len(indexes) > 0: index = indexes[0] item = self.model.itemFromIndex(index) data = item.data() if data is not None: link = data['link'] data_link = link.copy() data_link.append(data['key']) level = 1 menu = QMenu() menu.setStyleSheet(open(get_stylesheet()).read()) delete_action = QAction(QIcon(get_icon_link('delete_forever.svg')), '&Delete key', self) delete_action.setStatusTip('Delete') new_action = QAction(QIcon(get_icon_link('create_new_folder.svg')), '&Add key', self) new_action.setStatusTip('Add key') menu.addAction(new_action) if level == 1: menu.addAction(delete_action) action = menu.exec_(self.table_edit.viewport().mapToGlobal(position)) if action == delete_action: if data_link is not None and data_link != []: self.remove_data(self.jsonData, data_link, 0, len(data_link)) self.update() elif action == new_action: if self.is_list(self.jsonData, link, 0, len(link)): data_new = self.get_data(self.jsonData, data_link, 0, len(data_link)) self.duplicate_data(self.jsonData, link, data_new.copy(), 0, len(link)) self.update() elif self.is_list(self.jsonData, data_link, 0, len(data_link)): data_new = self.get_data(self.jsonData, data_link, 0, len(data_link)) self.add_data(self.jsonData, data_link, data_new.copy(), 0, len(data_link)) self.update() else: input_name = QInputDialog() input_name.setStyleSheet(open(get_stylesheet()).read()) text, ok = input_name.getText(self, 'New field', 'Field key:') if ok: if data_link: self.add_data(self.jsonData, data_link, text, 0, len(data_link)) else: self.jsonData[text] = "" self.update() def remove_data(self, obj, link, index, end): if index <= end - 1: i = link[index] if i.startswith(":"): i = int(i.replace(":", "")) if index == end - 1: del obj[i] else: self.remove_data(obj[i], link, index + 1, end) def is_list(self, obj, link, index, end): if index <= end - 1: i = link[index] if i.startswith(":"): i = int(i.replace(":", "")) if index == end - 1: return isinstance(obj[i], list) else: return self.is_list(obj[i], link, index + 1, end) else: return False def get_data(self, obj, link, index, end): if index <= end - 1: i = link[index] if i.startswith(":"): i = int(i.replace(":", "")) if index == end - 1: return obj[i] else: return self.get_data(obj[i], link, index + 1, end) else: return "" def add_data(self, obj, link, new, index, end): if index <= end - 1: i = link[index] if i.startswith(":"): i = int(i.replace(":", "")) if index == end - 1: if isinstance(obj[i], list): if len(obj[i]) > 1: obj[i].append(obj[i][0]) else: obj[new] = "" elif isinstance(obj[i], dict) or str(obj[i]) == "": obj[i] = {} obj[i][new] = "" else: obj[new] = "" else: self.add_data(obj[i], link, new, index + 1, end) def duplicate_data(self, obj, link, new, index, end): if index <= end - 1: i = link[index] if i.startswith(":"): i = int(i.replace(":", "")) if index == end - 1: if isinstance(obj[i], list): obj[i].append(new) else: self.duplicate_data(obj[i], link, new, index + 1, end) def text(self): return self.json_edit.toPlainText() def set_format_data(self, data): self.format_data = data
class TwoLines(QWidget): def __init__(self): super().__init__() self.top = QListWidget() if MOVIE_UNSEEN: self.current_dict = MOVIE_UNSEEN self.current_list = MOVIE_UNSEEN.keys() else: self.current_dict = MOVIE_SEEN self.current_list = MOVIE_SEEN.keys() self.tabs = QTabWidget() self.bottom = QLabel() self.tree = QTreeView() self.init_ui() def init_ui(self): hbox = QHBoxLayout(self) self.top.addItems(self.current_list) self.top.setCurrentRow(0) self.top.setContextMenuPolicy(Qt.CustomContextMenu) # TAB movie info self.data_to_show() # TAB ls dir self.ls_current_dir() # TABS self.set_tabs() # must be here because of QBasicTimer red msg # QBasicTimer can only be used with threads started with QThread def changed_item(): if self.top.currentItem(): self.data_to_show() self.ls_current_dir() self.top.currentItemChanged.connect(changed_item) self.top.customContextMenuRequested.connect(self.right_click) self.tree.doubleClicked.connect(self.clicked_movie) # to choose a browser # self.labelOnlineHelp.linkActivated.connect(self.link_handler) self.bottom.setOpenExternalLinks(True) splitter1 = QSplitter(Qt.Vertical) splitter1.addWidget(self.top) splitter1.addWidget(self.tabs) hbox.addWidget(splitter1) self.setLayout(hbox) def set_tabs(self): """ movie info on one tab ls dir on the other tab """ # movie info tab_synopsys = QWidget() # layout synopsys_vbox = QVBoxLayout() # ls dir tab_ls_dir = QWidget() lsdir_vbox = QVBoxLayout() # tab one synopsys_vbox.addWidget(self.bottom) tab_synopsys.setLayout(synopsys_vbox) self.tabs.addTab(tab_synopsys, "Movie Info") # tab two lsdir_vbox.addWidget(self.tree) tab_ls_dir.setLayout(lsdir_vbox) self.tabs.addTab(tab_ls_dir, "ls dir") def data_to_show(self): """ call HtmlTags to build html with a poster and a synopsis and put the result on self.bottom """ title = self.top.currentItem().text() url = self.current_dict[title][0] context = HtmlTags(url, title) self.bottom.setText(context.context) def ls_current_dir(self): path_to_dir = self.current_dict[self.top.currentItem().text()][-1] # ls content of the current dirQt.CustomContextMenu lsdir = QFileSystemModel() lsdir.setRootPath(path_to_dir) self.tree.setModel(lsdir) self.tree.setRootIndex(lsdir.index(path_to_dir)) self.tree.setColumnWidth(0, 450) def right_click(self): RightClickMenu(self.current_dict, self.top) def clicked_movie(self): item = self.tree.selectedIndexes()[0] file_to_play = item.model().filePath(item) if file_to_play.endswith(('.avi', 'mp4', '.mkv')): call(['/usr/bin/mpv', file_to_play]) def on_changed(self, text): self.lbl.setText(text) self.lbl.adjustSize()
class ScorePartsWidget(QSplitter): def __init__(self, parent): super(ScorePartsWidget, self).__init__(parent) self.typesLabel = QLabel() self.typesView = QTreeView( selectionMode=QTreeView.ExtendedSelection, selectionBehavior=QTreeView.SelectRows, animated=True, headerHidden=True) self.scoreLabel = QLabel() self.scoreView = widgets.treewidget.TreeWidget( selectionMode=QTreeView.ExtendedSelection, selectionBehavior=QTreeView.SelectRows, headerHidden=True, animated=True, dragDropMode=QTreeView.InternalMove) self.addButton = QPushButton(icon = icons.get("list-add")) self.removeButton = QPushButton(icon = icons.get("list-remove")) self.upButton = QToolButton(icon = icons.get("go-up")) self.downButton = QToolButton(icon = icons.get("go-down")) self.partSettings = QStackedWidget() w = QWidget() self.addWidget(w) layout = QVBoxLayout(spacing=0) w.setLayout(layout) layout.addWidget(self.typesLabel) layout.addWidget(self.typesView) layout.addWidget(self.addButton) w = QWidget() self.addWidget(w) layout = QVBoxLayout(spacing=0) w.setLayout(layout) layout.addWidget(self.scoreLabel) layout.addWidget(self.scoreView) box = QHBoxLayout(spacing=0) layout.addLayout(box) box.addWidget(self.removeButton) box.addWidget(self.upButton) box.addWidget(self.downButton) self.addWidget(self.partSettings) self.typesView.setModel(parts.model()) app.translateUI(self) # signal connections self.addButton.clicked.connect(self.slotAddButtonClicked) self.removeButton.clicked.connect(self.slotRemoveButtonClicked) self.typesView.doubleClicked.connect(self.slotDoubleClicked) self.scoreView.currentItemChanged.connect(self.slotCurrentItemChanged) self.upButton.clicked.connect(self.scoreView.moveSelectedChildrenUp) self.downButton.clicked.connect(self.scoreView.moveSelectedChildrenDown) def translateUI(self): bold = "<b>{0}</b>".format self.typesLabel.setText(bold(_("Available parts:"))) self.scoreLabel.setText(bold(_("Score:"))) self.addButton.setText(_("&Add")) self.removeButton.setText(_("&Remove")) self.upButton.setToolTip(_("Move up")) self.downButton.setToolTip(_("Move down")) def slotDoubleClicked(self, index): self.addParts([index]) def slotAddButtonClicked(self): self.addParts(self.typesView.selectedIndexes()) def addParts(self, indexes): """Adds the parts for the given indexes.""" # add to current if that is a container type currentItem = self.scoreView.currentItem() for index in indexes: category = index.internalPointer() if category: part = category.items[index.row()] box = QGroupBox(self.partSettings) self.partSettings.addWidget(box) # determine the parent: current or root if currentItem and issubclass(part, currentItem.part.accepts()): parent = currentItem parent.setExpanded(True) else: parent = self.scoreView item = PartItem(parent, part, box) def slotCurrentItemChanged(self, item): if isinstance(item, PartItem): self.partSettings.setCurrentWidget(item.box) def slotRemoveButtonClicked(self): self.scoreView.removeSelectedItems() def clear(self): """Called when the user clicks the clear button on this page.""" self.scoreView.clear() def rootPartItem(self): """Returns the invisibleRootItem(), representing the tree of parts in the score view.""" return self.scoreView.invisibleRootItem()
class EditScreen(QWidget): def __init__(self, rester: Rester): super(EditScreen, self).__init__() self.last_item_type = '' self.rester = rester self.tree = QTreeView() layout = QVBoxLayout() layout.addWidget(self.tree) self.model = QStandardItemModel() self.model.setHorizontalHeaderLabels(['Name']) self.tree.header().setDefaultSectionSize(200) self.tree.setModel(self.model) self.data = self.rester.list_request(list(APIEnum)) self.import_data(self.data) self.tree.setSortingEnabled(False) self.tree.expandAll() self.tree.setSelectionMode(QAbstractItemView.SingleSelection) self.tree.selectionModel().selectionChanged.connect(self.item_selected) self.init_ui() def init_ui(self): hbox = QHBoxLayout() vbox = QVBoxLayout() left_layout = QFormLayout() tree_group_box = QGroupBox() self.search_field = QLineEdit() # self.search_field.editingFinished.connect(self.search_field_changed) self.search_field.textEdited.connect(self.search_field_changed) left_layout.addRow(QLabel('Search:'), self.search_field) tree_group_box.setLayout(left_layout) vbox.addWidget(tree_group_box) vbox.addWidget(self.tree) gb = QGroupBox() gb.setLayout(vbox) self.splitter = QSplitter() self.splitter.addWidget(gb) self.ew = EquipmentEdit() self.splitter.addWidget(self.ew) hbox.addWidget(self.splitter) self.setLayout(hbox) def import_data(self, data, root=None): self.model.setRowCount(0) if root is None: root = self.model.invisibleRootItem() for key, value_list in data.items(): parent = TreeItem({'name': key}, '', parent=True) parent.setEditable(False) root.appendRow([parent]) for value in value_list: parent.appendRow([TreeItem(value, key)]) def search_field_changed(self): term = self.search_field.text() data = {} for key, value_list in self.data.items(): li = [] for value in value_list: for k in value.keys(): if term in k: li.append(value) data[key] = li self.import_data(data) def item_selected(self): indexes = self.tree.selectedIndexes() selected = indexes[0] item = self.model.itemFromIndex(selected) if item.is_parent: return #if item.typ != self.last_item_type: self.ew.hide() self.ew.destroy() self.ew = get_edit_widget(item.typ, self.data) self.splitter.addWidget(self.ew) self.last_item_type = item.typ data_dict = item.get_data_dict() self.ew.load_data(data_dict)
class AssetUploader(QMainWindow): def __init__(self): super().__init__() # Setup Box interface self.keyPath = Path("./authfiles") self.keyName = "appauth.json" self.box = None if os.path.isdir(str(self.keyPath)): if os.path.exists(str(self.keyPath / self.keyName)): self.box = BoxInterface.BoxInterface( str(self.keyPath / self.keyName)) else: os.makedirs(str(self.keyPath)) if self.box is None: auth = AuthWindowDialog(self) choice = auth.exec_() # blocks while dialog open if choice == 0: # cancelled sys.exit() else: if auth.importKey: copy(auth.path, (self.keyPath / self.keyName)) self.box = BoxInterface.BoxInterface(self.keyPath / self.keyName) else: self.box = BoxInterface.BoxInterface(auth.path) # Icons self.folderIcon = self.style().standardIcon( getattr(QStyle, 'SP_DirClosedIcon')) self.fileIcon = self.style().standardIcon( getattr(QStyle, 'SP_FileIcon')) self.windowIcon = self.style().standardIcon( getattr(QStyle, 'SP_ArrowUp')) self.title = ("Asset Uploader") self.width = 640 self.height = 240 self.initUI() def initUI(self): # Title/icon self.setWindowTitle(self.title) # Menu bar menu = self.menuBar() utilMenu = menu.addMenu('File') refreshButton = QAction('Refresh', self) utilMenu.addAction(refreshButton) refreshButton.triggered.connect(self.rebuildModel) # Status bar w/ logging statusBarHandler = StatusBarLogger(self) logging.getLogger().addHandler(statusBarHandler) logging.getLogger().setLevel(logging.WARNING) # Icons self.setWindowIcon(self.windowIcon) # Tree view self.treeView = QTreeView() self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) self.treeView.setHeaderHidden(True) self.model = QStandardItemModel() self.model.dataChanged.connect(self.updateIndex) self.rebuildModel() self.treeView.setModel(self.model) # Add all to main layout mainWidget = QWidget(self) mainLayout = QVBoxLayout(mainWidget) self.setCentralWidget(mainWidget) mainLayout.addWidget(self.treeView) # Communication self.treeView.customContextMenuRequested.connect(self.contextMenu) self.show() def buildFileTree(self, root): self.model.dataChanged.disconnect(self.updateIndex) self.__recursBuildFileTree(root) self.model.dataChanged.connect(self.updateIndex) def __recursBuildFileTree(self, root): for item in self.box.get_folder_contents(root.boxData): new = BoxItem.BoxItem(self.box, item) root.appendRow(new) if item['type'] == "folder": new.setIcon(self.folderIcon) self.__recursBuildFileTree(new) else: new.setIcon(self.fileIcon) self.treeView.setSortingEnabled(True) def rebuildModel(self): self.model.clear() root = BoxItem.BoxItem(self.box, self.box.get_folder('0')) root.setIcon(self.folderIcon) root.setIcon(self.folderIcon) self.model.appendRow(root) self.buildFileTree(root) def updateIndex(self, index): modified = index.model().itemFromIndex(index) parent = modified.parent() if parent is None: # Must be the root that was modified parent = modified parent.removeRows(0, parent.rowCount()) self.buildFileTree(parent) self.treeView def contextMenu(self, position): index = self.treeView.selectedIndexes()[0] selected = index.model().itemFromIndex(index) selected.showContextMenu(self.treeView, position)
class Widget(QWidget): def __init__(self, panel): super(Widget, self).__init__(panel) layout = QVBoxLayout() self.setLayout(layout) layout.setSpacing(0) self.searchEntry = SearchLineEdit() self.treeView = QTreeView(contextMenuPolicy=Qt.CustomContextMenu) self.textView = QTextBrowser() applyButton = QToolButton(autoRaise=True) editButton = QToolButton(autoRaise=True) addButton = QToolButton(autoRaise=True) self.menuButton = QPushButton(flat=True) menu = QMenu(self.menuButton) self.menuButton.setMenu(menu) splitter = QSplitter(Qt.Vertical) top = QHBoxLayout() layout.addLayout(top) splitter.addWidget(self.treeView) splitter.addWidget(self.textView) layout.addWidget(splitter) splitter.setSizes([200, 100]) splitter.setCollapsible(0, False) top.addWidget(self.searchEntry) top.addWidget(applyButton) top.addSpacing(10) top.addWidget(addButton) top.addWidget(editButton) top.addWidget(self.menuButton) # action generator for actions added to search entry def act(slot, icon=None): a = QAction(self, triggered=slot) self.addAction(a) a.setShortcutContext(Qt.WidgetWithChildrenShortcut) icon and a.setIcon(icons.get(icon)) return a # hide if ESC pressed in lineedit a = act(self.slotEscapePressed) a.setShortcut(QKeySequence(Qt.Key_Escape)) # import action a = self.importAction = act(self.slotImport, 'document-open') menu.addAction(a) # export action a = self.exportAction = act(self.slotExport, 'document-save-as') menu.addAction(a) # apply button a = self.applyAction = act(self.slotApply, 'edit-paste') applyButton.setDefaultAction(a) menu.addSeparator() menu.addAction(a) # add button a = self.addAction_ = act(self.slotAdd, 'list-add') a.setShortcut(QKeySequence(Qt.Key_Insert)) addButton.setDefaultAction(a) menu.addSeparator() menu.addAction(a) # edit button a = self.editAction = act(self.slotEdit, 'document-edit') a.setShortcut(QKeySequence(Qt.Key_F2)) editButton.setDefaultAction(a) menu.addAction(a) # set shortcut action a = self.shortcutAction = act(self.slotShortcut, 'preferences-desktop-keyboard-shortcuts') menu.addAction(a) # delete action a = self.deleteAction = act(self.slotDelete, 'list-remove') a.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Delete)) menu.addAction(a) # restore action a = self.restoreAction = act(self.slotRestore) menu.addSeparator() menu.addAction(a) # help button a = self.helpAction = act(self.slotHelp, 'help-contents') menu.addSeparator() menu.addAction(a) self.treeView.setSelectionBehavior(QTreeView.SelectRows) self.treeView.setSelectionMode(QTreeView.ExtendedSelection) self.treeView.setRootIsDecorated(False) self.treeView.setAllColumnsShowFocus(True) self.treeView.setModel(model.model()) self.treeView.setCurrentIndex(QModelIndex()) # signals self.searchEntry.returnPressed.connect(self.slotReturnPressed) self.searchEntry.textChanged.connect(self.updateFilter) self.treeView.doubleClicked.connect(self.slotDoubleClicked) self.treeView.customContextMenuRequested.connect(self.showContextMenu) self.treeView.selectionModel().currentChanged.connect(self.updateText) self.treeView.model().dataChanged.connect(self.updateFilter) # highlight text self.highlighter = highlight.Highlighter(self.textView.document()) # complete on snippet variables self.searchEntry.setCompleter(QCompleter([ ':icon', ':indent', ':menu', ':name', ':python', ':selection', ':set', ':symbol', ':template', ':template-run'], self.searchEntry)) self.readSettings() app.settingsChanged.connect(self.readSettings) app.translateUI(self) self.updateColumnSizes() self.setAcceptDrops(True) def dropEvent(self, ev): if not ev.source() and ev.mimeData().hasUrls(): filename = ev.mimeData().urls()[0].toLocalFile() if filename: ev.accept() from . import import_export import_export.load(filename, self) def dragEnterEvent(self, ev): if not ev.source() and ev.mimeData().hasUrls(): ev.accept() def translateUI(self): try: self.searchEntry.setPlaceholderText(_("Search...")) except AttributeError: pass # not in Qt 4.6 shortcut = lambda a: a.shortcut().toString(QKeySequence.NativeText) self.menuButton.setText(_("&Menu")) self.addAction_.setText(_("&Add...")) self.addAction_.setToolTip( _("Add a new snippet. ({key})").format(key=shortcut(self.addAction_))) self.editAction.setText(_("&Edit...")) self.editAction.setToolTip( _("Edit the current snippet. ({key})").format(key=shortcut(self.editAction))) self.shortcutAction.setText(_("Configure Keyboard &Shortcut...")) self.deleteAction.setText(_("&Remove")) self.deleteAction.setToolTip(_("Remove the selected snippets.")) self.applyAction.setText(_("A&pply")) self.applyAction.setToolTip(_("Apply the current snippet.")) self.importAction.setText(_("&Import...")) self.importAction.setToolTip(_("Import snippets from a file.")) self.exportAction.setText(_("E&xport...")) self.exportAction.setToolTip(_("Export snippets to a file.")) self.restoreAction.setText(_("Restore &Built-in Snippets...")) self.restoreAction.setToolTip( _("Restore deleted or changed built-in snippets.")) self.helpAction.setText(_("&Help")) self.searchEntry.setToolTip(_( "Enter text to search in the snippets list.\n" "See \"What's This\" for more information.")) self.searchEntry.setWhatsThis(''.join(map("<p>{0}</p>\n".format, ( _("Enter text to search in the snippets list, and " "press Enter to apply the currently selected snippet."), _("If the search text fully matches the value of the '{name}' variable " "of a snippet, that snippet is selected.").format(name="name"), _("If the search text starts with a colon ':', the rest of the " "search text filters snippets that define the given variable. " "After a space a value can also be entered, snippets will then " "match if the value of the given variable contains the text after " "the space."), _("E.g. entering {menu} will show all snippets that are displayed " "in the insert menu.").format(menu="<code>:menu</code>"), )))) def sizeHint(self): return self.parent().mainwindow().size() / 4 def readSettings(self): data = textformats.formatData('editor') self.textView.setFont(data.font) self.textView.setPalette(data.palette()) def showContextMenu(self, pos): """Called when the user right-clicks the tree view.""" self.menuButton.menu().popup(self.treeView.viewport().mapToGlobal(pos)) def slotReturnPressed(self): """Called when the user presses Return in the search entry. Applies current snippet.""" name = self.currentSnippet() if name: view = self.parent().mainwindow().currentView() insert.insert(name, view) self.parent().hide() # make configurable? view.setFocus() def slotEscapePressed(self): """Called when the user presses ESC in the search entry. Hides the panel.""" self.parent().hide() self.parent().mainwindow().currentView().setFocus() def slotDoubleClicked(self, index): name = self.treeView.model().name(index) view = self.parent().mainwindow().currentView() insert.insert(name, view) def slotAdd(self): """Called when the user wants to add a new snippet.""" edit.Edit(self, None) def slotEdit(self): """Called when the user wants to edit a snippet.""" name = self.currentSnippet() if name: edit.Edit(self, name) def slotShortcut(self): """Called when the user selects the Configure Shortcut action.""" from widgets import shortcuteditdialog name = self.currentSnippet() if name: collection = self.parent().snippetActions action = actions.action(name, None, collection) default = collection.defaults().get(name) mgr = actioncollectionmanager.manager(self.parent().mainwindow()) cb = mgr.findShortcutConflict dlg = shortcuteditdialog.ShortcutEditDialog(self, cb, (collection, name)) if dlg.editAction(action, default): mgr.removeShortcuts(action.shortcuts()) collection.setShortcuts(name, action.shortcuts()) self.treeView.update() def slotDelete(self): """Called when the user wants to delete the selected rows.""" rows = sorted(set(i.row() for i in self.treeView.selectedIndexes()), reverse=True) if rows: for row in rows: name = self.treeView.model().names()[row] self.parent().snippetActions.setShortcuts(name, []) self.treeView.model().removeRow(row) self.updateFilter() def slotApply(self): """Called when the user clicks the apply button. Applies current snippet.""" name = self.currentSnippet() if name: view = self.parent().mainwindow().currentView() insert.insert(name, view) def slotImport(self): """Called when the user activates the import action.""" filetypes = "{0} (*.xml);;{1} (*)".format(_("XML Files"), _("All Files")) caption = app.caption(_("dialog title", "Import Snippets")) filename = None filename = QFileDialog.getOpenFileName(self, caption, filename, filetypes)[0] if filename: from . import import_export import_export.load(filename, self) def slotExport(self): """Called when the user activates the export action.""" allrows = [row for row in range(model.model().rowCount()) if not self.treeView.isRowHidden(row, QModelIndex())] selectedrows = [i.row() for i in self.treeView.selectedIndexes() if i.column() == 0 and i.row() in allrows] names = self.treeView.model().names() names = [names[row] for row in selectedrows or allrows] filetypes = "{0} (*.xml);;{1} (*)".format(_("XML Files"), _("All Files")) n = len(names) caption = app.caption(_("dialog title", "Export {num} Snippet", "Export {num} Snippets", n).format(num=n)) filename = QFileDialog.getSaveFileName(self, caption, None, filetypes)[0] if filename: from . import import_export try: import_export.save(names, filename) except (IOError, OSError) as e: QMessageBox.critical(self, _("Error"), _( "Can't write to destination:\n\n{url}\n\n{error}").format( url=filename, error=e.strerror)) def slotRestore(self): """Called when the user activates the Restore action.""" from . import restore dlg = restore.RestoreDialog(self) dlg.setWindowModality(Qt.WindowModal) dlg.populate() dlg.show() dlg.finished.connect(dlg.deleteLater) def slotHelp(self): """Called when the user clicks the small help button.""" userguide.show("snippets") def currentSnippet(self): """Returns the name of the current snippet if it is visible.""" row = self.treeView.currentIndex().row() if row != -1 and not self.treeView.isRowHidden(row, QModelIndex()): return self.treeView.model().names()[row] def updateFilter(self): """Called when the text in the entry changes, updates search results.""" text = self.searchEntry.text() ltext = text.lower() filterVars = text.startswith(':') if filterVars: try: fvar, fval = text[1:].split(None, 1) fhide = lambda v: v.get(fvar) in (True, None) or fval not in v.get(fvar) except ValueError: fvar = text[1:].strip() fhide = lambda v: not v.get(fvar) for row in range(self.treeView.model().rowCount()): name = self.treeView.model().names()[row] nameid = snippets.get(name).variables.get('name', '') if filterVars: hide = fhide(snippets.get(name).variables) elif nameid == text: i = self.treeView.model().createIndex(row, 0) self.treeView.selectionModel().setCurrentIndex(i, QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows) hide = False elif nameid.lower().startswith(ltext): hide = False elif ltext in snippets.title(name).lower(): hide = False else: hide = True self.treeView.setRowHidden(row, QModelIndex(), hide) self.updateText() def updateText(self): """Called when the current snippet changes.""" name = self.currentSnippet() self.textView.clear() if name: s = snippets.get(name) self.highlighter.setPython('python' in s.variables) self.textView.setPlainText(s.text) def updateColumnSizes(self): self.treeView.resizeColumnToContents(0) self.treeView.resizeColumnToContents(1)
class App(QWidget): FROM, SUBJECT, DATE = range(3) print("#####################") def __init__(self): super().__init__() global list self.title = 'Traitement de CV' self.left = 100 self.top = 100 self.width = 940 self.height = 840 self.initUI() def initUI(self): self.setWindowTitle(self.title) self.setStyle(QStyleFactory.create("Fusion")) self.setGeometry(self.left, self.top, self.width, self.height) print(PyQt5.QtWidgets.QStyleFactory.keys()) self.dataGroupBox = QGroupBox("CVs") self.dataView = QTreeView() self.dataView.setStyle((QStyleFactory.create("Fusion"))) self.dataView.setRootIsDecorated(False) self.dataView.setAlternatingRowColors(True) advancedLayout = QGridLayout() #okButtonq.clicked.connect(self.runFunction()) self.textDisplay = QTextEdit() self.textDisplay.setStyle(QStyleFactory.create("Fusion")) #ConsulAppObject.textEdit = QtWidgets.QInputDialog font = PyQt5.QtGui.QFont() font.setFamily("Leelawadee UI") font.setPointSize(10) font.setBold(False) font.setItalic(True) font.setWeight(50) self.textDisplay.setStyleSheet( "alternate-background-color: rgb(50, 180,255);\n" "background-color: rgb(225, 225, 225);\n" "font: 9pt \"Leelawadee UI\";\n" "") self.textDisplay.setObjectName("textDisplay") self.dataView.setStyleSheet( "selection-background-color:rgb(50, 180,255);\n" "border: 1px outset rgb(0, 150,255);\n" "background-color: rgb(235, 235, 235);\n" "font: 10pt \"Leelawadee UI\";\n" "") self.dataView.setStyle(QStyleFactory.create("Fusion")) dataLayout = QHBoxLayout() dataLayout.addWidget(self.dataView) dataLayout.addWidget(self.textDisplay) self.dataGroupBox.setLayout(dataLayout) model = self.CreateModel(self) self.dataView.setModel(model) self.infoDisplay = QLabel() self.infoDisplay.setStyleSheet( "selection-background-color:rgb(50, 180,255); \n" "alternate-background-color: rgb(0, 0,0);\n" "border: 1px outset rgb(0, 150,255);\n" "background-color: rgb(235, 235, 235);\n" "font: 10pt \"Leelawadee UI\";\n" "") self.infoDisplay.setText("Likelihood of a match") self.infoDisplay.setAlignment(QtCore.Qt.AlignCenter) #mainLayout = QVBoxLayout() #mainLayout.addWidget(self.dataGroupBox) self.setLayout(advancedLayout) self.progressBar = PyQt5.QtWidgets.QProgressBar() self.progressBar.setStyleSheet("background-color: rgb(200, 200, 200);") self.progressBar.setValue(0) self.progressBar.setMinimum(0) self.progressBar.setMaximum(100) insertButton = QPushButton("Insert", self) insertButton.clicked.connect(lambda: self.InsertMethod(model)) deleteButton = QPushButton("Delete") deleteButton.clicked.connect(lambda: self.RemoveEntry(model)) applyButton = QPushButton("Apply") applyButton.setStyle(QStyleFactory.create("Fusion")) applyButton.setStyleSheet("alternate-background-color: rgb(0,0,0);\n" "background-color: rgb(50, 180,255);\n" "font: 10pt \"Leelawadee UI\";\n" "") applyButton.clicked.connect(lambda: self.ApplyButton()) selectButton = QPushButton("Select Text") selectButton.setStyle(QStyleFactory.create("Fusion")) selectButton.setStyleSheet("alternate-background-color: rgb(0,0,0);\n" "font: 10pt \"Leelawadee UI\";\n" "") selectButton.clicked.connect( lambda: self.OnChangeCurrentProjectClicked(model)) cancelButton = QPushButton("Close") cancelButton.setStyle(QStyleFactory.create("Fusion")) cancelButton.setStyleSheet("alternate-background-color: rgb(0,0,0);\n" "background-color: rgb(225,225,225);\n" "font: 8pt \"Leelawadee UI\";\n" "") cancelButton.clicked.connect(exit) hbox = QHBoxLayout() hbox.addStretch(1) advancedLayout.addWidget(applyButton, 2, 0, 1, 1) advancedLayout.addWidget(self.dataGroupBox, 0, 0, 1, 3) advancedLayout.addWidget(self.progressBar, 2, 1, 1, 2) advancedLayout.addWidget(self.infoDisplay, 3, 1, 1, 2) advancedLayout.addWidget(selectButton, 3, 0, 1, 1) advancedLayout.addWidget(insertButton, 4, 0, 1, 1) advancedLayout.addWidget(deleteButton, 5, 0, 1, 1) advancedLayout.addWidget(cancelButton, 6, 0, 1, 1) self.show() def animation(self, txtToDisplay): for c in itertools.cycle(['.', '..', '...', '\\']): print(threading.active_count()) self.textEdit.setPlaceholderText(txtToDisplay + " " + c) if threading.active_count() == 3: return time.sleep(0.5) def ReadDat(self, model): file = QtCore.QFile("save.dat") file.open(QtCore.QIODevice.ReadOnly) goIn = QtCore.QDataStream(file) array = [] self.ReadItem(model.invisibleRootItem(), array) file.close() return array def ReadItem(self, item, array=[]): for i in range(0, item.rowCount()): child = item.child(i, 0) print(child.text()) array.append(child.text()) # -------------------------------------------------------------------------------------------------------------------- def OnChangeCurrentProjectClicked(self, model): # create text file global file, f, text # open data stream item = model.invisibleRootItem() text = self.ChangeCurrentProject(item) print(txts) print(txts + "\\" + text) f = docx.Document(file) firstDraft = [p.text for p in f.paragraphs] self.textDisplay.setPlainText(" ".join(firstDraft)) def ChangeCurrentProject(self, item): file = open("CurrentProjectInf.bin", "wb") if item.rowCount() != None: a = self.dataView.selectedIndexes() if a != "[]": b = a[0].row() child = item.child(b, 0) c = child.text() return c file.close() # -------------------------------------------------------------------------------------------------------------------- def RemoveEntry(self, model): a = self.dataView.selectedIndexes() print(a) for x in range(0, len(a)): print(a[x]) b = a[x].row() item = model.invisibleRootItem() model.removeRow(b) # -------------------------------------------------------------------------------------------------------------------- def ApplyButton(self): #self.save_item() global f, file, txts if file == "": return result = MainOperations.Main.Naive(self, file) self.progressBar.setValue(result) def save_item(self, item, out): if item.rowCount() != None: for i in range(0, item.rowCount()): child = item.child(i, 0) child.write(out) print(child.text()) if child != None: print("EEE") self.save_item(child, out) else: return def copy_rename(self, old_file_name, new_file_name): src_dir = os.curdir dst_dir = os.path.join(os.curdir, "subfolder") src_file = os.path.join(src_dir, old_file_name) shutil.copy(src_file, dst_dir) dst_file = os.path.join(dst_dir, old_file_name) new_dst_file_name = os.path.join(dst_dir, new_file_name) os.rename(dst_file, new_dst_file_name) # -------------------------------------------------------------------------------------------------------------------- def InsertMethod(self, model): global file file = self.openFileNameDialog() self.addModel(model, file, 'insert', 'insert') def openFileNameDialog(self): options = PyQt5.QtWidgets.QFileDialog.Options() file, _ = PyQt5.QtWidgets.QFileDialog.getOpenFileName( self, "QFileDialog.getOpenFileName()", "", "CV(*.docx);", options=options) options = PyQt5.QtWidgets.QFileDialog.Options() file, _ = PyQt5.QtWidgets.QFileDialog.getOpenFileName( self, "QFileDialog.getOpenFileName()", "", "All Files (*);;Python Files (*.py)", options=options) return file def addModel(self, model, From, subject, date): if From == "": return model.insertRow(0) model.setData(model.index(0, self.FROM), From) model.setData(model.index(0, self.SUBJECT), subject) model.setData(model.index(0, self.DATE), date) # -------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------- def CreateModel(self, parent): model = QStandardItemModel(0, 1, parent) model.setHeaderData(self.FROM, Qt.Horizontal, "Potential Texts Names") model.setHeaderData(self.SUBJECT, Qt.Horizontal, "Path") model.setHeaderData(self.DATE, Qt.Horizontal, "Contains") return model
class MyMainWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.config_window() self.create_widgets() self.config_widgets() self.create_menubar() self.bind_widgets() self.show_widgets() def config_window(self): self.setWindowTitle('DirectoPy') self.setMinimumHeight(600) self.setMinimumWidth(1000) def create_widgets(self): self.central_widget = QWidget() self.main_layout = QGridLayout() self.moveup_button = QPushButton('Collapse all', self) self.goto_lineedit = QLineEdit('', self) self.goto_button = QPushButton('Go', self) self.folder_view = QTreeView(self) self.file_view = QTreeView(self) self.folder_model = QFileSystemModel(self) self.file_model = QFileSystemModel(self) def config_widgets(self): self.main_layout.addWidget(self.moveup_button, 0, 0) self.main_layout.addWidget(self.goto_lineedit, 0, 1, 1, 2) self.main_layout.addWidget(self.goto_button, 0, 3) self.main_layout.addWidget(self.folder_view, 1, 0, 1, 2) self.main_layout.addWidget(self.file_view, 1, 2, 1, 2) self.central_widget.setLayout(self.main_layout) # Кнопка "вверх" self.moveup_button.setMaximumWidth(100) # Кнопка "перейти" self.goto_button.setMaximumWidth(70) self.setCentralWidget(self.central_widget) # панели self.folder_model.setRootPath(None) self.folder_model.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot) self.folder_view.setModel(self.folder_model) self.folder_view.setRootIndex(self.folder_model.index(None)) self.folder_view.clicked[QModelIndex].connect(self.clicked_onfolder) self.folder_view.hideColumn(1) self.folder_view.hideColumn(2) self.folder_view.hideColumn(3) self.file_model.setFilter(QDir.Files) self.file_view.setModel(self.file_model) self.file_model.setReadOnly(False) self.file_view.setColumnWidth(0, 200) self.file_view.setSelectionMode(QAbstractItemView.ExtendedSelection) # открытие папки при нажати на неё в окне папок def clicked_onfolder(self, index): selection_model = self.folder_view.selectionModel() index = selection_model.currentIndex() dir_path = self.folder_model.filePath(index) self.file_model.setRootPath(dir_path) self.file_view.setRootIndex(self.file_model.index(dir_path)) # ф-я открытия нового файла def open_file(self): index = self.file_view.selectedIndexes() if not index: return else: index = index[0] file_path = self.file_model.filePath(index).replace('/', '\\') print(file_path) self.file_view.update() # ф-я создания нового файла def new_file(self): global file_name index = self.folder_view.selectedIndexes() if len(index) > 0: path = self.folder_model.filePath(index[0]) for i in range(1, 9999999999999999): if not os.path.isfile(os.path.join(path, "newfile{}.txt".format(i))): file_name = os.path.join(path, "newfile{}.txt".format(i)) break file_name = os.path.abspath(file_name) open(file_name, 'w').close() else: print("Please, select folder") # ф-я удаления файла def delete_file(self): indexes = self.file_view.selectedIndexes() for i in indexes: self.file_model.remove(i) # ф-я переименования файла def rename_file(self): index = self.file_view.selectedIndexes() if not index: return else: index = index[0] self.file_view.edit(index) # ф-я копирования файла def copy_file(self): print("COPY") ask = QFileDialog.getExistingDirectory(self, "Open Directory", "C:\\", QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) new_path = ask.replace('\\', '/') indexes = self.file_view.selectedIndexes()[::4] for i in indexes: new_filename = new_path + '/' + self.file_model.fileName(i) copy2(self.file_model.filePath(i), new_filename) # ф-я возвращения к корню пути def colapse(self): self.folder_view.collapseAll() # ф-я перемещения в заданную директорию def go_to(self): dir_path = self.goto_lineedit.text().replace('\\', '/') print(dir_path) self.file_model.setRootPath(dir_path) self.file_view.setRootIndex(self.file_model.index(dir_path)) #self.file_model.setRootPath() # ф-я перемещения файла def move_file(self): print("MOVE") ask = QFileDialog.getExistingDirectory(self, "Open Directory", "C:\\", QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) if ask == '': return new_path = ask.replace('\\', '/') indexes = self.file_view.selectedIndexes()[::4] for i in indexes: new_filename = new_path + '/' + self.file_model.fileName(i) move(self.file_model.filePath(i), new_filename) # ф-я создания новой папки def new_folder(self): global file_name index = self.folder_view.selectedIndexes() if len(index) > 0: path = self.folder_model.filePath(index[0]) for i in range(1, 9999999999999999): if not os.path.isdir(os.path.join(path, "newfolder{}".format(i))): file_name = os.path.join(path, "newfolder{}".format(i)) break file_name = os.path.abspath(file_name) os.mkdir(file_name) else: print("Please, select folder") # ф-я удаления папки def delete_folder(self): indexes = self.folder_view.selectedIndexes() for i in indexes: self.folder_model.remove(i) # ф-я переименования папки def rename_folder(self): index = self.folder_view.selectedIndexes() if not index: return else: index = index[0] self.folder_view.edit(index) # ф-я закрытия окна файлового менеджера def exit_application(self): print("EXIT") self.close() # задавание функции каждой кнопке def bind_widgets(self): self.open_file_action.triggered.connect(self.open_file) self.new_file_action.triggered.connect(self.new_file) self.delete_file_action.triggered.connect(self.delete_file) self.rename_file_action.triggered.connect(self.rename_file) self.copy_file_action.triggered.connect(self.copy_file) self.move_file_action.triggered.connect(self.move_file) self.exit_action.triggered.connect(self.exit_application) self.new_folder_action.triggered.connect(self.new_folder) self.delete_folder_action.triggered.connect(self.delete_folder) self.rename_folder_action.triggered.connect(self.rename_folder) self.goto_button.clicked.connect(partial(self.go_to)) self.moveup_button.clicked.connect(partial(self.colapse)) # создание меню def create_menubar(self): self.exit_action = QAction('Exit', self) self.exit_action.setShortcut('Ctrl+Q') self.new_file_action = QAction('New file', self) self.new_file_action.setShortcut('F4') self.open_file_action = QAction('Open file', self) self.open_file_action.setShortcut('F3') self.rename_file_action = QAction('Rename file', self) self.rename_file_action.setShortcut('F2') self.delete_file_action = QAction('Remove file', self) self.delete_file_action.setShortcut(QKeySequence.Delete) self.copy_file_action = QAction('Copy folder...', self) self.copy_file_action.setShortcut(QKeySequence.Copy) self.move_file_action = QAction('Move folder...', self) self.move_file_action.setShortcut(QKeySequence.Cut) self.new_folder_action = QAction('New folder', self) self.new_folder_action.setShortcut('Ctrl+Shift+N') self.delete_folder_action = QAction('Delete folder', self) self.delete_folder_action.setShortcut('Ctrl+Shift+Del') self.rename_folder_action = QAction('Rename folder', self) self.rename_folder_action.setShortcut('Ctrl+Shift+R') self.menubar = self.menuBar() self.file_menu = self.menubar.addMenu('File') self.file_menu.addAction(self.new_file_action) self.file_menu.addAction(self.open_file_action) self.file_menu.addAction(self.rename_file_action) self.file_menu.addAction(self.delete_file_action) self.file_menu.addAction(self.copy_file_action) self.file_menu.addAction(self.move_file_action) self.file_menu.addSeparator() self.file_menu.addAction(self.exit_action) self.folder_menu = self.menubar.addMenu('Folder') self.folder_menu.addAction(self.new_folder_action) self.folder_menu.addAction(self.delete_folder_action) self.folder_menu.addAction(self.rename_folder_action) def show_widgets(self): self.setLayout(self.main_layout)
class ScorePartsWidget(QSplitter): def __init__(self, parent): super(ScorePartsWidget, self).__init__(parent) self.typesLabel = QLabel() self.typesView = QTreeView(selectionMode=QTreeView.ExtendedSelection, selectionBehavior=QTreeView.SelectRows, animated=True, headerHidden=True) self.scoreLabel = QLabel() self.scoreView = widgets.treewidget.TreeWidget( selectionMode=QTreeView.ExtendedSelection, selectionBehavior=QTreeView.SelectRows, headerHidden=True, animated=True, dragDropMode=QTreeView.InternalMove) self.addButton = QPushButton(icon=icons.get("list-add")) self.removeButton = QPushButton(icon=icons.get("list-remove")) self.upButton = QToolButton(icon=icons.get("go-up")) self.downButton = QToolButton(icon=icons.get("go-down")) self.partSettings = QStackedWidget() w = QWidget() self.addWidget(w) layout = QVBoxLayout(spacing=0) w.setLayout(layout) layout.addWidget(self.typesLabel) layout.addWidget(self.typesView) layout.addWidget(self.addButton) w = QWidget() self.addWidget(w) layout = QVBoxLayout(spacing=0) w.setLayout(layout) layout.addWidget(self.scoreLabel) layout.addWidget(self.scoreView) box = QHBoxLayout(spacing=0) layout.addLayout(box) box.addWidget(self.removeButton) box.addWidget(self.upButton) box.addWidget(self.downButton) self.addWidget(self.partSettings) self.typesView.setModel(parts.model()) app.translateUI(self) # signal connections self.addButton.clicked.connect(self.slotAddButtonClicked) self.removeButton.clicked.connect(self.slotRemoveButtonClicked) self.typesView.doubleClicked.connect(self.slotDoubleClicked) self.scoreView.currentItemChanged.connect(self.slotCurrentItemChanged) self.upButton.clicked.connect(self.scoreView.moveSelectedChildrenUp) self.downButton.clicked.connect( self.scoreView.moveSelectedChildrenDown) def translateUI(self): bold = "<b>{0}</b>".format self.typesLabel.setText(bold(_("Available parts:"))) self.scoreLabel.setText(bold(_("Score:"))) self.addButton.setText(_("&Add")) self.removeButton.setText(_("&Remove")) self.upButton.setToolTip(_("Move up")) self.downButton.setToolTip(_("Move down")) def slotDoubleClicked(self, index): self.addParts([index]) def slotAddButtonClicked(self): self.addParts(self.typesView.selectedIndexes()) def addParts(self, indexes): """Adds the parts for the given indexes.""" # add to current if that is a container type currentItem = self.scoreView.currentItem() for index in indexes: category = index.internalPointer() if category: part = category.items[index.row()] box = QGroupBox(self.partSettings) self.partSettings.addWidget(box) # determine the parent: current or root if currentItem and issubclass(part, currentItem.part.accepts()): parent = currentItem parent.setExpanded(True) else: parent = self.scoreView item = PartItem(parent, part, box) def slotCurrentItemChanged(self, item): if isinstance(item, PartItem): self.partSettings.setCurrentWidget(item.box) def slotRemoveButtonClicked(self): self.scoreView.removeSelectedItems() def clear(self): """Called when the user clicks the clear button on this page.""" self.scoreView.clear() def rootPartItem(self): """Returns the invisibleRootItem(), representing the tree of parts in the score view.""" return self.scoreView.invisibleRootItem()
class FileTreeView(QWidget): on_menu_select = pyqtSignal(str, str) on_dir_change = pyqtSignal() def __init__(self, parent: Application): super().__init__() self.stopped = threading.Event() self.tree = QTreeView() self.handle = self.Handler(self) self.get_data_file() self.model = QtGui.QStandardItemModel() self.item_construct = {} v_box = QVBoxLayout() v_box.addWidget(self.finder()) v_box.addWidget(self.tree_view()) self.setLayout(v_box) self.watch_dog() parent.app_close.connect(self.exit_push) self.tree.setAlternatingRowColors(True) def exit_push(self): self.stopped.set() def finder(self): w_find = QLineEdit() w_find.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) w_find.textChanged.connect(self.sort_list) w_find.setPlaceholderText("Search file..") return w_find def sort_list(self, text): list_sort = [] if text == "": self.show_tree(self.list_file) else: for data in self.list_file: if text.lower() in data.name().lower(): list_sort.append(data) self.show_tree(list_sort) def watch_dog(self): watch = ProcessRunnable(target=watch_winform, args=(get_data_folder(), self.handle, self.stopped)) watch.start() class Handler(watchdog.events.PatternMatchingEventHandler): def __init__(self, parent): super().__init__() self.parent = parent def on_created(self, event): print("Watchdog received created event", event.src_path, sep=" : ") asyncio.run( self.parent.file_change('create', event.src_path, event.is_directory)) def on_modified(self, event): print("Watchdog received modified event", event.src_path, sep=" : ") asyncio.run( self.parent.file_change('modify', event.src_path, event.is_directory)) def on_moved(self, event): print("Watchdog received move event", event.src_path, event.dest_path, sep=" : ") asyncio.run( self.parent.file_change('move', event.src_path, event.is_directory, event.dest_path)) def on_deleted(self, event): print("Watchdog received delete event", event.src_path, sep=" : ") asyncio.run( self.parent.file_change('delete', event.src_path, event.is_directory)) async def file_change(self, tpe, old, is_directory, new=""): if tpe == "move": self.import_single(self.model.invisibleRootItem(), new) self.remove_single(old) elif tpe == "delete": self.remove_single(old) elif tpe == "create": self.import_single(self.model.invisibleRootItem(), old) if is_directory: self.on_dir_change.emit() def get_data_file(self): self.list_file = [] self.create_list(get_data_folder()) def create_list(self, dir): lst = os.listdir(path=dir) for f in lst: path = os.path.join(dir, f) file = MyFile() if os.path.isdir(path): file.setParentName(os.path.basename(dir)) file.setParent(dir) file.setName(f) file.setDir(True) self.list_file.append(file) self.create_list(path) else: file.setParentName(os.path.basename(dir)) file.setParent(dir) file.setName(f) file.setDir(False) self.list_file.append(file) def tree_view(self): self.tree.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self.open_menu) self.tree.doubleClicked.connect(self.open_event) # self.model.itemChanged.connect(self.data_change) self.tree.setModel(self.model) self.show_tree(self.list_file) return self.tree def show_tree(self, list_file): self.model.clear() self.model.setHorizontalHeaderLabels(['List API']) self.tree.header().setDefaultSectionSize(180) parent = self.model.invisibleRootItem() self.item_construct = {} self.import_data(parent, list_file) self.tree.expandAll() def import_data_by_path(self, parent, path: list, index): if index < len(path): full = os.sep.join(path[:index + 1]) if full in self.item_construct: item = self.item_construct[full] else: item = QStandardItem(path[index]) if os.path.isfile(os.path.join(get_data_folder(), full)): item.setToolTip( self.read_description( os.path.join(get_data_folder(), full))) item.setEditable(False) if not os.path.isdir(os.path.join(get_data_folder(), full)): item.setIcon(QIcon(get_icon_link("text_snippet.svg"))) else: item.setIcon(QIcon(get_icon_link("folder_yellow.svg"))) item.setData(full) parent.appendRow(item) self.item_construct[full] = item self.import_data_by_path(item, path, index + 1) def read_description(self, path): try: data = json.loads(open(path, encoding='utf-8').read()) json_data = APIData() json_data.construct(data) x = json_data.parseSave().description() if x.isspace() or x == "": return ".." else: return x except Exception as ex: print(ex) return ".." def import_data(self, parent, list_data): for i in list_data: self.import_single(parent, i) def import_single(self, parent, file_path): path = self.path_extract(file_path) self.import_data_by_path(parent, path, 0) def remove_single(self, file_path): path = self.path_extract(file_path) full = os.sep.join(path[:len(path)]) if full in self.item_construct: item = self.item_construct[full] (item.parent() or self.model.invisibleRootItem()).removeRow(item.row()) del self.item_construct[full] def path_extract(self, file_path): if isinstance(file_path, MyFile): path = os.path.join(file_path.parent(), file_path.name()) else: path = file_path path = path.replace(get_data_folder(), "") if path.startswith(os.sep): path = path.replace(os.sep, "", 1) path = path.split(os.sep) return path def open_menu(self, position): indexes = self.tree.selectedIndexes() level = 0 data = "" item = None if len(indexes) > 0: index = indexes[0] item = self.model.itemFromIndex(index) data = item.data() data = os.path.join(get_data_folder(), data) if os.path.isdir(data): level = 1 else: level = 2 menu = QMenu() menu.setStyleSheet(open(get_stylesheet()).read()) rename_action = QAction(QIcon(get_icon_link('edit.svg')), '&Rename', self) rename_action.setStatusTip('Rename') new_action = QAction(QIcon(get_icon_link('create_new_folder.svg')), '&New Folder', self) new_action.setStatusTip('New Folder') refresh_action = QAction(QIcon(get_icon_link('refresh.svg')), '&Refresh', self) refresh_action.setStatusTip('Refresh') delete_action = QAction(QIcon(get_icon_link('delete_forever.svg')), '&Delete', self) delete_action.setStatusTip('Delete') open_action = QAction(QIcon(get_icon_link('open_in_new.svg')), '&Open', self) open_action.setStatusTip('Open file') expand_action = QAction(QIcon(), '&Expand', self) expand_action.setStatusTip('Expand') collapse_action = QAction(QIcon(), '&Collapse', self) collapse_action.setStatusTip('Collapse') duplicate_action = QAction(QIcon(get_icon_link('content_copy.svg')), '&Duplicate', self) duplicate_action.setStatusTip('Duplicate') copy_action = QAction(QIcon(get_icon_link('content_copy.svg')), '&Copy', self) copy_action.setStatusTip('Copy') move_action = QAction(QIcon(get_icon_link('zoom_out_map.svg')), '&Move', self) move_action.setStatusTip('Move') if level == 1: menu.addAction(rename_action) menu.addAction(new_action) menu.addSeparator() menu.addAction(refresh_action) menu.addAction(expand_action) menu.addAction(collapse_action) menu.addSeparator() menu.addAction(delete_action) elif level == 2: menu.addAction(open_action) menu.addAction(new_action) menu.addAction(refresh_action) menu.addSeparator() menu.addAction(rename_action) menu.addAction(duplicate_action) menu.addAction(copy_action) menu.addAction(move_action) menu.addSeparator() menu.addAction(delete_action) else: menu.addAction(new_action) menu.addAction(refresh_action) action = menu.exec_(self.tree.viewport().mapToGlobal(position)) if action == open_action: if data != "": self.on_menu_select.emit("open", data) elif action == refresh_action: self.get_data_file() self.show_tree(self.list_file) elif action == expand_action: if item is not None: self.tree.expand(item.index()) elif action == collapse_action: if item is not None: self.tree.collapse(item.index()) elif action == delete_action: if data != "": msg = QMessageBox() msg.setStyleSheet(open(get_stylesheet()).read()) msg.setIcon(QMessageBox.Warning) msg.setBaseSize(QSize(500, 300)) msg.setText("Delete file.") msg.setInformativeText("Are you sure to detele " + os.path.basename(data) + "?") msg.setWindowTitle("Delete Warning!!!") msg.addButton('Delete', QMessageBox.YesRole) msg.addButton('Move to Trash', QMessageBox.YesRole) msg.addButton('Cancel', QMessageBox.NoRole) rs = msg.exec_() if rs == 0: if os.path.isdir(data): shutil.rmtree(data) else: os.remove(data) elif rs == 1: send2trash(data) elif action == new_action: if data == "": data = get_data_folder() # input_name = QInputDialog() # input_name.setStyleSheet(open(get_stylesheet()).read()) # text, ok = input_name.getText(self, 'New Folder', 'Folder name:') inp = QComboDialog('New Folder', 'Folder name:', QComboDialog.Text) ok = inp.exec_() if ok and inp.select: if os.path.isdir(data): try: os.mkdir(os.path.join(data, inp.select)) except Exception as ex: alert = Alert("Error", "Create folder error", str(ex)) alert.exec_() print(ex) else: new = os.path.join(os.path.dirname(data), inp.select) try: os.mkdir(new) except Exception as ex: alert = Alert("Error", "Create folder error", str(ex)) alert.exec_() print(ex) elif action == rename_action: if data != "": # input_name = QInputDialog() # input_name.setStyleSheet(open(get_stylesheet()).read()) # text, ok = input_name.getText(self, 'Rename file', 'New name:') inp = QComboDialog('Rename file', 'New name:', QComboDialog.Text) ok = inp.exec_() if ok and inp.select: if os.path.isdir(data): new = os.path.join(os.path.dirname(data), inp.select) try: os.rename(data, new) except Exception as ex: alert = Alert("Error", "Rename folder error", str(ex)) alert.exec_() print(ex) else: filename, file_extension = os.path.splitext(data) new = os.path.join(os.path.dirname(data), inp.select + file_extension) try: os.rename(data, new) except Exception as ex: alert = Alert("Error", "Rename file error", str(ex)) alert.exec_() print(ex) elif action == move_action: if data != "": items = get_list_folder(get_data_folder(), get_data_folder()) # # item, ok = QInputDialog.getItem(self, "Select folder dialog", # "Select the destination folder", items, 0, False) inp = QComboDialog("Move", "Select the destination folder", QComboDialog.ComboBox, items) ok = inp.exec_() if ok and inp.select: folder = inp.select if inp.select.startswith(os.sep): folder = inp.select.replace(os.sep, "", 1) new = os.path.join(get_data_folder(), folder, os.path.basename(data)) try: os.rename(data, new) except Exception as ex: alert = Alert("Error", "Move file error", str(ex)) alert.exec_() print(ex) elif action == duplicate_action: if data != "": # input_name = QInputDialog() # input_name.setStyleSheet(open(get_stylesheet()).read()) # text, ok = input_name.getText(self, 'Duplicate file', 'New name:') inp = QComboDialog('Duplicate file', 'New name:', QComboDialog.Text) filename, file_extension = os.path.splitext(data) inp.set_init_text(filename) ok = inp.exec_() if ok and inp.select: new = os.path.join(os.path.dirname(data), inp.select + file_extension) try: copyfile(data, new) except Exception as ex: alert = Alert("Error", "Duplicate file error", str(ex)) alert.exec_() print(ex) elif action == copy_action: if data != "": items = get_list_folder(get_data_folder(), get_data_folder()) inp = QComboDialog("Copy", "Select the destination folder", QComboDialog.ComboBox, items) ok = inp.exec_() # item, ok = QInputDialog.getItem(self, "Select folder dialog", # "Select the destination folder", items, 0, False) if ok and inp.select: folder = inp.select if inp.select.startswith(os.sep): folder = inp.select.replace(os.sep, "", 1) new = os.path.join(get_data_folder(), folder, os.path.basename(data)) try: copyfile(data, new) except Exception as ex: alert = Alert("Error", "Copy file error", str(ex)) alert.exec_() print(ex) def open_event(self, index): item = self.model.itemFromIndex(index) if item is not None: data = item.data() data = os.path.join(get_data_folder(), data) if os.path.isdir(data): level = 1 else: level = 2 if data != "": if level == 2: self.on_menu_select.emit("open", data) elif level == 1: if not self.tree.isExpanded(item.index()): self.tree.collapse(item.index()) else: self.tree.expand(item.index())
class DirectoriesDialog(QMainWindow): def __init__(self, app, **kwargs): super().__init__(None, **kwargs) self.app = app self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS self.recentFolders = Recent(self.app, "recentFolders") self._setupUi() self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView) self.directoriesDelegate = DirectoriesDelegate() self.treeView.setItemDelegate(self.directoriesDelegate) self._setupColumns() self.app.recentResults.addMenu(self.menuLoadRecent) self.app.recentResults.addMenu(self.menuRecentResults) self.recentFolders.addMenu(self.menuRecentFolders) self._updateAddButton() self._updateRemoveButton() self._updateLoadResultsButton() self._setupBindings() def _setupBindings(self): self.scanButton.clicked.connect(self.scanButtonClicked) self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger) self.addFolderButton.clicked.connect(self.actionAddFolder.trigger) self.removeFolderButton.clicked.connect(self.removeFolderButtonClicked) self.treeView.selectionModel().selectionChanged.connect(self.selectionChanged) self.app.recentResults.itemsChanged.connect(self._updateLoadResultsButton) self.recentFolders.itemsChanged.connect(self._updateAddButton) self.recentFolders.mustOpenItem.connect(self.app.model.add_directory) self.directoriesModel.foldersAdded.connect(self.directoriesModelAddedFolders) self.app.willSavePrefs.connect(self.appWillSavePrefs) def _setupActions(self): # (name, shortcut, icon, desc, func) ACTIONS = [ ("actionLoadResults", "Ctrl+L", "", tr("Load Results..."), self.loadResultsTriggered), ("actionShowResultsWindow", "", "", tr("Results Window"), self.app.showResultsWindow), ("actionAddFolder", "", "", tr("Add Folder..."), self.addFolderTriggered), ] createActions(ACTIONS, self) def _setupMenu(self): self.menubar = QMenuBar(self) self.menubar.setGeometry(QRect(0, 0, 42, 22)) self.menuFile = QMenu(self.menubar) self.menuFile.setTitle(tr("File")) self.menuView = QMenu(self.menubar) self.menuView.setTitle(tr("View")) self.menuHelp = QMenu(self.menubar) self.menuHelp.setTitle(tr("Help")) self.menuLoadRecent = QMenu(self.menuFile) self.menuLoadRecent.setTitle(tr("Load Recent Results")) self.setMenuBar(self.menubar) self.menuFile.addAction(self.actionLoadResults) self.menuFile.addAction(self.menuLoadRecent.menuAction()) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionQuit) self.menuView.addAction(self.app.actionPreferences) self.menuView.addAction(self.actionShowResultsWindow) self.menuView.addAction(self.app.actionIgnoreList) self.menuHelp.addAction(self.app.actionShowHelp) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuView.menuAction()) self.menubar.addAction(self.menuHelp.menuAction()) # Recent folders menu self.menuRecentFolders = QMenu() self.menuRecentFolders.addAction(self.actionAddFolder) self.menuRecentFolders.addSeparator() # Recent results menu self.menuRecentResults = QMenu() self.menuRecentResults.addAction(self.actionLoadResults) self.menuRecentResults.addSeparator() def _setupUi(self): self.setWindowTitle(self.app.NAME) self.resize(420, 338) self.centralwidget = QWidget(self) self.verticalLayout = QVBoxLayout(self.centralwidget) self.promptLabel = QLabel(tr('Select folders to scan and press "Scan".'), self.centralwidget) self.verticalLayout.addWidget(self.promptLabel) self.treeView = QTreeView(self.centralwidget) self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows) self.treeView.setAcceptDrops(True) triggers = ( QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked ) self.treeView.setEditTriggers(triggers) self.treeView.setDragDropOverwriteMode(True) self.treeView.setDragDropMode(QAbstractItemView.DropOnly) self.treeView.setUniformRowHeights(True) self.verticalLayout.addWidget(self.treeView) self.horizontalLayout = QHBoxLayout() self.removeFolderButton = QPushButton(self.centralwidget) self.removeFolderButton.setIcon(QIcon(QPixmap(":/minus"))) self.removeFolderButton.setShortcut("Del") self.horizontalLayout.addWidget(self.removeFolderButton) self.addFolderButton = QPushButton(self.centralwidget) self.addFolderButton.setIcon(QIcon(QPixmap(":/plus"))) self.horizontalLayout.addWidget(self.addFolderButton) spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem1) self.loadResultsButton = QPushButton(self.centralwidget) self.loadResultsButton.setText(tr("Load Results")) self.horizontalLayout.addWidget(self.loadResultsButton) self.scanButton = QPushButton(self.centralwidget) self.scanButton.setText(tr("Scan")) self.scanButton.setDefault(True) self.horizontalLayout.addWidget(self.scanButton) self.verticalLayout.addLayout(self.horizontalLayout) self.setCentralWidget(self.centralwidget) self._setupActions() self._setupMenu() if self.app.prefs.directoriesWindowRect is not None: self.setGeometry(self.app.prefs.directoriesWindowRect) else: moveToScreenCenter(self) def _setupColumns(self): header = self.treeView.header() header.setStretchLastSection(False) header.setSectionResizeMode(0, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.Fixed) header.resizeSection(1, 100) def _updateAddButton(self): if self.recentFolders.isEmpty(): self.addFolderButton.setMenu(None) else: self.addFolderButton.setMenu(self.menuRecentFolders) def _updateRemoveButton(self): indexes = self.treeView.selectedIndexes() if not indexes: self.removeFolderButton.setEnabled(False) return self.removeFolderButton.setEnabled(True) def _updateLoadResultsButton(self): if self.app.recentResults.isEmpty(): self.loadResultsButton.setMenu(None) else: self.loadResultsButton.setMenu(self.menuRecentResults) # --- QWidget overrides def closeEvent(self, event): event.accept() if self.app.model.results.is_modified: title = tr("Unsaved results") msg = tr("You have unsaved results, do you really want to quit?") if not self.app.confirm(title, msg): event.ignore() if event.isAccepted(): QApplication.quit() # --- Events def addFolderTriggered(self): title = tr("Select a folder to add to the scanning list") flags = QFileDialog.ShowDirsOnly dirpath = str(QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags)) if not dirpath: return self.lastAddedFolder = dirpath self.app.model.add_directory(dirpath) self.recentFolders.insertItem(dirpath) def appWillSavePrefs(self): self.app.prefs.directoriesWindowRect = self.geometry() def directoriesModelAddedFolders(self, folders): for folder in folders: self.recentFolders.insertItem(folder) def loadResultsTriggered(self): title = tr("Select a results file to load") files = ";;".join([tr("dupeGuru Results (*.dupeguru)"), tr("All Files (*.*)")]) destination = QFileDialog.getOpenFileName(self, title, "", files) if destination: self.app.model.load_from(destination) self.app.recentResults.insertItem(destination) def removeFolderButtonClicked(self): self.directoriesModel.model.remove_selected() def scanButtonClicked(self): if self.app.model.results.is_modified: title = tr("Start a new scan") msg = tr("You have unsaved results, do you really want to continue?") if not self.app.confirm(title, msg): return self.app.model.start_scanning() def selectionChanged(self, selected, deselected): self._updateRemoveButton()
class FileTreeView(QWidget): on_menu_select = pyqtSignal(str, dict) def __init__(self): super().__init__() self.list_file = [] self.get_data_file() v_box = QVBoxLayout() v_box.addLayout(self.finder()) v_box.addWidget(self.tree_view()) self.setLayout(v_box) def finder(self): w_find = QLineEdit() w_find.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) w_find_button = QPushButton() w_find_button.setText("Find") w_find_button.setFixedWidth(50) h_box = QHBoxLayout() h_box.addWidget(w_find) h_box.addWidget(w_find_button, 1) return h_box def get_data_file(self): self.list_file = self.create_list(get_data_folder()) def create_list(self, dir): lst = os.listdir(path=dir) list_file = [] for f in lst: path = dir + "\\" + f file = {} if os.path.isdir(path): file["dir"] = dir file["name"] = f file["isDir"] = True file["child"] = self.create_list(path) else: file["dir"] = dir file["name"] = f file["isDir"] = False list_file.append(file) return list_file def tree_view(self): self.tree = QTreeView() self.tree.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self.openMenu) self.model = QtGui.QStandardItemModel() self.model.setHorizontalHeaderLabels(['List API']) self.tree.header().setDefaultSectionSize(180) self.tree.setModel(self.model) self.import_data(self.model.invisibleRootItem(), self.list_file) self.tree.expandAll() return self.tree def import_data(self, parent_item, list): for i in list: if i["isDir"]: item = QStandardItem(i["name"]) item.setEditable(False) item.setIcon(QIcon(get_icon_link("folder_yellow.svg"))) item.setData(i) parent_item.appendRow(item) self.import_data(item, i["child"]) else: item = QStandardItem(i["name"]) item.setEditable(False) item.setIcon(QIcon(get_icon_link("text_snippet.svg"))) item.setData(i) parent_item.appendRow(item) def openMenu(self, position): indexes = self.tree.selectedIndexes() level = 0 data = {} if len(indexes) > 0: index = indexes[0] item = self.model.itemFromIndex(index) data = item.data() if data['isDir']: level = 1 else: level = 2 menu = QMenu() menu.setStyleSheet(open('stylesheet/default.qss').read()) # Create preference action rename_action = QAction(QIcon(get_icon_link('edit.svg')), '&Rename', self) rename_action.setStatusTip('Rename') # exitAction.triggered.connect(self.exitCall) # Create preference action new_action = QAction(QIcon(get_icon_link('create_new_folder.svg')), '&New Folder', self) new_action.setStatusTip('New Folder') # exitAction.triggered.connect(self.exitCall) # Create preference action refresh_action = QAction(QIcon(get_icon_link('refresh.svg')), '&Refresh', self) refresh_action.setStatusTip('Refresh') # exitAction.triggered.connect(self.exitCall) # Create preference action delete_action = QAction(QIcon(get_icon_link('delete_forever.svg')), '&Delete', self) delete_action.setStatusTip('Delete') # exitAction.triggered.connect(self.exitCall) # Create preference action open_action = QAction(QIcon(get_icon_link('open_in_new.svg')), '&Open', self) open_action.setStatusTip('Open file') # open_action.triggered.connect(self.open_file) # Create preference action expand_action = QAction(QIcon(), '&Expand', self) expand_action.setStatusTip('Expand') # exitAction.triggered.connect(self.exitCall) # Create preference action collapse_action = QAction(QIcon(), '&Collapse', self) collapse_action.setStatusTip('Collapse') # exitAction.triggered.connect(self.exitCall) # Create preference action copy_action = QAction(QIcon(get_icon_link('content_copy.svg')), '&Copy', self) copy_action.setStatusTip('Copy') # exitAction.triggered.connect(self.exitCall) # Create preference action move_action = QAction(QIcon(get_icon_link('zoom_out_map.svg')), '&Move', self) move_action.setStatusTip('Move') # exitAction.triggered.connect(self.exitCall) if level == 1: menu.addAction(rename_action) menu.addAction(new_action) menu.addSeparator() menu.addAction(refresh_action) menu.addAction(expand_action) menu.addAction(collapse_action) menu.addSeparator() menu.addAction(delete_action) elif level == 2: menu.addAction(open_action) menu.addAction(refresh_action) menu.addSeparator() menu.addAction(rename_action) menu.addAction(copy_action) menu.addAction(move_action) menu.addSeparator() menu.addAction(delete_action) else: menu.addAction(new_action) menu.addAction(refresh_action) action = menu.exec_(self.tree.viewport().mapToGlobal(position)) if action == open_action: self.on_menu_select.emit("open", data)
class RegisterViewWidget(WidgetBase): def __init__(self, parent: QWidget): super(RegisterViewWidget, self).__init__(parent) self._registers = [] self._running_task: asyncio.Task = None self._visibility_selector = QComboBox(self) self._visibility_selector.addItem("Show all registers", lambda _: True) self._visibility_selector.addItem("Only configuration parameters", lambda r: r.mutable and r.persistent) # noinspection PyUnresolvedReferences self._visibility_selector.currentIndexChanged.connect( lambda _: self._on_visibility_changed()) self._reset_selected_button = make_button( self, "Reset selected", icon_name="clear-symbol", tool_tip=f"Reset the currently selected registers to their default " f"values. The restored values will be committed " f"immediately. This function is available only if a " f"default value is defined. [{RESET_SELECTED_SHORTCUT}]", on_clicked=self._do_reset_selected, ) self._reset_all_button = make_button( self, "Reset all", icon_name="skull-crossbones", tool_tip=f"Reset the all registers to their default " f"values. The restored values will be committed " f"immediately.", on_clicked=self._do_reset_all, ) self._read_selected_button = make_button( self, "Read selected", icon_name="process", tool_tip=f"Read the currently selected registers only " f"[{READ_SELECTED_SHORTCUT}]", on_clicked=self._do_read_selected, ) self._read_all_button = make_button( self, "Read all", icon_name="process-plus", tool_tip="Read all registers from the device", on_clicked=self._do_read_all, ) self._export_button = make_button( self, "Export", icon_name="export", tool_tip="Export configuration parameters", on_clicked=self._do_export, ) self._import_button = make_button( self, "Import", icon_name="import", tool_tip="Import configuration parameters", on_clicked=self._do_import, ) self._expand_all_button = make_button( self, "", icon_name="expand-arrow", tool_tip="Expand all namespaces", on_clicked=lambda: self._tree.expandAll(), ) self._collapse_all_button = make_button( self, "", icon_name="collapse-arrow", tool_tip="Collapse all namespaces", on_clicked=lambda: self._tree.collapseAll(), ) self._status_display = QLabel(self) self._status_display.setWordWrap(True) self._reset_selected_button.setEnabled(False) self._reset_all_button.setEnabled(False) self._read_selected_button.setEnabled(False) self._read_all_button.setEnabled(False) self._export_button.setEnabled(False) self._import_button.setEnabled(False) self._tree = QTreeView(self) self._tree.setVerticalScrollMode(QTreeView.ScrollPerPixel) self._tree.setHorizontalScrollMode(QTreeView.ScrollPerPixel) self._tree.setAnimated(True) self._tree.setSelectionMode(QAbstractItemView.ExtendedSelection) self._tree.setAlternatingRowColors(True) self._tree.setContextMenuPolicy(Qt.ActionsContextMenu) # Not sure about this one. This hardcoded value may look bad on some platforms. self._tree.setIndentation(20) def add_action( callback: typing.Callable[[], None], icon_name: str, name: str, shortcut: typing.Optional[str] = None, ): action = QAction(get_icon(icon_name), name, self) # noinspection PyUnresolvedReferences action.triggered.connect(callback) if shortcut: action.setShortcut(shortcut) action.setAutoRepeat(False) try: action.setShortcutVisibleInContextMenu(True) except AttributeError: pass # This feature is not available in PyQt before 5.10 self._tree.addAction(action) add_action(self._do_read_all, "process-plus", "Read all registers") add_action( self._do_read_selected, "process", "Read selected registers", READ_SELECTED_SHORTCUT, ) add_action( self._do_reset_selected, "clear-symbol", "Reset selected to default", RESET_SELECTED_SHORTCUT, ) self._tree.setItemDelegateForColumn( int(Model.ColumnIndices.VALUE), EditorDelegate(self._tree, self._display_status), ) # It doesn't seem to be explicitly documented, but it seems to be necessary to select either top or bottom # decoration position in order to be able to use center alignment. Left or right positions do not work here. self._tree.setItemDelegateForColumn( int(Model.ColumnIndices.FLAGS), StyleOptionModifyingDelegate( self._tree, decoration_position=QStyleOptionViewItem.Top, # Important decoration_alignment=Qt.AlignCenter, ), ) header: QHeaderView = self._tree.header() header.setSectionResizeMode(QHeaderView.ResizeToContents) header.setStretchLastSection( False) # Horizontal scroll bar doesn't work if this is enabled buttons_layout = QGridLayout() buttons_layout.addWidget(self._read_selected_button, 0, 0) buttons_layout.addWidget(self._reset_selected_button, 0, 2) buttons_layout.addWidget(self._read_all_button, 1, 0) buttons_layout.addWidget(self._reset_all_button, 1, 2) buttons_layout.addWidget(self._import_button, 2, 0) buttons_layout.addWidget(self._export_button, 2, 2) for col in range(3): buttons_layout.setColumnStretch(col, 1) layout = lay_out_vertically( (self._tree, 1), buttons_layout, lay_out_horizontally( self._visibility_selector, (None, 1), self._expand_all_button, self._collapse_all_button, ), self._status_display, ) self.setLayout(layout) def reset(self): self.setup([]) def setup(self, registers: typing.Iterable[Register]): self._registers = list(registers) self._on_visibility_changed() def _replace_model( self, register_visibility_predicate: typing.Callable[[Register], bool]): # Cancel all operations that might be pending on the old model self._cancel_task() old_model = self._tree.model() # Configure the new model filtered_registers = list( filter(register_visibility_predicate, self._registers)) # It is important to set the Tree widget as the parent in order to let the widget take ownership new_model = Model(self._tree, filtered_registers) _logger.info("New model %r", new_model) self._tree.setModel(new_model) # The selection model is implicitly replaced when we replace the model, so it has to be reconfigured self._tree.selectionModel().selectionChanged.connect( lambda *_: self._on_selection_changed()) # TODO: Something fishy is going on. Something keeps the old model alive when we're replacing it. # We could call deleteLater() on it, but it seems dangerous, because if that something ever decided # to refer to that dead model later for any reason, we'll get a rougue dangling pointer access on # our hands. The horror! if old_model is not None: import gc model_referrers = gc.get_referrers(old_model) if len(model_referrers) > 1: _logger.warning( "Extra references to the old model %r: %r", old_model, model_referrers, ) # Update the widget - all root items are expanded by default for row in itertools.count(): index = self._tree.model().index(row, 0) if not index.isValid(): break self._tree.expand(index) self._reset_selected_button.setEnabled(False) self._read_selected_button.setEnabled(False) self._read_all_button.setEnabled(len(filtered_registers) > 0) self._reset_all_button.setEnabled(len(filtered_registers) > 0) self._export_button.setEnabled(len(filtered_registers) > 0) self._import_button.setEnabled(len(filtered_registers) > 0) self._display_status(f"{len(filtered_registers)} registers loaded") def _on_visibility_changed(self): self._replace_model(self._visibility_selector.currentData()) def _on_selection_changed(self): selected = self._get_selected_registers() self._reset_selected_button.setEnabled( any(map(lambda r: r.has_default_value, selected))) self._read_selected_button.setEnabled(len(selected) > 0) def _do_read_selected(self): selected = self._get_selected_registers() if selected: self._read_specific(selected) else: self._display_status("No registers are selected, nothing to read") def _do_reset_selected(self): rv = {} for r in self._get_selected_registers(): if r.has_default_value: rv[r] = r.default_value self._write_specific(rv) def _do_reset_all(self): rv = {} for r in self._registers: if r.has_default_value: rv[r] = r.default_value self._write_specific(rv) def _do_read_all(self): self._read_specific(self._tree.model().registers) def _do_import(self): import_registers(parent=self, registers=self._registers) def _do_export(self): export_registers(parent=self, registers=self._registers) def _read_specific(self, registers: typing.List[Register]): total_registers_read = None def progress_callback(register: Register, current_register_index: int, total_registers: int): nonlocal total_registers_read total_registers_read = total_registers self._display_status( f"Reading {register.name!r} " f"({current_register_index + 1} of {total_registers})") async def executor(): try: _logger.info("Reading registers: %r", [r.name for r in registers]) mod: Model = self._tree.model() await mod.read(registers=registers, progress_callback=progress_callback) except asyncio.CancelledError: self._display_status(f"Read has been cancelled") raise except Exception as ex: _logger.exception("Register read failed") show_error("Read failed", "Could not read registers", repr(ex), self) self._display_status(f"Could not read registers: {ex!r}") else: self._display_status( f"{total_registers_read} registers have been read") self._cancel_task() self._running_task = asyncio.get_event_loop().create_task(executor()) def _write_specific(self, register_value_mapping: typing.Dict[Register, typing.Any]): total_registers_assigned = None def progress_callback(register: Register, current_register_index: int, total_registers: int): nonlocal total_registers_assigned total_registers_assigned = total_registers self._display_status( f"Writing {register.name!r} " f"({current_register_index + 1} of {total_registers})") async def executor(): try: _logger.info( "Writing registers: %r", [r.name for r in register_value_mapping.keys()], ) mod: Model = self._tree.model() await mod.write( register_value_mapping=register_value_mapping, progress_callback=progress_callback, ) except asyncio.CancelledError: self._display_status(f"Write has been cancelled") raise except Exception as ex: _logger.exception("Register write failed") show_error("Write failed", "Could not read registers", repr(ex), self) self._display_status(f"Could not write registers: {ex!r}") else: self._display_status( f"{total_registers_assigned} registers have been written") self._cancel_task() self._running_task = asyncio.get_event_loop().create_task(executor()) def _get_selected_registers(self) -> typing.List[Register]: selected_indexes: typing.List[ QModelIndex] = self._tree.selectedIndexes() selected_registers = set() for si in selected_indexes: r = Model.get_register_from_index(si) if r is not None: selected_registers.add(r) # Beware that sets are not sorted, this may lead to weird user experience when watching the registers # read in a funny order. return list(sorted(selected_registers, key=lambda x: x.name)) def _cancel_task(self): # noinspection PyBroadException try: self._running_task.cancel() except Exception: pass else: _logger.info("A running task had to be cancelled: %r", self._running_task) finally: self._running_task = None def _display_status(self, text=None): self._status_display.setText(text)
class DirectoriesDialog(QMainWindow): def __init__(self, app, **kwargs): super().__init__(None, **kwargs) self.app = app self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS self.recentFolders = Recent(self.app, 'recentFolders') self._setupUi() self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView) self.directoriesDelegate = DirectoriesDelegate() self.treeView.setItemDelegate(self.directoriesDelegate) self._setupColumns() self.app.recentResults.addMenu(self.menuLoadRecent) self.app.recentResults.addMenu(self.menuRecentResults) self.recentFolders.addMenu(self.menuRecentFolders) self._updateAddButton() self._updateRemoveButton() self._updateLoadResultsButton() self._setupBindings() def _setupBindings(self): self.scanButton.clicked.connect(self.scanButtonClicked) self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger) self.addFolderButton.clicked.connect(self.actionAddFolder.trigger) self.removeFolderButton.clicked.connect(self.removeFolderButtonClicked) self.treeView.selectionModel().selectionChanged.connect( self.selectionChanged) self.app.recentResults.itemsChanged.connect( self._updateLoadResultsButton) self.recentFolders.itemsChanged.connect(self._updateAddButton) self.recentFolders.mustOpenItem.connect(self.app.model.add_directory) self.directoriesModel.foldersAdded.connect( self.directoriesModelAddedFolders) self.app.willSavePrefs.connect(self.appWillSavePrefs) def _setupActions(self): # (name, shortcut, icon, desc, func) ACTIONS = [ ('actionLoadResults', 'Ctrl+L', '', tr("Load Results..."), self.loadResultsTriggered), ('actionShowResultsWindow', '', '', tr("Results Window"), self.app.showResultsWindow), ('actionAddFolder', '', '', tr("Add Folder..."), self.addFolderTriggered), ] createActions(ACTIONS, self) def _setupMenu(self): self.menubar = QMenuBar(self) self.menubar.setGeometry(QRect(0, 0, 42, 22)) self.menuFile = QMenu(self.menubar) self.menuFile.setTitle(tr("File")) self.menuView = QMenu(self.menubar) self.menuView.setTitle(tr("View")) self.menuHelp = QMenu(self.menubar) self.menuHelp.setTitle(tr("Help")) self.menuLoadRecent = QMenu(self.menuFile) self.menuLoadRecent.setTitle(tr("Load Recent Results")) self.setMenuBar(self.menubar) self.menuFile.addAction(self.actionLoadResults) self.menuFile.addAction(self.menuLoadRecent.menuAction()) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionQuit) self.menuView.addAction(self.app.actionPreferences) self.menuView.addAction(self.actionShowResultsWindow) self.menuView.addAction(self.app.actionIgnoreList) self.menuHelp.addAction(self.app.actionShowHelp) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuView.menuAction()) self.menubar.addAction(self.menuHelp.menuAction()) # Recent folders menu self.menuRecentFolders = QMenu() self.menuRecentFolders.addAction(self.actionAddFolder) self.menuRecentFolders.addSeparator() # Recent results menu self.menuRecentResults = QMenu() self.menuRecentResults.addAction(self.actionLoadResults) self.menuRecentResults.addSeparator() def _setupUi(self): self.setWindowTitle(self.app.NAME) self.resize(420, 338) self.centralwidget = QWidget(self) self.verticalLayout = QVBoxLayout(self.centralwidget) self.promptLabel = QLabel( tr("Select folders to scan and press \"Scan\"."), self.centralwidget) self.verticalLayout.addWidget(self.promptLabel) self.treeView = QTreeView(self.centralwidget) self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows) self.treeView.setAcceptDrops(True) triggers = QAbstractItemView.DoubleClicked|QAbstractItemView.EditKeyPressed\ |QAbstractItemView.SelectedClicked self.treeView.setEditTriggers(triggers) self.treeView.setDragDropOverwriteMode(True) self.treeView.setDragDropMode(QAbstractItemView.DropOnly) self.treeView.setUniformRowHeights(True) self.verticalLayout.addWidget(self.treeView) self.horizontalLayout = QHBoxLayout() self.removeFolderButton = QPushButton(self.centralwidget) self.removeFolderButton.setIcon(QIcon(QPixmap(":/minus"))) self.removeFolderButton.setShortcut("Del") self.horizontalLayout.addWidget(self.removeFolderButton) self.addFolderButton = QPushButton(self.centralwidget) self.addFolderButton.setIcon(QIcon(QPixmap(":/plus"))) self.horizontalLayout.addWidget(self.addFolderButton) spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem1) self.loadResultsButton = QPushButton(self.centralwidget) self.loadResultsButton.setText(tr("Load Results")) self.horizontalLayout.addWidget(self.loadResultsButton) self.scanButton = QPushButton(self.centralwidget) self.scanButton.setText(tr("Scan")) self.scanButton.setDefault(True) self.horizontalLayout.addWidget(self.scanButton) self.verticalLayout.addLayout(self.horizontalLayout) self.setCentralWidget(self.centralwidget) self._setupActions() self._setupMenu() if self.app.prefs.directoriesWindowRect is not None: self.setGeometry(self.app.prefs.directoriesWindowRect) else: moveToScreenCenter(self) def _setupColumns(self): header = self.treeView.header() header.setStretchLastSection(False) header.setSectionResizeMode(0, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.Fixed) header.resizeSection(1, 100) def _updateAddButton(self): if self.recentFolders.isEmpty(): self.addFolderButton.setMenu(None) else: self.addFolderButton.setMenu(self.menuRecentFolders) def _updateRemoveButton(self): indexes = self.treeView.selectedIndexes() if not indexes: self.removeFolderButton.setEnabled(False) return self.removeFolderButton.setEnabled(True) def _updateLoadResultsButton(self): if self.app.recentResults.isEmpty(): self.loadResultsButton.setMenu(None) else: self.loadResultsButton.setMenu(self.menuRecentResults) #--- QWidget overrides def closeEvent(self, event): event.accept() if self.app.model.results.is_modified: title = tr("Unsaved results") msg = tr("You have unsaved results, do you really want to quit?") if not self.app.confirm(title, msg): event.ignore() if event.isAccepted(): QApplication.quit() #--- Events def addFolderTriggered(self): title = tr("Select a folder to add to the scanning list") flags = QFileDialog.ShowDirsOnly dirpath = str( QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags)) if not dirpath: return self.lastAddedFolder = dirpath self.app.model.add_directory(dirpath) self.recentFolders.insertItem(dirpath) def appWillSavePrefs(self): self.app.prefs.directoriesWindowRect = self.geometry() def directoriesModelAddedFolders(self, folders): for folder in folders: self.recentFolders.insertItem(folder) def loadResultsTriggered(self): title = tr("Select a results file to load") files = ';;'.join( [tr("dupeGuru Results (*.dupeguru)"), tr("All Files (*.*)")]) destination = QFileDialog.getOpenFileName(self, title, '', files) if destination: self.app.model.load_from(destination) self.app.recentResults.insertItem(destination) def removeFolderButtonClicked(self): self.directoriesModel.model.remove_selected() def scanButtonClicked(self): if self.app.model.results.is_modified: title = tr("Start a new scan") msg = tr( "You have unsaved results, do you really want to continue?") if not self.app.confirm(title, msg): return self.app.model.start_scanning() def selectionChanged(self, selected, deselected): self._updateRemoveButton()
class Navigation(QWidget): """ Navigation class definition. Provide a combobox to switch on each opened directories and display it into a tree view Provide 2 useful function (to use in alter module): - add_action(name, shortcut, callback) - callback take 2 arguments : file_info and parent - add_separator() """ SETTINGS_DIRECTORIES = 'navigation_dirs' SETTINGS_CURRENT_DIR = 'navigation_current_dir' onFileItemActivated = pyqtSignal(QFileInfo, name="onFileItemActivated") onDirItemActivated = pyqtSignal(QFileInfo, name="onDirItemActivated") def __init__(self, parent=None): super(Navigation, self).__init__(parent) self.setObjectName("Navigation") self.layout = QVBoxLayout(self) self.layout.setSpacing(0) self.layout.setContentsMargins(0, 0, 0, 0) self.menu_button = QPushButton('Select directory', self) self.menu_button.setFlat(True) # self.menu_button.clicked.connect(self.on_menu_button_clicked) self.menu = QMenu(self) self.menu_button.setMenu(self.menu) self.menu_directories = QMenu(self) self.menu_directories.setTitle('Directories') self.menu_add_action('Open directory', self.open_directory, None, QKeySequence.Open) self.menu_add_separator() self.menu_add_action('Refresh', self.reset, None, QKeySequence.Refresh) # @TODO invoke_all self.menu_add_separator() self.menu.addMenu(self.menu_directories) self.tree = QTreeView(self) self.model = FileSystemModel(self) self.tree.setModel(self.model) self.tree.setColumnHidden(1, True) self.tree.setColumnHidden(2, True) self.tree.setColumnHidden(3, True) self.tree.setHeaderHidden(True) # only to expand directory or activated with one click self.tree.clicked.connect(self.on_item_clicked) # else, for file use activated signal self.tree.activated.connect(self.on_item_activated) self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self.on_context_menu) self.widgets = collections.OrderedDict() self.widgets['menu_button'] = self.menu_button self.widgets['tree'] = self.tree # @ToDo: Alter.invoke_all('add_widget', self.widgets) for name, widget in self.widgets.items(): if name == 'menu_button': self.layout.addWidget(widget, 0, Qt.AlignLeft) else: self.layout.addWidget(widget) self.context_menu = QMenu(self) self.add_action('New file', QKeySequence.New, FileSystemHelper.new_file) self.add_separator() self.add_action('Copy', QKeySequence.Copy, FileSystemHelper.copy) self.add_action('Cut', QKeySequence.Cut, FileSystemHelper.cut) self.add_action('Paste', QKeySequence.Paste, FileSystemHelper.paste) self.add_separator() self.add_action('Delete', QKeySequence.Delete, FileSystemHelper.delete) # @ToDo Alter.invoke_all('navigation_add_action', self) #restore previous session and data dirs = ModuleManager.core['settings'].Settings.value( self.SETTINGS_DIRECTORIES, None, True) for directory_path in dirs: name = os.path.basename(directory_path) self.menu_add_directory(name, directory_path) current_dir = ModuleManager.core['settings'].Settings.value( self.SETTINGS_CURRENT_DIR, '') if current_dir: for action in self.menu_directories.actions(): if action.data() == current_dir: action.trigger() self.menu_button.setFocusPolicy(Qt.NoFocus) self.menu_button.setFocusProxy(self.tree) def reset(self, file_info): self.model.beginResetModel() current_dir = ModuleManager.core['settings'].Settings.value( self.SETTINGS_CURRENT_DIR, '') if current_dir: for action in self.menu_directories.actions(): if action.data() == current_dir: action.trigger() def on_menu_button_clicked(self): pos = self.mapToGlobal(self.menu_button.pos()) menu_width = self.menu.sizeHint().width() pos.setY(pos.y() + self.menu_button.height()) # pos.setX(pos.x() + self.menu_button.width() - menu_width) if len(self.menu.actions()) > 0: self.menu.exec(pos) def menu_add_action(self, name, callback, data=None, shortcut=None, icon=None): action = QAction(name, self) if icon: action.setIcon(icon) if shortcut: action.setShortcut(shortcut) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) if data: action.setData(data) action.triggered.connect(callback) self.addAction(action) self.menu.addAction(action) def menu_add_directory(self, name, data): action = QAction(name, self) action.setData(data) action.triggered.connect(self.on_menu_action_triggered) self.menu_directories.addAction(action) return action def menu_add_separator(self): self.menu.addSeparator() def add_action(self, name, shortcut, callback, icon=None): """ Ajoute une action au context menu et au widget navigation lui même. Créer une fonction à la volé pour fournir des arguments aux fonctions associé aux actions. """ action = QAction(name, self) if icon: action.setIcon(icon) action.setShortcut(shortcut) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.__wrapper(callback)) self.addAction(action) self.context_menu.addAction(action) def add_separator(self): """Simple abstraction of self.context_menu.addSeparator()""" self.context_menu.addSeparator() def __wrapper(self, callback): def __new_function(): """ __new_function représente la forme de tous les callbacks connecté à une action pour pouvoir utiliser les raccourcis en même temps que le menu contextuel. """ action = self.sender() file_info = action.data() if not file_info: indexes = self.tree.selectedIndexes() if indexes: model_index = indexes[0] file_info = self.model.fileInfo(model_index) callback(file_info, self) elif action.shortcut() == QKeySequence.New: file_info = self.model.fileInfo(self.tree.rootIndex()) callback(file_info, self) else: callback(file_info, self) action.setData(None) return __new_function def question(self, text, informative_text=None): message_box = QMessageBox(self) message_box.setText(text) if informative_text: message_box.setInformativeText(informative_text) message_box.setStandardButtons(QMessageBox.No | QMessageBox.Yes) message_box.setDefaultButton(QMessageBox.No) return message_box.exec() def on_context_menu(self, point): model_index = self.tree.indexAt(point) file_info = self.model.fileInfo(model_index) # pour chaque action on met a jour les data (file_info) # puis on altère les actions (ex enabled) for action in self.context_menu.actions(): if not action.isSeparator(): action.setData(file_info) action.setEnabled(model_index.isValid()) if action.shortcut() == QKeySequence.New: action.setEnabled(True) if not model_index.isValid(): file_info = self.model.fileInfo(self.tree.rootIndex()) action.setData(file_info) if action.shortcut() == QKeySequence.Paste: enable = FileSystemHelper.ready() and model_index.isValid() action.setEnabled(enable) if action.shortcut() == QKeySequence.Delete: # remove directory only if is an empty directory if model_index.isValid() and file_info.isDir(): path = file_info.absoluteFilePath() # QDir(path).count() always contains '.' and '..' action.setEnabled(QDir(path).count() == 2) # @ToDo #Alter.invoke_all( # 'navigation_on_menu_action', # model_index, file_info, action, self) if len(self.context_menu.actions()) > 0: self.context_menu.exec(self.tree.mapToGlobal(point)) # reset action data, sinon y a des problèmes dans _new_function for action in self.context_menu.actions(): action.setData(None) def on_item_activated(self, index): qFileInfo = self.model.fileInfo(index) if qFileInfo.isDir(): self.onDirItemActivated.emit(qFileInfo) else: self.onFileItemActivated.emit(qFileInfo) def on_item_clicked(self, index): qFileInfo = self.model.fileInfo(index) if qFileInfo.isDir(): self.onDirItemActivated.emit(qFileInfo) self.tree.setExpanded(index, not self.tree.isExpanded(index)) else: self.onFileItemActivated.emit(qFileInfo) def open_directory(self): path = QFileDialog.getExistingDirectory(self, "Open Directory", ".") if path: name = os.path.basename(path) action = self.menu_add_directory(name, path) self.save_directories_path() action.trigger() def on_menu_action_triggered(self): action = self.sender() path = action.data() if path: self.model.setRootPath(path) self.tree.setRootIndex(self.model.index(path)) self.menu_button.setText(os.path.basename(path)) self.save_current_dir(path) def save_directories_path(self): ModuleManager.core['settings'].Settings.set_value( self.SETTINGS_DIRECTORIES, [action.data() for action in self.menu_directories.actions()]) def save_current_dir(self, path): ModuleManager.core['settings'].Settings.set_value( self.SETTINGS_CURRENT_DIR, path)
class bands_pairing2(QWidget): def __init__(self, dataview): super().__init__() """a tree widget to create a list of datasets and select bands from each dataset""" self.dataview = dataview self.firstband = QTreeView() self.secondband = QTreeView() self.firstband.setModel(self.dataview.model) self.secondband.setModel(self.dataview.model) self.pairs = treewidget() self.pairs.setColumnCount(2) self.pairs.setHeaderLabels(['first band', 'second band']) self.pairbutton = QPushButton('Pair') self.pairbutton.clicked.connect(self.add_pair) mainlayout = QVBoxLayout() sublayout = QHBoxLayout() sublayout.addWidget(self.firstband) sublayout.addWidget(self.secondband) firstsecondgroup = QGroupBox() firstsecondgroup.setLayout(sublayout) mainlayout.addWidget(firstsecondgroup) mainlayout.addWidget(self.pairbutton) mainlayout.addWidget(self.pairs) self.maingroup = QGroupBox() self.maingroup.setLayout(mainlayout) def additems(self): """when the widget is activated, a list of selected bands will be added""" dict = self.dataview.collect_user_input() for i in dict['bandsdict'].keys(): name = os.path.basename(i) for j in dict['bandsdict'][name].keys(): band = QListWidgetItem(j) band2 = QListWidgetItem(j) self.firstband.addItem(band) self.secondband.addItem(band2) def add_pair(self): """when pair button is pressed the paire will be add to the list""" # get selected index firstitem = self.firstband.selectedIndexes() # item text (example:band1) firstitemtext = self.firstband.model().itemData(firstitem[0])[0] # parent text (example: rastername.tif) firstparent = self.firstband.model().itemFromIndex(firstitem[0]).parent().data(0) #text to appear on the pairing widget firstpairtext = os.path.basename(firstparent) + ' - ' + firstitemtext # get selected index seconditem = self.secondband.selectedIndexes() # item text (example:band1) seconditemtext = self.secondband.model().itemData(seconditem[0])[0] # parent text (example: rastername.tif) secondparent = self.firstband.model().itemFromIndex(firstitem[0]).parent().data(0) # text to appear on the pairing widget secondpairtext = os.path.basename(secondparent) + ' - ' + seconditemtext print(firstitemtext) print(os.path.basename(firstparent)) pairitem = QTreeWidgetItem() pairitem.setText(0, firstpairtext) pairitem.setText(1, secondpairtext) self.pairs.addTopLevelItem(pairitem) #self.get_pairs() def get_pairs(self): """returns 1. a dictionary of selected bands created with the datatreeview widget, 2. a list of featurebands, 3. a list of texts from the items in the widgets (dataset and band number) """ bandsdict = self.dataview.collectinput()[0] print (bandsdict) datasets = self.dataview.collectinput()[1] selectedbands = self.dataview.collectinput()[2] print(selectedbands) featurebands = [] feturebandstext = [] for i in range(self.pairs.topLevelItemCount()): item = self.pairs.topLevelItem(i) item1text = item.text(0) item2text = item.text(1) # get the list of the items from the widget feturebandstext += [[item1text, item2text]] band1 = bandsdict[item1text] band2 = bandsdict[item2text] # get the pair of bands by indexes pair = [selectedbands.index(band1), selectedbands.index(band2)] featurebands += [pair] print(selectedbands) print(featurebands) return [selectedbands, featurebands,feturebandstext]
class MainWindow(QWidget): Id, Password = range(2) CONFIG_FILE = 'config' def __init__(self): super().__init__() with open(self.CONFIG_FILE, 'a'): pass self.init() def init(self): # ------ initUI self.resize(555, 245) self.setFixedSize(555, 245) self.center() self.setWindowTitle('Portal Connector') self.setWindowIcon(QIcon('gao.ico')) self.backgroundRole() palette1 = QPalette() palette1.setColor(self.backgroundRole(), QColor(250, 250, 250)) # 设置背景颜色 self.setPalette(palette1) # ------setLeftWidget self.dataGroupBox = QGroupBox("Saved", self) self.dataGroupBox.setGeometry(10, 10, 60, 20) self.dataGroupBox.setStyleSheet(MyGroupBox) self.model = QStandardItemModel(0, 2, self) self.model.setHeaderData(self.Id, Qt.Horizontal, "Id") self.model.setHeaderData(self.Password, Qt.Horizontal, "Pw") self.dataView = QTreeView(self) self.dataView.setGeometry(10, 32, 255, 150) self.dataView.setRootIsDecorated(False) self.dataView.setAlternatingRowColors(True) self.dataView.setModel(self.model) self.dataView.setStyleSheet(MyTreeView) save_btn = QPushButton('Save', self) save_btn.setGeometry(15, 195, 100, 35) save_btn.setStyleSheet(MyPushButton) delete_btn = QPushButton('Delete', self) delete_btn.setGeometry(135, 195, 100, 35) delete_btn.setStyleSheet(MyPushButton) # ------ setRightWidget username = QLabel('Id:', self) username.setGeometry(300, 45, 50, 30) username.setStyleSheet(MyLabel) self.username_edit = QLineEdit(self) self.username_edit.setGeometry(350, 40, 190, 35) self.username_edit.setStyleSheet(MyLineEdit) password = QLabel('Pw:', self) password.setGeometry(300, 100, 50, 30) password.setStyleSheet(MyLabel) self.password_edit = QLineEdit(self) self.password_edit.setGeometry(350, 95, 190, 35) self.password_edit.setStyleSheet(MyLineEdit) status_label = QLabel('Result:', self) status_label.setGeometry(295, 150, 70, 30) status_label.setStyleSheet(UnderLabel) self.status = QLabel('Disconnect', self) self.status.setGeometry(360, 150, 190, 30) self.status.setStyleSheet(UnderLabel) connect_btn = QPushButton('Connect', self) connect_btn.setGeometry(320, 195, 100, 35) connect_btn.setStyleSheet(MyPushButton) test_btn = QPushButton('Test', self) test_btn.setGeometry(440, 195, 100, 35) test_btn.setStyleSheet(MyPushButton) # ------setTabOrder self.setTabOrder(self.username_edit, self.password_edit) self.setTabOrder(self.password_edit, connect_btn) self.setTabOrder(connect_btn, test_btn) # ------setEvent self.dataView.mouseDoubleClickEvent = self.set_text self.dataView.mousePressEvent = self.set_focus delete_btn.clicked.connect(self.removeItem) connect_btn.clicked.connect(self.connect_clicked) save_btn.clicked.connect(self.save_infomation) test_btn.clicked.connect(self.test_network) self.readItem(self.CONFIG_FILE) self.connect_clicked() self.show() def connect_clicked(self): result = connect_portal(self.username_edit.text(), self.password_edit.text()) self.status.setText(result) def save_infomation(self): if self.username_edit.text() and self.password_edit.text(): try: selected = self.dataView.selectedIndexes()[0].row() self.modifyItem(selected) except IndexError: self.addItem(self.username_edit.text(), self.password_edit.text()) def test_network(self): result = test_public() self.status.setText(result) def set_text(self, event=None): try: self.username_edit.setText( self.dataView.selectedIndexes()[0].data()) self.password_edit.setText( self.dataView.selectedIndexes()[1].data()) except IndexError: pass def set_focus(self, event): index = self.dataView.indexAt(event.pos()) if not index.isValid(): self.dataView.clearSelection() else: self.dataView.setCurrentIndex(index) def readItem(self, filename): with open(filename, 'r') as f: for line in f.readlines(): self.addItem(*(line.split())) self.dataView.setCurrentIndex(self.dataView.indexAt(QPoint(1, 1))) self.set_text() def addItem(self, username, password): self.model.insertRow(0) self.model.setData(self.model.index(0, self.Id), username) self.model.setData(self.model.index(0, self.Password), password) self.save_to_file() def modifyItem(self, row): self.model.setData(self.model.index(row, self.Id), self.username_edit.text()) self.model.setData(self.model.index(row, self.Password), self.password_edit.text()) self.save_to_file() def removeItem(self): try: self.model.removeRow(self.dataView.selectedIndexes()[0].row()) self.save_to_file() except IndexError: pass def save_to_file(self): with open(self.CONFIG_FILE, 'w') as f: for x in range(self.model.rowCount()): for y in range(self.model.columnCount()): f.write(self.model.data(self.model.index(x, y)) + " ") f.write("\n") def center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft())
class mainForm(QMainWindow): get_template_filename = pyqtSignal(str) exit_program = pyqtSignal() camera_changed = pyqtSignal(int) def __init__(self): super().__init__() self.stream = None self.image_difference_thread = None self.template_set = False self._countour_max_tresh = 10 self._countour_min_tresh = 1 self._transparency_max = 10 self._transparency_min = 0 self._countour_gamma_max = 10 self._countour_gamma_min = 1 self._color_max = 255 self._color_min = 0 self.screen_resolution = None self.grid_layout = None self.output_picture = None self.initUI() self.init_image_difference() if self.webcam_switcher.count() > 0: self.stream = streamCapture(self.webcam_switcher.itemData(self.webcam_switcher.currentIndex())) self.stream.getframe.connect(self.mat2qimage) self.webcam_switcher.currentIndexChanged.connect(self.camera_switcher_index_changed) self.camera_changed.connect(self.stream.reopenStream) self.stream.start() self.exit_program.connect(self.stream.exit) ### отрисовка интерфейса def initUI(self): self.screen_resolution = QApplication.desktop().screenGeometry() # self.resize(self.screen_resolution.size()) # self.move(self.screen_resolution.left(), self.screen_resolution.top()) self.grid_layout = QGridLayout() self.central_widget = QWidget() self.central_widget.setLayout(self.grid_layout) self.central_widget.setMaximumSize(self.screen_resolution.width() // 4 * 3, self.screen_resolution.height() // 4 * 3) self.setCentralWidget(self.central_widget) self.camera_label = QLabel("Camera:") self.grid_layout.addWidget(self.camera_label, 0, 0, 1, 1, Qt.AlignmentFlag.AlignHCenter) self.webcam_switcher = QComboBox() self.detect_webcam_devices(self.webcam_switcher) self.grid_layout.addWidget(self.webcam_switcher, 0, 1, 1, 3) self.output_picture = QLabel() self.grid_layout.addWidget(self.output_picture, 1, 0, 1, 4) ### creating right dock self.right_dock_layout = QVBoxLayout() self.right_dock_widget = QDockWidget() self.right_dock_widget.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable | QDockWidget.DockWidgetFeature.DockWidgetFloatable) self.right_dock_widget.setMinimumSize(self.screen_resolution.width() // 4, self.screen_resolution.height()) right_dock = QWidget(self.right_dock_widget) right_dock.setMinimumSize(self.screen_resolution.width() // 4, self.screen_resolution.height()) right_dock.setLayout(self.right_dock_layout) template_label = QLabel("Templates") template_label.setMinimumSize(50, 25) self.right_dock_layout.addWidget(template_label) self.filter_template_edit = QLineEdit() self.filter_template_edit.setPlaceholderText("Filter (Ctr + Alt + f)") template_label.setMinimumSize(90, 25) self.filter_template_edit.setStyleSheet( "background-image: url(../image_difference/icons/searchIcon.png); background-repeat: no-repeat; background-position: right;") self.right_dock_layout.addWidget(self.filter_template_edit) self.file_system_model = QFileSystemModel() self.file_system_model.setFilter(QDir.Filter.AllDirs | QDir.Filter.NoDotAndDotDot | QDir.Filter.AllEntries) self.file_system_model.setRootPath(QDir.currentPath()) self.directory_tree_view = QTreeView() self.directory_tree_view.setModel(self.file_system_model) self.directory_tree_view.setMinimumSize(200, 100) self.directory_tree_view.hideColumn(1) self.directory_tree_view.hideColumn(2) self.directory_tree_view.hideColumn(3) # self.directory_tree_view.sortByColumn(0) self.directory_tree_view.setSortingEnabled(True) self.directory_tree_view.doubleClicked.connect(self.load_template) self.directory_tree_view.setRootIndex(self.file_system_model.index("../image_difference/")) self.right_dock_layout.addWidget(self.directory_tree_view) self.load_template_button = QPushButton("Select Template") self.load_template_button.setMaximumSize(self.screen_resolution.width() // 4 - 30, 30) self.load_template_button.clicked.connect(self.load_template) self.right_dock_layout.addWidget(self.load_template_button) self.create_template_button = QPushButton("Create Template") self.create_template_button.setMaximumSize(self.screen_resolution.width() // 4 - 30, 30) self.create_template_button.clicked.connect(self.create_template) self.right_dock_layout.addWidget(self.create_template_button) self.template_image_widget = QWidget() self.template_image_widget.setMinimumSize(self.screen_resolution.width() // 4 - 20, self.screen_resolution.width() // 4 - 10) self.template_image_back = QLabel(self.template_image_widget) self.template_image_back.resize(self.screen_resolution.width() // 4 - 20, self.screen_resolution.width() // 4 - 10) pix = QPixmap(self.template_image_back.size()) pix.fill(Qt.lightGray) rect = QRectF(0.0, 0.0, self.template_image_back.size().width(), self.template_image_back.size().height()) painter = QPainter() painter.begin(pix) painter.setRenderHints(QPainter.Antialiasing, True) path = QPainterPath() path.addRoundedRect(rect, 5.0, 5.0) painter.drawPath(path) painter.end() self.template_image_back.setPixmap(pix) self.template_image = QLabel(self.template_image_widget) self.template_image.move(5, 5) self.template_image.resize(self.screen_resolution.width() // 4 - 30, self.screen_resolution.width() // 4 - 30) self.template_image_text = QLabel(self.template_image_widget, text="Current Template") self.template_image_text.setStyleSheet("font-weight: bold") self.template_image_text.move(self.screen_resolution.width() // 8 - 65, 20) self.right_dock_layout.addWidget(self.template_image_widget) self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.right_dock_widget) ### creating bottom dock self.bottom_dock_layout = QGridLayout() self.bottom_dock_layout.setSpacing(10) self.bottom_dock_widget = QDockWidget() self.bottom_dock_widget.setMinimumSize(self.screen_resolution.width() // 4 * 3 - 10, self.screen_resolution.height() // 4 - 10) bottom_dock = QWidget(self.bottom_dock_widget) bottom_dock.setMinimumSize(self.screen_resolution.width() // 4 * 3 - 20, self.screen_resolution.height() // 4 - 20) bottom_dock.move(10, 10) bottom_dock.setLayout(self.bottom_dock_layout) settings_label = QLabel("Settings:") self.bottom_dock_layout.addWidget(settings_label, 0, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop) countour_tresh_label = QLabel("Countour Tresh:") self.bottom_dock_layout.addWidget(countour_tresh_label, 1, 0, 1, 1, Qt.AlignmentFlag.AlignTop) self.countour_tresh_slider = QSlider(Qt.Orientation.Horizontal) self.countour_tresh_slider.setTickPosition(QSlider.TickPosition.TicksBelow) self.countour_tresh_slider.setRange(self._countour_min_tresh, self._countour_max_tresh) self.countour_tresh_slider.setValue(2) self.bottom_dock_layout.addWidget(self.countour_tresh_slider, 1, 1, 1, 1, Qt.AlignmentFlag.AlignTop) transparency_weight_label = QLabel("Transparency:") self.bottom_dock_layout.addWidget(transparency_weight_label, 2, 0, 1, 1, Qt.AlignmentFlag.AlignTop) self.transparency_weight_slider = QSlider(Qt.Orientation.Horizontal) self.transparency_weight_slider.setTickPosition(QSlider.TickPosition.TicksBelow) self.transparency_weight_slider.setValue(6) self.transparency_weight_slider.setRange(self._transparency_min, self._transparency_max) self.bottom_dock_layout.addWidget(self.transparency_weight_slider, 2, 1, 1, 1, Qt.AlignmentFlag.AlignTop) countour_gamma_label = QLabel("Countour Gamma:") self.bottom_dock_layout.addWidget(countour_gamma_label, 3, 0, 1, 1, Qt.AlignmentFlag.AlignTop) self.countour_gamma_slider = QSlider(Qt.Orientation.Horizontal) self.countour_gamma_slider.setTickPosition(QSlider.TickPosition.TicksBelow) self.countour_gamma_slider.setValue(8) self.countour_gamma_slider.setRange(self._countour_gamma_min, self._countour_gamma_max) self.bottom_dock_layout.addWidget(self.countour_gamma_slider, 3, 1, 1, 1, Qt.AlignmentFlag.AlignTop) ### right side of settings countour_color_label = QLabel("Countour Color:") self.bottom_dock_layout.addWidget(countour_color_label, 0, 2, 1, 2, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop) r_color_label = QLabel("R:") self.bottom_dock_layout.addWidget(r_color_label, 1, 2, 1, 1, Qt.AlignmentFlag.AlignTop) self.r_color_slider = QSlider(Qt.Orientation.Horizontal) self.r_color_slider.setTickPosition(QSlider.TickPosition.TicksBelow) self.r_color_slider.setRange(self._color_min, self._color_max) self.r_color_slider.setValue(255) self.bottom_dock_layout.addWidget(self.r_color_slider, 1, 3, 1, 1, Qt.AlignmentFlag.AlignTop) g_color_label = QLabel("G:") self.bottom_dock_layout.addWidget(g_color_label, 2, 2, 1, 1, Qt.AlignmentFlag.AlignTop) self.g_color_slider = QSlider(Qt.Orientation.Horizontal) self.g_color_slider.setTickPosition(QSlider.TickPosition.TicksBelow) self.g_color_slider.setRange(self._color_min, self._color_max) self.bottom_dock_layout.addWidget(self.g_color_slider, 2, 3, 1, 1, Qt.AlignmentFlag.AlignTop) b_color_label = QLabel("B:") self.bottom_dock_layout.addWidget(b_color_label, 3, 2, 1, 1, Qt.AlignmentFlag.AlignTop) self.b_color_slider = QSlider(Qt.Orientation.Horizontal) self.b_color_slider.setTickPosition(QSlider.TickPosition.TicksBelow) self.b_color_slider.setRange(self._color_min, self._color_max) self.bottom_dock_layout.addWidget(self.b_color_slider, 3, 3, 1, 1, Qt.AlignmentFlag.AlignTop) self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.bottom_dock_widget) self.setCorner(Qt.Corner.BottomRightCorner, Qt.DockWidgetArea.RightDockWidgetArea) def init_image_difference(self): self.image_difference_thread = imageDifference() self.get_template_filename.connect(self.image_difference_thread.set_template_image) self.countour_tresh_slider.valueChanged.connect(self.image_difference_thread.set_countour_tresh_value) self.transparency_weight_slider.valueChanged.connect(self.image_difference_thread.set_transparency_weight_value) self.countour_gamma_slider.valueChanged.connect(self.image_difference_thread.set_countour_gamma_value) self.r_color_slider.valueChanged.connect(self.image_difference_thread.set_countour_color_r) self.g_color_slider.valueChanged.connect(self.image_difference_thread.set_countour_color_g) self.b_color_slider.valueChanged.connect(self.image_difference_thread.set_countour_color_b) self.image_difference_thread.output_image_defference.connect(self.mat2qimage) self.image_difference_thread.set_template_picture.connect(self.set_template_picture) self.image_difference_thread.start() def detect_webcam_devices(self, combo_box): _video_capture = cv2.VideoCapture() _dev_id = 0 while (_dev_id < 3): if _video_capture.open(_dev_id): combo_box.addItem("Device #" + str(_dev_id + 1), _dev_id) _dev_id += 1 else: _dev_id += 1 _video_capture.release() def load_template(self): index = self.directory_tree_view.selectedIndexes()[0] if not QFileInfo(self.file_system_model.filePath(index)).isDir(): # print("load template, path:", self.file_system_model.filePath(index)) self.get_template_filename.emit(self.file_system_model.filePath(index)) def create_template(self): if self.stream is not None: template_to_save = self.stream.get_current_frame() cv2.imwrite("../image_difference/examples/template.jpg", template_to_save) print("create template") pyqtSlot(np.ndarray) def mat2qimage(self, image): rgbImage = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) rgbImage = cv2.resize(rgbImage, (self.output_picture.size().width(), self.output_picture.size().height())) # rgbImage = imutils.resize(rgbImage, height=self.output_picture.height()) h, w, ch = rgbImage.shape bytesPerLine = ch * w result_image = QImage(rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888) self.output_picture.setPixmap(QPixmap.fromImage(result_image)) pyqtSlot(np.ndarray) def set_template_picture(self, image): rgbImage = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # rgbImage = cv2.resize(rgbImage, (self.template_image.size().width(), # self.template_image.size().height())) rgbImage = imutils.resize(rgbImage, self.template_image.size().width()) h, w, ch = rgbImage.shape bytesPerLine = ch * w result_image = QImage(rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888) self.template_image.setPixmap(QPixmap.fromImage(result_image)) if not self.template_set: self.template_set = True self.stream.getframe.disconnect(self.mat2qimage) self.stream.getframe.connect(self.image_difference_thread.get_image) self.image_difference_thread.output_image_defference.connect(self.mat2qimage) def camera_switcher_index_changed(self, index): self.camera_changed.emit(self.webcam_switcher.itemData(index)) print("current index:", index) print("item data:", self.webcam_switcher.itemData(index)) def closeEvent(self, event): self.exit_program.emit() event.accept()
class AnalyzeTab(QWidget): analyzeDone = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject') def __init__(self): super().__init__() # Layouts self.mainLayout = QHBoxLayout() self.lVbox = QVBoxLayout() self.lHbox = QHBoxLayout() self.lHbox_top = QHBoxLayout() self.rVbox = QVBoxLayout() self.rHbox = QHBoxLayout() self.rVbox2 = QVBoxLayout() self.stack = QStackedWidget() self.stack_Vbox = QVBoxLayout() self.stack_Hbox1 = QHBoxLayout() self.stack_Hbox2 = QHBoxLayout() self.hSplit = QSplitter(Qt.Horizontal) self.hSplit.setFrameShape(QFrame.StyledPanel) self.vSplit = QSplitter(Qt.Vertical) self.vSplit.setFrameShape(QFrame.StyledPanel) self.mainLayout.addLayout(self.lVbox, 1) self.mainLayout.addLayout(self.rVbox, 3) # Setup file browser self.fileModel = QFileSystemModel() self.fileModel.setNameFilters(['*.wav']) self.fileModel.setRootPath(QDir.currentPath()) self.fileTree = QTreeView() self.fileTree.setModel(self.fileModel) self.fileTree.setRootIndex(self.fileModel.index(r'./')) self.fileTree.setSelectionMode(QAbstractItemView.SingleSelection) self.fileTree.setColumnHidden(2, True) self.fileTree.setColumnHidden(1, True) self.rootDirEdit = QLineEdit(os.path.dirname(__file__)) self.rootDirEdit.returnPressed.connect(self.on_edit_root) self.browseBtn = QPushButton('Browse') self.browseBtn.clicked.connect(self.on_browse) self.lHbox_top.addWidget(self.rootDirEdit, 3) self.lHbox_top.addWidget(self.browseBtn, 1) # Setup Canvas self.canvas = PlotCanvas(self) self.analyzeDone.connect(self.canvas.plot) self._analyze = lambda _: self.analyze(self.fileTree.selectedIndexes()) self.analyzeBtn = QPushButton('Analyze') self.analyzeBtn.clicked.connect(self._analyze) ## BATCH ANALYSIS CONTROLS ## self.batchAnalyzeChk = QCheckBox('Batch Analysis') self.dataTable = QTableWidget() self.batchCtrlBox = QGroupBox("Batch Analysis") self.batchCtrlBox.setLayout(self.stack_Vbox) # Analysis Mode self.modeGroup = QButtonGroup() self.modeBox = QGroupBox('Analysis Mode') self.modeBox.setLayout(self.stack_Hbox1) self.stack_Vbox.addWidget(self.modeBox) self.wavAnalysisChk = QCheckBox('Wav analysis') self.wavAnalysisChk.setChecked(True) self.calibrationLocationBox = QComboBox() self.calibrationLocationBox.addItems([str(n) for n in range(1, 11)]) self.calibrationCurveChk = QCheckBox('Calibration Curve') self.calibrationCurveChk.toggled.connect( lambda state: self.calibrationLocationBox.setEnabled(state)) self.calibrationCurveChk.setChecked(False) self.stack_Hbox1.addWidget(self.wavAnalysisChk, 3) self.stack_Hbox1.addWidget(self.calibrationCurveChk, 3) self.stack_Hbox1.addWidget(QLabel('Location: '), 1) self.stack_Hbox1.addWidget(self.calibrationLocationBox, 1) self.stack_Vbox.addLayout(self.stack_Hbox1) self.modeGroup.addButton(self.wavAnalysisChk) self.modeGroup.addButton(self.calibrationCurveChk) self.modeGroup.setExclusive(True) # Outputs self.outputCtrlBox = QGroupBox('Outputs') self.outputCtrlBox.setLayout(self.stack_Hbox2) self.stack_Vbox.addWidget(self.outputCtrlBox) self.toCSVchk = QCheckBox('.csv') self.toJSONchk = QCheckBox('.json') self.toCSVchk.stateChanged.connect(lambda _: self.update_settings( 'output', 'toCSV', self.toCSVchk.isChecked())) self.toJSONchk.stateChanged.connect(lambda _: self.update_settings( 'output', 'toJSON', self.toJSONchk.isChecked())) self.stack_Hbox2.addWidget(self.toCSVchk) self.stack_Hbox2.addWidget(self.toJSONchk) self.stack_Vbox.addLayout(self.stack_Hbox2) self.stack.addWidget(self.dataTable) self.stack.addWidget(self.batchCtrlBox) self.stack.setCurrentWidget(self.dataTable) self.stack.show() self.batchAnalyzeChk.stateChanged.connect(self.toggle_stack) self.batchAnalyzeChk.setChecked(False) self.stack_Vbox.addStretch() ## PROCESSING CONTROLS ## self.processControls = QGroupBox('Signal Processing') self.tOffsetSlider = QSlider(Qt.Horizontal, ) self.tOffsetSlider.setMinimum(1) self.tOffsetSlider.setMaximum(100) self.tOffsetSlider.setValue(100) self.tOffsetSlider.setTickPosition(QSlider.TicksBelow) self.tOffsetSlider.setTickInterval(10) self.tOffsetSlider.valueChanged.connect( lambda val: self.update_settings('processing', 'tChop', val)) self.tOffsetLayout = QHBoxLayout() self.tOffsetSlider_Box = QGroupBox( f'Chop Signal - {self.tOffsetSlider.value()}%') self.tOffsetSlider.valueChanged.connect( lambda val: self.tOffsetSlider_Box.setTitle(f'Chop Signal - {val}%' )) self.tOffsetSlider_Box.setLayout(self.tOffsetLayout) self.tOffsetLayout.addWidget(self.tOffsetSlider) self.nFFTSlider = QSlider(Qt.Horizontal, ) self.nFFTSlider.setMinimum(1) self.nFFTSlider.setMaximum(16) self.nFFTSlider.setValue(1) self.nFFTSlider.setTickPosition(QSlider.TicksBelow) self.nFFTSlider.setTickInterval(2) self.nFFTSlider.valueChanged.connect( lambda val: self.update_settings('processing', 'detail', val)) self.nFFTLayout = QHBoxLayout() self.nFFTSlider.valueChanged.connect( lambda val: self.nFFTSlider_Box.setTitle(f'FFT Size - {val*65536}' )) self.nFFTSlider_Box = QGroupBox( f'FFT Size - {self.nFFTSlider.value()*65536}') self.nFFTSlider_Box.setLayout(self.nFFTLayout) self.nFFTLayout.addWidget(self.nFFTSlider) self.rVbox2.addWidget(self.tOffsetSlider_Box) self.rVbox2.addWidget(self.nFFTSlider_Box) self.processControls.setLayout(self.rVbox2) self.lVbox.addLayout(self.lHbox_top, 1) self.lVbox.addWidget(self.fileTree, 7) self.lVbox.addLayout(self.lHbox, 1) self.lHbox.addWidget(self.analyzeBtn, 2) self.lHbox.addWidget(self.batchAnalyzeChk, 1) self.vSplit.addWidget(self.canvas) self.vSplit.addWidget(self.hSplit) self.rVbox.addWidget(self.vSplit) self.hSplit.addWidget(self.stack) self.hSplit.addWidget(self.processControls) self.settings = { 'processing': { 'tChop': self.tOffsetSlider.value(), 'detail': self.nFFTSlider.value() }, 'output': { 'toCSV': self.toCSVchk.isChecked(), 'toJSON': self.toJSONchk.isChecked() } } self.setLayout(self.mainLayout) def on_browse(self): # Browse to file tree root directory options = QFileDialog.Options() path = QFileDialog.getExistingDirectory( self, caption="Choose root directory", options=options) self.rootDirEdit.setText(path) self.fileTree.setRootIndex(self.fileModel.index(path)) def on_edit_root(self): # Update the file tree root directory self.fileTree.setRootIndex( self.fileModel.index(self.rootDirEdit.text())) def update_settings(self, category, setting, value): # Update settings and reprocess FFT if in single analysis mode self.settings[category][setting] = value if category == 'processing' and self.fileTree.selectedIndexes(): self.analyze(self.fileTree.selectedIndexes()) def toggle_stack(self, state): if state == 2: self.stack.setCurrentWidget(self.batchCtrlBox) self.fileTree.setSelectionMode(QAbstractItemView.MultiSelection) else: self.stack.setCurrentWidget(self.dataTable) self.fileTree.setSelectionMode(QAbstractItemView.SingleSelection) def analyze(self, filePaths): if self.batchAnalyzeChk.isChecked(): if self.wavAnalysisChk.isChecked(): self.batch_analyze_wav( [self.fileModel.filePath(path) for path in filePaths[::4]]) if self.calibrationCurveChk.isChecked(): self.generate_calibration_curve( [self.fileModel.filePath(path) for path in filePaths[::4]]) else: if os.path.isdir(self.fileModel.filePath( filePaths[0])) or len(filePaths) > 4: QMessageBox.information( self, 'Error', 'Please select only 1 file for single analysis.') return self.single_analyze_wav(self.fileModel.filePath(filePaths[0])) def single_analyze_wav(self, filePath): """ Do an FFT and find peaks on a single wav file :param filePath: file path to .wav file """ tChopped, vChopped, fVals,\ powerFFT, peakFreqs, peakAmps = Utils.AnalyzeFFT(filePath, tChop=self.settings['processing']['tChop'], detail=self.settings['processing']['detail']) self.analyzeDone.emit(tChopped, vChopped, fVals, powerFFT, peakFreqs, peakAmps, filePath) self.update_table(peakFreqs, peakAmps) def batch_analyze_wav(self, filePaths): """ Perform a batch analysis of many .wav files. Outputs FFTs and peaks in .csv or .json format :param filePaths: A list of folders containing the .wav files to be analyzed """ toCSV = self.settings['output']['toCSV'] toJSON = self.settings['output']['toJSON'] start = time.time() fileTotal = 0 for path in filePaths: if os.path.isdir(path): blockName = os.path.basename(path) print(f'Block: {blockName}') files = [ os.path.join(path, file) for file in os.listdir(path) if '.wav' in file ] fileTotal += len(files) if toCSV: if not os.path.exists(os.path.join(path, 'fft_results_csv')): os.makedirs(os.path.join(path, 'fft_results_csv')) resultFilePath = os.path.join(path, 'fft_results_csv') print('Processing FFTs...') with multiprocessing.Pool(processes=4) as pool: results = pool.starmap( Utils.AnalyzeFFT, zip(files, itertools.repeat(True), itertools.repeat(True))) results = [ result for result in results if result is not None ] peaks = [result[0] for result in results] ffts = [result[1] for result in results] print('Writing to .csv...') resultFileName = os.path.join(resultFilePath, f'{blockName}_Peaks.csv') peakFrames = pd.concat(peaks) peakFrames.to_csv(resultFileName, index=False, header=True) with concurrent.futures.ThreadPoolExecutor( max_workers=16) as executor: executor.map(self.multi_csv_write, ffts) if toJSON: if not os.path.exists( os.path.join(path, 'fft_results_json')): os.makedirs(os.path.join(path, 'fft_results_json')) print(os.path.join(path, 'fft_results_json')) print('Processing FFTs...') with multiprocessing.Pool(processes=4) as pool: results = pool.starmap( Utils.AnalyzeFFT, zip(files, itertools.repeat(True), itertools.repeat(False), itertools.repeat(True))) results = [ result for result in results if result is not None ] print('Writing to .json...') with concurrent.futures.ThreadPoolExecutor( max_workers=16) as executor: executor.map(self.multi_json_write, results) end = time.time() print( f'**Done!** {len(filePaths)} blocks with {fileTotal} files took {round(end-start, 1)}s' ) def generate_calibration_curve(self, filePaths): """ Attempt to fit an exponential function to a set of data points (x: Peak Frequency, y: Compressive strength) provided in JSON format. ex:{ "shape": "2-Hole", "testData": { "location": "1", "strength": 3.092453552, "peaks": [ { "frequency": 1134.5561082797967, "magnitude": 0.349102384777402 }] }, "waveData": [...], "freqData": [...] } Plot the curve, data points and give the function if successful. ** NOTE ** This function is still experimental and a bit buggy. Sometimes the scipy.optimize curve_fit won't converge with the initial guess given for the coeffecients. You're probably better off writing your own code. :param filePaths: A list of folders containing .jsons """ # Strike Location location = self.calibrationLocationBox.currentText() # Function to fit to the data exp_f = lambda x, a, b, c: a * np.exp(b * x) + c # Threaded method for opening all the .jsons and fitting calibCurve = ThreadedCalibrationCurve(filePaths, location, exp_f) progressDialog = QProgressDialog( f'Gettings samples for location: {location}', None, 0, len(filePaths), self) progressDialog.setModal(True) calibCurve.blocksSearched.connect(progressDialog.setValue) try: peakFreqs, strengths, popt, pcov, fitX = calibCurve.run() except Exception as e: QMessageBox.information(self, 'Error', e) return # Calculate R Squared residuals = strengths - exp_f(peakFreqs, *popt) ss_res = np.sum(residuals**2) ss_tot = np.sum((strengths - np.mean(strengths))**2) r_squared = 1 - (ss_res / ss_tot) # Plot Results fig = Figure() plt.scatter(peakFreqs, strengths) plt.plot(fitX, exp_f(fitX, *popt), '-k') ax = plt.gca() plt.text( 0.05, 0.9, f'y = {round(popt[0],3)}*exp({round(popt[1], 5)}x) + {round(popt[2], 3)}\n', ha='left', va='center', transform=ax.transAxes) plt.text(0.05, 0.85, f'R^2 = {round(r_squared,3)}', ha='left', va='center', transform=ax.transAxes) plt.title(f'Calibration Curve, Location: {location}') plt.xlabel('Frequency (Hz)') plt.ylabel('Compressive Strength (MPa)') plt.show() def multi_csv_write(self, frameTuple): frame = frameTuple[1] wavPath = frameTuple[0] resultFileDir = os.path.join(os.path.dirname(wavPath), 'fft_results_csv') resultFileName = os.path.basename(wavPath) + '_fft.csv' resultFilePath = os.path.join(resultFileDir, resultFileName) frame.to_csv(resultFilePath, index=False, header=True) def multi_json_write(self, results): data = results[0] wavPath = results[1] jsonFileDir = os.path.join(os.path.dirname(wavPath), 'fft_results_json') resultFileName = os.path.basename(wavPath) + '_fft.json' resultFilePath = os.path.join(jsonFileDir, resultFileName) # blockName = os.path.basename(os.path.dirname(wavPath)) # blockDir = os.path.join(jsonFileDir, blockName) # if not os.path.exists(blockDir): # os.makedirs(blockDir) # print(resultFilePath) with open(resultFilePath, 'w') as f: json.dump(data, f, indent=2) def update_table(self, peakFreqs, peakAmps): """ :param peakFreqs: :param peakAmps: :return: """ self.dataTable.setRowCount(2) self.dataTable.setColumnCount(len(peakFreqs) + 1) self.dataTable.setItem(0, 0, QTableWidgetItem("Frequencies: ")) self.dataTable.setItem(1, 0, QTableWidgetItem("Powers: ")) for col, freq in enumerate(peakFreqs, start=1): self.dataTable.setItem(0, col, QTableWidgetItem(str(round(freq)))) for col, power in enumerate(peakAmps, start=1): item = QTableWidgetItem(str(round(power, 3))) if power > 0.7: item.setBackground(QColor(239, 81, 28)) elif power >= 0.4: item.setBackground(QColor(232, 225, 34)) elif power < 0.4: item.setBackground(QColor(113, 232, 34)) self.dataTable.setItem(1, col, item)