diff --git a/README.md b/README.md index b400659..28cff40 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Author: Devansh Tayal Github: https://github.com/Devansh2005 - ========================================================================= # DESCRIPTION @@ -24,6 +23,7 @@ gtts ==2.2.1 pyttsx3==2.90 SpeechRecognition==3.8.1 PyAudio==0.2.11 +reportlab==4.4.10 ``` Then run it by double clicking on `dpad.py`! @@ -35,14 +35,19 @@ Then run it by double clicking on `dpad.py`! - Various themes are available. - All required font styles and colours. -- Text to speech - Text to speech (Beta). -- Speech to Text +- Speech to Text (Beta). # Editor's Interface Screenshot +## Before: + ![Screenshot](screenshot.png) +## After: + +![Screenshot](screenshot1.png) + ======================================================================= ## ๐Ÿ“Œ Opensource Programs diff --git a/contributors.md b/contributors.md index 9f7fbfa..1e01c40 100644 --- a/contributors.md +++ b/contributors.md @@ -4,3 +4,4 @@ - [**Name:** Abdul Adhil PK, **email:** 1abduladhilpkpe3@gmail.com] (https://www.github.com/adhilcodes) - [**Name:** Pranjal Srivastava, **email:** pranjalsr.biz@gmail.com] (https://github.com/Dernyt-TPE) - [**Name:** Kishan Mehta, **email:** kishanmehta3@gmail.com] (https://github.com/kishan3) +- [**Name:** Szymdows, **email:** szymdows-programming@outlook.com] (https://github.com/Szymdows) \ No newline at end of file diff --git a/dpad.py b/dpad.py index c27d4ba..64a9490 100644 --- a/dpad.py +++ b/dpad.py @@ -1,544 +1,1390 @@ -#Import modules +""" +โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— +โ•‘ ULTIMATE TEXT EDITOR v2.0 โ•‘ +โ•‘ Light-default โ€ข Tag-based rich formatting โ€ข No icon deps โ•‘ +โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +""" + import tkinter as tk -from tkinter import ttk -from tkinter import font, colorchooser, filedialog, messagebox -import os -import pyttsx3 -import speech_recognition as sr -import random - -main_application=tk.Tk() -main_application.geometry('1200x800') -main_application.title("Devansh's Text Editor") - - -###################### main menu ############### ############################ -# ---------&&&&&&&&&&& End main menu ----------------------------------------------- -main_menu= tk.Menu() -#file icons -new_icon= tk.PhotoImage(file="icons2/new.png") -open_icon= tk.PhotoImage(file="icons2/open.png") -save_icon= tk.PhotoImage(file="icons2/save.png") -save_as_icon= tk.PhotoImage(file="icons2/save_as.png") -exit_icon= tk.PhotoImage(file="icons2/exit.png") - -file=tk.Menu(main_menu, tearoff=False) - -## EDIT -#edit Icons -copy_icon=tk.PhotoImage(file="icons2/copy.png") -paste_icon=tk.PhotoImage(file="icons2/paste.png") -cut_icon=tk.PhotoImage(file="icons2/cut.png") -clear_all_icon=tk.PhotoImage(file="icons2/clear_all.png") -find_icon=tk.PhotoImage(file="icons2/find.png") - -edit=tk.Menu(main_menu, tearoff=False) - -### view icon -tool_bar_icon = tk.PhotoImage(file="icons2/tool_bar.png") -status_bar_icon= tk.PhotoImage(file="icons2/status_bar.png") - - -view=tk.Menu(main_menu, tearoff=False) - -###### Color Theme #### -light_default_icon=tk.PhotoImage(file="icons2/light_default.png") -light_plus_icon=tk.PhotoImage(file="icons2/light_plus.png") -dark_icon=tk.PhotoImage(file="icons2/dark.png") -red_icon=tk.PhotoImage(file="icons2/red.png") -monokai_icon=tk.PhotoImage(file="icons2/monokai.png") -night_blue_icon=tk.PhotoImage(file="icons2/night_blue.png") - -color_theme=tk.Menu(main_menu, tearoff=False) - -theme_choice= tk.StringVar() -color_icons = (light_default_icon,light_plus_icon, dark_icon, red_icon, monokai_icon, night_blue_icon) - -color_dict={ - "Light Default": ("#000000", "#ffffff"), - "Light Plus": ("#474747", "#e0e0e0"), - "Dark": ("#c4c4c4", "#2d2d2d"), - "Red": ("#2d2d2d", "#ffe8e8"), - "Monokai": ("#d3b774", "#474747"), - "Night Blue": ("#ededed", "#6b9dc2") +from tkinter import ttk, font as tkfont, colorchooser, filedialog, messagebox, simpledialog +import os, sys, json, re, datetime, threading, random, tempfile + +# โ”€โ”€ optional deps โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +try: + import pyttsx3; HAS_TTS = True +except ImportError: HAS_TTS = False +try: + import speech_recognition as sr; HAS_SR = True +except ImportError: HAS_SR = False +try: + from spellchecker import SpellChecker; HAS_SPELL = True +except ImportError: HAS_SPELL = False +try: + import markdown as md_lib; HAS_MD = True +except ImportError: HAS_MD = False +try: + from reportlab.pdfgen import canvas as rl_canvas + from reportlab.lib.pagesizes import A4; HAS_PDF = True +except ImportError: HAS_PDF = False + +# โ”€โ”€ constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +APP_TITLE = "Ultimate Text Editor" +CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".ute_config.json") +SESSION_FILE = os.path.join(os.path.expanduser("~"), ".ute_session.json") +MAX_RECENT = 12 +AUTOSAVE_MS = 60_000 +FONT_SIZES = [6,8,9,10,11,12,13,14,16,18,20,22,24,28,32,36,40,48,56,64,72] + +# โ”€โ”€ 10 colour themes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +THEMES = { + "Light Default": dict(bg="#ffffff", fg="#1a1a1a", sel_bg="#4a90d9", sel_fg="#ffffff", + ln_bg="#f5f5f5", ln_fg="#aaaaaa", ruler_bg="#ebebeb", + cursor="#1a1a1a", toolbar_bg="#f0f0f0", border="#d0d0d0", + status_bg="#e8e8e8", tag_bg="#fff3b0"), + "Light Warm": dict(bg="#fdf8f0", fg="#2c2416", sel_bg="#c8954a", sel_fg="#ffffff", + ln_bg="#f5ede0", ln_fg="#b89070", ruler_bg="#ede0cc", + cursor="#2c2416", toolbar_bg="#ede0cc", border="#d4b896", + status_bg="#e6d5be", tag_bg="#ffe4b5"), + "Solarized": dict(bg="#fdf6e3", fg="#657b83", sel_bg="#268bd2", sel_fg="#fdf6e3", + ln_bg="#eee8d5", ln_fg="#93a1a1", ruler_bg="#e8e2cf", + cursor="#657b83", toolbar_bg="#eee8d5", border="#ccc4b0", + status_bg="#e8e2cf", tag_bg="#b5e8b5"), + "Dark": dict(bg="#1e1e1e", fg="#d4d4d4", sel_bg="#264f78", sel_fg="#ffffff", + ln_bg="#252526", ln_fg="#5a5a5a", ruler_bg="#1a1a1a", + cursor="#aeafad", toolbar_bg="#2d2d30", border="#3f3f46", + status_bg="#007acc", tag_bg="#3a3a00"), + "Monokai": dict(bg="#272822", fg="#f8f8f2", sel_bg="#49483e", sel_fg="#f8f8f2", + ln_bg="#1e1f1c", ln_fg="#75715e", ruler_bg="#1a1b18", + cursor="#f8f8f0", toolbar_bg="#3e3d32", border="#555544", + status_bg="#75715e", tag_bg="#3d3b00"), + "Night Blue": dict(bg="#1b2333", fg="#cdd6f4", sel_bg="#45475a", sel_fg="#cdd6f4", + ln_bg="#161d2e", ln_fg="#6272a4", ruler_bg="#131929", + cursor="#f5e0dc", toolbar_bg="#1e2640", border="#313244", + status_bg="#313244", tag_bg="#1e3a1e"), + "Nord": dict(bg="#2e3440", fg="#d8dee9", sel_bg="#4c566a", sel_fg="#eceff4", + ln_bg="#272c36", ln_fg="#616e88", ruler_bg="#242933", + cursor="#88c0d0", toolbar_bg="#3b4252", border="#434c5e", + status_bg="#434c5e", tag_bg="#2d4a2d"), + "Dracula": dict(bg="#282a36", fg="#f8f8f2", sel_bg="#44475a", sel_fg="#f8f8f2", + ln_bg="#21222c", ln_fg="#6272a4", ruler_bg="#1e1f29", + cursor="#ff79c6", toolbar_bg="#343746", border="#44475a", + status_bg="#44475a", tag_bg="#3d2020"), + "Forest": dict(bg="#1a2318", fg="#b8d4b0", sel_bg="#3a6b35", sel_fg="#e8f5e8", + ln_bg="#141c13", ln_fg="#4a7a44", ruler_bg="#111910", + cursor="#a8e6a0", toolbar_bg="#223020", border="#2d4a2d", + status_bg="#2d4a2d", tag_bg="#3a1a00"), + "Crimson": dict(bg="#1a0a0a", fg="#f0c8c8", sel_bg="#8b1a1a", sel_fg="#ffffff", + ln_bg="#140606", ln_fg="#8b4444", ruler_bg="#100404", + cursor="#ff8080", toolbar_bg="#200c0c", border="#5a2020", + status_bg="#3a1010", tag_bg="#003a1a"), } - - -#Cascade -main_menu.add_cascade(label="File", menu=file) -main_menu.add_cascade(label="Edit", menu=edit) -main_menu.add_cascade(label="View", menu=view) -main_menu.add_cascade(label="Color Theme", menu=color_theme) - - - -###################### toolbar ############### ############################. - -tool_bar= ttk.Label(main_application) -tool_bar.pack(side=tk.TOP, fill=tk.X) - -# Font Box -font_tuple= tk.font.families() -font_family= tk.StringVar() -font_box= ttk.Combobox(tool_bar, width=30, textvariable=font_family, state="readonly") -font_box["values"]= font_tuple -font_box.current(font_tuple.index("Arial")) -font_box.grid(row=0,column=0,padx=5) - - -# Size Box -size_var=tk.IntVar() -font_size= ttk.Combobox(tool_bar, width=14, textvariable=size_var, state="readonly") -font_size["values"]= tuple(range(8,80,2)) -font_size.current(3) #12 is at index 4 -font_size.grid(row=0, column=1, padx=5) - -# Bold Button.. -bold_icon= tk.PhotoImage(file="icons2/bold.png") -bold_btn= ttk.Button(tool_bar, image=bold_icon) -bold_btn.grid(row=0, column=2, padx=5) - -# Italic Button -italic_icon= tk.PhotoImage(file="icons2/italic.png") -italic_btn = ttk.Button(tool_bar, image=italic_icon) -italic_btn.grid(row=0, column=3, padx=5) - -#underline button -underline_icon= tk.PhotoImage(file="icons2/underline.png") -underline_btn= ttk.Button(tool_bar, image=underline_icon) -underline_btn.grid(row=0, column=4, padx=5) - -# Font color button -font_color_icon= tk.PhotoImage(file="icons2/font_color.png") -font_color_btn= ttk.Button(tool_bar, image=font_color_icon) -font_color_btn.grid(row=0, column=5, padx=5) - -# align left button - -align_left_icon= tk.PhotoImage(file="icons2/align_left.png") -align_left_btn= ttk.Button(tool_bar, image=align_left_icon) -align_left_btn.grid(row=0, column=6, padx=5) - -# align center button -align_center_icon= tk.PhotoImage(file="icons2/align_center.png") -align_center_btn= ttk.Button(tool_bar, image=align_center_icon) -align_center_btn.grid(row=0, column=7, padx=5) - -#align right button -align_right_icon= tk.PhotoImage(file="icons2/align_right.png") -align_right_btn= ttk.Button(tool_bar, image=align_right_icon) -align_right_btn.grid(row=0, column=8, padx=5) - -#read text button -speak_icon = tk.PhotoImage(file="icons2/read.png") -speak_btn = ttk.Button(tool_bar, image=speak_icon, text="Read Text", compound="left") -speak_btn.grid(row=0, column=9, padx=5) - -#talk text button -talk_icon = tk.PhotoImage(file="icons2/speech.png") -talk_btn = ttk.Button(tool_bar, image=talk_icon, text="Speech to Text", compound="left") -talk_btn.grid(row=0, column=10, padx=5) - -# ----------&&&&&&&&&&& End main menu --------------------------------------------- - -###################### text editor ############### ############################ -text_editor=tk.Text(main_application) -text_editor.config(wrap="word", relief=tk.FLAT) - -scroll_bar= tk.Scrollbar(main_application) # to add scroll bar -text_editor.focus_set() # cursor position -scroll_bar.pack(side=tk.RIGHT, fill=tk.Y) #gridding scrollbar -text_editor.pack(fill= tk.BOTH, expand=True) -scroll_bar.config(command=text_editor.yview) -text_editor.config(yscrollcommand=scroll_bar.set) - -#font family and font size functionality -current_font_family= "Arial" -current_font_size = 12 - -def change_font(event=None): - global current_font_family - current_font_family= font_family.get() - text_editor.configure(font=(current_font_family, current_font_size)) - - -def change_font_size(event=None): - global current_font_size - current_font_size= size_var.get() - text_editor.configure(font=(current_font_family, current_font_size)) -font_box.bind("<>", change_font) - -font_size.bind("<>", change_font_size) - -### Buttons functionality - # bold button functionality - -def change_bold(): - text_property=tk.font.Font(font=text_editor["font"]) #dictionary - if text_property.actual()["weight"] == "normal": - text_editor.config(font=(current_font_family, current_font_size, "bold")) - if text_property.actual()["weight"] == "bold": - text_editor.config(font=(current_font_family, current_font_size, "normal")) - -bold_btn.configure(command= change_bold) - -# Italic Button Functionality -def change_italic(): - text_property=tk.font.Font(font=text_editor["font"]) #dictionary - if text_property.actual()["slant"] == "roman": - text_editor.config(font=(current_font_family, current_font_size, "italic")) - if text_property.actual()["slant"] == "italic": - text_editor.config(font=(current_font_family, current_font_size, "normal")) - -italic_btn.configure(command= change_italic) - -# Underline button Functionality -def change_underline(): - text_property=tk.font.Font(font=text_editor["font"]) #dictionary - if text_property.actual()["underline"] == 0: - text_editor.config(font=(current_font_family, current_font_size, "underline")) - if text_property.actual()["underline"] == 1: - text_editor.config(font=(current_font_family, current_font_size, "normal")) - -underline_btn.configure(command= change_underline) - -# Font color Button Functionality - -def change_font_color(): - color_var=tk.colorchooser.askcolor() - text_editor.configure(fg=color_var[1]) - -font_color_btn.configure(command=change_font_color) -### Align Functionality - -### Align Left -def align_left(): - text_content= text_editor.get(1.0, "end") - text_editor.tag_config("left", justify=tk.LEFT) - text_editor.delete(1.0, tk.END) - text_editor.insert(tk.INSERT, text_content, "left") - -align_left_btn.configure(command=align_left) - -### Align Center -def align_center(): - text_content= text_editor.get(1.0, "end") - text_editor.tag_config("center", justify=tk.CENTER) - text_editor.delete(1.0, tk.END) - text_editor.insert(tk.INSERT, text_content, "center") -align_center_btn.configure(command=align_center) - -### Align Right -def align_right(): - text_content= text_editor.get(1.0, "end") - text_editor.tag_config("right", justify=tk.RIGHT) - text_editor.delete(1.0, tk.END) - text_editor.insert(tk.INSERT, text_content, "right") -align_right_btn.configure(command=align_right) - -### Read Text -def read_text(**kwargs): - if 'text' in kwargs: - text = kwargs['text'] - else: - text = text_editor.get(1.0, 'end') # get text content - engine = pyttsx3.init() - engine.say(text) - engine.runAndWait() - engine.stop() -speak_btn.configure(command=read_text) - -### text formatter -def text_formatter(phrase): - interrogatives = ('how', 'why', 'what', 'when', 'who', 'where', 'is', 'do you', "whom", "whose") - capitalized = phrase.capitalize() - if phrase.startswith(interrogatives): - return (f'{capitalized}?') - else: - return (f'{capitalized}.') - -### Speech to text -def take_speech(): - errors=[ - "I don't know what you mean!", - "Excuse me?", - "Can you repeat it please?", - "Say that again please!", - "Sorry I didn't get that" - ] - r = sr.Recognizer() # initialize the listener - m = sr.Microphone() - with m as source: # set listening device to microphone - read_text(text = 'Please say the message you would like to the editor!') - r.pause_threshold = 2 # delay two second from program start before listening - audio= r.listen(source) +DEFAULT_CFG = dict(theme="Light Default", font_family="Georgia", font_size=13, + recent_files=[], autosave=True, wrap=True, + show_linenos=True, show_ruler=True, zoom=0) + +# โ”€โ”€ tag names for inline formatting โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +FMT_BOLD = "fmt_bold" +FMT_ITALIC = "fmt_italic" +FMT_UNDER = "fmt_underline" +FMT_STRIKE = "fmt_strike" + +# โ”€โ”€ config helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +def load_cfg(): + if os.path.exists(CONFIG_FILE): + try: + with open(CONFIG_FILE, "r", encoding="utf-8") as f: + d = json.load(f) + return {**DEFAULT_CFG, **d} + except Exception: + pass + return dict(DEFAULT_CFG) + +def save_cfg(cfg): try: - query = r.recognize_google(audio, language='en-UK') #listen to audio - query = text_formatter(query) + with open(CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(cfg, f, indent=2) except Exception: - error = random.choice(errors) - read_text(text = error) - query = take_speech() - text_editor.insert(tk.INSERT, query, tk.END) - return query -talk_btn.configure(command=take_speech) - -text_editor.configure(font=("Arial", 12)) - - - - -# ---------&&&&&&&&&&& End main menu --------------------------------------------- - - -###################### status bar ############### ############################ BUTTON - -status_bar=ttk.Label(main_application, text= "Status Bar") -status_bar.pack(side=tk.BOTTOM) - -text_changed = False - -def changed(event=None): - global text_changed - if text_editor.edit_modified(): - text_changed = True - words=len(text_editor.get(1.0, "end-1c").split()) - characters = len(text_editor.get(1.0, "end-1c")) - status_bar.config(text=f'Characters:{characters} Words: {words}') - text_editor.edit_modified(False) ## Increase the count of the char and words -text_editor.bind("<>", changed) - -# ---------&&&&&&&&&&& End main menu ------------------------------------------=-- - -# VAriable -url = "" -#new functionality -def new_file(event= None): - global url - url="" - text_editor.delete(1.0, tk.END) - - - -## file commands - -file.add_command(label="New", image=new_icon, compound=tk.LEFT, accelerator="Ctrl+N", command = new_file) -#open functionality -#opening file -def open_file(event=None): - global url - url = filedialog.askopenfilename(initialdir=os.getcwd(), title="Select File", filetypes=(("Text File", "*.txt"),("All Files", "*.*"))) + pass + +def load_sess(): + if os.path.exists(SESSION_FILE): + try: + with open(SESSION_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + pass + return {"tabs": []} + +def save_sess(data): try: - with open(url, "r") as fr: - text_editor.delete(1.0, tk.END) - text_editor.insert(1.0, fr.read()) - except FileNotFoundError: - return - except: - return - main_application.title(os.path.basename(url)) - -file.add_command(label="Open", image=open_icon, compound=tk.LEFT, accelerator="Ctrl+O", command=open_file) - -# Save Functionality -def save_file(event= None): - global url - try: - if url: - content = str(text_editor.get(1.0, tk.END)) - with open(url, "w", encoding="utf-8") as fw: - fw.write(content) + with open(SESSION_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + except Exception: + pass + +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +class EditorTab: + def __init__(self): + self.url = "" + self.frame = None + self.tw = None # tk.Text + self.lc = None # line-number canvas + self.modified = False + self.bookmarks = {} # name โ†’ index str + +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +class UltimateEditor: + + def __init__(self, root: tk.Tk): + self.root = root + self.cfg = load_cfg() + self.tabs = [] # list[EditorTab] + self.active = None # EditorTab | None + + self._ffam = self.cfg["font_family"] + self._fsize = self.cfg["font_size"] + self.cfg.get("zoom", 0) + self._theme = self.cfg["theme"] + + self._wrap = tk.BooleanVar(value=self.cfg["wrap"]) + self._linenos = tk.BooleanVar(value=self.cfg["show_linenos"]) + self._ruler_vis = tk.BooleanVar(value=self.cfg["show_ruler"]) + self._autosave = tk.BooleanVar(value=self.cfg["autosave"]) + self._spell_on = tk.BooleanVar(value=False) + self._show_tb = tk.BooleanVar(value=True) + self._show_sb = tk.BooleanVar(value=True) + self._fullscreen = False + self._md_open = False + self._spell = SpellChecker() if HAS_SPELL else None + + # active formatting state (toggled by buttons; applied on next keypress or selection) + self._fmt_bold = False + self._fmt_italic = False + self._fmt_under = False + self._fmt_strike = False + + root.title(APP_TITLE) + root.geometry("1380x880") + root.minsize(800, 500) + root.protocol("WM_DELETE_WINDOW", self._on_close) + + self._build_ui() + self._apply_theme(self._theme, first=True) + self._restore_session() + if not self.tabs: + self._new_tab() + self._start_autosave() + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # UI BUILD + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def _build_ui(self): + self._build_menu() + self._build_toolbar() + self._build_ruler() + self._build_notebook() + self._build_statusbar() + self._bind_shortcuts() + + # โ”€โ”€ MENU โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _build_menu(self): + mb = tk.Menu(self.root) + self.root.config(menu=mb) + + def add(m, label="", acc="", cmd=None, sep=False, check=None, var=None): + if sep: m.add_separator(); return + if check and var is not None: + m.add_checkbutton(label=label, variable=var, command=cmd); return + m.add_command(label=label, accelerator=acc, command=cmd) + + # File + fm = tk.Menu(mb, tearoff=False) + mb.add_cascade(label="File", menu=fm) + add(fm,"New Tab", "Ctrl+T", self._new_tab) + add(fm,"New Window", "Ctrl+Shift+N", self._new_window) + add(fm,sep=True) + add(fm,"Openโ€ฆ", "Ctrl+O", self._open_file) + self._recent_menu = tk.Menu(fm, tearoff=False) + fm.add_cascade(label="Recent Files", menu=self._recent_menu) + self._rebuild_recent() + add(fm,sep=True) + add(fm,"Save", "Ctrl+S", self._save_file) + add(fm,"Save Asโ€ฆ", "Ctrl+Shift+S", self._save_as) + add(fm,"Save All", "Ctrl+Alt+S", self._save_all) + add(fm,sep=True) + add(fm,"Export as HTMLโ€ฆ", "", self._export_html) + add(fm,"Export as PDFโ€ฆ", "", self._export_pdf) + add(fm,"Printโ€ฆ", "Ctrl+P", self._print_file) + add(fm,sep=True) + add(fm,"Close Tab", "Ctrl+W", self._close_tab) + add(fm,"Exit", "Ctrl+Q", self._on_close) + + # Edit + em = tk.Menu(mb, tearoff=False) + mb.add_cascade(label="Edit", menu=em) + add(em,"Undo", "Ctrl+Z", self._undo) + add(em,"Redo", "Ctrl+Y", self._redo) + add(em,sep=True) + add(em,"Cut", "Ctrl+X", lambda: self._tw() and self._tw().event_generate("<>")) + add(em,"Copy", "Ctrl+C", lambda: self._tw() and self._tw().event_generate("<>")) + add(em,"Paste", "Ctrl+V", lambda: self._tw() and self._tw().event_generate("<>")) + add(em,"Select All", "Ctrl+A", lambda: self._tw() and self._tw().tag_add("sel","1.0","end")) + add(em,"Clear All", "", lambda: self._tw() and self._tw().delete("1.0","end")) + add(em,sep=True) + add(em,"Find & Replaceโ€ฆ", "Ctrl+F", self._find_replace) + add(em,"Go to Lineโ€ฆ", "Ctrl+G", self._goto_line) + add(em,sep=True) + add(em,"Insert Date/Time", "F5", self._insert_datetime) + add(em,"Insert Special Charโ€ฆ","", self._insert_special) + add(em,sep=True) + add(em,"Add Bookmarkโ€ฆ", "", self._add_bookmark) + add(em,"Go to Bookmarkโ€ฆ", "", self._goto_bookmark) + + # Format + fmt = tk.Menu(mb, tearoff=False) + mb.add_cascade(label="Format", menu=fmt) + add(fmt,"Bold", "Ctrl+B", self._toggle_bold) + add(fmt,"Italic", "Ctrl+I", self._toggle_italic) + add(fmt,"Underline", "Ctrl+U", self._toggle_underline) + add(fmt,"Strikethrough", "Ctrl+Shift+K", self._toggle_strike) + add(fmt,sep=True) + add(fmt,"Font Colourโ€ฆ", "", self._font_color) + add(fmt,"Highlight Colourโ€ฆ","", self._highlight_color) + add(fmt,"Remove Formatting","Ctrl+Space", self._clear_formatting) + add(fmt,sep=True) + add(fmt,"Increase Font", "Ctrl+=", self._zoom_in) + add(fmt,"Decrease Font", "Ctrl+-", self._zoom_out) + add(fmt,"Reset Zoom", "Ctrl+0", self._zoom_reset) + add(fmt,sep=True) + add(fmt,"Align Left", "Ctrl+L", self._align_left) + add(fmt,"Align Centre", "Ctrl+E", self._align_center) + add(fmt,"Align Right", "Ctrl+R", self._align_right) + add(fmt,sep=True) + add(fmt,"UPPERCASE", "", lambda: self._change_case("upper")) + add(fmt,"lowercase", "", lambda: self._change_case("lower")) + add(fmt,"Title Case", "", lambda: self._change_case("title")) + + # View + vm = tk.Menu(mb, tearoff=False) + mb.add_cascade(label="View", menu=vm) + vm.add_checkbutton(label="Toolbar", variable=self._show_tb, command=self._toggle_toolbar) + vm.add_checkbutton(label="Status Bar", variable=self._show_sb, command=self._toggle_statusbar) + vm.add_checkbutton(label="Line Numbers",variable=self._linenos, command=self._toggle_linenos) + vm.add_checkbutton(label="Ruler", variable=self._ruler_vis, command=self._toggle_ruler) + vm.add_checkbutton(label="Word Wrap", variable=self._wrap, command=self._toggle_wrap) + vm.add_separator() + add(vm,"Markdown Preview", "Ctrl+Shift+M", self._toggle_md_preview) + add(vm,"Distraction-Free", "F11", self._toggle_fullscreen) + vm.add_separator() + tm = tk.Menu(vm, tearoff=False) + vm.add_cascade(label="Color Theme", menu=tm) + self._theme_var = tk.StringVar(value=self._theme) + for name in THEMES: + tm.add_radiobutton(label=name, variable=self._theme_var, value=name, + command=lambda n=name: self._apply_theme(n)) + + # Tools + tl = tk.Menu(mb, tearoff=False) + mb.add_cascade(label="Tools", menu=tl) + add(tl,"Word Count & Stats","", self._show_stats) + tl.add_checkbutton(label="Spell Check", variable=self._spell_on, command=self._toggle_spell) + tl.add_separator() + add(tl,"Read Aloud", "F7", self._read_text) + add(tl,"Speech to Text", "F8", self._take_speech) + tl.add_separator() + tl.add_checkbutton(label="Auto-save", variable=self._autosave) + add(tl,"Keyboard Shortcutsโ€ฆ","", self._show_shortcuts) + + # โ”€โ”€ TOOLBAR โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _build_toolbar(self): + t = THEMES[self._theme] + self.tb = tk.Frame(self.root, bd=0, relief="flat") + self.tb.pack(side=tk.TOP, fill=tk.X) + + # ---- row 1: font controls + formatting ---- + r1 = tk.Frame(self.tb) + r1.pack(side=tk.TOP, fill=tk.X, padx=4, pady=2) + + # Font family + families = sorted(set(tkfont.families())) + self._font_var = tk.StringVar(value=self._ffam) + self._font_box = ttk.Combobox(r1, textvariable=self._font_var, + values=families, width=26, state="readonly") + self._font_box.pack(side=tk.LEFT, padx=(0,4)) + self._font_box.bind("<>", self._on_font_change) + + # Font size + self._size_var = tk.IntVar(value=self._fsize) + self._size_box = ttk.Combobox(r1, textvariable=self._size_var, + values=FONT_SIZES, width=5, state="normal") + self._size_box.pack(side=tk.LEFT, padx=(0,6)) + self._size_box.bind("<>", self._on_size_change) + self._size_box.bind("", self._on_size_change) + + self._sep(r1) + + # Formatting toggle buttons โ€” we track their visual state + self._btn_bold = self._tbtn(r1, " B ", self._toggle_bold, tip="Bold (Ctrl+B)", font_kw={"weight":"bold"}) + self._btn_italic = self._tbtn(r1, " I ", self._toggle_italic, tip="Italic (Ctrl+I)", font_kw={"slant":"italic"}) + self._btn_under = self._tbtn(r1, " U ", self._toggle_underline, tip="Underline (Ctrl+U)") + self._btn_strike = self._tbtn(r1, " S ", self._toggle_strike, tip="Strikethrough") + self._sep(r1) + self._tbtn(r1, " Font Colour ", self._font_color) + self._tbtn(r1, " Highlight ", self._highlight_color) + self._tbtn(r1, " Clear Fmt ", self._clear_formatting, tip="Remove formatting (Ctrl+Space)") + self._sep(r1) + self._tbtn(r1, " โ† Left ", self._align_left) + self._tbtn(r1, " โ‰ก Centre ", self._align_center) + self._tbtn(r1, " Right โ†’ ", self._align_right) + self._sep(r1) + self._tbtn(r1, " A+ ", self._zoom_in, tip="Zoom In (Ctrl+=)") + self._tbtn(r1, " Aโˆ’ ", self._zoom_out, tip="Zoom Out (Ctrl+-)") + self._sep(r1) + self._tbtn(r1, " โ†ฉ Undo ", self._undo) + self._tbtn(r1, " โ†ช Redo ", self._redo) + self._sep(r1) + self._tbtn(r1, " ๐Ÿ” Find ", self._find_replace) + self._tbtn(r1, " ๐Ÿ“… Date ", self._insert_datetime) + self._tbtn(r1, " ๐Ÿ”Š Read ", self._read_text) + self._tbtn(r1, " ๐ŸŽค Speak ", self._take_speech) + + # live stats on right + self._stats_lbl = tk.Label(r1, text="", font=("Segoe UI", 9)) + self._stats_lbl.pack(side=tk.RIGHT, padx=10) + + def _tbtn(self, parent, text, cmd, tip="", font_kw=None): + """Create a flat toolbar button with optional bold/italic label styling.""" + f = ("Segoe UI", 9) + if font_kw: + f = tkfont.Font(family="Segoe UI", size=9, **font_kw) + b = tk.Button(parent, text=text, command=cmd, + relief="flat", bd=1, padx=4, pady=2, + font=f) + b.pack(side=tk.LEFT, padx=1) + # Hover highlight effect + b.bind("", lambda e: b.config(relief="groove")) + b.bind("", lambda e: b.config(relief="flat")) + if tip: + self._tooltip(b, tip) + return b + + def _sep(self, parent): + tk.Frame(parent, width=1, bg="#cccccc").pack(side=tk.LEFT, fill=tk.Y, padx=4, pady=2) + + def _tooltip(self, widget, text): + tip_win = [None] + def show(e): + if tip_win[0] and tip_win[0].winfo_exists(): + return + tw = tk.Toplevel(widget) + tw.wm_overrideredirect(True) + tw.wm_geometry(f"+{e.x_root+12}+{e.y_root+22}") + tk.Label(tw, text=text, bg="#ffffcc", fg="#000000", + relief="solid", bd=1, font=("Segoe UI", 8), + padx=4, pady=2).pack() + tip_win[0] = tw + def hide(e): + if tip_win[0]: + try: + tip_win[0].destroy() + except Exception: + pass + tip_win[0] = None + widget.bind("", show) + widget.bind("", hide) + widget.bind("", hide) + + # โ”€โ”€ RULER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _build_ruler(self): + self.ruler_frame = tk.Frame(self.root, height=16) + self.ruler_frame.pack(side=tk.TOP, fill=tk.X) + self.ruler_canvas = tk.Canvas(self.ruler_frame, height=16, highlightthickness=0) + self.ruler_canvas.pack(fill=tk.X, expand=True) + self.ruler_canvas.bind("", lambda e: self._draw_ruler()) + + def _draw_ruler(self): + c = self.ruler_canvas; c.delete("all") + t = THEMES[self._theme] + c.config(bg=t["ruler_bg"]) + w = c.winfo_width() + for i in range(0, w, 8): + if i % 80 == 0: + c.create_line(i,0,i,14, fill=t["ln_fg"]) + c.create_text(i+2,2, text=str(i//8), anchor="nw", + fill=t["ln_fg"], font=("Segoe UI",7)) + elif i % 40 == 0: + c.create_line(i,0,i,9, fill=t["ln_fg"]) + elif i % 8 == 0: + c.create_line(i,0,i,5, fill=t["ln_fg"]) + + # โ”€โ”€ NOTEBOOK โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _build_notebook(self): + self.nb = ttk.Notebook(self.root) + self.nb.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + self.nb.bind("<>", self._on_tab_switch) + self.nb.bind("", lambda e: self._close_tab()) + + # โ”€โ”€ STATUS BAR โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _build_statusbar(self): + self.sb_frame = tk.Frame(self.root, height=22) + self.sb_frame.pack(side=tk.BOTTOM, fill=tk.X) + self.sb_left = tk.Label(self.sb_frame, text="Ready", anchor="w", font=("Segoe UI",9)) + self.sb_left.pack(side=tk.LEFT, padx=8) + self.sb_right = tk.Label(self.sb_frame, text="", anchor="e", font=("Segoe UI",9)) + self.sb_right.pack(side=tk.RIGHT, padx=8) + self.sb_mid = tk.Label(self.sb_frame, text="", font=("Segoe UI",9)) + self.sb_mid.pack(side=tk.RIGHT, padx=20) + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # TAB MANAGEMENT + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def _new_tab(self, url="", content=""): + tab = EditorTab(); tab.url = url + tab.frame = ttk.Frame(self.nb) + title = os.path.basename(url) if url else "Untitled" + self.nb.add(tab.frame, text=title) + + # line-number canvas + tab.lc = tk.Canvas(tab.frame, width=48, highlightthickness=0) + tab.lc.pack(side=tk.LEFT, fill=tk.Y) + + vsb = ttk.Scrollbar(tab.frame, orient="vertical") + vsb.pack(side=tk.RIGHT, fill=tk.Y) + hsb = ttk.Scrollbar(tab.frame, orient="horizontal") + hsb.pack(side=tk.BOTTOM, fill=tk.X) + + tw = tk.Text(tab.frame, undo=True, maxundo=-1, + wrap="word" if self._wrap.get() else "none", + relief="flat", padx=12, pady=10, + font=(self._ffam, self._fsize), + insertwidth=2, spacing3=3) + tw.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + tab.tw = tw + + vsb.config(command=tw.yview) + hsb.config(command=tw.xview) + tw.config(yscrollcommand=lambda *a: (vsb.set(*a), self._update_linenos(tab)), + xscrollcommand=hsb.set) + + # โ”€โ”€ define formatting tags โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + self._init_tags(tw) + + if content: + tw.insert("1.0", content) + tw.edit_reset() + + # bindings + tw.bind("<>", lambda e, t=tab: self._on_modified(t)) + tw.bind("", lambda e, t=tab: self._on_key(t, e)) + tw.bind("", lambda e, t=tab: self._update_pos(t)) + tw.bind("", self._context_menu) + tw.bind("(", lambda e: self._autoclose(e, ")")) + tw.bind("[", lambda e: self._autoclose(e, "]")) + tw.bind("{", lambda e: self._autoclose(e, "}")) + + self.tabs.append(tab) + self.nb.select(tab.frame) + self.active = tab + self._apply_theme_to_tab(tab) + self._update_linenos(tab) + tw.focus_set() + return tab + + def _init_tags(self, tw): + """Define all rich-text tags on a text widget.""" + b_font = tkfont.Font(family=self._ffam, size=self._fsize, weight="bold") + i_font = tkfont.Font(family=self._ffam, size=self._fsize, slant="italic") + bi_font = tkfont.Font(family=self._ffam, size=self._fsize, weight="bold", slant="italic") + n_font = tkfont.Font(family=self._ffam, size=self._fsize) + + tw.tag_configure(FMT_BOLD, font=b_font) + tw.tag_configure(FMT_ITALIC, font=i_font) + tw.tag_configure(FMT_UNDER, underline=True) + tw.tag_configure(FMT_STRIKE, overstrike=True) + tw.tag_configure("fmt_bold_italic", font=bi_font) + tw.tag_configure("match", background="#ffff00", foreground="#000000") + tw.tag_configure("misspell", underline=True, foreground="#cc0000") + # colour tags created dynamically + + def _refresh_tags(self, tw): + """Update font objects inside tags when base font changes.""" + b_font = tkfont.Font(family=self._ffam, size=self._fsize, weight="bold") + i_font = tkfont.Font(family=self._ffam, size=self._fsize, slant="italic") + bi_font = tkfont.Font(family=self._ffam, size=self._fsize, weight="bold", slant="italic") + tw.tag_configure(FMT_BOLD, font=b_font) + tw.tag_configure(FMT_ITALIC, font=i_font) + tw.tag_configure("fmt_bold_italic", font=bi_font) + + def _close_tab(self, event=None): + if len(self.tabs) <= 1: + self._on_close(); return + idx = self.nb.index(self.nb.select()) + tab = self.tabs[idx] + if tab.modified: + ans = messagebox.askyesnocancel("Unsaved changes", + f"'{os.path.basename(tab.url) or 'Untitled'}' has unsaved changes. Save?") + if ans is True: self._save_file(tab=tab) + elif ans is None: return + self.nb.forget(idx) + self.tabs.pop(idx) + if self.tabs: + new = min(idx, len(self.tabs)-1) + self.nb.select(new); self.active = self.tabs[new] + + def _on_tab_switch(self, e=None): + if not self.tabs: return + try: + idx = self.nb.index(self.nb.select()) + if 0 <= idx < len(self.tabs): + self.active = self.tabs[idx] + self._update_pos(self.active) + self._sync_fmt_buttons() + except (tk.TclError, ValueError): + pass + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # INLINE RICH FORMATTING (the core fix) + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def _get_sel_or_word(self): + """Return (start, end) of selection, or current word if nothing selected.""" + tw = self._tw() + if not tw: return None, None + try: + return tw.index("sel.first"), tw.index("sel.last") + except tk.TclError: + # no selection โ†’ expand to word boundaries + idx = tw.index("insert") + start = tw.index(f"{idx} wordstart") + end = tw.index(f"{idx} wordend") + # if nothing useful (e.g. whitespace), just use insert position + if tw.get(start, end).strip() == "": + return idx, idx + return start, end + + def _apply_fmt_tag(self, tag_name): + """Toggle a formatting tag on the selection (or current word).""" + tw = self._tw() + if not tw: return + start, end = self._get_sel_or_word() + if not start or start == end: return + + existing = tw.tag_ranges(tag_name) + # check if the entire range is already tagged + covered = False + for i in range(0, len(existing), 2): + ts = str(existing[i]); te = str(existing[i+1]) + if tw.compare(ts,"<=",start) and tw.compare(te,">=",end): + covered = True; break + + if covered: + tw.tag_remove(tag_name, start, end) else: - url = filedialog.asksaveasfile(mode="w",defaultextension =".txt" , filetypes=(("Text File", "*.txt"),("All Files", "*.*"))) - content2= text_editor.get(1.0, tk.END) - url.write(content2) - url.close() - except: - return -file.add_command(label="Save", image=save_icon, compound=tk.LEFT, accelerator="Ctrl+S", command = save_file) - -# SAve AS Functionality -def save_as(event= None): - global url - try: - content = text_editor.get(1.0, tk.END) - url = filedialog.asksaveasfile(mode="w",defaultextension =".txt" , filetypes=(("Text File", "*.txt"),("All Files", "*.*"))) - url.write(content) - url.close() - except: - return -file.add_command(label="Save_As", image=save_as_icon, compound=tk.LEFT, accelerator="Ctrl+Alt+S", command= save_as) -# Exit functionality -def exit_func(event= None): - global url, text_changed #line239 - try: - if text_changed: - mbox= messagebox.askyesnocancel("Hey Wait ! Don't You want to save the File !") # Create a message box - if mbox is True: # Save krne h file :) is true to be used as cancel is also falue value - if url: - content = text_editor.get(1.0, tk.END) - with open(url, "w", encoding= "utf-8") as fw: - fw.write(content) - main_application.destroy() - else: - content2= text_editor.get(1.0, tk.END) - url = filedialog.asksaveasfile(mode="w",defaultextension =".txt" , filetypes=(("Text File", "*.txt"),("All Files", "*.*"))) - url.write(content2) - url.close() - main_application.destroy() - elif mbox is False: - main_application.destroy() + tw.tag_add(tag_name, start, end) + + self._sync_fmt_buttons() + + def _toggle_bold(self, event=None): self._apply_fmt_tag(FMT_BOLD); return "break" + def _toggle_italic(self, event=None): self._apply_fmt_tag(FMT_ITALIC); return "break" + def _toggle_underline(self,event=None):self._apply_fmt_tag(FMT_UNDER); return "break" + def _toggle_strike(self, event=None): self._apply_fmt_tag(FMT_STRIKE); return "break" + + def _clear_formatting(self, event=None): + tw = self._tw() + if not tw: return + start, end = self._get_sel_or_word() + if not start: return + for tag in (FMT_BOLD, FMT_ITALIC, FMT_UNDER, FMT_STRIKE): + tw.tag_remove(tag, start, end) + # remove colour tags + for tag in tw.tag_names(): + if tag.startswith("fc_") or tag.startswith("hl_"): + tw.tag_remove(tag, start, end) + self._sync_fmt_buttons() + + def _font_color(self): + color = colorchooser.askcolor(parent=self.root, title="Font Colour")[1] + if not color: return + tw = self._tw() + if not tw: return + start, end = self._get_sel_or_word() + if not start or start == end: return + tag = f"fc_{color.replace('#','')}" + tw.tag_configure(tag, foreground=color) + tw.tag_add(tag, start, end) + + def _highlight_color(self): + color = colorchooser.askcolor(parent=self.root, title="Highlight Colour")[1] + if not color: return + tw = self._tw() + if not tw: return + start, end = self._get_sel_or_word() + if not start or start == end: return + tag = f"hl_{color.replace('#','')}" + tw.tag_configure(tag, background=color) + tw.tag_add(tag, start, end) + + def _sync_fmt_buttons(self): + """Update toolbar button appearance to reflect formatting at cursor.""" + if not hasattr(self, "_btn_bold"): return # toolbar not yet built + tw = self._tw() + if not tw: return + try: pos = tw.index("insert") + except: return + t = THEMES[self._theme] + + def _lit(btn, active): + btn.config(relief="sunken" if active else "flat", + bg=t["sel_bg"] if active else t["toolbar_bg"], + fg=t["sel_fg"] if active else t["fg"]) + + _lit(self._btn_bold, FMT_BOLD in tw.tag_names(pos)) + _lit(self._btn_italic, FMT_ITALIC in tw.tag_names(pos)) + _lit(self._btn_under, FMT_UNDER in tw.tag_names(pos)) + _lit(self._btn_strike, FMT_STRIKE in tw.tag_names(pos)) + + # โ”€โ”€ ALIGN โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _align_left(self): self._set_align("left") + def _align_center(self): self._set_align("center") + def _align_right(self): self._set_align("right") + def _set_align(self, j): + tw = self._tw() + if not tw: return + try: + ls = tw.index("sel.first linestart") + le = tw.index("sel.last lineend") + except tk.TclError: + ls = tw.index("insert linestart") + le = tw.index("insert lineend") + tw.tag_configure(f"align_{j}", justify=j) + tw.tag_add(f"align_{j}", ls, le) + + # โ”€โ”€ CASE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _change_case(self, mode): + tw = self._tw() + if not tw: return + try: + s,e = tw.index("sel.first"), tw.index("sel.last") + except tk.TclError: + return + text = tw.get(s,e) + tw.delete(s,e); tw.insert(s, getattr(text, mode)()) + + # โ”€โ”€ ZOOM โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _zoom_in(self, e=None): self._zoom(+2) + def _zoom_out(self, e=None): self._zoom(-2) + def _zoom_reset(self,e=None): + self._fsize = self.cfg["font_size"] + self._size_var.set(self._fsize) + self._apply_font_all() + + def _zoom(self, d): + self._fsize = max(6, self._fsize + d) + self._size_var.set(self._fsize) + self._apply_font_all() + + def _on_font_change(self, e=None): + self._ffam = self._font_var.get() + self._apply_font_all() + + def _on_size_change(self, e=None): + try: self._fsize = int(self._size_var.get()) + except: return + self._apply_font_all() + + def _apply_font_all(self): + for tab in self.tabs: + tab.tw.configure(font=(self._ffam, self._fsize)) + self._refresh_tags(tab.tw) + if hasattr(self, "ruler_canvas"): + self._draw_ruler() + + # โ”€โ”€ UNDO / REDO โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _undo(self, e=None): + tw = self._tw() + if tw: + try: tw.edit_undo() + except tk.TclError: pass + + def _redo(self, e=None): + tw = self._tw() + if tw: + try: tw.edit_redo() + except tk.TclError: pass + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # FILE OPERATIONS + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def _new_window(self, e=None): + import subprocess + subprocess.Popen([sys.executable] + sys.argv) + + def _open_file(self, e=None, path=None): + if not path: + path = filedialog.askopenfilename(initialdir=os.getcwd(), + filetypes=[("Text","*.txt"),("Python","*.py"), + ("Markdown","*.md"),("All","*.*")]) + if not path: return + try: + with open(path, "r", encoding="utf-8", errors="replace") as f: + content = f.read() + except Exception as ex: + messagebox.showerror("Error", str(ex)); return + if self.active and not self.active.url and not self._tw().get("1.0","end-1c"): + self.active.url = path + self._tw().delete("1.0","end") + self._tw().insert("1.0", content) + self._tw().edit_reset() + self.nb.tab(self.active.frame, text=os.path.basename(path)) else: - main_application.destroy() - except: - return - -file.add_command(label="Exit", image=exit_icon, compound=tk.LEFT, accelerator="Ctrl+Q", command=exit_func) -## find functionaly in edit command (last option) -def find_func(event =None): - - def find(): - word = find_input.get() - text_editor.tag_remove('match', "1.0", tk.END) - matches =0 - if word: - start_pos = "1.0" - while True: - start_pos = text_editor.search(word, start_pos, stopindex=tk.END) - if not start_pos: - break - end_pos =f"{start_pos}+ {len(word)}c" - text_editor.tag_add("match", start_pos, end_pos) - matches +=1 - start_pos = end_pos - text_editor.tag_config("match", foreground ="yellow", background= "green") - def replace(): - word = find_input.get() - replace_text = replace_input.get() - content= text_editor.get(1.0, tk.END) - - new_content = content.replace(word, replace_text) - text_editor.delete(1.0, tk.END) - text_editor.insert(1.0, new_content) - ## find and replace funcytionality completed - - -# Dialog box for Find and Replace - find_dialogue = tk.Toplevel() - find_dialogue.geometry("375x250+500+200") - find_dialogue.title("Find") - find_dialogue.resizable(0,0) # Can't maximise and minimise - - # Frame - find_frame = ttk.LabelFrame(find_dialogue, text="Find/ Replace") - find_frame.pack(pady=20) - - #labels - text_find_label = ttk.Label(find_frame, text ="Find : ") - text_replace_label = ttk.Label(find_frame, text= "Replace :") - - #entry - - find_input = ttk.Entry(find_frame, width = 30) - replace_input =ttk.Entry(find_frame, width=30) - - #Button - find_button = ttk.Button(find_frame, text ="Find", command= find) - replace_button= ttk.Button(find_frame, text= "Replace", command= replace) - - # Label Grid - - text_find_label.grid(row= 0, column =0, padx=4, pady =4) - text_replace_label.grid(row=1, column=0, padx=4, pady =4) - - #Entry grid - - find_input.grid(row= 0, column=1, padx=4, pady=4) - replace_input.grid(row= 1, column=1, padx=4, pady=4) - - - # Button grid - - find_button.grid(row=2, column =0, padx=8, pady=4) - replace_button.grid(row=2, column=2, padx=8, pady=4) - - find_dialogue.mainloop() - - -## edit command - -edit.add_command(label="Copy", image=copy_icon, compound=tk.LEFT, accelerator="Ctrl+C", command = lambda:text_editor.event_generate("")) -edit.add_command(label="Paste", image=paste_icon, compound=tk.LEFT, accelerator="Ctrl+V",command = lambda:text_editor.event_generate("")) -edit.add_command(label="Cut", image=cut_icon, compound=tk.LEFT, accelerator="Ctrl+X", command = lambda:text_editor.event_generate("")) -edit.add_command(label="Clear All", image=clear_all_icon, compound=tk.LEFT, accelerator="Ctrl+Alt+X", command= lambda: text_editor.delete(1.0, tk.END)) -edit.add_command(label="Find", image=find_icon, compound=tk.LEFT, accelerator="Ctrl+F", command= find_func) - -## view check button commands - -# To see toolbar and status bar -show_statusbar =tk.BooleanVar() -show_statusbar.set(True) - -show_toolbar =tk.BooleanVar() -show_toolbar.set(True) - -# To hide toolbar -def hide_toolbar(): - global show_toolbar - if show_toolbar: - tool_bar.pack_forget() - show_toolbar= False - else: - text_editor.pack_forget() - status_bar.pack_forget() - tool_bar.pack(side=tk.TOP, fill= tk.X) - text_editor.pack(fill= tk.BOTH, expand= True) - status_bar.pack(side=tk.BOTTOM) - show_toolbar = True - -# To hide statusbar -def hide_statusbar(): - global show_statusbar - if show_statusbar: - status_bar.pack_forget() - show_statusbar = False - else: - status_bar.pack(side= tk.BOTTOM) - show_statusbar = True - - - -## View Button functionality Added - - -view.add_checkbutton(label="Tool Bar",onvalue=True,offvalue=False, variable = show_toolbar, image=tool_bar_icon, compound=tk.LEFT, command= hide_toolbar) -view.add_checkbutton(label="Status Bar",onvalue=True, offvalue=False,variable = show_statusbar, image=status_bar_icon, compound=tk.LEFT, command= hide_statusbar) - -## Color Theme commands -def change_theme(): - chosen_theme = theme_choice.get() - color_tuple= color_dict.get(chosen_theme) - fg_color, bg_color = color_tuple[0], color_tuple[1] - text_editor.config(background= bg_color, fg=fg_color) - -count= 0 -for i in color_dict: - color_theme.add_radiobutton(label= i, image=color_icons[count], variable=theme_choice, compound=tk.LEFT, command= change_theme) - count+=1 - - -####################### main menu functionality ############### ############################ -# ---------&&&&&&&&&&& End main menu ------------------------------------------=-- - -main_application.config(menu=main_menu) - -## Binding the NEW Option Shortcut keys and Find func in Edit Option -main_application.bind("", open_file) -main_application.bind("", new_file) -main_application.bind("", save_file) -main_application.bind("", save_as) -main_application.bind("", exit_func) -main_application.bind("", find_func) + tab = self._new_tab(url=path, content=content) + self.nb.tab(tab.frame, text=os.path.basename(path)) + self._add_recent(path) + self.root.title(f"{os.path.basename(path)} โ€” {APP_TITLE}") + + def _save_file(self, e=None, tab=None): + tab = tab or self.active + if not tab: return + if tab.url: + try: + with open(tab.url, "w", encoding="utf-8") as f: + f.write(tab.tw.get("1.0", "end-1c")) + tab.modified = False + self.nb.tab(tab.frame, text=os.path.basename(tab.url)) + self.sb_left.config(text=f"Saved {datetime.datetime.now():%H:%M:%S}") + except Exception as ex: + messagebox.showerror("Save Error", str(ex)) + else: + self._save_as(tab=tab) + + def _save_as(self, e=None, tab=None): + tab = tab or self.active + if not tab: return + path = filedialog.asksaveasfilename(defaultextension=".txt", + filetypes=[("Text","*.txt"),("Markdown","*.md"),("Python","*.py"),("All","*.*")]) + if not path: return + tab.url = path + self._save_file(tab=tab) + self._add_recent(path) + self.root.title(f"{os.path.basename(path)} โ€” {APP_TITLE}") + + def _save_all(self, e=None): + for tab in self.tabs: self._save_file(tab=tab) + + def _export_html(self): + tw = self._tw() + if not tw: return + content = tw.get("1.0","end-1c").replace("<","<").replace(">",">") + path = filedialog.asksaveasfilename(defaultextension=".html", + filetypes=[("HTML","*.html")]) + if not path: return + html = (f"" + f"" + f"
{content}
") + with open(path, "w", encoding="utf-8") as f: + f.write(html) + messagebox.showinfo("Exported", f"Saved as:\n{path}") + + def _export_pdf(self): + if not HAS_PDF: + messagebox.showwarning("reportlab missing","pip install reportlab"); return + tw = self._tw() + if not tw: return + path = filedialog.asksaveasfilename(defaultextension=".pdf", + filetypes=[("PDF","*.pdf")]) + if not path: return + c = rl_canvas.Canvas(path, pagesize=A4) + W, H = A4; margin=50; y=H-60; lh=15 + c.setFont("Courier", 11) + for line in tw.get("1.0","end-1c").split("\n"): + if y < margin: c.showPage(); y=H-60; c.setFont("Courier",11) + c.drawString(margin, y, line[:105]); y -= lh + c.save() + messagebox.showinfo("Exported", f"Saved as:\n{path}") + + def _print_file(self, e=None): + import webbrowser + tw = self._tw() + if not tw: return + content = tw.get("1.0","end-1c").replace("&","&").replace("<","<").replace(">",">") + with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False, encoding="utf-8") as f: + f.write(f"" + f"
{content}
") + path = f.name + # Convert to proper file URL (handles Windows backslashes) + from urllib.request import pathname2url + file_url = "file:///" + pathname2url(path).lstrip("/") + webbrowser.open(file_url) + + # โ”€โ”€ RECENT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _add_recent(self, path): + r = self.cfg.setdefault("recent_files",[]) + if path in r: r.remove(path) + r.insert(0, path) + self.cfg["recent_files"] = r[:MAX_RECENT] + save_cfg(self.cfg); self._rebuild_recent() + + def _rebuild_recent(self): + self._recent_menu.delete(0,"end") + for p in self.cfg.get("recent_files",[]): + self._recent_menu.add_command(label=p, command=lambda x=p: self._open_file(path=x)) + if not self.cfg.get("recent_files"): + self._recent_menu.add_command(label="(empty)", state="disabled") + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # FIND & REPLACE + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def _find_replace(self, e=None): + dlg = tk.Toplevel(self.root) + dlg.title("Find & Replace") + dlg.geometry("500x310+350+200") + dlg.resizable(False, False) + t = THEMES[self._theme] + dlg.config(bg=t["bg"]) + + frm = tk.LabelFrame(dlg, text="Find / Replace", bg=t["bg"], fg=t["fg"], + font=("Segoe UI",10), padx=10, pady=10) + frm.pack(fill="both",expand=True,padx=12,pady=12) + + def lbl(text, row): + tk.Label(frm,text=text,bg=t["bg"],fg=t["fg"], + font=("Segoe UI",10)).grid(row=row,column=0,sticky="e",padx=4,pady=6) + + lbl("Find:", 0); lbl("Replace:", 1) + find_v = tk.StringVar(); repl_v = tk.StringVar() + fe = ttk.Entry(frm,textvariable=find_v,width=34); fe.grid(row=0,column=1,columnspan=2,padx=4,pady=6) + ttk.Entry(frm,textvariable=repl_v,width=34).grid(row=1,column=1,columnspan=2,padx=4,pady=6) + + case_v = tk.BooleanVar(); regex_v = tk.BooleanVar() + ttk.Checkbutton(frm,text="Match case",variable=case_v).grid(row=2,column=0,columnspan=2,sticky="w",padx=4) + ttk.Checkbutton(frm,text="Regex",variable=regex_v).grid(row=2,column=2,sticky="w") + + count_lbl = tk.Label(frm,text="",bg=t["bg"],fg="#0077aa",font=("Segoe UI",9)) + count_lbl.grid(row=3,column=0,columnspan=3,pady=4) + + def do_find(): + tw = self._tw() + if not tw: return + tw.tag_remove("match","1.0","end") + word = find_v.get() + if not word: return + flags = 0 if case_v.get() else re.IGNORECASE + pat = word if regex_v.get() else re.escape(word) + matches = list(re.finditer(pat, tw.get("1.0","end"), flags)) + count_lbl.config(text=f"{len(matches)} match{'es' if len(matches)!=1 else ''}") + for m in matches: + tw.tag_add("match", f"1.0+{m.start()}c", f"1.0+{m.end()}c") + tw.tag_config("match",background="#ffff44",foreground="#000000") + + def do_replace_all(): + tw = self._tw() + if not tw: return + word=find_v.get(); repl=repl_v.get() + if not word: return + flags = 0 if case_v.get() else re.IGNORECASE + pat = word if regex_v.get() else re.escape(word) + content = tw.get("1.0","end-1c") + new, n = re.subn(pat, repl, content, flags=flags) + tw.delete("1.0","end"); tw.insert("1.0",new) + count_lbl.config(text=f"Replaced {n} occurrence{'s' if n!=1 else ''}") + + bf = tk.Frame(frm,bg=t["bg"]); bf.grid(row=4,column=0,columnspan=3,pady=8) + for txt,cmd in [("Find All",do_find),("Replace All",do_replace_all), + ("Close",dlg.destroy)]: + ttk.Button(bf,text=txt,command=cmd).pack(side=tk.LEFT,padx=6) + fe.focus() + + # โ”€โ”€ GO TO LINE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _goto_line(self, e=None): + tw = self._tw() + if not tw: return + line = simpledialog.askinteger("Go to Line","Line number:",parent=self.root) + if line: + tw.see(f"{line}.0"); tw.mark_set("insert",f"{line}.0"); tw.focus_set() + + # โ”€โ”€ INSERT DATE / SPECIAL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _insert_datetime(self, e=None): + tw = self._tw() + if tw: tw.insert("insert", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + + def _insert_special(self): + specials = list("ยฉยฎโ„ขยฐยฑร—รทโˆšโˆžโ‰ โ‰คโ‰ฅโ†โ†’โ†‘โ†“โ˜…โ˜†โ™ โ™ฃโ™ฅโ™ฆฮฑฮฒฮณฮดฮตฮถฮทฮธโ‚ฌยฃยฅยฟยกยงยถโ€ โ€กโ€ขโ€ฆยซยป""''") + dlg = tk.Toplevel(self.root); dlg.title("Special Characters"); dlg.geometry("440x200") + tk.Label(dlg,text="Click to insert:",font=("Segoe UI",10)).pack(pady=6) + g = tk.Frame(dlg); g.pack() + tw = self._tw() + for i,ch in enumerate(specials): + tk.Button(g,text=ch,width=3,command=lambda c=ch: tw and tw.insert("insert",c), + font=("Segoe UI",12),relief="flat",bd=1).grid(row=i//14,column=i%14,padx=1,pady=1) + + # โ”€โ”€ BOOKMARKS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _add_bookmark(self): + tw = self._tw() + if not tw or not self.active: return + name = simpledialog.askstring("Bookmark","Name:",parent=self.root) + if name: + self.active.bookmarks[name] = tw.index("insert") + self.sb_left.config(text=f"Bookmark '{name}' added") + + def _goto_bookmark(self): + if not self.active or not self.active.bookmarks: + messagebox.showinfo("Bookmarks","No bookmarks in this document."); return + dlg = tk.Toplevel(self.root); dlg.title("Go to Bookmark") + dlg.geometry("300x200") + lb = tk.Listbox(dlg,font=("Segoe UI",11)); lb.pack(fill="both",expand=True,padx=10,pady=10) + for n in self.active.bookmarks: lb.insert("end",n) + def go(): + sel = lb.curselection() + if sel: + idx = self.active.bookmarks[lb.get(sel[0])] + self._tw().see(idx); self._tw().mark_set("insert",idx); dlg.destroy() + ttk.Button(dlg,text="Go",command=go).pack(pady=4) + + # โ”€โ”€ STATS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _show_stats(self): + tw = self._tw() + if not tw: return + text = tw.get("1.0","end-1c") + words = len(text.split()) + chars = len(text); chars_ns = len(text.replace(" ","")) + lines = text.count("\n")+1 + sents = len(re.findall(r'[.!?]+',text)) + paras = len([p for p in text.split("\n\n") if p.strip()]) + reading= max(1, words//200) + messagebox.showinfo("Document Statistics", + f"Words {words:,}\n" + f"Characters {chars:,}\n" + f"Chars (no sp) {chars_ns:,}\n" + f"Lines {lines:,}\n" + f"Sentences {sents:,}\n" + f"Paragraphs {paras:,}\n" + f"Est. reading ~{reading} min") + + # โ”€โ”€ SPELL CHECK โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _toggle_spell(self): + if not HAS_SPELL: + messagebox.showwarning("Missing","pip install pyspellchecker") + self._spell_on.set(False); return + tw = self._tw() + if not tw: return + if self._spell_on.get(): + self._run_spell(tw) + else: + tw.tag_remove("misspell","1.0","end") + + def _run_spell(self, tw=None): + tw = tw or self._tw() + if not tw or not self._spell: return + tw.tag_remove("misspell","1.0","end") + text = tw.get("1.0","end-1c") + words_iter = list(re.finditer(r'\b[a-zA-Z]+\b',text)) + bad = self._spell.unknown([m.group() for m in words_iter]) + for m in words_iter: + if m.group().lower() in bad: + tw.tag_add("misspell",f"1.0+{m.start()}c",f"1.0+{m.end()}c") + + # โ”€โ”€ MARKDOWN PREVIEW โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _toggle_md_preview(self, e=None): + if not HAS_MD: + messagebox.showwarning("Missing","pip install markdown"); return + if self._md_open: + if hasattr(self,"_md_win") and self._md_win.winfo_exists(): + self._md_win.destroy() + self._md_open = False; return + self._md_open = True + self._md_win = tk.Toplevel(self.root) + self._md_win.title("Markdown Preview"); self._md_win.geometry("700x600") + self._md_win.protocol("WM_DELETE_WINDOW", lambda: setattr(self,"_md_open",False) or self._md_win.destroy()) + out = tk.Text(self._md_win,wrap="word",padx=12,pady=12,state="disabled") + out.pack(fill="both",expand=True) + def refresh(): + if not self._md_open: return + tw = self._tw() + if tw: + html = md_lib.markdown(tw.get("1.0","end-1c")) + out.config(state="normal"); out.delete("1.0","end") + out.insert("1.0",html); out.config(state="disabled") + self.root.after(2000,refresh) + refresh() + + # โ”€โ”€ TTS / STT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _read_text(self, e=None): + if not HAS_TTS: + messagebox.showwarning("Missing","pip install pyttsx3"); return + tw = self._tw() + text = tw.get("1.0","end-1c") if tw else "" + if not text.strip(): return + def _speak(): + eng = pyttsx3.init(); eng.say(text); eng.runAndWait(); eng.stop() + threading.Thread(target=_speak, daemon=True).start() + + def _take_speech(self, e=None): + if not HAS_SR: + messagebox.showwarning("Missing","pip install SpeechRecognition"); return + def _listen(): + r = sr.Recognizer() + with sr.Microphone() as src: + r.pause_threshold = 2 + try: + audio = r.listen(src, timeout=8) + q = r.recognize_google(audio, language="en-GB") + q = q.capitalize() + ("?" if q.lower().split()[0] in + ("how","why","what","when","who","where","is","do") else ".") + except: q = random.choice(["Didn't catch that.","Please try again."]) + self.root.after(0, lambda: self._tw() and self._tw().insert("insert"," "+q)) + threading.Thread(target=_listen, daemon=True).start() + self.sb_left.config(text="๐ŸŽค Listeningโ€ฆ") + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # VIEW TOGGLES + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def _toggle_toolbar(self): + if self._show_tb.get(): + # Re-pack toolbar at the top, below the menu + self.tb.pack(side=tk.TOP, fill=tk.X) + # Ensure correct stacking order: toolbar โ†’ ruler โ†’ notebook + self.tb.lift(self.root) + if hasattr(self, "ruler_frame"): + self.ruler_frame.lift(self.root) + if hasattr(self, "nb"): + self.nb.lift(self.root) + else: + self.tb.pack_forget() -main_application.mainloop() + def _toggle_statusbar(self): + if self._show_sb.get(): + self.sb_frame.pack(side=tk.BOTTOM, fill=tk.X) + else: + self.sb_frame.pack_forget() + + def _toggle_linenos(self): + for tab in self.tabs: + if self._linenos.get(): + tab.lc.pack(side=tk.LEFT, fill=tk.Y, before=tab.tw) + else: + tab.lc.pack_forget() + + def _toggle_ruler(self): + if self._ruler_vis.get(): + self.ruler_frame.pack(side=tk.TOP, fill=tk.X, before=self.nb) + else: + self.ruler_frame.pack_forget() + + def _toggle_wrap(self): + mode = "word" if self._wrap.get() else "none" + for tab in self.tabs: tab.tw.config(wrap=mode) + + def _toggle_fullscreen(self, e=None): + self._fullscreen = not self._fullscreen + self.root.attributes("-fullscreen", self._fullscreen) + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # THEMING + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def _apply_theme(self, name, first=False): + self._theme = name + t = THEMES.get(name, THEMES["Light Default"]) + + self.root.config(bg=t["bg"]) + + style = ttk.Style() + try: style.theme_use("clam") + except: pass + style.configure(".", background=t["bg"], foreground=t["fg"]) + style.configure("TFrame", background=t["bg"]) + style.configure("TLabel", background=t["bg"], foreground=t["fg"]) + style.configure("TButton", background=t["toolbar_bg"], foreground=t["fg"]) + style.configure("TCombobox", fieldbackground=t["bg"], foreground=t["fg"], + selectbackground=t["sel_bg"]) + style.configure("TNotebook", background=t["bg"]) + style.configure("TNotebook.Tab", background=t["ln_bg"], foreground=t["fg"], + padding=[10,3]) + style.map("TNotebook.Tab", + background=[("selected",t["bg"])], + foreground=[("selected",t["fg"])]) + style.configure("TScrollbar", background=t["ln_bg"], troughcolor=t["bg"]) + style.configure("TCheckbutton", background=t["bg"], foreground=t["fg"]) + style.configure("TRadiobutton", background=t["bg"], foreground=t["fg"]) + style.configure("TLabelframe", background=t["bg"], foreground=t["fg"]) + style.configure("TLabelframe.Label",background=t["bg"], foreground=t["fg"]) + style.configure("TEntry", fieldbackground=t["bg"], foreground=t["fg"]) + + if hasattr(self,"tb"): + self.tb.config(bg=t["toolbar_bg"]) + for w in self.tb.winfo_children(): + self._theme_widget(w, t) + if hasattr(self, "_stats_lbl"): + self._stats_lbl.config(bg=t["toolbar_bg"], fg=t["fg"]) + + if hasattr(self,"sb_frame"): + self.sb_frame.config(bg=t["status_bg"]) + for w in (self.sb_left, self.sb_mid, self.sb_right): + w.config(bg=t["status_bg"], fg=t["fg"]) + + if hasattr(self,"ruler_canvas"): + self._draw_ruler() + + for tab in self.tabs: + self._apply_theme_to_tab(tab) + + if hasattr(self,"_theme_var"): + self._theme_var.set(name) + if not first: + self.cfg["theme"] = name + save_cfg(self.cfg) + self._sync_fmt_buttons() + + def _theme_widget(self, w, t): + cls = w.winfo_class() + try: + if cls == "Frame": + w.config(bg=t["toolbar_bg"]) + elif cls == "Label": + w.config(bg=t["toolbar_bg"], fg=t["fg"]) + elif cls == "Button": + w.config(bg=t["toolbar_bg"], fg=t["fg"], + activebackground=t["sel_bg"], + activeforeground=t["sel_fg"]) + except tk.TclError: + pass + for child in w.winfo_children(): + self._theme_widget(child, t) + + def _apply_theme_to_tab(self, tab): + t = THEMES[self._theme] + tab.tw.config(bg=t["bg"], fg=t["fg"], + insertbackground=t["cursor"], + selectbackground=t["sel_bg"], + selectforeground=t["sel_fg"]) + tab.lc.config(bg=t["ln_bg"]) + self._update_linenos(tab) + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # LINE NUMBERS + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def _update_linenos(self, tab): + if not self._linenos.get(): + return + c = tab.lc + tw = tab.tw + t = THEMES[self._theme] + c.delete("all") + try: + i = tw.index("@0,0") + except tk.TclError: + return + while True: + try: + dline = tw.dlineinfo(i) + except tk.TclError: + break + if dline is None: + break + y = dline[1] + ln = i.split(".")[0] + c.create_text(44, y, anchor="ne", text=ln, + fill=t["ln_fg"], + font=(self._ffam, max(8, self._fsize - 2))) + try: + nxt = tw.index(f"{i}+1line") + except tk.TclError: + break + if nxt == i: + break + i = nxt + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # STATUS / EVENT HANDLERS + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def _update_pos(self, tab): + if not tab or not tab.tw: + return + tw = tab.tw + try: + idx = tw.index("insert") + line, col = idx.split(".") + text = tw.get("1.0", "end-1c") + words = len(text.split()) + chars = len(text) + try: + self.sb_left.config(text=f"Ln {line}, Col {int(col)+1}") + self.sb_mid.config(text=f"{words:,} words ยท {chars:,} chars") + self.sb_right.config(text=f"{self._theme} ยท {self._ffam} {self._fsize}pt") + except Exception: + pass + try: + self._stats_lbl.config( + text=f"Ln {line} Col {int(col)+1} | {words:,} words") + except Exception: + pass + self._sync_fmt_buttons() + except tk.TclError: + pass + + def _on_modified(self, tab): + try: + if not tab.tw.edit_modified(): + return + except tk.TclError: + return + tab.modified = True + title = os.path.basename(tab.url) or "Untitled" + try: + self.nb.tab(tab.frame, text=f"โ— {title}") + except tk.TclError: + pass + tab.tw.edit_modified(False) + self._update_pos(tab) + self._update_linenos(tab) + if self._spell_on.get() and HAS_SPELL: + self.root.after(2000, lambda: self._run_spell(tab.tw)) + + def _on_key(self, tab, e): + self._update_linenos(tab) + self._update_pos(tab) + + # โ”€โ”€ CONTEXT MENU โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _context_menu(self, event): + tw = event.widget + m = tk.Menu(self.root, tearoff=False) + m.add_command(label="Cut", command=lambda: tw.event_generate("<>")) + m.add_command(label="Copy", command=lambda: tw.event_generate("<>")) + m.add_command(label="Paste", command=lambda: tw.event_generate("<>")) + m.add_separator() + m.add_command(label="Select All", command=lambda: tw.tag_add("sel","1.0","end")) + m.add_separator() + m.add_command(label="Bold", command=self._toggle_bold) + m.add_command(label="Italic", command=self._toggle_italic) + m.add_command(label="Underline", command=self._toggle_underline) + m.add_command(label="Remove Fmt", command=self._clear_formatting) + m.add_separator() + m.add_command(label="Find & Replaceโ€ฆ", command=self._find_replace) + m.add_command(label="Word Count", command=self._show_stats) + m.post(event.x_root, event.y_root) + + # โ”€โ”€ AUTO-CLOSE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _autoclose(self, e, closing): + e.widget.insert("insert", closing) + e.widget.mark_set("insert","insert-1c") + + # โ”€โ”€ SHORTCUTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _bind_shortcuts(self): + r = self.root + # Plain commands (event arg ignored) + plain = [ + ("", self._new_tab), + ("", self._close_tab), + ("", self._open_file), + ("", self._save_file), + ("", self._save_as), + ("", self._save_all), + ("", self._on_close), + ("", self._find_replace), + ("", self._goto_line), + ("", self._undo), + ("", self._redo), + ("", self._align_left), + ("", self._align_center), + ("", self._align_right), + ("", self._zoom_in), + ("", self._zoom_out), + ("", self._zoom_reset), + ("", self._print_file), + ("", self._insert_datetime), + ("", self._read_text), + ("", self._take_speech), + ("", self._toggle_fullscreen), + ("", self._toggle_md_preview), + ] + for key, cmd in plain: + r.bind(key, lambda e, c=cmd: c()) + # Format shortcuts need to return "break" to stop default behaviour + r.bind("", lambda e: self._toggle_bold()) + r.bind("", lambda e: self._toggle_italic()) + r.bind("", lambda e: self._toggle_underline()) + r.bind("", lambda e: self._toggle_strike()) + r.bind("", lambda e: self._clear_formatting()) + # Tab cycling + r.bind("", lambda e: self._cycle_tabs(1)) + r.bind("", lambda e: self._cycle_tabs(-1)) + + def _cycle_tabs(self, d): + if not self.tabs: return + idx = self.nb.index(self.nb.select()) + self.nb.select((idx+d) % len(self.tabs)) + + def _show_shortcuts(self): + messagebox.showinfo("Keyboard Shortcuts", + "Ctrl+T/W New / Close tab\n" + "Ctrl+O/S Open / Save\n" + "Ctrl+Shift+S Save As\n" + "Ctrl+Z / Y Undo / Redo\n" + "Ctrl+F Find & Replace\n" + "Ctrl+G Go to Line\n" + "Ctrl+B/I/U Bold / Italic / Underline\n" + "Ctrl+K Strikethrough\n" + "Ctrl+Space Remove Formatting\n" + "Ctrl+L/E/R Align Left/Centre/Right\n" + "Ctrl+=/-/0 Zoom In/Out/Reset\n" + "Ctrl+P Print\n" + "F5 Date/Time stamp\n" + "F7 / F8 Read Aloud / Dictate\n" + "F11 Distraction-Free mode\n" + "Ctrl+Tab Cycle tabs\n" + ) + + # โ”€โ”€ AUTOSAVE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _start_autosave(self): + def tick(): + if self._autosave.get(): + for tab in self.tabs: + if tab.modified and tab.url: + self._save_file(tab=tab) + self.root.after(AUTOSAVE_MS, tick) + self.root.after(AUTOSAVE_MS, tick) + + # โ”€โ”€ SESSION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _restore_session(self): + for t in load_sess().get("tabs",[]): + url = t.get("url",""); content = t.get("content","") + if url and os.path.exists(url): + try: + with open(url, "r", encoding="utf-8") as f: + content = f.read() + except Exception: + pass + if url or content: + tab = self._new_tab(url=url, content=content) + if url: self.nb.tab(tab.frame, text=os.path.basename(url)) + + def _save_session(self): + save_sess({"tabs":[{"url":t.url,"content":t.tw.get("1.0","end-1c")[:40000]} + for t in self.tabs]}) + + # โ”€โ”€ CLOSE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _on_close(self): + for tab in self.tabs: + if tab.modified: + name = os.path.basename(tab.url) or "Untitled" + ans = messagebox.askyesnocancel("Unsaved Changes", + f"'{name}' has unsaved changes.\nSave before closing?") + if ans is True: self._save_file(tab=tab) + elif ans is None: return + self._save_session() + self.cfg.update(font_family=self._ffam, font_size=self._fsize, + theme=self._theme, wrap=self._wrap.get(), + show_linenos=self._linenos.get(), autosave=self._autosave.get()) + save_cfg(self.cfg) + self.root.destroy() + + # โ”€โ”€ helper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _tw(self): # -> tk.Text | None + return self.active.tw if self.active else None + +# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +if __name__ == "__main__": + root = tk.Tk() + app = UltimateEditor(root) + root.mainloop() diff --git a/requirements.txt b/requirements.txt index 80061e8..e1d7066 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ pyttsx3==2.90 SpeechRecognition==3.8.1 PyAudio==0.2.11 "platform_version == 0x000001D2206A19D8" +reportlab==4.4.10 diff --git a/screenshot1.png b/screenshot1.png new file mode 100644 index 0000000..d5f0437 Binary files /dev/null and b/screenshot1.png differ