
# abb 6/28/2012: separated the WX gui stuff to this module.
# -*- coding: utf-8 -*-
# above encoding is for non-ascii in source comments

"""
Andy at FosteringCourtImprovement.org, 12/21/2011, version 1.0 release
WX Python GUI script reads multiple AFCARS files, sorts and classifies rows,
displays color-coded records in a search-enabled grid, saves to AFCARS file
with 2 added fields: 1. removal episode id, and 2. record class.

History:
abb 5/18/2012: crashing bc I didn't check for unicode in ExcessData, fixed, version 1.1.
abb 5/19/2012: epid format was %20.20s, should be %21.21s, fixed, version 1.1.

"""

import os, sys
from afcarslinker1p2_data import *
import wx, wx.grid

#===========================================================
# GUI data handling objects:

# quicky function to output stats table:
def dict2html( d1, h1 ):
    if len(d1) > 0:
        t1 = '<TABLE border=1 cellspacing=0>'
        t1 += '\n<TR><TH>' + '</TH><TH>'.join(h1) + '</TH></TR>'
        for k, v in sorted(d1.items()):
            v = map( lambda i: str(i), v )
            t1 += '\n<TR><TH align=left>' + k + '</TH><TD align=right nowrap>' + '</TD><TD align=right nowrap>'.join(v) + '</TD></TR>'
        t1 += '\n</TABLE>'
    else:
        t1 = '<P>No valid records in files?</P>'

    return( t1 )

