#!/usr/bin/env python2
# -*- coding: utf-8 -*-
#
#  sticknot.py
#  
#  A hierarchical note-taking application for GTK 
#
#  (C) 2014 inkeso
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import os, gtk, pango, gobject, re

#######################
# Configfile template #
#######################

CONFIGFILE = os.path.expanduser("~/.config/sticknot.conf")

# config-file will be written, when program is closed. (see SticKnot.on_close())
TEMPLATE = """# SticKnot-config

# Notes are stored in this folder (and subfolders). Textfiles with a little bit markup.
PATH = "%s"

# Window-Size: (width, height) or None for minimal size
SIZE = %s

# Window-Position: (x, y) or None for letting your WM decide
POSITION = %s

# Set to True to deactivate Windowmanager-decoration.
BORDERLESS = %s

# If set to True, SticKnot won't show up in your taskbar.
SKIPTASKBAR = %s

# Value between 1.0 (fully visible) and 0.0 (completly transparent)
OPACITY = %f

# Saved on Exit to reopen last note on next start
CURRENTFILE = "%s"

# Remember scrollbar-position in current note and cursor-position
SCROLLPOSITION = %s
"""

# DEFAULTS
PATH = "~/.sticknot"
#SIZE = (640, 480)
#POSITION = None
BORDERLESS = False
SKIPTASKBAR = False
OPACITY = 1.0

# try to load config
try:
    cfg = open(CONFIGFILE, "r")
    exec cfg.read()
    cfg.close()
    print "config loaded:", CONFIGFILE
except:
    pass

print "notes are stored at", PATH

# increase handle-size (may not be neccessary with a different theme)
gtk.rc_parse_string("style 'ihs' { GtkPaned::handle-size = 6 } widget '*' style 'ihs'")

################################################################################

