#!/usr/bin/env python
"""
etmWX.py

Copyright (c) 2008-2010 Daniel Graham <daniel.graham@duke.edu>. All rights reserved.

License:
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. [http://www.gnu.org/licenses/gpl.html]

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.
"""
import os, os.path, shutil, re, copy
import wx
import wx.calendar
import wx.html as html
import wx.grid as  gridlib
import datetime
import wx.lib.mixins.listctrl as listmix
import locale
from copy import deepcopy
from etm.etmHTML import ETMhtml
from etm.etmSunMoon import getSunMoon
from etm.etmWeather import getWeather
from etm.etmCAL import ETM12cal
from etm.etmDialog import ETMdialog
from math import floor, ceil
from textwrap import wrap as text_wrap
import etm.etmData as etmData
from etm.etmParsers import *
from wx.lib.expando import ExpandoTextCtrl, EVT_ETC_LAYOUT_NEEDED
import wx.lib.colourdb
from etm.etmVersion import version

from pprint import pprint
import sys

from etmParsers import *
import time
from threading import Timer, Thread
from sched import scheduler

from pkg_resources import resource_filename
try:
    etm_alert = resource_filename(__name__, 'etm_alert.wav')
except:
    etm_alert = 'etm_alert.wav'

#### from the old etmRC ####
pressforhelp = "F1: help"
bgcolor="GRAY99"
bgdark = "GRAY90"
bgweek = bgday = wx.Colour(selectcolor[0], selectcolor[1], selectcolor[2], selectcolor[3])


fgday = 'DARKGREEN'
headercolor = 'NAVYBLUE'
todaycolor = 'NAVYBLUE'
holidaycolor = 'NAVYBLUE'

wxcolors = {}
for kn, val in colors.items():
    rpart,gpart,bpart = val
    wxcolors[kn] = wx.Colour(rpart, gpart, bpart)

datebar1 = '''%b %d (week %W day %j)'''
datebar2 = '''%b %d, %Y'''
datebarformatstring = '''%a, %b %d %Y (week %W day %j)'''
#### end old etmRC ####
wx.SetDefaultPyEncoding(gui_encoding)

columns = [
    ("First", first_column),
    ("Second", second_column),
    ("Third", 300),
    ]


border = wx.BORDER_SIMPLE
leadingzero = re.compile(r'(?<!(:|\d))0+(?=\d)')

if "wxMac" in wx.PlatformInfo:
    hourglass_type = 'png'
    hourglass = "etmlogo_128x128x32.png"
    hg_type = wx.BITMAP_TYPE_PNG
    tdy_padding = 6
elif "wxMSW" in wx.PlatformInfo:
    hourglass_type = 'ico'
    hourglass = "etmlogo.ico"
    tdy_padding = 6
elif "wxGTK" in wx.PlatformInfo:
    hourglass_type = 'png'
    hourglass = "etmlogo_32x32x32.png"
    hg_type = wx.BITMAP_TYPE_PNG
    tdy_padding = 2
else:
    hourglass_type = ''
    hourglass = ''
    hg_type = ''
    tdy_padding = 6

if hourglass:
    try:
        etm_hourglass = resource_filename(__name__, hourglass)
    except:
        etm_hourglass = hourglass

def infoMessage(message, title = 'etm', icon = None):
    dlg = wx.MessageBox("%s" % message,
        caption = "%s" % title, style = wx.OK | icon)

def rgb2hex(r,g,b):
    return "#%02X%02X%02X" % (r,g,b)

class MyTaskBarIcon(wx.TaskBarIcon):
    TBMENU_SHOW  = wx.NewId()
    TBMENU_HIDE  = wx.NewId()
    TBMENU_CLOSE = wx.NewId()

    def __init__(self, frame):
        wx.TaskBarIcon.__init__(self)
        self.frame = frame
        if hourglass_type == 'ico':
            self.SetIcon(wx.Icon(etm_hourglass, wx.BITMAP_TYPE_ICO), 'etm')
        elif hourglass_type == 'png':
            self.SetIcon(wx.Icon(etm_hourglass, wx.BITMAP_TYPE_PNG), 'etm')

        self.Bind(wx.EVT_TASKBAR_LEFT_DCLICK, self.OnTaskBarActivate)
        self.Bind(wx.EVT_MENU, self.OnTaskBarActivate, id=self.TBMENU_SHOW)
        self.Bind(wx.EVT_MENU, self.OnTaskBarDeActivate, id=self.TBMENU_HIDE)
        self.Bind(wx.EVT_MENU, self.OnTaskBarClose, id=self.TBMENU_CLOSE)

    def CreatePopupMenu(self):
        menu = wx.Menu()
        menu.Append(self.TBMENU_SHOW, "Show")
        menu.Append(self.TBMENU_HIDE, "Hide")
        menu.AppendSeparator()
        menu.Append(self.TBMENU_CLOSE, "Close")
        return menu

    def OnTaskBarClose(self, event):
        self.frame.Close()

    def OnTaskBarActivate(self, event):
        if "wxMSW" in wx.PlatformInfo and self.frame.IsIconized():
            self.frame.Iconize(False)
        if not self.frame.IsShown():
            self.frame.Show(True)
        self.frame.Raise()

    def OnTaskBarDeActivate(self, event):
        if self.frame.IsShown():
            self.frame.Hide()

class MyAlert(wx.Frame):
    def __init__(self, title, message, size=(-1, 120)):
        wx.Frame.__init__(self, None, -1, title, size=size,
            style=wx.DEFAULT_FRAME_STYLE | wx.STAY_ON_TOP)
        panel = wx.Panel(self, -1)
        dfont = wx.Font(basefontsize + datefontadj, wx.DEFAULT,
                wx.NORMAL, wx.NORMAL)
        self.panel = panel
        self.txt = wx.StaticText(panel, -1, message)
        self.txt.SetFont(dfont)
        if hourglass_type == 'ico':
            img = wx.Image(etm_hourglass, wx.BITMAP_TYPE_ICO).Scale(48,48)
        elif hourglass_type == 'png':
            img = wx.Image(etm_hourglass, wx.BITMAP_TYPE_PNG).Scale(48,48)
        bit = img.ConvertToBitmap()
        self.img = wx.StaticBitmap(panel, -1, bit)
        box = wx.BoxSizer(wx.VERTICAL)
        hbox = wx.BoxSizer(wx.HORIZONTAL)
        hbox.Add(self.img, 0, wx.LEFT | wx.RIGHT, 5)
        hbox.Add(self.txt, 1, wx.EXPAND | wx.ALL, 10)
        box.Add(hbox, 1)
        subbox = wx.BoxSizer(wx.HORIZONTAL)
        btn = wx.Button(panel, wx.ID_OK, "OK")
        btn.SetDefault()
        subbox.Add(btn, 0, wx.ALIGN_RIGHT | wx.ALL, 8)
        self.Bind(wx.EVT_BUTTON, self.OnClose, btn)
        box.Add(subbox, 0, wx.ALIGN_CENTER | wx.ALL, 0)
        panel.SetSizer(box)
        self.Layout()
        # sound = wx.Sound(etm_alert)
        # sound.Play(wx.SOUND_SYNC)
        # wx.Bell()

    def OnCancel(self, event):
        self.Destroy()

    def OnClose(self, event):
        self.Destroy()


class Mystatusbar(wx.StatusBar):
    def __init__(self, parent):
        wx.StatusBar.__init__(self, parent, -1)
        self.SetFieldsCount(4)
        self.SetStatusStyles([wx.SB_FLAT,wx.SB_FLAT,wx.SB_FLAT,wx.SB_FLAT])
        self.SetStatusWidths([90, 132, -1, -1])
        self.sizeChanged = False

        self.SetStatusText("", 0)
        self.SetStatusText("", 1)
        self.SetStatusText("", 2)
        self.SetStatusText("", 3)

