Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 166 additions & 18 deletions markdown_reader/ui.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import re
import sys
import threading
import tkinter as tk
import tkinter.font # moved here from inside methods
Expand Down Expand Up @@ -150,6 +151,89 @@ def __init__(self, root):
self._translation_session_counter = 0
self._translation_cancel_requested = False

self.global_shortcuts = [
("New File", "<Control-KeyPress-n>", self.new_file),
("Open File", "<Control-KeyPress-o>", self.open_file),
("Save File", "<Control-KeyPress-s>", self.save_file),
("Close Current Tab", "<Control-KeyPress-w>", self.close_current_tab),
("Close All Tabs", "<Control-Shift-KeyPress-W>", self.close_all_tabs),
("Undo", "<Control-KeyPress-z>", self.undo_action),
("Redo", "<Control-KeyPress-y>", self.redo_action),
("Redo", "<Control-Shift-KeyPress-Z>", self.redo_action),
(
"Translate Full Document with AI",
"<Control-Shift-KeyPress-T>",
lambda: self.translate_with_ai(selected_only=False),
),
(
"Open Preview in Browser",
"<Control-Shift-KeyPress-O>",
lambda: open_preview_in_browser(self.preview_file, self),
),
("Toggle Dark Mode", "<F6>", self.toggle_dark_mode),
]
self.editor_shortcuts = [
("Bold", "<Control-KeyPress-b>", self.toggle_bold),
("Italic", "<Control-KeyPress-i>", self.toggle_italic),
("Underline", "<Control-KeyPress-u>", self.toggle_underline),
(
"Translate Selected Text with AI",
"<Control-KeyPress-t>",
lambda: self.translate_with_ai(selected_only=True),
),
("Heading 1", "<Control-KeyPress-1>", lambda: self.apply_style("Heading 1")),
("Heading 2", "<Control-KeyPress-2>", lambda: self.apply_style("Heading 2")),
("Heading 3", "<Control-KeyPress-3>", lambda: self.apply_style("Heading 3")),
("Normal Text", "<Control-KeyPress-0>", lambda: self.apply_style("Normal text")),
("Insert Table", "<Control-Alt-KeyPress-t>", self.insert_table),
("Export to HTML", "<Control-Alt-KeyPress-h>", self.export_to_html_dialog),
("Export to Word", "<Control-Alt-KeyPress-d>", self.export_to_docx_dialog),
("Export to PDF", "<Control-Alt-KeyPress-p>", self.export_to_pdf_dialog),
]

if sys.platform == "darwin":
self.global_shortcuts.extend(
[
("New File", "<Command-KeyPress-n>", self.new_file),
("Open File", "<Command-KeyPress-o>", self.open_file),
("Save File", "<Command-KeyPress-s>", self.save_file),
("Close Current Tab", "<Command-KeyPress-w>", self.close_current_tab),
("Close All Tabs", "<Command-Shift-KeyPress-W>", self.close_all_tabs),
("Undo", "<Command-KeyPress-z>", self.undo_action),
("Redo", "<Command-Shift-KeyPress-Z>", self.redo_action),
(
"Translate Full Document with AI",
"<Command-Shift-KeyPress-T>",
lambda: self.translate_with_ai(selected_only=False),
),
(
"Open Preview in Browser",
"<Command-Shift-KeyPress-O>",
lambda: open_preview_in_browser(self.preview_file, self),
),
]
)
self.editor_shortcuts.extend(
[
("Bold", "<Command-KeyPress-b>", self.toggle_bold),
("Italic", "<Command-KeyPress-i>", self.toggle_italic),
("Underline", "<Command-KeyPress-u>", self.toggle_underline),
(
"Translate Selected Text with AI",
"<Command-KeyPress-t>",
lambda: self.translate_with_ai(selected_only=True),
),
("Heading 1", "<Command-KeyPress-1>", lambda: self.apply_style("Heading 1")),
("Heading 2", "<Command-KeyPress-2>", lambda: self.apply_style("Heading 2")),
("Heading 3", "<Command-KeyPress-3>", lambda: self.apply_style("Heading 3")),
("Normal Text", "<Command-KeyPress-0>", lambda: self.apply_style("Normal text")),
("Insert Table", "<Command-Option-KeyPress-t>", self.insert_table),
("Export to HTML", "<Command-Option-KeyPress-h>", self.export_to_html_dialog),
("Export to Word", "<Command-Option-KeyPress-d>", self.export_to_docx_dialog),
("Export to PDF", "<Command-Option-KeyPress-p>", self.export_to_pdf_dialog),
]
)

