1235 lines
36 KiB
Python
1235 lines
36 KiB
Python
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
#
|
|
# OutNoted: an outline note-taking editor
|
|
# Copyright 2024 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
|
|
|
|
import shutil
|
|
import subprocess
|
|
|
|
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
|
|
import json
|
|
import xml.dom.minidom
|
|
from xml.parsers.expat import ExpatError
|
|
|
|
class MarkupParser(object):
|
|
def __init__(self, lines, headChar):
|
|
self.lines = lines
|
|
|
|
self.metadata = {"title": ""}
|
|
|
|
self.cursor = 0
|
|
self.headChar = headChar
|
|
self.headline = None
|
|
|
|
def parseMeta(self):
|
|
return self.metadata # Most formats lack inherent metadata.
|
|
|
|
def skipSection(self):
|
|
if self.cursor >= len(self.lines):
|
|
return None
|
|
|
|
# body = []
|
|
|
|
while self.lines[self.cursor][0] != self.headChar:
|
|
# body.push(this.lines[this.cursor]);
|
|
self.cursor += 1
|
|
if self.cursor >= len(self.lines):
|
|
break;
|
|
|
|
# return body
|
|
|
|
def matchHeadline(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.headline = self.lines[self.cursor][i + 1:].strip()
|
|
self.cursor += 1
|
|
|
|
return True
|
|
|
|
class OrgParser(MarkupParser):
|
|
re_meta = re.compile("^\s*#\+([A-Z]+):(.*)$", re.IGNORECASE)
|
|
re_status = re.compile(r"^(TODO|NEXT|DONE)\b(.*)$")
|
|
|
|
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):
|
|
parser.skipSection()
|
|
|
|
subnodes = []
|
|
while parser.matchHeadline(level + 1):
|
|
node = {
|
|
"text": parser.headline,
|
|
"children": parseMarkup(parser, level + 1)
|
|
}
|
|
subnodes.append(node)
|
|
return subnodes
|
|
|
|
def parseStatus(outline):
|
|
for i in outline:
|
|
m = OrgParser.re_status.match(i["text"])
|
|
if m != None:
|
|
i["status"] = m.group(1)
|
|
i["text"] = m.group(2).strip()
|
|
parseStatus(i["children"])
|
|
return outline
|
|
|
|
class OPMLoader:
|
|
def __init__(self, source):
|
|
data = xml.dom.minidom.parse(source)
|
|
self.head = data.getElementsByTagName("head")[0]
|
|
self.body = data.getElementsByTagName("body")[0]
|
|
self.metadata = {"title": ""}
|
|
|
|
def parseMeta(self):
|
|
for i in self.head.childNodes:
|
|
if i.nodeType == i.ELEMENT_NODE:
|
|
text = i.firstChild.nodeValue
|
|
self.metadata[i.nodeName] = text
|
|
return self.metadata
|
|
|
|
def parseOPML(node):
|
|
subnodes = []
|
|
for i in node.childNodes:
|
|
if i.nodeType == i.ELEMENT_NODE:
|
|
node = {
|
|
"text": i.getAttribute("text"),
|
|
"children": parseOPML(i)
|
|
}
|
|
if i.hasAttribute("url"):
|
|
node["link"] = i.getAttribute("url")
|
|
elif i.hasAttribute("htmlUrl"):
|
|
node["link"] = i.getAttribute("htmlUrl")
|
|
|
|
if i.hasAttribute("xmlUrl"):
|
|
node["feed"] = i.getAttribute("xmlUrl")
|
|
subnodes.append(node)
|
|
return subnodes
|
|
|
|
def printMarkup(f, outline, headChar, level = 1):
|
|
for i in outline:
|
|
print(headChar * level, i["text"], file=f)
|
|
printMarkup(f, i["children"], headChar, level + 1)
|
|
|
|
def printOrgMarkup(f, outline, level = 1):
|
|
for i in outline:
|
|
if "status" in i and i["status"] != "":
|
|
print('*' * level, i["status"], i["text"], file=f)
|
|
else:
|
|
print('*' * level, i["text"], file=f)
|
|
printOrgMarkup(f, i["children"], level + 1)
|
|
|
|
def buildOutline(document, parent, outline):
|
|
for i in outline:
|
|
node = document.createElement("outline");
|
|
node.setAttribute("text", i["text"])
|
|
if "feed" in i and i["feed"] != "":
|
|
node.setAttribute("type", "rss")
|
|
node.setAttribute("xmlUrl", i["feed"])
|
|
if "link" in i and i["link"] != "":
|
|
node.setAttribute("htmlUrl", i["link"])
|
|
elif "link" in i and i["link"] != "":
|
|
node.setAttribute("type", "link")
|
|
node.setAttribute("url", i["link"])
|
|
buildOutline(document, node, i["children"])
|
|
parent.appendChild(node)
|
|
|
|
def buildOPML(metadata, outline):
|
|
document = xml.dom.minidom.parseString(
|
|
"<opml version='2.0'><head></head><body></body></opml>")
|
|
head = document.getElementsByTagName("head")[0]
|
|
if "title" in metadata:
|
|
title = document.createElement("title")
|
|
title.appendChild(
|
|
document.createTextNode(
|
|
metadata["title"]))
|
|
head.appendChild(title)
|
|
body = document.getElementsByTagName("body")[0]
|
|
buildOutline(document, body, outline)
|
|
return document
|
|
|
|
about_text = """
|
|
An outline note-taking editor
|
|
Version 2.2a (2 Feb 2024)
|
|
MIT License
|
|
"""
|
|
|
|
credits_text = """
|
|
Made by No Time To Play
|
|
based on knowledge gained
|
|
from TkDocs.com
|
|
"""
|
|
|
|
file_types = [("All files", ".*"),
|
|
'"OutNoted files" {.out}',
|
|
("Org Mode files", ".org"),
|
|
("OPML 2.0 files", ".opml"),
|
|
'"Markdown / Gemini" {.md .gmi}']
|
|
|
|
metadata = {"title": "OutNoted introduction"}
|
|
|
|
outline_filename = None
|
|
modified = False
|
|
editing = "" # Which node we're currently editing, if any.
|
|
|
|
clipboard = None
|
|
search = None
|
|
|
|
interp = Tcl()
|
|
|
|
top = Tk()
|
|
top.title("OutNoted")
|
|
top.option_add('*tearOff', FALSE)
|
|
top["padx"] = 4
|
|
|
|
if top.tk.call('tk', 'windowingsystem') == 'x11':
|
|
ttk.Style().theme_use('clam')
|
|
|
|
bookmark_icon_data = """
|
|
R0lGODlhGAAYAIABAAAAAP///yH5BAEKAAEALAAAAAAYABgAAAJKjI+py+0PgZxUwooBlExCyUiQ
|
|
xEiQxEiQxEiQxEiQxEiQxEiQdEiHBJEYKAYJRBLABCSQiqECoSAokIliApEwJBCAAwAJi8dkRgEA
|
|
Ow==
|
|
"""
|
|
bookmark_icon = PhotoImage(data=bookmark_icon_data)
|
|
|
|
app_icon_data = """
|
|
R0lGODdhIAAgALEAAAAAAP8AAP+A/////yH5BAEAAAEALAAAAAAgACAAAAKnjI+py+0P4wO0WirV
|
|
3SAnKoRiiGWhAY5iyVXIGaSqwLbdActqyfBBDpiRbj3iT4AKCn0aI1BIU86SL2RMusOOqDir7AsY
|
|
iMfAanKYoozJXmc7aomuxeVu0nWtzAd1A9Bnsdd3dJcAJ9hmdqUwhMjl57UQMhA2NwjDMEknwPZI
|
|
2KA5sumJKck5w5fYBQUp0noJZbW1qirhaljqkWvngeNg1RssPEycUAAAOw==
|
|
"""
|
|
app_icon = PhotoImage(data=app_icon_data)
|
|
if sys.version_info.major >= 3:
|
|
top.iconphoto("", app_icon)
|
|
|
|
def load_help():
|
|
viewport.insert("", "end", "about", text="About OutNoted", open=1)
|
|
viewport.insert("about", "end",
|
|
text="An outline note-taking editor")
|
|
viewport.insert("about", "end",
|
|
text="Version 2.2a (2 Feb 2024), by No Time To Play")
|
|
viewport.insert("about", "end",
|
|
text="Open source under the MIT License")
|
|
viewport.insert("", "end", "features", text="Features", open=1)
|
|
viewport.insert("features", "end",
|
|
text="Create and edit outlines made of one-line notes.")
|
|
viewport.insert("features", "end",
|
|
text="Open and save outline formats like Org Mode and OPML.")
|
|
viewport.insert("features", "end",
|
|
text="Treat any note as a task, link and/or newsfeed.")
|
|
viewport.insert("", "end", "usage", text="How to use", open=1)
|
|
tmp = viewport.insert("usage", "end", open=1,
|
|
text="Press Ctrl-Insert to add a note, Enter to save.")
|
|
viewport.insert(tmp, "end",
|
|
text="Or just click in the edit line and start typing.")
|
|
viewport.insert("usage", "end",
|
|
text="Keep typing to add more notes in the same place.")
|
|
viewport.insert("usage", "end",
|
|
text="Use Ctrl-Escape to clear the selection first.")
|
|
viewport.insert("usage", "end",
|
|
text="Press Tab from the edit line to focus the tree.")
|
|
viewport.insert("usage", "end",
|
|
text="Ctrl-E starts editing the selected note.")
|
|
viewport.insert("usage", "end",
|
|
text="Escape on the edit line cancels editing the note.")
|
|
viewport.insert("usage", "end",
|
|
text="Insert adds a child note to the one selected.")
|
|
viewport.insert("usage", "end",
|
|
text="Delete removes the selected note and its children.")
|
|
viewport.insert("usage", "end",
|
|
text="Use the numeric keypad to move notes around.")
|
|
|
|
def load_outline(data, item = ""):
|
|
for i in data:
|
|
added = viewport.insert(item, "end", text=i["text"], open=1)
|
|
if "status" in i:
|
|
viewport.set(added, "status", i["status"])
|
|
|
|
if "link" in i:
|
|
viewport.set(added, "link", i["link"])
|
|
viewport.item(added, image=bookmark_icon)
|
|
|
|
if "feed" in i:
|
|
viewport.set(added, "feed", i["feed"])
|
|
viewport.item(added, image=bookmark_icon)
|
|
load_outline(i["children"], added)
|
|
|
|
def unload_outline(item = ""):
|
|
outline = []
|
|
for i in viewport.get_children(item):
|
|
child = {
|
|
"text": viewport.item(i, "text"),
|
|
"children": unload_outline(i)
|
|
}
|
|
status = viewport.set(i, "status")
|
|
if status != "":
|
|
child["status"] = status
|
|
|
|
link = viewport.set(i, "link")
|
|
if link != "":
|
|
child["link"] = link
|
|
|
|
feed = viewport.set(i, "feed")
|
|
if feed != "":
|
|
child["feed"] = feed
|
|
|
|
outline.append(child)
|
|
return outline
|
|
|
|
def parse_file(f, ext):
|
|
if ext == ".out":
|
|
data = json.load(f)
|
|
return data["metadata"], data["outline"]
|
|
elif ext == ".md" or ext == ".gmi":
|
|
parser = MarkupParser(f.readlines(), '#')
|
|
return parser.parseMeta(), parseMarkup(parser)
|
|
elif ext == ".org":
|
|
parser = OrgParser(f.readlines())
|
|
meta = parser.parseMeta() # Parse metadata first!
|
|
markup = parseMarkup(parser)
|
|
return meta, parseStatus(markup)
|
|
elif ext == ".opml":
|
|
parser = OPMLoader(f)
|
|
return parser.parseMeta(), parseOPML(parser.body)
|
|
else:
|
|
raise RuntimeError("Unknown format")
|
|
|
|
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, outline = parse_file(f, ext)
|
|
viewport.delete(*viewport.get_children())
|
|
load_outline(outline)
|
|
modified = False
|
|
status["text"] = "Opened " + fn
|
|
top.title(fn + " | OutNoted")
|
|
return True
|
|
except RuntimeError as e:
|
|
showerror("Error reading file", str(e), parent=top)
|
|
except TypeError as e:
|
|
showerror("Error reading file", str(e), parent=top)
|
|
except KeyError as e:
|
|
showerror("Error reading file", str(e), parent=top)
|
|
except OSError as e:
|
|
showerror("Error opening file", str(e), parent=top)
|
|
except IOError as e: # For Python 2.7
|
|
showerror("Error opening file", str(e), parent=top)
|
|
except AttributeError as e:
|
|
showerror("Error reading file",
|
|
"File missing head or body: " + str(e),
|
|
parent=top)
|
|
except ExpatError as e:
|
|
showerror("Error reading file",
|
|
"Bad XML in input file: " + str(e),
|
|
parent=top)
|
|
return False
|
|
|
|
def write_file(f, ext, data):
|
|
if ext == ".out" or ext == ".json":
|
|
json.dump(data, f)
|
|
elif ext == ".md" or ext == ".gmi":
|
|
printMarkup(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)
|
|
printOrgMarkup(f, data["outline"])
|
|
elif ext == ".opml":
|
|
markup = buildOPML(data["metadata"], data["outline"])
|
|
markup.writexml(f, "", " ", "\n", encoding="UTF-8")
|
|
else:
|
|
raise RuntimeError("Unknown format")
|
|
|
|
def save_file(full_path):
|
|
global modified
|
|
|
|
fn = os.path.basename(full_path)
|
|
name, ext = os.path.splitext(fn)
|
|
data = {
|
|
"metadata": metadata,
|
|
"outline": unload_outline()
|
|
}
|
|
try:
|
|
with open(full_path, "w") as f:
|
|
write_file(f, ext, data)
|
|
f.flush()
|
|
modified = False
|
|
status["text"] = "Saved " + fn
|
|
top.title(fn + " | OutNoted")
|
|
return True
|
|
except RuntimeError as e:
|
|
showerror("Error writing file", str(e), parent=top)
|
|
return False
|
|
except OSError as e:
|
|
showerror("Error saving file", str(e), parent=top)
|
|
return False
|
|
except IOError as e: # For Python 2.7
|
|
showerror("Error saving file", str(e), parent=top)
|
|
return False
|
|
|
|
def all_item_ids(item = ""):
|
|
"Return a flat list of item IDs present in the viewport."
|
|
items = []
|
|
for i in viewport.get_children(item):
|
|
items.append(i)
|
|
items.extend(all_item_ids(i))
|
|
return items
|
|
|
|
def start_search(term, items):
|
|
term = term.lower()
|
|
for i in items:
|
|
if viewport.exists(i): # It might have been deleted.
|
|
text = viewport.item(i, "text")
|
|
if term in text.lower():
|
|
yield i
|
|
|
|
def handle_new():
|
|
global modified, outline_filename
|
|
|
|
if modified:
|
|
answer = askyesno(
|
|
title="New outline?",
|
|
message="Outline is unsaved. Start another?",
|
|
icon="question",
|
|
parent=top)
|
|
else:
|
|
answer = True
|
|
if answer:
|
|
viewport.delete(*viewport.get_children())
|
|
top.title("OutNoted")
|
|
outline_filename = None
|
|
metadata["title"] = ""
|
|
modified = False
|
|
else:
|
|
status["text"] = "New outline canceled."
|
|
|
|
def file_dir():
|
|
if outline_filename != None:
|
|
return os.path.dirname(outline_filename)
|
|
else:
|
|
return "."
|
|
|
|
def handle_open():
|
|
global outline_filename
|
|
|
|
if 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
|
|
|
|
if (top.tk.call('tk', 'windowingsystem') == 'x11'
|
|
and shutil.which("zenity") != None):
|
|
choice = open_with_zenity(outline_filename)
|
|
else:
|
|
choice = askopenfilename(
|
|
title="Open existing outline",
|
|
initialdir=file_dir(),
|
|
filetypes=file_types,
|
|
parent=top)
|
|
|
|
if len(choice) == 0:
|
|
status["text"] = "Opening canceled."
|
|
elif not os.path.isfile(choice):
|
|
showerror(
|
|
"Error opening file",
|
|
"File not found: " + choice,
|
|
parent=top)
|
|
elif load_file(choice):
|
|
outline_filename = choice
|
|
|
|
def open_with_zenity(file_name):
|
|
if file_name != None:
|
|
zenity_cmd = ["zenity", "--file-selection",
|
|
"--title", "Open existing outline",
|
|
"--filename", file_name,
|
|
"--file-filter", "All files | *",
|
|
"--file-filter", "Org Mode files | *.org",
|
|
"--file-filter", "Markdown / Gemini | *.md *.gmi"]
|
|
else:
|
|
zenity_cmd = ["zenity", "--file-selection",
|
|
"--title", "Open existing outline",
|
|
"--file-filter", "All files | *",
|
|
"--file-filter", "Org Mode files | *.org",
|
|
"--file-filter", "Markdown / Gemini | *.md *.gmi"]
|
|
proc = subprocess.run(zenity_cmd, capture_output=True)
|
|
return proc.stdout.decode().rstrip()
|
|
|
|
def handle_save():
|
|
if outline_filename == None:
|
|
handle_saveas()
|
|
else:
|
|
save_file(outline_filename)
|
|
|
|
def handle_saveas():
|
|
global outline_filename
|
|
|
|
if (top.tk.call('tk', 'windowingsystem') == 'x11'
|
|
and shutil.which("zenity") != None):
|
|
choice = save_with_zenity(outline_filename)
|
|
else:
|
|
choice = asksaveasfilename(
|
|
title="Save outline as...",
|
|
initialdir=file_dir(),
|
|
filetypes=file_types,
|
|
parent=top)
|
|
|
|
if len(choice) == 0:
|
|
status["text"] = "Save canceled."
|
|
elif save_file(choice):
|
|
outline_filename = choice
|
|
|
|
def save_with_zenity(file_name):
|
|
if file_name != None:
|
|
zenity_cmd = ["zenity", "--file-selection",
|
|
"--title", "Save outline as...",
|
|
"--save", "--confirm-overwrite",
|
|
"--filename", file_name,
|
|
"--file-filter", "All files | *",
|
|
"--file-filter", "Org Mode files | *.org",
|
|
"--file-filter", "Markdown / Gemini | *.md *.gmi"]
|
|
else:
|
|
zenity_cmd = ["zenity", "--file-selection",
|
|
"--title", "Save outline as...",
|
|
"--save", "--confirm-overwrite",
|
|
"--file-filter", "All files | *",
|
|
"--file-filter", "Org Mode files | *.org",
|
|
"--file-filter", "Markdown / Gemini | *.md *.gmi"]
|
|
proc = subprocess.run(zenity_cmd, capture_output=True)
|
|
return proc.stdout.decode().rstrip()
|
|
|
|
def handle_find():
|
|
global search
|
|
if search != None:
|
|
search.close()
|
|
answer = askstring("Search", "Find text:", parent=top)
|
|
if answer == None:
|
|
status["text"] = "Search canceled."
|
|
return
|
|
search = start_search(answer, all_item_ids())
|
|
do_find()
|
|
|
|
def find_again():
|
|
if search == None:
|
|
handle_find()
|
|
else:
|
|
do_find()
|
|
|
|
def do_find():
|
|
global search
|
|
next_result = next(search, "")
|
|
if next_result == "":
|
|
search.close()
|
|
search = None
|
|
status["text"] = "Nothing found."
|
|
else:
|
|
viewport.selection_set(next_result)
|
|
viewport.focus(next_result)
|
|
viewport.see(next_result)
|
|
|
|
def handle_insert():
|
|
global editing
|
|
editing = ""
|
|
edit_line.focus()
|
|
|
|
def handle_delete():
|
|
global modified
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
answer = False
|
|
elif len(viewport.get_children(focus)) > 0:
|
|
answer = askyesno(
|
|
title="Delete note?",
|
|
message="Note has children. Delete anyway?",
|
|
icon="question",
|
|
parent=top)
|
|
else:
|
|
answer = True
|
|
if answer:
|
|
viewport.selection_set(viewport.next(focus))
|
|
viewport.focus(viewport.next(focus))
|
|
viewport.delete(focus)
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
else:
|
|
status["text"] = "Deletion canceled."
|
|
|
|
def insert_after():
|
|
global modified
|
|
answer = askstring("Add note", "New note text:", parent=top)
|
|
if answer == None:
|
|
status["text"] = "Canceled adding note."
|
|
else:
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
parent = ""
|
|
position = "end"
|
|
else:
|
|
parent = viewport.parent(focus)
|
|
position = viewport.index(focus) + 1
|
|
child = viewport.insert(parent, position, text=answer)
|
|
viewport.see(child)
|
|
viewport.selection_set(child)
|
|
viewport.focus(child)
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def move_left():
|
|
global modified
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
return
|
|
parent = viewport.parent(focus)
|
|
if parent == "":
|
|
status["text"] = "Note is at top level."
|
|
return
|
|
index = viewport.index(parent)
|
|
viewport.move(focus, viewport.parent(parent), index + 1)
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def move_down():
|
|
global modified
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
return
|
|
viewport.move(focus,
|
|
viewport.parent(focus),
|
|
viewport.index(focus) + 1)
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def move_up():
|
|
global modified
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
return
|
|
viewport.move(focus,
|
|
viewport.parent(focus),
|
|
viewport.index(focus) - 1)
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def move_right():
|
|
global modified
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
return
|
|
prev = viewport.prev(focus)
|
|
if prev == "":
|
|
status["text"] = "No previous note."
|
|
return
|
|
viewport.move(focus, prev, "end")
|
|
viewport.see(focus)
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def handle_edit_line():
|
|
global editing, modified
|
|
if editing == "":
|
|
viewport.see(
|
|
viewport.insert(
|
|
viewport.focus(), "end",
|
|
text=edit_text.get()))
|
|
edit_text.set("")
|
|
else:
|
|
viewport.item(editing, text=edit_text.get())
|
|
editing = ""
|
|
edit_text.set("")
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def cancel_edit_line():
|
|
global editing
|
|
editing = ""
|
|
edit_text.set("")
|
|
|
|
def start_editing():
|
|
global editing
|
|
editing = viewport.focus()
|
|
edit_text.set(viewport.item(editing, "text"))
|
|
edit_line.focus()
|
|
|
|
def cancel_selection():
|
|
viewport.selection_set("")
|
|
viewport.focus("")
|
|
|
|
def set_note_status(text):
|
|
global modified
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
elif viewport.set(focus, "status") == text:
|
|
viewport.set(focus, "status", "")
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
else:
|
|
viewport.set(focus, "status", text)
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def set_link_icon(note):
|
|
v = viewport
|
|
if (v.set(note, "link") == "" and v.set(note, "feed") == ""):
|
|
v.item(note, image="")
|
|
else:
|
|
v.item(note, image=bookmark_icon)
|
|
|
|
def set_note_link():
|
|
global modified
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
return
|
|
answer = askstring(
|
|
"OutNoted", "Note link:", parent=top,
|
|
initialvalue=viewport.set(focus, "link"))
|
|
if answer != None:
|
|
status["text"] = "Link set to: " + answer
|
|
viewport.set(focus, "link", answer)
|
|
set_link_icon(focus)
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def set_note_feed():
|
|
global modified
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
return
|
|
answer = askstring(
|
|
"OutNoted", "Feed URL:", parent=top,
|
|
initialvalue=viewport.set(focus, "feed"))
|
|
if answer != None:
|
|
status["text"] = "Feed set to: " + answer
|
|
viewport.set(focus, "feed", answer)
|
|
set_link_icon(focus)
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def go_to_site():
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
return
|
|
link = viewport.set(focus, "link")
|
|
if link == "":
|
|
status["text"] = "Note isn't linked."
|
|
else:
|
|
try:
|
|
webbrowser.open_new_tab(link)
|
|
except webbrowser.Error as e:
|
|
showerror("Error opening browser", str(e), parent=top)
|
|
|
|
def reset_note():
|
|
global modified
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
return
|
|
else:
|
|
viewport.set(focus, "status", "")
|
|
viewport.set(focus, "link", "")
|
|
viewport.set(focus, "feed", "")
|
|
viewport.item(focus, image="")
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def handle_cut():
|
|
global clipboard, modified
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
else:
|
|
clipboard = [{
|
|
"text": viewport.item(focus, "text"),
|
|
"children": unload_outline(focus)
|
|
}]
|
|
top.clipboard_clear()
|
|
top.clipboard_append(viewport.item(focus, "text"))
|
|
viewport.delete(focus)
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def handle_copy():
|
|
global clipboard
|
|
focus = viewport.focus()
|
|
if focus == "":
|
|
status["text"] = "Nothing selected."
|
|
else:
|
|
clipboard = [{
|
|
"text": viewport.item(focus, "text"),
|
|
"children": unload_outline(focus)
|
|
}]
|
|
top.clipboard_clear()
|
|
top.clipboard_append(viewport.item(focus, "text"))
|
|
|
|
def handle_paste():
|
|
global modified
|
|
if clipboard == None:
|
|
status["text"] = "Clipboard is empty."
|
|
else:
|
|
load_outline(clipboard, viewport.focus())
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def fold_all():
|
|
for i in all_item_ids():
|
|
viewport.item(i, open=0)
|
|
|
|
def unfold_all():
|
|
for i in all_item_ids():
|
|
viewport.item(i, open=1)
|
|
|
|
def edit_props():
|
|
global modified
|
|
answer = askstring(
|
|
"Properties", "Outline title:", parent=top,
|
|
initialvalue=metadata["title"])
|
|
if answer != None:
|
|
metadata["title"] = answer
|
|
modified = True
|
|
status["text"] = "(modified)"
|
|
|
|
def show_stats():
|
|
items = all_item_ids()
|
|
note_count = len(items)
|
|
char_count = 0
|
|
link_count = 0
|
|
v = viewport
|
|
for i in items:
|
|
char_count += len(viewport.item(i, "text"))
|
|
if v.set(i, "link") != "" or v.set(i, "feed"):
|
|
link_count += 1
|
|
stats = "Notes: {}\nWith links: {}\nText size: {} chars"
|
|
stats = stats.format(note_count, link_count, char_count)
|
|
showinfo("Outline statistics", stats, parent=top)
|
|
|
|
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:
|
|
do_quit = askyesno(
|
|
title="Quit OutNoted?",
|
|
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 full_screen():
|
|
top.attributes("-fullscreen", not top.attributes("-fullscreen"))
|
|
|
|
def show_about():
|
|
showinfo("About OutNoted", about_text, parent=top)
|
|
|
|
def show_credits():
|
|
showinfo("OutNoted credits", credits_text, parent=top)
|
|
|
|
def show_site():
|
|
try:
|
|
webbrowser.open_new_tab(
|
|
"https://ctrl-c.club/~nttp/toys/outnoted/")
|
|
except webbrowser.Error as e:
|
|
showerror("Error opening browser", str(e), parent=top)
|
|
|
|
toolbar = ttk.Frame(top)
|
|
|
|
edit_text = StringVar()
|
|
edit_line = ttk.Entry(top, textvariable=edit_text)
|
|
|
|
viewport = ttk.Treeview(top, height=19, selectmode="browse",
|
|
columns=('status', 'link', 'feed'), displaycolumns=('status',))
|
|
scrollbar = ttk.Scrollbar(top, orient=VERTICAL, command=viewport.yview)
|
|
viewport["yscrollcommand"] = scrollbar.set
|
|
|
|
viewport.column("status", width=64, stretch=0, anchor="center")
|
|
viewport.heading("#0", text="Notes")
|
|
viewport.heading("status", text="Status")
|
|
|
|
status_line = ttk.Frame(top)
|
|
status = ttk.Label(status_line,
|
|
text=interp.eval("clock format [clock seconds]"), relief="sunken")
|
|
grip = ttk.Sizegrip(status_line)
|
|
|
|
toolbar.pack(side=TOP, fill="x", padx=2, pady=2)
|
|
edit_line.pack(side=TOP, fill="x", padx=2, pady=2, ipadx=4, ipady=4)
|
|
|
|
status_line.pack(side=BOTTOM, fill="x", padx=2, pady=2)
|
|
status.pack(side=LEFT, fill="x", ipadx=2, ipady=2, expand=1)
|
|
grip.pack(side=RIGHT, anchor="s")
|
|
|
|
viewport.pack(side=LEFT, fill="both", padx=2, pady=2, expand=1)
|
|
scrollbar.pack(side=RIGHT, fill="y", padx=2, pady=2, expand=0)
|
|
|
|
icon_data = {}
|
|
icon_data["document-new"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJFjI+pCe2+FBC0ihbR
|
|
tBQMkBkb533haJVmhFYPtrRcx84zGNtkrbt8f/n1cBIgLWckMoxBJFCpYUIPsptQNxW9trCQ91AA
|
|
ADs="""
|
|
icon_data["document-open"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJMjI+py20AHQOiAplo
|
|
tbD3pW3ieGXjuZVIyHlu9IjAQNd2XbLzzQ952uOpQrsg7qApGn0ry/KIFCiNqtg0WA1AnrTs4wX2
|
|
YsbksvlcAAA7"""
|
|
icon_data["document-save"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJjjI+pB90CI3QJjDtA
|
|
dPMCZGHaJIxhxmDZVnaica7TU6pfEI+S68Fq5gj+brGf8YWzBZc2n2e3OxFFUInUKavWesmndovq
|
|
ZqtXsY6MlH7BU8sZ6g6750sHUH7MI3H1fnABGFgAADs="""
|
|
icon_data["document-revert"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJWjI+pBu2+FBC0ihbR
|
|
tBQMkDFcN3zhxnklGKHWgy3u2LV0ao8uK+lfxUvMVLCiCFhKKpUslGoJNQV2UWgztSomr1jndHtM
|
|
vb6r8O1Cls5oIBijCA/JIwUAOw=="""
|
|
icon_data["document-properties"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJPjI+pyx0Ao4wNiIuz
|
|
AHXPuHWaxjHWB5aLNWaqwnrx9SYsMORzjZz5j6IcbpCf0fiabY7HJImJ7NGKUKAUhww6lzqXSEb6
|
|
tkImrdCBTqsPBQA7"""
|
|
|
|
icon_data["edit-clear"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJJjI+py+0Po3yg2otV
|
|
Fbz7DiQbZ5GjEB4nOgxVC4Bq7AHt7crBCuI0aeCRfLXZx4bLAY09F/KXYqJGpSVC+LFeMVzL5AsO
|
|
i8eKAgA7"""
|
|
icon_data["edit-cut"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJZjI+pBrAfGhMOIjAq
|
|
ENSykU1c9WCgxo0leB5bSrpseKWdzMaurc16DTO1LC/h0NPImH4QTJKpkBgdklLjisUuqlcGV1eN
|
|
xMJkUthl1qitXm+3qXV7xNA5ogAAOw=="""
|
|
icon_data["edit-copy"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJWjI+pCO2/EhC0UhCZ
|
|
eG3nM1kV9gUTd5amOKLKyW3DPJMaKzuDDW4xQKu9cLnGboiSBXmGEAuoY66SwOUQBzVKUY/gsVTd
|
|
RVVhr/Ql7p4lXtqawW2o5gUAOw=="""
|
|
icon_data["edit-paste"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJOjI+pa+DA1HNCTBjB
|
|
GKBWzUUBeJEiuKUpxqDl1K7ebMVbV9aLS3tswsv9ELyeTiIT2jjGIzB5cRJlxuEhGLU2Jk0t0JIV
|
|
Nbri0ctRTgcKADs="""
|
|
icon_data["edit-find"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJWjI+pi+DvWIIUSPOE
|
|
3k9mMYRctDhaiI6MibYaqbCtKMATAM7Da5V4nuKtfsBNrXcjGnnIg2NgWh6bGECoQolZodjO7Qq5
|
|
VMFe8TYrxnDL6Uq6EX7LLwUAOw=="""
|
|
icon_data["edit-change"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJHjI+py+0I4ltgVDCh
|
|
HRzny1neA3bWVG5jk17oZq5Me5Kw+lZxHrq2jvv1ag6AAOgjCY5BlJHpCUmlhsjTY81mB9VsV6vN
|
|
PAoAOw=="""
|
|
|
|
icon_data["list-add"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP////+A/yH5BAEAAAEALAAAAAAWABYAAAJDjI+pG+DvmAKjWiAT
|
|
tRXns3He13AdaYQXAz3m0EblS4tpjcN3Tnsqv5oBbcLhqRF7xUYaJap4JP10qCkzY30ur89MAQA7
|
|
"""
|
|
icon_data["list-remove"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAItjI+py+0Po5y0GoCz
|
|
3vuOD4YiCHjjGZYBgLaDyronLM9mnV7czln+DwwKh4wCADs="""
|
|
|
|
icon_data["go-previous"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP////+A/yH5BAEAAAEALAAAAAAWABYAAAI6jI+py+3PAIxyJoAt
|
|
wqNq3mHiKG7AgKbq6oHrm1YuDMsnTbc3zpp87wuRhgqORxMoIQ+ZJdMJjUoDBQA7"""
|
|
icon_data["go-down"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP////+A/yH5BAEAAAEALAAAAAAWABYAAAJCjI+pG+DvmAKjWiAT
|
|
tRXns3He13AdaYQXWprjp55oPLwZbUs4u+uQWYMsHEBRDkTrHBFE11KjivA2UlbjYWU+s4sCADs=
|
|
"""
|
|
icon_data["go-up"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP////+A/yH5BAEAAAEALAAAAAAWABYAAAI6jI+piwAMjXNxuTFo
|
|
PRfnt3XeV4kjyZgnmkzqN0XACob0Jt34fNalbuvhAryRTwb8CXdJ5DLkig2HBQA7"""
|
|
icon_data["go-next"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP////+A/yH5BAEAAAEALAAAAAAWABYAAAI8jI+py+0PA4gMTJrs
|
|
xQYMq2ji6H1bN6TqyoJHycaqK8l26sJ3e+k7vvHtaBISCcipESmaZOfEgTqnVEgBADs="""
|
|
|
|
icon_data["insert-link"] = """
|
|
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJOjI+pm+DOlhMCUBiN
|
|
tfXWzIXexzzmCZRjN6aKuIYSSrtNTLE2ArM6X6u9BkQiTDYsHkmNorG1OwCcxlOJOo1KsAEMyGnT
|
|
Jgdi0CODTisKADs="""
|
|
|
|
icons = {}
|
|
for i in icon_data:
|
|
icons[i] = PhotoImage(data=icon_data[i])
|
|
|
|
def tool_but(txt, img=None, comm=None):
|
|
return ttk.Button(toolbar, text=txt, image=img, command=comm)
|
|
|
|
tool_but("New", icons["document-new"], handle_new).pack(side=LEFT)
|
|
tool_but("Open", icons["document-open"], handle_open).pack(side=LEFT)
|
|
tool_but("Save", icons["document-save"], handle_save).pack(side=LEFT)
|
|
tool_but("Reload", icons["document-revert"], handle_reload).pack(side=LEFT)
|
|
tool_but("Props", icons["document-properties"], edit_props).pack(side=LEFT)
|
|
|
|
ttk.Separator(toolbar, orient=VERTICAL).pack(
|
|
side=LEFT, padx=4, pady=4, fill="y")
|
|
|
|
tool_but("Clear", icons["edit-clear"], cancel_selection).pack(side=LEFT)
|
|
tool_but("Cut", icons["edit-cut"], handle_cut).pack(side=LEFT)
|
|
tool_but("Copy", icons["edit-copy"], handle_copy).pack(side=LEFT)
|
|
tool_but("Paste", icons["edit-paste"], handle_paste).pack(side=LEFT)
|
|
|
|
ttk.Separator(toolbar, orient=VERTICAL).pack(
|
|
side=LEFT, padx=4, pady=4, fill="y")
|
|
|
|
tool_but("Find", icons["edit-find"], handle_find).pack(side=LEFT)
|
|
tool_but("Edit", icons["edit-change"], start_editing).pack(side=LEFT)
|
|
|
|
ttk.Separator(toolbar, orient=VERTICAL).pack(
|
|
side=LEFT, padx=4, pady=4, fill="y")
|
|
|
|
tool_but("Insert", icons["list-add"], handle_insert).pack(side=LEFT)
|
|
tool_but("Delete", icons["list-remove"], handle_delete).pack(side=LEFT)
|
|
|
|
ttk.Separator(toolbar, orient=VERTICAL).pack(
|
|
side=LEFT, padx=4, pady=4, fill="y")
|
|
|
|
tool_but("Move left", icons["go-previous"], move_left).pack(side=LEFT)
|
|
tool_but("Move down", icons["go-down"], move_down).pack(side=LEFT)
|
|
tool_but("Move up", icons["go-up"], move_up).pack(side=LEFT)
|
|
tool_but("Move right", icons["go-next"], move_right).pack(side=LEFT)
|
|
|
|
ttk.Separator(toolbar, orient=VERTICAL).pack(
|
|
side=LEFT, padx=4, pady=4, fill="y")
|
|
|
|
tool_but("Link", icons["insert-link"], set_note_link).pack(side=LEFT)
|
|
|
|
def menu_item(menu, lab, under, accel=None, comm=None):
|
|
menu.add_command(
|
|
label=lab, underline=under, accelerator=accel, command=comm)
|
|
|
|
menubar = Menu(top)
|
|
|
|
m = Menu(menubar)
|
|
menu_item(m, "New", 0, "Ctrl-N", handle_new)
|
|
menu_item(m, "Open", 0, "Ctrl-O", handle_open)
|
|
menu_item(m, "Save", 0, "Ctrl-S", handle_save)
|
|
m.add_separator()
|
|
menu_item(m, "Save as...", 5, comm=handle_saveas)
|
|
menu_item(m, "Reload", 0, "Ctrl-R", handle_reload)
|
|
m.add_separator()
|
|
menu_item(m, "Properties", 6, "Ctrl-T", edit_props)
|
|
menu_item(m, "Statistics", 4, None, show_stats)
|
|
m.add_separator()
|
|
menu_item(m, "Quit", 0, "Ctrl-Q", handle_quit)
|
|
menubar.add_cascade(menu=m, label="File", underline=0)
|
|
|
|
m = Menu(menubar)
|
|
menu_item(m, "Clear", 2, "Escape", cancel_selection)
|
|
m.add_separator()
|
|
menu_item(m, "Cut", 0, "Ctrl-X", handle_cut)
|
|
menu_item(m, "Copy", 1, "Ctrl-C", handle_copy)
|
|
menu_item(m, "Paste", 0, "Ctrl-V", handle_paste)
|
|
m.add_separator()
|
|
menu_item(m, "Find...", 0, "Ctrl-F", handle_find)
|
|
menu_item(m, "Next", 0, "Ctrl-G", find_again)
|
|
menubar.add_cascade(menu=m, label="Edit", underline=0)
|
|
|
|
m = Menu(menubar)
|
|
menu_item(m, "Insert", 0, "Insert", handle_insert)
|
|
menu_item(m, "Delete", 0, "Delete", handle_delete)
|
|
m.add_separator()
|
|
menu_item(m, "Insert after...", 7, "Ctrl-I", insert_after)
|
|
menu_item(m, "Edit", 0, "Ctrl-E", start_editing)
|
|
m.add_separator()
|
|
menu_item(m, "Move up", 5, "Num 8", move_up)
|
|
menu_item(m, "Move right", 5, "Num 6", move_right)
|
|
menu_item(m, "Move left", 5, "Num 4", move_left)
|
|
menu_item(m, "Move down", 5, "Num 2", move_down)
|
|
menubar.add_cascade(menu=m, label="Note", underline=0)
|
|
|
|
m = Menu(menubar)
|
|
menu_item(m, "ToDo", 0, "Ctrl-1", lambda: set_note_status("TODO"))
|
|
menu_item(m, "Next", 0, "Ctrl-2", lambda: set_note_status("NEXT"))
|
|
menu_item(m, "Done", 0, "Ctrl-3", lambda: set_note_status("DONE"))
|
|
m.add_separator()
|
|
menu_item(m, "Reset", 0, "Ctrl-0", lambda: set_note_status(""))
|
|
m.add_separator()
|
|
menu_item(m, "Link...", 0, "Ctrl-L", set_note_link)
|
|
menu_item(m, "Feed URL...", 0, "Ctrl-K", set_note_feed)
|
|
menu_item(m, "Go to site", 0, "Ctrl-H", go_to_site)
|
|
menubar.add_cascade(menu=m, label="Stat", underline=0)
|
|
|
|
m = Menu(menubar)
|
|
menu_item(m, "Fold all", 0, "Ctrl -", fold_all)
|
|
menu_item(m, "Unfold all", 0, "Ctrl +", unfold_all)
|
|
m.add_separator()
|
|
menu_item(m, "Full screen", 10, "F11", full_screen)
|
|
menubar.add_cascade(menu=m, label="View", underline=0)
|
|
|
|
m = Menu(menubar, name="help")
|
|
menu_item(m, "About", 0, "F1", show_about)
|
|
menu_item(m, "Credits", 0, comm=show_credits)
|
|
menu_item(m, "Website", 0, comm=show_site)
|
|
menubar.add_cascade(menu=m, label="Help", underline=0)
|
|
|
|
top["menu"] = menubar
|
|
|
|
edit_line.bind("<Return>", lambda e: handle_edit_line())
|
|
edit_line.bind("<Escape>", lambda e: cancel_edit_line())
|
|
|
|
viewport.bind("<Control-e>", lambda e: start_editing())
|
|
|
|
viewport.bind("<Escape>", lambda e: cancel_selection())
|
|
viewport.bind("<Insert>", lambda e: handle_insert())
|
|
viewport.bind("<Delete>", lambda e: handle_delete())
|
|
|
|
viewport.bind("<KP_8>", lambda e: move_up())
|
|
viewport.bind("<KP_6>", lambda e: move_right())
|
|
viewport.bind("<KP_4>", lambda e: move_left())
|
|
viewport.bind("<KP_2>", lambda e: move_down())
|
|
|
|
try:
|
|
top.bind("<KP_Up>", lambda e: move_up())
|
|
top.bind("<KP_Right>", lambda e: move_right())
|
|
top.bind("<KP_Left>", lambda e: move_left())
|
|
top.bind("<KP_Down>", lambda e: move_down())
|
|
except TclError as e:
|
|
print("Keypad arrows won't be available", file=sys.stderr)
|
|
|
|
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("<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-f>", lambda e: handle_find())
|
|
top.bind("<Control-g>", lambda e: find_again())
|
|
top.bind("<Command-f>", lambda e: handle_find())
|
|
top.bind("<Command-g>", lambda e: find_again())
|
|
|
|
top.bind("<Control-Escape>", lambda e: cancel_selection())
|
|
top.bind("<Control-Insert>", lambda e: handle_insert())
|
|
top.bind("<Control-Delete>", lambda e: handle_delete())
|
|
top.bind("<Command-Escape>", lambda e: cancel_selection())
|
|
top.bind("<Command-Insert>", lambda e: handle_insert())
|
|
top.bind("<Command-Delete>", lambda e: handle_delete())
|
|
|
|
top.bind("<Control-i>", lambda e: insert_after())
|
|
top.bind("<Command-i>", lambda e: insert_after())
|
|
|
|
top.bind("<Control-Key-1>", lambda e: set_note_status("TODO"))
|
|
top.bind("<Control-Key-2>", lambda e: set_note_status("NEXT"))
|
|
top.bind("<Control-Key-3>", lambda e: set_note_status("DONE"))
|
|
top.bind("<Control-Key-0>", lambda e: set_note_status(""))
|
|
top.bind("<Command-Key-1>", lambda e: set_note_status("TODO"))
|
|
top.bind("<Command-Key-2>", lambda e: set_note_status("NEXT"))
|
|
top.bind("<Command-Key-3>", lambda e: set_note_status("DONE"))
|
|
top.bind("<Command-Key-0>", lambda e: set_note_status(""))
|
|
|
|
top.bind("<Control-l>", lambda e: set_note_link())
|
|
top.bind("<Control-k>", lambda e: set_note_feed())
|
|
top.bind("<Control-h>", lambda e: go_to_site())
|
|
top.bind("<Command-l>", lambda e: set_note_link())
|
|
top.bind("<Command-k>", lambda e: set_note_feed())
|
|
top.bind("<Command-h>", lambda e: go_to_site())
|
|
|
|
viewport.bind("<Control-x>", lambda e: handle_cut())
|
|
viewport.bind("<Control-c>", lambda e: handle_copy())
|
|
viewport.bind("<Control-v>", lambda e: handle_paste())
|
|
viewport.bind("<Command-x>", lambda e: handle_cut())
|
|
viewport.bind("<Command-c>", lambda e: handle_copy())
|
|
viewport.bind("<Command-v>", lambda e: handle_paste())
|
|
|
|
top.bind("<Control-minus>", lambda e: fold_all())
|
|
top.bind("<Control-plus>", lambda e: unfold_all())
|
|
top.bind("<Control-equal>", lambda e: unfold_all())
|
|
top.bind("<Command-minus>", lambda e: fold_all())
|
|
top.bind("<Command-plus>", lambda e: unfold_all())
|
|
top.bind("<Command-equal>", lambda e: unfold_all())
|
|
|
|
top.bind("<Control-q>", lambda e: handle_quit())
|
|
top.bind("<Command-q>", lambda e: handle_quit())
|
|
|
|
top.bind("<F1>", lambda e: show_about())
|
|
top.bind("<F11>", lambda e: full_screen())
|
|
|
|
top.protocol("WM_DELETE_WINDOW", handle_quit)
|
|
|
|
if len(sys.argv) < 2:
|
|
load_help()
|
|
elif load_file(sys.argv[1]):
|
|
outline_filename = sys.argv[1]
|
|
else:
|
|
load_help()
|
|
|
|
top.mainloop()
|