Compare commits

...

2 Commits

Author SHA1 Message Date
cc8aaca206
Theme switcher implemented 2024-01-17 01:06:53 +01:00
d6d09ba405
fuzzy search implemented 2024-01-16 21:55:49 +01:00

288
main.py
View File

@ -15,9 +15,10 @@ Version: 0.1
import pathlib import pathlib
import platform import platform
import sys import sys
import threading
import webbrowser import webbrowser
import yaml import yaml
from PyQt6.QtCore import QFileSystemWatcher, Qt from PyQt6.QtCore import QFileSystemWatcher, Qt, QTimer
from PyQt6.QtGui import QAction, QStandardItem, QStandardItemModel, QIcon, QPalette from PyQt6.QtGui import QAction, QStandardItem, QStandardItemModel, QIcon, QPalette
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import QWebEngineSettings from PyQt6.QtWebEngineCore import QWebEngineSettings
@ -25,6 +26,9 @@ from PyQt6.QtWidgets import QApplication, QMainWindow, QTextEdit, QHBoxLayout, Q
QMessageBox, QTreeView, QFileDialog, QAbstractItemView, QLineEdit, QVBoxLayout QMessageBox, QTreeView, QFileDialog, QAbstractItemView, QLineEdit, QVBoxLayout
from markdown2 import markdown from markdown2 import markdown
from fuzzywuzzy import process, fuzz from fuzzywuzzy import process, fuzz
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
class MarkdownEditor(QMainWindow): class MarkdownEditor(QMainWindow):
@ -43,6 +47,7 @@ class MarkdownEditor(QMainWindow):
def __init__(self): def __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...")
super().__init__() super().__init__()
self.dataDirectory = None self.dataDirectory = None
self.preview = None self.preview = None
@ -62,100 +67,115 @@ class MarkdownEditor(QMainWindow):
self.update_directory_tree_view() self.update_directory_tree_view()
self.fileSystemWatcher.directoryChanged.connect(self.update_directory_tree_view) self.fileSystemWatcher.directoryChanged.connect(self.update_directory_tree_view)
self.load_initial_file() self.load_initial_file()
self.searchTimer = QTimer()
self.searchTimer.setSingleShot(True)
self.searchTimer.timeout.connect(self.perform_search)
def initialize_ui(self): def initialize_ui(self):
""" Sets up the application's user interface components. """ """ Sets up the application's user interface components. """
logging.info("Setting up the user interface...")
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)))
# Create the directory tree view
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)
self.directoryTree.doubleClicked.connect(self.on_file_double_clicked) self.directoryTree.doubleClicked.connect(self.on_file_double_clicked)
# Create the QLineEdit widget for the search box
self.searchBox = QLineEdit() self.searchBox = QLineEdit()
self.searchBox.setPlaceholderText("Search...") # Placeholder text for the search box self.searchBox.setPlaceholderText("Search...")
self.searchBox.textChanged.connect(self.on_search_text_changed) # Connect to search handler self.searchBox.textChanged.connect(self.on_search_text_changed)
# Create a QVBoxLayout for the directory tree and search box
directory_layout = QVBoxLayout() directory_layout = QVBoxLayout()
directory_layout.addWidget(self.searchBox) # Add the search box to the layout directory_layout.addWidget(self.searchBox)
directory_layout.addWidget(self.directoryTree) # Add the directory tree below the search box directory_layout.addWidget(self.directoryTree)
# Text editor setup
self.editor = QTextEdit() self.editor = QTextEdit()
self.editor.textChanged.connect(self.update_preview) self.editor.textChanged.connect(self.update_preview)
# Preview setup
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)
# Main layout setup
layout = QHBoxLayout() layout = QHBoxLayout()
layout.addLayout(directory_layout, 0) # Add the QVBoxLayout with the search box and directory tree layout.addLayout(directory_layout, 0)
layout.addWidget(self.editor, 1) layout.addWidget(self.editor, 1)
layout.addWidget(self.preview, 1) layout.addWidget(self.preview, 1)
# Set the main layout
container = QWidget() container = QWidget()
container.setLayout(layout) container.setLayout(layout)
self.setCentralWidget(container) self.setCentralWidget(container)
# Toolbar, menu bar, and status bar setup
self.create_tool_bar() self.create_tool_bar()
self.create_menu_bar() self.create_menu_bar()
self.setStatusBar(QStatusBar(self)) self.setStatusBar(QStatusBar(self))
def initialize_data_directory(self): def initialize_data_directory(self):
""" Initializes the data directory and sets up the file system watcher. """ """
data_directory = self.config['dataDirectory'] Initializes the data directory and sets up the file system watcher.
self.dataDirectory = pathlib.Path(data_directory).resolve() """
if not self.dataDirectory.is_dir(): logging.info("Initializing data directory.")
QMessageBox.critical(self, "Directory Error", "The application's directory is not valid.") 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) sys.exit(1)
self.fileSystemWatcher.addPath(str(self.dataDirectory))
def on_search_text_changed(self, text): def on_search_text_changed(self, text):
""" self.searchText = text
Handles the search functionality. if not text:
First, tries an exact match. If no results are found, uses fuzzy matching and gradually increases fuzziness. self.update_directory_tree_view()
""" else:
if not self.dataDirectory or not self.dataDirectory.is_dir(): self.searchTimer.start(60)
return
def update_search_results(self, text):
model = QStandardItemModel() model = QStandardItemModel()
self.populate_filtered_model(model, self.dataDirectory, text)
self.directoryTree.setModel(model) self.directoryTree.setModel(model)
try:
entries = list(self.dataDirectory.rglob('*'))
filtered_entries = []
if text:
filtered_entries = [entry for entry in entries if text.lower() in entry.name.lower()]
fuzziness = 100
while not filtered_entries and fuzziness > 0:
matches = [(entry, fuzz.partial_ratio(text.lower(), entry.name.lower()))
for entry in entries]
filtered_entries = [entry for entry, score in matches if score >= fuzziness]
if len(filtered_entries) >= 2:
break
fuzziness -= 5
for entry in filtered_entries:
self.add_item_to_model(model, entry, is_dir=entry.is_dir())
except Exception as e:
QMessageBox.critical(self, "Search Error", f"An error occurred during the search: {e}")
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.
def add_item_to_model(self, model, entry, is_dir=False): 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
@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. Adds an item to the directory tree model.
@ -171,7 +191,6 @@ class MarkdownEditor(QMainWindow):
item.setIcon(QIcon('icons/folder_icon.png')) item.setIcon(QIcon('icons/folder_icon.png'))
model.appendRow(item) model.appendRow(item)
@staticmethod @staticmethod
def get_config_path(): def get_config_path():
""" """
@ -211,10 +230,22 @@ class MarkdownEditor(QMainWindow):
Load the configuration from the configuration file. Load the configuration from the configuration file.
Returns: Returns:
dict: The loaded configuration. dict: The loaded configuration with default values if keys are missing.
""" """
with open(self.config_path) as file: default_config = {
return yaml.safe_load(file) '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): def save_config(self):
""" """
@ -286,7 +317,12 @@ class MarkdownEditor(QMainWindow):
view_menu = menu_bar.addMenu("&View") view_menu = menu_bar.addMenu("&View")
self.add_actions_to_menu(view_menu, [ self.add_actions_to_menu(view_menu, [
("Toggle Split View Mode", self.toggle_split_view), ("Toggle Split View Mode", self.toggle_split_view),
("Toggle Toolbar", self.toggle_toolbar) ("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") window_menu = menu_bar.addMenu("&Window")
@ -304,17 +340,17 @@ class MarkdownEditor(QMainWindow):
]) ])
def add_actions_to_menu(self, menu, actions): def add_actions_to_menu(self, menu, actions):
""" for action_text, action_func in actions:
Add actions to a given menu. if isinstance(action_func, list): # Submenu
submenu = menu.addMenu(action_text)
Args: for sub_action_text, sub_action_func in action_func:
menu (QMenu): The menu to which actions will be added. sub_action = QAction(sub_action_text, self)
actions (list of tuples): Each tuple contains the text for the menu item and the handler function. sub_action.triggered.connect(sub_action_func)
""" submenu.addAction(sub_action)
for action_text, action_handler in actions: else:
action = QAction(action_text, self) action = QAction(action_text, self)
action.triggered.connect(action_handler) action.triggered.connect(action_func)
menu.addAction(action) menu.addAction(action)
def create_tool_bar(self): def create_tool_bar(self):
""" """
@ -495,30 +531,53 @@ class MarkdownEditor(QMainWindow):
def populate_model(self, parent_item, directory, depth=0): def populate_model(self, parent_item, directory, depth=0):
""" """
Populates the directory tree model with directory contents. Populates the directory tree model with directory contents.
Shows all directories containing Markdown files and Markdown files themselves.
Args: Args:
parent_item (QStandardItem): Item to populate. parent_item (QStandardItem): Item to populate.
directory (pathlib.Path): Directory to scan. directory (pathlib.Path): Directory to scan.
depth (int): Depth level for recursive scanning. depth (int): Depth level for recursive scanning.
""" """
if depth > 5: if depth > self.depth:
return return
try: try:
for entry in sorted(directory.iterdir(), key=lambda e: (e.is_file(), e.name.lower())): for entry in sorted(directory.iterdir(), key=lambda e: (e.is_file(), e.name.lower())):
if entry.is_dir(): if entry.is_dir():
if any(file.suffix == '.md' for file in entry.glob('*.md')): self.process_directory(entry, parent_item, depth)
dir_item = QStandardItem(entry.name)
dir_item.setData(str(entry))
dir_item.setFlags(dir_item.flags() | Qt.ItemFlag.ItemIsEnabled)
dir_item.setIcon(QIcon('icons/folder_icon.png'))
parent_item.appendRow(dir_item)
self.populate_model(dir_item, entry, depth + 1)
elif entry.suffix == '.md': elif entry.suffix == '.md':
file_item = QStandardItem(entry.name) self.add_file_item(entry, parent_item)
file_item.setData(str(entry)) except PermissionError as e:
parent_item.appendRow(file_item) logging.error(f"Permission error accessing {directory}: {e}")
except PermissionError:
pass @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): def on_directory_tree_clicked(self, index):
""" """
@ -612,8 +671,64 @@ 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): def change_theme(self, theme):
pass 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
@ -655,6 +770,7 @@ class MarkdownEditor(QMainWindow):
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...")
app = QApplication(sys.argv) app = QApplication(sys.argv)
editor = MarkdownEditor() editor = MarkdownEditor()
editor.show() editor.show()