class MyListCtrl(wx.ListView, listmix.ListCtrlAutoWidthMixin):
    def __init__(self, parent, ID, pos=wx.DefaultPosition,
                 size=(-1,-1), style=wx.LC_SINGLE_SEL):
        wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
        listmix.ListCtrlAutoWidthMixin.__init__(self)

class MyTodayButton(wx.Button):
    def AcceptsFocus(self, *args, **kwargs):
        return False

class MyCalCtrl(wx.calendar.CalendarCtrl):

    def AcceptsFocus(self, *args, **kwargs):
        return True

class MyBusyPanel(wx.Window):
    def __init__(self, parent):
        wx.Window.__init__(self, parent, -1, pos=(0,0),
            size = (220,210), style = border)
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.busy_events = {}

    def AcceptsFocus(self, *args, **kwargs):
        return False

    def OnSize(self, event):
        self.Refresh()

    def OnEraseBackground(self, event):
        pass

    def SetEvents(self, events):
        events = self.SortEvents(events)
        self.busy_events.clear()
        self.busy_events = deepcopy(events)
        self.Refresh()

    def SortEvents(self, events):
        events_sort = {}
        for key, value in events.items():
            tmp_lst = [(x[2], x[0], x[1], x[2], x[3]) for x in value]
            tmp_lst.sort()
            events_sort[key] = [x[1:] for x in tmp_lst] 
        return(events_sort)

    def OnPaint(self, evt=None):
        pdc = wx.PaintDC(self)
        try:
            dc = wx.GCDC(pdc)
        except:
            dc = pdc
        # dc.SetBackground(wx.Brush(bgdark))
        dc.SetBackground(wx.Brush(bgcolor))
        dc.SetPen(wx.LIGHT_GREY_PEN)
        dc.Clear()

        self.top = top = 5
        self.left = left = 16
        self.rows = rows = 16
        # self.w = w = 22
        self.w = w = 14
        self.h = h = 12
        H = rows*h

        for i  in range(0,17):
            j = top+h*i
            for k in range(0,8):
                dc.DrawLine(left+k*(w+15), j, (k+1)*(w+15)+1, j)
        for i  in range(0,8):
            j = left + (w+15)*i
            dc.DrawLine(j,top,j, H+top)
            dc.DrawLine(j+w,top,j+w, H+top)

        dc.SetFont(wx.Font(10,wx.DEFAULT,wx.NORMAL,wx.NORMAL))
        dc.SetPen(wx.LIGHT_GREY_PEN)

        for i  in range(8,22):
            j = h*i
            if i%3 == 0:
                if i <= 12 or not use_ampm:
                    t = i
                else:
                    t = i-12
                dc.DrawText('%+2s' % t, 0, j-84)

        r, g, b = busycolor
        penclr   = wx.Colour(r, g, b, 96)
        brushclr = wx.Colour(r, g, b, 96)   # half transparent
        dc.SetPen(wx.Pen(penclr))
        dc.SetBrush(wx.Brush(brushclr))
        if self.busy_events:
            days = self.busy_events.keys();
            days.sort()
            for day in days:
                for event in self.busy_events[day]:
                    id, col, sm, em = event
                    start_pos = int(floor(float(max(sm, 7*60) - 7*60)/5.0))
                    end_pos = int(ceil(float(min(em, 23*60) - 7*60)/5.0))
                    h = end_pos - start_pos
                    y = top + start_pos
                    x = left + col*(w+15)
                    self.rect = wx.Rect(0,0,w,h)
                    pos = (x,y)
                    self.rect.SetPosition(pos)
                    dc.DrawRoundedRectangleRect(self.rect, 0)

