💾 Archived View for gemini.ctrl-c.club › ~nttp › toys › outnoted › outnoted.py captured on 2023-01-29 at 04:09:59.

View Raw

More Information

⬅️ Previous capture (2021-12-03)

➡️ Next capture (2023-03-20)

🚧 View Differences

-=-=-=-=-=-=-

#!/usr/bin/env python3
# coding=utf-8
#
# OutNoted: an outline note-taking editor
# 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

import webbrowser
import os.path
import json
import sys

if sys.version_info.major >= 3:
	from tkinter import *
	from tkinter import ttk
	from tkinter.simpledialog import askstring
	from tkinter.messagebox import showerror, askyesno
	from tkinter.filedialog import askopenfilename, asksaveasfilename
else:
	from Tkinter import *
	import ttk
	from tkSimpleDialog import askstring
	from tkMessageBox import showerror, askyesno
	from tkFileDialog import askopenfilename, asksaveasfilename

import re
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")
			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 "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

file_types = [("All files", ".*"),
	'"OutNoted files" {.out .json}',
	("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")

toolbar = ttk.Frame(top)

edit_text = StringVar()
edit_line = ttk.Entry(top, textvariable=edit_text)

viewport = ttk.Treeview(top, height=20, selectmode="browse",
	columns=('status', 'link'), 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)

bookmark_icon_data = """
R0lGODlhGAAYAIABAAAAAP///yH5BAEKAAEALAAAAAAYABgAAAJKjI+py+0PgZxUwooBlExCyUiQ
xEiQxEiQxEiQxEiQxEiQxEiQdEiHBJEYKAYJRBLABCSQiqECoSAokIliApEwJBCAAwAJi8dkRgEA
Ow==
"""
bookmark_icon = PhotoImage(data=bookmark_icon_data)
app_icon_data = """
R0lGODdhIAAgALEAAAAAAP8AAICAgP///yH5BAEAAAEALAAAAAAgACAAAAKmjI+py+0P4wO0WirV
3SAnOoRiiGWCYICjWHIVcqbAynaBWxrxPdNDvgDuVDSgxqZD8Xw/ZNA57BUBp2pVBlMSVxSrFXuI
1saraFZGqqRp5rB2tmmy32fey74NtZMyoGUOxmeXIKBWRucW6JY3sBcwtHDyA8hTxyCpJyDiuHOp
2ViVqdgZqelliph0uuOlyrnKeuXmCCGbpeQhqEDqgRuZCxwsPLxQAAA7
"""
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 1.3.1a (30 December 2022), 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 and / or link.")
	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.")
	viewport.insert("usage", "end",
		text="Right-click in the main view for a context menu.")

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)
		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
		outline.append(child)
	return outline

def parse_file(f, ext):
	if ext == ".out" or ext == ".json":
		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 opening file", str(e), parent=top)
	except TypeError as e:
		showerror("Error opening file", str(e), parent=top)
	except KeyError as e:
		showerror("Error opening 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 opening file",
			"File missing head or body: " + str(e),
			parent=top)
	except ExpatError as e:
		showerror("Error opening 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 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 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 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
	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_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_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)
		if answer == "":
			viewport.item(focus, image="")
		else:
			viewport.item(focus, image=bookmark_icon)
		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.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 viewport.get_children():
		viewport.item(i, open=0)

def unfold_all():
	for i in viewport.get_children():
		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 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."

toolbar.pack(side=TOP, fill="x", padx=2, pady=2)
edit_line.pack(side=TOP, fill="x", padx=2, pady=2, ipadx=2, ipady=2)

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)

def toolbutt(txt, under=None, cmd=None):
	return ttk.Button(
		toolbar,
		text=txt,
		width=6,
		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)
toolbutt("Edit", 0, start_editing).pack(side=LEFT)

ttk.Separator(toolbar, orient=VERTICAL).pack(
	side=LEFT, padx=4, pady=4, fill="y")