class RichEditor(gtk.VBox):
    # Format-tags
    Tags = [ 
        ["Reset",     {}],
        ["Bold",      {'weight':     pango.WEIGHT_BOLD                                 }],
        ["Italic",    {'style':      pango.STYLE_ITALIC                                }],
        ["Mono",      {'family':     "monospace",      'wrap_mode':  gtk.WRAP_NONE     }],
        ["Red",       {'background': "#fbb",           'foreground': "#800"            }],
        ["Green",     {'background': "#bfb",           'foreground': "#080"            }],
        ["Blue",      {'background': "#bbf",           'foreground': "#008"            }],
        ["Yellow",    {'background': "#ffb",           'foreground': "#220"            }],
        ["H1",        {'size':       14 * pango.SCALE, 'weight':     pango.WEIGHT_BOLD, 'rise': 16 * pango.SCALE }],
        ["H2",        {'size':       12 * pango.SCALE, 'weight':     pango.WEIGHT_BOLD, 'rise': 14 * pango.SCALE }],
        ["H3",        {'size':       10 * pango.SCALE, 'weight':     pango.WEIGHT_BOLD, 'rise': 12 * pango.SCALE }],
        ["A",         {'underline':  pango.UNDERLINE_SINGLE,  'foreground': "#FFAF00"     }]
    ]

    __gsignals__ = {
        'urlclicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
    }
    
    def __init__(self):
        TagEvents = {
            'A': self.on_urlclick
        }

        self.currentfile = None # currently opened filename (full path)
        
        # scrollable editor
        self.scrolledit = gtk.ScrolledWindow()
        self.scrolledit.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.tb = gtk.TextBuffer()
        self.te = gtk.TextView(self.tb)
        self.te.set_wrap_mode(gtk.WRAP_WORD)
        self.te.set_size_request(200, 200)
        self.scrolledit.add(self.te)
        self.dform = self.tb.register_deserialize_format("text/sticknotml", self.deserialize)
        self.sform = self.tb.register_serialize_format("text/sticknotml", self.serialize)
        
        # create some tags for formating and add them to a toolbar
        self.formatbar = gtk.Toolbar()
        self.formatbar.set_style(gtk.TOOLBAR_TEXT)
        for tag, form in self.Tags:
            tagobj = self.tb.create_tag(tag, **form)
            
            if tag in TagEvents.keys(): # Add events (if any)
                tagobj.connect("event", TagEvents[tag])
            mi = gtk.ToolButton(label=tag)
            mi.connect("clicked", self.on_applyformat, tag)
            self.formatbar.insert(mi, -1)
        
        # reuse last toolbutton to create large pixmaps for dir and error
        self.pbdir = mi.render_icon(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_DIALOG)
        self.pberr = mi.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_DIALOG)
        
        #self.widget = gtk.VBox()
        #self.widget.pack_start(self.formatbar, expand=False)
        #self.widget.pack_start(self.scrolledit, expand=True)
        gtk.VBox.__init__(self)
        self.pack_start(self.formatbar, expand=False)
        self.pack_start(self.scrolledit, expand=True)
        self.te.connect("motion-notify-event", self.hover)
        self.standard_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
        self.link_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
    
    def hover(self, win, ev):
        itr = win.get_iter_at_location(int(ev.x), int(ev.y))
        if itr.has_tag(self.tb.tag_table.lookup("A")): 
            win.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(self.link_cursor)
        else:
            win.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(self.standard_cursor)
    
    def on_urlclick(self, texttag, widget, event, itr):
        if event.type == gtk.gdk.BUTTON_RELEASE and event.button == 1:
            # get iter at mouseposition
            bx, by = self.te.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, int(event.x), int(event.y))
            off = self.te.get_iter_at_location(bx, by).get_offset()
            aqui = self.tb.get_iter_at_offset(off)
            # look for start of this tag
            if not aqui.begins_tag(texttag): aqui.backward_to_tag_toggle(texttag)
            # recort start & search for end
            start = aqui.copy()
            aqui.forward_to_tag_toggle(texttag)
            # aquire link-text and emit event with it
            link = self.tb.get_text(start, aqui)
            self.emit("urlclicked", link)
    
    def on_applyformat(self, widget, tag):
        # get selection, apply selected format-tag
        if self.tb.get_has_selection():
            selc = self.tb.get_selection_bounds()
            if tag == "Reset": 
                self.tb.remove_all_tags(*selc)
            else: 
                self.tb.apply_tag_by_name(tag, *selc)
            self.tb.set_modified(True)
    
    def serialize(self, rb, cb, start, end):
        """
        rb is the textbuffer that the format is registered with
        cb is the textbuffer containing the text to be serialized
        start and end are textiters bounding the text to be serialized
        function should return the serialized data
        """
        # insert start-tags (if any)
        targ = "".join(["<%s>" % p.get_property("name") for p in start.get_toggled_tags(True)])
        while start.get_offset() != end.get_offset():
            # find next tag
            nxt = start.copy()
            nxt.forward_to_tag_toggle(None)
            # append text from start to next (and replace markup-chars)
            targ += start.get_text(nxt).replace("<","&lt;").replace(">","&gt;")
            # insert off-tag(s)
            targ += "".join(["</%s>" % p.get_property("name") for p in nxt.get_toggled_tags(False)])
            # insert on-tag(s)
            targ += "".join(["<%s>" % p.get_property("name") for p in nxt.get_toggled_tags(True)])
            # jump
            start = nxt
        return targ


    def deserialize(self, rb, cb, itr, data, creatag):
        """
        rb is the textbuffer that the format is registered with
        cb is the textbuffer that data will be deserialized into
        data is the string to be deserialized
        itr is a textiter indicating the start of the deserialized data in cb
        creatag is a boolean indicating if tags should be created during deserialization
        function should return True if the data was successfully deserialized
        """
        # get a list of valid tags
        valid = []
        cb.get_tag_table().foreach(lambda x,y: y.append("<%s>" % x.get_property("name")), valid)

        # tagstack is a dict of lists: each tag is a key, the list represents the stack.
        # each list-item is a charcount (startpoint of this tag).
        # the last opened tag will be applied, once it's closed.
        tagstack = {}
        for t in valid: tagstack[t] = []
        foundtags = filter(lambda t: t.replace("/","") in valid, re.findall("<.+?>", data))
        if len(foundtags) == 0: # no tags? insert all, replacing markup-chars
            cb.insert(itr, data.replace("&lt;","<").replace("&gt;",">"))
            return True
        
        # some tags found. should be an even number
        assert len(foundtags) % 2 != 1, "Odd number of tags in textfile. Loading failed."
        
        for tag in foundtags:
            ti = data.find(tag)
            # insert text from current index up to the first tag, replacing markup-chars
            cb.insert(itr, data[:ti].replace("&lt;","<").replace("&gt;",">"))
            # truncate data
            data = data[ti + len(tag):]
            
            # TODO: avoid get_end_iter, because it may fail, if we are inserting into
            # a nonempty buffer. Should never happen, ATM, so it's less urgent
            itr = cb.get_end_iter()
            if tag[1] != "/": # open-tag; record to tagstack
                tagstack[tag].append(cb.get_char_count())
            else:             # close-tag; pop last iter, if any, and apply TextTag
                tag = tag.replace("/","")
                assert len(tagstack[tag]) > 0, "Nonmatching close-tag  %s found. Loading Failed." % tag
                cb.apply_tag_by_name(tag[1:-1], cb.get_iter_at_offset(tagstack[tag].pop()), itr)

        # tagstack empty?
        for k, v in tagstack.items():
            assert len(v) == 0, "Tag %s left open. Loading failed." % k

        # append last part (from last tag to EOF)
        cb.insert(itr, data.replace("&lt;","<").replace("&gt;",">"))
        return True
    
    def clear(self):
        self.tb.set_text("")
        self.tb.set_modified(False)
        self.currentfile = None
        return self.tb.get_start_iter()

    def load(self, path):
        # reset current file & clear buffer
        ti = self.clear()
        if os.path.isdir(path):        # if path is a directory: show brief info
            self.tb.insert_pixbuf(ti, self.pbdir)
            self.tb.insert_with_tags_by_name(ti, " Folder \n\n", "H1")
            self.tb.insert_with_tags_by_name(ti, path, "Mono")
        else:                          # if it's a file: load into editor
            try:
                fin = open(path, "r")
                cont = fin.read()
                if cont: self.tb.deserialize(self.tb, self.dform, ti, cont)
                fin.close()
                self.currentfile = path
                print "loaded:", path
            except Exception as e:
                self.tb.insert_pixbuf(ti, self.pberr)
                self.tb.insert_with_tags_by_name(ti, " Error \n\n", "H1")
                self.tb.insert_with_tags_by_name(ti, str(e), "Mono")
        self.tb.set_modified(False)

    def save(self):
        # save current file, if modified
        if self.currentfile and self.tb.get_modified():
            text = self.tb.serialize(self.tb, self.sform, *self.tb.get_bounds())
            try:
                fout = open(self.currentfile, "w")
                fout.write(text)
                fout.close()
                print "saved:", self.currentfile
                self.tb.set_modified(False)
            except:
                self.dlg.message("Could not save file " + self.currentfile)
                return False
        return True
    
    def get_pos(self):
        # get scrollbarpositions. return a tuple of x and y offset and cursor-position
        return (self.scrolledit.get_hadjustment().get_value(),
                self.scrolledit.get_vadjustment().get_value(),
                self.tb.get_iter_at_mark(self.tb.get_insert()).get_offset())
    
    def set_pos(self, pos):
        # get scrollbarpositions. return a tuple of x and y offset
        self.scrolledit.get_hadjustment().set_value(pos[0])
        self.scrolledit.get_vadjustment().set_value(pos[1])
        self.tb.place_cursor(self.tb.get_iter_at_offset(pos[2]))
    
    def focus(self):
        self.te.grab_focus()