# This GridDataTable supplies the wx.DataFrame with the data to put in its wx.grid.
class GridDataTable( wx.grid.PyGridTableBase ):
    _rowlabels = _collabels = _data = None
    _irow = None  # index to rows that are displayed, used for filtering & toggling display of classes of rows
    _firstfield = None
    # row classes should have bg, fg, & font associated with them. best structure for that?
    # dict, collections.namedtuple, class with __slots__?
    # http://dev.svetlyak.ru/using-slots-for-optimisation-in-python-en/
    rowclasslabels = RecordClass.recordlabels
    rowclassbgcols = RecordClass.recordcolors
    rowclassshow = [True] * len(rowclasslabels)
    colclasslabels = ( 'Field Change in Episode', )
    colclassbgcols = ( '#b000b0', )
    rowclasses = None  # working copy of class of each row in _data
    defaultfont = boldfont = strikethroughfont = None
    fieldfilterfield = None
    fieldfilterpat = None

    def GetRowLabelValue(self, row):
        if row < len(self._irow):
            row = self._irow[row]
            return self._rowlabels[row] if self._rowlabels and row < len(self._rowlabels) else None
        else:
            return None

    def GetRowLabelValues(self):
        return self._rowlabels[self._irow] if self._irow else self._rowlabels

    def GetColLabelValue(self, col):
        return self._collabels[col] if self._collabels else None

    def GetColLabelValues(self):
        return self._collabels

    def GetNumberRows(self):
        return len(self._irow) if self._irow else 0

    def GetNumberCols(self):
        return len(self._collabels) if self._collabels else 0

    def GetValue(self, row, col):
        #print >> sys.stderr, 'GetValue: ', row, col, len(self._data), len(self._data[0])
        if row < len(self._irow):
            row = self._irow[row]
            # linux-only bug: producing "list index out of range" when 2nd file opened 
            # and first file had fewer lines than screen. caching in grid?
            # this works around, but something somewhere still out of range?
            if self._data and row < len(self._data) and col < len(self._data[row]):
                return self._data[row][col]
            else:
                return ''
        else:
            return ''

    def GetAttr(self, row, col, kind):
        attr = wx.grid.GridCellAttr()
        if row < len(self._irow):
            row = self._irow[row]
            if self.rowclasses and row < len(self.rowclasses):
                attr.SetBackgroundColour( self.rowclassbgcols[self.rowclasses[row]] )
                if self.rowclasses[row] == RecordClass.MostRecentEpisode and self.boldfont:
                    attr.SetFont( self.boldfont )
                elif self.rowclasses[row] == RecordClass.Duplicate and self.strikethroughfont:
                    attr.SetFont( self.strikethroughfont )

                # Long test here, because not all rows have same # of cols.
                #if col==20 and self._data[row][self._firstfield].find('GBOSK4VJTH2L') >= 0:
                if row < (len(self._data)-1) and col > 4 and col < len(self._data[row]) and col < len(self._data[row+1]) and self._data[row][self._firstfield] == self._data[row+1][self._firstfield] and self._data[row][col].strip() != self._data[row+1][col].strip():
                    attr.SetBackgroundColour( self.colclassbgcols[0] ) # highlight changed fields
                #try: #except:
                #    print >> sys.stderr, 'GetAttr: ', row, col, len(self._data), len(self._data[0])
        return attr

    def SetFirstField( self, ff ):
        self._firstfield = ff

    def SetValues( self, dat, rownames=None, colnames=None ):
        self._rowlabels = list(rownames) or range(len(dat))
        self._collabels = list(colnames) or range(len(dat[0]))
        self._data = dat
        self._rowlabels0 = self._data0 = None

    def SetRowClasses( self, classes ):
        self.rowclasses = classes
        self.UpdateRowClasses()

    def SetDefaultCellFont( self, font ):
        import copy
        self.defaultfont = copy.copy(font)
        self.boldfont = wx.Font( font.GetPointSize(), font.GetFamily(), font.GetStyle(), wx.BOLD )
        # windows keep using same font no matter what I do? totally unpredictable: switched family to Arial?

        # various ms-win workarounds below:
        self.strikethroughfont = wx.Font( font.GetPointSize()-1, font.GetFamily(), font.GetStyle(), wx.NORMAL )
        fontdesc = (self.strikethroughfont.GetNativeFontInfoDesc()).split(';')
        if len(fontdesc) > 8:
            fontdesc[8] = '1'
            fi = wx.NativeFontInfo()
            fi.FromString( ';'.join(fontdesc) )
            self.strikethroughfont.SetNativeFontInfo( fi )  # modifies default/boldfont too on mswin?
            # mswin bug: all the same font, all the time -- workarounds above work on winxp & win7.

    def ToggleRows( self, rc, show ):
        self.rowclassshow[rc] = show
        self.UpdateRowClasses()

    def SetFieldFilterField( self, fieldname ):
        self.fieldfilterfield = self._collabels.index(fieldname)

    def GetFieldFilterField( self ):
        return self._collabels[self.fieldfilterfield] if self.fieldfilterfield else None

    def SetFieldFilter( self, pat ):
        self.fieldfilterpat = pat.upper()

    def GetFieldFilter( self ):
        return self.fieldfilterpat

    def UpdateRowClasses( self ):
        import re
        irow = []
        if self.rowclasses:
            #print >> sys.stderr, 'UpdateRowClasses: ', self.fieldfilterfield, self.fieldfilterpat, '.'
            pat = re.compile( self.fieldfilterpat ) if self.fieldfilterpat else None
            for row, rc in enumerate(self.rowclasses):
                #if self.rowclassshow[rc] and ( (not self.fieldfilterfield) or (not self.fieldfilterpat) or (self._data[row][self.fieldfilterfield].upper().find(self.fieldfilterpat) >= 0) ):
                if self.rowclassshow[rc] and ( (not self.fieldfilterfield) or (not pat) or (pat.search(self._data[row][self.fieldfilterfield].upper())) ):
                    irow.append( row )
        self._irow = irow


