#!/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.
"""
#  show_keys = True
show_keys = False
import wx
import wx.html as html

from etmParsers import *
import etmData
from etmHTML import ETMhtml, ETMnb
from etmInfo import getWeather
from etmInfo import ETM12cal
from etmInfo import getSunMoon

from math import floor, ceil

import wx.stc as stc

import wx.lib.colourdb
import wx.lib.mixins.treemixin

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'

# for the edge normal color
NORMAL = '#E5E5E5'
# for the edge modified color
ACTIVE = '#33FF00'
MODIFIED = '#FF6103'
# for search highlighting
SEARCHF = '#000000'
SEARCHB = '#FFE303'
sash_pos = 4

bgweek = bgday = wx.Colour(selectcolor[0], selectcolor[1],
        selectcolor[2], selectcolor[3])

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

# for html colors
hexcolors = {}
for k, v in colors.items():
    rpart,gpart,bpart = v
    hexcolors[k] = "#%02X%02X%02X" % (rpart,gpart,bpart)

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

wx.SetDefaultPyEncoding(gui_encoding)

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

class MySTC(stc.StyledTextCtrl):
    def __init__(self, parent, ID):
        stc.StyledTextCtrl.__init__(self, parent, ID, size=(-1,-1),
                style=border)

        self.Bind(stc.EVT_STC_MODIFIED, self.OnModified)

        self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
        self.SetEditable(False)
        self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "back:%s" % NORMAL)
        self.SetUseHorizontalScrollBar(False)
        #  self.SetUseVerticalScrollBar(True)
        self.SetUseVerticalScrollBar(False)
        self.SetWrapMode(True)
        self.SetEOLMode(2) # Use LF (\n) for end of lines
        # style 1 is for the search highlight
        self.StyleSetSpec(1, "fore:%s,back:%s" % (SEARCHF, SEARCHB))
        self.SetCaretWidth(2)
        self.SetMarginWidth(0,0)
        self.SetMarginWidth(1,3)
        self.SetMarginType(1, stc.STC_MARGIN_SYMBOL)
        self.SetMarginMask(1, 0)
        self.SetMarginType(2, stc.STC_MASK_FOLDERS)
        self.SetMarginWidth(2, 0)
        self.SetMargins(3,1)

        self.MarkerDefine(stc.STC_MARKNUM_FOLDER, stc.STC_MARK_BOXPLUS,
                "white", "black")

    def OnDestroy(self, evt):
        wx.TheClipboard.Flush()
        evt.Skip()

    def ChangeValue(self, txt):
        self.SetReadOnly(False)
        self.SetText(txt)
        self.SetReadOnly(True)

    def GetValue(self):
        return self.GetText()

    def SetEditable(self, bool):
        self.editable = bool
        #  print "MySTC editable", self.editable
        if bool:
            self.SetReadOnly(False)
            self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "back:%s" % ACTIVE)
        else:
            self.SetReadOnly(True)
            self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "back:%s" % NORMAL)

    def IsEditable(self):
        return self.editable

    def IsModified(self):
        return self.CanUndo()

    def SetStyle(self, b, e, ignore):
        self.StartStyling(b, 0xff)
        self.SetStyling(e-b, 1)

    def OnModified(self, evt):
        if self.IsEditable():
            if self.CanUndo():
                self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "back:%s"
                        % MODIFIED)
            else:
                self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "back:%s"
                        % ACTIVE)
        else:
            self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "back:%s"
                    % NORMAL)

    def ClearUndo(self):
        self.EmptyUndoBuffer()
        self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "back:%s"
                % ACTIVE)


    def getModType(self, evt):
        modType = evt.GetModificationType()
        st = ""
        table = [(stc.STC_MOD_INSERTTEXT, "InsertText"),
                 (stc.STC_MOD_DELETETEXT, "DeleteText"),
                 (stc.STC_MOD_CHANGESTYLE, "ChangeStyle"),
                 (stc.STC_MOD_CHANGEFOLD, "ChangeFold"),
                 (stc.STC_PERFORMED_USER, "UserFlag"),
                 (stc.STC_PERFORMED_UNDO, "Undo"),
                 (stc.STC_PERFORMED_REDO, "Redo"),
                 (stc.STC_LASTSTEPINUNDOREDO, "Last-Undo/Redo"),
                 (stc.STC_MOD_CHANGEMARKER, "ChangeMarker"),
                 (stc.STC_MOD_BEFOREINSERT, "B4-Insert"),
                 (stc.STC_MOD_BEFOREDELETE, "B4-Delete")
                 ]
        for flag,text in table:
            if flag & modType:
                st = st + text + " "
        if not st:
            st = 'UNKNOWN'
        return st

    def AcceptsFocus(self, *args, **kwargs):
        return self.editable

#  class MyTree(wx.TreeCtrl):
class MyTree(wx.lib.mixins.treemixin.ExpansionState, wx.TreeCtrl):
    '''
        Each child is a tuple
            0: header str
            1: detail str
            2: summary_col
            3: groupby_cols (a list of tuples)
            4: starting col
            5: list of children DATA tuples
    '''
    def __init__(self, parent, id, itemdata, prefix=16):
        wx.TreeCtrl.__init__(self, parent, id, wx.DefaultPosition, (500,-1),
                wx.TR_HIDE_ROOT | wx.TR_HAS_BUTTONS | border)
        self.root = None
        self.prefix = prefix
        self.root_items = []
        self.id2index = {}
        self.chld_lst = []
        self.expanded = []
        self.skipselection = False
        self.output = None
        self.selected = None
        self.selected_id = None
        self.active_id = None
        self.expandall = True
        self.show_leaves = True
        self.totals_first = False

        self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.OnItemExpanding,
                id=self.GetId())
        self.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self.OnItemCollapsed,
                id=self.GetId())
        self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelChanged,
                id=self.GetId())
        wx.CallAfter(self.doInit, itemdata)

    def doInit(self, itemdata):
        try:
            group = itemdata[2][0]
            if type(group) == list:
                group = tuple(group)
        except:
            group = None
        self.SetTree(itemdata)

    def SetPrefix(self, prefix):
        self.prefix = prefix

    def SetTree(self, itemdata):
        lst = etmData.expand_child(itemdata, self.prefix, self.totals_first,
                self.show_leaves)
        self.DeleteAllItems()
        self.root = self.AddRoot('root', -1, -1, wx.TreeItemData(itemdata))
        self.root_items = []
        self.id2index = {}
        self.chld_lst = []
        self.expanded = []
        self.selected = []
        self.childtuples = []
        self.skipselection = False
        if lst:
            for key in lst:
                name = key[0]
                new_item = self.AppendItem( self.root, name, -1, -1,
                                            wx.TreeItemData(key) )
                self.SetItemTextColour(new_item, wxcolors[11])
                self.root_items.append(new_item)
                if etmData.has_children(key, self.show_leaves):
                    self.SetItemHasChildren(new_item, True)
        if self.expandall:
            self.ExpandAll()

    def SetOutput(self, output):
        """
        Set output function (accepts single string). Used to display string
        representation of the selected object by OnSelChanged.
        """
        self.output = output

    def CollapseAll(self):
        for item in self.root_items:
            self.CollapseAllChildren(item)
        self.expandall = False

    def ExpandAll(self):
        for item in self.root_items:
            self.ExpandAllChildren(item)
        self.expandall = True

    def OnItemExpanding(self, event):
        item = event.GetItem()

        if self.IsExpanded(item):
            # This event can happen twice in the self.Expand call
            return

        obj = self.GetPyData( item )
        level = 10 + min(int(obj[4]), 3)
        lst = etmData.expand_child(obj, self.prefix,
                self.totals_first, self.show_leaves)

        for key in lst:
            new_obj = key
            name = key[0]
            det = key[1]

            new_item = self.AppendItem( item, name, -1, -1,
                                        wx.TreeItemData(new_obj) )
            num_left = etmData.has_children(new_obj, self.show_leaves)
            if num_left >= 1 or (num_left and self.prefix >= 0):
                self.SetItemHasChildren(new_item, True)
                self.SetItemTextColour(new_item, wxcolors[level])
            else:
                self.id2index[det] = new_item
                if len(key) > 6:
                    self.SetItemTextColour( new_item, wxcolors[key[6]] )
                else:
                    self.SetItemTextColour( new_item, wxcolors[11] )

    def OnItemCollapsed(self, event):
        """
        We need to remove all children here, otherwise we'll see all
        that old rubbish again after the next expansion.
        """
        item = event.GetItem()
        self.DeleteChildren(item)

    def OnSelChanged(self, event):
        """
        If an output function is defined, we call it to display some
        informative stuff in the entry bar.
        """
        item = event.GetItem()
        if not item:
            self.selected_id = None
            return()
        if self.skipselection:
            self.skipselection = False
        else:
            self.selected = item
        if not self.output :
            return()
        obj = self.GetPyData( item )
        try:
            self.selected_id = obj[1][0]
        except:
            return()
        if type(obj) == list:
            apply(self.output, (obj[1],))

    def unselect(self, event):
        self.skipselection = True
        self.UnselectAll()
        self.selected = None

    #  def SaveExpansionState(self):
        #  self.expanded = self.GetExpansionState()

    #  def RestoreExpansionState(self):
        #  self.SetExpansionState(self.expanded)

    def getChildren(self, treeItem, indent=0):
        if indent > 0:
            self.chld_lst.append("    "*(indent-1) + self.GetItemText(treeItem))
        subItem = self.GetFirstChild(treeItem)[0]
        while subItem.IsOk():
            self.getChildren(subItem, indent+1)
            subItem = self.GetNextSibling(subItem)

    def getChildTuples(self, treeItem, indent=0):
        if indent > 0:
            obj = self.GetPyData(treeItem)
            if len(obj) > 7: # this is a leaf
                self.childtuples.append(obj[7])
        subItem = self.GetFirstChild(treeItem)[0]
        while subItem.IsOk():
            self.getChildTuples(subItem, indent+1)
            subItem = self.GetNextSibling(subItem)

    def yankChildTuples(self, treeItem, indent=0, export=False):
        self.getChildTuples(treeItem, indent)
        tups = [x for x in self.childtuples if x]
        self.childtuples = []
        if export:
            r, s = etmData.make_vcal('export.ics', tups, {})
        else:
            r, s = etmData.make_vcal('', tups, {})
            self.do = wx.TextDataObject()
            self.do.SetText(s)
            if wx.TheClipboard.Open():
                wx.TheClipboard.SetData(self.do)
                wx.TheClipboard.Close()

    def yankChildren(self, treeItem, indent=0, export=False):
        self.getChildren(treeItem, indent)
        s = "\n".join(self.chld_lst)
        if export:
            # FIXME
            print s
        else:
            self.chld_lst = []
            self.do = wx.TextDataObject()
            self.do.SetText(s)
            if wx.TheClipboard.Open():
                wx.TheClipboard.SetData(self.do)
                wx.TheClipboard.Close()

    def returnChildren(self, treeItem, indent=0):
        self.chld_lst = ['<pre>']
        self.getChildren(treeItem, indent)
        self.chld_lst.append('</pre>')
        s = "\n".join(self.chld_lst)
        self.chld_lst = []
        return(s)

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 OnClose(self, event):
        self.Destroy()

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(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(busyfontsize, 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)
        sfont = wx.Font(basefontsize + statusfontadj, fontfam,
                wx.NORMAL, wx.NORMAL)
        dfont = wx.Font(basefontsize, wx.FONTFAMILY_DEFAULT,
                wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
        tfont = wx.Font(basefontsize, wx.FONTFAMILY_DEFAULT,
                wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
        mfont = wx.Font(basefontsize, wx.FONTFAMILY_MODERN,
                wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
        self.efont = mfont
        wx.Frame.__init__(self, None, -1, 'etm')
        wx.lib.colourdb.updateColourDB()
        #  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.Bind(wx.EVT_CLOSE, self.OnQuit)
        self.Bind(wx.EVT_ICONIZE, self.OnIconify)
        splitter = wx.SplitterWindow(panel, -1, style=wx.SP_3DSASH)
        splitter.SetSashSize(8)
        splitter.SetMinimumPaneSize(2*basefontsize)
        topPanel = wx.Panel(splitter, -1)
        topBox = wx.BoxSizer(wx.VERTICAL)

        self.itemTree = MyTree(topPanel, 1, [], prefix=16)
        self.itemTree_id = self.itemTree.GetId()
        self.itemTree.Bind(wx.EVT_CHAR, self.OnTreeChar)
        self.itemTree.Bind(wx.EVT_LEFT_DCLICK, self.OnSelActivated)
        self.itemTree.SetFont(mfont)
        self.itemTree.ExpandAll()
        topBox.Add(self.itemTree, -1, wx.EXPAND)
        topPanel.SetSizer(topBox)

        self.dayColors = {}
        self.ltz = self.ltn = ''
        self.set_ltz()

        tdy_pat = ' '*32

        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,bgweek)
        self.Bind(wx.calendar.EVT_CALENDAR_SEL_CHANGED, self.OnCalSelected,
                id=self.cal.GetId())
        self.cal.Bind(wx.EVT_CHAR, self.OnCommonChar)
        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.OnSelActivated)
        self.busy.Bind(wx.EVT_CHAR, self.OnCommonChar)
        self.busy.SetBackgroundColour(bgcolor)

        self.datebar = wx.StaticText(panel, -1,
            "loading data ...",
            size = wx.DefaultSize,
            style = wx.BORDER_NONE
            | wx.ALIGN_CENTER_VERTICAL
            )
        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.index = -1
        bottomPanel = wx.Panel(splitter, -1)
        bottomBox = wx.BoxSizer(wx.VERTICAL)
        self.details = MySTC(bottomPanel, -1)
        self.details.Bind(wx.EVT_CHAR, self.OnEntryChar)
        self.details.Bind(wx.EVT_LEFT_DCLICK, self.OnSelActivated)
        self.details.Bind(wx.EVT_KEY_DOWN, self.OnEntryChar)
        self.details.StyleSetFont(stc.STC_STYLE_DEFAULT, mfont)
        bottomBox.Add(self.details, -1, wx.EXPAND | wx.BORDER_NONE)
        bottomPanel.SetSizer(bottomBox)
        splitter.SetSashGravity(1)
        splitter.SplitHorizontally(topPanel, bottomPanel,
                -sash_pos*(basefontsize+8))


        self.details.SetBackgroundColour(bgcolor)
        self.itemTree.SetOutput(self.SetDetails)

        # status 0: help
        self.sbar_help = wx.StaticText(panel, -1, F1Help,
            size = (-1, -1),
            style = wx.BORDER_NONE
            | wx.ALIGN_LEFT
            | wx.ALIGN_CENTER_VERTICAL
            )
        self.sbar_help.SetFont(sfont)

        self.sbar_alrt = wx.StaticText(panel, -1,
            '',
            size = (-1, -1),
            style = wx.BORDER_NONE
            | wx.ALIGN_LEFT
            | wx.ALIGN_CENTER_VERTICAL
            )
        self.sbar_alrt.SetFont(sfont)

        self.sbar_opts = wx.StaticText(panel, -1,
            '',
            size = (-1,-1),
            style = wx.BORDER_NONE
            | wx.ALIGN_LEFT
            | wx.ALIGN_CENTER_VERTICAL
            )
        self.sbar_opts.SetFont(sfont)

        self.sbar_info = wx.StaticText(panel, -1,
            '',
            size = (-1,-1),
            style = wx.BORDER_NONE
            | wx.ALIGN_LEFT
            | wx.ALIGN_CENTER_VERTICAL
            )
        self.sbar_info.SetFont(sfont)

        self.options = {}
        self.options_saved = {}
        self.o_lines = ''
        self.b_lines = ''
        self.j_lines = ''
        self.alert_count = 0
        self.alert_ids = []
        self.alert_msg = []
        self.alert_queue = []
        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.timer_hash = {}
        self.last_time = None
        self.timer_delta = None
        self.sel_id = None
        # for the completion lists
        self.contexts = []
        self.keywords = []
        self.locations = []
        # for the history lists
        self.item_lst = []

        self.options_lst = []

        self.clip = None
        self.helpFrame = None
        self.content = ''
        self.current_msg = ''
        self.sound_alert = wx.Sound(etm_alert)
        self.neg_fields = {}
        self.regex_fields = {}

        # mode: replace, create, view, busy
        # type: action, event, note, task, project
        # determines how to process entry
        self.typeMode = None
        self.entryMode = None

        self.infoTimer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.OnInfoTimer, self.infoTimer)
        self.infoTimerRunning = False
        self.infoMessage('')

        # 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
            , 54)
        hbox_show_short.Add(self.datebar, 3,
            wx.ALIGN_CENTER_VERTICAL
            | wx.LEFT
            , 20)
        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(splitter, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 4)
        hbox1 = wx.BoxSizer(wx.HORIZONTAL)
        hbox1.Add(vbox1, 0, wx.EXPAND)
        hbox1.Add(vbox2, 2, wx.EXPAND)
        hbox2 = wx.BoxSizer(wx.HORIZONTAL)
        vbox3 = wx.BoxSizer(wx.VERTICAL)
        sbar_box = wx.BoxSizer(wx.HORIZONTAL)
        sbar_box.Add((4, -1), 0, wx.EXPAND)
        sbar_box.Add(self.sbar_help, 0, wx.EXPAND)
        sbar_box.Add((24, -1), 0, wx.EXPAND)
        sbar_box.Add(self.sbar_alrt, 1, wx.EXPAND)
        sbar_box.Add((24, -1), 0, wx.EXPAND)
        sbar_box.Add(self.sbar_opts, 2, wx.EXPAND)
        sbar_box.Add((24, -1), 0, wx.EXPAND)
        sbar_box.Add(self.sbar_info, 3, wx.EXPAND)
        vbox3.Add(sbar_box, 0, wx.EXPAND | wx.ALL, 4)

        hbox2.Add(vbox3, -1, wx.EXPAND)
        vbox.Add(hbox1, -1, wx.EXPAND)
        vbox.Add(hbox2, 0, wx.EXPAND)
        panel.SetAutoLayout(True)
        self.now = datetime.datetime.now(tzlocal())
        self.today = (self.now.year, self.now.month, self.now.day)
        self.selday = (self.now.year, self.now.month, self.now.day)
        self.Layout()
        panel.SetSizerAndFit(vbox)
        self.Fit()
        self.CenterOnScreen()
        self.SetMinSize(self.GetSize())
        splitter.SetSashPosition(- sash_pos * (basefontsize+8))
        self.commonPrefix = ''  # this plus the relative path is the full path
        self.alltups = []
        self.alerts = []
        self.busytimes = {}
        #  self.alltimes = {}
        #  self.matchingtimes = {}
        self.currentHash = {}
        self.lastModified = []
        self.startTimer()   # put here instead of doInit to avoid multiple timers
        wx.CallAfter(self.doInit)


    def doInit(self):
        self.selday = (self.now.year, self.now.month, self.now.day)
        self.soondate = tuple(map(int, (self.now + soon * oneday).strftime(
            "%Y,%m,%d").split(',')))
        self.reloadData()
        self.Today()

    def reloadData(self):
        self.now = datetime.datetime.now(tzlocal())
        self.today = (self.now.year, self.now.month, self.now.day)
        # year_beg and year_end from the etm rc file
        year = int(self.now.year)
        beg_year = year - year_beg
        end_year = year + year_end
        data_beg = (beg_year, 1, 1)
        data_end = (end_year, 12, 31)

        res = etmData.getTuples(data_beg, data_end)
        self.commonPrefix   = res[0]
        self.alltups        = res[1]
        self.alerts         = res[2]
        self.id2hash        = res[3]
        self.lastModified   = res[4]
        self.currentHash    = res[5]
        self.contexts       = res[6]
        self.keywords       = res[7]
        self.locations      = res[8]
        self.msgs           = res[9]
        # tups, options, busy = etmData.opts2Tuples(self.alltups, {})
        tups, options, busy = etmData.opts2Tuples(self.alltups,
            self.options_saved)
        self.busytimes = busy
        #  self.busytimes = deepcopy(self.alltimes)
        self.item_templates = [normalize_whitespace(x) for x
                in item_templates] # from etmrc
        self.display_templates = [normalize_whitespace(x) for x
                in display_templates] # from etmrc
        self.item_templates.extend(
                ["@c %s" % normalize_whitespace(x) for x in self.contexts])
        self.display_templates.extend(
                ["-c %s" % normalize_whitespace(x) for x in self.contexts])
        self.item_templates.extend(
                ["@k %s" % normalize_whitespace(x) for x in self.keywords])
        self.display_templates.extend(
                ["-k %s" %normalize_whitespace(x) for x in self.keywords])
        self.item_templates.extend(
                ["@l %s" % normalize_whitespace(x) for x in self.locations])
        self.display_templates.extend(
                ["-l %s" % normalize_whitespace(x) for x in self.locations])
        self.item_templates.extend(
                ["@z %s" % normalize_whitespace(x) for x in zonelist])
        self.dayColors = etmData.data2DayColors(data_beg, data_end,
                self.busytimes)
        if self.msgs:
            self.msgs.insert(0,
                    '<font color="red" size="+1">Errors loading data:</font>')
            self.show_errors(self.msgs)
        self.refreshAlerts()

    def SetDetails(self, tup):
        if self.details.IsModified():
            return()
        id = tup[0]
        if id in self.id2hash:
            det = "%s\n[%s]" % (self.id2hash[id][u'DT'], id)
            self.details.ChangeValue(det)
            if 'search' in self.regex_fields and \
                not self.neg_fields['search']:
                for m in self.regex_fields['search'].finditer(det):
                    b, e = m.span()
                    self.details.SetStyle(b,e, wx.TextAttr(wx.RED))
        else:
            self.details.ChangeValue('')

    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 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 ShowHtml(self, page):
        dlg = ETMhtml(self, size=(600,340), page=page)
        dlg.Show(True)

    def openProject(self):
        if not (editor and editcmd):
            page = """To use this command both 'editor' and 'editcmd' must be
            set in '%s'""" % etmrc
            dlg = wx.MessageDialog(None, page, 'etm',
                    wx.OK)
            return(dlg.ShowModal())
        common_prefix, filetuples = etmData.getFiles()
        relflst = [x[1] for x in filetuples]
        dflt = None
        if self.itemTree.selected_id:
            rel_path, num_beg, num_end = self.itemTree.selected_id.split(':')
            dflt = relflst.index(rel_path)
        f = self.getSelection("The project file to open", relflst, dflt)
        if f:
            fp = os.path.join(common_prefix, f)
            last_modified = os.path.getmtime(fp)
            command = editcmd % {'e': editor, 'n': 1, 'f': fp}
            etmData.backup(fp)
            os.system(command)
            #  wx.CallAfter(os.system, command)
            if os.path.getmtime(fp) != last_modified:
                self.reloadData()
                self.infoMessage('reloaded data', 4)
                self.Today()

    def openRC(self):
        if not (editor and editcmd):
            page = """To use this command both 'editor' and 'editcmd' must be
            set in '%s'""" % etmrc
            dlg = wx.MessageDialog(None, page, 'etm',
                    wx.OK)
            return(dlg.ShowModal())
        last_modified = os.path.getmtime(etmrc)
        command = editcmd % {'e': editor, 'n': 1, 'f': etmrc}
        os.system(command)
        if os.path.getmtime(etmrc) != last_modified:
            dlg = wx.MessageDialog(None,
                """etm must be restarted for changes in '%s'
                to take effect.""" % etmrc, 'etm', wx.OK)
            return(dlg.ShowModal())

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

    def OnSearchCancel(self, evnt):
        self.search.SetValue('')
        if 'search' in self.options:
            del self.options['search']
        self.showDay()
        self.itemTree.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):
        self.options_saved = deepcopy(self.options)
        # print("DoSearch 1", self.options_saved)
        self.options = {'search': search_str}
        self.showOps()
        matching, self.neg_fields, self.regex_fields, busyTimes = etmData.getMatchingTuples(
                self.alltups, self.options)
        if matching:
            match_data = ['', '', 5, ((0,1,2),), 0, matching]
            self.busytimes = busyTimes
            self.itemTree.SetTree(match_data)
            label = "matching '%s'" % search_str.strip()
            self.datebar.SetLabel(label)
            #  self.busytimes = deepcopy(self.matchingtimes)
        else:
            self.infoMessage('no matches for "%s" were found' % search_str, 4)
        self.options = deepcopy(self.options_saved)
        # print("DoSearch 2", self.options_saved)

    def infoMessage(self, msg, seconds=0):
        if seconds > 0 and not self.infoTimerRunning:
            self.current_msg = self.sbar_info.GetLabel()
            self.infoTimerRunning = True
            self.infoTimer.Start(seconds*1000, oneShot=True)
        self.sbar_info.SetLabel(msg)

    def OnInfoTimer(self, event):
        self.sbar_info.SetLabel(self.current_msg)
        self.infoTimerRunning = False

    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, mode, action='append'):
        common_prefix, filetuples = etmData.getFiles()
        relflst = [x[1] for x in filetuples]
        dflt = None
        if mode in rotate:
            defaultFile, defaultDir = rotate[mode]
            curfull = self.currentHash[mode]
            currel = etmData.relpath(curfull, common_prefix)
            dflt = relflst.index(currel)

        if action == 'append':
            f = self.getSelection("The project file for the new %s" %
                    mode, relflst, dflt)
        elif action == 'open':
            f = self.getSelection("The project file to open with %s" %
                    editor, relflst, dflt)
        elif action == 'move':
            f = self.getSelection("The target project file for the item",
                    relflst)
        elif action == 'create':
            filedlg = wx.FileDialog(
                self, message = "New project file",
                defaultDir = etmdata,
                defaultFile="",
                wildcard="etm project file (*.text)|*.text",
                style=wx.SAVE
            )
            if filedlg.ShowModal() == wx.ID_OK:
                f= filedlg.GetPath()
                name, ext = os.path.splitext(f)
                f = "%s.text" % name
                if os.path.isfile(f):
                    self.show_errors(
                            ["Error: file '%s' already exists" % fname])
                    return()
                return(f)
            else:
                return()
        if f:
            return(os.path.join(common_prefix, f))
        else:
            return(None)

    def createProject(self, lines):
        if not lines:
            return([False, 'Error: missing project line'])
        f = self.getFile('project', action='create')
        if not f:
            return()
        fo = open(f, 'w')
        fo.write("%s\n" % lines)
        fo.close()
        etmData.logaction(f, "created '%s'" % f)
        self.afterChange('project created')

    def itemAdd(self, lines, file):
        etmData.linesAdd(lines, file)
        self.afterChange('item added')

    def itemReplace(self, id, lines):
        if self.confirmChange(msg = ['save changes?']):
            rel_path, num_beg, num_end = id.split(':')
            file = os.path.join(self.commonPrefix, rel_path)
            etmData.linesReplace(num_beg, num_end, lines, file)
            self.afterChange('changes saved')
        else:
            self.Clear()
            self.infoMessage('canceled', 4)

    def itemDelete(self, id):
        if self.confirmChange(msg = ['delete item?']):
            rel_path, num_beg, num_end = id.split(':')
            file = os.path.join(self.commonPrefix, rel_path)
            etmData.linesDelete(int(num_beg), int(num_end), file)
            self.afterChange('item deleted')
        else:
            self.Clear()
            self.infoMessage('canceled', 4)

    def itemMove(self, id):
        file = self.getFile('project', action="move")
        if file:
            rel_path, num_beg, num_end = id.split(':')
            source = os.path.join(self.commonPrefix, rel_path)
            # get the line first
            lines = etmData.linesGet(int(num_beg), int(num_end), source)
            target = os.path.join(self.commonPrefix, file)
            # add it to the target
            etmData.linesAdd(lines, target)
            # and then delete it from the source
            etmData.linesDelete(num_beg, num_end, source)
            self.afterChange('item moved')
        else:
            self.Clear()
            self.infoMessage('canceled', 4)

    def itemFinish(self, id):
        if id in self.id2hash:
            item = self.id2hash[id]
            tdy = datetime.datetime.today().strftime("%Y-%m-%d")
            if item[u'leader'][0] not in ['-', '+']:
                self.infoMessage('only tasks can be finished', 4)
                return()
            tmp = {}
            for key in item:
                tmp[key] = item[key]
            if u'_f' in tmp:
                if 'r' in tmp:
                    lst = list(tmp[u'_f'])
                    tup = map(int, tdy.split('-'))
                    lst.append(tup)
                    tmp[u'_f'] = tuple(lst)
                    slst = ["%s-%s-%s" % (x[0],x[1],x[2]) for x in lst]
                    tmp[u'f'] = "(%s)" % ', '.join(slst)
                else:
                    self.infoMessage(
                        'this non repeating task is already finished', 4)
                    return()
            else:
                if 'r' in tmp:
                    tmp[u'f'] = "(%s)" % tdy
                else:
                    tmp[u'f'] = "%s" % tdy
            return(etmData.hash2Str(tmp))
        return()

    def itemUnfinish(self, id):
        if id in self.id2hash:
            item = self.id2hash[id]
            if item[u'leader'][0] not in ['-', '+']:
                self.infoMessage('only tasks can be unfinished', 4)
                return()
            tmp = {}
            for key in item:
                tmp[key] = item[key]
            if u'_f' in tmp:
                if len(tmp[u'_f']) > 1:
                    tmp[u'_f'] = item[u'_f'][:-1]
                    lst = list(tmp[u'_f'])
                    tmp[u'_f'] = tuple(lst)
                    slst = ["%s-%s-%s" % (x[0],x[1],x[2]) for x in lst]
                    tmp[u'f'] = "(%s)" % ', '.join(slst)
                else:
                    del tmp[u'_f']
                    del tmp[u'f']
                return(etmData.hash2Str(tmp))
            else:
                self.infoMessage('there are no finish dates to remove', 4)
        return()

    def SelectOpen(self, event):
        """
        Open goto link
        """
        id = self.itemTree.selected_id
        if id in self.id2hash:
            item = self.id2hash[id]
            if not u'g' in item:
                return()
            m = parens_regex.match(item[u'g'])
            if m:
                parts = m.group(1).split(',')
                goto = self.getSelection('The target to open', parts)
            else:
                goto = item[u'g']
            try:
                OpenWithDefault(goto)
            except:
                self.show_errors(
                        ["Error: could not open '%s'" % goto])
        return()

    def TimerToggle(self, v='', h={}, minutes=0):
        if self.timer_status == 'stopped':
            # beginning
            if minutes:
                seconds = int(minutes) * 60
            else:
                seconds = 0
            self.timer_delta = datetime.timedelta(seconds = seconds)
            now = datetime.datetime.now(tzlocal())
            now.replace(second=0)
            self.last_time = now
            self.timer_status = 'running'
            self.timer_entry = v
            if h:
                self.timer_hash = h
        elif self.timer_status == 'running':
            # pausing
            now = datetime.datetime.now(tzlocal())
            now.replace(second=0)
            if now > self.last_time:
                self.timer_delta += (now - self.last_time)
            self.timer_status = 'paused'
        elif self.timer_status == 'paused':
            # restarting
            self.last_time = datetime.datetime.now(tzlocal())
            self.timer_status = 'running'
        if self.timer_status != 'stopped':
            self.timer_minutes = self.timer_delta.seconds//60
            if self.timer_delta.seconds%60 >= 30:
                self.timer_minutes += 1
            h = self.timer_minutes//60
            m = self.timer_minutes%60
            timer = "%d:%02d %s: %s" % (h, m, self.timer_status,
                    self.timer_entry)
            self.current_msg = timer
            self.infoMessage(timer)

    def TimerStop(self):
        v = ""
        if self.timer_status in ['running', 'paused']:
            now = datetime.datetime.now(tzlocal())
            self.timer_delta += (now - self.last_time)
            # change this to stopped after action is recorded
            #  self.cur_tmpl = actn_tmpl
            self.timer_status = 'paused'
            self.timer_minutes = self.timer_delta.seconds//60
            if self.timer_delta.seconds%60 >= 30:
                self.timer_minutes += 1
            if increment > 1:
                minutes = (self.timer_minutes//increment)*increment + \
                        max(increment, self.timer_minutes%increment)
            else:
                minutes = self.timer_minutes
            td = datetime.date.today().strftime(date_fmt)
            h = minutes//60
            m = minutes%60
            self.timer_hash[u'e'] = "+%d:%02d" % (h, m)
            timer = "%d:%02d %s: %s" % (h, m, self.timer_status,
                    self.timer_entry)
            self.current_msg = timer
            self.infoMessage(timer)
            v = etmData.hash2Str(self.timer_hash)
        return(v)

    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+5)/(self.busy.w+15), 6)
        m = 7*60+y*5

        sel_id = None
        selday = None
        self.sel_id = None

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

        if self.sel_id and self.sel_id in self.itemTree.id2index:
            self.itemTree.SetFocus()
            index = self.itemTree.id2index[self.sel_id]
            self.itemTree.SelectItem(index)
            self.itemTree.EnsureVisible(index)
        evt.Skip()

    def alert_status(self):
        ret = ['<title>etm alert status</title>',
                '<pre>', '']
        if len(self.alert_queue) > 0:
            ret.extend(["<center><b>Currently scheduled alerts</b></center>", ''])
            for alert in self.alert_queue:
                line = "    %s: %s %s" % (alert[0], alert[1], alert[2])
                ret.append('    <font color="blue">%s</font>' % line)
        else:
            ret.append("<center><b>No alerts are currently scheduled.</b></center>")
        ret.append("</pre>")
        html = "\n".join(ret)
        dlg = ETMhtml(self, size=(600,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_alert.Play(wx.SOUND_SYNC)
        self.alert.Show()

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

    def startTimer(self):
        self.timer_running = True
        self.timer_sched = scheduler(time.time, time.sleep)
        self.timer_thread = Thread(target=self.timer_sched.run)
        self.timer_sched.enter(0, 0, self.onTimer, ())
        self.timer_thread.setDaemon(1)
        self.timer_thread.start()

    def onTimer(self):
        self.now = datetime.datetime.now(tzlocal())
        nxt = 60 - self.now.second
        self.timer_sched.enter(nxt, 0, self.onTimer, ())
        if self.timer_status == 'running':
            # seconds to add since last update
            self.timer_delta += (self.now - self.last_time)
            self.last_time = self.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.refreshTdy)
        return(True)

    def OnSelActivated(self, event):
        item = self.itemTree.selected
        if not item or self.details.IsEditable():
            event.Skip()
            return()
        item = self.itemTree.GetPyData(item)
        if item[1] and item[1][0].strip():
            self.content = self.details.GetValue()
            id = item[1][0]
            hsh = self.id2hash[id]
            typ = hsh[u'leader'][0]
            det = hsh[u'DT']
            if det:
                tag_str = det
            else:
                tag_str = ""
            self.details.StyleSetFont(stc.STC_STYLE_DEFAULT, self.efont)
            self.details.ChangeValue(det)
            self.active_id = id
            self.selected_id = id
            self.treeFocused = False
            self.details.SetFocus()
            self.details.DocumentStart()
            self.details.SetEditable(True)
            self.details.EmptyUndoBuffer()
            self.typeMode = 'replace'

    def DeActivate(self, event=None, msg=''):
        if self.details.IsEditable():
            if self.details.IsModified():
                if self.timer_status == 'paused':
                    dlg = wx.MessageDialog(None, 'restart action timer?',
                        'etm', wx.YES_NO | wx.NO_DEFAULT )
                    if dlg.ShowModal() == wx.ID_YES:
                        self.TimerToggle()
                        self.Clear(event, "restarting timer")
                        return()

                dlg = wx.MessageDialog(None, 'abandon changes?',  'etm',
                    wx.YES_NO | wx.YES_DEFAULT )
                if dlg.ShowModal() == wx.ID_NO:
                    self.details.SetFocus()
                    return ()
                else:
                    #  if self.timer_status == 'paused':
                    self.entryMode = None
                    self.typeMode = None
                    self.timer_status = 'stopped'
                    self.timer_minutes = 0
                    self.current_msg = ''
        self.Clear(event, msg)

    def Clear(self, event=None, msg=''):
        self.infoMessage(msg, 4)
        if self.timer_status == 'stopped':
            self.current_msg = ''
        self.itemTree.unselect(event)
        self.itemTree.selected_id = None
        self.details.ChangeValue('')
        self.details.EmptyUndoBuffer()
        self.entrMode = None
        self.typeMode = None
        self.details.SetEditable(False)
        self.itemTree.SetFocus()

    def beginEntry(self, c):
        if c in ltr2Char:
            c = ltr2Char[c]
        if c in char2Mode:
            self.typeMode = char2Mode[c]
        else:
            self.typeMode = None
            return()
        self.content = self.details.GetValue()
        if c == 'A' and self.timer_status != 'stopped':
            # stop the action timer and insert the result into details
            self.entryMode = 'create'
            s = self.TimerStop()
            if s:
                self.details.ChangeValue("")
                self.details.SetEditable(True)
                self.details.InsertText(0, "%s" % s)
                self.typeMode = 'Actions'
        elif c in ['o', 'b', 'j']:
            # we will be parsing options
            opts = {}
            if c == 'j':
                self.entryMode = 'jump'
            elif c == 'o':
                self.entryMode = 'outline'
            elif c == 'b':
                self.entryMode = 'busy'

            if c == 'j':
                #  value = '%s ' % c
                if self.j_lines and self.j_lines.strip() != 'j':
                    value = self.j_lines
                elif self.selday and self.selday != self.today:
                    b = "%d-%02d-%02d" % self.selday
                    value = '%s %s' % (c, b)
                else:
                    b = "%d-%02d-%02d" % self.today
                    value = '%s %s' % (c, b)
            elif c == 'o' and self.o_lines and self.o_lines != 'o':
                value = self.o_lines
            elif c == 'b' and self.b_lines and self.b_lines != 'b':
                value = self.b_lines
            elif self.selday and self.selday != self.today:
                b = "-b %d-%02d-%02d" % self.selday
                value = '%s %s' % (c, b)
            else:
                value = '%s ' % c
            self.details.ChangeValue(value)
            self.details.DocumentEnd()
            ep = self.details.GetCurrentPos()
            self.details.SetSelection(2, ep)
            self.details.SetEditable(True)
            self.infoMessage('setting %s options' % self.entryMode, 4)
        elif c in ['p']:
            # we will be opening a project in the external editor
            self.entryMode = 'open'
            self.infoMessage('opening project file', 4)
            wx.Yield()
            self.openProject()
            return()
        elif c in ['P']:
            # we will be creating a new project
            self.entryMode = 'project'
            self.details.ChangeValue('')
            self.details.SetEditable(True)
            self.infoMessage(
                    'creating a new project - press shift-return to save', 6)
        elif c in ['~', '*', '!', '-', '+', 'A']:
            # we will be creating a new item
            self.entryMode = 'create'
            if c == '~':
                if self.timer_status != 'stopped':
                    self.TimerToggle()
                    return()
            if c == 'A':
                item = {u'leader' : '~'}
                #  c = '~'
            else:
                item = {u'leader' : c}

            item[u'd'] = "%d-%02d-%02d" % self.selday

            if (self.itemTree.selected_id and self.itemTree.selected_id in
                    self.id2hash):
                selitem = self.id2hash[self.itemTree.selected_id]
                for key in char2Keys[c] + special_keys:
                    if key in selitem:
                        item[key] = selitem[key]
                if c == '~' and selitem[u'leader'] == '~':
                    # we are restarting an old action
                    if 'e' in item:
                        del item['e']
                    item[u'd'] = "%d-%02d-%02d" % self.today

            self.details.ChangeValue("")
            self.details.SetEditable(True)
            # create new item based on selected item
            s = etmData.hash2Str(item)
            self.details.InsertText(0, "%s" % s)
            self.details.ClearUndo()
            self.details.DocumentEnd()
            ep = self.details.GetCurrentPos()
            self.details.SetSelection(ep, 2)
            self.infoMessage(
                    'creating a new %s - press shift-return to save' %
                    self.typeMode[:-1], 6)
        else:
            # we will need a selected id
            if not self.itemTree.selected_id:
                self.infoMessage('an item must be selected', 4)
                return()
            id = self.itemTree.selected_id
            if id not in self.id2hash:
                self.infoMessage('an item must be selected', 4)
                return()
            if c in ['f', 'u', 'c']:
                # we need change the content of details appropriately
                if c == 'f':
                    s = self.itemFinish(id)
                elif c == 'u':
                    s = self.itemUnfinish(id)
                else: # clone
                    if self.id2hash[id][u'leader'] == '~' and \
                        self.id2hash[id][u'e']:
                        # we're cloning an action and need to reset the extent
                        h = {}
                        for key in self.id2hash[id]:
                            h[key] = self.id2hash[id][key]
                        del h[u'e']
                        s = etmData.hash2Str(h)
                    else:
                        s = self.id2hash[id][u'DT']
                if s:
                    self.details.ChangeValue('')
                    self.details.SetEditable(True)
                    self.details.InsertText(0, s)
                    if c == 'c':
                        # clear the undo buffer for clones
                        self.details.ClearUndo()
                    self.details.SetFocus()
                    self.details.DocumentEnd()
                    self.infoMessage(mode2Description[self.typeMode], 4)
            else: # 'm' or 'd'
                if c == 'd':
                    self.itemDelete(id)
                else:
                    self.itemMove(id)
                self.itemTree.SetFocus()
                return()
        self.details.SetFocus()

    def endEntry(self, event):
        # we have self.typeMode and self.entryMode
        if not self.details.IsEditable() or not self.details.IsModified() or \
                not self.typeMode:
            return()
        lines = self.ContentLines().strip()
        if self.typeMode in ['project']:
            self.createProject(lines)
            return()
        elif self.typeMode in ['outline', 'jump']:
            # parse lines as options or use defaults if there are no lines
            if self.typeMode in ['jump']:
                # drop the 'j'
                j_str = "%s" % normalize_whitespace(lines[1:])
                self.j_lines = "j %s" % j_str.strip()
                if self.o_lines:
                    # add the new begin date to the other options
                    lines = "%s -b %s" % (self.o_lines, j_str)
                else:
                    # set the begin date
                    lines = "o -b %s" % j_str
                option_str = normalize_whitespace(lines)
            else:
                option_str = normalize_whitespace(lines)
                self.o_lines = option_str
            label, treedata, options, self.busytimes = etmData.getView(
                    self.alltups, lines, self.id2hash)
            self.options_saved = deepcopy(options)
            if 'begin_date' in options:
                self.options['begin_date'] = options['begin_date']
                y,m,d = map(int, options['begin_date'])
                dt = wx.DateTimeFromDMY(d, m-1, y)
                self.cal.SetDate(dt)
                if ('details' not in options or options['details'] in [0,1]):
                    self.showDay()
            self.options = options
            msg = ''
            if 'details' in options and options['details'] not in [0,1]:
                if 'cols' in options and 'F' in options['cols']:
                    h, data, options = etmData.make_tree(self.alltups,
                            options, self.id2hash)
                    dt = etmData.getChildren(data, level=-1, dt=[],
                            prefix=25,
                            totalsfirst=options['totalsfirst'],
                            details=options['details'],
                            id2hash=self.id2hash)
                    while dt and not dt[-1][0]:
                        dt.pop(-1)
                else:
                    dt = etmData.data2Report(self.alltups, options,
                            self.id2hash)
                rep = ['<font color="%s">%s</font>' % (hexcolors[a], s)
                        for s, f, a, i in dt]
                page = """<pre>%s</pre>""" % "\n".join(rep)
                self.sbar_opts.SetLabel("options: %s" % option_str)
                self.ShowHtml(page)
            elif 'cols' in options and 'F' in options['cols']:
                self.itemTree.SetPrefix(25)
                self.itemTree.CollapseAll()
            else:
                self.itemTree.SetPrefix(16)
                self.itemTree.ExpandAll()
            if 'vcal' in options and options['vcal']:
                ret, msg = etmData.make_vcal('export.ics', self.alltups,
                        options)
            if 'values' in options and options['values']:
                msg = etmData.make_csv('export.csv', self.alltups, options)
            if 'details' in options and options['details'] in [0, '0']:
                self.itemTree.show_leaves = False
            else:
                self.itemTree.show_leaves = True
            if 'totalsfirst' and options['totalsfirst']:
                self.itemTree.totals_first = True
            else:
                self.itemTree.totals_first = False
            self.busy.SetEvents(self.busytimes)
            self.itemTree.SetTree(treedata)
            lab = leadingzero.sub('',label)
            self.datebar.SetLabel(lab)
            self.sbar_opts.SetLabel("options: %s" % option_str)
            if option_str not in self.options_lst:
                self.options_lst.append(option_str)
            self.Clear()
            if msg:
                self.infoMessage(msg, 4)
            return()
        if self.typeMode in ['busy']:
            option_str = normalize_whitespace(lines)
            self.b_lines = option_str
            label, treedata, options, self.busytimes = etmData.getView(
                    self.alltups, self.b_lines, self.id2hash)
            matching, options, busytimes = etmData.opts2Tuples(
                    self.alltups, options)
            page = etmData.getBusy(busytimes, self.b_lines)
            self.ShowHtml(page)
            self.sbar_opts.SetLabel("options: %s" % self.b_lines)
            nlines = normalize_whitespace(lines)
            if nlines not in self.options_lst:
                self.options_lst.append(nlines)
            self.Clear()
            return()

        # we need a selected_id for these  modes
        if self.typeMode in ['replace', 'finish', 'unfinish', 'clone']:
            if (not self.itemTree.selected_id or self.itemTree.selected_id
                not in self.id2hash):
                return()
            else:
                id = self.itemTree.selected_id
        # we need lines for these modes
        if self.typeMode in ['replace', 'finish', 'unfinish', 'clone',
               'Actions', 'actions', 'events', 'notes', 'tasks']:
            if not lines:
                # we need lines for these modes
                return()
            msg, hsh, s = etmData.checkLines(lines)
            if msg:
                try:
                    page = "\n".join(msg)
                    dlg = wx.MessageDialog(None, page, 'etm',
                            wx.OK)
                    return(dlg.ShowModal())
                except:
                    print 'except', msg
                return()
            # check the current leader and set the page and mode accordingly
            c = hsh[u'leader'][0]

        if self.typeMode in ['replace', 'finish', 'unfinish', 'clone',
                'Actions', 'actions', 'events', 'notes', 'tasks']:
            if auto_set_timezone and 'd' in hsh and 'z' not in hsh:
                if self.ltz:
                    hsh[u'z'] = self.ltz
                    s = etmData.hash2Str(hsh)
                else:
                    print "error: @z missing but could not set local zone"
            if etmUser and 'U' not in hsh:
                hsh[u'U'] = etmUser
                s = etmData.hash2Str(hsh)
            self.details.SetText(s)
            #  if c == '~' and u'e' not in hsh:
            if c == '~' and self.entryMode == 'create' \
                    and not self.timer_minutes:
                l = text_wrap(hsh[u'DS'], 26)
                if len(l) > 1:
                    v = "%s ..." % l[0]
                else:
                    v = l[0]
                if u'e' in hsh:
                    start_minutes = hsh['EM']
                else:
                    start_minutes = 0
                #  hsh['DT'] = etmData.hash2Str(hsh)
                self.TimerToggle(v, hsh, start_minutes)
                if self.typeMode == 'actions':
                    self.details.ChangeValue('')
                    self.details.EmptyUndoBuffer()
                    self.typeMode = None
                    self.details.SetEditable(False)
                    self.itemTree.SetFocus()
                    return()
            if self.typeMode in ['Actions', 'actions', 'events',
                    'clone', 'notes', 'tasks']:
                # we need a file for these modes
                f = self.getFile(self.typeMode)
                v = self.details.GetValue()
                if (self.typeMode in ['Actions', 'actions'] and
                        self.entryMode == 'create' and
                        self.timer_status in ['running', 'paused']):
                    self.entryMode = None
                    self.typeMode = None
                    self.timer_status = 'stopped'
                    self.timer_minutes = 0
                if f and v:
                    self.itemAdd(v, f)
                else:
                    self.current_msg = ''
                    self.DeActivate()
                    return()
            else:
                self.itemReplace(id, s)
        return()

    def afterChange(self, msg):
        expansionState = self.itemTree.GetExpansionState()
        self.details.EmptyUndoBuffer()
        self.DeActivate(None, 'working ...')
        wx.Yield()
        #  time.sleep(1)
        self.reloadData()
        self.options = self.options_saved
        label, treedata, options, busy_times = etmData.getView(
                self.alltups, self.options, self.id2hash)
        self.busy.SetEvents(busy_times)
        self.itemTree.SetTree(treedata)
        if not ('cols' in self.options and self.options['cols'] == 'F'):
            self.showDay()
        self.itemTree.SetExpansionState(expansionState)
        self.infoMessage(msg, 4)
        return()

    def ContentLines(self):
        """
            Return a string with newline chars and any empty trailing lines
            removed.
        """
        content = self.details.GetValue()
        lines = content.split('\n')
        while not lines[-1].rstrip('\n'):
            lines.pop()
        return("\n".join(lines))

    def processAlerts(self, alertList):
        now = self.now.replace(second=0, microsecond=0, tzinfo=tzlocal())
        alerts = []
        active = []
        for alert in alertList:
            if alert[0] > now:
                alerts.append(alert)
            elif alert[0] == now:
                active.append(alert)
        return(active, alerts)

    def refreshAlerts(self, event=None):
        self.alert_schedule = []
        self.alert_queue = []
        acts, alrts = self.processAlerts(self.alerts)
        if len(acts) > 0:
            for act in acts:
                self.onAlert(act[2], act[4], None)
        for alert in alrts:
            self.alert_schedule.append(tuple((alert[0], alert[2], alert[4])))
            self.alert_queue.append(tuple((alert[1], alert[2], alert[3])))
        if len(self.alert_queue) > 0:
            self.sbar_alrt.SetLabel("%s %s" % (nextalert,
                    self.alert_queue[0][0]))
        else:
            self.sbar_alrt.SetLabel('')

    def refreshTdy(self, event=None):
        """
            Check for change in today. If modified, getTuples.
        """
        if  self.today != (self.now.year, self.now.month, self.now.day):
            self.today = (self.now.year, self.now.month, self.now.day)
            self.reloadData()
            self.showDay()
            self.infoMessage('reloaded data for date change', 8)
            #  self.sound_alert.Play(wx.SOUND_ASYNC)
        elif etmData.needsUpdating(self.lastModified):
            self.reloadData()
            self.showDay()
            self.infoMessage('reloaded modified data files', 8)
            self.sound_alert.Play(wx.SOUND_ASYNC)
        d = self.now.strftime(status_fmt)
        t = self.now.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)

        self.refreshAlerts()

        if self.timer_status != 'stopped':
            self.timer_minutes = self.timer_delta.seconds//60
            if self.timer_delta.seconds%60 >= 30:
                self.timer_minutes += 1
            h = self.timer_minutes//60
            m = self.timer_minutes%60
            timer = "%d:%02d %s: %s" % (h, m, self.timer_status,
                    self.timer_entry)
            self.sbar_info.SetLabel(timer)
            if self.timer_play:
                self.sound_alert.Play(wx.SOUND_ASYNC)
                self.timer_play = False

    def onAlert(self, title, msg, altcmd):
        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):
        if self.selday != self.today:
            self.selday = self.today
        self.options = {}
        self.itemTree.ShowLeaves = True
        self.options['begin_date'] = 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.options['begin_date'] = (y,m,d)
        if 'end_date' in self.options:
            del self.options['end_date']
        self.showDay()

    def showOps(self):
        l = []
        #  print "showOps", self.options
        k = [x for x in self.options if x in short_opts]
        for key in k:
            if self.options[key] != None:
                if key == 'details':
                    if self.options[key] != 1:
                        l.append("-%s %s" % (short_opts[key], self.options[key]))
                elif key in ['begin_date', 'end_date']:
                    pass
                else:
                    l.append("-%s %s" % (short_opts[key], self.options[key]))
        if l:
            self.sbar_opts.SetLabel("options: o %s" % ' '.join(l))
        else:
            self.sbar_opts.SetLabel("")

    def showDay(self, force=True):
        if 'begin_date' in self.options and self.options['begin_date']:
            self.selday = self.options['begin_date']
        elif self.selday:
            pass
        else:
            self.options['begin_date'] = map(int,
                    datetime.datetime.today().strftime("%Y,%m,%d").split(','))
            self.selday = self.options['begin_date']
        # if 'end_date' in self.options and self.options['end_date']:
        #     print('showDay 1 end_date', self.options['end_date'])


        self.search.SetValue('')
        if 'search' in self.options:
            del self.options['search']
        self.showOps()
        self.index = -1
        self.month = self.selday[0]
        if force:
            self.showMonth()
            self.itemTree.DeleteAllItems()
            matching, options, busytimes = etmData.opts2Tuples(self.alltups,
                    self.options)
            label, treedata, busy_times = etmData.getWeek(
                    self.alltups, self.selday, options)
            self.busy.SetEvents(busy_times)
            self.itemTree.SetTree(treedata)
            if self.itemTree.root_items:
                self.itemTree.ScrollTo(self.itemTree.root_items[0])
            lab = leadingzero.sub('',label)
            self.datebar.SetLabel(lab)
            # if 'end_date' in self.options and self.options['end_date']:
            #     print('showDay 2 end_date', self.options['end_date'])

        if self.selday == etmData.today:
            self.datebar.SetForegroundColour(todaycolor)
        else:
            self.datebar.SetForegroundColour("BLACK")

    def showMonth(self):
        attrs= {}
        ym = (self.selday[0], self.selday[1])
        daynum = self.selday[2]
        if ym in self.dayColors:
            for day, color in self.dayColors[ym]:
                attrs[day] = wx.calendar.CalendarDateAttr()
                if day in range(daynum+1,daynum+7):
                    attrs[day].SetTextColour(color)
                    attrs[day].SetBackgroundColour(bgweek)
                    #  attrs[day].SetBorder(wx.calendar.CAL_BORDER_SQUARE)
                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 OnEntryChar(self, event):
        keycode = event.GetKeyCode()
        ukeycode = event.GetUnicodeKey()    # for control keys
        shift = event.ShiftDown()
        control = event.ControlDown()
        modifier = event.GetModifiers()
        if show_keys:
            print "OnEntryChar", keycode, ukeycode, shift, modifier
        if self.details.IsEditable():
            if shift and keycode == 13:      # shift-enter
                text = self.details.GetText()
                self.endEntry(event)
            elif keycode == 17:              # Ctrl-Q quit
                self.OnQuit()
            elif keycode == wx.WXK_F1:  # F1 Toggle Help
                self.OnHelp(event)
            elif keycode in [27]:
                if self.typeMode in ['outline', 'busy', 'jump']:
                    self.Clear()
                else:
                    self.DeActivate()
            elif shift and keycode == 13:
                self.DeActivate()

            elif control and keycode in [ord('p'), ord('P')]:   # p print
                if self.itemTree.selected and \
                        self.itemTree.IsSelected(self.itemTree.selected):
                    page = self.itemTree.returnChildren(
                            self.itemTree.selected, 1)
                else:
                    page = self.itemTree.returnChildren(
                            self.itemTree.root, 0)
                self.ShowHtml(page)
            elif control and keycode in [ord('y'), ord('Y')]:  # Y yank
                if self.itemTree.selected and \
                        self.itemTree.IsSelected(self.itemTree.selected):
                    self.itemTree.yankChildren(self.itemTree.selected, 1)
                    self.infoMessage("selected branch copied to clipboard",
                            4)
                else:
                    self.itemTree.yankChildren(self.itemTree.root, 0)
                    self.infoMessage("tree copied to clipboard", 4)
            elif control and keycode in [wx.WXK_TAB, 32]:
                # control + tab completion
                selection = None
                # here we match on the entire entry string
                if self.typeMode in ['outline', 'busy']:
                    cur_regex = display_template_regex
                    full_regex = display_full_regex
                    templates = self.display_templates + self.options_lst
                    #  templates = self.options_lst
                    title = "display option templates"
                else:
                    cur_regex = item_template_regex
                    full_regex = item_full_regex
                    templates = self.item_templates
                    title = "item templates"
                try:
                    p = self.details.GetCurrentPos()
                    s = self.details.GetTextRange(0,p)
                    m = cur_regex.match(s)
                    if not m:
                        m = full_regex.match(s)
                except:
                    m = None
                if m:
                    entry = m.group(1).strip()
                    if entry:
                        regex = re.compile(r'^%s' % re.escape(entry))
                        length = len(entry)
                    else:
                        regex = re.compile(r'^.*')
                        length = 0
                    lst = [x for x in templates if regex.match(x)]
                    if lst:
                        selection = self.getSelection(title, lst)
                    else:
                        selection = None
                        self.infoMessage('no matching templates were found',
                                4)
                if selection:
                    self.details.InsertText(p, selection[length:])
                    self.details.SetCurrentPos(p + len(selection) - length)
                    nselection = normalize_whitespace(selection)
                    if nselection not in (self.display_templates +
                            self.options_lst):
                        self.options_lst.append(nselection)
            else:
                event.Skip()
        else:
            self.OnCommonChar(event)

    def OnTreeChar(self, event):
        keycode = event.GetKeyCode()
        if show_keys:
            print "OnTreeChar", keycode
        ukeycode = event.GetUnicodeKey()    # for control keys
        shift = event.ShiftDown()
        control = event.ControlDown()
        modifier = event.GetModifiers()

        if keycode == 17:           # Ctrl-Q quit
            self.OnQuit()
        elif keycode in [13]:
            self.OnSelActivated(event)
        elif keycode in [27]:
            self.DeActivate()
            self.itemTree.unselect(event)
            self.itemTree.selected_id = None
            self.details.ChangeValue('')
        elif keycode == wx.WXK_UP:
            if (self.itemTree.selected and
                    self.itemTree.IsSelected(self.itemTree.selected)):
                if shift and control:
                    if self.itemTree.root_items:
                        self.itemTree.SelectItem(
                                self.itemTree.root_items[0])
                elif shift:
                    prev_sib = self.itemTree.GetPrevSibling(
                        self.itemTree.selected)
                    if prev_sib:
                        self.itemTree.SelectItem(prev_sib)
                elif control:
                    parent = self.itemTree.GetItemParent(
                        self.itemTree.selected)
                    first_child, cookie = self.itemTree.GetFirstChild(parent)
                    if first_child.IsOk:
                        self.itemTree.SelectItem(first_child)
                else:
                    event.Skip()
            else:
                event.Skip()
        elif keycode == wx.WXK_DOWN:
            if (self.itemTree.selected and
                    self.itemTree.IsSelected(self.itemTree.selected)):
                if shift and control:
                    if self.itemTree.root_items:
                        self.itemTree.SelectItem(
                                self.itemTree.root_items[-1])
                elif shift:
                    next_sib = self.itemTree.GetNextSibling(
                        self.itemTree.selected)
                    if next_sib:
                        self.itemTree.SelectItem(next_sib)
                elif control:
                    parent = self.itemTree.GetItemParent(
                        self.itemTree.selected)
                    last_child = self.itemTree.GetLastChild(parent)
                    if last_child:
                        self.itemTree.SelectItem(last_child)
                else:
                    event.Skip()
            else:
                event.Skip()
        elif keycode == wx.WXK_LEFT:
            if (shift and control):
               self.itemTree.CollapseAll()
            elif (self.itemTree.selected and
                    self.itemTree.IsSelected(self.itemTree.selected)):
                parent = self.itemTree.GetItemParent(
                            self.itemTree.selected)
                has_parent = parent and parent != self.itemTree.root
                if shift:
                    has_children = self.itemTree.ItemHasChildren(
                            self.itemTree.selected)
                    expanded = has_children and self.itemTree.IsExpanded(
                            self.itemTree.selected)
                    if expanded:
                        self.itemTree.CollapseAllChildren(
                            self.itemTree.selected)
                    if has_parent:
                        self.itemTree.SelectItem(parent)
                elif control:
                    if has_parent:
                        self.itemTree.SelectItem(parent)
                        self.itemTree.CollapseAllChildren(parent)
                else:
                    event.Skip()
            else:
                event.Skip()
        elif keycode == wx.WXK_RIGHT:
            if (shift and control):
               self.itemTree.ExpandAll()
            elif (self.itemTree.selected and
                    self.itemTree.IsSelected(self.itemTree.selected)):
                has_children = self.itemTree.ItemHasChildren(
                        self.itemTree.selected)
                expanded = has_children and self.itemTree.IsExpanded(
                            self.itemTree.selected)
                if has_children:
                    self.itemTree.Expand(self.itemTree.selected)
                    first_child, cookie = self.itemTree.GetFirstChild(
                            self.itemTree.selected)
                    if first_child.IsOk:
                        if shift:
                            self.itemTree.ExpandAllChildren(first_child)
                            self.itemTree.SelectItem(first_child)
                        elif control:
                            self.itemTree.ExpandAllChildren(
                                    self.itemTree.selected)
                            self.itemTree.SelectItem(first_child)
                        else:
                            event.Skip()
                    else:
                        event.Skip()
                else:
                    event.Skip()
            else:
                event.Skip()
        else:
            self.OnCommonChar(event)

    def OnCommonChar(self, event):
        keycode = event.GetKeyCode()
        control = event.ControlDown()
        shift = event.ShiftDown()
        if show_keys:
            print "OnCommonChar", keycode, control, shift
        if keycode == 32:  # space
            #  self.busytimes = self.alltimes
            self.itemTree.totals_first = False
            self.itemTree.SetPrefix(16)
            self.itemTree.show_leaves = True
            self.itemTree.selected_id = None
            self.itemTree.ExpandAll()
            self.options_saved = {}
            self.Today()
        elif keycode in range(256) and (chr(keycode) in char2Mode or
                chr(keycode) in ltr2Char):
            self.beginEntry(chr(keycode))
        elif keycode == 27:               # Escape
            self.DeActivate()
            self.itemTree.SetFocus()
        elif keycode == 19:          # ^S
            self.search.SetFocus()
        elif keycode == ord(','):
            self.cal.SetFocus()
            self.infoMessage('Calendar has focus', 2)
        elif keycode == ord('.'):
            self.itemTree.SetFocus()
            self.infoMessage('Outline has focus', 2)
        elif keycode == 16:           # ^P preview/print children
            if self.itemTree.selected and \
                    self.itemTree.IsSelected(self.itemTree.selected):
                if self.itemTree.selected_id.strip():
                    dt = etmData.getLeaf(self.id2hash[
                        self.itemTree.selected_id])
                    page = """<pre>%s</pre>""" % "\n".join(dt)
                else:
                    page = self.itemTree.returnChildren(self.itemTree.selected, 1)
            else:
                page = self.itemTree.returnChildren(self.itemTree.root, 0)
            self.ShowHtml(page)
        elif keycode == 22:           # ^V export children as vCal
            if self.itemTree.selected and \
                    self.itemTree.IsSelected(self.itemTree.selected):
                if self.itemTree.selected_id.strip():
                    self.infoMessage(
                            "leaf exported as vCal to clipboard", 4)
                else:
                    self.infoMessage(
                            "branch exported as vCal to clipboard", 4)
                self.itemTree.yankChildTuples(self.itemTree.selected, indent=1)
            else:
                self.itemTree.yankChildTuples(self.itemTree.root, indent=0)
                self.infoMessage("tree exported as vCal to clipboard", 4)
        elif keycode == 6:           # ^F export children as vCal to export.ics
            if self.itemTree.selected and \
                    self.itemTree.IsSelected(self.itemTree.selected):
                if self.itemTree.selected_id.strip():
                    self.infoMessage(
                            "leaf exported as vCal to 'export.ics'", 4)
                else:
                    self.infoMessage(
                            "branch exported as vCal to 'export.ics'", 4)
                self.itemTree.yankChildTuples(self.itemTree.selected, 1, True)
            else:
                self.itemTree.yankChildTuples(self.itemTree.root, 0, True)
                self.infoMessage("tree exported as vCal to 'export.ics'", 4)
        elif keycode == 25:           # ^Y yank
            if self.itemTree.selected and \
                    self.itemTree.IsSelected(self.itemTree.selected):
                if self.itemTree.selected_id.strip():
                    dt = etmData.getLeaf(self.id2hash[
                        self.itemTree.selected_id])
                    s = "\n".join(dt)
                    self.do = wx.TextDataObject()
                    self.do.SetText(s)
                    if wx.TheClipboard.Open():
                        wx.TheClipboard.SetData(self.do)
                        wx.TheClipboard.Close()
                    self.infoMessage("leaf copied to clipboard", 4)
                else:
                    self.itemTree.yankChildren(self.itemTree.selected, 0)
                    self.infoMessage("branch copied to clipboard", 4)
            else:
                self.itemTree.yankChildren(self.itemTree.root, 0)
                self.infoMessage("tree copied to clipboard", 4)
        elif keycode in range(48, 58): # 0, ..., 9
            if display_shortcuts:
                self.typeMode = char2Mode['o']
                indx = min(len(display_shortcuts)-1, keycode - 48)
                value = "%s" % normalize_whitespace(
                        display_shortcuts[indx])
                if '-b' not in value and self.selday:
                    # add the selected day as the value for -b
                    value = "%s -b %s-%s-%s" % (value, self.selday[0], self.selday[1], self.selday[2])
            else:
                self.infoMessage('add elements to "display_shortcuts" in etmrc', 4)
                return()
            c = value[0]
            self.typeMode = char2Mode[c]
            self.details.ChangeValue(value)
            self.details.DocumentEnd()
            ep = self.details.GetCurrentPos()
            self.details.SetSelection(2, ep)
            self.details.SetEditable(True)
            self.details.SetFocus()
            self.infoMessage('setting %s options' % self.typeMode, 4)
        elif keycode == wx.WXK_LEFT and (shift and control):
            self.itemTree.CollapseAll()
        elif keycode == wx.WXK_RIGHT and (shift and control):
            self.itemTree.ExpandAll()
        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
            self.reloadData()
            self.infoMessage('reloaded data', 4)
            self.Today()
        elif keycode == ord('g') and self.itemTree.selected_id:
            self.SelectOpen(self.itemTree.selected_id)
        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
            ok, msg = newer()
            dlg = MyAlert('etm version data', msg,
                size=(540, 120))
            dlg.Show()
        elif keycode == wx.WXK_F4:  # F4 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:
            ok, l = getSunMoon()
            if ok:
                txt = "\n".join(l)
            else:
                txt = l
            dlg = MyAlert('USNO local sun and moon data', txt, size=(540, 500))
            dlg.Show()
        else:
            event.Skip()

    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.License = license
                info.WebSite = ("http://www.duke.edu/~dgraham/ETM",
                        "etm home page")
                info.Developers = [
                        "Daniel A. Graham <daniel.graham@duke.edu>", ]
                wx.AboutBox(info)

    def OnHelp(self, event):
        self.helpFrame = ETMnb(self)
        self.helpFrame.Show(True)

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

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

class App(wx.App):

    def OnInit(self):
        try:
            wx.lib.colourdb.updateColourDB()
            self.frame = MyFrame()
            self.frame.Show(True)
            self.SetTopWindow(self.frame)
            return True
        except:
            print "failed OnInit"
            print sys.exc_info()
            return False

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

if __name__ == '__main__':
    main()