class Dialogs:
    '''
    show a message.
    '''
    def message(self, message):
        dialog = gtk.MessageDialog(None,
                                   gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                                   gtk.MESSAGE_INFO, gtk.BUTTONS_OK, message)
        dialog.set_border_width(10)
        dialog.run()
        dialog.destroy()
        
    '''
    show a confirmation-dialog (OK/Cancel) return True if OK.
    '''
    def confirm(self, message):
        label = gtk.Label("\n"+message+"\n")
        dialog = gtk.Dialog("Confirm", None,
                    gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                   (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
                    gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
        dialog.set_border_width(10)
        dialog.vbox.pack_start(label)
        label.show()
        response = dialog.run()
        dialog.destroy()
        return (response == gtk.RESPONSE_ACCEPT)
    
    '''
    show a simple input-dialog. return input-text or None
    '''
    def input(self, message, eTxt=""):
        label = gtk.Label("\n"+message+"\n")
        dialog = gtk.Dialog("Input", None,
                    gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                   (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, 
                    gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT))
        dialog.set_border_width(10)
        dialog.vbox.pack_start(label)
        label.show()
        entry = gtk.Entry()
        entry.set_text(eTxt)
        dialog.vbox.pack_end(entry)
        entry.show()
        entry.grab_focus()
        entry.connect("activate", lambda w: dialog.response(gtk.RESPONSE_ACCEPT))
        response = dialog.run()
        rtxt = entry.get_text()
        dialog.destroy()
        if response == gtk.RESPONSE_ACCEPT and len(rtxt) > 0:
            return rtxt
        else:
            return None

class SticKnot:
    # Toolbar-buttons
    Buttons = [ 
        ["New Note",   gtk.STOCK_NEW,       "on_newnote"],
        ["New Folder", gtk.STOCK_DIRECTORY, "on_newfolder"],
        ["Rename",     gtk.STOCK_SAVE_AS,   "on_rename"],
        ["Delete",     gtk.STOCK_DELETE,    "on_delete"],
        ["Refresh",    gtk.STOCK_REFRESH,   "on_refresh"]
    ]
    
    def __init__(self):
        self.notes = os.path.expanduser(PATH) # actual path
        self.files = [] # contains a recursive list of files. See self.on_refresh()
        
        # Filetree (in a scrolled view)
        self.scrolltree = gtk.ScrolledWindow()
        self.scrolltree.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.treestore = gtk.TreeStore(str, gtk.gdk.Pixbuf)
        self.treeview = gtk.TreeView(self.treestore)
        
        # TreeViewColumn containing icon & filename
        one = gtk.TreeViewColumn("Notes")
        crpb = gtk.CellRendererPixbuf()
        crtx = gtk.CellRendererText()
        one.pack_start(crpb, expand=False)
        one.pack_start(crtx, expand=True)
        one.add_attribute(crpb, 'pixbuf', 1)
        one.add_attribute(crtx, 'text', 0)
        self.treeview.append_column(one)
        self.treeview.set_headers_visible(False)
        self.treeview.set_search_column(0)
        self.scrolltree.add(self.treeview)
        self.treeview.connect("cursor_changed", self.on_filechange)
        # Allow enable drag and drop of rows
        self.treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, [('TREEROW', gtk.TARGET_SAME_WIDGET, 0)], gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_MOVE)
        self.treeview.enable_model_drag_dest([('TREEROW', gtk.TARGET_SAME_WIDGET, 0)], gtk.gdk.ACTION_DEFAULT)
        self.treeview.connect("drag_data_get", self.drag_get_data)
        self.treeview.connect("drag_data_received", self.drag_rec_data)
        
        # Buttons
        self.butts = gtk.Toolbar()
        self.butts.set_style(gtk.TOOLBAR_ICONS)
        self.butts.set_tooltips(True)
        self.butts.set_size_request(170, -1)
        for (n, s, e) in self.Buttons:
            img = gtk.Image()
            img.set_from_stock(s, gtk.ICON_SIZE_BUTTON)
            mi = gtk.ToolButton(img, n)
            mi.set_tooltip_text(n)
            mi.connect("clicked", eval("self."+e))
            self.butts.insert(mi, -1)
        
        # reuse the last (temporary) Image-Object for creating two pixmaps for
        # files & folders
        self.pbdir  = mi.render_icon(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU)
        self.pbfile = mi.render_icon(gtk.STOCK_FILE, gtk.ICON_SIZE_MENU)
        
        # combine tree and buttons
        self.leftpane = gtk.VBox()
        self.leftpane.pack_start(self.scrolltree, expand=True, fill=True)
        self.leftpane.pack_start(self.butts, expand=False, fill=True)
        
        # editor
        self.editor = RichEditor()
        self.editor.connect("urlclicked", self.urlopen)
        
        # Tree and editor in a HPane
        self.hpane = gtk.HPaned()
        self.hpane.set_border_width(6)
        self.hpane.pack1(self.leftpane, shrink=False)
        self.hpane.pack2(self.editor, shrink=False)
        
        # Main Window
        self.wind = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.wind.set_title("SticKnot")
        self.wind.set_icon(mi.render_icon(gtk.STOCK_EDIT, gtk.ICON_SIZE_MENU))
        self.wind.connect("delete_event", self.on_close)
        self.wind.connect("event", self.on_generic)
        self.wind.add(self.hpane)
        self.wind.set_opacity(OPACITY)
        self.wind.set_decorated(not BORDERLESS)
        self.wind.set_skip_taskbar_hint(SKIPTASKBAR)
        try: self.wind.resize(*SIZE)
        except: pass
        try: self.wind.move(*POSITION)
        except: pass
        self.dlg = Dialogs()
    
    def main(self):
        # Check for notes-dir (create if neccessary)
        if not os.path.exists(self.notes):
            mkdir = self.dlg.confirm("Folder »%s« doesn't exists. Create?" % self.notes)
            if (mkdir):
                try: os.makedirs(self.notes)
                except: self.dlg.message("Creation of directory failed.")
            else:
                exit(1)
        try: # setting currentfile from config
            if CURRENTFILE != "None": self.editor.currentfile = CURRENTFILE
        except:
            pass
        self.on_refresh(None) # load filetree
        # Ready to go: paint window
        self.wind.show_all()
        while gtk.events_pending(): gtk.main_iteration()
        try: # setting scrollposition and cursor in textview
            self.editor.set_pos(SCROLLPOSITION)
            self.editor.focus()
        except:
            pass
        gtk.main()
    
    def get_tree(self, sDir=None):
        if sDir == None: sDir = self.notes
        tree = { 
            "path": sDir, 
            "name": os.path.basename(sDir),
            "files": [],
            "dirs": []     # contains a list of subtrees of same format
        }
        for fnam in os.listdir(sDir):
            if fnam[0] == '.': continue         # ignore hidden files
            if fnam == 'lost+found': continue   # ignore journal
            nKey = os.path.join(sDir, fnam)
            if os.path.isdir(nKey):
                tree["dirs"].append(self.get_tree(nKey))
            else:
                tree["files"].append(nKey)
        # sort
        tree["files"].sort(key=str.lower)
        tree["dirs"].sort(key=lambda x: x["name"].lower())
        return tree
    
    def add_treeview(self, tree, root=None):
        # add subdirs
        for subtree in tree["dirs"]:
            piter = self.treestore.append(root, [subtree["name"], self.pbdir])
            self.add_treeview(subtree, piter)
        # add files
        for leaf in tree["files"]:
            self.treestore.append(root, [os.path.basename(leaf), self.pbfile])
    
    def on_refresh(self, widget, data=None):
        # repopulate Treeview
        self.files = self.get_tree()
        self.treestore.clear()
        self.add_treeview(self.files)
        self.treeview.expand_all()
        # jump to currentfile, if any
        if self.editor.currentfile:
            # find path in tree & set cursor to it
            def func(model, path, itr):
                if self.editor.currentfile == self.path_from_iter(itr):
                    self.tpath = path
            self.treestore.foreach(func)
            self.treeview.set_cursor(self.tpath)

    def drag_get_data(self, treeview, context, selection, target_id, etime):
        itr = treeview.get_selection().get_selected()[1]
        selection.set(selection.target, 8, self.path_from_iter(itr))

    def drag_rec_data(self, tv, context, x, y, sel, info, etime):
        drop_info = tv.get_dest_row_at_pos(x, y)
        targpath = self.notes
        if drop_info:
            targpath = self.path_from_iter(tv.get_model().get_iter(drop_info[0]))
        if os.path.isfile(targpath): targpath = os.path.split(targpath)[0]
        targpath = os.path.join(targpath, os.path.basename(sel.data))
        print "move:", sel.data, "-->", targpath
        os.rename(sel.data, targpath)
        self.editor.currentfile = targpath
        self.on_refresh(None)
    
    def on_newnote(self, widget, data=None):
        # create new file in current folder (or top if none)
        curp = self.path_from_cursor()
        if curp is None: curp = self.notes
        if os.path.isfile(curp): curp = os.path.split(curp)[0]
        path = self.dlg.input("Create new note in »%s«" % curp)
        if path is not None:
            path = os.path.join(curp, path)
            open(path, "w").close()
            print "new file:", path
            self.editor.load(path)
            self.on_refresh(None)
    
    def on_newfolder(self, widget, data=None):
        # create new subfolder in current folder (or top if none)
        curp = self.path_from_cursor()
        if curp is None: curp = self.notes
        if os.path.isfile(curp): curp = os.path.split(curp)[0]
        path = self.dlg.input("Create new folder in »%s«" % curp)
        if path:
            path = os.path.join(curp, path)
            os.makedirs(path)
            print "new dir: ", path
            self.editor.currentfile = path
            self.on_refresh(None)
    
    def on_rename(self, widget, data=None):
        # rename (and reopen) current file
        curfile = self.editor.currentfile or self.path_from_cursor()
        curf = os.path.split(curfile)
        newname = self.dlg.input("Rename", curf[1])
        if newname:
            newf = os.path.join(curf[0], newname)
            os.rename(curfile, newf)
            print "rename:", curfile, "-->", newf
            self.editor.currentfile = newf
            self.on_refresh(None)
    
    def on_delete(self, widget, data=None):
        # delete current file
        delfile = self.editor.currentfile or self.path_from_cursor()
        def delnode(arg):
            if os.path.isfile(arg):
                os.remove(arg)
                print "deleted file:", arg
            else:
                for f in os.listdir(arg): delnode(os.path.join(arg, f))
                os.rmdir(arg)
                print "deleted dir: ", arg
        
        if self.dlg.confirm("Delete %s »%s« ?" % ("Folder" if os.path.isdir(delfile) else "Note", delfile)):
            delnode(delfile)
            self.editor.currentfile = None
            self.editor.clear()
            self.on_refresh(None)
    
    def path_from_cursor(self):
        # return the path of the currently selected file or folder
        try:
            itr = self.treestore.get_iter(self.treeview.get_cursor()[0])
            return self.path_from_iter(itr)
        except:
            return None

    def path_from_iter(self, itr):
        # get filepath from a given treeiter
        cfile = [self.treestore.get_value(itr, 0)]
        # prepend parents
        for o in range(self.treestore.iter_depth(itr)):
            itr = self.treestore.iter_parent(itr)
            cfile.insert(0, self.treestore.get_value(itr, 0))
        # prepend notes-dir and join everything to full absolute path
        cfile.insert(0, self.notes)
        return os.path.join(*cfile)
    
    def on_filechange(self, widget, data=None):
        # eventhandler for selecting a different row in the treeview.
        # save the current one and load the new one
        if not self.editor.save(): return
        self.editor.load(self.path_from_cursor())
    
    def on_close(self, widget, data=None):
        if self.editor.save(): 
            # export settings
            try:
                cfg = open(CONFIGFILE, "w")
                cfg.write(TEMPLATE % (
                    PATH, 
                    self.wind.get_size(), 
                    self.wind.get_position(), 
                    BORDERLESS, 
                    SKIPTASKBAR, 
                    OPACITY, 
                    self.editor.currentfile,
                    self.editor.get_pos()
                ))
                cfg.close()
                print "config written to:", CONFIGFILE
            except:
                print "Couldn't save config"
            gtk.main_quit()
            return False
        else:
            return True
    
    def on_generic(self, widget, data=None):
        # all events pass through this function, so we can close-on-Esc
        if data.type == gtk.gdk.KEY_PRESS and data.keyval == gtk.keysyms.Escape:
            self.on_close(None)
        return False
    
    def urlopen(self, event, url):
        print url
        os.system("firefox \""+url+"\"")

sk = SticKnot()
sk.main()