"""
Can only set background color with generic StaticText class, not wx.StaticText for some reason.
http://wxpython-users.1045709.n5.nabble.com/wx-StaticText-problem-td2361234.html
from wx.lib.stattext import GenStaticText as StaticText
st2 = StaticText(self, wx.ID_ANY, text ) #, wx.DefaultPosition, wx.DefaultSize, wx.ALIGN_LEFT, '' )
st2.SetFont(wx.Font(8, wx.SWISS, wx.NORMAL, wx.BOLD, False))
Can't set checkbox bg color on GTK systems (Linux) and no generic checkbox class?
One option is cb with no label, hsizer with a statictext box, but need to pass through to cb methods ...
Too much trouble.  Just reverse fg & bg under GTK, since fg color works.
"""
#===========================================================
# A custom status bar subclass that appends static text fields to traditional status text field.
# Derived from demo/StaticText.py.
class MyStatusBar(wx.StatusBar):
    def __init__(self, parent, nfields=0, fieldwidth=100):
        wx.StatusBar.__init__(self, parent, -1)


        self.SetFieldsCount(nfields+1)
        self._widgets = [None] * (nfields+1)
        self._callbacks = [None] * (nfields+1)
        # Preserve field 0 as normal, stretchy statusbar field.
        self.SetStatusWidths( [-1] + [fieldwidth]*nfields )

        self.sizeChanged = True
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_IDLE, self.OnIdle)

    def SetLabel( self, pos, text, fg='black', bg=None, callback=None, font=None ):
        if self._widgets[pos]:
            self.RemoveChild(self._widgets[pos])
            self._widgets[pos].Destroy()


        st2 = wx.CheckBox( self, wx.ID_ANY, text )
        self.Bind(wx.EVT_CHECKBOX, self.OnToggle, st2)
        st2.SetValue(True)
        if callback:
            self._callbacks[pos] = callback
        else:
            st2.Enable(False)
        if font:
            st2.SetFont( font )

        if st2.GetGtkWidget():  # better test if we're on gtk, and can't set cb bg color?
            st2.SetForegroundColour( bg )
        else:
            st2.SetBackgroundColour( bg )
            st2.SetForegroundColour( fg )
        self._widgets[pos] = st2
        self.Reposition()
        return st2

    def OnToggle(self, evt):
        #print >> sys.stderr, self._widgets.index(evt.GetEventObject()), '.'
        pos = self._widgets.index( evt.GetEventObject() )
        if pos and self._callbacks[pos]:
            self._callbacks[pos]( pos, self._widgets[pos].GetLabelText(), self._widgets[pos].GetValue() )

    def OnSize(self, evt):
        self.Reposition()  # for normal size events
        # Set a flag so the idle time handler will also do the repositioning.
        # It is done this way to get around a buglet where GetFieldRect is not
        # accurate during the EVT_SIZE resulting from a frame maximize.
        self.sizeChanged = True

    def OnIdle(self, evt):
        if self.sizeChanged:
            self.Reposition()

    # reposition the widgets
    def Reposition(self):
        for pos, widget in enumerate(self._widgets):
            if widget:
                rect = self.GetFieldRect(pos)
                widget.SetPosition((rect.x+2, rect.y+2))
                widget.SetSize((rect.width-4, rect.height-4))
        self.sizeChanged = False

#===========================================================
# A custom search bar to remember searches.
# Derived from demo/ToolBar.py
class MySearchCtrl(wx.SearchCtrl):
    maxSearches = 20
    
    def __init__(self, parent, id=-1, value="",
                 pos=wx.DefaultPosition, size=wx.DefaultSize, style=0,
                 prompt='search', doSearch=None):
        style |= wx.TE_PROCESS_ENTER
        wx.SearchCtrl.__init__(self, parent, id, value, pos, size, style)
        self.ShowCancelButton( True )
        self.Bind(wx.EVT_TEXT_ENTER, self.OnTextEntered)
        self.Bind(wx.EVT_MENU_RANGE, self.OnMenuItem, id=1, id2=self.maxSearches)
        self.Bind( wx.EVT_SEARCHCTRL_CANCEL_BTN, self.OnClear )

        self.SetDescriptiveText( prompt )
        self.doSearch = doSearch
        self.searches = []

    def OnTextEntered(self, evt):
        text = self.GetValue()
        if self.doSearch(text) and len(text) > 0 and (not( text in self.searches)):
            self.searches.append(text)
            if len(self.searches) > self.maxSearches:
                del self.searches[0]
            self.SetMenu(self.MakeMenu())            

    def OnClear(self, evt):
        self.SetValue("")
        self.OnTextEntered( None )

    def OnMenuItem(self, evt):
        text = self.searches[evt.GetId()-1]
        self.SetValue(text)
        self.doSearch(text)
        
    def MakeMenu(self):
        menu = wx.Menu()
        item = menu.Append(-1, "Recent Searches")
        item.Enable(False)
        for idx, txt in enumerate(self.searches):
            menu.Append(1+idx, txt)
        return menu