self.root.protocol("WM_DELETE_WINDOW", self.quit)

self.create_widgets()
Expand Down Expand Up @@ -229,6 +313,22 @@ def create_widgets(self):
tablemenu.add_command(label="Table Syntax Help", command=self.show_table_help)
menubar.add_cascade(label="Table", menu=tablemenu)

shortcutsmenu = tk.Menu(menubar, tearoff=0)
for action_name, pattern, handler in self.global_shortcuts:
shortcutsmenu.add_command(
label=action_name,
accelerator=self._format_shortcut_pattern(pattern),
command=handler,
)
shortcutsmenu.add_separator()
for action_name, pattern, handler in self.editor_shortcuts:
shortcutsmenu.add_command(
label=action_name,
accelerator=self._format_shortcut_pattern(pattern),
command=handler,
)
menubar.add_cascade(label="Shortcuts", menu=shortcutsmenu)

self.root.config(menu=menubar)

# --- Toolbar ---
Expand Down Expand Up @@ -419,25 +519,63 @@ def create_widgets(self):

def bind_events(self):
"""
Sets up key binds for shortcuts.
Register application-wide keyboard shortcuts.

:return: A None value after all app-wide shortcut bindings are registered.

:raises tk.TclError: If a shortcut binding cannot be registered on the root widget.
"""

# Keyboard shortcuts
self.root.bind_all("<Control-s>", lambda event: self.save_file())
self.root.bind_all("<Command-s>", lambda event: self.save_file())
self.root.bind_all("<Control-z>", lambda event: self.undo_action())
self.root.bind_all("<Control-y>", lambda event: self.redo_action())
self.root.bind_all(
"<Control-Shift-T>",
lambda event: self.translate_with_ai(selected_only=False),
)
self.root.bind_all("<Control-n>", lambda event: self.new_file())
self.root.bind_all("<Command-z>", lambda event: self.undo_action())
self.root.bind_all("<Command-Shift-Z>", lambda event: self.redo_action())
self.root.bind_all(
"<Command-Shift-T>",
lambda event: self.translate_with_ai(selected_only=False),
)
for _shortcut_name, pattern, handler in self.global_shortcuts:
self.root.bind_all(
pattern,
lambda event, handler=handler: (handler(), "break")[1],
)

def _format_shortcut_pattern(self, pattern):
"""
Converts a Tk key binding pattern into a human-readable shortcut label.

:param str pattern: The Tk event pattern to format.

:return: A str containing the formatted shortcut label, or an empty string when no pattern is provided.

:raises AttributeError: If a non-string pattern is provided and does not support string operations.
"""

if not pattern:
return ""

cleaned = pattern.strip("<>")
parts = [part for part in cleaned.split("-") if part]
pretty_parts = []

for part in parts:
lowered = part.lower()

if lowered == "keypress":
continue
if lowered == "control":
pretty_parts.append("Ctrl")
continue
if lowered == "command":
pretty_parts.append("Cmd")
continue
if lowered in ("option", "alt"):
pretty_parts.append("Alt")
continue
if lowered == "shift":
pretty_parts.append("Shift")
continue

if part.startswith("F") and part[1:].isdigit():
pretty_parts.append(part)
elif len(part) == 1:
pretty_parts.append(part.upper())
else:
pretty_parts.append(part)

return "+".join(pretty_parts)

def _on_drop_files(self, event):
"""
Expand All @@ -462,7 +600,11 @@ def _on_drop_files(self, event):

def new_file(self):
"""
Handles opening a new file and deals with relevant IME files.
Create a new editor tab and register editor-scoped behavior.

:return: A None value after the new editor tab is created and configured.

:raises tk.TclError: If the editor widget or tab cannot be created.
"""

frame = tk.Frame(self.notebook)
Expand All @@ -480,6 +622,12 @@ def new_file(self):
text_area.bind("<B2-Motion>", self._on_middle_drag)
text_area.bind("<ButtonRelease-2>", self._on_middle_release)

for _shortcut_name, pattern, handler in self.editor_shortcuts:
text_area.bind(
pattern,
lambda event, handler=handler: (handler(), "break")[1],
)

# Setup IME interception
wid = str(text_area)
self._ime_states[wid] = {"active": False}
Expand Down