#!/usr/bin/env python3 # Copyright © 2012-13 Qtrac Ltd. All rights reserved. # This program or module is free software: you can redistribute it # and/or modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. It is provided for # educational purposes and is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. import os import sys import tkinter as tk import tkinter.ttk as ttk import tkinter.filedialog as filedialog import tkinter.font as tkfont import tkinter.messagebox as messagebox Spinbox = ttk.Spinbox if hasattr(ttk, "Spinbox") else tk.Spinbox if __name__ == "__main__": # For stand-alone testing with parallel TkUtil sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import About import Colors import Display import Editor import Find import Options import Preferences import TkUtil import TkUtil.DockManager import TkUtil.Settings import TkUtil.Tooltip from Globals import * class Window(ttk.Frame): def __init__(self, master, filename=None): super().__init__(master, padding=PAD) settings = TkUtil.Settings.Data if settings.get_bool(GENERAL, RESTORE): geometry = settings.get_str(GENERAL, GEOMETRY) if geometry is not None: self.master.geometry(geometry) self.create_variables() self.update_variables() self.create_ui() self.update_ui() self.maybe_load_file(filename) def maybe_load_file(self, filename): settings = TkUtil.Settings.Data self.restoreLastFile = settings.get_bool(GENERAL, RESTORE_LAST_FILE, True) lastFile = settings.get_str(GENERAL, LAST_FILE) if filename is None and self.restoreLastFile: filename = lastFile elif lastFile is not None and os.path.isfile(lastFile): self.update_recent_files(lastFile) if filename is not None and os.path.isfile(filename): self.master.after(100, lambda: self.load(os.path.abspath(filename))) def create_variables(self): settings = TkUtil.Settings.Data self.restore = settings.get_bool(GENERAL, RESTORE, True) self.menuImages = {} self.toolbarImages = {} self.toolbars = [] self.toolbarMenu = None self.dockWindows = [] self.dockWindowMenu = None self.statusText = tk.StringVar() self.fontFamily = tk.StringVar() self.fontPointSize = tk.StringVar() self.bold = tk.BooleanVar() self.italic = tk.BooleanVar() self.alignment = tk.StringVar() self.recentFiles = [] self.findDialog = None self.x = self.y = self.dock = None def update_variables(self): settings = TkUtil.Settings.Data family, pointSize, bold, italic = ("Helvetica", 11, False, False) self.restoreFont = settings.get_bool(FONT, RESTORE, True) if self.restoreFont: family = settings.get_str(FONT, FAMILY, family) pointSize = settings.get_int(FONT, FONT_SIZE, pointSize) bold = settings.get_bool(FONT, BOLD, bold) italic = settings.get_bool(FONT, ITALIC, italic) self.fontFamily.set(family) self.fontPointSize.set(pointSize) self.fontPointSize.trace("w", self.update_font) self.bold.set(bold) self.italic.set(italic) self.recentFiles = [] for i in range(MAX_RECENT_FILES): filename = settings.get_str(RECENT_FILES, "file{}".format(i)) if filename is not None and os.path.exists(filename): self.update_recent_files(filename, False) self.recentFiles.reverse() def create_ui(self): self.create_images() self.create_central_area() self.create_menubar() self.create_toolbars() self.create_dock_areas() self.create_dock_windows() self.create_dock_window_menu() self.create_statusbar() self.create_bindings() self.master.columnconfigure(1, weight=1) self.master.rowconfigure(1, weight=1) self.master.minsize(300, 300) def create_images(self): imagePath = os.path.join(os.path.dirname( os.path.realpath(__file__)), "images") for name in (NEW, OPEN, SAVE, SAVEAS, PREFERENCES, QUIT, UNDO, REDO, COPY, CUT, PASTE, FIND, BOLD, ITALIC, ALIGNLEFT, ALIGNCENTER, ALIGNRIGHT, HELP, ABOUT): filename = os.path.join(imagePath, name + "_16x16.gif") if os.path.exists(filename): self.menuImages[name] = tk.PhotoImage(file=filename) filename = os.path.join(imagePath, name + "_24x24.gif") if os.path.exists(filename): self.toolbarImages[name] = tk.PhotoImage(file=filename) filename = os.path.join(imagePath, "ToolbarMenu_3x24.gif") self.toolbarImages[TOOLBARMENU] = tk.PhotoImage(file=filename) def create_menubar(self): self.menubar = tk.Menu(self.master) self.master.config(menu=self.menubar) self.create_file_menu() self.create_edit_menu() self.create_view_menu() self.create_window_menu() self.create_help_menu() def create_file_menu(self): modifier = TkUtil.menu_modifier() self.fileMenu = tk.Menu(self.menubar, name="apple") self.fileMenu.add_command(label=NEW, underline=0, command=self.new, image=self.menuImages[NEW], compound=tk.LEFT, accelerator=modifier + "+N") self.fileMenu.add_command(label=OPEN + ELLIPSIS, underline=0, command=self.open, image=self.menuImages[OPEN], compound=tk.LEFT, accelerator=modifier + "+O") self.fileMenu.add_cascade(label=OPEN_RECENT, underline=5, image=self.menuImages[OPEN], compound=tk.LEFT) self.fileMenu.add_command(label=SAVE, underline=0, command=self.save, image=self.menuImages[SAVE], compound=tk.LEFT, accelerator=modifier + "+S") self.fileMenu.add_command(label=SAVE_AS + ELLIPSIS, underline=5, command=self.save_as, image=self.menuImages[SAVEAS], compound=tk.LEFT) if TkUtil.mac(): self.master.createcommand("::tk::mac::ShowPreferences", self.preferences) self.master.createcommand("exit", self.close) else: self.fileMenu.add_separator() self.fileMenu.add_command(label=PREFERENCES + ELLIPSIS, underline=0, image=self.menuImages[PREFERENCES], compound=tk.LEFT, command=self.preferences) self.fileMenu.add_separator() self.fileMenu.add_command(label=QUIT, underline=0, command=self.close, compound=tk.LEFT, image=self.menuImages[QUIT], accelerator=modifier + "+Q") self.menubar.add_cascade(label="File", underline=0, menu=self.fileMenu) # NOTE: the Tkinter API doesn't seem to let us check whether redo is # possible (so here we always leave Redo enabled). def create_edit_menu(self): modifier = TkUtil.menu_modifier() self.editMenu = tk.Menu(self.menubar) self.editMenu.add_command(label=UNDO, underline=0, command=self.editor.edit_undo, image=self.menuImages[UNDO], compound=tk.LEFT, accelerator=modifier + "+Z") redo = "+Shift+Z" if TkUtil.windows(): redo = "+Y" self.editMenu.add_command(label=REDO, underline=0, command=self.editor.edit_redo, image=self.menuImages[REDO], compound=tk.LEFT, accelerator=modifier + redo) self.editMenu.add_separator() self.editMenu.add_command(label=COPY, underline=0, command=lambda: self.editor.text.event_generate( "<<Copy>>"), image=self.menuImages[COPY], compound=tk.LEFT, accelerator=modifier + "+C") self.editMenu.add_command(label=CUT, underline=2, command=lambda: self.editor.text.event_generate("<<Cut>>"), image=self.menuImages[CUT], compound=tk.LEFT, accelerator=modifier + "+X") self.editMenu.add_command(label=PASTE, underline=0, command=lambda: self.editor.text.event_generate( "<<Paste>>"), image=self.menuImages[PASTE], compound=tk.LEFT, accelerator=modifier + "+V") self.editMenu.add_separator() self.editMenu.add_command(label=FIND + ELLIPSIS, underline=0, command=self.find, image=self.menuImages[FIND], compound=tk.LEFT, accelerator=modifier + "+F") self.menubar.add_cascade(label="Edit", underline=0, menu=self.editMenu) # Tcl/Tk 8.6 provides access to the native font chooser dialog def create_view_menu(self): modifier = TkUtil.menu_modifier() viewMenu = tk.Menu(self.menubar) viewMenu.add_checkbutton(label=BOLD, underline=0, image=self.menuImages[BOLD], compound=tk.LEFT, variable=self.bold, command=lambda: self.toggle_button(self.boldButton)) viewMenu.add_checkbutton(label=ITALIC, underline=0, image=self.menuImages[ITALIC], compound=tk.LEFT, variable=self.italic, command=lambda: self.toggle_button(self.italicButton)) viewMenu.add_separator() viewMenu.add_radiobutton(label=ALIGN_LEFT, underline=6, image=self.menuImages[ALIGNLEFT], compound=tk.LEFT, variable=self.alignment, value=tk.LEFT, command=self.toggle_alignment) viewMenu.add_radiobutton(label=ALIGN_CENTER, underline=6, image=self.menuImages[ALIGNCENTER], compound=tk.LEFT, variable=self.alignment, value=tk.CENTER, command=self.toggle_alignment) viewMenu.add_radiobutton(label=ALIGN_RIGHT, underline=6, image=self.menuImages[ALIGNRIGHT], compound=tk.LEFT, variable=self.alignment, value=tk.RIGHT, command=self.toggle_alignment) self.menubar.add_cascade(label="View", underline=0, menu=viewMenu) def create_window_menu(self): modifier = TkUtil.menu_modifier() self.windowMenu = tk.Menu(self.menubar, name="window") self.windowToolbarMenu = tk.Menu(self.windowMenu) self.windowMenu.add_cascade(label="Toolbars", underline=0, menu=self.windowToolbarMenu) self.windowMenu.add_cascade(label="Dock Windows", underline=0) self.menubar.add_cascade(label="Window", underline=0, menu=self.windowMenu) def create_help_menu(self): helpMenu = tk.Menu(self.menubar, name="help") if TkUtil.mac(): self.master.createcommand("tkAboutDialog", self.about) self.master.createcommand("::tk::mac::ShowHelp", self.help) else: helpMenu.add_command(label=HELP, underline=0, command=self.help, image=self.menuImages[HELP], compound=tk.LEFT, accelerator="F1") helpMenu.add_command(label=ABOUT, underline=0, command=self.about, image=self.menuImages[ABOUT], compound=tk.LEFT) self.menubar.add_cascade(label=HELP, underline=0, menu=helpMenu) def create_toolbars(self): self.toolbarFrame = ttk.Frame(self.master) self.create_file_toolbar() self.create_edit_toolbar() self.create_view_toolbar() self.create_alignment_toolbar() self.toolbarFrame.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E)) self.master.after(50, self.initialize_toolbars) def initialize_toolbars(self): settings = TkUtil.Settings.Data for toolbar in self.toolbars: # Show them all first to size them toolbar.visible = tk.BooleanVar() toolbar.visible.set(True) self.force_update_toolbars() for toolbar in self.toolbars: # Hide those that should be hidden toolbar.visible.set(settings.get_bool(toolbar.text, VISIBLE, True)) self.windowToolbarMenu.add_checkbutton(label=toolbar.text, underline=toolbar.underline, variable=toolbar.visible, onvalue=True, offvalue=False) toolbar.visible.trace("w", self.force_update_toolbars) self.force_update_toolbars() self.master.bind("<Configure>", self.update_toolbars, "+") def update_toolbars(self, event=None): TkUtil.layout_in_rows(self.toolbarFrame, self.master.winfo_width(), DEFAULT_TOOLBAR_HEIGHT, self.toolbars) def force_update_toolbars(self, *args): TkUtil.layout_in_rows(self.toolbarFrame, self.master.winfo_width(), DEFAULT_TOOLBAR_HEIGHT, self.toolbars, True) def create_file_toolbar(self): settings = TkUtil.Settings.Data self.fileToolbar = ttk.Frame(self.toolbarFrame, relief=tk.RAISED) self.fileToolbar.text = FILE_TOOLBAR self.fileToolbar.underline = 0 menuButton = ttk.Button(self.fileToolbar, text="File Toolbar Menu", image=self.toolbarImages[TOOLBARMENU], command=self.toolbar_menu) TkUtil.bind_context_menu(menuButton, self.toolbar_menu) TkUtil.Tooltip.Tooltip(menuButton, text="File Toolbar Menu") newButton = ttk.Button(self.fileToolbar, text=NEW, image=self.toolbarImages[NEW], command=self.new) TkUtil.Tooltip.Tooltip(newButton, text="New Document") openButton = ttk.Button(self.fileToolbar, text=OPEN, image=self.toolbarImages[OPEN], command=self.open) TkUtil.Tooltip.Tooltip(openButton, text="Open Document") self.saveButton = ttk.Button(self.fileToolbar, text=SAVE, image=self.toolbarImages[SAVE], command=self.save) TkUtil.Tooltip.Tooltip(self.saveButton, text="Save Document") preferencesButton = ttk.Button(self.fileToolbar, text=PREFERENCES, image=self.toolbarImages[PREFERENCES], command=self.preferences) TkUtil.Tooltip.Tooltip(preferencesButton, text=PREFERENCES) TkUtil.add_toolbar_buttons(self.fileToolbar, (menuButton, newButton, openButton, self.saveButton, None, preferencesButton)) self.toolbars.append(self.fileToolbar) def create_edit_toolbar(self): settings = TkUtil.Settings.Data self.editToolbar = ttk.Frame(self.toolbarFrame, relief=tk.RAISED) self.editToolbar.text = EDIT_TOOLBAR self.editToolbar.underline = 0 menuButton = ttk.Button(self.editToolbar, text="Edit Toolbar Menu", image=self.toolbarImages[TOOLBARMENU], command=self.toolbar_menu) TkUtil.bind_context_menu(menuButton, self.toolbar_menu) TkUtil.Tooltip.Tooltip(menuButton, text="Edit Toolbar Menu") self.undoButton = ttk.Button(self.editToolbar, text=UNDO, image=self.toolbarImages[UNDO], command=self.editor.edit_undo) TkUtil.Tooltip.Tooltip(self.undoButton, text=UNDO) self.redoButton = ttk.Button(self.editToolbar, text=REDO, image=self.toolbarImages[REDO], command=self.editor.edit_redo) TkUtil.Tooltip.Tooltip(self.redoButton, text=REDO) self.copyButton = ttk.Button(self.editToolbar, text=COPY, image=self.toolbarImages[COPY], command=self.editor.text.event_generate("<<Copy>>")) TkUtil.Tooltip.Tooltip(self.copyButton, text=COPY) self.cutButton = ttk.Button(self.editToolbar, text=CUT, image=self.toolbarImages[CUT], command=self.editor.text.event_generate("<<Cut>>")) TkUtil.Tooltip.Tooltip(self.cutButton, text=CUT) self.pasteButton = ttk.Button(self.editToolbar, text=PASTE, image=self.toolbarImages[PASTE], command=self.editor.text.event_generate("<<Paste>>")) TkUtil.Tooltip.Tooltip(self.pasteButton, text=PASTE) self.findButton = ttk.Button(self.editToolbar, text=FIND, image=self.toolbarImages[FIND], command=self.find) TkUtil.Tooltip.Tooltip(self.findButton, text=FIND) TkUtil.add_toolbar_buttons(self.editToolbar, (menuButton, self.undoButton, self.redoButton, None, self.copyButton, self.cutButton, self.pasteButton, None, self.findButton)) self.toolbars.append(self.editToolbar) def create_view_toolbar(self): settings = TkUtil.Settings.Data self.viewToolbar = ttk.Frame(self.toolbarFrame, relief=tk.RAISED) self.viewToolbar.text = FORMAT_TOOLBAR self.viewToolbar.underline = 1 menuButton = ttk.Button(self.viewToolbar, text="Format Toolbar Menu", image=self.toolbarImages[TOOLBARMENU], command=self.toolbar_menu) TkUtil.bind_context_menu(menuButton, self.toolbar_menu) TkUtil.Tooltip.Tooltip(menuButton, text="Format Toolbar Menu") self.fontFamilyCombobox = ttk.Combobox(self.viewToolbar, width=15, textvariable=self.fontFamily, state="readonly", values=TkUtil.font_families()) self.fontFamilyCombobox.bind("<<ComboboxSelected>>", self.update_font) TkUtil.set_combobox_item(self.fontFamilyCombobox, self.fontFamily.get()) TkUtil.Tooltip.Tooltip(self.fontFamilyCombobox, text="Font Family") self.fontSizeSpinbox = Spinbox(self.viewToolbar, width=2, textvariable=self.fontPointSize, from_=6, to=72, justify=tk.RIGHT, validate="all") self.fontSizeSpinbox.config(validatecommand=( self.fontSizeSpinbox.register(self.validate_int), "fontSizeSpinbox", "%P")) TkUtil.Tooltip.Tooltip(self.fontSizeSpinbox, text="Font Point Size") self.boldButton = ttk.Button(self.viewToolbar, text=BOLD, image=self.toolbarImages[BOLD]) self.boldButton.config( command=lambda: self.toggle_button(self.boldButton, self.bold)) TkUtil.Tooltip.Tooltip(self.boldButton, text=BOLD) self.italicButton = ttk.Button(self.viewToolbar, text=ITALIC, image=self.toolbarImages[ITALIC]) self.italicButton.config( command=lambda: self.toggle_button(self.italicButton, self.italic)) TkUtil.Tooltip.Tooltip(self.italicButton, text=ITALIC) TkUtil.add_toolbar_buttons(self.viewToolbar, (menuButton, self.fontFamilyCombobox, self.fontSizeSpinbox, self.boldButton, self.italicButton)) self.toolbars.append(self.viewToolbar) def validate_int(self, spinbox, number): spinbox = getattr(self, spinbox) return TkUtil.validate_spinbox_int(spinbox, number) def toggle_button(self, button, variable=None): if button.instate((TkUtil.SELECTED,)): button.state((TkUtil.NOT_SELECTED,)) else: button.state((TkUtil.SELECTED,)) if variable is not None: variable.set(not variable.get()) self.update_font() def create_alignment_toolbar(self): settings = TkUtil.Settings.Data self.alignmentToolbar = ttk.Frame(self.toolbarFrame, relief=tk.RAISED) self.alignmentToolbar.text = ALIGNMENT_TOOLBAR self.alignmentToolbar.underline = 0 menuButton = ttk.Button(self.alignmentToolbar, text="Alignment Toolbar Menu", image=self.toolbarImages[TOOLBARMENU], command=self.toolbar_menu) TkUtil.bind_context_menu(menuButton, self.toolbar_menu) TkUtil.Tooltip.Tooltip(menuButton, text="Alignment Toolbar Menu") self.leftButton = ttk.Button(self.alignmentToolbar, text=ALIGN_LEFT, image=self.toolbarImages[ALIGNLEFT]) self.leftButton.config( command=lambda: self.toggle_alignment(tk.LEFT)) TkUtil.Tooltip.Tooltip(self.leftButton, text=ALIGN_LEFT) self.centerButton = ttk.Button(self.alignmentToolbar, text=ALIGN_CENTER, image=self.toolbarImages[ALIGNCENTER]) self.centerButton.config( command=lambda: self.toggle_alignment(tk.CENTER)) TkUtil.Tooltip.Tooltip(self.centerButton, text=ALIGN_CENTER) self.rightButton = ttk.Button(self.alignmentToolbar, text=ALIGN_RIGHT, image=self.toolbarImages[ALIGNRIGHT]) self.rightButton.config( command=lambda: self.toggle_alignment(tk.RIGHT)) TkUtil.Tooltip.Tooltip(self.rightButton, text=ALIGN_RIGHT) TkUtil.add_toolbar_buttons(self.alignmentToolbar, (menuButton, self.leftButton, self.centerButton, self.rightButton)) self.toolbars.append(self.alignmentToolbar) self.leftButton.state((TkUtil.SELECTED,)) self.alignment.set(tk.LEFT) def toggle_alignment(self, alignment=None): if alignment is None: alignment = self.alignment.get() else: self.alignment.set(alignment) for button, value in ((self.leftButton, tk.LEFT), (self.centerButton, tk.CENTER), (self.rightButton, tk.RIGHT)): if value == alignment: button.state((TkUtil.SELECTED,)) self.editor.align(alignment) else: button.state((TkUtil.NOT_SELECTED,)) def toolbar_menu(self, event=None): if self.toolbarMenu is None: self.toolbarMenu = tk.Menu(self.toolbarMenu) for toolbar in self.toolbars: self.toolbarMenu.add_checkbutton(label=toolbar.text, underline=toolbar.underline, variable=toolbar.visible, onvalue=True, offvalue=False) self.toolbarMenu.tk_popup(self.winfo_pointerx(), self.winfo_pointery()) def create_dock_areas(self): self.leftDockArea = ttk.Frame(self.master) self.rightDockArea = ttk.Frame(self.master) self.leftDockArea.grid(row=1, column=0, sticky=(tk.N, tk.S)) self.leftDockArea.grid_remove() self.rightDockArea.grid(row=1, column=2, sticky=(tk.N, tk.S)) self.rightDockArea.grid_remove() def create_dock_windows(self): self.dockManager = TkUtil.DockManager.DockManager( left=self.leftDockArea, right=self.rightDockArea) self.colorsDock = Colors.Dock(self.master, self.dockManager) self.colorsDock.dock_right() self.dockWindows.append(self.colorsDock) self.colorsDock.bind("<<ForegroundChange>>", lambda *args: self.editor.text.config( foreground=self.colorsDock.foreground)) self.colorsDock.bind("<<BackgroundChange>>", lambda *args: self.editor.text.config( background=self.colorsDock.background)) self.displayDock = Display.Dock(self.master, self.dockManager) self.displayDock.dock_right() self.dockWindows.append(self.displayDock) self.displayDock.bind("<<WordWrapChanged>>", lambda *args: self.editor.text.config(wrap=self.displayDock.word_wrap)) self.displayDock.bind("<<BlockCursorChanged>>", lambda *args: self.editor.text.config( blockcursor=self.displayDock.block_cursor)) self.displayDock.bind("<<LineSpacingChanged>>", lambda *args: self.editor.text.config( spacing3=self.displayDock.line_spacing)) def dock_window_menu(self, event=None): self.dockWindowMenu.tk_popup(self.winfo_pointerx(), self.winfo_pointery()) def create_dock_window_menu(self): self.dockWindowMenu = tk.Menu(self.windowMenu) for dockWindow in self.dockWindows: self.dockWindowMenu.add_checkbutton(label=dockWindow.title, underline=dockWindow.underline, variable=dockWindow.visible, onvalue=True, offvalue=False) self.windowMenu.entryconfigure("Dock Windows", menu=self.dockWindowMenu) def create_central_area(self): self.editor = Editor.Editor(self.master, set_status_text=self.set_status_text, font=self.create_font(), maxundo=0, undo=True, wrap=tk.WORD) self.editor.grid(row=1, column=1, sticky=(tk.N, tk.S, tk.W, tk.E), padx=PAD, pady=PAD) self.editor.text.bind("<<Selection>>", self.on_selection) self.editor.text.bind("<<Modified>>", self.on_modified) self.editor.text.bind("<KeyRelease>", self.on_moved, "+") self.editor.text.bind("<ButtonRelease>", self.on_moved, "+") self.editor.text.focus() def create_statusbar(self): statusBar = ttk.Frame(self.master) statusLabel = ttk.Label(statusBar, textvariable=self.statusText) statusLabel.grid(column=0, row=0, sticky=(tk.W, tk.E)) self.modifiedLabel = ttk.Label(statusBar, relief=tk.SUNKEN, anchor=tk.CENTER) self.modifiedLabel.grid(column=1, row=0, pady=2, padx=1) TkUtil.Tooltip.Tooltip(self.modifiedLabel, text="MOD if the text has unsaved changes") self.positionLabel = ttk.Label(statusBar, relief=tk.SUNKEN, anchor=tk.CENTER) self.positionLabel.grid(column=2, row=0, sticky=(tk.W, tk.E), pady=2, padx=1) TkUtil.Tooltip.Tooltip(self.positionLabel, text="Current line and column position") ttk.Sizegrip(statusBar).grid(row=0, column=4, sticky=(tk.S, tk.E)) statusBar.columnconfigure(0, weight=1) statusBar.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E)) self.set_status_text("Start typing to create a new document or " "click File→Open") def create_bindings(self): modifier = TkUtil.key_modifier() self.master.bind("<{}-f>".format(modifier), self.find) self.master.bind("<{}-n>".format(modifier), self.new) self.master.bind("<{}-o>".format(modifier), self.open) self.master.bind("<{}-q>".format(modifier), self.close) self.master.bind("<{}-s>".format(modifier), self.save) self.master.bind("<F1>", self.help) for key in "fnoqs": # Avoid conflicts self.unbind_class("Text", "<{}-{}>".format(modifier, key)) # Ctrl+C etc. from the edit menu are already supported by the # Text (Editor) widget. TkUtil.bind_context_menu(self.editor.text, self.context_menu) self.master.bind("<ButtonPress-1>", self.clicked, "+") self.master.bind("<B1-Motion>", self.maybe_dragging) self.master.bind("<ButtonRelease-1>", self.released, "+") def update_ui(self): self.on_modified() self.on_selection() self.on_moved() self.update_recent_files_menu() self.boldButton.state((TkUtil.SELECTED if bool(self.bold.get()) else TkUtil.NOT_SELECTED,)) self.italicButton.state((TkUtil.SELECTED if bool(self.italic.get()) else TkUtil.NOT_SELECTED,)) self.update_font() def on_modified(self, event=None): if not hasattr(self, "modifiedLabel"): self.editor.edit_modified(False) return if self.editor.edit_modified(): text, mac, state = "MOD", True, tk.NORMAL else: text, mac, state = "", False, tk.DISABLED self.modifiedLabel.config(text=text) if TkUtil.mac(): self.master.attributes("-modified", mac) self.fileMenu.entryconfigure(SAVE, state=state) self.fileMenu.entryconfigure(SAVE_AS + ELLIPSIS, state=state) self.saveButton.config(state=state) self.editMenu.entryconfigure(UNDO, state=state) self.undoButton.config(state=state) def on_selection(self, event=None): state = (tk.NORMAL if self.editor.text.tag_ranges(tk.SEL) else tk.DISABLED) self.editMenu.entryconfigure(COPY, state=state) self.copyButton.config(state=state) self.editMenu.entryconfigure(CUT, state=state) self.cutButton.config(state=state) def on_moved(self, event=None): state = tk.NORMAL if not self.editor.is_empty() else tk.DISABLED self.editMenu.entryconfigure(FIND + ELLIPSIS, state=state) self.findButton.config(state=state) lineCol = self.editor.index(tk.INSERT).split(".") self.positionLabel.config(text="↓{}→{}".format(lineCol[0], lineCol[1])) def context_menu(self, event): modifier = TkUtil.menu_modifier() menu = tk.Menu(self.master) if self.editor.text.tag_ranges(tk.SEL): menu.add_command(label=COPY, underline=0, command=lambda: self.editor.text.event_generate( "<<Copy>>"), image=self.menuImages[COPY], compound=tk.LEFT, accelerator=modifier + "+C") menu.add_command(label=CUT, underline=2, command=lambda: self.editor.text.event_generate( "<<Cut>>"), image=self.menuImages[CUT], compound=tk.LEFT, accelerator=modifier + "+X") menu.add_command(label=PASTE, underline=0, command=lambda: self.editor.text.event_generate( "<<Paste>>"), image=self.menuImages[PASTE], compound=tk.LEFT, accelerator=modifier + "+V") menu.add_separator() menu.add_checkbutton(label=BOLD, underline=0, image=self.menuImages[BOLD], compound=tk.LEFT, variable=self.bold, command=lambda: self.toggle_button(self.boldButton)) menu.add_checkbutton(label=ITALIC, underline=0, image=self.menuImages[ITALIC], compound=tk.LEFT, variable=self.italic, command=lambda: self.toggle_button(self.italicButton)) menu.add_separator() menu.add_radiobutton(label=ALIGN_LEFT, underline=6, image=self.menuImages[ALIGNLEFT], compound=tk.LEFT, variable=self.alignment, value=tk.LEFT, command=self.toggle_alignment) menu.add_radiobutton(label=ALIGN_CENTER, underline=6, image=self.menuImages[ALIGNCENTER], compound=tk.LEFT, variable=self.alignment, value=tk.CENTER, command=self.toggle_alignment) menu.add_radiobutton(label=ALIGN_RIGHT, underline=6, image=self.menuImages[ALIGNRIGHT], compound=tk.LEFT, variable=self.alignment, value=tk.RIGHT, command=self.toggle_alignment) menu.tk_popup(event.x_root, event.y_root) def update_font(self, *args): self.editor.text.config(font=self.create_font()) def set_status_text(self, text): self.statusText.set(text) self.master.after(STATUS_SHOW_TIME, lambda: self.statusText.set("")) def create_font(self): weight = tkfont.BOLD if int(self.bold.get()) else tkfont.NORMAL slant = tkfont.ITALIC if int(self.italic.get()) else tkfont.ROMAN return tkfont.Font(family=self.fontFamily.get(), size=self.fontPointSize.get(), weight=weight, slant=slant) def new(self, event=None): if self.ok_to_clear(): self.editor.new() self.update_ui() def load(self, filename): if self.ok_to_clear(): if self.editor.load(filename): self.update_recent_files(filename) self.update_ui() def open(self, event=None,): if self.ok_to_clear(): path = "." if self.editor.filename is not None: path = os.path.dirname(self.editor.filename) filename = filedialog.askopenfilename(parent=self, title="{} — {}".format(OPEN, APPNAME), initialdir=path, filetypes=FILE_TYPES) if filename: if self.editor.load(filename): self.update_recent_files(filename) self.update_ui() def save(self, event=None): if self.editor.filename is None: return self.save_as() return self.editor.save() def save_as(self): path = "." if self.editor.filename is not None: path = os.path.dirname(self.editor.filename) filename = filedialog.asksaveasfilename(parent=self, title="{} — {}".format(SAVE_AS, APPNAME), initialdir=path, defaultextension=".txt", filetypes=FILE_TYPES) if filename: self.editor.filename = filename result = self.save() if result: self.update_recent_files(filename) return result return False def ok_to_clear(self): if not self.editor.edit_modified(): return True if messagebox.askyesno( "Unsaved Changes — {}".format(APPNAME), "Save unsaved changes?", parent=self): return self.save() return True def update_recent_files(self, filename, populateMenu=True): if filename not in self.recentFiles: self.recentFiles.insert(0, filename) self.recentFiles = self.recentFiles[:MAX_RECENT_FILES] if populateMenu: self.update_recent_files_menu() def update_recent_files_menu(self): if self.recentFiles: menu = tk.Menu(self.fileMenu) i = 1 for filename in self.recentFiles: if filename != self.editor.filename: menu.add_command(label="{}. {}".format(i, filename), underline=0, command=lambda filename=filename: self.load(filename)) i += 1 self.fileMenu.entryconfigure(OPEN_RECENT, menu=menu) self.fileMenu.entryconfigure(OPEN_RECENT, state=tk.NORMAL if i > 1 else tk.DISABLED) else: self.fileMenu.entryconfigure(OPEN_RECENT, state=tk.DISABLED) def close(self, event=None): if not self.ok_to_clear(): return settings = TkUtil.Settings.Data settings.put(GENERAL, RESTORE, self.restore) if self.restore: settings.put(GENERAL, GEOMETRY, self.master.geometry()) self.save_toolbars() self.save_font() self.save_recent_files() TkUtil.Settings.save() self.quit() def save_toolbars(self): settings = TkUtil.Settings.Data settings.put(FILE_TOOLBAR, VISIBLE, self.fileToolbar.visible.get()) settings.put(EDIT_TOOLBAR, VISIBLE, self.editToolbar.visible.get()) settings.put(FORMAT_TOOLBAR, VISIBLE, self.viewToolbar.visible.get()) settings.put(ALIGNMENT_TOOLBAR, VISIBLE, self.alignmentToolbar.visible.get()) def save_font(self): settings = TkUtil.Settings.Data settings.put(FONT, RESTORE, self.restoreFont) if self.restoreFont: settings.put(FONT, BOLD, self.bold.get()) settings.put(FONT, ITALIC, self.italic.get()) settings.put(FONT, FAMILY, self.fontFamily.get()) settings.put(FONT, FONT_SIZE, self.fontPointSize.get()) def save_recent_files(self): """Make sure the current file is the first in the list if not restoring last file""" settings = TkUtil.Settings.Data settings.put(GENERAL, RESTORE_LAST_FILE, self.restoreLastFile) filename = self.editor.filename if filename is not None and os.path.isfile(filename): try: self.recentFiles.remove(filename) except ValueError: pass if self.restoreLastFile: settings.put(GENERAL, LAST_FILE, filename) else: self.recentFiles.insert(0, filename) else: settings.put(GENERAL, LAST_FILE, "") for i, filename in enumerate(self.recentFiles): settings.put(RECENT_FILES, "file{}".format(i), filename) def preferences(self): settings = TkUtil.Settings.Data blink = settings.get_bool(GENERAL, BLINK, True) options = Options.Options(self.restore, self.restoreFont, self.restoreLastFile, blink) preferences = Preferences.Window(self, options) if preferences.ok: self.restore = options.restoreWindow self.restoreFont = options.restoreFont self.restoreLastFile = options.restoreSession if options.blink != blink: settings.put(GENERAL, BLINK, options.blink) self.editor.text.config(insertofftime=300 if options.blink else 0) self.focus() def find(self, event=None): if self.findDialog is None: self.findDialog = Find.Window(self, self.editor.text) else: self.findDialog.deiconify() # TODO make sure that it repositions correctly def help(self, event=None): paras = [ """{} is a basic plain text editor (using the UTF-8 encoding).""".format( APPNAME), """The purpose is really to show how to create a standard main-window-style application with menus, toolbars, dock windows, etc., as well as showing basic use of the Text widget.""", ] messagebox.showinfo("{} — {}".format(HELP, APPNAME), "\n\n".join([para.replace("\n", " ") for para in paras]), parent=self) def about(self): About.Window(self) def clicked(self, event): for dock in self.dockWindows: if event.widget == dock.dockLabel: self.x = event.x self.y = event.y self.dock = dock return self.x = self.y = self.dock = None def maybe_dragging(self, event): if self.dock is None: return if abs(self.x - event.x) > SHAKE or abs(self.y - event.y) > SHAKE: self.set_drag_cursor() def released(self, event): if self.dock is None: return self.set_drag_cursor(False) if self.winfo_containing(event.x_root, event.y_root) is None: # Dropped outside the window self.dock.undock(event.x_root, event.y_root) else: # Dropped inside the window geometry = TkUtil.geometry_for_str( self.master.winfo_geometry()) middle = geometry.x + (geometry.width // 2) if event.x_root < middle: self.dock.dock_left() else: self.dock.dock_right() self.x = self.y = self.dock = None def set_drag_cursor(self, on=True): if on: cursor = "sizing" # Mac OS X can only use built-in Tk cursors path = os.path.realpath(os.path.join(os.path.dirname(__file__), "images")) if TkUtil.windows(): cwd = None try: cwd = os.getcwd() os.chdir(path) # Paths don't work on Windows 7 self.master.config(cursor="@drag.cur") finally: if cwd is not None: os.chdir(cwd) elif not TkUtil.mac(): # Mask made by http://www.kirsle.net/wizards/xbmask.cgi cursor = ("@" + os.path.join(path, "drag.xbm"), os.path.join(path, "drag_mask.xbm"), "#DF00FF", "white") self.master.config(cursor=cursor) else: self.master.config(cursor="") self.master.update() if __name__ == "__main__": if sys.stdout.isatty(): application = tk.Tk() application.title("Window") TkUtil.Settings.DOMAIN = "Qtrac" TkUtil.Settings.APPNAME = APPNAME TkUtil.Settings.load() window = Window(application) application.protocol("WM_DELETE_WINDOW", window.close) application.mainloop() else: print("Loaded OK")