#!/usr/bin/python
license=""" 
Copyright (C) Dwayne Zon 2009 <dwayne.zon@gmail.com> 

Based on gene cash todo application and John Stowers 2007 <john.stowers@gmail.com> (example of gtkgenerictreemodel with sqlite database

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 2 of the license or (at your option) any later version
""" 
version_long='V0.6: <2009-Apr 22>'
version_short='0.6'

import datetime, gobject
from pysqlite2 import dbapi2 as sqlite
import gtk, hildon, fcntl
try:
    import osso
    nokia=True
except:
    nokia=False
import os, pickle, sys

def attach_menu(menu, window=None):
    if nokia:
        if window:
            window.set_menu(menu)
        else:
            root.set_menu(menu)
    else:
        # ugly as hell
        mb=gtk.MenuBar()
        i=gtk.MenuItem('Operation Menu')
        i.set_submenu(menu)
        mb.append(i)
        vb.pack_start(mb, False, False)

# detect hardware key presses
#def cb_key_press(w, event,title):
def cb_key_press(w, event):
# title "ToDo Item Details"
    if event.keyval == gtk.keysyms.F6:
        # the "full screen" hardware key
        if w.window_in_fullscreen:
            w.unfullscreen()
        else:
            w.fullscreen()
    elif event.keyval == gtk.keysyms.F7:
        parmblock.scale_factor = parmblock.scale_factor + .1
        if nokia:
			result = osso_note.system_note_infoprint("Zooming to %d%%" % (parmblock.scale_factor*100))          
        rend_subject.props.scale = parmblock.scale_factor
        view.columns_autosize()
    elif event.keyval == gtk.keysyms.F8:
        parmblock.scale_factor = parmblock.scale_factor - .1
        if nokia:
			result = osso_note.system_note_infoprint("Zooming to %d%%" % (parmblock.scale_factor*100))
        rend_subject.props.scale = parmblock.scale_factor
        view.columns_autosize()

# detect when the window is toggled to full-screen
def cb_window_state_change(w, event):
    if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
        w.window_in_fullscreen=True
    else:
        w.window_in_fullscreen=False

# create Hildonized dialog window
def dialog_window(title, modal=True):
    if nokia:
        w=hildon.Window()
        app.add_window(w)
    else:
        w=gtk.Window()
        w.resize(800, 480)
    w.set_transient_for(root)
    w.set_destroy_with_parent(True)
    if modal:
        w.set_modal(True)
    w.set_title(title)
    if nokia:
        w.window_in_fullscreen=False
        w.connect('key-press-event', cb_key_press,title)
        w.connect('window-state-event', cb_window_state_change)
    return w

# button with stock image but without stock label
def button_with_image(label, stock, fcn, *args):
    b=gtk.Button(label)
    i=gtk.Image()
    if label and nokia:
        # large button
        i.set_from_stock(stock, gtk.ICON_SIZE_DND)
    else:
        # small button
        i.set_from_stock(stock, gtk.ICON_SIZE_BUTTON)
    b.set_image(i)
    b.connect('clicked', fcn, *args)
    return b

def application_setup(text_name, sys_name, homedir='.python'):
    global program_name, logfile, root, osso_context, osso_note, context_name, app, vb

    program_name=text_name
    # "context" must start with "org.maemo."
    context_name='org.maemo.'+sys_name
    osso_context=None
    app=None
    # change into data dir
    f=os.getenv('HOME') + '/' + homedir
    if not os.path.exists(f):
        os.mkdir(f)
    os.chdir(f)
    if nokia:
        # log errors to a file for post-mortem debugging w/o a console
        logfile=sys_name+'.log'
        sys.stderr=open(logfile, 'w')
        # Nokia tablet setup
        osso_context=osso.Context(context_name, version_short, False)
        osso_note = osso.SystemNote(osso_context)
        app=hildon.Program()
        root=hildon.Window()
        app.add_window(root)
        root.window_in_fullscreen=False
        root.connect('key-press-event', cb_key_press)
        root.connect('window-state-event', cb_window_state_change)
        # note that this also sets the main window title in Hildon
        gtk.set_application_name(program_name)
    else:
        root=gtk.Window()
        root.resize(800, 480)
        root.set_title(program_name)

    # exit gracefully when window is closed
    root.connect('destroy', lambda wid: gtk.main_quit())
    root.connect('delete_event', lambda a1, a2: gtk.main_quit())

    # exit gracefully on shutdown
#    signal.signal(15, lambda a1: gtk.main_quit())

    vb=gtk.VBox()
    root.add(vb)
    return root, vb

# display logfile
class show_logfile:
    def __init__(self, *x):
        if not nokia:
            return

        # create window with scrolling text
        self.win=dialog_window('View Logs', False)
        sw=gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.win.add(sw)
        tv=gtk.TextView()
        tv.set_wrap_mode(gtk.WRAP_WORD)
        tv.set_editable(False)
        buf=tv.get_buffer()
        end_mark=buf.create_mark('end', buf.get_end_iter(), False)
        sw.add(tv)

        # read log file and load widget
        sys.stderr.flush()
        buf.insert_at_cursor(file(logfile, 'rb').read())

        # create menu
        self.menu=gtk.Menu()
        i=gtk.MenuItem('Clear Log')
        i.connect('activate',self.clear_log)                      
        self.menu.append(i)
        i=gtk.MenuItem('Close')
        i.connect('activate',self.close)                      
        self.menu.append(i)

        self.win.set_menu(self.menu)
        self.win.show_all()
        tv.scroll_mark_onscreen(end_mark)

    def close(self, b):
        self.win.destroy()

    def clear_log(self, b):
        sys.stderr=file(logfile, 'w')
        self.win.destroy()

