# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.
# Portions copyright 2001-2005 Python Software Foundation. All Rights Reserved.

"""A simple application framework.

Some of this is borrowed from IDLE.

"""

import os
import re

import tkinter as Tkinter
from tkinter import filedialog as tkFileDialog
from tkinter import messagebox as tkMessageBox

from idlelib import window as windowList

class Document(object):

    tentative = False

    def __init__(self, app, filename):
        self.app = app
        self.filename = filename
        self.menubar = Tkinter.Menu(app.root)
        self.top = windowList.ListedToplevel(app.root, menu=self.menubar)
        self.top.wm_protocol("WM_DELETE_WINDOW", self.on_close_window)
        self.create_menubar()
        self.create_structure()
        self.apply_bindings()
        self.bind_callbacks()
        self.set_saved(True)
        windowList.register_callback(self.post_windows_menu)
        self.app.register_document(self.filename, self)
        if os.path.exists(self.filename):
            error = self.read_from_file(filename)
            if not error:
                return
            self.show_error("Read Error", error)
        self.create_empty_document()

    def create_structure(self):
        pass # Override in subclass

    def destroy(self):
        self.app.unregister_document(self.filename)
        windowList.unregister_callback(self.post_windows_menu)
        self.top.destroy()

    def set_saved(self, flag):
        self.saved = flag
        self.set_title()
        if not flag:
            self.tentative = False

    def get_title(self):
        return os.path.basename(self.filename)

    def set_title(self):
        title = self.get_title()
        if not self.saved:
            title = "*%s*" % title
        if not os.path.exists(self.filename):
            title += " (new)"
        title = "%s - %s" % (title, self.app.appName)
        self.top.wm_title(title)

    def wake_up(self):
        self.top.wakeup() # a ListedToplevel method
        self.top.update_idletasks()

    def post_windows_menu(self):
        menu = self.menudict["window"]
        menu.delete(1, "end")
        windowList.add_windows_to_menu(menu)

    def bind_callbacks(self):
        for name in dir(self):
            if name.startswith("on_"):
                event = "<<%s>>" % name[3:].replace("_", "-")
                method = getattr(self, name)
                self.top.bind(event, method)

    def on_open_new_window(self, event):
        self.app.open_document()

    def on_open_window_from_file(self, event):
        self.app.interactive_open(self)

    def on_save_window(self, event):
        if not os.path.exists(self.filename):
            return self.on_save_window_as_file(event)
        self.save_to_file(self.filename, True)

    def on_save_window_as_file(self, event):
        self.app.interactive_save(self, True)

    def on_save_copy_of_window_as_file(self, event):
        self.app.interactive_save(self, False)

    def on_close_all_windows(self, event):
        self.app.close_all()

    def on_close_window(self, event=None):
        if self.request_close() == "cancel":
            return "break"

    def on_help(self, event):
        m = tkMessageBox.Message(
            title="About %s" % self.app.appName,
            message=self.app.appHelp,
            icon=tkMessageBox.INFO,
            type=tkMessageBox.OK,
            parent=self.top)
        m.show()

    def request_close(self):
        reply = self.request_save()
        if reply == "cancel":
            return reply
        self.destroy()
        return "yes"

    def save_to_file(self, filename, saved):
        # XXX Would be nice to do mtime logic like Emacs
        error = self.write_to_file(filename)
        if error:
            self.show_error("Write Error", error)
            if filename == self.filename:
                self.set_saved(False)
            return
        if saved and filename != self.filename:
            self.app.rename_document(self.filename, filename, self)
            self.filename = filename
            self.set_title()
            self.top.update_windowlist_registry(None) # Arg is unused
        if saved:
            self.set_saved(True)

    def request_save(self):
        if self.saved:
            return "yes"
        self.wake_up()
        m = tkMessageBox.Message(
            title="Save On Close",
            message="Do you want to save %s before closing?" % self.filename,
            icon=tkMessageBox.QUESTION,
            type=tkMessageBox.YESNOCANCEL,
            parent=self.top)
        reply = str(m.show())
        if reply == "yes":
            self.on_save_window(None)
            if not self.saved:
                reply = "cancel"
        return reply

    def write_to_file(self, filename):
        pass # Override in subclass; return error message or None

    def read_from_file(self, filename):
        pass # Override in subclass; return error message or None

    def show_error(self, title, error):
        m = tkMessageBox.Message(title=title,
                                 message=error,
                                 icon=tkMessageBox.ERROR,
                                 type=tkMessageBox.OK,
                                 parent=self.top)
        m.show()

    # The menu subsystem is largely copied from IDLE

    # The list of menus present in the menu bar; map menu names to titles
    menu_specs = [
        ("file", "_File"),
        ("edit", "_Edit"),
        ("window", "_Window"),
        ("help", "_Help"),
    ]

    # What's in each menu; map menu names to map from menu entry to event name
    default_menudefs = [
        ("file", [
            ("_New Window", "<<open-new-window>>"),
            ("_Open...", "<<open-window-from-file>>"),
            None,
            ("_Save", "<<save-window>>"),
            ("Save _As...", "<<save-window-as-file>>"),
            ("Save Co_py As...", "<<save-copy-of-window-as-file>>"),
            None,
            ("_Close", "<<close-window>>"),
            ("E_xit", "<<close-all-windows>>"),
            ]),
        ("edit", [
            ("_Undo", "<<undo>>"),
            ("_Redo", "<<redo>>"),
            None,
            ("Cu_t", "<<cut>>"),
            ("_Copy", "<<copy>>"),
            ("_Paste", "<<paste>>"),
            None,
            ("Select _All", "<<select-all>>"),
            None,
            ("_Delete", "<<delete>>"),
            None,
            ("_Find", "<<find-dialog>"),
            ("_Find Next", "<<find-next>>"),
            ]),
        ("help", [
            ("About this application", "<<help>>"),
        ]),
        ]

    # Key bindings; map event name to list of key bindings
    default_keydefs = {
        # File menu
        "<<open-new-window>>": ["<Control-n>", "<Control-N>"],
        "<<open-window-from-file>>": ["<Control-o>", "<Control-O>"],
        "<<save-copy-of-window-as-file>>": ["<Alt-Shift-S>"],
        "<<save-window-as-file>>": ["<Alt-s>"],
        "<<save-window>>": ["<Control-s>", "<Control-S>"],
        "<<close-window>>": ["<Alt-F4>"],
        "<<close-all-windows>>": ["<Control-q>", "<Control-Q>"],
        # Edit menu
        "<<undo>>": ["<Control-z>", "<Control-Z>"],
        "<<redo>>": ["<Control-y>", "<Control-Y>"],
        "<<cut>>": ["<Control-x>", "<Control-X>"],
        "<<copy>>": ["<Control-c>", "<Control-C>"],
        "<<paste>>": ["<Control-v>", "<Control-V>"],
        "<<select-all>>": ["<Control-a>", "<Control-A>"],
        "<<delete>>": ["<Control-d>", "<Control-D>", "<Key-Delete>"],
        "<<find-dialog>>": ["<Control-f>", "<Control-F>"],
        "<<find-next>>": ["<Control-g>", "<Control-G>"],
        # Windows menu
        # Help menu
        "<<help>>": ["<Key-F1>"],
        }

    def create_menubar(self):
        mbar = self.menubar
        self.menudict = menudict = {}
        for name, label in self.menu_specs:
            underline, label = prepstr(label)
            menudict[name] = menu = Tkinter.Menu(mbar, name=name)
            mbar.add_cascade(label=label, menu=menu, underline=underline)
        self.fill_menus()

    def apply_bindings(self, keydefs=None):
        if keydefs is None:
            keydefs = self.default_keydefs
        for event, keylist in keydefs.items():
            if keylist:
                self.top.event_add(event, *keylist)

    def fill_menus(self, menudefs=None, keydefs=None):
        """Add appropriate entries to the menus and submenus

        Menus that are absent or None in self.menudict are ignored.
        """
        if menudefs is None:
            menudefs = self.default_menudefs
        if keydefs is None:
            keydefs = self.default_keydefs
        menudict = self.menudict
        for mname, entrylist in menudefs:
            menu = menudict.get(mname)
            if not menu:
                continue
            for entry in entrylist:
                if not entry:
                    menu.add_separator()
                else:
                    label, eventname = entry
                    checkbutton = (label[:1] == "!")
                    if checkbutton:
                        label = label[1:]
                    underline, label = prepstr(label)
                    accelerator = get_accelerator(keydefs, eventname)
                    def command(eventname=eventname):
                        self.generate_event(eventname)
                    if checkbutton:
                        var = self.get_var_obj(eventname, Tkinter.BooleanVar)
                        menu.add_checkbutton(label=label, underline=underline,
                            command=command, accelerator=accelerator,
                            variable=var)
                    else:
                        menu.add_command(label=label, underline=underline,
                                         command=command,
                                         accelerator=accelerator)

    def generate_event(self, eventname):
        self.top.event_generate(eventname)

