4 # TabNote - Note Taking Application
5 # Copyright 2011 Paul Hänsch
6 # Contact: haensch.paul@gmail.com
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 from pysqlite2 import dbapi2
28 Database storage layer
33 Open a database, create and initialize the database if needed
35 self.dbcon = dbapi2.connect(os.getenv('HOME') + os.sep + '.tabnote.sqlite')
36 self.dbcur = self.dbcon.cursor()
40 initnote = ('Welcome to TabNote!\n\n' +
41 'Click the "+"-Tab to open a new note.\n' +
42 'Double Click on the selected tab to relabel or delete it.\n' +
43 'Notes are stored when you exit the program.\n\n' +
45 'Ctrl Left - switch to previous tab\n' +
46 'Ctrl Right - switch to next tab\n' +
47 'Ctrl Return - bring up relabel/delete dialog\n' +
48 'Ctrl t - create new tab\n' +
49 'Ctrl q - save and quit')
50 initnote = self.escape(initnote)
53 self.dbcur.execute('CREATE TABLE settings (key TEXT, value TEXT, UNIQUE (key));')
54 self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("version", "1.6");')
55 self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("geometry", "420x300");')
56 self.dbcur.execute('CREATE TABLE notes (uuid TEXT, seq INTEGER PRIMARY KEY, label TEXT, ' +
57 'content TEXT, revision DATETIME, lastsync DATETIME, UNIQUE (uuid));')
58 self.dbcur.execute('INSERT INTO notes (uuid, label, content, revision, lastsync) VALUES ' +
59 '(lower(hex(randomblob(16))), "Intro", "' + initnote +
60 '", datetime(\'now\'), datetime(\'1970-01-01\'));')
63 def getSetting(self, key):
64 self.dbcur.execute('SELECT value FROM settings WHERE key = "' + self.escape(key) + '";')
65 try: return self.dbcur.fetchall()[0][0]
68 def setSetting(self, key, value):
70 self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("' + self.escape(key) +
71 '", "' + self.escape(value) + '");')
73 self.dbcur.execute('UPDATE settings SET value = "' + self.escape(value) +
74 '" WHERE key = "' + self.escape(key) + '";')
76 def escape(self, string):
78 Return sanitized string for use in SQL-Statements
82 if c == '"': ret += '""'
88 Return list of all recorded notes
89 Each item is an array of the format [id, label, textcontent]
91 self.dbcur.execute('SELECT uuid, label, content FROM notes ORDER BY seq;')
92 return self.dbcur.fetchall()
94 def setRecord(self, uuid = None, label = None, content = None):
96 Create or alter a record
99 if label: label = self.escape(label)
101 if content: content = self.escape(content)
103 self.dbcur.execute('INSERT INTO notes (uuid, label, content, revision, lastsync) VALUES ' +
104 '(lower(hex(randomblob(16))), "' + str(label) + '", "' + str(content) +
105 '", datetime(\'now\'), datetime(\'1970-01-01\'));')
108 self.dbcur.execute('UPDATE notes SET content = "' + self.escape(content) +
109 '", revision = datetime(\'now\') WHERE uuid = "' + str(uuid) +
110 '" AND NOT content = "' + self.escape(content) + '";')
112 self.dbcur.execute('UPDATE notes SET label = "' + self.escape(label) +
113 '", revision = datetime(\'now\') WHERE uuid = "' + str(uuid) + '";')
116 self.dbcur.execute('SELECT uuid FROM notes WHERE seq = "' + str(self.dbcur.lastrowid) + '";')
117 return self.dbcur.fetchall()[0][0]
120 def delRecord(self, uuid):
124 self.dbcur.execute('DELETE FROM notes WHERE uuid = "' + str(uuid) + '";')
128 Flush and close databese
136 Main application class
140 Open Database, create Main application window, and retrieve stored data
143 self.storage = Persistence()
144 self.protocol('WM_DELETE_WINDOW', self.quit)
145 self.title('TabNote')
147 self.relabelWin = False
150 self.tablist = Pmw.NoteBook(self, raisecommand = self.tabselect, lowercommand = self.tabunselect)
152 self.tablist.pack(expand = True, fill = BOTH)
153 for tab in self.storage.allRecords():
154 self.addTab(tab[0], tab[1], tab[2])
155 self.tablist.delete(Pmw.END)
156 self.tablist.add('+')
158 self.bind_all('<Control-Left>', self.previouspage)
159 self.bind_all('<Control-Right>', self.nextpage)
160 self.bind_all('<Control-t>', self.newTab)
161 self.bind_all('<Control-q>', self.quit)
162 self.bind_all('<Control-Return>', self.tabedit)
165 self.geometry(self.storage.getSetting('geometry'))
166 self.tablist.selectpage(self.storage.getSetting('activeTab'))
169 def newTab(self, junk):
171 Event wrapper for new tab
173 self.tablist.selectpage('+')
175 def previouspage(self, junk):
177 Event wrapper for tab switching
179 if self.tablist.getcurselection() == self.tablist.pagenames()[0]:
180 self.tablist.selectpage(self.tablist.pagenames()[-2])
182 self.tablist.previouspage()
184 def nextpage(self, junk):
186 Event wrapper for tab switching
188 if self.tablist.getcurselection() == self.tablist.pagenames()[-2]:
189 self.tablist.selectpage(0)
191 self.tablist.nextpage()
193 def closeRelabel(self, junk = None):
195 Close relabeling dialog window
197 self.relabelWin.destroy()
198 self.relabelWin = False
200 def delTab(self, junk = None):
202 Delete a tab (and its record)
203 Called by relabeling dialog
205 for tab in self.contents:
206 if tab[1] == self.tablist.page(self.relabel):
207 self.storage.delRecord(tab[0])
208 self.contents.remove(tab)
209 self.tablist.previouspage()
210 self.tablist.delete(self.relabel)
214 def relabelTab(self, junk = None):
216 Change the name of a tab
217 Called by relabeling dialog
219 for tab in self.contents:
220 if tab[1] == self.tablist.page(self.relabel) and self.relabel != self.relabelText.get():
221 label = self.newName(self.relabelText.get())
222 content = tab[2].getvalue()[:-1]
224 tab[1] = self.tablist.insert(label, before = self.tablist.index(self.relabel))
225 self.tablist.tab(label).bind(sequence = '<Double-Button-1>', func = self.tabedit)
226 tab[2] = Pmw.ScrolledText(tab[1])
227 tab[2].setvalue(content)
228 tab[2].pack(expand = True, fill = BOTH)
230 self.tablist.previouspage()
231 self.tablist.delete(self.relabel)
232 self.storage.setRecord(tab[0], label = label)
236 def tabedit(self, junk):
238 Bring up relabeling dialog
240 if not self.relabelWin:
241 self.relabel = self.tablist.getcurselection()
242 self.relabelWin = Tk()
243 self.relabelWin.bind('<Escape>', self.closeRelabel)
244 self.relabelWin.protocol('WM_DELETE_WINDOW', self.closeRelabel)
245 self.relabelText = Entry(self.relabelWin)
246 self.relabelText.insert(0, self.relabel)
247 self.relabelText.focus()
248 self.relabelText.bind('<Return>', self.relabelTab)
249 self.relabelText.pack(side = TOP, fill = X, expand = True)
250 relabelButton = Button(self.relabelWin, text='Relabel', command=self.relabelTab)
251 relabelButton.bind('<Return>', self.relabelTab)
252 relabelButton.pack(side = LEFT, fill = BOTH, expand = True)
253 deleteButton = Button(self.relabelWin, text='Delete', command=self.delTab)
254 deleteButton.bind('<Return>', self.delTab)
255 deleteButton.pack(side = LEFT, fill = BOTH, expand = True)
257 def newName(self, basename = 'New'):
259 Return a unique name for a tab by appending a number to the given basename
261 if not basename in self.tablist.pagenames(): return basename
263 while (basename + str(incrementor)) in self.tablist.pagenames(): incrementor += 1
264 return (basename + str(incrementor))
266 def tabunselect(self, name):
268 Called when a tab becomes unselected, write the DB record for this tab
270 for tab in self.contents:
271 if tab[1] == self.tablist.page(name):
272 self.storage.setRecord(tab[0], content = tab[2].getvalue()[:-1])
274 def addTab(self, rid, label = 'New', content = ""):
278 label = self.newName(label)
279 self.contents.append([rid, self.tablist.insert(label, before = self.tablist.index(Pmw.END)), None])
280 self.tablist.tab(label).bind(sequence = '<Double-Button-1>', func = self.tabedit)
281 self.contents[-1][2] = Pmw.ScrolledText(self.contents[-1][1])
282 self.contents[-1][2].setvalue(content)
283 self.contents[-1][2].pack(expand = True, fill = BOTH)
285 def tabselect(self, name):
287 Called when a tab is selected
288 Normally sets focus to the text editor widget
289 If the + tab is selected, this function creates a new record and tab
292 self.addTab(self.storage.setRecord(None, 'New', ''), 'New', '');
293 self.tablist.previouspage();
295 for tab in self.contents:
296 if tab[1] == self.tablist.page(name):
297 tab[2].component('text').focus()
299 def quit(self, junk = None):
301 Store all data and terminate program
303 self.storage.setSetting('activeTab', self.tablist.getcurselection())
304 self.storage.setSetting('geometry', str(self.geometry()))
305 self.tabunselect(self.tablist.getcurselection())