scrunch-edit/scrunch2.py

971 lines
27 KiB
Python

#!/usr/bin/env python3
# coding=utf-8
#
# Scrunch Edit: a two-pane outliner for Org Mode and Markdown files
# Copyright 2021, 2022 Felix Pleșoianu <https://felix.plesoianu.ro/>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from __future__ import print_function
from __future__ import division
import webbrowser
import os.path
import sys
if sys.version_info.major >= 3:
from tkinter import *
from tkinter import ttk
from tkinter.simpledialog import askstring
from tkinter.messagebox import showinfo, showerror, askyesno
from tkinter.filedialog import askopenfilename, asksaveasfilename
else:
from Tkinter import *
import ttk
from tkSimpleDialog import askstring
from tkMessageBox import showinfo, showerror, askyesno
from tkFileDialog import askopenfilename, asksaveasfilename
about_text = """
A two-pane outliner
Version 2.5 (25 Dec 2022)
MIT License
"""
credits_text = """
Made by No Time To Play
based on knowledge gained
from TkDocs.com
"""
class MarkupParser(object):
def __init__(self, lines, headChar):
self.lines = lines
self.cursor = 0
self.headChar = headChar
self.heading = None
def parseSection(self):
if self.cursor >= len(self.lines):
return ""
body = []
while self.lines[self.cursor][0] != self.headChar:
body.append(self.lines[self.cursor].rstrip('\n'))
self.cursor += 1
if self.cursor >= len(self.lines):
break;
return '\n'.join(body)
def matchHeading(self, level = 0):
if self.cursor >= len(self.lines):
return False
for i in range(level):
if self.lines[self.cursor][i] != self.headChar:
return False
self.heading = self.lines[self.cursor][i + 1:].strip()
self.cursor += 1
return True
def parseMarkup(parser, level = 0):
subnodes = []
while parser.matchHeading(level + 1):
node = {
"text": parser.heading,
"section": parser.parseSection(),
"children": parseMarkup(parser, level + 1)
}
subnodes.append(node)
return subnodes
def writeMarkup(f, outline, headChar, level = 1):
for i in outline:
print(headChar * level, i["text"], file=f)
if i["section"] != "" and i["section"] != "\n":
print(i["section"], file=f)
writeMarkup(f, i["children"], headChar, level + 1)
file_types = [("All files", ".*"),
("Org Mode files", ".org"),
'"Markdown / Gemini files" {.md .gmi}']
node_text = {"": ""}
outline_filename = None
modified = False
editing = "" # Which node we're currently editing, if any.
search = None
term = None
min_font_size = 6
max_font_size = 16
default_font_size = 11
font_size = default_font_size
def add_scrolled_text(parent, w, h):
text = Text(parent, width=w, height=h,
wrap="word", undo=True,
font="Courier " + str(font_size))
scroll = ttk.Scrollbar(
parent, orient=VERTICAL, command=text.yview)
text.pack(side=LEFT, fill="both", expand=TRUE)
text["yscrollcommand"] = scroll.set
scroll.pack(side=RIGHT, fill="y")
return text
def load_text(content, text_box):
text_box.delete('1.0', 'end')
text_box.insert("end", content)
text_box.edit_reset()
text_box.edit_modified(False)
def text_selection(text_box):
if len(text_box.tag_ranges("sel")) > 0:
return text_box.get("sel.first", "sel.last")
else:
return ""
interp = Tcl()
top = Tk()
top.title("Scrunch Edit")
top.option_add('*tearOff', FALSE)
top["padx"] = 4
#top["pady"] = 4
icon_data = """
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAADFBMVEUAAAD/AACAgID///8+Fw++
AAAABHRSTlP/AP//07BylAAAAGZJREFUOMvN01EKwCAMA9Ca3v/OcxsD20azjwnLr09sKFoTsRfA
F5EAN7AhEoABR08/u0LBmXx5CyBdpgD8idqlzpAm+RY4ULugbDN1YSB0oWCcZAd4usxB3OyfgUOB
sM2GRfTvPgAY2wicuMw+DAAAAABJRU5ErkJggg==
"""
app_icon = PhotoImage(data=icon_data)
if sys.version_info.major >= 3:
top.iconphoto("", app_icon)
toolbar = ttk.Frame(top)
workspace = ttk.PanedWindow(top, orient=HORIZONTAL)
tree_pane = ttk.Frame(workspace)
edit_pane = ttk.Frame(workspace)
workspace.add(tree_pane, weight=1)
workspace.add(edit_pane, weight=3)
tree = ttk.Treeview(tree_pane, selectmode="browse", height=20)
tree.heading("#0", text="Sections")
tree_scroll = ttk.Scrollbar(tree_pane, orient=VERTICAL, command=tree.yview)
tree["yscrollcommand"] = tree_scroll.set
editor = add_scrolled_text(edit_pane, 42, 24)
status_line = ttk.Frame(top)
status = ttk.Label(status_line,
text=interp.eval("clock format [clock seconds]"), relief="sunken")
grip = ttk.Sizegrip(status_line)
def load_outline(data, item = ""):
for i in data:
added = tree.insert(item, "end", text=i["text"], open=1)
node_text[added] = i["section"]
load_outline(i["children"], added)
def unload_outline(item = ""):
outline = []
for i in tree.get_children(item):
child = {
"text": tree.item(i, "text"),
"section": node_text[i],
"children": unload_outline(i)
}
outline.append(child)
return outline
def parse_file(f, ext):
if ext == ".md" or ext == ".gmi":
parser = MarkupParser(f.readlines(), '#')
elif ext == ".org":
parser = MarkupParser(f.readlines(), '*')
else:
raise RuntimeError("Unknown format")
preface = parser.parseSection()
children = parseMarkup(parser)
return preface, children
def load_file(full_path):
global modified
fn = os.path.basename(full_path)
name, ext = os.path.splitext(fn)
try:
with open(full_path) as f:
node_text[""], outline = parse_file(f, ext)
tree.delete(*tree.get_children())
load_outline(outline)
load_text(node_text[""], editor)
modified = False
status["text"] = "Opened " + fn
top.title(fn + " | Scrunch Edit")
return True
except RuntimeError as e:
showerror("Error parsing file", str(e), parent=top)
return False
except OSError as e:
showerror("Error opening file", str(e), parent=top)
return False
except IOError as e: # For Python 2.7
showerror("Error opening file", str(e), parent=top)
return False
def write_file(f, ext, data):
if data["preface"] != "" and data["preface"] != "\n":
print(data["preface"], file=f)
if ext == ".md" or ext == ".gmi":
writeMarkup(f, data["outline"], '#')
elif ext == ".org":
writeMarkup(f, data["outline"], '*')
else:
raise RuntimeError("Unknown format")
def save_file(full_path):
if editor.edit_modified():
node_text[editing] = editor.get("1.0", "end").rstrip() + "\n"
editor.edit_modified(False)
fn = os.path.basename(full_path)
name, ext = os.path.splitext(fn)
data = {
"preface": node_text[""],
"outline": unload_outline()
}
try:
with open(full_path, "w") as f:
write_file(f, ext, data)
f.flush()
set_modified(False)
status["text"] = "Saved " + fn
top.title(fn + " | Scrunch Edit")
return True
except RuntimeError as e:
showerror("Error writing file", str(e), parent=top)
return False
except OSError as e:
showerror("Error saving file", str(e), parent=top)
return False
except IOError as e: # For Python 2.7
showerror("Error saving file", str(e), parent=top)
return False
def file_dir():
if outline_filename != None:
return os.path.dirname(outline_filename)
else:
return "."
def all_item_ids(item = ""):
"Return a flat list of item IDs present in the tree."
items = []
for i in tree.get_children(item):
items.append(i)
items.extend(all_item_ids(i))
return items
def start_search(term, items):
term = term.lower()
if term in node_text[""].lower():
yield ""
for i in items:
if not tree.exists(i): # It might have been deleted.
continue
assert i in node_text
if term in tree.item(i, "text").lower():
yield i
elif term in node_text[i].lower():
yield i
def highlight_text(text, start="1.0"):
idx = editor.search(text, start, nocase=True)
if len(idx) > 0:
pos = "{} +{} chars".format(idx, len(text))
editor.tag_remove("sel", "1.0", "end")
editor.tag_add("sel", idx, pos)
editor.mark_set("insert", pos)
editor.see("insert")
editor.focus()
def select_section():
global editing
if editor.edit_modified():
node_text[editing] = editor.get("1.0", "end").rstrip() + "\n"
set_modified()
editing = tree.focus()
load_text(node_text[editing], editor)
if term != None:
highlight_text(term)
def select_preface():
tree.selection_set("")
tree.focus("")
# Don't call select_section() here, it's implied by selection_set()
def set_modified(state=True):
global modified
modified = state
def show_modified():
if modified or editor.edit_modified():
status["text"] = "(modified)"
def handle_new():
global outline_filename
if modified or editor.edit_modified():
answer = askyesno(
title="New outline?",
message="Outline is unsaved.\nStart another?",
icon="question",
parent=top)
else:
answer = True
if answer:
tree.delete(*tree.get_children())
top.title("Scrunch Edit")
editor.delete('1.0', 'end')
outline_filename = None
node_text.clear()
node_text[""] = ""
editor.edit_reset()
editor.edit_modified(False)
set_modified(False)
status["text"] = interp.eval("clock format [clock seconds]")
else:
status["text"] = "New outline canceled."
def handle_open():
global outline_filename
if modified or editor.edit_modified():
do_open = askyesno(
title="Open another outline?",
message="Outline is unsaved.\nOpen another?",
icon="question",
parent=top)
if not do_open:
status["text"] = "Opening canceled."
return
choice = askopenfilename(
title="Open existing outline",
initialdir=file_dir(),
filetypes=file_types,
parent=top)
if len(choice) == 0:
status["text"] = "Opening canceled."
elif not os.path.isfile(choice):
showerror(
"Error opening file",
"File not found: " + choice,
parent=top)
elif load_file(choice):
outline_filename = choice
def handle_save():
if outline_filename == None:
handle_saveas()
else:
save_file(outline_filename)
def handle_saveas():
global outline_filename
choice = asksaveasfilename(
title="Save outline as...",
initialdir=file_dir(),
filetypes=file_types,
parent=top)
if len(choice) == 0:
status["text"] = "Save canceled."
elif save_file(choice):
outline_filename = choice
def handle_reload():
if outline_filename == None:
showinfo("Oops!", "The outline was never saved.", parent=top)
else:
do_open = askyesno(
title="Reload outline?",
message="Reload outline from last save?",
icon="question",
parent=top)
if not do_open:
status["text"] = "Reloading canceled."
else:
load_file(outline_filename)
def show_stats():
section_count = len(node_text)
char_count = sum(len(node_text[i]) for i in node_text)
char_average = int(char_count / section_count)
stats = "Sections: {}\nCharacters: {}\nAverage: {}"
stats = stats.format(section_count, char_count, char_average)
showinfo("File statistics", stats, parent=top)
def handle_quit():
if modified or editor.edit_modified():
do_quit = askyesno(
title="Quit Scrunch Edit?",
message="Outline is unsaved. Quit anyway?",
icon="question",
parent=top)
else:
do_quit = True
if do_quit:
top.destroy()
else:
status["text"] = "Quit canceled."
def cut_content():
top.tk.eval("tk_textCut " + str(editor))
def copy_content():
top.tk.eval("tk_textCopy " + str(editor))
def paste_content():
if len(editor.tag_ranges("sel")) > 0:
editor.delete("sel.first", "sel.last")
top.tk.eval("tk_textPaste " + str(editor))
return "break"
def select_all(widget):
widget.tag_remove("sel", "1.0", "end")
widget.tag_add("sel", "1.0", "end")
return "break"
def handle_find():
global search, term
if search != None:
search.close()
answer = askstring("Find", "Search pattern:", parent=top,
initialvalue=text_selection(editor))
if answer == None:
status["text"] = "Search canceled."
return
search = start_search(answer, all_item_ids())
term = answer
do_find()
def find_again():
if search == None:
handle_find()
else:
do_find()
def do_find():
global search, term
next_result = next(search, None)
if next_result == None:
search.close()
search = None
term = None
status["text"] = "Nothing found."
elif next_result == "":
select_preface()
else:
tree.selection_set(next_result)
tree.focus(next_result)
tree.see(next_result)
def set_up_node(node):
node_text[node] = ""
tree.selection_set(node)
tree.focus(node)
tree.see(node)
set_modified()
show_modified()
def lower_case():
sel = text_selection(editor)
if len(sel) > 0:
editor.replace("sel.first", "sel.last", sel.lower())
else:
status["text"] = "Nothing selected."
return "break"
def title_case():
sel = text_selection(editor)
if len(sel) > 0:
editor.replace("sel.first", "sel.last", sel.title())
else:
status["text"] = "Nothing selected."
return "break"
def upper_case():
sel = text_selection(editor)
if len(sel) > 0:
editor.replace("sel.first", "sel.last", sel.upper())
else:
status["text"] = "Nothing selected."
def join_lines():
text = text_selection(editor).replace("\n", " ")
if len(text) > 0:
editor.replace("sel.first", "sel.last", text)
else:
status["text"] = "Nothing selected."
def open_line():
editor.insert("insert +0 chars", "\n")
editor.mark_set("insert", "insert -1 chars")
return "break"
def add_child():
answer = askstring("Add section", "New section title:", parent=top)
if answer == None:
status["text"] = "Canceled adding section."
else:
focus = tree.focus()
child = tree.insert(focus, "end", text=answer)
set_up_node(child)
return "break"
def insert_after():
answer = askstring("Add section", "New section title:", parent=top)
if answer == None:
status["text"] = "Canceled adding section."
else:
focus = tree.focus()
if focus == "":
parent = ""
position = "end"
else:
parent = tree.parent(focus)
position = tree.index(focus) + 1
child = tree.insert(parent, position, text=answer)
set_up_node(child)
def delete_section():
focus = tree.focus()
if focus == "":
status["text"] = "Can't delete preface."
answer = False
elif node_text[focus] != "" or len(tree.get_children(focus)) > 0:
answer = askyesno(
title="Delete section?",
message="Section isn't empty.\nDelete anyway?",
icon="question",
parent=top)
else:
answer = True
if answer:
tree.selection_set(tree.next(focus))
tree.focus(tree.next(focus))
tree.delete(focus)
del node_text[focus]
set_modified()
show_modified()
else:
status["text"] = "Deletion canceled."
return "break"
def rename_section():
focus = tree.focus()
if focus == "":
status["text"] = "Can't rename preface."
else:
answer = askstring(
"Rename section", "New section title:", parent=top,
initialvalue=tree.item(focus, "text"))
if answer == None:
status["text"] = "Canceled renaming section."
else:
tree.item(focus, text=answer)
set_modified()
show_modified()
def move_left():
focus = tree.focus()
if focus == "":
status["text"] = "Can't move preface."
return
parent = tree.parent(focus)
if parent == "":
status["text"] = "Section is at top level."
return
index = tree.index(parent)
tree.move(focus, tree.parent(parent), index + 1)
set_modified()
show_modified()
def move_down():
focus = tree.focus()
if focus == "":
status["text"] = "Can't move preface."
return
tree.move(focus, tree.parent(focus), tree.index(focus) + 1)
set_modified()
show_modified()
def move_up():
focus = tree.focus()
if focus == "":
status["text"] = "Can't move preface."
return
tree.move(focus, tree.parent(focus), tree.index(focus) - 1)
set_modified()
show_modified()
def move_right():
focus = tree.focus()
if focus == "":
status["text"] = "Can't move preface."
return
prev = tree.prev(focus)
if prev == "":
status["text"] = "No previous section."
return
tree.move(focus, prev, "end")
tree.see(focus)
set_modified()
show_modified()
def fold_all():
for i in tree.get_children():
tree.item(i, open=0)
def unfold_all():
for i in tree.get_children():
tree.item(i, open=1)
def make_font_bigger():
global font_size
if font_size > min_font_size:
font_size += 1
editor.configure(font="Courier " + str(font_size))
def make_font_smaller():
global font_size
if font_size < max_font_size:
font_size -= 1
editor.configure(font="Courier " + str(font_size))
def reset_font():
global font_size
font_size = default_font_size
editor.configure(font="Courier " + str(font_size))
def full_screen():
top.attributes("-fullscreen", not top.attributes("-fullscreen"))
def show_about():
showinfo("About Scrunch Edit", about_text, parent=top)
def show_credits():
showinfo("Scrunch Edit credits", credits_text, parent=top)
def show_site():
try:
webbrowser.open_new_tab(
"https://ctrl-c.club/~nttp/toys/scrunch/")
except webbrowser.Error as e:
showerror("Error opening browser", str(e), parent=top)
toolbar.pack(side=TOP, pady=4)
workspace.pack(side=TOP, fill="both", expand=1)
tree.pack(side=LEFT, fill="both", expand=1)
tree_scroll.pack(side=RIGHT, fill="y", padx=4, expand=0)
status_line.pack(side=BOTTOM, fill="x", pady=4)
status.pack(side=LEFT, fill="x", expand=1)
grip.pack(side=RIGHT, anchor="s")
def toolbutt(txt, under=None, cmd=None):
return ttk.Button(
toolbar,
text=txt,
width=8,
underline=under,
command=cmd)
toolbutt("New", 0, handle_new).pack(side=LEFT)
toolbutt("Open", 0, handle_open).pack(side=LEFT)
toolbutt("Save", 0, handle_save).pack(side=LEFT)
ttk.Separator(toolbar, orient=VERTICAL).pack(
side=LEFT, padx=4, pady=4, fill="y")
toolbutt("Find", 0, handle_find).pack(side=LEFT)
toolbutt("Again", 1, find_again).pack(side=LEFT)
ttk.Separator(toolbar, orient=VERTICAL).pack(
side=LEFT, padx=4, pady=4, fill="y")
toolbutt("Insert", 0, insert_after).pack(side=LEFT)
toolbutt("Delete", 0, delete_section).pack(side=LEFT)
toolbutt("Rename", 4, rename_section).pack(side=LEFT)
menubar = Menu(top)
file_menu = Menu(menubar)
menubar.add_cascade(menu=file_menu, label="File", underline=0)
file_menu.add_command(
label="New", underline=0, accelerator="Ctrl-N",
command=handle_new)
file_menu.add_command(
label="Open...", underline=0, accelerator="Ctrl-O",
command=handle_open)
file_menu.add_command(
label="Save", underline=0, accelerator="Ctrl-S",
command=handle_save)
file_menu.add_separator()
file_menu.add_command(
label="Save as...", underline=5, command=handle_saveas)
file_menu.add_command(
label="Reload", underline=0, accelerator="Ctrl-R",
command=handle_reload)
file_menu.add_command(
label="Statistics", underline=1, accelerator="Ctrl-T",
command=show_stats)
file_menu.add_separator()
file_menu.add_command(
label="Quit", underline=0, accelerator="Ctrl-Q",
command=handle_quit)
edit_menu = Menu(menubar)
menubar.add_cascade(menu=edit_menu, label="Edit", underline=0)
edit_menu.add_command(
label="Undo", underline=0, accelerator="Ctrl-Z",
command=lambda: editor.edit_undo())
edit_menu.add_command(
label="Redo", underline=0, accelerator="Ctrl-Y",
command=lambda: editor.edit_redo())
edit_menu.add_separator()
edit_menu.add_command(
label="Cut", underline=0, accelerator="Ctrl-X",
command=cut_content)
edit_menu.add_command(
label="Copy", underline=1, accelerator="Ctrl-C",
command=copy_content)
edit_menu.add_command(
label="Paste", underline=0, accelerator="Ctrl-V",
command=paste_content)
edit_menu.add_separator()
edit_menu.add_command(
label="Select all", underline=0, accelerator="Ctrl-A",
command=lambda: select_all(editor))
edit_menu.add_separator()
edit_menu.add_command(
label="Find...", underline=0, accelerator="Ctrl-F",
command=handle_find)
edit_menu.add_command(
label="Again", underline=1, accelerator="Ctrl-G",
command=find_again)
format_menu = Menu(menubar)
menubar.add_cascade(menu=format_menu, label="Format", underline=3)
format_menu.add_command(label="Join lines", command=join_lines,
underline=0, accelerator="Alt-J")
format_menu.add_command(label="Open line", command=open_line,
underline=0, accelerator="Alt-O")
format_menu.add_separator()
format_menu.add_command(
label="Lower case", underline=0, accelerator="Alt-L",
command=lower_case)
format_menu.add_command(
label="Title case", underline=0, accelerator="Alt-T",
command=title_case)
format_menu.add_command(
label="Upper case", underline=0, accelerator="Alt-U",
command=upper_case)
view_menu = Menu(menubar)
menubar.add_cascade(menu=view_menu, label="View", underline=0)
view_menu.add_command(
label="Preface", underline=2, accelerator="Ctrl-E",
command=select_preface)
view_menu.add_separator()
view_menu.add_command(
label="Fold all", underline=0, accelerator="Ctrl <",
command=fold_all)
view_menu.add_command(
label="Unfold all", underline=0, accelerator="Ctrl >",
command=unfold_all)
view_menu.add_separator()
view_menu.add_command(
label="Bigger font", underline=0, accelerator="Ctrl +",
command=make_font_bigger)
view_menu.add_command(
label="Smaller font", underline=0, accelerator="Ctrl -",
command=make_font_smaller)
view_menu.add_command(
label="Reset font", underline=0, accelerator="Ctrl-0",
command=reset_font)
view_menu.add_separator()
view_menu.add_command(
label="Full screen", underline=10, accelerator="F11",
command=full_screen)
section_menu = Menu(menubar)
menubar.add_cascade(menu=section_menu, label="Section", underline=0)
section_menu.add_command(
label="Add child...", underline=0, accelerator="Ctrl-Ins",
command=add_child)
section_menu.add_separator()
section_menu.add_command(
label="Insert after...", underline=0, accelerator="Ctrl-I",
command=insert_after)
section_menu.add_command(
label="Delete", underline=4, accelerator="Ctrl-D",
command=delete_section)
section_menu.add_command(
label="Rename...", underline=4, accelerator="Ctrl-M",
command=rename_section)
section_menu.add_separator()
section_menu.add_command(
label="Move up", underline=5, accelerator="Num 8",
command=move_up)
section_menu.add_command(
label="Move right", underline=5, accelerator="Num 6",
command=move_right)
section_menu.add_command(
label="Move left", underline=5, accelerator="Num 4",
command=move_left)
section_menu.add_command(
label="Move down", underline=5, accelerator="Num 2",
command=move_down)
help_menu = Menu(menubar, name="help")
menubar.add_cascade(menu=help_menu, label="Help", underline=0)
help_menu.add_command(label="About", underline=0, command=show_about)
help_menu.add_command(label="Credits", underline=0, command=show_credits)
help_menu.add_command(label="Website", underline=0, command=show_site)
top["menu"] = menubar
tree.bind("<<TreeviewSelect>>", lambda e: select_section())
editor.bind("<<Modified>>", lambda e: show_modified())
top.bind("<Control-n>", lambda e: handle_new())
top.bind("<Control-o>", lambda e: handle_open())
top.bind("<Control-s>", lambda e: handle_save())
top.bind("<Control-r>", lambda e: handle_reload())
top.bind("<Control-t>", lambda e: show_stats())
top.bind("<Control-q>", lambda e: handle_quit())
top.bind("<Command-n>", lambda e: handle_new())
top.bind("<Command-o>", lambda e: handle_open())
top.bind("<Command-s>", lambda e: handle_save())
top.bind("<Command-r>", lambda e: handle_reload())
top.bind("<Command-t>", lambda e: show_stats())
top.bind("<Command-q>", lambda e: handle_quit())
# Undo, cut and copy are already bound to their usual keys by default.
editor.bind("<Control-y>", lambda e: editor.edit_redo())
editor.bind("<Control-v>", lambda e: paste_content())
editor.bind("<Control-a>", lambda e: select_all(editor))
top.bind("<Control-f>", lambda e: handle_find())
top.bind("<Control-g>", lambda e: find_again())
editor.bind("<Command-y>", lambda e: editor.edit_redo())
editor.bind("<Command-v>", lambda e: paste_content())
editor.bind("<Command-a>", lambda e: select_all(editor))
top.bind("<Command-f>", lambda e: handle_find())
top.bind("<Command-g>", lambda e: find_again())
editor.tk.call("bind", "Text", "<Control-o>", "")
editor.bind("<Alt-j>", lambda e: join_lines())
editor.bind("<Alt-o>", lambda e: open_line())
editor.bind("<Alt-l>", lambda e: lower_case())
editor.bind("<Alt-t>", lambda e: title_case())
editor.bind("<Alt-u>", lambda e: upper_case())
tree.bind("<Insert>", lambda e: add_child())
tree.bind("<Delete>", lambda e: delete_section())
top.bind("<Control-Insert>", lambda e: add_child())
top.bind("<Control-Delete>", lambda e: delete_section())
top.bind("<Command-Insert>", lambda e: add_child())
top.bind("<Command-Delete>", lambda e: delete_section())
top.bind("<Control-i>", lambda e: insert_after())
top.bind("<Control-d>", lambda e: delete_section())
top.bind("<Control-m>", lambda e: rename_section())
top.bind("<F2>", lambda e: rename_section())
top.bind("<Command-i>", lambda e: insert_after())
top.bind("<Command-d>", lambda e: delete_section())
top.bind("<Command-m>", lambda e: rename_section())
tree.bind("<KP_8>", lambda e: move_up())
tree.bind("<KP_6>", lambda e: move_right())
tree.bind("<KP_4>", lambda e: move_left())
tree.bind("<KP_2>", lambda e: move_down())
try:
tree.bind("<KP_Up>", lambda e: move_up())
tree.bind("<KP_Right>", lambda e: move_right())
tree.bind("<KP_Left>", lambda e: move_left())
tree.bind("<KP_Down>", lambda e: move_down())
except TclError as e:
print("Keypad arrows won't be available.", file=sys.stderr)
top.bind("<Control-e>", lambda e: select_preface())
top.bind("<Control-period>", lambda e: fold_all())
top.bind("<Control-comma>", lambda e: unfold_all())
top.bind("<Control-less>", lambda e: fold_all())
top.bind("<Control-greater>", lambda e: unfold_all())
top.bind("<Control-minus>", lambda e: make_font_smaller())
top.bind("<Control-equal>", lambda e: make_font_bigger())
top.bind("<Control-Key-0>", lambda e: reset_font())
top.bind("<Command-e>", lambda e: select_preface())
top.bind("<Command-period>", lambda e: fold_all())
top.bind("<Command-comma>", lambda e: unfold_all())
top.bind("<Command-less>", lambda e: fold_all())
top.bind("<Command-greater>", lambda e: unfold_all())
top.bind("<Command-minus>", lambda e: make_font_smaller())
top.bind("<Command-equal>", lambda e: make_font_bigger())
top.bind("<Command-Key-0>", lambda e: reset_font())
top.bind("<F11>", lambda e: full_screen())
top.protocol("WM_DELETE_WINDOW", handle_quit)
if top.tk.call('tk', 'windowingsystem') == 'x11':
ttk.Style().theme_use('clam')
if len(sys.argv) < 2:
pass
elif not os.path.exists(sys.argv[1]):
outline_filename = os.path.abspath(sys.argv[1])
fn = os.path.basename(outline_filename)
top.title(fn + " | Scrunch Edit")
elif load_file(sys.argv[1]):
outline_filename = os.path.abspath(sys.argv[1])
top.mainloop()