included functionality of addRecord in setRecord
[tabnote] / tabnote
1 #!/usr/bin/python
2 #encoding: utf-8
3
4 # TabNote - Note Taking Application
5 # Copyright 2011 Paul Hänsch
6 # Contact: haensch.paul@gmail.com
7 #
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.
12 #
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.
17 #
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/>.
20
21 from Tkinter import *
22 from pysqlite2 import dbapi2
23 import os
24 import Pmw
25
26 class Persistence():
27   """
28   Database storage layer
29   """
30   def __init__(self):
31     """
32     Constructor
33     Open a database, create and initialize the database if needed
34     """
35     self.dbcon = dbapi2.connect(os.getenv('HOME') + os.sep + '.tabnote.sqlite')
36     self.dbcur = self.dbcon.cursor()
37     initnote = ('Welcome to TabNote!\n\n' +
38                 'Click the "+"-Tab to open a new note.\n' +
39                 'Double Click on the selected tab to relabel or delete it.\n' +
40                 'Notes are stored when you exit the program.\n\n' +
41                 'Shortcuts:\n' +
42                 'Ctrl Left   - switch to previous tab\n' +
43                 'Ctrl Right  - switch to next tab\n' +
44                 'Ctrl Return - bring up relabel/delete dialog\n' +
45                 'Ctrl t      - create new tab\n' +
46                 'Ctrl q      - save and quit')
47     try:
48       self.dbcur.execute('CREATE TABLE settings (key TEXT, value TEXT, UNIQUE (key))')
49       self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("version", "1.0");')
50       self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("geometry", "420x300");')
51       self.dbcur.execute('CREATE TABLE notes (id INTEGER PRIMARY KEY, label TEXT, content TEXT);')
52       self.dbcur.execute('INSERT INTO notes (label, content) VALUES ("Introduction", "' +
53                          self.escape(initnote) + '");')
54     except: pass
55
56   def getSetting(self, key):
57     self.dbcur.execute('SELECT value FROM settings WHERE key = "' + self.escape(key) + '";')
58     try: return self.dbcur.fetchall()[0][0]
59     except: return ''
60
61   def setSetting(self, key, value):
62     try:
63       self.dbcur.execute('INSERT INTO settings (key, value) VALUES ("' + self.escape(key) +
64                          '", "' + self.escape(value) + '");')
65     except:
66       self.dbcur.execute('UPDATE settings SET value = "' + self.escape(value) +
67                          '" WHERE key = "' + self.escape(key) + '";')
68
69   def escape(self, string):
70     """
71     Return sanitized string for use in SQL-Statements
72     """
73     ret = ''
74     for c in string:
75       if c == '"': ret += '""'
76       else: ret += c
77     return ret
78
79   def allRecords(self):
80     """
81     Return list of all recorded notes
82     Each item is an array of the format [id, label, textcontent]
83     """
84     self.dbcur.execute('SELECT id, label, content FROM notes ORDER BY id;')
85     return self.dbcur.fetchall()
86
87   def setRecord(self, rid = None, label = None, content = None):
88     """
89     Create or alter a record
90     """
91     if not rid:
92       if label: label = self.escape(label)
93       else: label = ''
94       if content: content = self.escape(content)
95       else: content = ''
96       self.dbcur.execute('INSERT INTO notes (label, content) VALUES ("' + self.escape(label) +
97                          '", "' + self.escape(content) + '");')
98     else:    
99       if content:
100         self.dbcur.execute('UPDATE notes SET content = "' + self.escape(content) +
101                            '" WHERE id = ' + str(rid) + ';')
102       if label:
103         self.dbcur.execute('UPDATE notes SET label = "' + self.escape(label) +
104                            '" WHERE id = ' + str(rid) + ';')
105     return self.dbcur.lastrowid
106
107   def delRecord(self, rid):
108     """
109     Delete record by ID
110     """
111     self.dbcur.execute('DELETE FROM notes WHERE id = ' + str(rid) + ';')
112
113   def close(self):
114     """
115     Flush and close databese
116     """
117     self.dbcur.close()
118     self.dbcon.commit()
119     self.dbcon.close()
120
121 class Main(Tk):
122   """
123   Main application class
124   """
125   def __init__(self):
126     """
127     Open Database, create Main application window, and retrieve stored data
128     """
129     Tk.__init__(self)
130     self.storage = Persistence()
131     self.protocol('WM_DELETE_WINDOW', self.quit)
132     self.title('TabNote')
133
134     self.relabelWin = False
135     self.contents = []
136
137     self.tablist = Pmw.NoteBook(self, raisecommand = self.tabselect, lowercommand = self.tabunselect)
138     self.tablist.add('')
139     self.tablist.pack(expand = True, fill = BOTH)
140     for tab in self.storage.allRecords():
141       self.addTab(tab[0], tab[1], tab[2])
142     self.tablist.delete(Pmw.END)
143     self.tablist.add('+')
144
145     self.bind_all('<Control-Left>', self.previouspage)
146     self.bind_all('<Control-Right>', self.nextpage)
147     self.bind_all('<Control-t>', self.newTab)
148     self.bind_all('<Control-q>', self.quit)
149     self.bind_all('<Control-Return>', self.tabedit)
150
151     try:
152       self.geometry(self.storage.getSetting('geometry'))
153       self.tablist.selectpage(self.storage.getSetting('activeTab'))
154     except: pass
155
156   def newTab(self, junk):
157     """
158     Event wrapper for new tab
159     """
160     self.tablist.selectpage('+')
161
162   def previouspage(self, junk):
163     """
164     Event wrapper for tab switching
165     """
166     if self.tablist.getcurselection() == self.tablist.pagenames()[0]:
167       self.tablist.selectpage(self.tablist.pagenames()[-2])
168     else:
169       self.tablist.previouspage()
170
171   def nextpage(self, junk):
172     """
173     Event wrapper for tab switching
174     """
175     if self.tablist.getcurselection() == self.tablist.pagenames()[-2]:
176       self.tablist.selectpage(0)
177     else:
178       self.tablist.nextpage()
179
180   def closeRelabel(self, junk = None):
181     """
182     Close relabeling dialog window
183     """
184     self.relabelWin.destroy()
185     self.relabelWin = False
186
187   def delTab(self, junk = None):
188     """
189     Delete a tab (and its record)
190     Called by relabeling dialog
191     """
192     for tab in self.contents:
193       if tab[1] == self.tablist.page(self.relabel):
194         self.storage.delRecord(tab[0])
195         self.contents.remove(tab)
196         self.tablist.previouspage()
197         self.tablist.delete(self.relabel)
198         break
199     self.closeRelabel()
200
201   def relabelTab(self, junk = None):
202     """
203     Change the name of a tab
204     Called by relabeling dialog
205     """
206     for tab in self.contents:
207       if tab[1] == self.tablist.page(self.relabel) and self.relabel != self.relabelText.get():
208         label = self.newName(self.relabelText.get())
209         content = tab[2].getvalue()[:-1]
210  
211         tab[1] = self.tablist.insert(label, before = self.tablist.index(self.relabel))
212         self.tablist.tab(label).bind(sequence = '<Double-Button-1>', func = self.tabedit)
213         tab[2] = Pmw.ScrolledText(tab[1])
214         tab[2].setvalue(content)
215         tab[2].pack(expand = True, fill = BOTH)
216  
217         self.tablist.previouspage()
218         self.tablist.delete(self.relabel)
219         self.storage.setRecord(tab[0], label = label)
220         break
221     self.closeRelabel()
222
223   def tabedit(self, junk):
224     """
225     Bring up relabeling dialog
226     """
227     if not self.relabelWin:
228       self.relabel = self.tablist.getcurselection()
229       self.relabelWin = Tk()
230       self.relabelWin.bind('<Escape>', self.closeRelabel)
231       self.relabelWin.protocol('WM_DELETE_WINDOW', self.closeRelabel)
232       self.relabelText = Entry(self.relabelWin)
233       self.relabelText.insert(0, self.relabel)
234       self.relabelText.focus()
235       self.relabelText.bind('<Return>', self.relabelTab)
236       self.relabelText.pack(side = TOP, fill = X, expand = True)
237       relabelButton = Button(self.relabelWin, text='Relabel', command=self.relabelTab)
238       relabelButton.bind('<Return>', self.relabelTab)
239       relabelButton.pack(side = LEFT, fill = BOTH, expand = True)
240       deleteButton = Button(self.relabelWin, text='Delete', command=self.delTab)
241       deleteButton.bind('<Return>', self.delTab)
242       deleteButton.pack(side = LEFT, fill = BOTH, expand = True)
243
244   def newName(self, basename = 'New'):
245     """
246     Return a unique name for a tab by appending a number to the given basename
247     """
248     if not basename in self.tablist.pagenames(): return basename
249     incrementor = 1
250     while (basename + str(incrementor)) in self.tablist.pagenames(): incrementor += 1
251     return (basename + str(incrementor))
252
253   def tabunselect(self, name):
254     """
255     Called when a tab becomes unselected, write the DB record for this tab
256     """
257     for tab in self.contents:
258       if tab[1] == self.tablist.page(name):
259         self.storage.setRecord(tab[0], content = tab[2].getvalue()[:-1])
260
261   def addTab(self, rid, label = 'New', content = ""):
262     """
263     Create a new tab
264     """
265     label = self.newName(label)
266     self.contents.append([rid, self.tablist.insert(label, before = self.tablist.index(Pmw.END)), None])
267     self.tablist.tab(label).bind(sequence = '<Double-Button-1>', func = self.tabedit)
268     self.contents[-1][2] = Pmw.ScrolledText(self.contents[-1][1])
269     self.contents[-1][2].setvalue(content)
270     self.contents[-1][2].pack(expand = True, fill = BOTH)
271
272   def tabselect(self, name):
273     """
274     Called when a tab is selected
275     Normally sets focus to the text editor widget
276     If the + tab is selected, this function creates a new record and tab
277     """
278     if name == '+':
279       self.addTab(self.storage.setRecord(None, 'New', ''), 'New', '');
280       self.tablist.previouspage();
281     else:
282       for tab in self.contents:
283         if tab[1] == self.tablist.page(name):
284           tab[2].component('text').focus()
285
286   def quit(self, junk = None):
287     """
288     Store all data and terminate program
289     """
290     self.storage.setSetting('activeTab', self.tablist.getcurselection())
291     self.storage.setSetting('geometry', str(self.geometry()))
292     self.tabunselect(self.tablist.getcurselection())
293     self.storage.close()
294     self.destroy()
295
296 win = Main()
297 win.mainloop()