###################################################################################################################
### dialog to edit categories

class edit_categories:
    def __init__(self, *x):
        self.win=dialog_window('Edit Categories')
        vb=gtk.VBox()
        self.win.add(vb)

        # list of categories - we cheat and use the model from the dropdown
        self.mdl=combo_category.get_model()
        self.list_cat=gtk.TreeView(self.mdl)
        rend=gtk.CellRendererText()
        col=gtk.TreeViewColumn('Categories', rend, text=0)
        self.list_cat.append_column(col)
        self.list_cat.connect('cursor-changed', self.cb_edit_select)

        # make it scrollable
        sw=gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        sw.add(self.list_cat)
        vb.pack_start(sw, True, True, 0)

        # new name/rename entry
        hb=gtk.HBox()
        vb.pack_start(hb, False, False)
        hb.pack_start(gtk.Label('Name:'), False, False)
        self.entry_name=gtk.Entry()
        hb.pack_start(self.entry_name, False, False)

        # buttons
        btn_new=button_with_image('New', gtk.STOCK_NEW, self.cb_edit_new)
        btn_rename=button_with_image('Rename', gtk.STOCK_REFRESH, self.cb_edit_rename)
        btn_delete=button_with_image('Delete', gtk.STOCK_DELETE, self.cb_edit_delete)
        btn_done=button_with_image('Done', gtk.STOCK_APPLY, self.cb_edit_done)

        hb.pack_start(btn_new, False, False)
        hb.pack_start(btn_rename, False, False)
        hb.pack_start(btn_delete, False, False)
        hb.pack_start(btn_done, False, False)

        self.win.show_all()

    # editing done
    def cb_edit_done(self, x):
        self.win.destroy()

    # delete category
    def cb_edit_delete(self, x):
        try:
            row=self.list_cat.get_cursor()[0][0]
            name=self.mdl[row][0]
        except:
            return
        if name != 'All' and name != 'None':
            combo_category.remove_text(row)

    # new category
    def cb_edit_new(self, x):
        name_new=self.entry_name.get_text()
        if name_new in (x[0] for x in self.mdl):
            return
        self.mdl.append([name_new,True])

    # rename a category
    def cb_edit_rename(self, x):
        name_new=self.entry_name.get_text()
        if not name_new:
            return
        try:
            row=self.list_cat.get_cursor()[0][0]
            name_old=self.mdl[row][0]
        except:
            return
        if name_new == name_old or name_old == 'All' or name_old == 'None':
            return
        self.mdl[row][0] = name_new

    # fill text box when row selected
    def cb_edit_select(self, tree):
        row=self.list_cat.get_cursor()[0][0]
        t=self.mdl[row][0]
        self.entry_name.set_text(t)

###################################################################################################################
### Dialog to edit single records 

