Word counting and Syntax Highlighting added

This commit is contained in:
Isaak Buslovich 2024-01-17 22:51:39 +01:00
parent cc8aaca206
commit 998fd28691
Signed by: Isaak
GPG Key ID: EEC31D6437FBCC63

326
main.py
View File

@ -12,21 +12,22 @@ Author: Isaak Buslovich
Date: 2024-01-13 Date: 2024-01-13
Version: 0.1 Version: 0.1
""" """
import logging
import pathlib import pathlib
import platform import platform
import sys import sys
import threading import threading
import webbrowser import webbrowser
import yaml import yaml
from PyQt6.QtCore import QFileSystemWatcher, Qt, QTimer from PyQt6.QtCore import QFileSystemWatcher, Qt, QTimer, QSize, QRegularExpression
from PyQt6.QtGui import QAction, QStandardItem, QStandardItemModel, QIcon, QPalette from PyQt6.QtGui import QAction, QStandardItem, QStandardItemModel, QIcon, QPalette, QSyntaxHighlighter, \
from PyQt6.QtWebEngineWidgets import QWebEngineView QTextCharFormat, QColor, QFont
from PyQt6.QtWebEngineCore import QWebEngineSettings from PyQt6.QtWebEngineCore import QWebEngineSettings
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QApplication, QMainWindow, QTextEdit, QHBoxLayout, QWidget, QToolBar, QStatusBar, \ 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 markdown2 import markdown
from fuzzywuzzy import process, fuzz
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@ -46,9 +47,15 @@ class MarkdownEditor(QMainWindow):
""" """
def __init__(self): 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. """ """ Initializes the Markdown editor application with UI setup and configuration management. """
logging.info("Initializing the Markdown Editor...") logging.info("Initializing the Markdown Editor...")
super().__init__()
self.dataDirectory = None self.dataDirectory = None
self.preview = None self.preview = None
self.editor = None self.editor = None
@ -71,15 +78,44 @@ class MarkdownEditor(QMainWindow):
self.searchTimer.setSingleShot(True) self.searchTimer.setSingleShot(True)
self.searchTimer.timeout.connect(self.perform_search) 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): def initialize_ui(self):
""" Sets up the application's user interface components. """ """ Sets up the application's user interface components, now with a ribbon interface. """
logging.info("Setting up the user interface...") logging.info("Setting up the user interface with a ribbon...")
self.setWindowTitle("Markdown Editor") self.setWindowTitle("Markdown Editor")
width = self.config['window']['width'] width = self.config['window']['width']
height = self.config['window']['height'] height = self.config['window']['height']
self.setGeometry(100, 100, width, height) self.setGeometry(100, 100, width, height)
icon_path = self.dataDirectory / 'data' / 'smile.png' icon_path = self.dataDirectory / 'data' / 'smile.png'
self.setWindowIcon(QIcon(str(icon_path))) 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 = QTreeView()
self.directoryTree.setHeaderHidden(True) self.directoryTree.setHeaderHidden(True)
self.directoryTree.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.directoryTree.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
@ -91,20 +127,20 @@ class MarkdownEditor(QMainWindow):
directory_layout.addWidget(self.searchBox) directory_layout.addWidget(self.searchBox)
directory_layout.addWidget(self.directoryTree) directory_layout.addWidget(self.directoryTree)
self.editor = QTextEdit() self.editor = QTextEdit()
self.editor.textChanged.connect(self.update_word_count)
self.editor.textChanged.connect(self.update_preview) self.editor.textChanged.connect(self.update_preview)
self.highlighter = MarkdownHighlighter(self.editor.document())
self.preview = QWebEngineView() self.preview = QWebEngineView()
web_engine_settings = self.preview.page().settings() web_engine_settings = self.preview.page().settings()
web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True) web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True) web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
layout = QHBoxLayout() split_layout = QHBoxLayout()
layout.addLayout(directory_layout, 0) split_layout.addLayout(directory_layout, 0)
layout.addWidget(self.editor, 1) split_layout.addWidget(self.editor, 1)
layout.addWidget(self.preview, 1) split_layout.addWidget(self.preview, 1)
container = QWidget() layout.addLayout(split_layout)
container.setLayout(layout) central_widget.setLayout(layout)
self.setCentralWidget(container) self.setCentralWidget(central_widget)
self.create_tool_bar()
self.create_menu_bar()
self.setStatusBar(QStatusBar(self)) self.setStatusBar(QStatusBar(self))
def initialize_data_directory(self): def initialize_data_directory(self):
@ -124,6 +160,93 @@ class MarkdownEditor(QMainWindow):
QMessageBox.critical(self, "Directory Error", str(e)) QMessageBox.critical(self, "Directory Error", str(e))
sys.exit(1) 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): def on_search_text_changed(self, text):
self.searchText = text self.searchText = text
if not text: if not text:
@ -166,6 +289,11 @@ class MarkdownEditor(QMainWindow):
dir_added = True dir_added = True
return dir_added 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 @staticmethod
def is_match(filename, text, fuzziness): def is_match(filename, text, fuzziness):
if not text: if not text:
@ -290,58 +418,9 @@ class MarkdownEditor(QMainWindow):
except Exception as e: except Exception as e:
QMessageBox.critical(self, "File Open Error", f"An error occurred: {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): def add_actions_to_menu(self, menu, actions):
for action_text, action_func in actions: for action_text, action_func in actions:
if isinstance(action_func, list): # Submenu if isinstance(action_func, list):
submenu = menu.addMenu(action_text) submenu = menu.addMenu(action_text)
for sub_action_text, sub_action_func in action_func: for sub_action_text, sub_action_func in action_func:
sub_action = QAction(sub_action_text, self) sub_action = QAction(sub_action_text, self)
@ -540,15 +619,16 @@ class MarkdownEditor(QMainWindow):
""" """
if depth > self.depth: if depth > self.depth:
return return
try: 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(): if entry.is_dir():
self.process_directory(entry, parent_item, depth) self.process_directory(entry, parent_item, depth)
elif entry.suffix == '.md': elif entry.suffix == '.md':
self.add_file_item(entry, parent_item) self.add_file_item(entry, parent_item)
except PermissionError as e: except PermissionError as permission_error:
logging.error(f"Permission error accessing {directory}: {e}") logging.error(f"Permission error accessing {directory}: {permission_error}")
@staticmethod @staticmethod
def add_file_item(file_path, parent_item): def add_file_item(file_path, parent_item):
@ -671,65 +751,6 @@ class MarkdownEditor(QMainWindow):
else: else:
QMessageBox.critical(self, "Directory Selection Error", "Invalid directory path") 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): def open_settings(self):
pass pass
@ -768,6 +789,49 @@ class MarkdownEditor(QMainWindow):
pass 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(): def main():
""" Main function to run the Markdown Editor application. """ """ Main function to run the Markdown Editor application. """
logging.info("Launching the application...") logging.info("Launching the application...")