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 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 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 DataViewer(QWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setMinimumSize(500, 400) main_layout = QHBoxLayout() main_layout.setSpacing(5) self.setLayout(main_layout) control_column = QVBoxLayout() main_layout.addLayout(control_column, stretch=1) refresh_button = QPushButton('🔄 Refresh') refresh_button.setFont(cn.EMOJI_FONT) refresh_button.clicked.connect(self.refresh_list) control_column.addWidget(refresh_button) self.gesture_tree_view = QTreeView() self.gesture_tree_view.setMinimumWidth(250) self.gesture_tree_view.header().hide() self.gesture_tree_view.setEditTriggers( QAbstractItemView.NoEditTriggers) self.gesture_tree_view.clicked.connect(self.show_selected) self.gesture_tree_view.setContextMenuPolicy(Qt.CustomContextMenu) self.gesture_tree_view.customContextMenuRequested.connect( self.gesture_context_menu) self.gesture_model = QStandardItemModel() self.gesture_tree_view.setModel(self.gesture_model) self.gesture_tree_view.setAnimated(True) control_column.addWidget(self.gesture_tree_view) self.displayed_gestures_layout = QVBoxLayout() display_column = QVBoxLayout() close_all_button = QPushButton('❌ Close all opened') close_all_button.setFont(cn.EMOJI_FONT) def close_all_displayed_gestures(): for i in range(self.displayed_gestures_layout.count()): self.displayed_gestures_layout.itemAt(i).widget().close() close_all_button.clicked.connect(close_all_displayed_gestures) control_column.addWidget(close_all_button) display_column.addLayout( VerticalScrollableExtension(self.displayed_gestures_layout)) main_layout.addLayout(display_column, stretch=2) self.refresh_list() def refresh_list(self): gestures = sorted(os.listdir(cn.DATA_FOLDER)) gesture_tree = {} for gesture in gestures: parts = gesture.split(cn.FILE_NAME_SEPARATOR) if parts[0] == cn.SESSION_PREFIX: continue if len(parts) < 3 or parts[0] != cn.GESTURE_PREFIX: logger.debug(f'Skipping file {gesture}, unknown naming.') continue index = int(parts[1]) if index < 0 or index >= len(cn.GESTURES): logger.debug(f'Invalid index on {gesture}, skipping.') continue gesture = cn.GESTURES[index] parts[1] = str(gesture) current_node = gesture_tree for part in parts[1:]: current_node = current_node.setdefault(part, {}) self.gesture_model.clear() root = self.gesture_model.invisibleRootItem() def add_tree(tree: dict, node: QStandardItem): for item in tree: child_node = QStandardItem(item) node.appendRow(child_node) add_tree(tree[item], child_node) add_tree(gesture_tree, root) @staticmethod def get_filename(model_index): name = [] node = model_index while node.isValid(): name.append(node.data()) node = node.parent() name.append(cn.GESTURE_PREFIX) # TODO this could be nicer for i, gesture_spec in enumerate(cn.GESTURES): if str(gesture_spec) == name[-2]: name[-2] = str(i) return cn.FILE_NAME_SEPARATOR.join(name[::-1]) def show_selected(self, model_index): is_leaf = not model_index.child(0, 0).isValid() if not is_leaf: self.gesture_tree_view.setExpanded( model_index, not self.gesture_tree_view.isExpanded(model_index)) return filename = DataViewer.get_filename(model_index) selected_file = cn.DATA_FOLDER / filename data = np.load(selected_file) signal_widget = StaticSignalWidget() signal_widget.plot_data(data) widget = NamedExtension(filename, signal_widget) widget = BlinkExtension(widget) widget = ClosableExtension(widget) widget.setMinimumWidth(600) widget.setFixedHeight(200) self.displayed_gestures_layout.addWidget(widget) def gesture_context_menu(self, point): model_index = self.gesture_tree_view.indexAt(point) is_leaf = not model_index.child(0, 0).isValid() if not is_leaf: self.gesture_tree_view.setExpanded( model_index, not self.gesture_tree_view.isExpanded(model_index)) return menu = QMenu() def move_dialog(): Renamer(DataViewer.get_filename(model_index)).exec() menu.addAction('Move', move_dialog) def trash_and_remove_from_tree(): if Renamer.trash_gesture(DataViewer.get_filename(model_index)): self.gesture_model.removeRow(model_index.row(), model_index.parent()) menu.addAction('Trash', trash_and_remove_from_tree) menu.exec(QCursor.pos())
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 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 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 VGExplorer(QWidget): def __init__(self, app, config): super().__init__() self.clipboard = app.clipboard() self.config = config self.setWindowTitle(config.server_name) rootPath = self.get_cwd() self.model = QFileSystemModel() index = self.model.setRootPath(rootPath) self.tree = QTreeView() self.tree.setModel(self.model) self.tree.setRootIndex(index) self.tree.setAnimated(False) self.tree.setIndentation(20) self.tree.hideColumn(1) self.tree.hideColumn(2) self.tree.hideColumn(3) self.tree.setHeaderHidden(True) self.tree.doubleClicked.connect(self.on_double_click) self.tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self.show_menu) windowLayout = QVBoxLayout() windowLayout.addWidget(self.tree) self.setLayout(windowLayout) # Shortcut for hide self.shortcut = QShortcut(QKeySequence(config.toggle_key), self) self.shortcut.activated.connect(self.hide) if not config.hidden: self.show() def toggle_show(self): if self.isHidden(): self.show() else: self.hide() def open_file(self, index): path = self.sender().model().filePath(index) if os.path.isfile(path): subprocess.call([ self.config.vim, "--servername", self.config.server_name, "--remote", path ]) def get_cwd(self): path = subprocess.check_output([ self.config.vim, "--servername", self.config.server_name, "--remote-expr", "getcwd()" ]) return path.decode("utf-8").strip() def on_double_click(self, index): self.open_file(index) def show_menu(self, clickPos): index = self.tree.indexAt(clickPos) selected_path = self.tree.model().filePath(index) enclosing_dir = self.find_enclosing_dir(selected_path) menu = QMenu(self) openAction = menu.addAction("Open") newFolderAction = menu.addAction("New Folder") newFileAction = menu.addAction("New File") copyAction = menu.addAction("Copy") pasteAction = menu.addAction("Paste") renameAction = menu.addAction("Rename") fileInfo = menu.addAction("Properties") menuPos = QPoint(clickPos.x() + 15, clickPos.y() + 15) action = menu.exec_(self.mapToGlobal(menuPos)) if action == openAction: self.open_file(index) elif action == newFolderAction: path = self.get_dialog_str("New Folder", "Enter name for new folder:") if path: self.mkdir(os.path.join(enclosing_dir, path)) elif action == newFileAction: path = self.get_dialog_str("New File", "Enter name for new file:") if path: self.touch(os.path.join(enclosing_dir, path)) elif action == renameAction: path = self.get_dialog_str("Rename File", "Enter new name:") # Naive validation if "/" in path: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("Filename cannot contain '/'") msg.setWindowTitle("Error") msg.exec_() return new_path = os.path.join(enclosing_dir, path) self.move(selected_path, new_path) elif action == copyAction: mime_data = QMimeData() # TODO: support multiple selections mime_data.setUrls([QUrl(Path(selected_path).as_uri())]) self.clipboard.setMimeData(mime_data) elif action == pasteAction: mime_data = self.clipboard.mimeData() if not mime_data: return if mime_data.hasUrls(): for src_url in mime_data.urls(): self.copy(src_url.path(), enclosing_dir) def get_dialog_str(self, title, message): text, confirm = QInputDialog.getText(self, title, message, QLineEdit.Normal, "") if confirm and text != '': return text return None ''' Filesystem and OS Functions ''' def copy(self, src_file, dest_dir): src_basename = os.path.basename(src_file) dest_file = os.path.join(dest_dir, src_basename) # First confirm file doesn't already exist if os.path.exists(dest_file): print(f"Destination path '{dest_file}' already exists, skipping") return print(f"Pasting {src_file} -> {dest_file}") shutil.copy2(src_file, dest_file) def move(self, old_path, new_path): os.rename(old_path, new_path) def mkdir(self, path): if not os.path.exists(path): os.mkdir(path) def touch(self, path): subprocess.run(["touch", path]) def find_enclosing_dir(self, path): ''' If path is file, return dir it is in If path is dir, return itself ''' if os.path.isdir(path): return path if os.path.isfile(path): return str(Path(path).parent)
class FileBrowserWidget(QWidget): on_open = pyqtSignal(str) def __init__(self): super().__init__() self.initUI() def initUI(self): self.model = QFileSystemModel() self.rootFolder = '' self.model.setRootPath(self.rootFolder) self.tree = QTreeView() self.tree.setModel(self.model) self.tree.setAnimated(False) self.tree.setIndentation(20) self.tree.setSortingEnabled(True) self.tree.sortByColumn(0, 0) self.tree.setColumnWidth(0, 200) self.tree.setDragEnabled(True) self.tree.setWindowTitle("Dir View") self.tree.resize(640, 480) self.tree.doubleClicked.connect(self.onDblClick) self.tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect( self.onCustomContextMenuRequested) windowLayout = QVBoxLayout() windowLayout.addWidget(self.tree) windowLayout.setContentsMargins(0, 0, 0, 0) self.setLayout(windowLayout) def onCustomContextMenuRequested(self, point): index = self.tree.indexAt(point) selectedFile = None selectedFolder = None ctx = QMenu("Context menu", self) if index.isValid(): file = self.model.fileInfo(index) selectedFile = file.absoluteFilePath() selectedFolder = selectedFile if file.isDir( ) else file.absolutePath() if file.isDir(): ctx.addAction( "Open in file manager", lambda: QDesktopServices.openUrl( QUrl.fromLocalFile(selectedFile))) if not file.isDir(): for wndTyp, meta in WindowTypes.types: text = 'Open with ' + meta.get('displayName', meta['name']) print(wndTyp, meta) ctx.addAction( QAction(text, self, statusTip=text, triggered=lambda dummy, meta=meta: navigate( "WINDOW", "Type=" + meta['name'], "FileName=" + selectedFile))) ctx.addSeparator() ctx.addAction("Set root folder ...", lambda: self.selectRootFolder(preselect=selectedFolder)) ctx.exec(self.tree.viewport().mapToGlobal(point)) def selectRootFolder(self, preselect=None): if preselect == None: preselect = self.rootFolder dir = QFileDialog.getExistingDirectory(self, "Set root folder", preselect) if dir != None: self.setRoot(dir) def setRoot(self, dir): self.rootFolder = dir self.model.setRootPath(dir) self.tree.setRootIndex(self.model.index(dir)) def onDblClick(self, index): if index.isValid(): file = self.model.fileInfo(index) if not file.isDir(): navigate("OPEN", "FileName=" + file.absoluteFilePath()) def saveState(self): if self.tree.currentIndex().isValid(): info = self.model.fileInfo(self.tree.currentIndex()) return {"sel": info.absoluteFilePath(), "root": self.rootFolder} def restoreState(self, state): try: self.setRoot(state["root"]) except: pass try: idx = self.model.index(state["sel"]) if idx.isValid(): self.tree.expand(idx) self.tree.setCurrentIndex(idx) self.tree.scrollTo(idx, QAbstractItemView.PositionAtCenter) except: pass
class Window(QWidget): def __init__(self, connection): super(Window, self).__init__() self.conn = connection self.proxyModel = MySortFilterProxyModel(self) self.proxyModel.setDynamicSortFilter(True) self.proxyView = QTreeView() set_tree_view(self.proxyView) self.proxyView.setModel(self.proxyModel) self.proxyView.customContextMenuRequested.connect(self.pop_menu) self.filterType = QComboBox() self.filterModule = QComboBox() self.filterClass = QComboBox() self.filterNote = QComboBox() self.infLabel = QLabel() self.link_type = QComboBox() self.resView = QTreeView() set_tree_view(self.resView) self.resModel = QSortFilterProxyModel(self.resView) self.resView.setModel(self.resModel) self.resView.customContextMenuRequested.connect(self.menu_res_view) self.link_box = self.set_layout() self.sort_key = None self.repo = [] self.old_links = [] self.new_links = [] self.query_time = time_run() self.curr_id_db = 0 self.setWindowTitle("Custom Sort/Filter Model") self.resize(900, 750) def set_layout(self): filter_box: QGroupBox = self.set_filter_box() height = 92 filter_box.setMaximumHeight(height) link_box = self.set_link_box() link_box.setMaximumHeight(height) link_box.hide() stack_layout = QVBoxLayout() stack_layout.addWidget(filter_box) stack_layout.addWidget(link_box) proxyLayout = QGridLayout() proxyLayout.addWidget(self.proxyView, 0, 0) proxyLayout.addLayout(stack_layout, 1, 0) proxyLayout.addWidget(self.resView, 2, 0) proxyLayout.setRowStretch(0, 5) proxyLayout.setRowStretch(1, 0) proxyLayout.setRowStretch(2, 3) proxyGroupBox = QGroupBox("Module/Class/Method list") proxyGroupBox.setLayout(proxyLayout) mainLayout = QVBoxLayout() mainLayout.addWidget(proxyGroupBox) self.setLayout(mainLayout) return link_box def save_clicked(self, btn): { "Unload DB": save_init, "Copy all links": copy_to_clipboard }[btn.text()]() def set_filter_box(self): save_btn = QDialogButtonBox(Qt.Vertical) save_btn.addButton("Unload DB", QDialogButtonBox.ActionRole) save_btn.addButton("Copy all links", QDialogButtonBox.ActionRole) save_btn.clicked.connect(self.save_clicked) self.filterNote.addItem("All") self.filterNote.addItem("Not blank") filterTypeLabel = QLabel("&Type Filter") filterTypeLabel.setBuddy(self.filterType) filterModuleLabel = QLabel("&Module Filter") filterModuleLabel.setBuddy(self.filterModule) filterClassLabel = QLabel("&Class Filter") filterClassLabel.setBuddy(self.filterClass) filterNoteLabel = QLabel("&Remark Filter") filterNoteLabel.setBuddy(self.filterNote) filter_box = QGridLayout() filter_box.addWidget(filterTypeLabel, 0, 0) filter_box.addWidget(filterModuleLabel, 0, 1) filter_box.addWidget(filterClassLabel, 0, 2) filter_box.addWidget(filterNoteLabel, 0, 3) filter_box.addWidget(save_btn, 0, 4, 2, 1) filter_box.addWidget(self.filterType, 1, 0) filter_box.addWidget(self.filterModule, 1, 1) filter_box.addWidget(self.filterClass, 1, 2) filter_box.addWidget(self.filterNote, 1, 3) self.set_filters_combo() self.textFilterChanged() self.filterType.currentIndexChanged.connect(self.textFilterChanged) self.filterModule.currentIndexChanged.connect( self.textFilterModuleChanged) self.filterClass.currentIndexChanged.connect(self.textFilterChanged) self.filterNote.currentIndexChanged.connect(self.textFilterChanged) grp_box = QGroupBox() grp_box.setFlat(True) grp_box.setLayout(filter_box) return grp_box def set_filters_combo(self): curs = self.conn.cursor() self.filterType.clear() self.filterType.addItem("All") curs.execute(qsel3) for cc in curs: self.filterType.addItem(memb_type[cc[0]]) self.filterModule.clear() self.filterModule.addItem("All") self.filterModule.addItem("") curs.execute(qsel0) for cc in curs: self.filterModule.addItem(cc[0]) self.filterClass.clear() self.filterClass.addItem("All") curs.execute(qsel1) for cc in curs: self.filterClass.addItem(cc[0]) def textFilterModuleChanged(self): curs = self.conn.cursor() self.filterClass.clear() self.filterClass.addItem("All") if self.filterModule.currentText() == "All": curs.execute(qsel1) else: curs.execute( ("select distinct class from methods2 " "where module = ? order by class;"), (self.filterModule.currentText(), ), ) for cc in curs: self.filterClass.addItem(cc[0]) for cc in curs: self.filterType.addItem(memb_type[cc[0]]) def menu_res_view(self, pos): """ only copy to clipboard """ menu = QMenu(self) menu.addAction("clipboard") # menu.addAction("refresh") action = menu.exec_(self.resView.mapToGlobal(pos)) if action: self._to_clipboard() def _to_clipboard(self): rr = [] for rep in self.repo: pp = [str(x) for x in rep] rr.append("\t".join(pp)) QApplication.clipboard().setText("\n".join(rr)) def pop_menu(self, pos): idx = self.proxyView.indexAt(pos) menu = QMenu(self) if idx.isValid(): menu.addAction("First level only") menu.addSeparator() menu.addAction("sort by level") menu.addAction("sort by module") menu.addSeparator() menu.addAction("append row") if idx.isValid(): menu.addAction("delete rows") menu.addAction("edit links") menu.addSeparator() menu.addAction("not called") menu.addSeparator() menu.addAction("complexity") menu.addSeparator() menu.addAction("refresh") menu.addAction("reload DB") action = menu.exec_(self.proxyView.mapToGlobal(pos)) if action: self.menu_action(action.text()) def setSourceModel(self, model: QStandardItemModel): self.proxyModel.setSourceModel(model) set_columns_width(self.proxyView) set_headers(self.proxyModel, main_headers) def textFilterChanged(self): self.proxyModel.filter_changed( self.filterType.currentText(), self.filterModule.currentText(), self.filterClass.currentText(), self.filterNote.currentText(), ) def menu_action(self, act: str): { "First level only": self.first_level_only, "sort by level": self.sort_by_level, "sort by module": self.sort_by_module, "append row": self.append_row, "refresh": self.refresh, "not called": self.is_not_called, "complexity": self.recalc_complexity, "reload DB": self.reload_data, "edit links": self.edit_links, "delete rows": self.delete_selected_rows, }[act]() def recalc_complexity(self): """ add radon cyclomatic complexity repor data """ mm = self.filterModule.currentText() module = "" if mm == "All" else mm cc_list = cc_report(module) for row in cc_list: self.update_cc(row) mark_deleted_methods(cc_list, module) def update_cc(self, row: Iterable): """ @param row: CC, length, type(C/F/M), module, class, method """ sql_sel = ("select id from methods2 where " "type = ? and module = ? and class = ? and method = ?") sql_upd = "update methods2 set cc = ?, length = ? " "where id = ?" sql_ins = ("insert into methods2 (CC, length, type, module, " "Class, method, remark) values(?,?,?,?,?,?,?);") rr = (*row, ) qq = self.conn.cursor() id = qq.execute(sql_sel, rr[2:]).fetchone() if id: qq.execute(sql_upd, (*rr[:2], id[0])) else: tt = datetime.now().strftime("%Y-%m-%d %H:%M") qq.execute(sql_ins, (*rr, tt)) self.conn.commit() def is_not_called(self): qq = self.conn.cursor() qq.execute(not_called) self.set_res_model(qq, call_headers, False) set_columns_width(self.resView, proportion=(2, 2, 5, 7, 7, 2, 3, 5)) set_headers(self.resModel, call_headers) def reload_data(self): sql1 = ( "delete from methods2;", "insert into methods2 (" "ID, type, module, class, method, CC, length, remark) " "values (?, ?, ?, ?, ?, ?, ?, ?);", ) input_file = prj_path / input_meth load_table(input_file, sql1) sql2 = ( "delete from one_link;", "insert into one_link (id, call_id) values (?, ?);", ) input_file = prj_path / input_link load_table(input_file, sql2) curs = conn.cursor() curs.execute("delete from links;") conn.commit() curs.execute(all_levels_link) conn.commit() self.refresh() def refresh(self): model = QStandardItemModel(0, len(main_headers.split(",")), self.proxyView) qq = conn.cursor() qq.execute(qsel2) vv = ((x[0], memb_type[x[1]], *x[2:-2], x[-2].rjust(4), x[-1]) for x in qq) fill_in_model(model, vv) self.setSourceModel(model) def clear_report_view(self): self.repo.clear() model = QStandardItemModel(0, len(rep_headers.split(",")), self.resView) self.resModel.setSourceModel(model) set_columns_width(self.resView, proportion=(3, 2, 2, 2, 7, 7, 7, 2, 2, 1)) set_headers(self.resModel, rep_headers) self.query_time = time_run() def append_row(self): crs = conn.cursor() items = ( memb_key[self.proxyModel.type_filter], self.proxyModel.module_filter, self.proxyModel.class_filter, "", "", "", "", self.query_time[0], ) crs.execute(ins0, items) idn = crs.lastrowid conn.commit() param = ( self.proxyModel.rowCount(), (self.proxyModel.type_filter, *items[1:]), idn, ) add_row(self.proxyModel, param) def delete_selected_rows(self): idx_list = self.proxyView.selectionModel().selectedRows() idx_list.reverse() for p_idx in idx_list: if p_idx.isValid(): row = p_idx.row() self.delete_from_db(p_idx) self.proxyModel.removeRows(row, 1) def delete_from_db(self, index: QModelIndex): id_db = self.proxyModel.get_data(index, Qt.UserRole) conn.execute("delete from methods2 where id=?;", (id_db, )) conn.commit() def edit_links(self): index = self.proxyView.currentIndex() ss = self.proxyModel.get_data(index) id_db = self.proxyModel.get_data(index, Qt.UserRole) self.infLabel.setText("{:04d}: {}".format(id_db, ".".join(ss[1:4]))) self.link_box.show() qq = conn.cursor() qq.execute(sql_links.format(id_db, id_db)) self.set_res_model(qq, link_headers, True) self.repo.append((id_db, 'Sel', *ss[:4])) set_columns_width(self.resView, proportion=(3, 2, 8, 8, 8)) set_headers(self.resModel, link_headers) self.old_links = qq.execute(sql_id2.format(id_db, id_db)).fetchall() self.new_links = self.old_links[:] self.curr_id_db = id_db def set_res_model(self, qq: Iterable, headers: str, user_data: bool): self.repo.clear() for row in qq: self.repo.append(row) model = QStandardItemModel(0, len(headers.split(",")), self.resView) fill_in_model(model, self.repo, user_data) self.resModel.setSourceModel(model) def set_link_box(self): self.link_type.addItem("What") self.link_type.addItem("From") f_type = QLabel("Link &type:") f_type.setBuddy(self.link_type) ok_btn = QDialogButtonBox() ok_btn.setStandardButtons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) ok_btn.addButton("+", QDialogButtonBox.ActionRole) ok_btn.addButton("-", QDialogButtonBox.ActionRole) ok_btn.clicked.connect(self.btn_clicked) l_box = QGridLayout() l_box.addWidget(self.infLabel, 0, 0) l_box.addWidget(f_type, 1, 0) l_box.addWidget(self.link_type, 1, 1) l_box.addWidget(ok_btn, 1, 2) l_box.setRowStretch(0, 1) l_box.setRowStretch(1, 0) l_box.setRowStretch(2, 1) grp = QGroupBox() grp.setFlat(True) grp.setLayout(l_box) return grp def btn_clicked(self, btn): { "OK": self.ok_clicked, "Cancel": self.cancel_cliked, "+": self.plus_clicked, "-": self.minus_clicked, }[btn.text()]() def ok_clicked(self): s_new = set(self.new_links) s_old = set(self.old_links) added = s_new - s_old removed = s_old - s_new if removed: for link in removed: conn.execute("delete from one_link where id=? and call_id=?;", link) if added: for link in added: conn.execute( "insert into one_link (id, call_id) values (?, ?);", link) conn.commit() self.resModel.sourceModel().clear() self.link_box.hide() if removed or added: recreate_links() def cancel_cliked(self): self.resModel.sourceModel().clear() self.link_box.hide() def plus_clicked(self): """ add link to resModel """ to_insert = self.collect_links_with_selected() row_no = self.resModel.rowCount() for row in to_insert: add_row(self.resModel, (row_no, row[1:], row[0])) row_no += 1 def collect_links_with_selected(self): """ creation links according to selected rows in proxyView and direction of link selected in self.link_type: self.curr_id_db - DB id of edited method (object) link is a pair of ids (what called, called from) """ stat = self.link_type.currentText() idx_sel = self.proxyView.selectedIndexes() idx_col0 = [ix for ix in idx_sel if ix.column() == 0] to_insert = [] for idx in idx_col0: id = self.proxyModel.get_data(idx, Qt.UserRole) link = (id, self.curr_id_db) if stat == "What" else (self.curr_id_db, id) if link in self.new_links or link[::-1] in self.new_links: continue self.new_links.append(link) row = self.proxyModel.get_data(idx)[:-1] to_insert.append([id, stat] + row) return to_insert def minus_clicked(self): idx_sel = self.resView.selectionModel().selectedRows() idx_sel.reverse() for idx in idx_sel: self.remove_in_new_links(idx) self.remove_in_model(idx) def remove_in_new_links(self, index: QModelIndex): link_type = self.resModel.data(index) id_db = self.resModel.data(index, Qt.UserRole) link = ((id_db, self.curr_id_db) if link_type == "What" else (self.curr_id_db, id_db)) self.new_links.remove(link) def remove_in_model(self, index): row = index.row() self.resModel.removeRows(row, 1) def get_selected_methods(self): """ Returns lists of rows selected in the proxyView: @return: list of selected methods """ indexes = self.proxyView.selectionModel().selectedRows() methods = [] for idx in indexes: methods.append(self.proxyModel.get_data(idx)) return methods def first_level_only(self): """ select method to create link-report depending on number of selected methods @return: None """ self.clear_report_view() self.sort_key = sort_keys["by module"] ids = self.proxyView.selectionModel().selectedRows() opt = len(ids) if len(ids) < 3 else "more than 2" { 1: self.selected_only_one, 2: self.selected_exactly_two, "more than 2": self.selected_more_than_two }[opt](1) def prep_sql(self, sql: str, lvl: int = 0) -> str: mod = self.filterModule.currentText() cls = self.filterClass.currentText() return (sql + ("" if mod == "All" else where_mod.format(mod)) + ("" if cls == "All" else where_cls.format(cls)) + (and_level if lvl else "") + group_by) def selected_only_one(self, lvl): pre = (self.query_time[1], "Sel", "") names = self.get_selected_methods() self.sorted_report(self.repo, (pre, names, "")) lst = self.first_1_part(what_call_1, lvl) pre = (self.query_time[1], "What", "") self.sorted_report(self.repo, (pre, lst, "")) lst = self.first_1_part(called_from_1, lvl) pre = (self.query_time[1], "From", "") self.sorted_report(self.repo, (pre, lst, "")) fill_in_model(self.resModel.sourceModel(), self.repo, user_data=False) def first_1_part(self, sql: str, lvl: int): p_sql = self.prep_sql(sql, lvl) ids = self.get_db_ids() lst = self.exec_sql_b(p_sql, ids) return [(*map(str, x), ) for x in lst] def get_db_ids(self): ids = [] indexes = self.proxyView.selectionModel().selectedRows() for idx in indexes: ids.append(self.proxyModel.get_data(idx, Qt.UserRole)) return ids def selected_exactly_two(self, lvl): pre = (self.query_time[1], "Sel") names = self.get_selected_methods() n_names = [("A", *names[0]), ("B", *names[1])] self.sorted_report(self.repo, (pre, n_names, "")) self.report_four("What", lvl) self.report_four("From", lvl) fill_in_model(self.resModel.sourceModel(), self.repo, user_data=False) def report_four(self, what, lvl): sql = {"What": what_call_1, "From": called_from_1}[what] p_sql = self.prep_sql(sql, lvl) ids = self.get_db_ids() lst_a = self.first_2_part((ids[0], ), sql) lst_b = self.first_2_part((ids[1], ), sql) self.sorted_report(self.repo, ( (self.query_time[1], what, "A | B"), list(set(lst_a) | set(lst_b)), "", )) self.sorted_report(self.repo, ( (self.query_time[1], what, "A - B"), list(set(lst_a) - set(lst_b)), "", )) self.sorted_report(self.repo, ( (self.query_time[1], what, "B - A"), list(set(lst_b) - set(lst_a)), "", )) self.sorted_report(self.repo, ( (self.query_time[1], what, "A & B"), list(set(lst_a) & set(lst_b)), "", )) def first_2_part(self, ids: Iterable, sql: str) -> list: lst = self.exec_sql_b(sql, ids) return [(*map(str, x), ) for x in lst] def selected_more_than_two(self, lvl): pre = (self.query_time[1], "Sel", "") names = self.get_selected_methods() self.sorted_report(self.repo, (pre, names, "")) self.report_23("What", lvl) self.report_23("From", lvl) fill_in_model(self.resModel.sourceModel(), self.repo, user_data=False) def report_23(self, param, lvl): sql = {"What": what_id, "From": from_id}[param] ids = self.get_db_ids() links = self.exec_sql_2(ids, lvl, sql) rep_prep = pre_report(links) self.methods_by_id_list(three_or_more, rep_prep[0:3:2], param, "ALL") self.methods_by_id_list(three_or_more, rep_prep[1:], param, "ANY") def exec_sql_2(self, ids, lvl, sql) -> list: """ @param: ids - list of id of selected rows @param: lvl - level of call: all or only first @param: sql - select methods by type of link: "call What"/"called From" @return: list of tuples (method_id, level of call) """ res = [] curs = self.conn.cursor() loc_sql = sql.format("and level=1" if lvl else "") for id_ in ids: w_id = curs.execute(loc_sql, (id_, )) res.append(dict(w_id)) return res def methods_by_id_list(self, sql: str, ids: list, what: str, all_any: str): if ids: cc = self.exec_sql_f(sql, (",".join((map(str, ids[0]))), )) pre = (self.query_time[1], what, all_any) vv = insert_levels(cc, ids[1]) self.sorted_report(self.repo, (pre, vv, "")) def sort_by_level(self): """ Show lists of methods sorted by level @param ids: indexes of selected methods @param names: selected methods as (module, class, method) list @return: None """ self.clear_report_view() self.sort_key = sort_keys["by level"] self.sel_count_handle() def sort_by_module(self): """ Show lists of methods sorted by module name @param ids: indexes of selected methods @param names: selected methods as (module, class, method) list @return: None """ self.clear_report_view() self.sort_key = sort_keys["by module"] self.sel_count_handle() def sel_count_handle(self): """ This method does the same as the "first_level_only" method @return: None """ ids = self.proxyView.selectionModel().selectedRows() opt = len(ids) if len(ids) < 3 else "more than 2" { 1: self.selected_only_one, 2: self.selected_exactly_two, "more than 2": self.selected_more_than_two }[opt](0) def exec_sql_b(self, sql: str, sql_par: tuple): """ exesute SQL - bind parameters with '?' @param sql: @param sql_par: @return: list of lists of strings """ curs = self.conn.cursor() cc = curs.execute(sql, sql_par) return [(*map(str, x), ) for x in cc] def exec_sql_f(self, sql: str, sql_par: tuple): """ exesute SQL - insert parameters into SQL with str.format method @param sql: @param sql_par: @return: list of lists of strings """ curs = self.conn.cursor() cc = curs.execute(sql.format(*sql_par)) return [(*map(str, x), ) for x in cc] def sorted_report(self, report: list, rep_data: tuple): pre, lst, post = rep_data lst.sort(key=self.sort_key) for ll in lst: report.append((*pre, *ll, *post))