class detail_edit:
    def __init__(self, path, model):
        # create dialog-style window
        iter = model.get_iter(path)
        self.duedate = datetime.datetime.strptime(model.get_value(iter,5),"%Y-%m-%d") if model.get_value(iter,5) else None
        self.win=dialog_window('ToDo Item Details')
        vb=gtk.VBox()
        self.win.add(vb)

        hb=gtk.HBox()
        vb.pack_start(hb, False, False)

        # IsCompleted
        self.checkbox_iscompleted = gtk.CheckButton('Completed')
        self.checkbox_iscompleted.connect('toggled',self.cb_change_iscompleted)
        self.checkbox_iscompleted.set_active(model.get_value(iter,2))
        hb.pack_start(self.checkbox_iscompleted, False, False,10)

        # Priority combo box 
        self.combo_priority=gtk.ComboBox(priorities)
        cell = gtk.CellRendererText()
        self.combo_priority.pack_start(cell,True)                             
        self.combo_priority.add_attribute(cell,'text',0)
        self.combo_priority.set_active(int(model.get_value(iter,1))-1)
        self.combo_priority.set_property('has-frame',False)
        self.combo_priority.connect('changed', self.cb_change_priority)
        hb.pack_start(self.combo_priority, False, False)

        # Category drop down list
        self.combo_category=gtk.ComboBoxEntry()
        rendfilter=categories.filter_new()
        rendfilter.set_visible_column(1)
        self.combo_category.set_property('model', rendfilter)
        self.combo_category.set_property('text-column', 0)
        cell = gtk.CellRendererText()
        self.combo_category.pack_start(cell,True)
        category =  model.get_value(iter,3)
        i = categories.get_iter_first()
        j = 0
        while (i) and (category != categories[i][0]):
            if categories[i][1]:
                j += 1
            i = categories.iter_next(i)
        if not i:
            categories.append([category,True])
        self.combo_category.set_active(j)
        self.combo_category.connect('changed', self.cb_change_category)
        hb.pack_start(self.combo_category, False, False)

        # Duedate
        self.tv_duedate = gtk.Button()
        if self.duedate:
            self.tv_duedate.set_label("DueDate:" + self.duedate.strftime("%Y-%m-%d"))
        else:
            self.tv_duedate.set_label("DueDate: None")
        self.tv_duedate.connect('clicked',self.cb_duedate)        
        hb.pack_start(self.tv_duedate, False, False)

        # IsDeleted checkbox if this is not a recurring item
        if not model.get_value(iter,8):
            self.checkbox_isdeleted = gtk.CheckButton('Delete')
            self.checkbox_isdeleted.connect('toggled',self.cb_change_isdeleted)
            self.checkbox_isdeleted.set_active(model.get_value(iter,7))
            hb.pack_end(self.checkbox_isdeleted, False, False)

        # Subject edit box
        tv_subject=gtk.TextView()
        tv_subject.set_name('SmallLabelText')
        tv_subject.scale = parmblock.scale_factor
        tv_subject.set_wrap_mode(gtk.WRAP_WORD)
        self.text_buf=tv_subject.get_buffer()
        self.text_buf.set_text(model.get_value(iter,4))
        sw=gtk.ScrolledWindow()
        sw.add(tv_subject)
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        sw.set_shadow_type(gtk.SHADOW_ETCHED_IN)
        vb.pack_start(sw, False, False, 0)

        # buttons
        btn_ok=button_with_image('OK', gtk.STOCK_APPLY, self.cb_detail_ok,model,iter)
        btn_cancel=button_with_image('Cancel', gtk.STOCK_CANCEL, self.cb_detail_cancel,model,iter)
        btn_note=button_with_image('Note', gtk.STOCK_NEW, self.cb_detail_note,model,iter,path)
        hb=gtk.HBox()
        vb.pack_start(hb, False, False)
        hb.pack_start(btn_ok, False, False,10)
        hb.pack_start(btn_cancel, False, False,10)
        hb.pack_start(btn_note, False, False,10)
        self.prioritychanged = False
        self.categorychanged = False
        self.duedatechanged = False
        self.isdeletedchanged = False
        self.iscompletedchanged = False
        self.text_buf.set_modified(False)
        self.win.show_all()

    # detail dialog: cancel button
    def cb_detail_cancel(self, b,model,rowref):
        if model.get_value(rowref,9) == '':
            model._delete_record(rowref) 

        self.win.destroy()

    # detail dialog: OK button
    def cb_detail_ok(self, b,model,iter):
        if (not model.get_value(iter,8)) and self.checkbox_isdeleted.get_active() and model.get_value(iter,0) < 0:
            model._delete_record(iter)
        else:
            if self.prioritychanged:
                model._set_value(iter,"Priority",self.combo_priority.get_active_text())
            if self.iscompletedchanged:
                model._set_value(iter,"IsCompleted",self.checkbox_iscompleted.get_active())
            if self.categorychanged:
                model._set_value(iter,"Category",self.combo_category.get_active_text())
            if self.duedatechanged:
                model._set_value(iter,"DueDate",self.duedate)
            if self.isdeletedchanged:
                model._set_value(iter,"DelFlag",self.checkbox_isdeleted.get_active())
            if self.text_buf.get_modified():
                model._set_value(iter,"Subject",self.text_buf.get_text(*self.text_buf.get_bounds()))
        self.win.destroy()

    def cb_detail_note(self, b,model,iter,path):
        self.cb_detail_ok(b,model,iter)
        row_select(view, path, col_note,model)

    # Update the new priority field
    def cb_change_priority(self,b):
        self.prioritychanged = True 

    # Update the new category field    
    def cb_change_category(self,b):
        self.categorychanged = True

    # IsDeleted has been changed
    def cb_change_isdeleted(self,b):
        self.isdeletedchanged = True    
    # IsCompleted has been changed
    def cb_change_iscompleted(self,b):
        self.iscompletedchanged = True    
    # Pop up a calendar    
    def cb_duedate(self, b):
        self.win2=dialog_window('Select Due Date')
        vb=gtk.VBox()
        self.win2.add(vb)
        self.calendar = gtk.Calendar()
        vb.pack_start(self.calendar, False, False)
        curdate = self.duedate if self.duedate else datetime.date.today()
        result = self.calendar.select_month(curdate.month-1,curdate.year)
        self.calendar.select_day(curdate.day)
        self.calendar.connect("day_selected_double_click",self.calendar_day_selected,self,self.tv_duedate)
        hb=gtk.HBox()
        vb.pack_start(hb, False, False)
        # Cancel button
        btn_can=gtk.Button("Cancel",gtk.STOCK_CANCEL)
        btn_can.connect("clicked",lambda x: self.win2.destroy())
        hb.pack_start(btn_can, False, False)
        # Clear button
        btn_clear=gtk.Button("Clear",gtk.STOCK_CLEAR)
        btn_clear.connect("clicked",self.cb_duedate_clear,self,self.tv_duedate)
        hb.pack_end(btn_clear, False, False)
        self.win2.show_all()

    def calendar_day_selected(self,widget,detail_edit,tv_duedate):
        year, month, day = self.calendar.get_date()
        newdate = datetime.date(year,month+1,day)
        if newdate <> detail_edit.duedate:
            self.duedatechanged = True
            tv_duedate.set_label(str(newdate))
            detail_edit.duedate = newdate
        self.win2.destroy()

    def cb_duedate_clear(self,widget,detail_edit,tv_duedate):
        if detail_edit.duedate <> None:
            self.duedatechanged = True
            tv_duedate.set_label("Duedate: None")
            detail_edit.duedate = None 
        self.win2.destroy()

###################################################################################################################
### dialog to edit duedate, subject and note

