#!/usr/bin/python
#encoding: utf-8
# TabNote - Note Taking Application
# Copyright 2011 Paul Hänsch
# Contact: haensch.paul@gmail.com
#
# 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 3 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, see .
from Tkinter import *
from pysqlite2 import dbapi2
import os
import Pmw
class Persistence():
"""
Database storage layer
"""
def __init__(self):
"""
Constructor
Open a database, create and initialize the database if needed
"""
self.dbcon = dbapi2.connect(os.getenv('HOME') + os.sep + '.tabnote.sqlite')
self.dbcur = self.dbcon.cursor()
self.initDB()
def initDB(self):
initnote = ('Welcome to TabNote!\n\n' +
'Click the "+"-Tab to open a new note.\n' +
'Double Click on the selected tab to relabel or delete it.\n' +
'Notes are stored when you exit the program.\n\n' +
'Shortcuts:\n' +
'Ctrl Left - switch to previous tab\n' +
'Ctrl Right - switch to next tab\n' +
'Ctrl Return - bring up relabel/delete dialog\n' +
'Ctrl t - create new tab\n' +
'Ctrl q - save and quit')
initnote = self.escape(initnote)
try:
self.dbcur.execute('CREATE TABLE settings (key TEXT, value TEXT, UNIQUE (key));')
self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("version", "1.6");')
self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("geometry", "420x300");')
self.dbcur.execute('CREATE TABLE notes (uuid TEXT, seq INTEGER PRIMARY KEY, label TEXT, ' +
'content TEXT, revision DATETIME, lastsync DATETIME, UNIQUE (uuid));')
self.dbcur.execute('INSERT INTO notes (uuid, label, content, revision, lastsync) VALUES ' +
'(lower(hex(randomblob(16))), "Intro", "' + initnote +
'", datetime(\'now\'), datetime(\'1970-01-01\'));')
except: pass
def getSetting(self, key):
self.dbcur.execute('SELECT value FROM settings WHERE key = "' + self.escape(key) + '";')
try: return self.dbcur.fetchall()[0][0]
except: return ''
def setSetting(self, key, value):
try:
self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("' + self.escape(key) +
'", "' + self.escape(value) + '");')
except:
self.dbcur.execute('UPDATE settings SET value = "' + self.escape(value) +
'" WHERE key = "' + self.escape(key) + '";')
def escape(self, string):
"""
Return sanitized string for use in SQL-Statements
"""
ret = ''
for c in string:
if c == '"': ret += '""'
else: ret += c
return str(ret)
def allRecords(self):
"""
Return list of all recorded notes
Each item is an array of the format [id, label, textcontent]
"""
self.dbcur.execute('SELECT uuid, label, content FROM notes ORDER BY seq;')
return self.dbcur.fetchall()
def setRecord(self, uuid = None, label = None, content = None):
"""
Create or alter a record
"""
if not uuid:
if label: label = self.escape(label)
else: label = ''
if content: content = self.escape(content)
else: content = ''
self.dbcur.execute('INSERT INTO notes (uuid, label, content, revision, lastsync) VALUES ' +
'(lower(hex(randomblob(16))), "' + str(label) + '", "' + str(content) +
'", datetime(\'now\'), datetime(\'1970-01-01\'));')
else:
if content:
self.dbcur.execute('UPDATE notes SET content = "' + self.escape(content) +
'", revision = datetime(\'now\') WHERE uuid = "' + str(uuid) +
'" AND NOT content = "' + self.escape(content) + '";')
if label:
self.dbcur.execute('UPDATE notes SET label = "' + self.escape(label) +
'", revision = datetime(\'now\') WHERE uuid = "' + str(uuid) + '";')
try:
self.dbcur.execute('SELECT uuid FROM notes WHERE seq = "' + str(self.dbcur.lastrowid) + '";')
return self.dbcur.fetchall()[0][0]
except: return 0
def delRecord(self, uuid):
"""
Delete record by ID
"""
self.dbcur.execute('DELETE FROM notes WHERE uuid = "' + str(uuid) + '";')
def close(self):
"""
Flush and close databese
"""
self.dbcur.close()
self.dbcon.commit()
self.dbcon.close()
class Main(Tk):
"""
Main application class
"""
def __init__(self):
"""
Open Database, create Main application window, and retrieve stored data
"""
Tk.__init__(self)
self.storage = Persistence()
self.protocol('WM_DELETE_WINDOW', self.quit)
self.title('TabNote')
self.relabelWin = False
self.contents = []
self.tablist = Pmw.NoteBook(self, raisecommand = self.tabselect, lowercommand = self.tabunselect)
self.tablist.add('')
self.tablist.pack(expand = True, fill = BOTH)
for tab in self.storage.allRecords():
self.addTab(tab[0], tab[1], tab[2])
self.tablist.delete(Pmw.END)
self.tablist.add('+')
self.bind_all('', self.previouspage)
self.bind_all('', self.nextpage)
self.bind_all('', self.newTab)
self.bind_all('', self.quit)
self.bind_all('', self.tabedit)
try:
self.geometry(self.storage.getSetting('geometry'))
self.tablist.selectpage(self.storage.getSetting('activeTab'))
except: pass
def newTab(self, junk):
"""
Event wrapper for new tab
"""
self.tablist.selectpage('+')
def previouspage(self, junk):
"""
Event wrapper for tab switching
"""
if self.tablist.getcurselection() == self.tablist.pagenames()[0]:
self.tablist.selectpage(self.tablist.pagenames()[-2])
else:
self.tablist.previouspage()
def nextpage(self, junk):
"""
Event wrapper for tab switching
"""
if self.tablist.getcurselection() == self.tablist.pagenames()[-2]:
self.tablist.selectpage(0)
else:
self.tablist.nextpage()
def closeRelabel(self, junk = None):
"""
Close relabeling dialog window
"""
self.relabelWin.destroy()
self.relabelWin = False
def delTab(self, junk = None):
"""
Delete a tab (and its record)
Called by relabeling dialog
"""
for tab in self.contents:
if tab[1] == self.tablist.page(self.relabel):
self.storage.delRecord(tab[0])
self.contents.remove(tab)
self.tablist.previouspage()
self.tablist.delete(self.relabel)
break
self.closeRelabel()
def relabelTab(self, junk = None):
"""
Change the name of a tab
Called by relabeling dialog
"""
for tab in self.contents:
if tab[1] == self.tablist.page(self.relabel) and self.relabel != self.relabelText.get():
label = self.newName(self.relabelText.get())
content = tab[2].getvalue()[:-1]
tab[1] = self.tablist.insert(label, before = self.tablist.index(self.relabel))
self.tablist.tab(label).bind(sequence = '', func = self.tabedit)
tab[2] = Pmw.ScrolledText(tab[1])
tab[2].setvalue(content)
tab[2].pack(expand = True, fill = BOTH)
self.tablist.previouspage()
self.tablist.delete(self.relabel)
self.storage.setRecord(tab[0], label = label)
break
self.closeRelabel()
def tabedit(self, junk):
"""
Bring up relabeling dialog
"""
if not self.relabelWin:
self.relabel = self.tablist.getcurselection()
self.relabelWin = Tk()
self.relabelWin.bind('', self.closeRelabel)
self.relabelWin.protocol('WM_DELETE_WINDOW', self.closeRelabel)
self.relabelText = Entry(self.relabelWin)
self.relabelText.insert(0, self.relabel)
self.relabelText.focus()
self.relabelText.bind('', self.relabelTab)
self.relabelText.pack(side = TOP, fill = X, expand = True)
relabelButton = Button(self.relabelWin, text='Relabel', command=self.relabelTab)
relabelButton.bind('', self.relabelTab)
relabelButton.pack(side = LEFT, fill = BOTH, expand = True)
deleteButton = Button(self.relabelWin, text='Delete', command=self.delTab)
deleteButton.bind('', self.delTab)
deleteButton.pack(side = LEFT, fill = BOTH, expand = True)
def newName(self, basename = 'New'):
"""
Return a unique name for a tab by appending a number to the given basename
"""
if not basename in self.tablist.pagenames(): return basename
incrementor = 1
while (basename + str(incrementor)) in self.tablist.pagenames(): incrementor += 1
return (basename + str(incrementor))
def tabunselect(self, name):
"""
Called when a tab becomes unselected, write the DB record for this tab
"""
for tab in self.contents:
if tab[1] == self.tablist.page(name):
self.storage.setRecord(tab[0], content = tab[2].getvalue()[:-1])
def addTab(self, rid, label = 'New', content = ""):
"""
Create a new tab
"""
label = self.newName(label)
self.contents.append([rid, self.tablist.insert(label, before = self.tablist.index(Pmw.END)), None])
self.tablist.tab(label).bind(sequence = '', func = self.tabedit)
self.contents[-1][2] = Pmw.ScrolledText(self.contents[-1][1])
self.contents[-1][2].setvalue(content)
self.contents[-1][2].pack(expand = True, fill = BOTH)
def tabselect(self, name):
"""
Called when a tab is selected
Normally sets focus to the text editor widget
If the + tab is selected, this function creates a new record and tab
"""
if name == '+':
self.addTab(self.storage.setRecord(None, 'New', ''), 'New', '');
self.tablist.previouspage();
else:
for tab in self.contents:
if tab[1] == self.tablist.page(name):
tab[2].component('text').focus()
def quit(self, junk = None):
"""
Store all data and terminate program
"""
self.storage.setSetting('activeTab', self.tablist.getcurselection())
self.storage.setSetting('geometry', str(self.geometry()))
self.tabunselect(self.tablist.getcurselection())
self.storage.close()
self.destroy()
win = Main()
win.mainloop()