diary/main.py

846 lines
33 KiB
Python

"""
Markdown Editor Application using PyQt6.
Key Features:
- Create, edit, and preview Markdown files.
- Live HTML preview of Markdown content.
- Configurable settings stored in YAML.
Dependencies: PyQt6, yaml, markdown2.
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, 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, QStyle, QTabWidget, QLabel
from fuzzywuzzy import fuzz
from markdown2 import markdown
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
class MarkdownEditor(QMainWindow):
"""
A PyQt6-based Markdown editor with file navigation, text editing, and live HTML preview.
Attributes:
config_path (pathlib.Path): Configuration file path.
config (dict): Application configuration.
fileSystemWatcher (QFileSystemWatcher): Monitors file system changes.
dataDirectory (pathlib.Path): Data directory path.
directoryTree (QTreeView): File navigation widget.
editor (QTextEdit): Markdown text editor.
preview (QWebEngineView): Live HTML preview widget.
"""
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...")
self.dataDirectory = None
self.preview = None
self.editor = None
self.directoryTree = None
self.config_path = pathlib.Path(__file__).parent / 'config.yaml'
self.toolbar = None
if not self.config_path.exists():
self.config = {'window': {'width': 800, 'height': 600},
'dataDirectory': str(pathlib.Path(__file__).parent.resolve())}
self.save_config()
else:
self.config = self.load_config()
self.fileSystemWatcher = QFileSystemWatcher()
self.initialize_data_directory()
self.initialize_ui()
self.update_directory_tree_view()
self.fileSystemWatcher.directoryChanged.connect(self.update_directory_tree_view)
self.load_initial_file()
self.searchTimer = QTimer()
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, 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)
self.directoryTree.doubleClicked.connect(self.on_file_double_clicked)
self.searchBox = QLineEdit()
self.searchBox.setPlaceholderText("Search...")
self.searchBox.textChanged.connect(self.on_search_text_changed)
directory_layout = QVBoxLayout()
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)
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):
"""
Initializes the data directory and sets up the file system watcher.
"""
logging.info("Initializing data directory.")
try:
data_directory = self.config.get('dataDirectory', str(pathlib.Path.home()))
self.dataDirectory = pathlib.Path(data_directory).resolve()
if not self.dataDirectory.is_dir():
raise FileNotFoundError(f"Data directory not found: {self.dataDirectory}")
self.fileSystemWatcher.addPath(str(self.dataDirectory))
logging.info(f"Data directory set to: {self.dataDirectory}")
except Exception as e:
logging.error(f"Failed to initialize data directory: {e}")
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:
self.update_directory_tree_view()
else:
self.searchTimer.start(60)
def update_search_results(self, text):
model = QStandardItemModel()
self.populate_filtered_model(model, self.dataDirectory, text)
self.directoryTree.setModel(model)
def perform_search(self):
threading.Thread(target=self.update_search_results, args=(self.searchText,)).start()
def populate_filtered_model(self, parent_item, directory, text, fuzziness=100, depth=0):
"""
Recursively populates the directory tree model with filtered contents based on the search text.
Args:
parent_item (QStandardItem): The parent item in the tree view model.
directory (pathlib.Path): The directory to scan.
text (str): The search text.
fuzziness (int): The fuzziness level for matching file names.
depth (int): Current depth of recursion.
"""
if depth > self.depth:
return False
dir_added = False
for entry in sorted(directory.iterdir(), key=lambda e: (e.is_file(), e.name.lower())):
if entry.is_dir():
dir_item = QStandardItem(entry.name)
if self.populate_filtered_model(dir_item, entry, text, fuzziness, depth + 1):
parent_item.appendRow(dir_item)
dir_added = True
elif entry.suffix == '.md' and self.is_match(entry.name, text, fuzziness):
file_item = QStandardItem(entry.name)
file_item.setData(str(entry))
parent_item.appendRow(file_item)
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:
return False
if fuzziness == 100:
return text.lower() in filename.lower()
return fuzz.partial_ratio(text.lower(), filename.lower()) >= fuzziness
@staticmethod
def add_item_to_model(model, entry, is_dir=False):
"""
Adds an item to the directory tree model.
Args:
model (QStandardItemModel): The model of the directory tree.
entry (Path): The file or directory path.
is_dir (bool): Indicates if the entry is a directory.
"""
item = QStandardItem(entry.name)
item.setData(str(entry))
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEnabled)
if is_dir:
item.setIcon(QIcon('icons/folder_icon.png'))
model.appendRow(item)
@staticmethod
def get_config_path():
"""
Get the path to the configuration file based on the operating system.
Returns:
Path: The path to the configuration file.
"""
app_name = "YourApp"
home = pathlib.Path.home()
if platform.system() == "Windows":
config_path = pathlib.Path(home, "AppData", "Roaming", app_name, "config.yaml")
elif platform.system() == "Darwin":
config_path = pathlib.Path(home, "Library", "Application Support", app_name, "config.yaml")
else:
config_path = pathlib.Path(home, ".config", app_name, "config.yaml")
config_path.parent.mkdir(parents=True, exist_ok=True)
return config_path
config_file = get_config_path()
def closeEvent(self, event):
"""
Handle the close event of the application. Save the configuration before closing.
Args:
event (QCloseEvent): The close event.
"""
self.config['window']['width'] = self.size().width()
self.config['window']['height'] = self.size().height()
self.config['dataDirectory'] = str(self.dataDirectory)
self.save_config()
super().closeEvent(event)
def load_config(self):
"""
Load the configuration from the configuration file.
Returns:
dict: The loaded configuration with default values if keys are missing.
"""
default_config = {
'window': {'width': 800, 'height': 600},
'dataDirectory': str(pathlib.Path(__file__).parent.resolve()),
'depth': 5
}
try:
with open(self.config_path) as file:
config = yaml.safe_load(file) or {}
except FileNotFoundError:
config = {}
for key, value in default_config.items():
config.setdefault(key, value)
self.depth = config['depth']
return config
def save_config(self):
"""
Save the configuration to the configuration file.
"""
with open(self.config_path, 'w') as file:
yaml.dump(self.config, file)
def load_initial_file(self):
"""
Load an initial Markdown file for editing. This method looks for a 'cheatsheet.md' file in the
application's data directory and loads it into the editor if it exists.
"""
cheatsheet_path = self.dataDirectory / 'cheatsheet.md'
if cheatsheet_path.is_file():
with open(cheatsheet_path, encoding='utf-8') as file:
markdown_content = file.read()
self.editor.setText(markdown_content)
self.update_preview()
else:
QMessageBox.warning(self, "File Not Found", f"Cheatsheet file not found: {cheatsheet_path}")
def on_file_double_clicked(self, index):
"""
Loads a Markdown file into the editor on double-click in the directory tree.
Args:
index (QModelIndex): Index of the clicked item in the directory tree.
"""
try:
file_path = pathlib.Path(index.model().itemFromIndex(index).data())
if not file_path.exists():
QMessageBox.critical(self, "Error", f"The item does not exist: {file_path}")
return
if not file_path.is_file():
QMessageBox.critical(self, "Error", "The selected item is not a file.")
return
with open(file_path, encoding='utf-8') as file:
markdown_content = file.read()
self.editor.setText(markdown_content)
self.update_preview()
except Exception as e:
QMessageBox.critical(self, "File Open Error", f"An error occurred: {e}")
def add_actions_to_menu(self, menu, actions):
for action_text, action_func in actions:
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)
sub_action.triggered.connect(sub_action_func)
submenu.addAction(sub_action)
else:
action = QAction(action_text, self)
action.triggered.connect(action_func)
menu.addAction(action)
def create_tool_bar(self):
"""
Create the main toolbar for the application. It adds actions like New, Open, and Save with icons.
"""
self.toolbar = QToolBar("Main Toolbar")
self.addToolBar(self.toolbar)
self.add_actions_to_toolbar(self.toolbar, [
("New", "📄", self.new_file),
("Open", "📂", self.open_file),
("Save", "💾", self.save_entry)
])
def add_actions_to_toolbar(self, toolbar, actions):
"""
Add actions to a given toolbar.
"""
for name, emoji, handler in actions:
action = QAction(emoji + ' ' + name, self)
action.triggered.connect(handler)
toolbar.addAction(action)
@staticmethod
def markdown_table_to_html(markdown_content):
"""
Convert Markdown tables in the given content to HTML table format.
Args:
markdown_content (str): The Markdown content containing tables.
Returns:
str: The HTML content with Markdown tables converted to HTML tables.
"""
lines = markdown_content.split('\n')
html_lines = []
in_table = False
for line in lines:
if '|' in line:
if not in_table:
html_lines.append('<table>')
in_table = True
columns = line.split('|')[1:-1]
if '---' in columns[0]:
html_lines.append('<tr>' + ''.join('<th>{}</th>'.format(col.strip()) for col in columns) + '</tr>')
else:
html_lines.append('<tr>' + ''.join('<td>{}</td>'.format(col.strip()) for col in columns) + '</tr>')
else:
if in_table:
html_lines.append('</table>')
in_table = False
html_lines.append(line)
if in_table:
html_lines.append('</table>')
return '\n'.join(html_lines)
@staticmethod
def convert_image_paths(html_content, base_directory):
"""
Converts relative image paths in the HTML content to absolute file URLs.
Args:
html_content (str): The HTML content with Markdown images.
base_directory (Path): The base directory where the 'data' folder is located.
Returns:
str: Modified HTML content with absolute image paths.
"""
image_folder = base_directory / 'data'
def replace_path(match):
"""
Replaces a matched image path with an absolute URL.
Args:
match (MatchObject): Regex match object containing the image path.
Returns:
str: The image URL with the absolute path.
"""
image_relative_path = match.group(1)
image_absolute_path = image_folder / image_relative_path
return f'src="{image_absolute_path.as_uri()}"'
import re
html_content = re.sub(r'src="([^"]+)"', replace_path, html_content)
return html_content
def update_preview(self):
""" Updates the HTML preview based on the current Markdown content. """
markdown_content = self.editor.toPlainText()
html_content = markdown(markdown_content)
html_content = self.markdown_table_to_html(html_content)
data_directory = pathlib.Path(__file__).parent.resolve()
html_content = self.convert_image_paths(html_content, data_directory)
html_content = html_content.replace('[x]', '')
html_content = html_content.replace('[ ]', '')
palette = QApplication.instance().palette()
background_color = palette.color(QPalette.ColorRole.Window).name()
foreground_color = palette.color(QPalette.ColorRole.WindowText).name()
accent_color = "#9f74b3"
css = f"""
<style>
@import url('https://fonts.googleapis.com/css2?family=Lora&display=swap');
/* Main body styling */
body {{
font-family: 'Lora', serif;
background-color: {background_color};
color: {foreground_color};
margin: 0;
padding: 0;
display: flex;
justify-content: center;
}}
/* Container for the content */
.content-container {{
max-width: 600px;
width: 95%;
margin: 20px;
}}
/* Headers styling */
h1, h2, h3, h4, h5, h6 {{
color: {accent_color};
}}
/* Style for scrollbar */
::-webkit-scrollbar {{
width: 12px;
}}
::-webkit-scrollbar-track {{
background: {background_color};
}}
::-webkit-scrollbar-thumb {{
background: #555;
border-radius: 6px;
}}
::-webkit-scrollbar-thumb:hover {{
background: {accent_color};
}}
/* Table styling */
table {{
border-collapse: collapse;
width: 100%;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}}
th, td {{
border: 1px solid {foreground_color};
padding: 8px;
text-align: left;
background-color: {background_color};
color: {foreground_color};
}}
th {{
background-color: {accent_color};
color: white;
}}
tr:nth-child(even) {{
background-color: rgba(255, 255, 255, 0.1);
}}
tr:hover {{
background-color: rgba(255, 255, 255, 0.2);
}}
</style>
"""
html_with_css = css + '<div class="content-container">' + html_content + '</div>'
self.preview.setHtml(html_with_css)
def update_directory_tree_view(self):
""" Refreshes the directory tree view with files and directories. """
if not self.dataDirectory or not self.dataDirectory.is_dir():
return
model = QStandardItemModel()
self.directoryTree.setModel(model)
self.populate_model(model, self.dataDirectory)
def populate_model(self, parent_item, directory, depth=0):
"""
Populates the directory tree model with directory contents.
Shows all directories containing Markdown files and Markdown files themselves.
Args:
parent_item (QStandardItem): Item to populate.
directory (pathlib.Path): Directory to scan.
depth (int): Depth level for recursive scanning.
"""
if depth > self.depth:
return
try:
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 permission_error:
logging.error(f"Permission error accessing {directory}: {permission_error}")
@staticmethod
def add_file_item(file_path, parent_item):
"""
Adds a file item to the model.
Args:
file_path (pathlib.Path): The file path to add.
parent_item (QStandardItem): The parent item in the model.
"""
file_item = QStandardItem(file_path.name)
file_item.setData(str(file_path))
parent_item.appendRow(file_item)
def process_directory(self, directory, parent_item, depth):
"""
Processes a single directory, adding it to the model if it contains Markdown files.
Args:
directory (pathlib.Path): The directory to process.
parent_item (QStandardItem): The parent item in the model.
depth (int): Current depth in the directory tree.
"""
if any(file.suffix == '.md' for file in directory.iterdir()):
dir_item = QStandardItem(directory.name)
dir_item.setData(str(directory))
dir_item.setIcon(QIcon('icons/folder_icon.png'))
parent_item.appendRow(dir_item)
self.populate_model(dir_item, directory, depth + 1)
def on_directory_tree_clicked(self, index):
"""
Handle clicks on the directory tree. It loads the selected file into the editor if it is a Markdown file.
Args:
index (QModelIndex): The model index of the clicked item in the directory tree.
"""
file_path = pathlib.Path(index.data())
if file_path.is_file():
self.load_file(file_path)
def load_file(self, file_path):
"""
Loads the content of a file into the text editor.
Args:
file_path (pathlib.Path): The path of the file to load.
"""
if not file_path.is_file():
QMessageBox.critical(self, "File Open Error", "The selected file does not exist.")
return
try:
with open(file_path, encoding='utf-8') as file:
markdown_content = file.read()
except OSError as e:
QMessageBox.critical(self, "File Open Error", f"Error opening file: {e}")
return
self.editor.setText(markdown_content)
self.update_preview()
def new_file(self):
"""
Clear the editor to create a new file. This action empties the text editor for writing new content.
"""
self.editor.clear()
def open_file(self):
"""
Open a file dialog to select a Markdown file. This method allows the user to select a Markdown file from
the file system and loads its content into the editor.
"""
filename, _ = QFileDialog.getOpenFileName(
self,
"Open Markdown File",
str(self.dataDirectory),
'Markdown Files (*.md)')
if not filename:
return
try:
with open(filename, encoding='utf-8') as file:
markdown_content = file.read()
except OSError as e:
QMessageBox.critical(self, "File Open Error", f"Error opening file: {e}")
return
self.editor.setText(markdown_content)
self.update_preview()
def save_entry(self):
"""
Save the current entry. This method is intended to save the content of the text editor to a file.
"""
pass
def about(self):
"""
Displays an 'About' dialog with information about the Markdown Editor.
"""
QMessageBox.about(self, "About Markdown Editor", "Markdown Editor\nVersion 1.0")
def open_data_directory(self):
""" Opens the application's data directory in the system's default file explorer. """
try:
directory_url = pathlib.Path(self.dataDirectory).resolve().as_uri()
webbrowser.open(directory_url)
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to open directory: {e}")
def change_data_directory(self):
"""
Opens a dialog to select a new data directory for the application.
"""
new_data_directory = QFileDialog.getExistingDirectory(self, "Select Data Directory", str(self.dataDirectory))
if new_data_directory:
new_path = pathlib.Path(new_data_directory)
if new_path.is_dir():
self.dataDirectory = new_path
self.update_directory_tree_view()
else:
QMessageBox.critical(self, "Directory Selection Error", "Invalid directory path")
def open_settings(self):
pass
def undo_action(self):
pass
def redo_action(self):
pass
def toggle_split_view(self):
""" Toggles the visibility of the preview pane. """
self.preview.setVisible(not self.preview.isVisible())
self.centralWidget().layout().activate()
def toggle_toolbar(self):
""" Toggle the visibility of the toolbar. """
if self.toolbar:
self.toolbar.setVisible(not self.toolbar.isVisible())
def minimize(self):
""" Minimize the main window. """
self.showMinimized()
def search(self):
pass
def view_cheatsheet(self):
""" Load and display the cheatsheet markdown file. """
cheatsheet_path = self.dataDirectory / 'cheatsheet.md'
if cheatsheet_path.is_file():
self.load_file(cheatsheet_path)
else:
QMessageBox.warning(self, "Cheatsheet Not Found", f"Cheatsheet file not found: {cheatsheet_path}")
def open_git_hub(self):
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...")
app = QApplication(sys.argv)
editor = MarkdownEditor()
editor.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()