class MyFrame(wx.Frame):
    def __init__(self):
        self.selagenda = self.selfile = ''
        fontfam = wx.DEFAULT
        lfont = wx.Font(basefontsize + listfontadj, fontfam,
                wx.NORMAL, wx.NORMAL)
        dfont = wx.Font(basefontsize + datefontadj, fontfam,
                wx.NORMAL, wx.NORMAL)
        sfont = wx.Font(basefontsize + statusfontadj, fontfam,
                wx.NORMAL, wx.NORMAL)
        wx.Frame.__init__(self, None, -1, 'etm')
        self.frame_id = self.GetId()
        panel = wx.Panel(self, -1)
        if hourglass_type == 'ico':
            self.SetIcon(wx.Icon(etm_hourglass, wx.BITMAP_TYPE_ICO))
            self.tbicon = MyTaskBarIcon(self)
        elif hourglass_type == 'png':
            self.SetIcon(wx.Icon(etm_hourglass, wx.BITMAP_TYPE_PNG))
            self.tbicon = MyTaskBarIcon(self)
        self.panel = panel
        self.panel.SetBackgroundColour(bgdark)
        self.Bind(wx.EVT_CLOSE, self.OnQuit)
        self.Bind(wx.EVT_ICONIZE, self.OnIconify)
        self.itemlist = MyListCtrl(panel, -1, size = wx.DefaultSize,
            style= wx.LC_REPORT
            | wx.LC_SINGLE_SEL
            | wx.WANTS_CHARS
            | wx.LC_NO_HEADER
            | border )
        self.itemlist.SetMinSize((610, -1))
        self.SetBackgroundColour(bgcolor)

        self.set_ltz()

        if use_ampm:
            tdy_pat = 'XX:XXam WWW, MMM, DD ZZZ '
        else:
            tdy_pat = 'XX:XX WWW, MMM, DD ZZZ'

        self.tdy = wx.StaticText(panel, -1,
            tdy_pat, 
            size = wx.DefaultSize,
            style = wx.BORDER_NONE
            | wx.ALIGN_CENTER
            | wx.ALIGN_CENTER_VERTICAL
            )
        self.tdy.SetFont(dfont)
        if sundayfirst:
            self.cal = wx.calendar.CalendarCtrl(panel, -1, wx.DateTime_Now(), size = (-1,-1),
                    style = wx.calendar.CAL_SHOW_HOLIDAYS
                    | wx.calendar.CAL_SEQUENTIAL_MONTH_SELECTION
                    | border
                    | wx.WANTS_CHARS
                    )
        else:
            self.cal = wx.calendar.CalendarCtrl(panel, -1, 
                wx.DateTime_Now(), size = (-1,-1),
                style = wx.calendar.CAL_SHOW_HOLIDAYS
                | wx.calendar.CAL_SEQUENTIAL_MONTH_SELECTION
                | wx.calendar.CAL_MONDAY_FIRST
                | border
                | wx.WANTS_CHARS
                )
        if calfontsize:
            cfont = wx.Font(calfontsize, fontfam,
                wx.NORMAL, wx.NORMAL)
            self.cal.SetFont(cfont)
        self.cal.SetHeaderColours(headercolor,bgcolor)
        self.cal.SetHolidayColours(holidaycolor,bgcolor)
        self.cal.SetHighlightColours(fgday,bgday)
        self.cal.SetBackgroundColour(bgcolor)
        self.Bind(wx.calendar.EVT_CALENDAR_SEL_CHANGED, self.OnCalSelected,
                id=self.cal.GetId())
        self.cal.Bind(wx.EVT_CHAR, self.OnChar)
        self.cal_id = self.cal.GetId()
        for i in range(1,32):
            attr = wx.calendar.CalendarDateAttr()
            self.cal.ResetAttr(i)
            attr.SetBackgroundColour(bgcolor)
            self.cal.SetAttr(i, attr)

        self.busy = MyBusyPanel(panel)
        self.busy.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.busy.Bind(wx.EVT_LEFT_DCLICK, self.OnItemActivated)
        self.busy.Bind(wx.EVT_CHAR, self.OnChar)
        self.busy.SetBackgroundColour(bgcolor)

        self.datebar = wx.StaticText(panel, -1,
            "XXX, XXX XX (week XX day XXX) ~ XXX, XXX XX XXXX", 
            size = wx.DefaultSize,
            style = wx.BORDER_NONE
            | wx.ALIGN_CENTER_VERTICAL
            )
        # self.datebar_id = self.datebar.GetId()
        self.datebar.SetFont(dfont)
        self.search_menu = []
        self.search = wx.SearchCtrl(panel, size=(120,-1),
            style=wx.TE_PROCESS_ENTER)
        self.search.ShowCancelButton(True)
        self.search.SetMenu( self.SearchMenu() )
        self.searchMenu = self.search.GetMenu()
        self.search.SetFont(dfont)
        self.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self.OnSearchCancel, self.search)
        self.Bind(wx.EVT_TEXT_ENTER, self.OnSearch, self.search)
        self.search.Bind(wx.EVT_MENU, self.OnMenuChoice)
        self.search.Bind(wx.EVT_CHAR, self.OnSearchChar)
        self.itemlist.SetFont(lfont)
        self.itemlist.SetBackgroundColour(bgcolor)
        self.cal.Bind(wx.EVT_CHAR, self.OnChar)
        self.index = -1
        for i in range(0,len(columns)):
            if i in [1]:
                self.itemlist.InsertColumn(i, columns[i][0],wx.LIST_FORMAT_RIGHT)
            else:
                self.itemlist.InsertColumn(i, columns[i][0])
        # don't set the width for the last column
        for i in range(0,len(columns)-1):
            self.itemlist.SetColumnWidth(i, columns[i][1])
        self.itemlist.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.itemlist.Bind(wx.EVT_CHAR, self.OnChar)
        self.itemlist.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated)
        self.itemlist.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected)
        self.itemlist.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnItemDeSelected)
        self.itemlist_id = self.itemlist.GetId()
        self.details = ExpandoTextCtrl(panel, size=(-1, -1), value="",
            style = border | wx.TE_READONLY)
        self.details.SetFont(lfont)
        self.details_id = self.details.GetId()
        #  self.details.Bind(wx.EVT_SET_FOCUS, self.OnFocus)
        self.details.SetBackgroundColour(bgcolor)
        self.Bind(EVT_ETC_LAYOUT_NEEDED, self.OnRefit, self.details)

        self.statusbar = Mystatusbar(panel)
        self.statusbar.SetFont(sfont)
        self.statusbar_id = self.statusbar.GetId()
        self.statusbar.SetBackgroundColour(bgdark)
        self.statusbar.SetStatusText(pressforhelp, 0)
        self.alert_count = 0
        self.alert_ids = []
        self.alert_msg = []
        self.alert_times = []
        self.alert_update = None
        self.alerts_running = False
        self.DetailsDirty = False
        self.InDetails = False
        self.cal_advance = 0
        self.timer_status = 'stopped' # or running or paused
        self.timer_minutes = 0
        self.timer_play = False
        self.timer_entry = ''
        self.last_time = None
        self.timer_delta = None
        self.sel_id = None
        self.cur_abbrv = {}
        # for the history lists
        self.main_lst = []
        self.item_lst = []
        self.busy_lst = []
        self.data_lst = []
        self.ledger_lst = []
        self.cur_lst = []
        self.cur_hist = []
        self.clip = None
        self.sound = wx.Sound(etm_alert)

        # The layout
        vbox = wx.BoxSizer(wx.VERTICAL)
        hbox_show_short = wx.BoxSizer(wx.HORIZONTAL)
        hbox_show_short.Add(self.tdy, 0, 
            wx.ALIGN_CENTER_VERTICAL 
            | wx.LEFT 
            | wx.RIGHT
            , 50)
        hbox_show_short.Add(self.datebar, 3,
            wx.ALIGN_CENTER_VERTICAL 
            | wx.LEFT
            , 10)
        if "wxMac" in wx.PlatformInfo:
            hbox_show_short.Add(self.search, 1,
                wx.ALIGN_CENTER_VERTICAL 
                | wx.ST_NO_AUTORESIZE
                | wx.BOTTOM
                , 5)
        elif "wxMSW" in wx.PlatformInfo:
            hbox_show_short.Add(self.search, 1,
                wx.ALIGN_CENTER_VERTICAL 
                | wx.ST_NO_AUTORESIZE
                | wx.BOTTOM
                , 5)
        elif "wxGTK" in wx.PlatformInfo:
            hbox_show_short.Add(self.search, 1,
                wx.ALIGN_CENTER_VERTICAL 
                | wx.ST_NO_AUTORESIZE
                | wx.ALL, 0)
        else:
            hbox_show_short.Add(self.search, 1,
                wx.ALIGN_CENTER_VERTICAL 
                | wx.ST_NO_AUTORESIZE
                | wx.ALL, 0)
        vbox.Add(hbox_show_short, 0, wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT | wx.BOTTOM, 4)
        vbox1 = wx.BoxSizer(wx.VERTICAL)
        vbox1.Add(self.cal, 0, wx.EXPAND | wx.ALIGN_CENTER | wx.LEFT, 4)
        vbox1.Add(self.busy, 1, wx.EXPAND | wx.ALIGN_CENTER | wx.LEFT, 4)
        vbox2 = wx.BoxSizer(wx.VERTICAL)
        vbox2.Add(self.itemlist, 2, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 4)
        vbox2.Add(self.details, 0,  wx.EXPAND | wx.LEFT | wx.RIGHT,  4)
        hbox1 = wx.BoxSizer(wx.HORIZONTAL)
        hbox1.Add(vbox1, 0, wx.EXPAND)
        hbox1.Add(vbox2, 1, wx.EXPAND)
        hbox2 = wx.BoxSizer(wx.HORIZONTAL)
        vbox3 = wx.BoxSizer(wx.VERTICAL)
        vbox3.Add(self.statusbar, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 4)
        hbox2.Add(vbox3, -1, wx.EXPAND)
        vbox.Add(hbox1, -1, wx.EXPAND)
        vbox.Add(hbox2, 0, wx.EXPAND)
        panel.SetAutoLayout(True)
        # Show events for the current date.
        self.lastday = self.lastweek = self.lastmonth = None
        day = datetime.datetime.now()
        self.selday = datetime.datetime(day.year, day.month, day.day,
            0, 0, 0)
        self.Layout()
        panel.SetSizerAndFit(vbox)
        self.Fit()
        self.SetMinSize(self.GetSize())
        self.etmData = etmData.ETMData()
        if self.etmData.errors:
            self.show_errors(self.etmData.errors)
            self.etmData.errors = []
        self.etmData.get_alerts()
        self.start_alerts()
        self.Today()

    def cancel(self, msg=['cancel entry?']):
        return(self.confirmChange(msg = msg))

    def confirmChange(self, msg=[]):
        s = "\n".join(msg)
        dlg = wx.MessageDialog(None, s, 'etm',
                wx.YES_NO | wx.YES_DEFAULT )
        return(dlg.ShowModal() == wx.ID_YES)

    def confirm(self, item):
        s = "\n".join(msg)
        res = False
        #  if etmUser:
                #  item['details'] = "%s @U %s" % (item['details'], etmUser)
                #  print "details", item['details']
        if has(item, 'id'):
            dlg = wx.MessageDialog(None, 'save changes?',  'etm',
                    wx.YES_NO | wx.YES_DEFAULT )
            if auto_set_timezone and 'd' in item and 'z' not in item:
                if self.ltz:
                    item['details'] = "%s @z %s" % (item['details'], self.ltz)
                else:
                    print "error: @z missing but could not set local zone"
            if dlg.ShowModal() == wx.ID_YES:
                self.itemReplace(item['id'], item['details'])
                res = True
            dlg.Destroy()
        elif has(item, 'type'):
            if item['type'] == 'project':
                file = self.getFile(item['type'], mode='create')
                if file:
                    self.createProject(item['details'], file)
                    res = True
            else:
                file = self.getFile(item['type'], mode='append')
                if file:
                    self.itemAdd(item['details'], file)
                    res = True

        return(res)

    def set_ltz(self):
        if not auto_set_timezone:
            self.ltz = self.ltn = ''
        else:
            possible = get_localtz(timezones)
            choices = ["%s, %s" % (x[0], x[1]) for x in possible]
            ltz = 'none'
            ltn = ''
            if choices:
                if len(choices) > 1:
                    prompt = """\
    The following are possible values for your local time zone. 
    Please select the one you would like to use.

    If your local time zone is not in this list you should edit
        %s
    add your local time zone to 'timezones' and remove any
    entries that you do not need.""" % etmrc
                    res = self.getSelection(prompt, choices)
                else: # must be just one
                    res = choices[0]
                if res:
                    ltz, ltn = res.split(', ')
            self.ltz = ltz
            self.ltn = ltn

    def openProject(self):
        if self.sel_id:
            rel_path, num = self.sel_id.split(':')
            file = os.path.join(etmdata, rel_path)
            self.sel_id = None
        else: 
            file = self.getFile('project', mode="open")
        if file:
            command = editcmd % {'e': editor, 'n': 1, 'f': file}
            self.etmData.backup(file)
            self.etmData.changed.append(file)
            os.system(command)
            self.update_alerts()

    def openRC(self):
        command = editcmd % {'e': editor, 'n': 1, 'f': etmrc}
        os.system(command) 
        dlg = wx.MessageDialog(None, 
            "etm must be restarted for changes in\n   %s\nto take effect." \
                    % etmrc, 'etm', wx.OK)
        dlg.ShowModal()

    def OnKeyDown(self, event):
        keycode = event.GetKeyCode()
        if keycode == wx.WXK_CONTROL:
            return()
        elif keycode == wx.WXK_SPACE:
            self.sel_id = None
            self.Today()
            self.refreshprompt()
        else:
            event.Skip()

    def OnSearchChar(self, evnt):
        keycode = evnt.GetKeyCode()
        if keycode == 27:               # Escape
            self.OnSearchCancel(evnt)
        else:
            evnt.Skip()


    def OnSearchCancel(self, evnt):
        self.search.SetValue('')
        self.showDay()
        self.itemlist.SetFocus()

    def OnSearch(self, evnt):
        search_str = self.search.GetValue().strip()
        if search_str:
            if search_str not in self.search_menu:
                self.search_menu.append(search_str)
                self.search_menu.sort()
                self.search.SetMenu( self.SearchMenu() )
            self.DoSearch(search_str)

    def DoSearch(self, search_str):
        matching = self.etmData.get_matching(search_str)
        if len(matching) > 0:
            self.itemlist.DeleteAllItems()
            self.id2index = {}
            self.index2id = {}
            self.index = -1
            self.details.Clear()
            for i in range(len(matching)):
                first, second, third, id, weekday, attr = matching[i]
                columns = [first, second, third]
                index = self.itemlist.InsertStringItem(i, id)
                self.id2index[(id, weekday)] = index
                self.index2id[index] = id
                for j in range(len(columns)):
                    self.itemlist.SetStringItem(index, j,
                        " %s" % columns[j])
                try: 
                    attr_color = wxcolors[attr]
                except:
                    print 'except wxcolors attr', attr
                item = self.itemlist.GetItem(index)
                item.SetTextColour(attr_color)
                self.itemlist.SetItem(item) 
            label = "matching '%s'" % search_str.strip()
            self.datebar.SetLabel(label)

    def SearchMenu(self):
        menu = wx.Menu()
        item = menu.Append(-1, "Recent Searches")
        item.Enable(False)
        for i in range(len(self.search_menu)):
            menu.Append(i+1, self.search_menu[i]) 
        return menu

    def OnMenuChoice(self, evt):
        item = self.search_menu[evt.GetId() - 1]
        self.search.SetValue(item)
        self.DoSearch(item)

    def getFile(self, type, mode='append'):
        file = None
        if mode == 'create':
            filedlg = wx.FileDialog(
                self, message = "New project file",
                defaultDir = self.etmData.defaultDir[type],
                defaultFile="",
                wildcard="etm project file (*.txt)|*.txt",
                style=wx.SAVE
            )
            if filedlg.ShowModal() == wx.ID_OK:
                file = filedlg.GetPath()
                name, ext = os.path.splitext(file)
                fname = "%s.txt" % name
                if os.path.isfile(fname):
                    self.show_errors(
                            ["Error: file '%s' already exists" % fname])
                    return()
                return(fname)
            else:
                return()
        elif mode == 'open':
            filedlg = wx.FileDialog(
                self, message = "Open project file in external editor",
                defaultDir = self.etmData.defaultDir[type],
                defaultFile="",
                wildcard="etm project file (*.txt)|*.txt",
                style=wx.OPEN
            )
            if filedlg.ShowModal() == wx.ID_OK:
                file = filedlg.GetPath()
                return(file)
            else:
                return()
        elif mode == 'move':
            filedlg = wx.FileDialog(
                self, message = "Move item to project file",
                defaultDir = self.etmData.defaultDir[type],
                defaultFile="",
                wildcard="etm project file (*.txt)|*.txt",
                style=wx.OPEN
            )
            if filedlg.ShowModal() == wx.ID_OK:
                file = filedlg.GetPath()
                return(file)
            else:
                return()
        else:
            flst = [ x[0] for x in self.etmData.filelist ]
            #  flst.sort()
            if type == 'event':
                cur = self.etmData.current_hash[event]
            elif type == 'task':
                cur = self.etmData.current_hash[task]
            elif type == 'action':
                cur = self.etmData.current_hash[action]
            elif type == 'note':
                cur = self.etmData.current_hash[note]
            elif type == 'reminder':
                cur = self.etmData.current_hash[reminder]
            flst.sort()

            dflt = flst.index(cur)
            if mode == 'append':
                file = self.getSelection("The project file for the new %s" %
                        type, flst, dflt)
            elif mode == 'open':
                file = self.getSelection("The project file to open with %s" %
                        editor, flst, dflt)
        if file:
            return(os.path.join(etmdata, file))
        else:
            return()

    def createProject(self, line, file):
        if os.path.isfile(file):
            return([False, "Error: file '%s' already exists" % file])
        fo = open(file, 'w')
        fo.write("%s\n" % line)
        fo.close()
        self.etmData.logaction(file, "created '%s'" % file)


    def itemAdd(self, line, rel_path):
        file = os.path.join(etmdata, rel_path)
        self.etmData.lineAdd(line, file)
        self.update_alerts()

    def itemReplace(self, id, line):
        rel_path, num = id.split(':')
        file = os.path.join(etmdata, rel_path)
        self.etmData.lineReplace(num, line, file)
        self.update_alerts()

    def itemDelete(self, id):
        if self.confirmChange(msg = ['delete selected item?']): 
            rel_path, num = id.split(':')
            num = int(num)
            file = os.path.join(etmdata, rel_path)
            self.etmData.lineDelete(num, file)
            self.update_alerts()

    def itemMove(self, id):
        file = self.getFile('project', mode="move")
        if file:
            rel_path, num = id.split(':')
            num = int(num)
            source = os.path.join(etmdata, rel_path)
            # get the line first
            line = self.etmData.lineGet(num, source)
            target = os.path.join(etmdata, file)
            # add it to the target
            self.etmData.lineAdd(line, target)
            # and then delete it from the source
            self.etmData.lineDelete(num, source)
            self.update_alerts()


    def itemFinish(self, id):
        if self.etmData.id2item[self.sel_id]:
            item = self.etmData.id2item[self.sel_id]
            orig_value = item['details']
            print "itemFinish: orig_value", orig_value
            new_value = "%s @f %s" % (orig_value, datetime.date.today().strftime(date_fmt))
            response = self.getItem(size=(400, -1), value=new_value, check = True, id = self.sel_id)

    def itemUnfinish(self, id):
        if self.etmData.id2item[self.sel_id]:
            item = self.etmData.id2item[self.sel_id]
            orig_value = item['details']
            m = finish_regex.search(orig_value)
            if m:
                new_value = finish_regex.sub('', orig_value)
                self.getItem(size=(400, -1), value=new_value, check = True, id = self.sel_id)


    def getItem(self,  size=wx.DefaultSize, value='', check = False, 
        id = None):
        if value == '':
            dlg = ETMdialog(self, size=size, value = value, mode = 'project', check = check, id = id)
        else:
            dlg = ETMdialog(self, size=size, value = value, mode = 'item', check = check,  id = id)
        entry = ''
        dlg.text.SetFocus()
        if dlg.ShowModal() == wx.ID_OK:
            entry = dlg.text.GetValue()
        else:
            entry = None
        dlg.Destroy()
        return(entry)

    def getOpts(self,  size=wx.DefaultSize, value='', check = False, 
        id = None):

        if value == 'm':
            self.cur_hist = main_history
            self.cur_lst = self.main_lst
        elif value == 'i':
            self.cur_hist = item_history
            self.cur_lst = self.item_lst
        elif value == 'b':
            self.cur_hist = busy_history
            self.cur_lst = self.busy_lst
        elif value == 'd':
            self.cur_hist = data_history
            self.cur_lst = self.data_lst
        elif value == 'l':
            self.cur_hist = ledger_history
            self.cur_lst = self.ledger_lst
        else:
            self.cur_hist = []
            self.cur_lst = []

        dlg = ETMdialog(self, size=size, value = value, check = check, 
            id = id, mode = 'view')
        entry = ''
        dlg.text.SetFocus()
        if dlg.ShowModal() == wx.ID_OK:
            entry = dlg.text.GetValue()
        else:
            dlg.Destroy()
        self.statusbar.Refresh()
        retval = "%s %s" % (value,entry)
        return(retval)

    def OnFocus(self, event):
        self.cal.SetFocus()

    def OnItemActivated(self, event):
        if self.etmData.id2item[self.sel_id]:
            item = self.etmData.id2item[self.sel_id]
            value = item['details']
            self.getItem(size=(400, -1), value=value, check = True, id = self.sel_id)

    def OnItemCloned(self, event):
        if self.etmData.id2item[self.sel_id]:
            item = self.etmData.id2item[self.sel_id]
            value = item['details']
            self.getItem(size=(400, -1), value=value, check = True)

    def SelectOpen(self, event):
        if self.etmData.id2item[self.sel_id]:
            item = self.etmData.id2item[self.sel_id]
            if not 'goto' in item:
                return()
            goto_hsh = item['goto']
            goto_lst = goto_hsh.keys()
            goto_lst.sort()
            key = self.getSelection("Open using default application:", goto_lst, 0)
            if key:
                try:
                    OpenWithDefault(goto_hsh[key])
                except:
                    self.show_errors(
                            ["Error: could not open '%s'" % goto_hsh[key]])
        return()



    def newItem(self, type):
        if type == 'e':
            self.cur_tmpl = evnt_tmpl
            t = '* '
        elif type == 't':
            self.cur_tmpl = task_tmpl
            t = '. '
        elif type == 'A':
            self.cur_tmpl = actn_tmpl
            t = '~ '
        elif type == 'n':
            self.cur_tmpl = note_tmpl
            t = '! '
        elif type == 'r':
            self.cur_tmpl = rmdr_tmpl
            t = '& '
        elif type == 'p':
            self.cur_tmpl = proj_tmpl
            t = ''
        else:
            return()
        self.cur_abbrv = {}
        for item in self.cur_tmpl:
            m = abbrv_regex.match(item)
            if m:
                self.cur_abbrv[m.group(1)] = m.group(2)
        response = self.getItem(size=(400, -1), value = t, check = True)

    def TimerToggle(self):
        if self.timer_status == 'stopped':
            # beginning
            self.last_time = datetime.datetime.now()
            self.timer_delta = datetime.timedelta(seconds = 0)
            self.timer_minutes = 0
            response = self.getItem(size=(400, -1), value = '~ ')
            # get initial action entry
            if response:
                self.timer_status = 'running'
                self.timer_entry = response
        elif self.timer_status == 'running':
            # pausing
            now = datetime.datetime.now()
            self.timer_delta += (now - self.last_time)
            self.timer_status = 'paused'
        elif self.timer_status == 'paused':
            # restarting
            self.last_time = datetime.datetime.now()
            self.timer_status = 'running'
        self.refreshprompt()

    def TimerStop(self):
        if self.timer_status in ['running', 'paused']:
            now = datetime.datetime.now()
            self.timer_delta += (now - self.last_time)
            self.timer_status = 'stopped'
            self.timer_minutes = self.timer_delta.seconds/60
            self.cur_tmpl = actn_tmpl
            if self.timer_delta.seconds%60 >= 30:
                self.timer_minutes += 1
            td = datetime.date.today().strftime(date_fmt)
            response = self.getItem(size=(400, -1), 
                value = "%s @p %s @d %s" % (self.timer_entry, self.timer_minutes, td), 
                check = True)
        else:
            self.newItem('A')
        self.refreshprompt()

    def refreshprompt(self):
        self.statusbar.SetStatusText(pressforhelp, 0)
        ddn = datetime.datetime.now(tzlocal())
        d = ddn.strftime(status_fmt)
        t = ddn.strftime(timefmt)
        t = t.lower()
        if use_ampm:
            t = t[:-1]
        td = leadingzero.sub('', "%s %s %s" % (t, d, self.ltn))
        self.tdy.SetLabel(td)
        if self.alert_times:
            label = "next alert: %s  " % self.alert_times[0]
        else:
            label = ""
        self.statusbar.SetStatusText(label, 1)
        if self.timer_status == 'stopped':
            self.statusbar.SetStatusText('', 3)
        else:
            timer = "%sm %s: %s" % (self.timer_minutes, self.timer_status, self.timer_entry)
            self.statusbar.SetStatusText(timer, 3)
            if self.timer_play:
                self.sound.Play(wx.SOUND_SYNC)
                self.timer_play = False

        y,m,d = map(int, self.selday.strftime("%Y %m %d").split())

        self.selcalday = wx.DateTimeFromDMY(d,m-1,y)
        self.cal.SetDate(self.selcalday)

        s = get_mainopts(self.etmData.options)
        self.statusbar.SetStatusText(s, 2)
        self.statusbar.Refresh()


    def showChoice(self, event):
        str = event.GetString()
        tmp = "%s" % str.split()[0]
        if tmp == 'all':
            f = 'l'
        else:
            f = tmp[0]
        # self.filter = f
        self.show()

    def OnClick(self, event):
        self.Today()

    def OnRefit(self, evt):
        # For the Expando control
        self.Fit()

    def DownInList(self, n = 1):
        if len(self.show_lst):
            if self.index < 0:
                index = 0
            else:
                index = self.index + n
            self.index = min(index, self.itemlist.GetItemCount() - 1)
            self.itemlist.SetFocus()
            self.itemlist.EnsureVisible(self.index)
            self.itemlist.SetItemState(self.index,
                wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
                wx.LIST_STATE_SELECTED )

    def UpInList(self, n = 1):
        if len(self.show_lst):
            index = self.index - n
            self.index = max(-1, index)
            if self.index < 0:
                self.showDay()
                self.cal.SetFocus()
                self.sel_id = None
            else:
                self.index = index
                self.itemlist.SetFocus()
                self.itemlist.EnsureVisible(self.index)
                self.itemlist.SetItemState(self.index,
                    wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
                    wx.LIST_STATE_SELECTED )

    def OnLeftDown(self, evt):
        # get the pt_num and date (col) for the location clicked in busy
        # and then select that date and action in gtd
        pos = evt.GetPosition()
        x,y = pos - (self.busy.left, self.busy.top)
        # c = min((x-self.busy.left)/(self.busy.w+11)+1, 6)
        c = min((x+5)/(self.busy.w+15), 6)
        m = 7*60+y*5

        sel_id = None
        selday = None
        for day, events in self.busy.busy_events.items():
            for event in events:
                id, col, sm, em = event
                if col == c and m >= sm and m <= em:
                    sel_id = id
                    selday = day
                    selcol = col
                    break

        if sel_id and (sel_id, selcol) in self.id2index:
            self.sel_id = sel_id
            index = self.id2index[(self.sel_id, selcol)]
            self.itemlist.EnsureVisible(index)
            self.itemlist.SetItemState(index,
                wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
                wx.LIST_STATE_SELECTED )
        evt.Skip()


    def OnItemSelected(self, event):
        sel_index = event.m_itemIndex
        self.index = sel_index
        sel_id = self.index2id[sel_index]
        self.sel_id = sel_id
        if self.etmData.id2item[sel_id]:
            item = self.etmData.id2item[sel_id]
        self.details.Clear()
        if show_ptitle:
            self.details.WriteText("%s [%s:%s%s]" % (item['details'], item['id'], item['j'], item['defaults']))
        else:
            self.details.WriteText("%s [%s%s]" % (item['details'], item['id'], item['defaults']))
        self.itemlist.EnsureVisible(sel_index)
        self.itemlist.SetItemState(sel_index,
            wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
            wx.LIST_STATE_SELECTED )
        event.Skip()

    def OnItemDeSelected(self, event):
        self.sel_id = None
        self.details.Clear()

    def start_alerts(self):
        self.alerts_running = True
        self.alert_sched = scheduler(time.time, time.sleep)
        self.alert_thread = Thread(target=self.alert_sched.run)
        self.alert_sched.enter(0, 0, self.update_alerts, ())
        self.alert_thread.setDaemon(1)
        self.alert_thread.start()

    def stop_alerts(self):
        if self.alerts_running:
            self.alerts_running = False
            if not self.alert_sched.empty():
                for a in self.alert_ids:
                    try:
                        self.alert_sched.cancel(a)
                    except:
                        pass
        return(True)

    def alert_status(self):
        ret = ['<title>etm alert status</title>',
                '<pre>']
        if self.alerts_running:
            curtime = time.time()
            lastmtime = self.alert_update
            nextsec = int(lastmtime + 60)
            nexttime = datetime.datetime.fromtimestamp(nextsec).strftime("%H:%M:%S")
            difftime = int(lastmtime + inc - curtime)
            if difftime >= 0:
                lines = self.alert_msg
                if len(lines) > 0:
                    ret.extend(["The following alerts are currently in the queue:", ''])
                    for line in lines:
                        ret.append('    <font color="magenta">%s</font>' % line)
                else:
                    ret.append("The alert queue is currently empty.")
                ret.extend(['',
                    "The next update will be %s seconds from now at %s." %
                    (difftime, nexttime)])
        else:
            ret.append("The alert queue is not running.")
        # ret.append("getresponse active: %s" % self.in_getresponse )
        ret.append("</pre></body>")
        html = "\n".join(ret)
        dlg = ETMhtml(self, size=(600,340), page = html)
        dlg.Show(True)

    def data_lists(self):
        projects = self.etmData.projects.values()
        projects.sort()
        keywords = self.etmData.keywords.keys()
        keywords.sort()
        contexts = self.etmData.contexts.keys()
        contexts.sort()
        zones = self.etmData.zones.keys()
        zones.sort()
        ret = ['<title>etm data outline</title>','<pre>']
        if len(projects) > 0:
            count = 0
            ret.append('<font color="blue"><b>Projects</b></font>')
            ret.append('    <font color="magenta"><b>%-26s %3s %3s %3s %3s %3s %3s    %s</b></font>' % 
                ('title',
                    'a', 'e', 'f', 'n', 'r', 'u', 'relative path'))
            for line in projects:
                count += 1
                if count%2:
                    ret.append('    <font color="darkgreen">%s</font>' % line)
                else:
                    ret.append('    <font color="navy">%s</font>' % line)
        else:
            ret.append('<font color="blue"><b>The list of projects is empty</b></font>')
        ret.append('')
        if len(keywords) > 0:
            count = 0
            ret.append('<font color="blue"><b>Keywords</b></font>')
            ret.append('    <font color="magenta"><b>%-53s %s</b></font>' % 
                ('keyword', '# of uses'))
            for word in keywords:
                count += 1
                if count%2:
                    ret.append('    <font color="darkgreen">%-53s %3s</font>' % (word, self.etmData.keywords[word]))
                else:
                    ret.append('    <font color="navy">%-53s %3s</font>' % (word,  self.etmData.keywords[word]))
        else:
            ret.append('<font color="blue"><b>The list of keywords is empty</b></font>')
        ret.append('')
        if len(contexts) > 0:
            count = 0
            ret.append('<font color="blue"><b>Contexts</b></font>')
            ret.append('    <font color="magenta"><b>%-53s %s</b></font>' % 
                ('context', '# of uses'))
            for context in contexts:
                count += 1
                if count%2:
                    ret.append('    <font color="darkgreen">%-53s %3s</font>' % (context, self.etmData.contexts[context]))
                else:
                    ret.append('    <font color="navy">%-53s %3s</font>' % (context,  self.etmData.contexts[context]))
        else:
            ret.append('<font color="blue"><b>The list of contexts is empty</b></font>')
        ret.append('')
        if len(zones) > 0:
            count = 0
            ret.append('<font color="blue"><b>Time Zones</b></font>')
            ret.append('    <font color="magenta"><b>%-53s %s</b></font>' % 
                ('zone', '# of uses'))
            for z in zones:
                if not self.etmData.zones[z]:
                    continue
                count += 1
                if count%2:
                    ret.append('    <font color="darkgreen">%-53s %3s</font>' % (z, self.etmData.zones[z]))
                else:
                    ret.append('    <font color="navy">%-53s %3s</font>' % (z,  self.etmData.zones[z]))
        else:
            ret.append('<font color="blue"><b>The list of time zones is empty</b></font>')
        ret.append('')
        ret.append("</pre></body>")
        html = "\n".join(ret)
        dlg = ETMhtml(self, size=(700,340), page = html)
        dlg.Show(True)

    def ShowAlert(self, T, msg):
        title = "etm alert at %s" % T
        self.alert = MyAlert(title, msg, size=(540, 120))
        self.sound.Play(wx.SOUND_SYNC)
        self.alert.Show()

    def show_cal(self):
        dlg = ETM12cal()
        dlg.Show(True)

    @trace
    def update_queue(self):
        self.alert_msg = []
        self.alert_times = []
        self.alert_update = time.time()
        queue = self.alert_sched.queue
        self.alert_count = len(self.alert_sched.queue) - 1
        for e in queue:
            prts = e[3]
            alt = ''
            if len(prts) > 2 and prts[2]:
                title = prts[0]
                msg = prts[1]
                cmd = prts[2]
                if len(title) > 33:
                    title = "%s..." % title[:30]
                if len(cmd) > 33:
                    cmd = "%s..." % cmd[:30]
                m = "%s [%s]" % (title, cmd)
            elif len(prts) > 1:
                title = prts[0]
                msg = prts[1]
                if len(title) > 68:
                    title = "%s..." % title[:65]
                m = "%s" % (title)
            else:
                m = " ".join(prts).strip()
            dt = datetime.datetime.fromtimestamp(e[0])
            tfmt = dt.strftime(timefmt)
            if m:
                self.alert_msg.append("%s: %s" % (tfmt, m))
                if use_ampm:
                    t = leadingzero.sub('', tfmt)
                    t = t[:-1]
                    t = t.lower()
                else:
                    t = tfmt
                self.alert_times.append(t)

    @trace 
    def update_alerts(self):
        sec = datetime.datetime.now().second
        self.alert_sched.enter(60-sec, 0, self.update_alerts, ())
        changed = self.etmData.get_modified()
        now = datetime.datetime.now()
        if changed:
            current_month = now.month
            current_year = now.year
            if current_month == 1:
                start = parse("%s-%s-01" % (current_year -1, 12))
                stop = parse("%s-%s-01" % (current_year, 4)) - oneday
            elif current_month <= 9:
                start = parse("%s-%s-01" % (current_year, current_month-1))
                stop = parse("%s-%s-01" % (current_year, current_month+3)) - oneday
            else: # 10, 11, 12
                start = parse("%s-%s-01" % (current_year, current_month-1))
                stop = parse("%s-%s-01" % (current_year+1, current_month-9)) - oneday
            self.etmData.select_items(start, stop)
            if self.etmData.errors:
                self.show_errors(self.etmData.errors)
                self.etmData.errors = []
            self.etmData.get_alerts()
            wx.CallAfter(self.showDay, force=True)

        self.clear_alerts()
        self.load_alerts()
        self.update_queue()
        if self.timer_status == 'running':
            # seconds to add since last update
            self.timer_delta += (now - self.last_time)
            self.last_time = now 
            self.timer_minutes = self.timer_delta.seconds/60
            if self.timer_delta.seconds%60 >= 30:
                self.timer_minutes += 1
            if (action_interval and self.timer_minutes  
                and self.timer_minutes % int(action_interval) == 0):
                self.timer_play = True
        wx.CallAfter(self.refreshprompt)
        return(True)

    def clear_alerts(self):
        if not self.alert_sched.empty():
            for a in self.alert_ids:
                try:
                    self.alert_sched.cancel(a)
                except:
                    pass

    @trace
    def load_alerts(self):
        self.alert_ids = []
        # self.data.alerts is a list of tuples: (title, starttime, minutes)
        # where minutes is a list of integer minutes before
        # starttime at which alerts should be given
        for event in self.etmData.alerts:
            (title, starttime, alerts, altcmd) = event
            n = parse(starttime)
            now = datetime.datetime.now()
            for alert in alerts:
                t = n - alert * oneminute
                if t < now:
                    continue
                elif altcmd:
                    msg = ""
                else:
                    if alert > 1:
                        msg = "%s %s" % (alert, minutes)
                    elif alert == 1:
                        msg = "1 %s" % (minute)
                    else:
                        msg = "%s" % (rightnow)
                at = time.mktime(t.timetuple())
                id = self.alert_sched.enterabs(at, 1, self.onAlert, 
                        (title, msg, altcmd))
                self.alert_ids.append(id)

    def onAlert(self, title, msg, altcmd):
        wx.CallAfter(self.update_queue)
        wx.CallAfter(self.refreshprompt)
        T = time.strftime(timefmt, time.localtime(time.time()))
        if use_ampm:
            T = leadingzero.sub('', T)
        t = "%s %s" % (thetimeis, T)
        if altcmd:
            rephash = {'t' : t, 'T' : T, 'm' : title}
            cmd = altcmd % rephash
            os.system(cmd)
        elif alertcmd:
            rephash = {'t' : t, 'T' : T, 'm' : "%s %s" % (title, msg)}
            cmd = alertcmd % rephash
            os.system(cmd)
        else:
            wx.CallAfter(self.ShowAlert, T, "%s %s" % (title, msg))

    def Today(self, event=None):
        self.selday = etmData.midnight()
        if self.selday != self.etmData.today:
            self.etmData.today = self.selday
            self.refreshprompt()
        self.etmData.options['begin'] = self.selday
        self.cal.SetDate(wx.DateTime_Now())
        self.showDay()
        self.cal.SetFocus()

    def OnCalSelected(self, event):
        self.cal.SetFocus()
        y,m,d = map(int,event.GetDate().Format("%Y %m %d").split())
        self.selday = datetime.datetime(y,m,d,0,0,0)
        self.etmData.options['begin'] = self.selday
        self.showDay()
        self.refreshprompt()

    def showDay(self, force=True):
        if has(self.etmData.options, 'begin'):
            self.selday = \
            datetime.datetime.combine(self.etmData.options['begin'], datetime.time())
        else:
            self.etmData.options['begin'] = self.selday
        self.search.SetValue('')
        self.index = -1
        self.details.Clear()
        self.month = datetime.datetime(self.selday.year, self.selday.month,
            1, 0, 0, 0)
        if force or not self.lastmonth or self.month != self.lastmonth:
            self.showMonth()
            self.lastmonth = self.month
            self.lastday = self.selday
            self.itemlist.DeleteAllItems()
            h, self.show_lst = self.etmData.show(view = 'm')
            self.busy.SetEvents(self.etmData.sel_events)
            self.id2index = {}
            self.index2id = {}
            if len(self.show_lst) > 0:
                for i in range(len(self.show_lst)):
                    first, second, third, id, weekday, attr = self.show_lst[i]
                    # first, second, third, id, attr = self.show_lst[i]
                    columns = [first, second, third]
                    index = self.itemlist.InsertStringItem(i, id)
                    self.id2index[(id, weekday)] = index
                    self.index2id[index] = id
                    for j in range(len(columns)):
                        self.itemlist.SetStringItem(index, j,
                            " %s" % columns[j])
                    try: 
                        attr_color = wxcolors[attr]
                    except:
                        print 'except color_attr', attr
                    item = self.itemlist.GetItem(index)
                    item.SetTextColour(attr_color)
                    self.itemlist.SetItem(item) 
            stopday = (self.selday + 6*oneday)
            label = "%s ~ %s" % (
                self.selday.strftime(datebar1),
                stopday.strftime(datebar2))
            lab = leadingzero.sub('',label)
            self.datebar.SetLabel(lab)
        self.details.Enable(True)
        self.details.Clear()
        try:
            self.details.WriteText("")
        except:
            pass

        if self.selday.strftime("%Y-%m-%d") == \
            datetime.date.today().strftime("%Y-%m-%d"):
            self.datebar.SetForegroundColour(todaycolor)
        else:
            self.datebar.SetForegroundColour("BLACK")

    def showMonth(self):
        self.etmData.get_daycolors(self.selday)
        attrs= {}
        daynum = self.selday.day
        for day, color in self.etmData.day2color.items():
            attrs[day] = wx.calendar.CalendarDateAttr()
            if day in range(daynum+1,daynum+7):
                attrs[day].SetBackgroundColour(bgweek)
                attrs[day].SetTextColour(fgday)
            else:
                attrs[day].SetTextColour(color)
                attrs[day].SetBackgroundColour(bgcolor)
            self.cal.ResetAttr(day)
            self.cal.SetAttr(day, attrs[day])
        self.cal.Refresh()

    def show_errors(self, msg):
        s = "\n".join(msg)
        page = "<pre>%s</pre>" % s
        dlg = ETMhtml(self, size = (600,340), page = page)
        dlg.Show()

    def getSelection(self, prompt, choices, dflt=None):
        dlg = wx.SingleChoiceDialog(self, prompt, 'etm',
                choices, wx.CHOICEDLG_STYLE)
        if dflt:
            dlg.SetSelection(dflt)
        if dlg.ShowModal() == wx.ID_OK:
            return dlg.GetStringSelection()
        else:
            return ""

    def str2clip(self, str):
        self.do = wx.TextDataObject()
        self.do.SetText(str)
        if wx.TheClipboard.Open():
            wx.TheClipboard.SetData(self.do)
            wx.TheClipboard.Close()

    def OnChar(self, event):
        keycode = event.GetKeyCode()
        shift = event.ShiftDown()
        if keycode == 32:  # space
            # print "etmWX OnChar: space"
            self.sel_id = None
            self.Today()
            self.refreshprompt()
        elif keycode == 27:               # Escape
            self.sel_id = None
            self.showDay()
            self.cal.SetFocus()
        elif keycode == 6:          # ^F
            self.search.SetFocus()
        elif keycode == ord(',') or (shift and keycode == wx.WXK_LEFT):
            self.UpInList(1)
        elif shift and keycode in [ord('<'), wx.WXK_UP]:
            self.UpInList(5)
        elif keycode == ord('.') or (shift and keycode == wx.WXK_RIGHT):
            self.DownInList(1)
        elif shift and keycode in [ord('>'), wx.WXK_DOWN]:
            self.DownInList(5)
        elif keycode == ord('0'): # zero
            self.etmData.options.clear()
            self.details.Clear()
            self.showDay()
            self.refreshprompt()
        elif keycode in range(49,58): # 1, ..., 9
            if main_history:
                indx = min(len(main_history)-1, keycode - 49)
                entry = main_history[indx]
                hash = {}
                res = historyleader.sub('', entry)
                hash, msg, s = parse_opts("%s %s" % ('m', res))
                self.etmData.options = hash
                self.details.Clear()
                self.showDay()
                self.refreshprompt()
        elif keycode in [wx.WXK_LEFT, wx.WXK_UP, wx.WXK_RIGHT, wx.WXK_DOWN]:
            self.details.Clear()
            self.cal.SetFocus()
            event.Skip()
        elif keycode in [wx.WXK_BACK, wx.WXK_DELETE] and self.sel_id:
            self.itemDelete(self.sel_id)
        elif keycode == 17:           # Ctrl-Q quit
            self.OnQuit()
        elif keycode == 18:           # Ctrl-R edit rc
            self.openRC() 
        elif keycode == 21:           # Ctrl-U update data files
            etmData.cleanPkls()
            self.etmData = etmData.ETMData()
            if self.etmData.errors:
                self.show_errors(self.etmData.errors)
                self.etmData.errors = []
            self.Today()
        elif keycode == ord('c') and self.sel_id:
            self.OnItemCloned(self.sel_id)
        elif keycode == ord('f') and self.sel_id:
            item = self.etmData.id2item[self.sel_id]
            if item['type'] == 'task' and not has(item, 'f'):
                self.itemFinish(self.sel_id)
            else:
                event.Skip()
        elif keycode == ord('g') and self.sel_id:
            self.SelectOpen(self.sel_id)
        elif keycode in [ord('m'), ord('d'), ord('i'), ord('b'), ord('l')]:
            response = self.getOpts(size=(-1, -1), value = chr(keycode), check = True) 
        elif keycode == ord('M') and self.sel_id:
            self.itemMove(self.sel_id)
        elif keycode == ord('u') and self.sel_id:
            item = self.etmData.id2item[self.sel_id]
            if item['type'] == 'task' and has(item, 'f'):
                self.itemUnfinish(self.sel_id)
            else:
                event.Skip()
        elif keycode == ord('a'):
            self.TimerToggle()
        elif keycode == ord('A'):
            self.TimerStop()
        elif keycode in [ord('e'), ord('t'), ord('n'), ord('r')]:
            self.newItem(chr(keycode))
        elif keycode == ord('P'):
            self.newItem('p')
        elif keycode == ord('p'):
            self.openProject()
        elif keycode == ord('o'):
            self.data_lists()
        elif keycode == ord('q'):
            self.alert_status()
        elif keycode == wx.WXK_F1:  # F1 Show Help
            self.OnHelp(event)
        elif keycode == wx.WXK_F2:  # F2 Show About box
            self.OnAbout(event)
        elif keycode == wx.WXK_F3:  # F3 Show latest version
            dlg = MyAlert('etm version data', newer(),
                size=(540, 120))
            dlg.Show()
        elif keycode == wx.WXK_F4:  # F2 Show 12month calendar
            self.show_cal()
        elif keycode == wx.WXK_F5:
            dlg = wx.TextEntryDialog(
                self,
                "Enter an expression of the form 'date (+|-) string'\nwhere string is either a date or an integer followed by 'days',\ne.g., 'dec 1 + 90 days' or 'nov 30 - sep 1'.\nThe result will be copied to the system clipboard.",
                    'etm date calculator', '')
            if dlg.ShowModal() == wx.ID_OK:
                s = dlg.GetValue()
                if s:
                    msg, res = date_calculator(str(s))
                    dlg = wx.MessageDialog(None, msg, 
                        'etm date calculator',
                        wx.OK)
                    if res:
                        self.str2clip(res)
                    dlg.ShowModal()
                    dlg.Destroy()
        elif keycode == wx.WXK_F6:
            ok, l = getWeather()
            if ok:
                txt = "\n".join(l)
            else:
                txt = l
            dlg = MyAlert('Yahoo local weather data', txt, size=(540, 260))
            dlg.Show()
        elif keycode == wx.WXK_F7:
            txt = "\n".join(getSunMoon())
            dlg = MyAlert('USNO local sun and moon data', txt, size=(540, 500))
            dlg.Show()
        else:
            event.Skip()

    def Refresh(self, event=None):
        self.showDay(force=True)
        self.refreshprompt()
        self.statusbar.Refresh()
        self.details.WriteText("")

    def OnAbout(self, event):
                info = wx.AboutDialogInfo()
                info.Name = "etm"
                info.Version = "%s" % version
                info.Copyright = "(C) %s Daniel A. Graham" % copyright
                info.Description = description
                info.WebSite = ("http://www.duke.edu/~dgraham/ETM", 
                        "etm home page")
                info.Developers = [ "Daniel A. Graham <daniel.graham@duke.edu>", ]

                info.License = license
                wx.AboutBox(info)


    def OnHelp(self, event):
        dlg = ETMhtml(self, size=(600,340), page = help_text)
        dlg.Show(True)

    def OnIconify(self, event):
        if "wxMSW" in wx.PlatformInfo:
            self.Hide()

    def OnQuit(self, evnt=None):
        self.timer_active = False
        self.stop_alerts()
        self.tbicon.Destroy()
        self.Destroy()

class App(wx.App):

    def OnInit(self):
        status = 0
        try:
            wx.lib.colourdb.updateColourDB()
            status = 1
            self.frame = MyFrame()
            status = 2
            self.frame.Show(True)
            status = 3
            self.SetTopWindow(self.frame)
            status = 4
            return True
        except:
            print "failed OnInit with status: ", status
            print sys.exc_info()
            return False

def main():
    app = App(redirect=False)
    app.MainLoop()

if __name__ == '__main__':
    main()