# http://www.blog.pythonlibrary.org/2008/06/11/wxpython-creating-an-about-box/
# alternate: http://www.daniweb.com/software-development/python/threads/128350/921519#post921519
import wx.html
class AboutDlg(wx.Frame):
 
    def __init__(self, parent, msg):
        wx.Frame.__init__(self, parent, wx.ID_ANY, title="About", size=(700,500))
        html = wxHTML(self, parent)
        html.SetPage( msg )
 
class wxHTML(wx.html.HtmlWindow):
    def __init__(self, parent, grandma):
        wx.html.HtmlWindow.__init__(self, parent)
        self.parent = grandma

    def OnLinkClicked(self, link):
     #   webbrowser.open(link.GetHref())
        # ugly, should make a parent.Search( 'RecNum', link.GetHref() ) ...
        self.parent.cb1.SetStringSelection( 'RecNum' )
        self.parent.data.SetFieldFilterField( 'RecNum' )
        self.parent.sc1.SetValue( link.GetHref() )
        self.parent.sc1.OnTextEntered( None )
        #print >> sys.stderr, link.GetHref()



#===========================================================
# Top level window holding my data grid:
# started from http://stackoverflow.com/questions/1866321/highlight-a-row-in-wxgrid-with-wxpython-when-a-cell-in-that-row-changes-progra

