Add version 2.0.2
This commit is contained in:
parent
1b1a4d7438
commit
d00e0d25f8
|
@ -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
|
43
README.md
43
README.md
|
@ -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.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 112 KiB |
|
@ -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()
|
|
@ -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()
|
Loading…
Reference in New Issue