class row_select:
    def __init__(self, tree, path, column, model):
        iter = model.get_iter(path)
        if column == col_duedate:     #duedate
            self.cb_duedate(iter,model)
            return
        if column == col_subject:
            detail_edit(path,model)
            return
        if column != col_note:
            return

        # create dialog-style window
        self.win=dialog_window('Edit Note')
        vb=gtk.VBox()
        self.win.add(vb)

        # label at top identifying note
        vb.pack_start(gtk.Label('Note for item: "'+model.get_value(iter,4)+'"'), False, False)

        # comment editing box
        tv_note=gtk.TextView()
        tv_note.set_name('SmallLabelText')
        tv_note.set_wrap_mode(gtk.WRAP_WORD)
        tv_note.scale = parmblock.scale_factor
        self.text_buf=tv_note.get_buffer()
        self.text_buf.set_text(model.get_value(iter,6))
        sw=gtk.ScrolledWindow()
        sw.add(tv_note)
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        sw.set_shadow_type(gtk.SHADOW_ETCHED_IN)
        vb.pack_start(sw, True, True, 0)

        # buttons
        btn_ok=button_with_image('OK', gtk.STOCK_APPLY, self.cb_edit_ok,model,iter)
        btn_cancel=button_with_image('Cancel', gtk.STOCK_CANCEL, self.cb_edit_cancel)
        btn_delete=button_with_image('Delete', gtk.STOCK_DELETE, self.cb_edit_delete,model,iter)

        hb=gtk.HBox()
        vb.pack_start(hb, False, False)
        hb.pack_start(btn_ok, False, False)
        hb.pack_start(btn_cancel, False, False)
        hb.pack_start(btn_delete, False, False)

        self.win.show_all()

    # edit dialog: cancel button
    def cb_edit_cancel(self, b):
        self.win.destroy()

    # edit dialog: OK button
    def cb_edit_ok(self, b,model,iter):
        if self.text_buf.get_modified():
            model._set_value(iter,"Note",self.text_buf.get_text(*self.text_buf.get_bounds()))
        self.win.destroy()

    # edit dialog: delete button
    def cb_edit_delete(self, b,model,path):
        dialog=gtk.MessageDialog(self.win, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                                 gtk.MESSAGE_WARNING, gtk.BUTTONS_OK_CANCEL,
                                 'Are you sure you want to delete this note?')
        r=dialog.run()
        dialog.destroy()
        if r == gtk.RESPONSE_OK:
            model._set_value(iter,"Note","")
            self.win.destroy()

    def cb_duedate(self, iter, model):
        self.win=dialog_window('Select Due Date')
        vb=gtk.VBox()
        self.win.add(vb)
        self.calendar = gtk.Calendar()
        vb.pack_start(self.calendar, False, False)
        curdate = datetime.datetime.strptime(model.get_value(iter,5),"%Y-%m-%d") if model.get_value(iter,5) else datetime.date.today()
        result = self.calendar.select_month(curdate.month-1,curdate.year)
        self.calendar.select_day(curdate.day)
        self.calendar.connect("day_selected_double_click",self.calendar_day_selected,iter,model)
        hb=gtk.HBox()
        vb.pack_start(hb, False, False)
        # Cancel button
        btn_can=gtk.Button("Cancel",gtk.STOCK_CANCEL)
        btn_can.connect("clicked",lambda x: self.win.destroy())
        hb.pack_start(btn_can, False, False)
        # Clear button
        btn_clear=gtk.Button("Clear",gtk.STOCK_CLEAR)
        btn_clear.connect("clicked",self.cb_duedate_clear,iter,model)
        hb.pack_end(btn_clear, False, False)
        self.win.show_all()

    def calendar_day_selected(self,widget,iter,model):
        year, month, day = self.calendar.get_date()
        model._set_value(iter,"DueDate",datetime.date(year,month+1,day))
        self.win.destroy()

    def cb_duedate_clear(self,widget,iter,model):
        model._set_value(iter,"DueDate",None)
        self.win.destroy()

###################################################################################################################
### utility functions

# detect hardware key presses
def cb_key_press2(w, event):
    if event.keyval == gtk.keysyms.F7:
        # the "+" hardware key
        cb_next_prev(1)
    elif event.keyval == gtk.keysyms.F8:
        # the "-" hardware key
        cb_next_prev(-1)

###################################################################################################################
### GUI widget callback functions

# button to create new row
def cb_new(b):
    store.insertrow()

# toggle entry checkbox
def cb_iscompleted_toggled(cell, path, model):
    model._toggle_value(model.get_iter(path),"IsCompleted",2)

# Pop up the combo box immediately
def cb_category_edit_start(rend,editor,ndx):
    editor.connect('changed',cb_category_edit_end,int(ndx))
    gobject.timeout_add(350,editor.popup)

def cb_category_edit_end(cb,ndx):
    cb.popdown()
    cb_cat_edited(None, ndx, cb.get_active_text())
    cb.destroy()

def cb_priority_edit_start(rend,editor,ndx):
    editor.connect('changed',cb_priority_edit_end,int(ndx))
    gobject.timeout_add(350,editor.popup)

def cb_priority_edit_end(cb,ndx):
    cb.popdown()
    cb_priority_edited(None, ndx, cb.get_active_text())
    cb.destroy()

# Fired when category has changed
def cb_cat_edited(cell, path, new_text):
    iter = store.get_iter(path)
    store._set_value(iter,"Category",new_text)
    newpath = store.get_path(iter)
    if newpath[0] < 0:
        store.row_deleted (path)
    elif newpath != path:
        store.row_deleted (path)
        store.row_inserted(newpath,iter)
    else:
        store.row_changed(path, iter)

# Fired when category has changed
def cb_priority_edited(cell, path, new_text):
    store._set_value(store.get_iter(path),"Priority",new_text)

# cb_col_note: set or unset note icon column as necessary
def cb_col_note(col, cell, model, row):
    if model.get_value(row,6):
        cell.set_property('stock-id', gtk.STOCK_JUSTIFY_LEFT)
        cell.set_property('visible', True)
    else:
        cell.set_property('visible', False)
    cell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)

# Adjust the wrap size to be the same as the column size for the Subject column
def cb_change_column_size(treeview, allocation, column, cell):
    otherColumns = (c for c in treeview.get_columns() if c != column and c.get_visible())
    newWidth = allocation.width - sum(c.get_width() for c in otherColumns)  
    newWidth -= treeview.style_get_property("horizontal-separator") * 2 
    if column == col_subject:
        parmblock.col_subject_size = col_subject.get_width()
    if cell.props.wrap_width == newWidth or newWidth <= 0:
        return
    parmblock.subject_wrap_width = newWidth
    cell.props.wrap_width = newWidth
    store = treeview.get_model()
    iter = store.get_iter_first()
    i = 0  
    while iter and store.iter_is_valid(iter):
        store.row_changed((i,), iter)
        iter = store.iter_next(iter)
        treeview.set_size_request(0,-1)
        i += 1

