def parse(node: QTreeWidgetItem, data: DataDict): """Parse file to tree format.""" node.takeChildren() file_name = getpath(node) suffix_text = file_suffix(file_name) if node.text(2): code = int(node.text(2)) else: code = data.new_num() node.setText(2, str(code)) if node not in LOADED_FILES and suffix_text in _SUPPORTED_FILE_SUFFIX: LOADED_FILES.append(node) if suffix_text == 'md': # Markdown node.setIcon(0, file_icon("markdown")) parse_markdown(file_name, node, code, data) elif suffix_text == 'py': # Python script node.setIcon(0, file_icon("python")) parse_text(file_name, code, data) elif suffix_text == 'html': # TODO: Need to parse HTML (reveal.js index.html) node.setIcon(0, file_icon("html")) parse_text(file_name, code, data) elif suffix_text == 'kmol': # Kmol project node.setIcon(0, file_icon("kmol")) _parse_tree(node, data) else: # Text files and Python scripts. node.setIcon(0, file_icon("txt")) parse_text(file_name, code, data) print("Loaded: {}".format(node.text(1)))
def _write_tree(proj_name: str, root_node: QTreeWidgetItem, data: DataDict): """Write to YAML file.""" yml_data: YMLData = {} my_codes: List[int] = [] def add_node(node: QTreeWidgetItem) -> NodeDict: code_int = int(node.text(2)) node_dict: NodeDict = { 'code': code_int, 'name': node.text(0), 'path': node.text(1), 'sub': [], } if file_suffix(node.text(1)) not in _SUPPORTED_FILE_SUFFIX: my_codes.append(code_int) if QFileInfo(QDir(node_getpath(node.parent())).filePath(node.text(1))).isFile(): # Files do not need to make a copy. return node_dict for j in range(node.childCount()): node_dict['sub'].append(add_node(node.child(j))) return node_dict root_code = int(root_node.text(2)) yml_data['description'] = root_code yml_data['node'] = [] for i in range(root_node.childCount()): yml_data['node'].append(add_node(root_node.child(i))) yml_data['data'] = {root_code: _LiteralDoc(data[root_code]) or ''} for code in my_codes: yml_data['data'][code] = _LiteralDoc(data[code]) or '' data.save_all() yml_str = ( f"# Generated by Kmol editor {__version__}\n\n" + yaml.dump(yml_data, default_flow_style=False) ) with open(proj_name, 'w', encoding='utf-8') as f: f.write(yml_str) print("Saved: {}".format(proj_name))
def _parse_tree(root_node: QTreeWidgetItem, data: DataDict): """Parse in to tree widget.""" try: with open(root_node.text(1), encoding='utf-8') as f: yaml_script = f.read() except FileNotFoundError: return yml_data: YMLData = yaml.load(yaml_script, Loader=yaml.FullLoader) parse_list: List[QTreeWidgetItem] = [] root_node.setText(2, str(yml_data['description'])) data.update(yml_data['data']) def add_node(node_dict: NodeDict) -> QTreeWidgetItem: """Add node in to tree widget.""" name: str = node_dict['name'] code_int: int = node_dict['code'] path: str = node_dict['path'] node = QTreeItem(name, path, str(code_int)) if name.startswith('@'): node.setIcon(0, file_icon("python")) data.add_macro(name[1:], code_int) suffix_text = file_suffix(path) if suffix_text: parse_list.append(node) elif path: node.setIcon(0, file_icon("directory")) subs: List[NodeDict] = node_dict['sub'] for sub in subs: node.addChild(add_node(sub)) return node child_node_dicts: List[NodeDict] = yml_data['node'] for child_node_dict in child_node_dicts: root_node.addChild(add_node(child_node_dict)) for node_item in parse_list: parse(node_item, data) data.save_all()
def save_file(node: QTreeWidgetItem, data: DataDict) -> Tuple[str, bool]: """Recursive to all the contents of nodes.""" text_data = [] all_saved = data.is_saved(int(node.text(2))) for i in range(node.childCount()): doc, saved = save_file(node.child(i), data) text_data.append(doc) all_saved &= saved my_content = data[int(node.text(2))].splitlines() for i in range(len(my_content)): content_text = my_content[i] if content_text.endswith("@others"): preffix = content_text[:-len("@others")] my_content[i] = '\n\n'.join(preffix + t for t in text_data) my_content = '\n'.join(my_content) path_text = QFileInfo(node.text(1)).fileName() if path_text and not all_saved: suffix_text = QFileInfo(path_text).suffix() if suffix_text == 'kmol': # Save project. _write_tree(node.text(1), node, data) else: # File path. file_path = QDir(QFileInfo(node_getpath(node)).absolutePath()) if not file_path.exists(): file_path.mkpath('.') print("Create Folder: {}".format(file_path.absolutePath())) file_name = file_path.filePath(path_text) if suffix_text in _SUPPORTED_FILE_SUFFIX: # Add end new line. if my_content and (my_content[-1] != '\n'): my_content += '\n' try: with open(file_name, 'w', encoding='utf-8') as f: f.write(my_content) except UnicodeError: print(f"Unicode Error in: {file_name}") else: print(f"Saved: {file_name}") elif suffix_text: print(f"Ignore file: {file_name}") return my_content, all_saved
def __init__(self): super(MainWindow, self).__init__(None) self.setupUi(self) # Start new window. @pyqtSlot() def new_main_window(): XStream.back() run = self.__class__() run.show() self.action_New_Window.triggered.connect(new_main_window) # Text editor self.text_editor = TextEditor(self) self.h_splitter.addWidget(self.text_editor) self.text_editor.word_changed.connect(self.__set_not_saved_title) self.edge_line_option.toggled.connect(self.text_editor.setEdgeMode) self.trailing_blanks_option.toggled.connect(self.text_editor.set_remove_trailing_blanks) # Highlighters self.highlighter_option.addItems(sorted(QSCIHIGHLIGHTERS)) self.highlighter_option.setCurrentText("Markdown") self.highlighter_option.currentTextChanged.connect( self.text_editor.set_highlighter ) # Tree widget context menu. self.tree_widget.customContextMenuRequested.connect( self.on_tree_widget_context_menu ) self.popMenu_tree = QMenu(self) self.popMenu_tree.setSeparatorsCollapsible(True) self.popMenu_tree.addAction(self.action_new_project) self.popMenu_tree.addAction(self.action_open) self.tree_add = QAction("&Add Node", self) self.tree_add.triggered.connect(self.add_node) self.tree_add.setShortcut("Ctrl+I") self.tree_add.setShortcutContext(Qt.WindowShortcut) self.popMenu_tree.addAction(self.tree_add) self.popMenu_tree.addSeparator() self.tree_path = QAction("Set Path", self) self.tree_path.triggered.connect(self.set_path) self.popMenu_tree.addAction(self.tree_path) self.tree_refresh = QAction("&Refresh from Path", self) self.tree_refresh.triggered.connect(self.refresh_proj) self.popMenu_tree.addAction(self.tree_refresh) self.tree_openurl = QAction("&Open from Path", self) self.tree_openurl.triggered.connect(self.open_path) self.popMenu_tree.addAction(self.tree_openurl) self.action_save.triggered.connect(self.save_proj) self.popMenu_tree.addAction(self.action_save) self.tree_copy = QAction("Co&py", self) self.tree_copy.triggered.connect(self.copy_node) self.popMenu_tree.addAction(self.tree_copy) self.tree_clone = QAction("C&lone", self) self.tree_clone.triggered.connect(self.clone_node) self.popMenu_tree.addAction(self.tree_clone) self.tree_copy_tree = QAction("Recursive Copy", self) self.tree_copy_tree.triggered.connect(self.copy_node_recursive) self.popMenu_tree.addAction(self.tree_copy_tree) self.tree_clone_tree = QAction("Recursive Clone", self) self.tree_clone_tree.triggered.connect(self.clone_node_recursive) self.popMenu_tree.addAction(self.tree_clone_tree) self.popMenu_tree.addSeparator() self.tree_delete = QAction("&Delete", self) self.tree_delete.triggered.connect(self.delete_node) self.popMenu_tree.addAction(self.tree_delete) self.tree_close = QAction("&Close", self) self.tree_close.triggered.connect(self.close_file) self.popMenu_tree.addAction(self.tree_close) self.tree_main.header().setSectionResizeMode(QHeaderView.ResizeToContents) # Console self.console.setFont(self.text_editor.font) if not ARGUMENTS.debug_mode: XStream.stdout().messageWritten.connect(self.__append_to_console) XStream.stderr().messageWritten.connect(self.__append_to_console) for info in INFO: print(info) print('-' * 7) # Searching function. find_next = QShortcut(QKeySequence("F3"), self) find_next.activated.connect(self.find_next_button.click) find_previous = QShortcut(QKeySequence("F4"), self) find_previous.activated.connect(self.find_previous_button.click) find_tab = QShortcut(QKeySequence("Ctrl+F"), self) find_tab.activated.connect(lambda: self.panel_widget.setCurrentIndex(1)) find_project = QShortcut(QKeySequence("Ctrl+Shift+F"), self) find_project.activated.connect(self.find_project_button.click) # Replacing function. replace = QShortcut(QKeySequence("Ctrl+R"), self) replace.activated.connect(self.replace_node_button.click) replace_project = QShortcut(QKeySequence("Ctrl+Shift+R"), self) replace_project.activated.connect(self.replace_project_button.click) # Translator. self.panel_widget.addTab(TranslatorWidget(self), "Translator") # Node edit function. (Ctrl + ArrowKey) move_up_node = QShortcut(QKeySequence("Ctrl+Up"), self) move_up_node.activated.connect(self.__move_up_node) move_down_node = QShortcut(QKeySequence("Ctrl+Down"), self) move_down_node.activated.connect(self.__move_down_node) move_right_node = QShortcut(QKeySequence("Ctrl+Right"), self) move_right_node.activated.connect(self.__move_right_node) move_left_node = QShortcut(QKeySequence("Ctrl+Left"), self) move_left_node.activated.connect(self.__move_left_node) # Run script button. run_sript = QShortcut(QKeySequence("F5"), self) run_sript.activated.connect(self.exec_button.click) self.macros_toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) # Splitter self.h_splitter.setStretchFactor(0, 10) self.h_splitter.setStretchFactor(1, 60) self.v_splitter.setStretchFactor(0, 30) self.v_splitter.setStretchFactor(1, 10) # Data self.data = DataDict() self.data.not_saved.connect(self.__set_not_saved_title) self.data.all_saved.connect(self.__set_saved_title) self.env = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation) for filename in ARGUMENTS.r: filename = QFileInfo(filename).canonicalFilePath() if not filename: return root_node = QTreeRoot(QFileInfo(filename).baseName(), filename, '') self.tree_main.addTopLevelItem(root_node) parse(root_node, self.data) self.__add_macros()
class MainWindow(QMainWindow, Ui_MainWindow): """Main window of kmol editor.""" def __init__(self): super(MainWindow, self).__init__(None) self.setupUi(self) # Start new window. @pyqtSlot() def new_main_window(): XStream.back() run = self.__class__() run.show() self.action_New_Window.triggered.connect(new_main_window) # Text editor self.text_editor = TextEditor(self) self.h_splitter.addWidget(self.text_editor) self.text_editor.word_changed.connect(self.__set_not_saved_title) self.edge_line_option.toggled.connect(self.text_editor.setEdgeMode) self.trailing_blanks_option.toggled.connect(self.text_editor.set_remove_trailing_blanks) # Highlighters self.highlighter_option.addItems(sorted(QSCIHIGHLIGHTERS)) self.highlighter_option.setCurrentText("Markdown") self.highlighter_option.currentTextChanged.connect( self.text_editor.set_highlighter ) # Tree widget context menu. self.tree_widget.customContextMenuRequested.connect( self.on_tree_widget_context_menu ) self.popMenu_tree = QMenu(self) self.popMenu_tree.setSeparatorsCollapsible(True) self.popMenu_tree.addAction(self.action_new_project) self.popMenu_tree.addAction(self.action_open) self.tree_add = QAction("&Add Node", self) self.tree_add.triggered.connect(self.add_node) self.tree_add.setShortcut("Ctrl+I") self.tree_add.setShortcutContext(Qt.WindowShortcut) self.popMenu_tree.addAction(self.tree_add) self.popMenu_tree.addSeparator() self.tree_path = QAction("Set Path", self) self.tree_path.triggered.connect(self.set_path) self.popMenu_tree.addAction(self.tree_path) self.tree_refresh = QAction("&Refresh from Path", self) self.tree_refresh.triggered.connect(self.refresh_proj) self.popMenu_tree.addAction(self.tree_refresh) self.tree_openurl = QAction("&Open from Path", self) self.tree_openurl.triggered.connect(self.open_path) self.popMenu_tree.addAction(self.tree_openurl) self.action_save.triggered.connect(self.save_proj) self.popMenu_tree.addAction(self.action_save) self.tree_copy = QAction("Co&py", self) self.tree_copy.triggered.connect(self.copy_node) self.popMenu_tree.addAction(self.tree_copy) self.tree_clone = QAction("C&lone", self) self.tree_clone.triggered.connect(self.clone_node) self.popMenu_tree.addAction(self.tree_clone) self.tree_copy_tree = QAction("Recursive Copy", self) self.tree_copy_tree.triggered.connect(self.copy_node_recursive) self.popMenu_tree.addAction(self.tree_copy_tree) self.tree_clone_tree = QAction("Recursive Clone", self) self.tree_clone_tree.triggered.connect(self.clone_node_recursive) self.popMenu_tree.addAction(self.tree_clone_tree) self.popMenu_tree.addSeparator() self.tree_delete = QAction("&Delete", self) self.tree_delete.triggered.connect(self.delete_node) self.popMenu_tree.addAction(self.tree_delete) self.tree_close = QAction("&Close", self) self.tree_close.triggered.connect(self.close_file) self.popMenu_tree.addAction(self.tree_close) self.tree_main.header().setSectionResizeMode(QHeaderView.ResizeToContents) # Console self.console.setFont(self.text_editor.font) if not ARGUMENTS.debug_mode: XStream.stdout().messageWritten.connect(self.__append_to_console) XStream.stderr().messageWritten.connect(self.__append_to_console) for info in INFO: print(info) print('-' * 7) # Searching function. find_next = QShortcut(QKeySequence("F3"), self) find_next.activated.connect(self.find_next_button.click) find_previous = QShortcut(QKeySequence("F4"), self) find_previous.activated.connect(self.find_previous_button.click) find_tab = QShortcut(QKeySequence("Ctrl+F"), self) find_tab.activated.connect(lambda: self.panel_widget.setCurrentIndex(1)) find_project = QShortcut(QKeySequence("Ctrl+Shift+F"), self) find_project.activated.connect(self.find_project_button.click) # Replacing function. replace = QShortcut(QKeySequence("Ctrl+R"), self) replace.activated.connect(self.replace_node_button.click) replace_project = QShortcut(QKeySequence("Ctrl+Shift+R"), self) replace_project.activated.connect(self.replace_project_button.click) # Translator. self.panel_widget.addTab(TranslatorWidget(self), "Translator") # Node edit function. (Ctrl + ArrowKey) move_up_node = QShortcut(QKeySequence("Ctrl+Up"), self) move_up_node.activated.connect(self.__move_up_node) move_down_node = QShortcut(QKeySequence("Ctrl+Down"), self) move_down_node.activated.connect(self.__move_down_node) move_right_node = QShortcut(QKeySequence("Ctrl+Right"), self) move_right_node.activated.connect(self.__move_right_node) move_left_node = QShortcut(QKeySequence("Ctrl+Left"), self) move_left_node.activated.connect(self.__move_left_node) # Run script button. run_sript = QShortcut(QKeySequence("F5"), self) run_sript.activated.connect(self.exec_button.click) self.macros_toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) # Splitter self.h_splitter.setStretchFactor(0, 10) self.h_splitter.setStretchFactor(1, 60) self.v_splitter.setStretchFactor(0, 30) self.v_splitter.setStretchFactor(1, 10) # Data self.data = DataDict() self.data.not_saved.connect(self.__set_not_saved_title) self.data.all_saved.connect(self.__set_saved_title) self.env = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation) for filename in ARGUMENTS.r: filename = QFileInfo(filename).canonicalFilePath() if not filename: return root_node = QTreeRoot(QFileInfo(filename).baseName(), filename, '') self.tree_main.addTopLevelItem(root_node) parse(root_node, self.data) self.__add_macros() def dragEnterEvent(self, event): """Drag file in to our window.""" if event.mimeData().hasUrls(): event.acceptProposedAction() def dropEvent(self, event): """Drop file in to our window.""" for url in event.mimeData().urls(): filename = url.toLocalFile() root_node = QTreeRoot(QFileInfo(filename).baseName(), filename, '') self.tree_main.addTopLevelItem(root_node) parse(root_node, self.data) self.tree_main.setCurrentItem(root_node) self.__add_macros() event.acceptProposedAction() @pyqtSlot() def __set_not_saved_title(self): """Show star sign on window title.""" if '*' not in self.windowTitle(): self.setWindowTitle(self.windowTitle() + '*') @pyqtSlot() def __set_saved_title(self): """Remove star sign on window title.""" self.setWindowTitle(self.windowTitle().replace('*', '')) @pyqtSlot(str) def __append_to_console(self, log): """After inserted the text, move cursor to end.""" self.console.moveCursor(QTextCursor.End) self.console.insertPlainText(log) self.console.moveCursor(QTextCursor.End) @pyqtSlot(QPoint, name='on_tree_widget_context_menu') def __tree_context_menu(self, point: QPoint): """Operations.""" self.__action_changed() self.popMenu_tree.exec_(self.tree_widget.mapToGlobal(point)) @pyqtSlot(name='on_action_new_project_triggered') def new_proj(self): """New file.""" filename, _ = QFileDialog.getSaveFileName( self, "New Project", self.env, SUPPORT_FILE_FORMATS ) if not filename: return self.env = QFileInfo(filename).absolutePath() root_node = QTreeRoot( QFileInfo(filename).baseName(), filename, str(self.data.new_num()) ) suffix_text = file_suffix(filename) if suffix_text == 'md': root_node.setIcon(0, file_icon("markdown")) elif suffix_text == 'html': root_node.setIcon(0, file_icon("html")) elif suffix_text == 'kmol': root_node.setIcon(0, file_icon("kmol")) else: root_node.setIcon(0, file_icon("txt")) self.tree_main.addTopLevelItem(root_node) @pyqtSlot(name='on_action_open_triggered') def open_proj(self): """Open file.""" file_names, ok = QFileDialog.getOpenFileNames( self, "Open Projects", self.env, SUPPORT_FILE_FORMATS ) if not ok: return def in_widget(path: str) -> int: """Is name in tree widget.""" for i in range(self.tree_main.topLevelItemCount()): if path == self.tree_main.topLevelItem(i).text(1): return i return -1 for file_name in file_names: self.env = QFileInfo(file_name).absolutePath() index = in_widget(file_name) if index == -1: root_node = QTreeRoot(QFileInfo(file_name).baseName(), file_name, '') self.tree_main.addTopLevelItem(root_node) parse(root_node, self.data) self.tree_main.setCurrentItem(root_node) else: self.tree_main.setCurrentItem(self.tree_main.topLevelItem(index)) self.__add_macros() @pyqtSlot() def refresh_proj(self): """Re-parse the file node.""" node = self.tree_main.currentItem() if not node.text(1): QMessageBox.warning( self, "No path", "Can only refresh from valid path." ) parse(node, self.data) self.tree_main.setCurrentItem(node) self.text_editor.setText(self.data[int(node.text(2))]) @pyqtSlot() def open_path(self): """Open path of current node.""" node = self.tree_main.currentItem() filename = getpath(node) QDesktopServices.openUrl(QUrl(filename)) print("Open: {}".format(filename)) @pyqtSlot() def add_node(self): """Add a node at current item.""" node = self.tree_main.currentItem() new_node = QTreeItem( "New node", "", str(self.data.new_num()) ) if node.childCount() or node.text(1): node.addChild(new_node) return parent = node.parent() if parent: parent.insertChild(parent.indexOfChild(node) + 1, new_node) return self.tree_main.indexOfTopLevelItem( self.tree_main.indexOfTopLevelItem(node) + 1, new_node ) @pyqtSlot() def set_path(self): """Set file directory.""" node = self.tree_main.currentItem() filename, ok = QFileDialog.getOpenFileName( self, "Open File", self.env, SUPPORT_FILE_FORMATS ) if not ok: return self.env = QFileInfo(filename).absolutePath() project_path = QDir(_get_root(node).text(1)) project_path.cdUp() node.setText(1, project_path.relativeFilePath(filename)) @pyqtSlot() def copy_node(self): """Copy current node.""" node_origin = self.tree_main.currentItem() parent = node_origin.parent() node = node_origin.clone() node.takeChildren() code = self.data.new_num() self.data[code] = self.data[int(node.text(2))] node.setText(2, str(code)) parent.insertChild(parent.indexOfChild(node_origin) + 1, node) @pyqtSlot() def clone_node(self): """Copy current node with same pointer.""" node_origin = self.tree_main.currentItem() parent = node_origin.parent() node = node_origin.clone() node.takeChildren() parent.insertChild(parent.indexOfChild(node_origin) + 1, node) @pyqtSlot() def copy_node_recursive(self): """Copy current node and its sub-nodes.""" node_origin = self.tree_main.currentItem() parent = node_origin.parent() node_origin_copy = node_origin.clone() def new_pointer(node: QTreeWidgetItem): """Give a new pointer code for node.""" code = self.data.new_num() self.data[code] = self.data[int(node.text(2))] node.setText(2, str(code)) for i in range(node.childCount()): new_pointer(node.child(i)) new_pointer(node_origin_copy) parent.insertChild(parent.indexOfChild(node_origin) + 1, node_origin_copy) @pyqtSlot() def clone_node_recursive(self): """Copy current node and its sub-nodes with same pointer.""" node_origin = self.tree_main.currentItem() parent = node_origin.parent() parent.insertChild(parent.indexOfChild(node_origin) + 1, node_origin.clone()) @pyqtSlot() def save_proj(self, index: Optional[int] = None, *, for_all: bool = False): """Save project and files.""" if for_all: for row in range(self.tree_main.topLevelItemCount()): self.save_proj(row) return node = self.tree_main.currentItem() if not node: return if index is None: root = _get_root(node) else: root = self.tree_main.topLevelItem(index) self.__save_current() save_file(root, self.data) self.data.save_all() def __save_current(self): """Save the current text of editor.""" self.text_editor.remove_trailing_blanks() item = self.tree_main.currentItem() if item: self.data[int(item.text(2))] = self.text_editor.text() @pyqtSlot() def delete_node(self): """Delete the current item.""" node = self.tree_main.currentItem() parent = node.parent() self.tree_main.setCurrentItem(parent) self.__delete_node_data(node) parent.removeChild(node) def __delete_node_data(self, node: QTreeWidgetItem): """Delete data from data structure.""" name = node.text(0) if name.startswith('@'): for action in self.macros_toolbar.actions(): if action.text() == name[1:]: self.macros_toolbar.removeAction(action) del self.data[int(node.text(2))] for i in range(node.childCount()): self.__delete_node_data(node.child(i)) @pyqtSlot() def close_file(self): """Close project node.""" if not self.data.is_all_saved(): reply = QMessageBox.question( self, "Not saved", "Do you went to save the project?", QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save ) if reply == QMessageBox.Save: self.save_proj() elif reply == QMessageBox.Cancel: return root = self.tree_main.currentItem() self.__delete_node_data(root) self.tree_main.takeTopLevelItem(self.tree_main.indexOfTopLevelItem(root)) self.text_editor.clear() @pyqtSlot() def __move_up_node(self): """Move up current node.""" node = self.tree_main.currentItem() if not node: return tree_main = node.treeWidget() parent = node.parent() if parent: # Is sub-node. index = parent.indexOfChild(node) if index == 0: return parent.removeChild(node) parent.insertChild(index - 1, node) else: # Is root. index = tree_main.indexOfTopLevelItem(node) if index == 0: return tree_main.takeTopLevelItem(index) tree_main.insertTopLevelItem(index - 1, node) tree_main.setCurrentItem(node) self.__root_unsaved() @pyqtSlot() def __move_down_node(self): """Move down current node.""" node = self.tree_main.currentItem() if not node: return tree_main = node.treeWidget() parent = node.parent() if parent: # Is sub-node. index = parent.indexOfChild(node) if index == parent.childCount() - 1: return parent.removeChild(node) parent.insertChild(index + 1, node) else: # Is root. index = tree_main.indexOfTopLevelItem(node) if index == tree_main.topLevelItemCount() - 1: return tree_main.takeTopLevelItem(index) tree_main.insertTopLevelItem(index + 1, node) tree_main.setCurrentItem(node) self.__root_unsaved() @pyqtSlot() def __move_right_node(self): """Move right current node.""" node = self.tree_main.currentItem() if not node: return tree_main = node.treeWidget() parent = node.parent() if parent: # Is sub-node. index = parent.indexOfChild(node) if index == 0: return parent.removeChild(node) parent.child(index - 1).addChild(node) else: # Is root. index = tree_main.indexOfTopLevelItem(node) if index == 0: return tree_main.takeTopLevelItem(index) tree_main.topLevelItem(index - 1).addChild(node) tree_main.setCurrentItem(node) self.__root_unsaved() @pyqtSlot() def __move_left_node(self): """Move left current node.""" node = self.tree_main.currentItem() if not node: return tree_main = node.treeWidget() parent = node.parent() if not parent: return # Must be a sub-node. grand_parent = parent.parent() if not grand_parent: return index = grand_parent.indexOfChild(parent) parent.removeChild(node) grand_parent.insertChild(index + 1, node) tree_main.setCurrentItem(node) self.__root_unsaved() @pyqtSlot(name='on_action_about_qt_triggered') def __about_qt(self): """Qt about.""" QMessageBox.aboutQt(self) @pyqtSlot(name='on_action_about_triggered') def __about(self): """Kmol editor about.""" QMessageBox.about(self, "About Kmol Editor", '\n'.join(INFO + ( '', "Author: " + __author__, "Email: " + __email__, __copyright__, "License: " + __license__, ))) @pyqtSlot(name='on_action_mde_tw_triggered') def __mde_tw(self): """Mde website.""" QDesktopServices.openUrl(QUrl("http://mde.tw")) @pyqtSlot(name='on_exec_button_clicked') def __exec(self): """Run the script from current text editor.""" self.__exec_script(self.text_editor.text()) def __exec_script(self, code: Union[int, str]): """Run a script in a new thread.""" self.__save_current() variables = { # Qt file operation classes. 'QStandardPaths': QStandardPaths, 'QFileInfo': QFileInfo, 'QDir': QDir, } node = self.tree_main.currentItem() variables['node'] = node if node: root = _get_root(node) variables['root'] = root variables['root_path'] = QFileInfo(root.text(1)).absoluteFilePath() variables['node_path'] = getpath(node) def chdir_tree(path: str): if QFileInfo(path).isDir(): chdir(path) elif QFileInfo(path).isFile(): chdir(QFileInfo(path).absolutePath()) variables['chdir'] = chdir_tree thread = Thread( target=exec, args=(self.data[code] if type(code) == int else code, variables) ) thread.start() @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem, name='on_tree_main_currentItemChanged') def __switch_data( self, current: QTreeWidgetItem, previous: QTreeWidgetItem ): """Switch node function. + Auto collapse and expand function. + Important: Store the string data. """ if self.auto_expand_option.isChecked(): self.tree_main.expandItem(current) self.tree_main.scrollToItem(current) if previous: self.data[int(previous.text(2))] = self.text_editor.text() if current: # Auto highlight. path = current.text(1) filename = QFileInfo(path).fileName() suffix = QFileInfo(filename).suffix() if current.text(0).startswith('@'): self.highlighter_option.setCurrentText("Python") else: self.highlighter_option.setCurrentText("Markdown") if path: for name_m, suffix_m in HIGHLIGHTER_SUFFIX.items(): if suffix in suffix_m: self.highlighter_option.setCurrentText(name_m) break else: for name_m, filename_m in HIGHLIGHTER_FILENAME.items(): if filename in filename_m: self.highlighter_option.setCurrentText(name_m) break self.text_editor.setText(self.data[int(current.text(2))]) self.__action_changed() @pyqtSlot(QTreeWidgetItem, int, name='on_tree_main_itemChanged') def __reload_nodes(self, node: QTreeWidgetItem, _: int): """Mark edited node as unsaved.""" name = node.text(0) code = int(node.text(2)) if name.startswith('@'): self.__add_macro(name[1:], code) self.__root_unsaved() def __root_unsaved(self): """Let tree to re-save.""" node = self.tree_main.currentItem() if node: self.data.set_saved(int(_get_root(node).text(2)), False) def __action_changed(self): node = self.tree_main.currentItem() has_item = bool(node) is_root = (not node.parent()) if has_item else False for action in ( self.action_open, self.action_new_project, ): action.setVisible(is_root or not has_item) self.tree_close.setVisible(has_item and is_root) for action in ( self.tree_add, self.tree_refresh, self.tree_openurl, self.action_save, ): action.setVisible(has_item) for action in ( self.tree_copy, self.tree_clone, self.tree_copy_tree, self.tree_clone_tree, self.tree_path, self.tree_delete, ): action.setVisible(has_item and not is_root) def __add_macros(self): """Add macro buttons from data structure.""" for name, code in self.data.macros(): self.__add_macro(name, code) def __add_macro(self, name: str, code: Union[int, Hashable]): """Add macro button.""" for action in self.macros_toolbar.actions(): if action.text() == name: break else: action = self.macros_toolbar.addAction(QIcon(QPixmap(":icons/python.png")), name) action.triggered.connect(lambda: self.__exec_script(code)) def __find_text(self, forward: bool): """Find text by options.""" if not self.search_bar.text(): self.search_bar.setText(self.search_bar.placeholderText()) pos = self.text_editor.positionFromLineIndex( *self.text_editor.getCursorPosition() ) if not self.text_editor.findFirst( self.search_bar.text(), self.re_option.isChecked(), self.match_case_option.isChecked(), self.whole_word_option.isChecked(), self.wrap_around.isChecked(), forward, *self.text_editor.lineIndexFromPosition(pos if forward else pos - 1) ): QMessageBox.information( self, "Text not found.", "\"{}\" is not in current document".format( self.search_bar.text() ) ) @pyqtSlot(name='on_find_next_button_clicked') def __find_next(self): """Find to next.""" self.__find_text(True) @pyqtSlot(name='on_find_previous_button_clicked') def __find_previous(self): """Find to previous.""" self.__find_text(False) @pyqtSlot(name='on_replace_node_button_clicked') def __replace(self): """Replace current text by replace bar.""" self.text_editor.replace(self.replace_bar.text()) self.text_editor.findNext() @pyqtSlot(name='on_find_project_button_clicked') def __find_project(self): """Find in all project.""" self.find_list.clear() node_current = self.tree_main.currentItem() if not node_current: return root = _get_root(node_current) if not self.search_bar.text(): self.search_bar.setText(self.search_bar.placeholderText()) text = self.search_bar.text() flags = re.MULTILINE if not self.re_option.isChecked(): text = re.escape(text) if self.whole_word_option.isChecked(): text = r'\b' + text + r'\b' if not self.match_case_option.isChecked(): flags |= re.IGNORECASE def add_find_result(code: int, last_name: str, start: int, end: int): """Add result to list.""" item = QListWidgetItem("{}: [{}, {}]".format(code, start, end)) item.setToolTip(last_name) self.find_list.addItem(item) def find_in_nodes(node: QTreeWidgetItem, last_name: str = ''): """Find the word in all nodes.""" last_name += node.text(0) if node.childCount(): last_name += '->' code = int(node.text(2)) doc = self.data[code] pattern = re.compile(text, flags) for m in pattern.finditer(doc): add_find_result(code, last_name, *m.span()) for i in range(node.childCount()): find_in_nodes(node.child(i), last_name) find_in_nodes(root) @pyqtSlot( QListWidgetItem, QListWidgetItem, name='on_find_list_currentItemChanged') def __find_results(self, *_: QListWidgetItem): """TODO: Switch to target node."""
def parse_markdown(file_name: str, node: QTreeWidgetItem, code: int, data: DataDict): """Parse Markdown file to tree nodes.""" try: f = open(file_name, encoding='utf-8') except FileNotFoundError as e: if not data[code]: data[code] = str(e) return with f: string_list = f.read().split('\n') # Read the first level of title mark. # titles = [(line_num, level), ...] titles = [] previous_line = "" in_code_block = False for line_num, line in enumerate(string_list): if line.startswith('```'): in_code_block = not in_code_block if in_code_block: previous_line = line continue for level, string in enumerate(['===', '---']): if not line.startswith(string) or previous_line.startswith(" "): continue if len(set(line)) == 1 and previous_line: # Under line with its title. titles.append((line_num - 1, level)) if len(set(line)) > 1: prefix = line.split(maxsplit=1)[0] if set(prefix) == {'#'}: titles.append((line_num, len(prefix) - 1)) previous_line = line # Joint nodes. if not titles: # Plain text. data[code] = '\n'.join(string_list) return if titles[0][0] == 0: # Start with line 0. data[code] = "@others\n" else: data[code] = '\n'.join(string_list[:titles[0][0]]) tree_items = [] def parent(t_index: int, t_level: int) -> QTreeWidgetItem: """The parent of current title.""" for i, (pre_line, pre_level) in reversed(tuple(enumerate(titles[:t_index]))): if pre_level < t_level: return tree_items[i] return node titles_count = len(titles) - 1 for index, (line_num, level) in enumerate(titles): code = data.new_num() if index == titles_count: doc = string_list[line_num:] else: doc = string_list[line_num:titles[index + 1][0]] if titles[index + 1][1] > level: # Has child. doc.append('@others') doc.append('') data[code] = '\n'.join(doc) title = doc[0] if title.startswith("#"): title = title.split(maxsplit=1)[1] item = QTreeItem(title, '', str(code)) parent(index, level).addChild(item) tree_items.append(item)
def __init__(self): super(MainWindowBase, self).__init__() self.setupUi(self) # Start new window @Slot() def new_main_window(): XStream.back() run = self.__class__() run.show() self.action_New_Window.triggered.connect(new_main_window) # Settings self.settings = QSettings("Kmol", "Kmol Editor") # Text editor self.text_editor = TextEditor(self) self.h2_splitter.addWidget(self.text_editor) self.html_previewer = QWebEngineView() self.html_previewer.setContextMenuPolicy(Qt.NoContextMenu) self.html_previewer.setContent(b"", "text/plain") self.h2_splitter.addWidget(self.html_previewer) self.text_editor.word_changed.connect(self.reload_html_viewer) self.text_editor.word_changed.connect(self.set_not_saved_title) self.edge_line_option.toggled.connect(self.text_editor.setEdgeMode) self.trailing_blanks_option.toggled.connect( self.text_editor.set_remove_trailing_blanks) # Highlighters self.highlighter_option.addItems(sorted(QSCI_HIGHLIGHTERS)) self.highlighter_option.setCurrentText("Markdown") self.highlighter_option.currentTextChanged.connect( self.text_editor.set_highlighter) self.highlighter_option.currentTextChanged.connect( self.reload_html_viewer) # Tree widget context menu self.tree_widget.customContextMenuRequested.connect( self.tree_context_menu) self.pop_menu_tree = QMenu(self) self.pop_menu_tree.setSeparatorsCollapsible(True) self.pop_menu_tree.addAction(self.action_new_project) self.pop_menu_tree.addAction(self.action_open) self.tree_add = QAction("&Add Node", self) self.tree_add.triggered.connect(self.add_node) self.tree_add.setShortcutContext(Qt.WindowShortcut) self.pop_menu_tree.addAction(self.tree_add) self.pop_menu_tree.addSeparator() self.tree_path = QAction("Set Path", self) self.tree_path.triggered.connect(self.set_path) self.pop_menu_tree.addAction(self.tree_path) self.tree_refresh = QAction("&Refresh from Path", self) self.tree_refresh.triggered.connect(self.refresh_proj) self.pop_menu_tree.addAction(self.tree_refresh) self.tree_openurl = QAction("&Open from Path", self) self.tree_openurl.triggered.connect(self.open_path) self.pop_menu_tree.addAction(self.tree_openurl) self.action_save.triggered.connect(self.save_proj) self.pop_menu_tree.addAction(self.action_save) self.tree_copy = QAction("Co&py", self) self.tree_copy.triggered.connect(self.copy_node) self.pop_menu_tree.addAction(self.tree_copy) self.tree_clone = QAction("C&lone", self) self.tree_clone.triggered.connect(self.clone_node) self.pop_menu_tree.addAction(self.tree_clone) self.tree_copy_tree = QAction("Recursive Copy", self) self.tree_copy_tree.triggered.connect(self.copy_node_recursive) self.pop_menu_tree.addAction(self.tree_copy_tree) self.tree_clone_tree = QAction("Recursive Clone", self) self.tree_clone_tree.triggered.connect(self.clone_node_recursive) self.pop_menu_tree.addAction(self.tree_clone_tree) self.pop_menu_tree.addSeparator() self.tree_delete = QAction("&Delete", self) self.tree_delete.triggered.connect(self.delete_node) self.pop_menu_tree.addAction(self.tree_delete) self.tree_close = QAction("&Close", self) self.tree_close.triggered.connect(self.close_proj) self.pop_menu_tree.addAction(self.tree_close) self.tree_main.header().setSectionResizeMode( QHeaderView.ResizeToContents) # Console self.console.setFont(self.text_editor.font) if not ARGUMENTS.debug_mode: XStream.stdout().message_written.connect(self.append_to_console) XStream.stderr().message_written.connect(self.append_to_console) for info in INFO: print(info) print('-' * 7) # Searching function find_next = QShortcut(QKeySequence("F3"), self) find_next.activated.connect(self.find_next_button.click) find_previous = QShortcut(QKeySequence("F4"), self) find_previous.activated.connect(self.find_previous_button.click) find_tab = QShortcut(QKeySequence("Ctrl+F"), self) find_tab.activated.connect(self.start_finder) find_project = QShortcut(QKeySequence("Ctrl+Shift+F"), self) find_project.activated.connect(self.find_project_button.click) self.find_list_node: Dict[int, QTreeWidgetItem] = {} # Replacing function replace = QShortcut(QKeySequence("Ctrl+R"), self) replace.activated.connect(self.replace_node_button.click) replace_project = QShortcut(QKeySequence("Ctrl+Shift+R"), self) replace_project.activated.connect(self.replace_project_button.click) # Node edit function (Ctrl + ArrowKey) new_node = QShortcut(QKeySequence("Ctrl+Ins"), self) new_node.activated.connect(self.add_node) del_node = QShortcut(QKeySequence("Ctrl+Del"), self) del_node.activated.connect(self.delete_node) move_up_node = QShortcut(QKeySequence("Ctrl+Up"), self) move_up_node.activated.connect(self.move_up_node) move_down_node = QShortcut(QKeySequence("Ctrl+Down"), self) move_down_node.activated.connect(self.move_down_node) move_right_node = QShortcut(QKeySequence("Ctrl+Right"), self) move_right_node.activated.connect(self.move_right_node) move_left_node = QShortcut(QKeySequence("Ctrl+Left"), self) move_left_node.activated.connect(self.move_left_node) # Run script button run_sript = QShortcut(QKeySequence("F5"), self) run_sript.activated.connect(self.exec_button.click) self.macros_toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) # File keeper self.keeper = None # Data self.data = DataDict() self.data.not_saved.connect(self.set_not_saved_title) self.data.all_saved.connect(self.set_saved_title) self.env = QStandardPaths.writableLocation( QStandardPaths.DesktopLocation)