diff --git a/main.py b/main.py index 9e6d119..99e35b8 100644 --- a/main.py +++ b/main.py @@ -12,21 +12,22 @@ Author: Isaak Buslovich Date: 2024-01-13 Version: 0.1 """ +import logging import pathlib import platform import sys import threading import webbrowser import yaml -from PyQt6.QtCore import QFileSystemWatcher, Qt, QTimer -from PyQt6.QtGui import QAction, QStandardItem, QStandardItemModel, QIcon, QPalette -from PyQt6.QtWebEngineWidgets import QWebEngineView +from PyQt6.QtCore import QFileSystemWatcher, Qt, QTimer, QSize, QRegularExpression +from PyQt6.QtGui import QAction, QStandardItem, QStandardItemModel, QIcon, QPalette, QSyntaxHighlighter, \ + QTextCharFormat, QColor, QFont from PyQt6.QtWebEngineCore import QWebEngineSettings +from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWidgets import QApplication, QMainWindow, QTextEdit, QHBoxLayout, QWidget, QToolBar, QStatusBar, \ - QMessageBox, QTreeView, QFileDialog, QAbstractItemView, QLineEdit, QVBoxLayout + QMessageBox, QTreeView, QFileDialog, QAbstractItemView, QLineEdit, QVBoxLayout, QStyle, QTabWidget, QLabel +from fuzzywuzzy import fuzz from markdown2 import markdown -from fuzzywuzzy import process, fuzz -import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') @@ -46,9 +47,15 @@ class MarkdownEditor(QMainWindow): """ def __init__(self): + super().__init__() + self.config_path = pathlib.Path(__file__).parent / 'config.yaml' + self.config = self.load_config() + self.setup_ui() + QTimer.singleShot(0, self.post_init) + + def post_init(self): """ Initializes the Markdown editor application with UI setup and configuration management. """ logging.info("Initializing the Markdown Editor...") - super().__init__() self.dataDirectory = None self.preview = None self.editor = None @@ -71,15 +78,44 @@ class MarkdownEditor(QMainWindow): self.searchTimer.setSingleShot(True) self.searchTimer.timeout.connect(self.perform_search) + def setup_ui(self): + """ Sets up the minimal UI components necessary for initial display. """ + self.setWindowTitle("Markdown Editor") + + # Basic window geometry + width = self.config.get('window', {}).get('width', 800) + height = self.config.get('window', {}).get('height', 600) + self.setGeometry(100, 100, width, height) + + # Central widget and layout + central_widget = QWidget() + layout = QVBoxLayout(central_widget) + layout.setContentsMargins(5, 5, 5, 5) + + # Placeholder for main content + placeholder_label = QLabel("Loading...", alignment=Qt.AlignmentFlag.AlignCenter) + layout.addWidget(placeholder_label) + + # Set the central widget + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + + # Status bar (optional) + self.setStatusBar(QStatusBar(self)) + def initialize_ui(self): - """ Sets up the application's user interface components. """ - logging.info("Setting up the user interface...") + """ Sets up the application's user interface components, now with a ribbon interface. """ + logging.info("Setting up the user interface with a ribbon...") self.setWindowTitle("Markdown Editor") width = self.config['window']['width'] height = self.config['window']['height'] self.setGeometry(100, 100, width, height) icon_path = self.dataDirectory / 'data' / 'smile.png' self.setWindowIcon(QIcon(str(icon_path))) + central_widget = QWidget() + layout = QVBoxLayout(central_widget) + layout.setContentsMargins(5, 5, 5, 5) + layout.addWidget(self.create_ribbon_interface()) self.directoryTree = QTreeView() self.directoryTree.setHeaderHidden(True) self.directoryTree.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) @@ -91,20 +127,20 @@ class MarkdownEditor(QMainWindow): directory_layout.addWidget(self.searchBox) directory_layout.addWidget(self.directoryTree) self.editor = QTextEdit() + self.editor.textChanged.connect(self.update_word_count) self.editor.textChanged.connect(self.update_preview) + self.highlighter = MarkdownHighlighter(self.editor.document()) self.preview = QWebEngineView() web_engine_settings = self.preview.page().settings() web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True) web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True) - layout = QHBoxLayout() - layout.addLayout(directory_layout, 0) - layout.addWidget(self.editor, 1) - layout.addWidget(self.preview, 1) - container = QWidget() - container.setLayout(layout) - self.setCentralWidget(container) - self.create_tool_bar() - self.create_menu_bar() + split_layout = QHBoxLayout() + split_layout.addLayout(directory_layout, 0) + split_layout.addWidget(self.editor, 1) + split_layout.addWidget(self.preview, 1) + layout.addLayout(split_layout) + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) self.setStatusBar(QStatusBar(self)) def initialize_data_directory(self): @@ -124,6 +160,93 @@ class MarkdownEditor(QMainWindow): QMessageBox.critical(self, "Directory Error", str(e)) sys.exit(1) + def create_ribbon_interface(self): + """ Creates the ribbon interface with tabs for File, Edit, View, Window, and Help. """ + ribbon = QTabWidget() + ribbon.setTabPosition(QTabWidget.TabPosition.North) + ribbon.addTab(self.create_file_tab(), "File") + ribbon.addTab(self.create_edit_tab(), "Edit") + ribbon.addTab(self.create_view_tab(), "View") + ribbon.addTab(self.create_window_tab(), "Window") + ribbon.addTab(self.create_help_tab(), "Help") + ribbon.setMaximumHeight(ribbon.sizeHint().height()) + return ribbon + + def create_file_tab(self): + """ Creates the File tab for the ribbon. """ + toolbar = QToolBar() + toolbar.setIconSize(QSize(16, 16)) + toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + toolbar.addAction(self.create_action("New", QStyle.StandardPixmap.SP_FileIcon, self.new_file)) + toolbar.addAction(self.create_action("Open", QStyle.StandardPixmap.SP_DirOpenIcon, self.open_file)) + toolbar.addAction(self.create_action("Save", QStyle.StandardPixmap.SP_DialogSaveButton, self.save_entry)) + toolbar.addAction( + self.create_action("Open Data Directory", QStyle.StandardPixmap.SP_DirIcon, self.open_data_directory)) + toolbar.addAction(self.create_action("Change Data Directory", QStyle.StandardPixmap.SP_FileDialogDetailedView, + self.change_data_directory)) + toolbar.addAction( + self.create_action("Settings", QStyle.StandardPixmap.SP_FileDialogInfoView, self.open_settings)) + toolbar.addAction(self.create_action("Exit", QStyle.StandardPixmap.SP_DialogCloseButton, self.close)) + return toolbar + + def create_edit_tab(self): + """ Creates the Edit tab for the ribbon. """ + toolbar = QToolBar() + toolbar.setIconSize(QSize(16, 16)) + toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + toolbar.addAction(self.create_action("Undo", QStyle.StandardPixmap.SP_ArrowBack, self.undo_action)) + toolbar.addAction(self.create_action("Redo", QStyle.StandardPixmap.SP_ArrowForward, self.redo_action)) + return toolbar + + def create_view_tab(self): + """ Creates the View tab for the ribbon. """ + toolbar = QToolBar() + toolbar.setIconSize(QSize(16, 16)) + toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + toolbar.addAction(self.create_action("Toggle Split View", QStyle.StandardPixmap.SP_FileDialogListView, + self.toggle_split_view)) + toolbar.addAction( + self.create_action("Toggle Toolbar", QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton, + self.toggle_toolbar)) + return toolbar + + def create_window_tab(self): + """ Creates the Window tab for the ribbon. """ + toolbar = QToolBar() + toolbar.setIconSize(QSize(16, 16)) + toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + toolbar.addAction(self.create_action("Close", QStyle.StandardPixmap.SP_DialogCloseButton, self.close)) + toolbar.addAction(self.create_action("Minimize", QStyle.StandardPixmap.SP_TitleBarMinButton, self.minimize)) + toolbar.addAction(self.create_action("Search", QStyle.StandardPixmap.SP_FileDialogContentsView, self.search)) + return toolbar + + def create_help_tab(self): + """ Creates the Help tab for the ribbon. """ + toolbar = QToolBar() + toolbar.setIconSize(QSize(16, 16)) + toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + toolbar.addAction(self.create_action("About", QStyle.StandardPixmap.SP_MessageBoxInformation, self.about)) + toolbar.addAction( + self.create_action("View Cheatsheet", QStyle.StandardPixmap.SP_FileDialogInfoView, self.view_cheatsheet)) + toolbar.addAction(self.create_action("GitHub", QStyle.StandardPixmap.SP_DriveNetIcon, self.open_git_hub)) + return toolbar + + def create_action(self, text, icon, callback): + """ Creates an action for the toolbar in the ribbon interface. """ + action = QAction(self.style().standardIcon(icon), text, self) + action.triggered.connect(callback) + return action + + def create_toolbar(self): + """ Creates a toolbar for the ribbon interface. """ + toolbar = QToolBar() + toolbar.setIconSize(QSize(16, 16)) + toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + toolbar.addAction(self.create_action("New", QStyle.StandardPixmap.SP_FileIcon, self.new_file)) + toolbar.addAction(self.create_action("Open", QStyle.StandardPixmap.SP_DirOpenIcon, self.open_file)) + toolbar.addAction(self.create_action("Save", QStyle.StandardPixmap.SP_DialogSaveButton, self.save_entry)) + return toolbar + def on_search_text_changed(self, text): self.searchText = text if not text: @@ -166,6 +289,11 @@ class MarkdownEditor(QMainWindow): dir_added = True return dir_added + def update_word_count(self): + text = self.editor.toPlainText() + word_count = len(text.split()) + self.statusBar().showMessage(f"Word Count: {word_count}") + @staticmethod def is_match(filename, text, fuzziness): if not text: @@ -290,58 +418,9 @@ class MarkdownEditor(QMainWindow): except Exception as e: QMessageBox.critical(self, "File Open Error", f"An error occurred: {e}") - def create_menu_bar(self): - """ - Create the menu bar for the application. It adds various menus like File, Edit, View, Window, and Help - with their respective actions. - """ - menu_bar = self.menuBar() - file_menu = menu_bar.addMenu("&File") - self.add_actions_to_menu(file_menu, [ - ("&New", self.new_file), - ("&Open", self.open_file), - ("&Save", self.save_entry), - ("Open Data Directory", self.open_data_directory), - ("Change Data Directory", self.change_data_directory), - ("Theme", self.change_theme), - ("Settings", self.open_settings), - ("E&xit", self.close) - ]) - - edit_menu = menu_bar.addMenu("&Edit") - self.add_actions_to_menu(edit_menu, [ - ("Undo", self.undo_action), - ("Redo", self.redo_action) - ]) - - view_menu = menu_bar.addMenu("&View") - self.add_actions_to_menu(view_menu, [ - ("Toggle Split View Mode", self.toggle_split_view), - ("Toggle Toolbar", self.toggle_toolbar), - ("Theme", [ - ("Dark", lambda: self.change_theme("dark")), - ("Light", lambda: self.change_theme("light")), - ("System", lambda: self.change_theme("system")) - ]) - ]) - - window_menu = menu_bar.addMenu("&Window") - self.add_actions_to_menu(window_menu, [ - ("Close", self.close), - ("Minimize", self.minimize), - ("Search", self.search) - ]) - - help_menu = menu_bar.addMenu("&Help") - self.add_actions_to_menu(help_menu, [ - ("&About", self.about), - ("View Cheatsheet", self.view_cheatsheet), - ("GitHub", self.open_git_hub) - ]) - def add_actions_to_menu(self, menu, actions): for action_text, action_func in actions: - if isinstance(action_func, list): # Submenu + if isinstance(action_func, list): submenu = menu.addMenu(action_text) for sub_action_text, sub_action_func in action_func: sub_action = QAction(sub_action_text, self) @@ -540,15 +619,16 @@ class MarkdownEditor(QMainWindow): """ if depth > self.depth: return - try: - for entry in sorted(directory.iterdir(), key=lambda e: (e.is_file(), e.name.lower())): + directory_entries = sorted(directory.iterdir(), + key=lambda dir_entry: (dir_entry.is_file(), dir_entry.name.lower())) + for entry in directory_entries: if entry.is_dir(): self.process_directory(entry, parent_item, depth) elif entry.suffix == '.md': self.add_file_item(entry, parent_item) - except PermissionError as e: - logging.error(f"Permission error accessing {directory}: {e}") + except PermissionError as permission_error: + logging.error(f"Permission error accessing {directory}: {permission_error}") @staticmethod def add_file_item(file_path, parent_item): @@ -671,65 +751,6 @@ class MarkdownEditor(QMainWindow): else: QMessageBox.critical(self, "Directory Selection Error", "Invalid directory path") - def change_theme(self, theme): - if theme == "dark": - self.apply_dark_theme() - elif theme == "light": - self.apply_light_theme() - elif theme == "system": - self.apply_system_theme() - - def apply_dark_theme(self): - dark_stylesheet = """ - QMainWindow { - background-color: #282c34; /* Dark gray background */ - color: #abb2bf; /* Light gray text */ - } - QMenuBar { - background-color: #21252b; /* Slightly darker gray for menu bar */ - color: #abb2bf; - } - QMenuBar::item { - background-color: #21252b; - color: #abb2bf; - } - QMenuBar::item:selected { /* when selected using mouse or keyboard */ - background-color: #282c34; - } - QTextEdit, QTreeView { - background-color: #282c34; - color: #abb2bf; - } - """ - self.setStyleSheet(dark_stylesheet) - - def apply_light_theme(self): - light_stylesheet = """ - QMainWindow { - background-color: #fafafa; /* Very light gray, almost white */ - color: #383a42; /* Dark gray text */ - } - QMenuBar { - background-color: #f0f0f0; /* Light gray for menu bar */ - color: #383a42; - } - QMenuBar::item { - background-color: #f0f0f0; - color: #383a42; - } - QMenuBar::item:selected { - background-color: #fafafa; - } - QTextEdit, QTreeView { - background-color: #fafafa; - color: #383a42; - } - """ - self.setStyleSheet(light_stylesheet) - - def apply_system_theme(self): - self.setStyleSheet("") - def open_settings(self): pass @@ -768,6 +789,49 @@ class MarkdownEditor(QMainWindow): pass +class SyntaxColors: + COMMENT = "#6272a4" + CYAN = "#8be9fd" + GREEN = "#50fa7b" + ORANGE = "#ffb86c" + PINK = "#ff79c6" + PURPLE = "#9f74b3" + RED = "#ff5555" + YELLOW = "#f1fa8c" + + +class MarkdownHighlighter(QSyntaxHighlighter): + def __init__(self, parent=None): + super().__init__(parent) + self.setupRules() + + def setupRules(self): + heading_format = QTextCharFormat() + heading_format.setForeground(QColor(SyntaxColors.PURPLE)) + + italic_format = QTextCharFormat() + italic_format.setFontItalic(True) + italic_format.setForeground(QColor(SyntaxColors.GREEN)) + + bold_format = QTextCharFormat() + bold_format.setFontWeight(QFont.Weight.Bold) + bold_format.setForeground(QColor(SyntaxColors.ORANGE)) + + self.highlightingRules = [ + (QRegularExpression("^#.*"), heading_format), + (QRegularExpression("\*{1}[^*]+\*{1}"), italic_format), + (QRegularExpression("\*{2}[^*]+\*{2}"), bold_format), + ] + + def highlightBlock(self, text): + for pattern, format in self.highlightingRules: + match_iterator = pattern.globalMatch(text) + while match_iterator.hasNext(): + match = match_iterator.next() + self.setFormat(match.capturedStart(), match.capturedLength(), format) + self.setCurrentBlockState(0) + + def main(): """ Main function to run the Markdown Editor application. """ logging.info("Launching the application...")