# Menu item Show Category toggled
def cb_show_category_toggled(checkedmenuitem):
    parmblock.show_category = not parmblock.show_category
    checkedmenuitem.set_active(parmblock.show_category)
    col_cat.set_visible(parmblock.show_category)
    view.columns_autosize()

# Menu item Show Completed toggled
def cb_show_completed_toggled(checkedmenuitem):
    parmblock.show_completed = not parmblock.show_completed
    parmblock.filters[0] = (not parmblock.show_completed,parmblock.filters[0][1])
    set_filter_string(parmblock)   

# Menu item Show Deleted toggled
def cb_show_deleted_toggled(checkedmenuitem):
    parmblock.show_deleted = not parmblock.show_deleted
    parmblock.filters[1] = (not parmblock.show_deleted,parmblock.filters[1][1])
    set_filter_string(parmblock)   

# Menu item Show Duedate toggled
def cb_show_duedate_toggled(checkedmenuitem):
    parmblock.show_duedate = not parmblock.show_duedate
    checkedmenuitem.set_active(parmblock.show_duedate)
    col_duedate.set_visible(parmblock.show_duedate)
    view.columns_autosize()

# Menu item Show Headers toggled
def cb_show_headers_toggled(checkedmenuitem):
    parmblock.show_headers = not parmblock.show_headers
    checkedmenuitem.set_active(parmblock.show_headers)
    view.set_headers_visible(parmblock.show_headers)

# catagory combo box at top
def cb_select_category(cb):
    t = cb.get_active()
    if parmblock.current_category == t:
        return
    if t == 0:
        parmblock.filters[2] = (False,"*")
    else:
        parmblock.filters[2] = (True,"*")
    parmblock.current_category = t
    set_filter_string(parmblock)

# About dialog box
def cb_about(*x):
    d=gtk.AboutDialog()
    d.set_name(program_name)
    d.set_version(version_short)
    d.set_comments('Todo list management that syncs with Outlook')
    d.set_authors(['Dwayne Zon <dwayne.zon@gmail.com'])
    d.set_license(license)
    d.set_website('http://ztodo.garage.maemo.org/')
    d.connect('response', lambda d, r: d.destroy())
    d.run()

###################################################################################################################
### searching

#def start_search(*x):
#    global search_list
#
#    btn_find.hide()
#    dialog=gtk.Dialog('Search', root, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
#                      (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
#    i=gtk.Image()
#    i.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_DIALOG)
#    dialog.vbox.pack_start(i, False, False)
#    dialog.vbox.pack_start(gtk.Label('Search For:'), False, False)
#    e=gtk.Entry()
#    dialog.vbox.pack_start(e, False, False)
#    dialog.show_all()
#    r=dialog.run()
#    search_text=e.get_text().strip().upper()
#    dialog.destroy()
#    if r == gtk.RESPONSE_OK:
#        search_list=[]
#        if len(search_text):
#            for row in sorted(((i, x[CATEGORY], x[TEXT], x[NOTE], x[CHECKED]) for i, x in enumerate(list_store)),
#                              key=lambda x: (x[1]+':'+x[2]).upper()):
#                for field in (2, 3):
#                    if search_text in row[field].upper():
#                        search_list.append((row[0], row[1], row[4]))
#            if search_list:
#                btn_find.show()
#                # for some reason it doesn't work if we call it directly
#                gobject.idle_add(search_next)
#            else:
#                timed_info(gtk.STOCK_DIALOG_WARNING, 'No matches found')
#
#def search_next(*x):
#    global search_list, current_category, checked_flag
#
#    row, current_category, checked=search_list[0]
#    del search_list[0]
#    if not len(search_list):
#        timed_info(gtk.STOCK_DIALOG_INFO, 'No more matches found')
#        btn_find.hide()
#
#    # select category with item
#    n=list(x[0] for x in combo_category.get_model()).index(current_category)
#    combo_category.set_active(n)
#    save_data()
#    # reset checked flag so we can see it
#    if checked and not checked_flag:
#        checked_flag=True
#        img_checked.set_active(checked_flag)
#
#    # display it
#    list_store_filtered.refilter()
#    view.columns_autosize()
#    row=list_store_sorted_filtered.convert_child_path_to_path(list_store_filtered.convert_child_path_to_path(row))
#    view.set_cursor(row)
#    return False

###################################################################################################################
### communicate with other applications via OSSO-RPC

##def rpc_callback(interface, method, arguments, user_data):
##    if method == 'add':
##        # add a to-do sent from another program
##        cat, item, note=arguments
##        list_store.append([cat, False, item, note])
##        if cat not in (x[0] for x in combo_category.get_model()):
##            combo_category.append_text(cat)
##    elif method == 'list':
##        # return category list
##        return binascii.b2a_base64(pickle.dumps(list(x[0] for x in combo_category.get_model())))
##    elif method == 'exit':
##        root.quit()

###################################################################################################################
### Define link between generictreemodel and database

# gtk.TreeModel implementation that saves and stores data directly to and from a sqlite database. 

