Add version 2.0.2

This commit is contained in:
No Time To Play 2022-07-18 12:30:50 +00:00
parent 1b1a4d7438
commit d00e0d25f8
5 changed files with 1948 additions and 2 deletions

79
NEWS.md Normal file
View File

@ -0,0 +1,79 @@
# Scrunch Edit news
## Version 2.0.2 (27 June 2022)
### Fixed
* Blank lines accumulating at end of sections
### Removed
* "Trim blank lines" formatting option
## Version 2.0.1 (26 June 2022)
### Fixed
* Modified flag on starting new file
### Removed
* Unused imports
* Unused metadata code
## Version 2.0 beta (27 November 2021)
### Added
* File statistics dialog
* Format menu with a few basic options
### Removed
* Metadata handling; it's now left as-is in the preface
## Version 1.1.1b (27 November 2021)
### Added
* Ctrl-R shortcut for the Reload menu option
### Fixed
* The preface is now saved to Markdown / Gemini files.
### Removed
* (internal) Unused add_dialog function
## Version 1.1.1 (27 October 2021)
### Added
* Search dialog is now prefilled from the selection, if any.
### Changed
* Find Again now prompts for a new search if none is active.
## Version 1.1 (23 October 2021)
### Added
* Searches now look in the preface as well.
* The first search result in each section is now highlighted.
## Version 1.0 beta (19 October 2021)
### Added
* Search functions.
* Help menu option to visit the website.
### Fixed
* The "new file" command now also clears the title bar.
## Version 2021-10-18 alpha
* Initial public release

View File

@ -1,3 +1,42 @@
# scrunch-edit
# Scrunch Edit introduction
A two-pane outliner for Markdown and org files
Often a text editor is more than enough to handle even long, complex documents. But past a certain point it gets hard to navigate the structure. This is where outliners come in. It's just that most outliners use their own file formats, and have poor or non-existent support for import and export. We can do better.
Scrunch Edit is a two-pane outliner with as many main goals:
- support popular markup formats such as Org Mode and Markdown;
- fit in a single Python script, to be easily copied and modified.
It otherwise acts much like a simple text editor.
## Uses
- Keep many small text files in one to save disk space and directory entries.
- Interoperate with applications like Emacs and Orgzly.
## System requirements
Scrunch Edit requires a Python runtime. Python comes preinstalled on the Mac and most Linux distributions; Windows users can get it from the python.org website. You'll also need the Tkinter module, which is bundled with most Windows runtimes and preinstalled on the Mac. On Linux you might need to add it yourself: look in your package manager for something called "tkinter", "python-tk" or the like.
Python version 3.x is preferred, but 2.7 should work as well.
Recommended screen resolution: 800x600.
## Status
Scrunch Edit 1.x is complete as of 27 November 2021.
Scrunch Edit 2.x is the new current version. See the changelog for details.
## Usage notes
Scrunch Edit guesses formats based on file extensions. There's no other way short of separate "open" commands, since the same text file can be interpreted in different ways based on context.
- The preface is any text that comes before the first heading, in Org mode parlance. That's unusual in Markdown, but you can still have one.
- If you add sections while a search is active, Find Again will skip them.
While an effort has been made to provide key combos for most functions, Scrunch Edit is best used with mouse and keyboard together.
## License and credits
Scrunch Edit is made by No Time To Play, based on knowledge gained from TkDocs.com, and offered as open source under the MIT License. See the source code for details.

BIN
scrunch-edit-20211018.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

916
scrunch.py Normal file
View File

@ -0,0 +1,916 @@
#!/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()

912
scrunch2.py Normal file
View File

@ -0,0 +1,912 @@
#!/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.0.2 (27 Jun 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"] == "":
pass
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}']
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 = 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"] != "":
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 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").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. 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
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. 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 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():
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 lower_case():
sel = text_selection(editor)
if len(sel) > 0:
editor.replace("sel.first", "sel.last", sel.lower())
else:
status["text"] = "Nothing selected."
def title_case():
sel = text_selection(editor)
if len(sel) > 0:
editor.replace("sel.first", "sel.last", sel.title())
else:
status["text"] = "Nothing selected."
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 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="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, 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=0, accelerator="Ctrl-G",
command=find_again)
format_menu = Menu(menubar)
menubar.add_cascade(menu=format_menu, label="Format", underline=1)
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)
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("<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())
top.bind("<Alt-l>", lambda e: lower_case())
top.bind("<Alt-t>", lambda e: title_case())
top.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_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()