class DataFrame(wx.Frame):
    datafile = None

    def __init__(self):
        wx.Frame.__init__(self, None, size=( wx.DisplaySize()[0], 600 ))

        self.title = 'AFCARS File Linker'
        self.SetTitle( self.title )
        import os
        ip = os.path.join( ScriptPath().d, 'fci.ico' )
        if os.path.isfile( ip ):
            try:
                ib = wx.IconBundle()
                ib.AddIconFromFile( ip, wx.BITMAP_TYPE_ANY )
                self.SetIcons(ib)
            except:
                pass

        # Holds the actual data:
        self.data = GridDataTable()
        # Renders the data in a Grid:
        self.grid = wx.grid.Grid(self)
        self.grid.SetTable(self.data)
        # Small font
        font = self.grid.GetDefaultCellFont()
        font.SetPointSize( 8 )  # fixed and copied throughout
        self.grid.SetDefaultCellFont( font )
        self.data.SetDefaultCellFont( font )

        self.grid.SetColLabelTextOrientation( wx.VERTICAL )
        self.grid.SetColLabelAlignment( wx.ALIGN_LEFT, wx.ALIGN_CENTRE )
        self.grid.SetColLabelSize( 120 ) #wx.GRID_AUTOSIZE )  # fixed at this
        self.grid.SetRowMinimalAcceptableHeight( 0 ) # To hide rows, need to set this to zero?
        self.grid.SetRowLabelSize( 120 )  # fit to data later
        self.grid.SetRowLabelAlignment( wx.ALIGN_RIGHT, wx.ALIGN_CENTRE )

        # No top menu.
        # Create toolbar with filter box.
        self.tb = self.CreateToolBar( wx.TB_HORIZONTAL | wx.RAISED_BORDER )
        #self.tb.SetMarginsXY( 3, 3 )

        tsize = (24,24)
        bmp = wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, tsize)
        self.tb.AddLabelTool( wx.ID_FILE, 'Open', bmp, shortHelp='Open File', longHelp='Add Another AFCARS File to the Linking')
        self.Bind( wx.EVT_TOOL, self.OnOpenFile, id=wx.ID_FILE )
        bmp = wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, tsize)
        self.tb.AddLabelTool( wx.ID_SAVEAS, 'Save As', bmp, shortHelp='Save File', longHelp='Save Linked Records as a CSV or Fixed-Width Text File')
        self.Bind( wx.EVT_TOOL, self.OnSaveAs, id=wx.ID_SAVEAS )
        bmp = wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, tsize)
        self.tb.AddLabelTool( wx.ID_ABOUT, 'About', bmp, shortHelp='Statistics', longHelp='Version and Statistics')
        self.Bind( wx.EVT_TOOL, self.OnAbout, id=wx.ID_ABOUT )
        self.tb.AddSeparator()

        self.cb1 = wx.Choice( self.tb, wx.ID_ANY, size=(1.4*self.grid.GetColLabelSize(),-1), name='Filter Field' )
        self.Bind( wx.EVT_CHOICE, self.OnFieldFilterChoice )
        self.tb.AddControl( self.cb1 )
        self.sc1 = MySearchCtrl( self.tb, size=(200,-1), prompt='filter records', doSearch=self.OnFieldFilter )
        self.tb.AddControl( self.sc1 )

        #self.CreateStatusBar(3) # Statusbar at bottom
        self.sb = MyStatusBar(self, len(self.data.rowclasslabels)+len(self.data.colclasslabels), 170)
        # Add checkbox controls to bottom statusbar.
        font.SetWeight(wx.BOLD)
        pos = 1
        for i in range(0, len(self.data.rowclasslabels)):
            self.sb.SetLabel( pos, self.data.rowclasslabels[i], 'black', self.data.rowclassbgcols[i], callback=self.OnToggleRows, font=font )
            pos = pos + 1
        for i in range(0, len(self.data.colclasslabels)):
            self.sb.SetLabel( pos, self.data.colclasslabels[i], 'black', self.data.colclassbgcols[i], font=font )
            pos = pos + 1
        self.SetStatusBar(self.sb)

        # Copy handler (copy to Excel?)
        self.grid.Bind( wx.EVT_KEY_DOWN, self.OnKey )

        # Data grid fills a sizer for positioning.
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        # don't have to add toolbar to sizer.
        self.sizer.Add(self.grid, 1, wx.EXPAND, 0 )
        self.SetSizer(self.sizer)

        self.tb.Realize()
        self.grid.SetFocus()

    #===========================================================
    # Event Handling
    def OnKey( self, event ):
        if event.ControlDown() and event.GetKeyCode() == 67 and self.grid.IsSelection():
            self.CopyRangeToClipboard()
        # Skip other Key events
        if event.GetKeyCode():
            event.Skip()
            return

    def OnAbout( self, event=None, msg='' ):
        version = '1.2'
        title = '<H2>AFCARS Foster Care File Linker v'+version+'</H2>'
        if self.datafile.filestats and self.data.rowclasslabels:
            fs2 = self.datafile.filestats.ToPct()
            pats = []
            row = 0
            for fn in sorted(fs2.keys()):  # sorting depends on filenames, so unreliable
                for col in range(0, len(fs2[fn])):
                    pct = fs2[fn][col]
                    fs2[fn][col] = ''+str(self.datafile.filestats[fn][col])+' ('+str(pct)+'%)'
                    if row>0 and col==FileStats.ilinks2prior and pct < 20:
                        fs2[fn][col] = '<FONT color=red><B>'+fs2[fn][col]+'</B></FONT>'
                        pats += self.datafile.RareRecnumPats( fn, 10 )
                row += 1
            t1 = dict2html( fs2, ['File Name']+list(FileStats.colnames) )

            if len(pats) > 0:
                l1 = '<P>Potential record number linkage errors. Try these searches:\n<UL>'
                for pat in pats:
                    l1 += '<LI><A href="'+pat+'">'+pat+'</A></LI>\n'
                l1 += '</UL>\n'
            else:
                l1 = ''
        else: 
            t1 = '<P>Select a file first.</P>'
            l1 = ''
        aboutDlg = AboutDlg( self, title+msg+t1+l1 )
        aboutDlg.Show()

    def OnExit( self, event ):
        self.Close(True)  # Close this frame.

    def LinkAlert( self, filenames ):
        # check if any of the just-opened files link to prior files at less than 20%.
        if self.datafile.filestats:
            fs2 = self.datafile.filestats.ToPct()
            if len(fs2) > 1 and filenames:
                for fn in reversed(sorted(fs2.keys())[1:]):
                    if fn in filenames and fs2[fn][FileStats.ilinks2prior] < 20:
                        print >> sys.stderr, 'WARNING: Links in '+fn+' < 20%.'
                        msg = '<H1 align=center><FONT color=red>'
                        msg += 'Potential record number linkage error in file '+fn+'.'
                        msg += '</FONT></H1>'
                        self.OnAbout( msg=msg )
                        break

    def _OpenFile( self, filename, path ):
            self.SetStatusText( 'Examining '+filename+' in '+path )
            print >> sys.stderr, 'reading ', filename, '...',
            self.datafile.ReadFile( filename, updatefunc )
            #self.UpdateValues()  # too slow, just display when done.
            print >> sys.stderr, filename, 'done.'
            self.SetStatusText( filename+' done.' )

    def OnOpenFile( self, event=None ):
        # Open a file
        # patterns like *.[tT]?? don't work on win7, but ';' as sep does.
        dlg = wx.FileDialog(self, "Choose a file to open", os.getcwd(), "", "Fixed Width Text Files (*.txt;*.dat)|*.t??;*.T??;*.d??;*.D??|CSV Files (*.csv)|*.c??;*.C??|Zip Archives (*.zip)|*.z??;*.Z??|All Files|*", wx.OPEN|wx.FD_CHANGE_DIR|wx.FD_MULTIPLE)
        filenames = None
        if dlg.ShowModal() == wx.ID_OK:
            filenames = dlg.GetFilenames()
            for filename in filenames:
                self._OpenFile( filename, dlg.GetDirectory() )
        dlg.Destroy()

        self.UpdateValues()
        self.SetStatusText( 'All Files Done.' )
        print >> sys.stderr, 'All files done.'
        self.SetTitle( self.title+': '+(', '.join(self.datafile.GetFilenames())) )
        self.grid.SetFocus()
        if filenames:
            # check only the files we just opened to avoid redundant messages:
            # (basenames unnecessary with file dialog cwd, just in case)
            self.LinkAlert( (os.path.basename(fn) for fn in filenames) )

    def OnSaveAs( self, event=None ):
        # Open a file
        dlg = wx.FileDialog( self, "Choose a file to save into", os.getcwd(), "", "CSV Files (*.csv)|*.csv|Fixed Width Text Files (*.txt)|*.txt|All Files|*", wx.SAVE|wx.FD_CHANGE_DIR|wx.FD_OVERWRITE_PROMPT )
        if dlg.ShowModal() == wx.ID_OK:
            filename = dlg.GetFilename()
            path = dlg.GetDirectory()
            # Append extension if needed
            if dlg.GetFilterIndex() == 0 and not filename.lower().endswith(".csv"):
                filename = filename + ".csv"
            elif dlg.GetFilterIndex() == 1 and not filename.lower().endswith(".txt"):
                filename = filename + ".txt"
            self.SetStatusText( 'Saving '+filename+' in '+path )
            print >> sys.stderr, 'saving ', filename, '...',
            wx.BeginBusyCursor()
            try:
                self.datafile.SaveFile( filename, updatefunc )
                print >> sys.stderr, filename, 'saved.'
                self.SetStatusText( filename+' saved.' )
            except:
                msg = 'ERROR: '+filename+' save failed. Exception = '+str(sys.exc_info()[1])
                print >> sys.stderr, msg
                self.SetStatusText( msg )
                # should do a popup alert too ...
            if wx.IsBusy():
                wx.EndBusyCursor()
        dlg.Destroy()
        self.grid.SetFocus()

    def OnToggleRows( self, pos, text, value ):
        #print >> sys.stderr, pos, text, value, '.'
        # Could use self.grid.GetSelectedRows() here, then scroll to near same spot after?
        self.SetStatusText( ('Showing ' if value else 'Hiding ')+text+' rows ...' )
        self.grid.BeginBatch()
        # Copying arrays & refreshing much faster than setting row heights to zero, no other row hiding in wx.
        i = self.data.rowclasslabels.index( text )
        self.data.ToggleRows( i, value )
        self.UpdateTable()
        self.grid.EndBatch()
        self.grid.SetFocus()
        #self.grid.ForceRefresh()
        self.SetStatusText( 'Done. '+str(self.data.GetNumberRows())+' records selected.' )

    def OnFieldFilterChoice( self, event ):
        #print >> sys.stderr, 'OnFieldFilterChoice:', event.GetString()
        if self.data.GetFieldFilterField() != event.GetString():
            self.data.SetFieldFilterField( event.GetString() )
            self.sc1.SetValue( '' )
            self.OnFieldFilter( '' )
        self.grid.SetFocus()

    def OnFieldFilter( self, filt ):
        fieldname = self.data.GetFieldFilterField()
        self.SetStatusText( 'Filtering for '+fieldname+' matching "'+filt+'" ...' )
        self.grid.BeginBatch()
        self.data.SetFieldFilter( filt )
        self.data.UpdateRowClasses()
        self.UpdateTable()
        self.grid.EndBatch()
        self.SetStatusText( 'Done. '+str(self.data.GetNumberRows())+' records selected.' )
        self.grid.SetFocus()
        return( True )

    def UpdateTable( self ):
        self.grid.SetTable( self.data )  # best way to totally update grid?
        #self.grid.AutoSize()  # big performance hit
        #print >> sys.stderr, 'TopWindow.data: ', len(self.data._data), len(self.data._data[0])
        # todo: skip this if col sizes already set
        for i, j in enumerate( self.datafile.GetFieldWidths() ):
            #print >> sys.stderr, 'fieldwidth: ', i, j
            self.grid.SetColSize( i, j*7 if j >= 3 else 18 )
        if self.data.GetColLabelValues() != self.cb1.GetItems():
            self.cb1.SetItems( self.data.GetColLabelValues() )
            #print >> sys.stderr, 'idfield: ', self.datafile.fieldnames[self.datafile.idfield]
            self.cb1.SetStringSelection( self.datafile.fieldnames[self.datafile.idfield] )
            self.data.SetFieldFilterField( self.datafile.fieldnames[self.datafile.idfield] )
        #self.grid.AutoSizeRows()  # huge performance hit

    #===========================================================
    # Data Handling
    def UpdateValues( self ):
        if self.datafile and self.datafile.GetNumberRows() > 0 :
            self.grid.BeginBatch()
            self.SetStatusText( 'Sorting for display ...' )
            data2 = self.datafile.Sort()
            self.SetStatusText( 'Done sorting.' )
            #print >> sys.stderr, 'data2: ', len(data2), len(data2[0])
            #fn = self.datafile.GetFieldNames()
            #print >> sys.stderr, 'fieldnames: ', fn
            self.data.SetValues( data2, self.datafile.GetRowNames(), self.datafile.GetFieldNames() )
            self.data.SetFirstField( self.datafile.idfield )
            self.SetStatusText( 'Classifying records ...' )
            self.data.SetRowClasses( self.datafile.ClassifyRows() )
            self.SetStatusText( 'Refreshing display ...' )
            nc = 10
            for i in self.datafile.GetRowNames():
                nc = max( nc, len(i) )
            self.grid.SetRowLabelSize( 6.1*min(nc, 40) )
            self.UpdateTable()
            self.grid.EndBatch()
            #self.grid.ForceRefresh()
            self.SetStatusText( 'Done.' )

    def SetDatafile( self, d0 ):
        self.datafile = d0
        self.UpdateValues()

    # http://wxpython-users.1045709.n5.nabble.com/grid-to-clipboard-to-spreadsheet-td2368594.html
    def CopyRangeToClipboard( self ):
        grid = self.grid
        if not grid.IsSelection(): return
        #print >> sys.stderr, 'CopyRange: ', self.grid.IsSelection(), self.grid.GetSelectionBlockTopLeft(), self.grid.GetSelectionBlockBottomRight() , self.grid.GetSelectedRows() , self.grid.GetSelectedCols()
        if grid.GetSelectionBlockBottomRight():
            RowTopLeft, ColTopLeft = grid.GetSelectionBlockTopLeft()[-1]
            RowBottomRight, ColBottomRight = grid.GetSelectionBlockBottomRight()[-1]
            rows = range(RowTopLeft, RowBottomRight + 1)
            cols = range(ColTopLeft, ColBottomRight + 1)
        elif grid.GetSelectedCols():
            rows = range(grid.GetNumberRows())
            cols = grid.GetSelectedCols()
        else:
            rows = grid.GetSelectedRows()
            cols = range(grid.GetNumberCols())

        self.grid.BeginBatch()
        data = '\t'.join(grid.GetColLabelValue(c) for c in cols) + '\n'
        data += '\n'.join('\t'.join(grid.GetCellValue(r, c) for c in cols) for r in rows)
        clipboard = wx.TextDataObject()
        clipboard.SetText(data)
        self.grid.EndBatch()

        if wx.TheClipboard.Open():
            wx.TheClipboard.SetData(clipboard)
            wx.TheClipboard.Close()
        else:
            wx.MessageBox("Sorry, can't open the clipboard", "Error")