class DBListModel(gtk.GenericTreeModel):
    column_names = ["priority", "iscomplete", "category", "subject", "duedate",    "isnote", "Delflag", "IsRecurring", "LastModificationTime"]
    column_types = (int,str,        int,      str,       str,           str,        str,        int,        int,            str)              
    def __init__(self, table):
        gtk.GenericTreeModel.__init__(self)
        self.db = sqlite.connect(table + ".db", isolation_level=None) 
        self.db.text_factory = str
        self.name = table
        self.dbc = self.db.cursor()
        self.dbc.execute("PRAGMA locking_mode=EXCLUSIVE")
        self.dbc.execute("""
            create table IF NOT EXISTS %s (
                PrimeKey integer primary key,
                Priority Varchar(1),
                IsCompleted Boolean,
                Category varchar(20),
                Subject varchar(255),
                DueDate Date,
                Note varchar(4096),
                DelFlag Boolean,
                IsRecurring Boolean,
                LastModificationTime DateTime)
           """ % self.name)
        self.primekeycache =[]

# Cleanup on close
    def cleanup(self):
        self.dbc.close()
        self.db.close()        
# Return the column names
    def get_column_names(self):
        return self.column_names[:]

# Returns the gtk.TreeModelFlags for the gtk.TreeModel implementation. The gtk.TreeIter data is derived from
#       the database primekey for records and therefore is persistant across row deletion and inserts.
    def on_get_flags(self):
        return gtk.TREE_MODEL_LIST_ONLY | gtk.TREE_MODEL_ITERS_PERSIST

# Returns the number of columns found in the table metadata.
    def on_get_n_columns(self):
        return len(self.column_types)

# Return Column type
    def on_get_column_type(self, index):
        return self.column_types[index]

# Get records from database
    def _get_rows(self,sql,args=()):
        self.dbc.execute(sql,args)
        for raw in self.dbc: 
            yield raw 

# Get one records from database
    def _get_row(self,sql,args=()):
        for i in self._get_rows(sql,args): 
            return i

# Traslates a gtk.TreePath to a gtk.TreeIter. This is done by finding the primekey for the row in the database at the same
#       offset as the path.
    def on_get_iter(self, path):
        if len(path) > 1:
            return None #We are a list not a tree
        self.row = self._get_row("SELECT * FROM %s %s ORDER BY %s LIMIT 1 OFFSET %d" % (self.name, get_filter_string(parmblock,"WHERE"),order_option[parmblock.order_by],path[0]))
        if self.row == None:
            return None
        else:
            return self.row[0]

# Returns the rowrefs offset in the table which is used to generate the gtk.TreePath.
    def on_get_path(self, rowref):
        return self._get_offset(rowref)

# Returns the data for a rowref at the givin column.
# Parameters:
#       rowref -- the rowref passed back in the on_get_iter method.
#       column -- the integer offset of the column desired.
    def on_get_value(self, rowref, column):
        if rowref != self.row[0]:
            self.dbc.execute("SELECT * FROM %s WHERE Primekey = %d" % (self.name, rowref))
            self.row = self.dbc.fetchone()
        if column > len(self.column_names):
            return None
        elif column == 0:
            return rowref
        elif column == 5:
            if self.row[5]:
                return datetime.date(*map(int, self.row[5].split("-")))
            else:
                return None 
        else:
            return self.row[column]

# Load cache
    def _load_cache(self):
        self.primekeycache = [r for (r,) in self._get_rows("SELECT Primekey FROM %s %s ORDER BY %s" % (self.name, get_filter_string(parmblock,"WHERE"),order_option[parmblock.order_by]))]

# Returns the next Primekey found in the sqlite table.
# Parameters:
#       rowref -- the PrimeKey of the current iter.
    def on_iter_next(self, rowref):
        if not len(self.primekeycache):
            self._load_cache()
        try:
            index = self.primekeycache.index(rowref)
            return self.primekeycache[index+1]
        except:
            return None

# Returns children for a given rowref. This will always be None unless the rowref is None, which is our root node.
# Parameters:
#       rowref -- the oid of the desired row.
    def on_iter_children(self, rowref):
        if rowref:
            return None
        self.dbc.execute("SELECT * FROM %s WHERE Primekey =%d LIMIT 1" % (self.name, rowref))
        self.row = self.dbc.fetchone()
        if self.row == None:
            return None
        else:
            return self.row[0]

# Always returns False as List based TreeModels do not have children.
    def on_iter_has_child(self, rowref):
        return False

# Returns the number of children a row has. Since only the  root node may have children, we return 0 unless the request
#       is made for the count of all rows. 
    def on_iter_n_children(self, rowref):
        if rowref:
            return 0
        self.dbc.execute("SELECT COUNT(PrimeKey) FROM %s ORDER BY %s %s" % (self.name,order_option[parmblock.order_by],get_filter_string(parmblock,"WHERE")))
        return int(self.dbc.fetchone()[0])

# Returns the primekey of the nth child from rowref. This will only return a value if rowref is None, which is the root node.
# Parameters:
#       rowref -- the primekey of the row.
#       n -- the row offset to retrieve.
    def on_iter_nth_child(self, rowref, n):
        if rowref:
            return None
        self.dbc.execute("SELECT * FROM %s %s ORDER BY %s LIMIT 1 OFFSET %d" % (self.name,get_filter_string(parmblock,"WHERE"), order_option[parmblock.order_by], n))
        self.row = self.dbc.fetchone()
        if self.row == None:
            return None
        else:
            return self.row[0]

# Always returns None as lists do not have parent nodes.
    def on_iter_parent(self, child):
        return None
# Delete a record
    def _delete_record(self,iter):
        self.row_deleted (self.get_path(iter))
        self.dbc.execute("DELETE FROM %s WHERE Primekey = %d" % (self.name, self.get_value(iter,0)))

