#!/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() 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') try: self.dbcur.execute('CREATE TABLE settings (key TEXT, value TEXT, UNIQUE (key))') self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("version", "1.0");') self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("geometry", "420x300");') self.dbcur.execute('CREATE TABLE notes (id INTEGER PRIMARY KEY, label TEXT, content TEXT);') self.dbcur.execute('INSERT INTO notes (label, content) VALUES ("Introduction", "' + self.escape(initnote) + '");') 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 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 id, label, content FROM notes ORDER BY id;') return self.dbcur.fetchall() def setRecord(self, rid, content = None, label = None): """ Alter an existing record """ if content: self.dbcur.execute('UPDATE notes SET content = "' + self.escape(content) + '" WHERE id = ' + str(rid) + ';') if label: self.dbcur.execute('UPDATE notes SET label = "' + self.escape(label) + '" WHERE id = ' + str(rid) + ';') def addRecord(self, label = '', content = ''): """ Create a new record, return the new record id """ self.dbcur.execute('INSERT INTO notes (label, content) VALUES ("' + self.escape(label) + '", "' + self.escape(content) + '");') return self.dbcur.lastrowid def delRecord(self, rid): """ Delete record by ID """ self.dbcur.execute('DELETE FROM notes WHERE id = ' + str(rid) + ';') 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.addRecord('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()