toolbutt("1 ToDo", 0, lambda: set_note_status("TODO")).pack(side=LEFT)
toolbutt("2 Next", 0, lambda: set_note_status("NEXT")).pack(side=LEFT)
toolbutt("3 Done", 0, lambda: set_note_status("DONE")).pack(side=LEFT)

ttk.Separator(toolbar, orient=VERTICAL).pack(
	side=LEFT, padx=4, pady=4, fill="y")

menu_button = ttk.Menubutton(toolbar, text="Menu", underline=0, width=5)
menu_button.pack(side=LEFT)

main_menu = Menu(menu_button, tearoff=0)
main_menu.add_command(
	label="Reset note", underline=7, accelerator="Ctrl-0",
	command=lambda: set_note_status(""))
#main_menu.add_separator()
main_menu.add_command(
	label="Link...", underline=0, accelerator="Ctrl-L",
	command=set_note_link)
main_menu.add_command(
	label="Go to site", underline=0, accelerator="Ctrl-H",
	command=go_to_site)
main_menu.add_separator()
main_menu.add_command(
	label="Cut", underline=0, accelerator="Ctrl-X", command=handle_cut)
main_menu.add_command(
	label="Copy", underline=1, accelerator="Ctrl-C", command=handle_copy)
main_menu.add_command(
	label="Paste", underline=0, accelerator="Ctrl-V", command=handle_paste)
main_menu.add_separator()
main_menu.add_command(
	label="Fold all", underline=0, accelerator="Ctrl -",
	command=fold_all)
main_menu.add_command(
	label="Unfold all", underline=0, accelerator="Ctrl +",
	command=unfold_all)
main_menu.add_separator()
main_menu.add_command(label="Properties...", underline=6, command=edit_props)
main_menu.add_command(label="Save as...", underline=0, command=handle_saveas)
main_menu.add_command(label="Reload...", underline=0, command=handle_reload)
main_menu.add_separator()
main_menu.add_command(
	label="Quit", underline=0, accelerator="Ctrl-Q",
	command=handle_quit)

menu_button.configure(menu=main_menu)

context_menu = Menu(top, tearoff=0)
context_menu.add_command(
	label="Insert", underline=2, accelerator="Ins",
	command=handle_insert)
context_menu.add_command(
	label="Delete", underline=4, accelerator="Del",
	command=handle_delete)
context_menu.add_command(
	label="Clear", underline=3, accelerator="Esc",
	command=cancel_selection)
context_menu.add_separator()
context_menu.add_command(
	label="Insert after...", underline=7, accelerator="Ctrl-I",
	command=insert_after)
context_menu.add_separator()
context_menu.add_command(
	label="Move up", underline=5, accelerator="Num 8",
	command=move_up)
context_menu.add_command(
	label="Move right", underline=5, accelerator="Num 6",
	command=move_right)
context_menu.add_command(
	label="Move left", underline=5, accelerator="Num 4",
	command=move_left)
context_menu.add_command(
	label="Move down", underline=5, accelerator="Num 2",
	command=move_left)

if top.tk.call('tk', 'windowingsystem') == 'aqua':
	top.bind('<2>',
		lambda e: context_menu.post(e.x_root, e.y_root))
	top.bind('<Control-1>',
		lambda e: context_menu.post(e.x_root, e.y_root))
else:
	top.bind('<3>',
		lambda e: context_menu.post(e.x_root, e.y_root))

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("<Double-1>", lambda e: handle_insert())

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-h>", lambda e: go_to_site())
top.bind("<Command-l>", lambda e: set_note_link())
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.protocol("WM_DELETE_WINDOW", handle_quit)

if top.tk.call('tk', 'windowingsystem') == 'x11':
	ttk.Style().theme_use('clam')

if len(sys.argv) < 2:
	load_help()
elif load_file(sys.argv[1]):
	outline_filename = sys.argv[1]
else:
	load_help()	

top.mainloop()