# Insert a new row (add button pushed)
    def insertrow(self):
        self.primekeycache=[]
        self.dbc.execute("SELECT PrimeKey FROM %s ORDER BY PrimeKey LIMIT 1" % self.name)
        self.row = self.dbc.fetchone()
        try:
            NewPrimeKey = min(self.row[0]-1,-1)
        except:
            NewPrimeKey = -1
        NewCategory = "None" if parmblock.current_category == 0 else categories[parmblock.current_category][0]
        self.dbc.execute("""Insert into zToDo (PrimeKey, Priority, IsCompleted, Category, Subject, Duedate, Note, DelFlag, IsRecurring, LastModificationTime) values
                                              (?,        ?,         ?,             ?,      ?,         ?,          ?,        ?,      ?,     ?)""", 
                         (NewPrimeKey,"1",0,NewCategory, "" , None, "", 0, 0, ""))
        offset = self._get_offset(NewPrimeKey)
        self.dbc.execute("SELECT * FROM %s WHERE Primekey= %d" % (self.name,NewPrimeKey))
        self.row = self.dbc.fetchone()
        rowref = self.get_iter((offset,)) 
        path = self.get_path(rowref) 
        self.row_inserted(path, rowref)
        detail_edit(path,store)

# Update field in table
    def _set_value(self, rowref, column, newvalue):
        primekey = self.get_value(rowref,0)
        path = (self._get_offset(primekey),)

        self.primekeycache=[]
        sql = "UPDATE %s SET %s = ?, LastModificationTime = ? Where Primekey = ?" % (self.name,column,)
        self.dbc.execute(sql, (newvalue, datetime.datetime.now(), primekey))
        # if not showing completed and setting to complete, then delete from view
        newpath = (self._get_offset(primekey),)
        if path[0] >= 0:
            if newpath[0] < 0:
                self.row_deleted (path)
            elif newpath != path:
                self.row_deleted (path)
                self.row_inserted(newpath,rowref)
            else:
                self.row_changed(path, rowref)

# Toggle boolean field in table 
    def _toggle_value(self, rowref, column, columnnumber):
        primekey = self.get_value(rowref,0)
        path = (self._get_offset(primekey),)
        self.primekeycache=[]
        newstate = not self.get_value(rowref,columnnumber)
        sql = "UPDATE %s SET %s = ?, LastModificationTime = ? Where Primekey = ?" % (self.name,column,)
        self.dbc.execute(sql, (newstate, datetime.datetime.now(), primekey))
        newpath = (self._get_offset(primekey),)
        if path[0] >= 0:
            if newpath[0] < 0:
                self.row_deleted (path)
            elif newpath != path:
                self.row_deleted (path)
                self.row_inserted(newpath,rowref)
            else:
                self.row_changed(path, rowref)

# Get offset of Primekey                         
    def _get_offset(self, primekey):
        if not len(self.primekeycache):
            self._load_cache()
        try:
            return self.primekeycache.index(primekey)
        except:
            return -1

###################################################################################################################
### These parms are configuration items saved from user interaction
class parmblock_model:
    filters=[(False,"IsCompleted=0"),
             (True,"DelFlag=0"),
             (False,"*")]
    current_category = 0
    subject_wrap_width = 50
    show_category = True
    show_completed = True
    show_deleted = False 
    show_duedate = True
    show_headers = True
    filter_string = ''
    order_by = 0
    col_subject_size = 100
    scale_factor = 1

def initcategories(full):
    global categories
    categories = gtk.ListStore(str,'gboolean')
    if full:
        categories.append(["All",False])
        categories.append(["None",True])
        set_filter_string(parmblock)

def set_filter_string(parmblock):
    parmblock.filter_string = ""
    view.set_model(None)
    for (test,fstring) in parmblock.filters:
        if test:
            if parmblock.filter_string != "":
                parmblock.filter_string = parmblock.filter_string + " AND "
            if fstring == "*" and parmblock.current_category != 0:
                parmblock.filter_string = parmblock.filter_string + "Category='" + categories[parmblock.current_category][0] + "'"
            else:
                parmblock.filter_string = parmblock.filter_string + fstring
    store.primekeycache=[]
    view.set_model(store)

def get_filter_string(parmblock,prefix):
    if parmblock.filter_string == "":
        return ""
    else:
        return prefix + " " + parmblock.filter_string
###################################################################################################################
### start of main code

root, vb=application_setup('zToDo', 'zToDo','.zToDo')
print >> sys.stderr, 'Started:', datetime.datetime.now(), ' ',version_long
# Set sort options
order_option = ['priority, subject','category, priority']
#order_option = ['Primekey','priority, subject','category, priority']
view=gtk.TreeView()
store = DBListModel("zToDo")

# Lock the database so that zsync does not replace it while ztodo is running
fstore = open('zToDo.db')
fcntl.flock(fstore,fcntl.LOCK_EX | fcntl.LOCK_NB)
# Load saved parms
try:
    parmfile = file("zToDo.dat","rb")
    parmblock = pickle.load(parmfile)
    parmblock.filters = pickle.load(parmfile)
    initcategories(False)
    savecats=pickle.load(parmfile)
    for i in savecats:
        categories.append(i)
    parmfile.close
except:
    parmblock = parmblock_model()
    initcategories(True)
# Setup dropdown for priorities
priorities = gtk.ListStore(str)
for i in range(1,6):
    priorities.append(str(i))
# buttons at top
hb=gtk.HBox()
vb.pack_start(hb, False, False)

