917 lines
24 KiB
Python
917 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
#
|
|
# Scrunch Edit: a two-pane outliner for Org Mode and Markdown files
|
|
# Copyright 2021 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
|
|
|
|
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
|
|
|
|
import re
|
|
|
|
about_text = """
|
|
A two-pane outliner
|
|
Version 1.1.1b (27 Nov 2021)
|
|
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.metadata = {"title": "", "author": "", "date": ""}
|
|
|
|
self.cursor = 0
|
|
self.headChar = headChar
|
|
self.heading = None
|
|
|
|
def parseMeta(self):
|
|
return self.metadata # Most formats lack inherent metadata.
|
|
|
|
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
|
|
|
|
class OrgParser(MarkupParser):
|
|
re_meta = re.compile("^\s*#\+([A-Z]+):(.*)$", re.IGNORECASE)
|
|
|
|
def __init__(self, lines):
|
|
super(OrgParser, self).__init__(lines, '*')
|
|
|
|
def parseMeta(self):
|
|
while self.cursor < len(self.lines):
|
|
ln = self.lines[self.cursor]
|
|
m = self.re_meta.match(ln)
|
|
if m != None:
|
|
key = m.group(1).strip()
|
|
value = m.group(2).strip()
|
|
self.metadata[key] = value
|
|
else:
|
|
break
|
|
self.cursor += 1
|
|
return self.metadata
|
|
|
|
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"] == "":
|
|
pass
|
|
# elif i["section"].endswith('\n'):
|
|
# print(i["section"], file=f, end='')
|
|
else:
|
|
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}']
|
|
|
|
metadata = {"title": "", "author": "", "date": ""}
|
|
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
|
|
|
|
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 = OrgParser(f.readlines())
|
|
else:
|
|
raise RuntimeError("Unknown format")
|
|
meta = parser.parseMeta()
|
|
preface = parser.parseSection()
|
|
children = parseMarkup(parser)
|
|
return meta, preface, children
|
|
|
|
def load_file(full_path):
|
|
global modified, metadata
|
|
|
|
fn = os.path.basename(full_path)
|
|
name, ext = os.path.splitext(fn)
|
|
try:
|
|
with open(full_path) as f:
|
|
metadata, 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 opening 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 ext == ".md" or ext == ".gmi":
|
|
if data["preface"] != "":
|
|
print(data["preface"], file=f)
|
|
writeMarkup(f, data["outline"], '#')
|
|
elif ext == ".org":
|
|
for i in data["metadata"]:
|
|
if data["metadata"][i] != "":
|
|
tmp = "#+{}: {}".format(
|
|
i, data["metadata"][i])
|
|
print(tmp, file=f)
|
|
print(file=f)
|
|
if data["preface"] != "":
|
|
print(data["preface"], file=f)
|
|
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")
|
|
editor.edit_modified(False)
|
|
fn = os.path.basename(full_path)
|
|
name, ext = os.path.splitext(fn)
|
|
data = {
|
|
"metadata": metadata,
|
|
"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 saving 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 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")
|
|
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. Start 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
|
|
metadata["title"] = ""
|
|
metadata["author"] = ""
|
|
metadata["date"] = ""
|
|
node_text.clear()
|
|
node_text[""] = ""
|
|
set_modified(False)
|
|
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. Open another?",
|
|
icon="question",
|
|
parent=top)
|
|
if not do_open:
|
|
status["text"] = "Opening canceled."
|
|
return
|
|
choice = askopenfilename(
|
|
title="Open existing outline",
|
|
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...",
|
|
filetypes=file_types,
|
|
parent=top)
|
|
if len(choice) == 0:
|
|
status["text"] = "Save canceled."
|
|
elif save_file(choice):
|
|
outline_filename = choice
|
|
|
|
def edit_props():
|
|
answer = askstring(
|
|
"Properties", "Outline title:", parent=top,
|
|
initialvalue=metadata["title"])
|
|
if answer != None:
|
|
metadata["title"] = answer
|
|
set_modified()
|
|
show_modified()
|
|
|
|
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 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():
|
|
top.tk.eval("tk_textPaste " + str(editor))
|
|
|
|
def select_all(widget):
|
|
widget.tag_remove("sel", "1.0", "end")
|
|
widget.tag_add("sel", "1.0", "end")
|
|
|
|
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 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)
|
|
|
|
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. Delete 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."
|
|
|
|
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 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="Properties...", underline=6, command=edit_props)
|
|
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_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=0, accelerator="Ctrl-G",
|
|
command=find_again)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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("<Command-r>", lambda e: handle_reload())
|
|
|
|
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("<Control-q>", lambda e: handle_quit())
|
|
top.bind("<Command-q>", lambda e: handle_quit())
|
|
|
|
# Undo is already bound to Ctrl-z by default.
|
|
|
|
editor.bind("<Control-y>", lambda e: editor.edit_redo())
|
|
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-a>", lambda e: select_all(editor))
|
|
|
|
top.bind("<Command-f>", lambda e: handle_find())
|
|
top.bind("<Command-g>", lambda e: find_again())
|
|
|
|
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_bigger())
|
|
top.bind("<Control-plus>", lambda e: make_font_smaller())
|
|
top.bind("<Control-equal>", lambda e: make_font_smaller())
|
|
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_bigger())
|
|
top.bind("<Command-plus>", lambda e: make_font_smaller())
|
|
top.bind("<Command-equal>", lambda e: make_font_smaller())
|
|
top.bind("<Command-Key-0>", lambda e: reset_font())
|
|
|
|
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 load_file(sys.argv[1]):
|
|
outline_filename = sys.argv[1]
|
|
|
|
top.mainloop()
|