def _init_widget(self, listbox: QTreeView): listbox.setModel(self) listbox.setItemDelegate(self.item_delegate) listbox.setContextMenuPolicy(Qt.CustomContextMenu) # listbox.customContextMenuRequested.connect(self.context_menu) listbox.customContextMenuRequested.connect(self.menu) listbox.setDragEnabled(True) listbox.setAcceptDrops(True) listbox.setDropIndicatorShown(True) listbox.setSelectionMode(listbox.ExtendedSelection) # listbox.setMovement(QTreeView.Snap) # listbox.setDragDropMode(QTreeView.InternalMove) listbox.setDragDropMode(QAbstractItemView.DragDrop) # listbox.setAlternatingRowColors(True) # listbox.setDefaultDropAction(Qt.MoveAction) # listbox.setDragDropOverwriteMode(True) # listbox.entered.connect(self.layer_entered) # listbox.setFont(QFont('Andale Mono', 13)) # the various signals that may result from the user changing the selections # listbox.activated.connect(self.changedSelection) # listbox.clicked.connect(self.changedSelection) # listbox.doubleClicked.connect(self.changedSelection) # listbox.pressed.connect(self.changedSelection) listbox.selectionModel().selectionChanged.connect( self.changedSelection) self.widgets.append(listbox)
class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle('Drag & Drop') # Даем разрешение на Drop self.setAcceptDrops(True) self.list_files = QListWidget() self.label_total_files = QLabel() model = QFileSystemModel() model.setRootPath(QDir.currentPath()) model.setReadOnly(False) self.tree = QTreeView() self.tree.setModel(model) self.tree.setRootIndex(model.index(QDir.currentPath())) self.tree.setSelectionMode(QTreeView.SingleSelection) self.tree.setDragDropMode(QTreeView.InternalMove) main_layout = QVBoxLayout() main_layout.addWidget(self.tree) main_layout.addWidget(QLabel('Перетащите файл:')) main_layout.addWidget(self.list_files) main_layout.addWidget(self.label_total_files) central_widget = QWidget() central_widget.setLayout(main_layout) self.setCentralWidget(central_widget) self._update_states() def _update_states(self): self.label_total_files.setText('Files: {}'.format( self.list_files.count())) def dragEnterEvent(self, event): # Тут выполняются проверки и дается (или нет) разрешение на Drop mime = event.mimeData() # Если перемещаются ссылки if mime.hasUrls(): # Разрешаем event.acceptProposedAction() def dropEvent(self, event): # Обработка события Drop for url in event.mimeData().urls(): file_name = url.toLocalFile() self.list_files.addItem(file_name) self._update_states() return super().dropEvent(event)
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 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 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 OpenedFileExplorer(DockWidget): """Opened File Explorer is list widget with list of opened files. It implements switching current file, files sorting. Uses _OpenedFileModel internally. Class instance created by Workspace. """ def __init__(self, workspace): DockWidget.__init__(self, workspace, "&Opened Files", QIcon(":/enkiicons/filtered.png"), "Alt+O") self._workspace = workspace self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.tvFiles = QTreeView(self) self.tvFiles.setHeaderHidden(True) self.tvFiles.setEditTriggers(QAbstractItemView.SelectedClicked) self.tvFiles.setContextMenuPolicy(Qt.CustomContextMenu) self.tvFiles.setDragEnabled(True) self.tvFiles.setDragDropMode(QAbstractItemView.InternalMove) self.tvFiles.setRootIsDecorated(False) self.tvFiles.setTextElideMode(Qt.ElideMiddle) self.tvFiles.setUniformRowHeights(True) self.tvFiles.customContextMenuRequested.connect(self._onTvFilesCustomContextMenuRequested) self.setWidget(self.tvFiles) self.setFocusProxy(self.tvFiles) self.model = _OpenedFileModel(self) # Not protected, because used by Configurator self.tvFiles.setModel(self.model) self.tvFiles.setAttribute(Qt.WA_MacShowFocusRect, False) self.tvFiles.setAttribute(Qt.WA_MacSmallSize) self._workspace.currentDocumentChanged.connect(self._onCurrentDocumentChanged) # disconnected by startModifyModel() self.tvFiles.selectionModel().selectionChanged.connect(self._onSelectionModelSelectionChanged) self.tvFiles.activated.connect(self._workspace.focusCurrentDocument) core.actionManager().addAction("mView/aOpenedFiles", self.showAction()) def terminate(self): """Explicitly called destructor """ core.actionManager().removeAction("mView/aOpenedFiles") def startModifyModel(self): """Blocks signals from model while it is modified by code """ self.tvFiles.selectionModel().selectionChanged.disconnect(self._onSelectionModelSelectionChanged) def finishModifyModel(self): """Unblocks signals from model """ self.tvFiles.selectionModel().selectionChanged.connect(self._onSelectionModelSelectionChanged) @pyqtSlot(Document, Document) def _onCurrentDocumentChanged(self, oldDocument, currentDocument): # pylint: disable=W0613 """ Current document has been changed on workspace """ if currentDocument is not None: index = self.model.documentIndex(currentDocument) self.startModifyModel() self.tvFiles.setCurrentIndex(index) # scroll the view self.tvFiles.scrollTo(index) self.finishModifyModel() @pyqtSlot(QItemSelection, QItemSelection) def _onSelectionModelSelectionChanged(self, selected, deselected): # pylint: disable=W0613 """ Item selected in the list. Switch current document """ if not selected.indexes(): # empty list, last file closed return index = selected.indexes()[0] # backup/restore current focused widget as setting active mdi window will steal it focusWidget = self.window().focusWidget() # set current document document = self._workspace.sortedDocuments[index.row()] self._workspace.setCurrentDocument(document) # restore focus widget if focusWidget: focusWidget.setFocus() @pyqtSlot(QPoint) def _onTvFilesCustomContextMenuRequested(self, pos): """Connected automatically by uic """ menu = QMenu() menu.addAction(core.actionManager().action("mFile/mClose/aCurrent")) menu.addAction(core.actionManager().action("mFile/mSave/aCurrent")) menu.addAction(core.actionManager().action("mFile/mReload/aCurrent")) menu.addSeparator() menu.addAction(core.actionManager().action("mFile/mFileSystem/aRename")) toggleExecutableAction = core.actionManager().action("mFile/mFileSystem/aToggleExecutable") if toggleExecutableAction: # not available on Windows menu.addAction(toggleExecutableAction) core.actionManager().action("mFile/mFileSystem").menu().aboutToShow.emit() # to update aToggleExecutable menu.exec_(self.tvFiles.mapToGlobal(pos))
class OpenedFileExplorer(DockWidget): """Opened File Explorer is list widget with list of opened files. It implements switching current file, files sorting. Uses _OpenedFileModel internally. Class instance created by Workspace. """ def __init__(self, workspace): DockWidget.__init__(self, workspace, "&Opened Files", QIcon(":/enkiicons/filtered.png"), "Alt+O") self._workspace = workspace self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.tvFiles = QTreeView(self) self.tvFiles.setHeaderHidden(True) self.tvFiles.setEditTriggers(QAbstractItemView.SelectedClicked) self.tvFiles.setContextMenuPolicy(Qt.CustomContextMenu) self.tvFiles.setDragEnabled(True) self.tvFiles.setDragDropMode(QAbstractItemView.InternalMove) self.tvFiles.setRootIsDecorated(False) self.tvFiles.setTextElideMode(Qt.ElideMiddle) self.tvFiles.setUniformRowHeights(True) self.tvFiles.customContextMenuRequested.connect( self._onTvFilesCustomContextMenuRequested) self.setWidget(self.tvFiles) self.setFocusProxy(self.tvFiles) self.model = _OpenedFileModel( self) # Not protected, because used by Configurator self.tvFiles.setModel(self.model) self.tvFiles.setAttribute(Qt.WA_MacShowFocusRect, False) self.tvFiles.setAttribute(Qt.WA_MacSmallSize) self._workspace.currentDocumentChanged.connect( self._onCurrentDocumentChanged) # disconnected by startModifyModel() self.tvFiles.selectionModel().selectionChanged.connect( self._onSelectionModelSelectionChanged) self.tvFiles.activated.connect(self._workspace.focusCurrentDocument) core.actionManager().addAction("mView/aOpenedFiles", self.showAction()) def terminate(self): """Explicitly called destructor """ core.actionManager().removeAction("mView/aOpenedFiles") def startModifyModel(self): """Blocks signals from model while it is modified by code """ self.tvFiles.selectionModel().selectionChanged.disconnect( self._onSelectionModelSelectionChanged) def finishModifyModel(self): """Unblocks signals from model """ self.tvFiles.selectionModel().selectionChanged.connect( self._onSelectionModelSelectionChanged) @pyqtSlot(Document, Document) def _onCurrentDocumentChanged(self, oldDocument, currentDocument): # pylint: disable=W0613 """ Current document has been changed on workspace """ if currentDocument is not None: index = self.model.documentIndex(currentDocument) self.startModifyModel() self.tvFiles.setCurrentIndex(index) # scroll the view self.tvFiles.scrollTo(index) self.finishModifyModel() @pyqtSlot(QItemSelection, QItemSelection) def _onSelectionModelSelectionChanged(self, selected, deselected): # pylint: disable=W0613 """ Item selected in the list. Switch current document """ if not selected.indexes(): # empty list, last file closed return index = selected.indexes()[0] # backup/restore current focused widget as setting active mdi window will steal it focusWidget = self.window().focusWidget() # set current document document = self._workspace.sortedDocuments[index.row()] self._workspace.setCurrentDocument(document) # restore focus widget if focusWidget: focusWidget.setFocus() @pyqtSlot(QPoint) def _onTvFilesCustomContextMenuRequested(self, pos): """Connected automatically by uic """ menu = QMenu() menu.addAction(core.actionManager().action("mFile/mClose/aCurrent")) menu.addAction(core.actionManager().action("mFile/mSave/aCurrent")) menu.addAction(core.actionManager().action("mFile/mReload/aCurrent")) menu.addSeparator() menu.addAction( core.actionManager().action("mFile/mFileSystem/aRename")) toggleExecutableAction = core.actionManager().action( "mFile/mFileSystem/aToggleExecutable") if toggleExecutableAction: # not available on Windows menu.addAction(toggleExecutableAction) core.actionManager().action("mFile/mFileSystem").menu( ).aboutToShow.emit() # to update aToggleExecutable menu.exec_(self.tvFiles.mapToGlobal(pos))
class OpenedFileExplorer(DockWidget): """Opened File Explorer is list widget with list of opened files. It implements switching current file, files sorting. Uses _OpenedFileModel internally. Class instance created by Workspace. """ def __init__(self, workspace): DockWidget.__init__(self, workspace, "&Opened Files", QIcon(":/enkiicons/filtered.png"), "Alt+O") self._workspace = workspace self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.tvFiles = QTreeView(self) self.tvFiles.setHeaderHidden(True) self.tvFiles.setEditTriggers(QAbstractItemView.SelectedClicked) self.tvFiles.setContextMenuPolicy(Qt.CustomContextMenu) self.tvFiles.setDragEnabled(True) self.tvFiles.setDragDropMode(QAbstractItemView.InternalMove) self.tvFiles.setRootIsDecorated(False) self.tvFiles.setTextElideMode(Qt.ElideMiddle) self.tvFiles.setUniformRowHeights(True) self.tvFiles.customContextMenuRequested.connect(self._onTvFilesCustomContextMenuRequested) self.setWidget(self.tvFiles) self.setFocusProxy(self.tvFiles) self.model = _OpenedFileModel(self) # Not protected, because used by Configurator self.tvFiles.setModel(self.model) self.tvFiles.setAttribute(Qt.WA_MacShowFocusRect, False) self.tvFiles.setAttribute(Qt.WA_MacSmallSize) self._workspace.currentDocumentChanged.connect(self._onCurrentDocumentChanged) # disconnected by startModifyModel() self.tvFiles.selectionModel().selectionChanged.connect(self._onSelectionModelSelectionChanged) self.tvFiles.activated.connect(self._workspace.focusCurrentDocument) core.actionManager().addAction("mView/aOpenedFiles", self.showAction()) # Add auto-hide capability. self._waitForCtrlRelease = False core.actionManager().action("mNavigation/aNext").triggered.connect( self._setWaitForCtrlRelease) core.actionManager().action("mNavigation/aPrevious").triggered.connect( self._setWaitForCtrlRelease) QApplication.instance().installEventFilter(self) def terminate(self): """Explicitly called destructor """ core.actionManager().removeAction("mView/aOpenedFiles") QApplication.instance().removeEventFilter(self) def startModifyModel(self): """Blocks signals from model while it is modified by code """ self.tvFiles.selectionModel().selectionChanged.disconnect(self._onSelectionModelSelectionChanged) def finishModifyModel(self): """Unblocks signals from model """ self.tvFiles.selectionModel().selectionChanged.connect(self._onSelectionModelSelectionChanged) @pyqtSlot(Document, Document) def _onCurrentDocumentChanged(self, oldDocument, currentDocument): # pylint: disable=W0613 """ Current document has been changed on workspace """ if currentDocument is not None: index = self.model.documentIndex(currentDocument) self.startModifyModel() self.tvFiles.setCurrentIndex(index) # scroll the view self.tvFiles.scrollTo(index) self.finishModifyModel() @pyqtSlot(QItemSelection, QItemSelection) def _onSelectionModelSelectionChanged(self, selected, deselected): # pylint: disable=W0613 """ Item selected in the list. Switch current document """ if not selected.indexes(): # empty list, last file closed return index = selected.indexes()[0] # backup/restore current focused widget as setting active mdi window will steal it focusWidget = self.window().focusWidget() # set current document document = self._workspace.sortedDocuments[index.row()] self._workspace.setCurrentDocument(document) # restore focus widget if focusWidget: focusWidget.setFocus() @pyqtSlot(QPoint) def _onTvFilesCustomContextMenuRequested(self, pos): """Connected automatically by uic """ menu = QMenu() menu.addAction(core.actionManager().action("mFile/mClose/aCurrent")) menu.addAction(core.actionManager().action("mFile/mSave/aCurrent")) menu.addAction(core.actionManager().action("mFile/mReload/aCurrent")) menu.addSeparator() menu.addAction(core.actionManager().action("mFile/mFileSystem/aRename")) toggleExecutableAction = core.actionManager().action("mFile/mFileSystem/aToggleExecutable") if toggleExecutableAction: # not available on Windows menu.addAction(toggleExecutableAction) core.actionManager().action("mFile/mFileSystem").menu().aboutToShow.emit() # to update aToggleExecutable menu.exec_(self.tvFiles.mapToGlobal(pos)) def _setWaitForCtrlRelease(self): # We can't see actual Ctrl+PgUp/PgDn keypresses, since these get eaten # by the QAction and don't even show up in the event filter below. We # want to avoid waiting for a Ctrl release if the menu item brought us # here. As a workaround, check that Ctrl is pressed. If so, it's # unlikely to be the menu item. if QApplication.instance().keyboardModifiers() & Qt.ControlModifier: self._waitForCtrlRelease = True self.show() else: # If this was a menu selection, then update the MRU list. We can't # do this now, since the current document hasn't been changed yet. QTimer.singleShot(0, self.model.sortDocuments) def eventFilter(self, obj, event): """An event filter that looks for ctrl key releases and focus out events.""" # Wait for the user to release the Ctrl key. if ( self._waitForCtrlRelease and event.type() == QEvent.KeyRelease and event.key() == Qt.Key_Control and event.modifiers() == Qt.NoModifier): self.model.sortDocuments() self._waitForCtrlRelease = False if not self.isPinned(): self.hide() # Look for a focus out event sent by the containing widget's focus # proxy. if event.type() == QEvent.FocusOut and obj == self.focusProxy(): self.model.sortDocuments() return QObject.eventFilter(self, obj, event)
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()
def createGUI(self): root = TagManager.createSimpleTree() model = TagTreeModel(root, self._tag_checker, self._color_helper, parent=self) model.invalidValueSetted.connect( self.invalidValueSettedByUserToTreeViewModel) #model.dataChanged.connect(dataChanged) self.setMinimumSize(300, 150) self.setWindowTitle('My Tag Manager') layout = QVBoxLayout(self) ## Menu Layout menu_layout = QHBoxLayout(self) but_save = QPushButton('Save', self) but_save.setIcon(QApplication.style().standardIcon( QStyle.SP_DialogSaveButton)) menu_layout.addWidget(but_save) but_save.clicked.connect(self.but_save_clicked) but_load = QPushButton('Load', self) but_load.setIcon(QApplication.style().standardIcon( QStyle.SP_DirOpenIcon)) menu_layout.addWidget(but_load) but_load.clicked.connect(self.but_load_clicked) but_new_f = QPushButton('New tag collections', self) but_new_f.setIcon(QApplication.style().standardIcon( QStyle.SP_FileIcon)) but_new_f.setToolTip('New tag collections') menu_layout.addWidget(but_new_f) but_new_f.clicked.connect(self.but_new_tree_clicked) layout.addLayout(menu_layout) ## Menu Layout END tv = QTreeView(self) tv.setDragDropMode(QAbstractItemView.InternalMove) tv.setDragEnabled(True) tv.setAcceptDrops(True) tv.setDropIndicatorShown(True) tv.setContextMenuPolicy(Qt.CustomContextMenu) tv.customContextMenuRequested.connect( self.customContextMenuRequestedForTreeView) tv.setModel(model) #tv.setAlternatingRowColors(True) layout.addWidget(tv) self._widget_tv = tv but = QPushButton('Remove Selected', self) but.setIcon(QApplication.style().standardIcon(QStyle.SP_TrashIcon)) layout.addWidget(but) but.clicked.connect(self.but_remove_clicked) but_add = QPushButton('Add', self) layout.addWidget(but_add) but_add.clicked.connect(self.but_add_clicked) but_add_to_root = QPushButton('Add new element to root') but_add_to_root.setToolTip("Add new element to root of tree.") layout.addWidget(but_add_to_root) but_add_to_root.clicked.connect(self.but_add_to_root_clicked) but_item_up = QPushButton("Up") but_item_up.setToolTip("Up selected item.") but_item_up.clicked.connect(self.but_item_up_clicked) layout.addWidget(but_item_up) but_item_down = QPushButton("Down") but_item_down.setToolTip("Down selected item.") but_item_down.clicked.connect(self.but_item_down_clicked) layout.addWidget(but_item_down) but_font = QPushButton('Set Font', self) layout.addWidget(but_font) but_font.clicked.connect(self.but_font_dialog_clicked) self.layout().deleteLater() # Remove default layout self.setLayout(layout)
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 OpenedFileExplorer(DockWidget): """Opened File Explorer is list widget with list of opened files. It implements switching current file, files sorting. Uses _OpenedFileModel internally. Class instance created by Workspace. """ def __init__(self, workspace): DockWidget.__init__(self, workspace, "&Opened Files", QIcon(":/enkiicons/filtered.png"), "Alt+O") self._workspace = workspace self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.tvFiles = QTreeView(self) self.tvFiles.setHeaderHidden(True) self.tvFiles.setEditTriggers(QAbstractItemView.SelectedClicked) self.tvFiles.setContextMenuPolicy(Qt.CustomContextMenu) self.tvFiles.setDragEnabled(True) self.tvFiles.setDragDropMode(QAbstractItemView.InternalMove) self.tvFiles.setRootIsDecorated(False) self.tvFiles.setTextElideMode(Qt.ElideMiddle) self.tvFiles.setUniformRowHeights(True) self.tvFiles.customContextMenuRequested.connect( self._onTvFilesCustomContextMenuRequested) self.setWidget(self.tvFiles) self.setFocusProxy(self.tvFiles) self.model = _OpenedFileModel( self) # Not protected, because used by Configurator self.tvFiles.setModel(self.model) self.tvFiles.setAttribute(Qt.WA_MacShowFocusRect, False) self.tvFiles.setAttribute(Qt.WA_MacSmallSize) self._workspace.currentDocumentChanged.connect( self._onCurrentDocumentChanged) # disconnected by startModifyModel() self.tvFiles.selectionModel().selectionChanged.connect( self._onSelectionModelSelectionChanged) self.tvFiles.activated.connect(self._workspace.focusCurrentDocument) core.actionManager().addAction("mView/aOpenedFiles", self.showAction()) # Add auto-hide capability. self._waitForCtrlRelease = False core.actionManager().action("mNavigation/aNext").triggered.connect( self._setWaitForCtrlRelease) core.actionManager().action("mNavigation/aPrevious").triggered.connect( self._setWaitForCtrlRelease) QApplication.instance().installEventFilter(self) def terminate(self): """Explicitly called destructor """ core.actionManager().removeAction("mView/aOpenedFiles") QApplication.instance().removeEventFilter(self) def startModifyModel(self): """Blocks signals from model while it is modified by code """ self.tvFiles.selectionModel().selectionChanged.disconnect( self._onSelectionModelSelectionChanged) def finishModifyModel(self): """Unblocks signals from model """ self.tvFiles.selectionModel().selectionChanged.connect( self._onSelectionModelSelectionChanged) @pyqtSlot(Document, Document) def _onCurrentDocumentChanged(self, oldDocument, currentDocument): # pylint: disable=W0613 """ Current document has been changed on workspace """ if currentDocument is not None: index = self.model.documentIndex(currentDocument) self.startModifyModel() self.tvFiles.setCurrentIndex(index) # scroll the view self.tvFiles.scrollTo(index) self.finishModifyModel() @pyqtSlot(QItemSelection, QItemSelection) def _onSelectionModelSelectionChanged(self, selected, deselected): # pylint: disable=W0613 """ Item selected in the list. Switch current document """ if not selected.indexes(): # empty list, last file closed return index = selected.indexes()[0] # backup/restore current focused widget as setting active mdi window will steal it focusWidget = self.window().focusWidget() # set current document document = self._workspace.sortedDocuments[index.row()] self._workspace.setCurrentDocument(document) # restore focus widget if focusWidget: focusWidget.setFocus() @pyqtSlot(QPoint) def _onTvFilesCustomContextMenuRequested(self, pos): """Connected automatically by uic """ menu = QMenu() menu.addAction(core.actionManager().action("mFile/mClose/aCurrent")) menu.addAction(core.actionManager().action("mFile/mSave/aCurrent")) menu.addAction(core.actionManager().action("mFile/mReload/aCurrent")) menu.addSeparator() menu.addAction( core.actionManager().action("mFile/mFileSystem/aRename")) toggleExecutableAction = core.actionManager().action( "mFile/mFileSystem/aToggleExecutable") if toggleExecutableAction: # not available on Windows menu.addAction(toggleExecutableAction) core.actionManager().action("mFile/mFileSystem").menu( ).aboutToShow.emit() # to update aToggleExecutable menu.exec_(self.tvFiles.mapToGlobal(pos)) def _setWaitForCtrlRelease(self): # We can't see actual Ctrl+PgUp/PgDn keypresses, since these get eaten # by the QAction and don't even show up in the event filter below. We # want to avoid waiting for a Ctrl release if the menu item brought us # here. As a workaround, check that Ctrl is pressed. If so, it's # unlikely to be the menu item. if QApplication.instance().keyboardModifiers() & Qt.ControlModifier: self._waitForCtrlRelease = True self.show() else: # If this was a menu selection, then update the MRU list. We can't # do this now, since the current document hasn't been changed yet. QTimer.singleShot(0, self.model.sortDocuments) def eventFilter(self, obj, event): """An event filter that looks for ctrl key releases and focus out events.""" # Wait for the user to release the Ctrl key. if (self._waitForCtrlRelease and event.type() == QEvent.KeyRelease and event.key() == Qt.Key_Control and event.modifiers() == Qt.NoModifier): self.model.sortDocuments() self._waitForCtrlRelease = False if not self.isPinned(): self.hide() # Look for a focus out event sent by the containing widget's focus # proxy. if event.type() == QEvent.FocusOut and obj == self.focusProxy(): self.model.sortDocuments() return QObject.eventFilter(self, obj, event)