# "new item" button
btn_new=button_with_image(None, gtk.STOCK_ADD, cb_new)
hb.pack_start(btn_new, False, False)
# set up dropdown menu
menu=gtk.Menu()
i=gtk.CheckMenuItem('Show Category')
i.set_active(parmblock.show_category)
i.connect('toggled',cb_show_category_toggled)                      
menu.append(i)
i=gtk.CheckMenuItem('Show Completed')
i.set_active(parmblock.show_completed)
i.connect('toggled',cb_show_completed_toggled)                      
menu.append(i)
i=gtk.CheckMenuItem('Show Duedate')
i.set_active(parmblock.show_duedate)
i.connect('toggled',cb_show_duedate_toggled)                      
menu.append(i)
i=gtk.CheckMenuItem('Show Deleted')
i.set_active(parmblock.show_deleted)
i.connect('toggled',cb_show_deleted_toggled)                      
menu.append(i)
i=gtk.CheckMenuItem('Show Headers')
i.set_active(parmblock.show_headers)
i.connect('toggled',cb_show_headers_toggled)                      
menu.append(i)
i=gtk.MenuItem('Edit Categories')
i.connect('activate',edit_categories)                      
menu.append(i)
i=gtk.MenuItem('View Log')
i.connect('activate',show_logfile)
menu.append(i)
i=gtk.MenuItem('About')
i.connect('activate',cb_about)
menu.append(i)
i=gtk.MenuItem('Close')
i.connect('activate',lambda x: gtk.main_quit())                      
menu.append(i)
#            i.set_submenu(action)
#               ('Sort by Subject, Priority', purge),
#               ('Sort by Priority, Subject', purge),
#               ('Sort by Category, Priority', purge),
#               ('Sort by Priority, Category', purge),
attach_menu(menu)

# select category dropdown
combo_category=gtk.ComboBox(categories)
combo_category.connect('changed', cb_select_category)
cell = gtk.CellRendererText()
combo_category.pack_start(cell,True)                             
combo_category.add_attribute(cell,'text',0)
combo_category.set_active(parmblock.current_category)
hb.pack_end(combo_category, False, False)

# "find next" button                             
#btn_find=button_with_image('Find Next', gtk.STOCK_FIND, search_next)
#btn_find.set_no_show_all(True)
#hb.pack_end(btn_find, False, False)

# scrollable window of to-do items
sw=gtk.ScrolledWindow()
sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
vb.pack_start(sw, True, True, 0)

sw.add(view)
view.set_headers_visible(parmblock.show_headers)
view.set_headers_clickable(True)
view.connect('row-activated', row_select,store)
view.set_fixed_height_mode=gtk.FALSE
view.set_property('hover-selection', True)

# Iscompleted column
rend = gtk.CellRendererToggle()
rend.set_property('activatable', True)
rend.set_property('indicator-size',10)
rend.connect('toggled', cb_iscompleted_toggled, store)
col_check = gtk.TreeViewColumn('', rend)
col_check.add_attribute(rend, "active",2)
view.append_column(col_check)

# Priority column
rend_priority=gtk.CellRendererCombo()
rend_priority.set_property('model', priorities)
rend_priority.set_property('text-column', 0)
rend_priority.set_property('editable', True)
rend_priority.set_property('has-entry', False)
rend_priority.connect('editing-started',cb_priority_edit_start)
rend_priority.connect('edited', cb_priority_edited)
col_priority = gtk.TreeViewColumn('  ', rend_priority, text=1)
col_priority.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) 
col_priority.set_fixed_width(30) 
view.append_column(col_priority)

# categories columnrend_category.set_visible_column(parmblock.show_category)
rend_category=gtk.CellRendererCombo()
rendfilter=categories.filter_new()
rendfilter.set_visible_column(1)
rend_category.set_property('model', rendfilter)
rend_category.set_property('text-column', 0)
rend_category.set_property('editable', True)
rend_category.set_property('has-entry', False)
rend_category.connect('editing-started',cb_category_edit_start)
rend_category.connect('edited', cb_cat_edited)
col_cat=gtk.TreeViewColumn('Category', rend_category, text=3, style=7)
col_cat.set_visible(parmblock.show_category)
view.append_column(col_cat)

rend_subject = gtk.CellRendererText()
rend_subject.props.wrap_mode = gtk.WRAP_WORD
rend_subject.props.wrap_width = parmblock.subject_wrap_width
rend_subject.props.underline = 1
rend_subject.props.underline_set = True
rend_subject.props.scale = parmblock.scale_factor

col_subject = gtk.TreeViewColumn('Subject', rend_subject, text=4, strikethrough=2, style=7)
# this is the column that gets any free space
col_subject.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
col_subject.set_resizable(True)
col_subject.set_expand(True)
view.append_column(col_subject)
col_subject.set_fixed_width(parmblock.col_subject_size) 

# duedate column
rend_duedate = gtk.CellRendererText()
col_duedate = gtk.TreeViewColumn('Due', rend_duedate, text=5, )
col_duedate.set_visible(parmblock.show_duedate)
view.append_column(col_duedate)

# note column
rend=gtk.CellRendererPixbuf()
col_note=gtk.TreeViewColumn('', rend)
col_note.set_cell_data_func(rend, cb_col_note)
col_note.set_expand(False)

view.append_column(col_note)
view.set_model(store)
root.show_all()
#root.connect('key-press-event', cb_key_press2)
view.connect_after("size-allocate", cb_change_column_size, col_subject, rend_subject)

# ignore "SystemError: NULL object passed to Py_BuildValue" at poweroff
try:
    gtk.main()
except:
    pass

# close database
store.cleanup
parmfile = open("zToDo.dat","wb")
pickle.dump(parmblock,parmfile)
pickle.dump(parmblock.filters,parmfile)
savecats=[]
for i in categories:
    savecats.append((i[0],i[1]))
#print savecats
pickle.dump(savecats, parmfile)
parmfile.close
fcntl.flock(fstore,fcntl.LOCK_UN)
fstore.close
