initial code commit
authorpaul <paul@plutz.net>
Tue, 11 Jan 2011 07:31:59 +0000 (07:31 +0000)
committerpaul <paul@plutz.net>
Tue, 11 Jan 2011 07:31:59 +0000 (07:31 +0000)
svn path=/trunk/; revision=2

tabnote [new file with mode: 0755]

diff --git a/tabnote b/tabnote
new file mode 100755 (executable)
index 0000000..21de6bb
--- /dev/null
+++ b/tabnote
@@ -0,0 +1,296 @@
+#!/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 <http://www.gnu.org/licenses/>.
+
+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('<Control-Left>', self.previouspage)
+    self.bind_all('<Control-Right>', self.nextpage)
+    self.bind_all('<Control-t>', self.newTab)
+    self.bind_all('<Control-q>', self.quit)
+    self.bind_all('<Control-Return>', 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 = '<Double-Button-1>', 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('<Escape>', 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('<Return>', self.relabelTab)
+      self.relabelText.pack(side = TOP, fill = X, expand = True)
+      relabelButton = Button(self.relabelWin, text='Relabel', command=self.relabelTab)
+      relabelButton.bind('<Return>', self.relabelTab)
+      relabelButton.pack(side = LEFT, fill = BOTH, expand = True)
+      deleteButton = Button(self.relabelWin, text='Delete', command=self.delTab)
+      deleteButton.bind('<Return>', 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 = '<Double-Button-1>', 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()