class Application(object):

    # Override these in a subclass
    className = "Application"
    appName = "Application" # Help window title
    appHelp = "Application" # Help text

    def documentFactory(self, doc, filename):
        return Document(doc, filename)

    def __init__(self, filenames):
        self.root = Tkinter.Tk(className=self.className)
        self.root.withdraw()
        self.documents = {}
        for filename in filenames:
            self.open_document(filename)
        if not self.documents:
            doc = self.open_document()
            doc.tentative = True

    def mainloop(self):
        self.root.mainloop()

    def destroy(self):
        self.root.destroy()

    new_document_counter = 0

    def open_document(self, filename=None, doc=None):
        if not filename:
            pattern = self.untitled_pattern()
            while True:
                self.new_document_counter += 1
                filename = pattern % self.new_document_counter
                if (filename not in self.documents and
                    not os.path.exists(filename)):
                    break
        else:
            filename = os.path.realpath(filename)
        if filename in self.documents:
            doc = self.documents[filename]
            doc.wake_up()
        elif doc is not None and doc.tentative:
            # doc is "Untitled-N"; reuse it instead of creating a new one
            doc.tentative = False # Only do this once
            error = doc.read_from_file(filename)
            if error:
                doc.show_error("Read Error", error)
            else:
                self.rename_document(doc.filename, filename, doc)
                doc.filename = filename
                doc.set_saved(True)
            return doc
        else:
            doc = self.documentFactory(self, filename)
        return doc

    def untitled_pattern(self):
        """Overridable helper to generate a new 'Untitled' pathname.

        Should return an absolute pathname with exactly one %d in it.

        """
        return os.path.realpath("Untitled-%d" + self.default_extension())

    def default_extension(self):
        """Overridable helper to generate the default extension."""
        return ""

    def register_document(self, filename, doc):
        self.documents[filename] = doc

    def unregister_document(self, filename):
        del self.documents[filename]

    def rename_document(self, oldfn, newfn, doc):
        self.unregister_document(oldfn)
        self.register_document(newfn, doc)

    def close_all(self):
        for filename, doc in self.documents.items():
            reply = doc.request_close()
            if reply == "cancel":
                break

    open_dialog = None
    save_dialog = None

    file_types = [
        ("All files", "*"),
        ]

    # The open and save dialogs are cached to preserve the initial directory.
    # (XXX This would better be done per document?)

    def interactive_open(self, doc):
        if self.open_dialog is None:
            dir = os.path.dirname(doc.filename)
            ext = self.default_extension()
            self.open_dialog = tkFileDialog.Open(master=self.root,
                                                 filetypes=self.file_types,
                                                 initialdir=dir,
                                                 defaultextension=ext)
        filename = self.open_dialog.show()
        if filename:
            self.open_document(filename, doc)

    def interactive_save(self, doc, saved):
        if self.save_dialog is None:
            dir = os.path.dirname(doc.filename)
            self.save_dialog = tkFileDialog.SaveAs(master=self.root,
                                                   filetypes=self.file_types,
                                                   initialdir=dir)
        fn = os.path.basename(doc.filename)
        base, ext = os.path.splitext(fn)
        filename = self.save_dialog.show(initialfile=fn, defaultextension=ext)
        if filename:
            filename = os.path.realpath(filename)
            doc.save_to_file(filename, saved)

# Helper code for fill_menus() above; copied literally from IDLE.

def prepstr(s):
    # Helper to extract the underscore from a string, e.g.
    # prepstr("Co_py") returns (2, "Copy").
    i = s.find("_")
    if i >= 0:
        s = s[:i] + s[i+1:]
    return i, s

keynames = {
 "bracketleft": "[",
 "bracketright": "]",
 "slash": "/",
}

def get_accelerator(keydefs, eventname):
    keylist = keydefs.get(eventname)
    if not keylist:
        return ""
    s = keylist[0]
    s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
    s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
    s = re.sub("Key-", "", s)
    s = re.sub("Cancel", "Ctrl-Break", s)
    s = re.sub("Control-", "Ctrl-", s)
    s = re.sub("-", "+", s)
    s = re.sub("><", " ", s)
    s = re.sub("<", "", s)
    s = re.sub(">", "", s)
    return s
