added filter lists to call history

"Robert Redburn" <[email protected]>
Newsgroups gmane.comp.mobile.bitpim.devel
Message-ID <[email protected]>
I wanted more powerful filtering for call history entries so added filter
lists for each column.  This allows for combinational filtering on any
values present in the data.  I prefer the vertical orientation of the
filters down the right side of the panel but also added a horizontal
option.  I debated removing the folders incoming, outgoing, etc. in the tree
as this sort of makes them obsolete, but instead I've simply made the filter
lists honor the folder selection for now.  I didn't want to mess with
virtual list controls for this initial test so performance on huge call
histories may suffer a little.  Any feedback is welcome, especially
regarding OS's other than windows which I have not tested.

The diff of call_history.py looks a little funny so I also attached my full
copy.  I left the old callhistorylist implementation in tact for now, but it
could be removed if the change is accepted.

-Robert

-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2005.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/

_______________________________________________
BitPim-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/bitpim-devel
history-filters.patch (application/octet-stream, 22.2 KB)
Index: src/call_history.py
===================================================================
--- src/call_history.py	(revision 4539)
+++ src/call_history.py	(working copy)
@@ -64,6 +64,18 @@
 callhistoryobjectfactory=database.dataobjectfactory(CallHistoryDataobject)
 
 #-------------------------------------------------------------------------------
+def GetDurationStr(duration):
+    """convert duration int into an h:mm:ss formatted string"""
+    if duration==None:
+        sec=min=hr=0
+    else:
+        sec=duration%60
+        min=duration/60
+        hr=min/60
+        min=min%60
+    return "%d:%02d:%02d" % (hr, min, sec)
+
+#-------------------------------------------------------------------------------
 class CallHistoryEntry(object):
     Folder_Incoming='Incoming'
     Folder_Outgoing='Outgoing'
@@ -164,6 +176,17 @@
         self._set_or_del(self._duration_key, v)
     duration=property(fget=_get_duration, fset=_set_duration)
 
+##    def get_duration_str(self, dur = self._get_duration()):
+##        """convert duration int into an h:mm:ss formatted string"""
+##        if dur == None:
+##            sec = min = hr = 0
+##        else:
+##            sec = dur%60
+##            min = dur/60
+##            hr = min/60
+##            min = min%60
+##        return "%d:%02d:%02d" % (hr, min, sec)        
+
     def _get_datetime(self):
         return self._data.get(self._datetime_key, '')
     def _set_datetime(self, v):
@@ -196,6 +219,7 @@
         else:
             s=s[:4]+'-'+s[4:6]+'-'+s[6:8]+' '+s[9:11]+':'+s[11:13]+':'+s[13:]
         return s
+
     def summary(self, name=None):
         # return a short summary for this entry in the format of
         # MM/DD hh:mm <Number/Name>
@@ -216,7 +240,7 @@
 class CallHistoryWidget(scrolled.ScrolledPanel, widgets.BitPimWidget):
     _data_key='call_history'
     stat_list=("Data", "Missed", "Incoming", "Outgoing", "All")
-    def __init__(self, mainwindow, parent):
+    def __init__(self, mainwindow, parent, showFilters = True):
         super(CallHistoryWidget, self).__init__(parent, -1)
         self._main_window=mainwindow
         self.call_history_tree_nodes={}
@@ -226,7 +250,13 @@
         self._data={}
         self._name_map={}
         pubsub.subscribe(self._OnPBLookup, pubsub.RESPONSE_PB_LOOKUP)
-        self.list_widget=CallHistoryList(self._main_window, self._parent, self)
+        self.showFilters = showFilters
+
+        if self.showFilters:
+            self.list_widget=CallHistoryListWithFilters(self._main_window, self._parent, self, True)
+        else:
+            self.list_widget=CallHistoryList(self._main_window, self._parent, self)
+
         # main box sizer
         vbs=wx.BoxSizer(wx.VERTICAL)
         # data date adjuster
@@ -341,6 +371,8 @@
                 if not self._name_map.has_key(e.number):
                     pubsub.publish(pubsub.REQUEST_PB_LOOKUP,
                                    { 'item': e.number } )
+        if self.showFilters:
+            self.list_widget.populateFilters()
         self.list_widget.populate()
         #update stats
         self.CalculateStats()
@@ -373,18 +405,11 @@
         self.total_out.SetLabel('  Outgoing Calls: '+`out`)
         self.total_missed.SetLabel('  Missed Calls: '+`miss`)
         self.total_data.SetLabel('  Data Calls: '+`data`)
-        self.duration_all.SetLabel('  Total Duration(h:m:s): '+self.GetDurationStr(total_d))
-        self.duration_in.SetLabel('  Incoming Duration(h:m:s): '+self.GetDurationStr(in_d))
-        self.duration_out.SetLabel('  Outgoing Duration(h:m:s): '+self.GetDurationStr(out_d))
-        self.duration_data.SetLabel('  Data Duration(h:m:s): '+self.GetDurationStr(data_d))
+        self.duration_all.SetLabel('  Total Duration(h:m:s): '+GetDurationStr(total_d))
+        self.duration_in.SetLabel('  Incoming Duration(h:m:s): '+GetDurationStr(in_d))
+        self.duration_out.SetLabel('  Outgoing Duration(h:m:s): '+GetDurationStr(out_d))
+        self.duration_data.SetLabel('  Data Duration(h:m:s): '+GetDurationStr(data_d))
             
-    def GetDurationStr(self, duration):
-        sec=duration%60
-        min=duration/60
-        hr=min/60
-        min=min%60
-        return "%d:%02d:%02d" % (hr, min, sec)
-
     def _OnPBLookup(self, msg):
         d=msg.data
         k=d.get('item', None)
@@ -481,7 +506,7 @@
         column_info.append(("Call Type", 80, False))
         column_info.append(("Date", 120, False))
         column_info.append(("Number", 100, False))
-        column_info.append(("Duration", 80, True))
+        column_info.append(("Duration", 80, False))
         column_info.append(("Name", 130, False))
         self._item_list=guiwidgets.BitPimListCtrl(self, column_info)
         self._item_list.ResetView(self.nodes, self.nodes_keys)
@@ -568,10 +593,267 @@
         index=0
         for k,e in self._stats._data.items():
             if self._display_filter=="All" or e.folder==self._display_filter:
-                if e.duration==None:
-                    duration=0
+                name=e.name
+                if name==None or name=="":
+                    temp=self._stats._name_map.get(e.number, None)
+                    if temp !=None:
+                        name=temp
+                    else:
+                        name=""
+                self.nodes[index]=(e.folder, e.get_date_time_str(),
+                                   phonenumber.format(e.number),
+                                   GetDurationStr(e.duration), name)
+                self.nodes_keys[index]=k
+                index+=1
+        self._item_list.ResetView(self.nodes, self.nodes_keys)
+
+    def CanDelete(self):
+        if self._stats.read_only:
+            return False
+        sels_idx=self._item_list.GetFirstSelected()
+        if sels_idx==-1:
+            return False
+        return True
+
+    def GetDeleteInfo(self):
+        return wx.ART_DEL_BOOKMARK, "Delete Call Record"
+
+    def OnDelete(self, _):
+        if self._stats.read_only:
+            return
+        sels_idx=self._item_list.GetSelections()
+        if len(sels_idx):
+            # delete them from the data list
+            for i,item in sels_idx.items():
+                del self._stats._data[self._item_list.GetItemData(item)]
+                self._item_list.Select(item, 0)
+            self.populate()
+            self._stats._save_to_db(self._stats._data)
+            self._stats.CalculateStats()
+    def GetHelpID(self):
+        return helpids.ID_TAB_CALLHISTORY
+
+
+#-------------------------------------------------------------------------------
+# adds full combinational filtering of history data
+#-------------------------------------------------------------------------------
+class CallHistoryListWithFilters(wx.Panel, widgets.BitPimWidget):
+    """adds filter lists for each call history column"""
+    _by_type=0
+    _by_date=1
+    _by_number=2
+    def __init__(self, mainwindow, parent, stats, filtersVertical = True):
+        super(CallHistoryListWithFilters, self).__init__(parent, -1)
+        self._main_window=mainwindow
+        self._stats=stats
+        self.nodes={}
+        self.nodes_keys={}
+        self._display_filter="All"
+        # main box sizer
+        vbs=wx.BoxSizer(wx.VERTICAL)
+        # data date adjuster
+        hbs=wx.BoxSizer(wx.HORIZONTAL)
+        static_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1,
+                                                 'Historical Data Status (EXP):'),
+                                    wx.VERTICAL)
+        self.historical_data_label=wx.StaticText(self, -1, 'Current Data')
+        static_bs.Add(self.historical_data_label, 1, wx.EXPAND|wx.ALL, 5)
+        hbs.Add(static_bs, 1, wx.EXPAND|wx.ALL, 5)
+        vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 0)
+
+        column_info=[]                                                  
+        column_info.append(("Call Type", 80, False))
+        column_info.append(("Date", 120, False))
+        column_info.append(("Number", 100, False))
+        column_info.append(("Duration", 80, False))
+        column_info.append(("Name", 130, False))
+
+        # filter lists
+        filterHeight=80
+        
+        self._type_list=guiwidgets.FilterListCtrl(self, [column_info[0]])
+        fsize = (column_info[0][1]+10,filterHeight)
+        self._type_list.SetMinSize(fsize) # set in case horizontal orient
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnFilterChanged, self._type_list)
+        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnFilterChanged, self._type_list)
+        self.Bind(wx.EVT_LIST_INSERT_ITEM, self.OnFilterChanged, self._type_list)
+        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnFilterChanged, self._type_list)
+
+        self._date_list=guiwidgets.FilterListCtrl(self, [column_info[1],])
+        fsize = (column_info[1][1]+20,filterHeight)
+        self._date_list.SetMinSize(fsize)
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnFilterChanged, self._date_list)
+        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnFilterChanged, self._date_list)
+        self.Bind(wx.EVT_LIST_INSERT_ITEM, self.OnFilterChanged, self._date_list)
+        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnFilterChanged, self._date_list)
+
+        self._number_list=guiwidgets.FilterListCtrl(self, [column_info[2],])
+        fsize = (column_info[2][1]+10,filterHeight)
+        self._number_list.SetMinSize(fsize)
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnFilterChanged, self._number_list)
+        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnFilterChanged, self._number_list)
+        self.Bind(wx.EVT_LIST_INSERT_ITEM, self.OnFilterChanged, self._number_list)
+        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnFilterChanged, self._number_list)
+        
+        self._duration_list=guiwidgets.FilterListCtrl(self, [column_info[3],])
+        fsize = (column_info[3][1]+10,filterHeight)
+        self._duration_list.SetMinSize(fsize)
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnFilterChanged, self._duration_list)
+        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnFilterChanged, self._duration_list)
+        self.Bind(wx.EVT_LIST_INSERT_ITEM, self.OnFilterChanged, self._duration_list)
+        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnFilterChanged, self._duration_list)
+
+        self._name_list = guiwidgets.FilterListCtrl(self, [column_info[4]])
+        fsize = (column_info[4][1]+10,filterHeight)
+        self._name_list.SetMinSize(fsize)
+        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnFilterChanged, self._name_list)
+        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnFilterChanged, self._name_list)
+        self.Bind(wx.EVT_LIST_INSERT_ITEM, self.OnFilterChanged, self._name_list)
+        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnFilterChanged, self._name_list)
+
+        filter_reset_button = wx.Button(self, -1, "Clear Filters")
+        self.Bind(wx.EVT_BUTTON, self.OnClearFilters, filter_reset_button)
+
+        # main list                                                  
+        self._item_list=guiwidgets.BitPimListCtrl(self, column_info)
+        self._item_list.ResetView(self.nodes, self.nodes_keys)
+
+        if filtersVertical:
+            filter_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Filters:"), wx.VERTICAL)
+            filter_bs.Add(filter_reset_button, 0, wx.EXPAND|wx.ALL, 5)
+            filter_bs.Add(self._type_list, 1, wx.EXPAND|wx.ALL, 5)
+            filter_bs.Add(self._date_list, 1, wx.EXPAND|wx.ALL, 5)
+            filter_bs.Add(self._number_list, 1, wx.EXPAND|wx.ALL, 5)
+            filter_bs.Add(self._duration_list, 1, wx.EXPAND|wx.ALL, 5)
+            filter_bs.Add(self._name_list, 1, wx.EXPAND|wx.ALL, 5)
+            list_hb=wx.BoxSizer(wx.HORIZONTAL)
+            list_hb.Add(self._item_list, 1, wx.EXPAND|wx.ALL, 5)
+            list_hb.Add(filter_bs, 0, wx.EXPAND|wx.ALL, 5)
+            vbs.Add(list_hb, 1, wx.EXPAND|wx.ALL, 0)
+        else:
+            filter_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Filters:"), wx.VERTICAL)
+            filter_hs=wx.BoxSizer(wx.HORIZONTAL)
+            filter_bs.Add(filter_reset_button, 0, wx.ALL, 5)
+            filter_bs.Add(filter_hs, 1, wx.EXPAND|wx.ALL, 0)
+            filter_hs.Add(self._type_list, 0, wx.ALL, 5)
+            filter_hs.Add(self._date_list, 0, wx.EXPAND|wx.ALL, 5)
+            filter_hs.Add(self._number_list, 0, wx.ALL, 5)
+            filter_hs.Add(self._duration_list, 0, wx.ALL, 5)
+            filter_hs.Add(self._name_list, 0, wx.ALL, 5)
+            vbs.Add(filter_bs, 0, wx.EXPAND|wx.ALL, 5)
+            vbs.Add(self._item_list, 1, wx.EXPAND|wx.ALL, 5)
+
+        vbs.Add(wx.StaticText(self, -1, '  Note: Click column headings to sort data'), 0, wx.ALIGN_CENTRE|wx.BOTTOM, 10)
+
+        # all done
+        today.bind_notification_event(self.OnTodaySelectionIncoming,
+                                      today.Today_Group_IncomingCalls)
+        today.bind_notification_event(self.OnTodaySelectionMissed,
+                                      today.Today_Group_MissedCalls)
+        self.today_data=None
+        self.SetSizer(vbs)
+        self.SetAutoLayout(True)
+        vbs.Fit(self)
+
+    def OnSelected(self, node):
+        for stat in self._stats.stat_list:
+            if self._stats.call_history_tree_nodes[stat]==node:
+                if self._display_filter!=stat:
+                    self._display_filter=stat
+                    # for some reason GetTopItem return 0 (instead of -1)
+                    # when the list is empty
+                    if self._item_list.GetItemCount():
+                        item=self._item_list.GetTopItem()
+                        # deselect all the items when changing view
+                        while item!=-1:
+                            self._item_list.Select(item, 0)
+                            item=self._item_list.GetNextItem(item)
+                    self.populateFilters()
+                    self.populate()
+                self._on_today_selection()
+                return
+
+    def OnTodaySelectionIncoming(self, evt):
+        node=self._stats.call_history_tree_nodes["Incoming"]
+        self.today_data=evt.data
+        self.ActivateSelf(node)
+
+    def OnTodaySelectionMissed(self, evt):
+        node=self._stats.call_history_tree_nodes["Missed"]
+        self.today_data=evt.data
+        self.ActivateSelf(node)
+
+    def _on_today_selection(self):
+        if self.today_data and self._item_list.GetItemCount():
+            item=self._item_list.GetTopItem()
+            while item!=-1:
+                if self.today_data['id']==self._item_list.GetItemData(item):
+                    self._item_list.Select(item, 1)
+                    self._item_list.EnsureVisible(item)
                 else:
-                    duration=e.duration
+                    self._item_list.Select(item, 0)
+                item=self._item_list.GetNextItem(item)
+        self.today_data=None
+
+    def GetRightClickMenuItems(self, node):
+        result=[]
+        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EDITSELECTALL, "Select All", "Select All Items"))
+        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EDITDELETEENTRY, "Delete Selected", "Delete Selected Items"))
+        result.append((widgets.BitPimWidget.MENU_SPACER, 0, "", ""))
+        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EXPORT_CSV_CALL_HISTORY, "Export to CSV ...", "Export the call history to a csv file"))
+        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_DATAHISTORICAL, "Historical Data ...", "Display Historical Data"))
+        return result
+
+    def CanSelectAll(self):
+        if self._item_list.GetItemCount():
+            return True
+        return False
+
+    def OnSelectAll(self, _):
+        item=self._item_list.GetTopItem()
+        while item!=-1:
+            self._item_list.Select(item)
+            item=self._item_list.GetNextItem(item)
+
+    def HasHistoricalData(self):
+        return self._stats.HasHistoricalData()
+
+    def OnHistoricalData(self):
+        return self._stats.OnHistoricalData()
+
+    def populateFilters(self):
+        """populates the filter lists.  I think this needs to be done separate from
+           populate() since when a filter selection state changes populate gets called.
+           We don't want to re-populate the filter list everytime a filter selection is 
+           made.  
+        """
+        self._type_list.DeleteAllItems()
+        self._date_list.DeleteAllItems()
+        self._number_list.DeleteAllItems()
+        self._duration_list.DeleteAllItems()
+        self._name_list.DeleteAllItems()
+        for k,e in self._stats._data.items():
+            if self._display_filter != "All" and e.folder != self._display_filter:
+                continue
+            self._type_list.InsertStringItem(self._type_list.GetItemCount(), e.folder)
+            self._date_list.InsertStringItem(self._date_list.GetItemCount(), e.get_date_time_str()[:10]) #no time
+            self._number_list.InsertStringItem(self._number_list.GetItemCount(), phonenumber.format(e.number))
+            self._duration_list.InsertStringItem(self._duration_list.GetItemCount(),GetDurationStr(e.duration)[:4]) #no seconds
+            name=e.name
+            if name==None or name=="":
+                temp=self._stats._name_map.get(e.number, None)
+                if temp !=None:
+                    name=temp
+                else:
+                    name=""
+            self._name_list.InsertStringItem(self._name_list.GetItemCount(), name)
+    
+    def populate(self):
+        self.nodes={}
+        self.nodes_keys={}
+        index=0
+        for k,e in self._stats._data.items():
+            if self._display_filter=="All" or e.folder==self._display_filter:
                 name=e.name
                 if name==None or name=="":
                     temp=self._stats._name_map.get(e.number, None)
@@ -579,13 +861,35 @@
                         name=temp
                     else:
                         name=""
+                # check filters, needs to happen after name lookup
+                if name not in self._name_list.getSelectedOrAll() or \
+                   e.folder not in self._type_list.getSelectedOrAll() or \
+                   phonenumber.format(e.number) not in self._number_list.getSelectedOrAll() or \
+                   GetDurationStr(e.duration)[:4] not in self._duration_list.getSelectedOrAll() or \
+                   e.get_date_time_str()[:10] not in self._date_list.getSelectedOrAll():
+                    continue
+                
+                # entry passed all filters, add to display
                 self.nodes[index]=(e.folder, e.get_date_time_str(),
                                    phonenumber.format(e.number),
-                                   `duration`, name)
+                                   GetDurationStr(e.duration), name)
                 self.nodes_keys[index]=k
                 index+=1
         self._item_list.ResetView(self.nodes, self.nodes_keys)
 
+    def OnFilterChanged(self, event):
+        """a filter list selection or inser/delete event occurred, update display"""
+        self.populate()
+        
+    def OnClearFilters(self, event):
+        """reset any filter selections, populate() gets called automatically
+           by filter events fired as selection status changes"""
+        self._type_list.clearAllSelections()
+        self._date_list.clearAllSelections()
+        self._name_list.clearAllSelections()
+        self._number_list.clearAllSelections()
+        self._duration_list.clearAllSelections()        
+        
     def CanDelete(self):
         if self._stats.read_only:
             return False
@@ -606,8 +910,13 @@
             for i,item in sels_idx.items():
                 del self._stats._data[self._item_list.GetItemData(item)]
                 self._item_list.Select(item, 0)
+            self.populateFilters()
             self.populate()
             self._stats._save_to_db(self._stats._data)
             self._stats.CalculateStats()
+
     def GetHelpID(self):
         return helpids.ID_TAB_CALLHISTORY
+
+
+        
Index: src/guiwidgets.py
===================================================================
--- src/guiwidgets.py	(revision 4539)
+++ src/guiwidgets.py	(working copy)
@@ -1823,6 +1823,55 @@
                                     k)
 
 
+
+
+class FilterListCtrl(wx.ListCtrl):
+    """list control with methods to assist with holding unique filter strings"""
+    def __init__(self, parent, column_info):
+        self.lcparent=parent
+        wx.ListCtrl.__init__(self, self.lcparent, wx.NewId(), 
+                             style=wx.LC_REPORT|wx.LC_SORT_ASCENDING) #|wx.LC_VIRTUAL)
+        index=0
+        for info in column_info:
+            text, width, int_sort=info
+            self.InsertColumn(index, text, width=width)
+            index+=1
+        self.font=wx.TheFontList.FindOrCreateFont(10, family=wx.SWISS, 
+                  style=wx.NORMAL, weight=wx.NORMAL)
+
+    def InsertStringItem(self, idx, strval):
+        """inserts a string only if it doesn't already exist in the list.
+           returns the index of the inserted item or -1 if not added"""
+        if self.FindItem(-1, strval) < 0:
+            return wx.ListCtrl.InsertStringItem(self, idx, strval)
+        else:
+            return -1
+ 
+    def getAllItems(self, state = wx.LIST_STATE_DONTCARE):
+        """get all items with a given state, defaults to everything"""
+        items = []
+        itemIdx = self.GetNextItem(-1, wx.LIST_NEXT_ALL, state)
+        while itemIdx != -1:
+            items.append(self.GetItemText(itemIdx))
+            itemIdx = self.GetNextItem(itemIdx, wx.LIST_NEXT_ALL, state)
+        return items
+        
+    def getSelectedOrAll(self):
+        """returns a list of all selected items or if nothing is selected
+           it returns all items"""
+        if self.GetSelectedItemCount() == 0:
+            return self.getAllItems()
+        else:
+            return self.getAllItems(wx.LIST_STATE_SELECTED)
+        
+    def clearAllSelections(self):
+        """clear selection status of all items"""
+        itemIdx = self.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
+        while itemIdx != -1:
+            self.SetItemState(itemIdx, 0, wx.LIST_STATE_SELECTED)
+            itemIdx = self.GetNextItem(itemIdx, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
+        
+
 class BitPimListCtrl(wx.ListCtrl, listmix.ColumnSorterMixin):
     def __init__(self, parent, column_info):
         self.lcparent=parent
call_history.py (text/x-python, 38.8 KB)
### BITPIM
###
### Copyright (C) 2005 Joe Pham <[email protected]>
###
### This program is free software; you can redistribute it and/or modify
### it under the terms of the BitPim license as detailed in the LICENSE file.
###
### $Id: call_history.py 4374 2007-08-23 03:14:24Z djpham $

"""
Code to handle Call History data storage and display.

The format of the Call History is standardized.  It is an object with the
following attributes:

folder: string (where this item belongs)
datetime: string 'YYYYMMDDThhmmss' or (y,m,d,h,m,s)
number: string (the phone number of this call)
name: string (optional name associated with this number)
duration: int (optional duration of the call in minutes)

To implement Call History feature for a phone module:

  Add an entry into Profile._supportedsyncs:
  ('call_history', 'read', None),

  Implement the following method in your Phone class:
  def getcallhistory(self, result, merge):
     ...
     return result

The result dict key is 'call_history'.

"""

# standard modules
from __future__ import with_statement
import copy
import sha
import time

# wx modules
import wx
import wx.lib.scrolledpanel as scrolled

# BitPim modules
import database
import guiwidgets
import guihelper
import helpids
import phonenumber
import pubsub
import today
import widgets

#-------------------------------------------------------------------------------
class CallHistoryDataobject(database.basedataobject):
    _knownproperties=['folder', 'datetime', 'number', 'name', 'duration' ]
    _knownlistproperties=database.basedataobject._knownlistproperties.copy()
    def __init__(self, data=None):
        if data is None or not isinstance(data, CallHistoryEntry):
            return;
        self.update(data.get_db_dict())
callhistoryobjectfactory=database.dataobjectfactory(CallHistoryDataobject)

#-------------------------------------------------------------------------------
def GetDurationStr(duration):
    """convert duration int into an h:mm:ss formatted string"""
    if duration==None:
        sec=min=hr=0
    else:
        sec=duration%60
        min=duration/60
        hr=min/60
        min=min%60
    return "%d:%02d:%02d" % (hr, min, sec)

#-------------------------------------------------------------------------------
class CallHistoryEntry(object):
    Folder_Incoming='Incoming'
    Folder_Outgoing='Outgoing'
    Folder_Missed='Missed'
    Folder_Data='Data'
    Valid_Folders=(Folder_Incoming, Folder_Outgoing, Folder_Missed, Folder_Data)
    _folder_key='folder'
    _datetime_key='datetime'
    _number_key='number'
    _name_key='name'
    _duration_key='duration'
    _unknown_datetime='YYYY-MM-DD hh:mm:ss'
    _id_index=0
    _max_id_index=999
    def __init__(self):
        self._data={ 'serials': [] }
        self._create_id()

    def __eq__(self, rhs):
        return self.folder==rhs.folder and self.datetime==rhs.datetime and\
               self.number==rhs.number
    def __ne__(self, rhs):
        return self.folder!=rhs.folder or self.datetime!=rhs.datetime or\
               self.number!=rhs.number
    def get(self):
        return copy.deepcopy(self._data, {})
    def set(self, d):
        self._data={}
        self._data.update(d)

    def get_db_dict(self):
        return self.get()
    def set_db_dict(self, d):
        self.set(d)

    def _create_id(self):
        "Create a BitPim serial for this entry"
        self._data.setdefault("serials", []).append(\
            {"sourcetype": "bitpim",
             "id": '%.3f%03d'%(time.time(), CallHistoryEntry._id_index) })
        if CallHistoryEntry._id_index<CallHistoryEntry._max_id_index:
            CallHistoryEntry._id_index+=1
        else:
            CallHistoryEntry._id_index=0
    def _get_id(self):
        s=self._data.get('serials', [])
        for n in s:
            if n.get('sourcetype', None)=='bitpim':
                return n.get('id', None)
        return None
    def _set_id(self, id):
        s=self._data.get('serials', [])
        for n in s:
            if n.get('sourcetype', None)=='bitpim':
                n['id']=id
                return
        self._data['serials'].append({'sourcetype': 'bitpim', 'id': id } )
    id=property(fget=_get_id, fset=_set_id)

    def _set_or_del(self, key, v, v_list=[]):
        if v is None or v in v_list:
            if self._data.has_key(key):
                del self._data[key]
        else:
            self._data[key]=v

    def _get_folder(self):
        return self._data.get(self._folder_key, '')
    def _set_folder(self, v):
        if v is None:
            if self._data.has_key(self._folder_key):
                del self._data[self._folder_key]
                return
        if not isinstance(v, (str, unicode)):
            raise TypeError,'not a string or unicode type'
        if v not in self.Valid_Folders:
            raise ValueError,'not a valid folder'
        self._data[self._folder_key]=v
    folder=property(fget=_get_folder, fset=_set_folder)

    def _get_number(self):
        return self._data.get(self._number_key, '')
    def _set_number(self, v):
        self._set_or_del(self._number_key, v, [''])
    number=property(fget=_get_number, fset=_set_number)

    def _get_name(self):
        return self._data.get(self._name_key, '')
    def _set_name(self, v):
        self._set_or_del(self._name_key, v, ('',))
    name=property(fget=_get_name, fset=_set_name)

    def _get_duration(self):
        return self._data.get(self._duration_key, None)
    def _set_duration(self, v):
        if v is not None and not isinstance(v, int):
            raise TypeError('duration property is an int arg')
        self._set_or_del(self._duration_key, v)
    duration=property(fget=_get_duration, fset=_set_duration)

##    def get_duration_str(self, dur = self._get_duration()):
##        """convert duration int into an h:mm:ss formatted string"""
##        if dur == None:
##            sec = min = hr = 0
##        else:
##            sec = dur%60
##            min = dur/60
##            hr = min/60
##            min = min%60
##        return "%d:%02d:%02d" % (hr, min, sec)        

    def _get_datetime(self):
        return self._data.get(self._datetime_key, '')
    def _set_datetime(self, v):
        # this routine supports 2 formats:
        # (y,m,d,h,m,s) and 'YYYYMMDDThhmmss'
        # check for None and delete manually
        if v is None:
            if self._data.has_key(self._datetime_key):
                del self._data[self._datetime_key]
            return
        if isinstance(v, (tuple, list)):
            if len(v)!=6:
                raise ValueError,'(y, m, d, h, m, s)'
            s='%04d%02d%02dT%02d%02d%02d'%tuple(v)
        elif isinstance(v, (str, unicode)):
            # some primitive validation
            if len(v)!=15 or v[8]!='T':
                raise ValueError,'value must be in format YYYYMMDDThhmmss'
            s=v
        else:
            raise TypeError
        self._data[self._datetime_key]=s
    datetime=property(fget=_get_datetime, fset=_set_datetime)
    def get_date_time_str(self):
        # return a string representing this date/time in the format of
        # YYYY-MM-DD hh:mm:ss
        s=self.datetime
        if not len(s):
            s=self._unknown_datetime
        else:
            s=s[:4]+'-'+s[4:6]+'-'+s[6:8]+' '+s[9:11]+':'+s[11:13]+':'+s[13:]
        return s

    def summary(self, name=None):
        # return a short summary for this entry in the format of
        # MM/DD hh:mm <Number/Name>
        s=self.datetime
        if s:
            s=s[4:6]+'/'+s[6:8]+' '+s[9:11]+':'+s[11:13]+' '
        else:
            s='**/** **:** '
        if name:
            s+=name
        elif self.name:
            s+=self.name
        else:
            s+=phonenumber.format(self.number)
        return s

#-------------------------------------------------------------------------------
class CallHistoryWidget(scrolled.ScrolledPanel, widgets.BitPimWidget):
    _data_key='call_history'
    stat_list=("Data", "Missed", "Incoming", "Outgoing", "All")
    def __init__(self, mainwindow, parent, showFilters = True):
        super(CallHistoryWidget, self).__init__(parent, -1)
        self._main_window=mainwindow
        self.call_history_tree_nodes={}
        self._parent=parent
        self.read_only=False
        self.historical_date=None
        self._data={}
        self._name_map={}
        pubsub.subscribe(self._OnPBLookup, pubsub.RESPONSE_PB_LOOKUP)
        self.showFilters = showFilters

        if self.showFilters:
            self.list_widget=CallHistoryListWithFilters(self._main_window, self._parent, self, True)
        else:
            self.list_widget=CallHistoryList(self._main_window, self._parent, self)

        # main box sizer
        vbs=wx.BoxSizer(wx.VERTICAL)
        # data date adjuster
        hbs=wx.BoxSizer(wx.HORIZONTAL)
        static_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1,
                                                 'Historical Data Status:'),
                                    wx.VERTICAL)
        self.historical_data_label=wx.StaticText(self, -1, 'Current Data')
        static_bs.Add(self.historical_data_label, 1, wx.EXPAND|wx.ALL, 5)
        hbs.Add(static_bs, 1, wx.EXPAND|wx.ALL, 5)
        vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
        # main stats display
        self.total_calls=wx.StaticText(self, -1, '  Total Calls: 0')
        self.total_in=wx.StaticText(self, -1, '  Incoming Calls: 0')
        self.total_out=wx.StaticText(self, -1, '  Outgoing Calls: 0')
        self.total_missed=wx.StaticText(self, -1, '  Missed Calls: 0')
        self.total_data=wx.StaticText(self, -1, '  Data Calls: 0')
        self.duration_all=wx.StaticText(self, -1, '  Total Duration(h:m:s): 0')
        self.duration_in=wx.StaticText(self, -1, '  Incoming Duration(h:m:s): 0')
        self.duration_out=wx.StaticText(self, -1, '  Outgoing Duration(h:m:s): 0')
        self.duration_data=wx.StaticText(self, -1, '  Data Duration(h:m:s): 0')
        vbs.Add(wx.StaticText(self, -1, ''), 0, wx.ALIGN_LEFT|wx.ALL, 2)
        vbs.Add(self.total_calls, 0, wx.ALIGN_LEFT|wx.ALL, 2)
        vbs.Add(self.total_in, 0, wx.ALIGN_LEFT|wx.ALL, 2)
        vbs.Add(self.total_out, 0, wx.ALIGN_LEFT|wx.ALL, 2)
        vbs.Add(self.total_missed, 0, wx.ALIGN_LEFT|wx.ALL, 2)
        vbs.Add(self.total_data, 0, wx.ALIGN_LEFT|wx.ALL, 2)
        vbs.Add(wx.StaticText(self, -1, ''), 0, wx.ALIGN_LEFT|wx.ALL, 2)
        vbs.Add(self.duration_all, 0, wx.ALIGN_LEFT|wx.ALL, 2)
        vbs.Add(self.duration_in, 0, wx.ALIGN_LEFT|wx.ALL, 2)
        vbs.Add(self.duration_out, 0, wx.ALIGN_LEFT|wx.ALL, 2)
        vbs.Add(self.duration_data, 0, wx.ALIGN_LEFT|wx.ALL, 2)
        # all done
        self.SetSizer(vbs)
        self.SetAutoLayout(True)
        vbs.Fit(self)
        self.SetupScrolling()
        self.SetBackgroundColour(wx.WHITE)
        # populate data
        self._populate()

    def populate(self, dict, force=False):
        if self.read_only and not force:
            # historical data, bail
            return
        self._data=dict.get(self._data_key, {})
        self._populate()

    def OnInit(self):
        for stat in self.stat_list:
            self.call_history_tree_nodes[stat]=self.AddSubPage(self.list_widget, stat, self._tree.calls)

    def GetRightClickMenuItems(self, node):
        result=[]
        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EXPORT_CSV_CALL_HISTORY, "Export to CSV ...", "Export the call history to a csv file"))
        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_DATAHISTORICAL, "Historical Data ...", "Display Historical Data"))
        return result

    def HasHistoricalData(self):
        return True

    def OnHistoricalData(self):
        """Display current or historical data"""
        if self.read_only:
            current_choice=guiwidgets.HistoricalDataDialog.Historical_Data
        else:
            current_choice=guiwidgets.HistoricalDataDialog.Current_Data
        with guihelper.WXDialogWrapper(guiwidgets.HistoricalDataDialog(self,
                                                                       current_choice=current_choice,
                                                                       historical_date=self.historical_date,
                                                                       historical_events=\
                                                                       self._main_window.database.getchangescount(self._data_key)),
                                       True) as (dlg, retcode):
            if retcode==wx.ID_OK:
                with guihelper.MWBusyWrapper(self._main_window):
                    current_choice, self.historical_date=dlg.GetValue()
                    r={}
                    if current_choice==guiwidgets.HistoricalDataDialog.Current_Data:
                        self.read_only=False
                        msg_str='Current Data'
                        self.getfromfs(r)
                    else:
                        self.read_only=True
                        msg_str='Historical Data as of %s'%\
                                 str(wx.DateTimeFromTimeT(self.historical_date))
                        self.getfromfs(r, self.historical_date)
                    self.populate(r, True)
                    self.historical_data_label.SetLabel(msg_str)
                    self.list_widget.historical_data_label.SetLabel(msg_str)

    def _publish_today_data(self):
        keys=[(x.datetime, k) for k,x in self._data.items()]
        keys.sort()
        keys.reverse()
        today_event_in=today.TodayIncomingCallsEvent()
        today_event_miss=today.TodayMissedCallsEvent()
        for _,k in keys:
            if self._data[k].folder==CallHistoryEntry.Folder_Incoming:
                today_event_in.append(self._data[k].summary(self._name_map.get(self._data[k].number, None)), {'id':k})
            if self._data[k].folder==CallHistoryEntry.Folder_Missed:
                today_event_miss.append(self._data[k].summary(self._name_map.get(self._data[k].number, None)), {'id':k})
        today_event_in.broadcast()
        today_event_miss.broadcast()

    def _populate(self):
        # lookup phone book for names
        for k,e in self._data.items():
            if e.name:
                if not self._name_map.has_key(e.number):
                    self._name_map[e.number]=e.name
            else:
                if not self._name_map.has_key(e.number):
                    pubsub.publish(pubsub.REQUEST_PB_LOOKUP,
                                   { 'item': e.number } )
        if self.showFilters:
            self.list_widget.populateFilters()
        self.list_widget.populate()
        #update stats
        self.CalculateStats()
        # tell today 'bout it
        self._publish_today_data()

    def CalculateStats(self):
        total=inc=out=miss=data=0
        total_d=in_d=out_d=data_d=0
        for k, e in self._data.items():
            total+=1
            if e.duration==None:
                dur=0
            else:
                dur=e.duration
            total_d+=dur
            if e.folder==CallHistoryEntry.Folder_Incoming:
                inc+=1
                in_d+=dur
            elif e.folder==CallHistoryEntry.Folder_Outgoing:
                out+=1
                out_d+=dur
            elif e.folder==CallHistoryEntry.Folder_Missed:
                miss+=1
            elif e.folder==CallHistoryEntry.Folder_Data:
                data+=1
                data_d+=dur
        self.total_calls.SetLabel('  Total Calls: '+`total`)
        self.total_in.SetLabel('  Incoming Calls: '+`inc`)
        self.total_out.SetLabel('  Outgoing Calls: '+`out`)
        self.total_missed.SetLabel('  Missed Calls: '+`miss`)
        self.total_data.SetLabel('  Data Calls: '+`data`)
        self.duration_all.SetLabel('  Total Duration(h:m:s): '+GetDurationStr(total_d))
        self.duration_in.SetLabel('  Incoming Duration(h:m:s): '+GetDurationStr(in_d))
        self.duration_out.SetLabel('  Outgoing Duration(h:m:s): '+GetDurationStr(out_d))
        self.duration_data.SetLabel('  Data Duration(h:m:s): '+GetDurationStr(data_d))
            
    def _OnPBLookup(self, msg):
        d=msg.data
        k=d.get('item', None)
        name=d.get('name', None)
        if k is None:
            return
        self._name_map[k]=name

    def getdata(self, dict, want=None):
        dict[self._data_key]=copy.deepcopy(self._data)

    def _save_to_db(self, dict):
        if self.read_only:
            return
        db_rr={}
        for k,e in dict.items():
            db_rr[k]=CallHistoryDataobject(e)
        database.ensurerecordtype(db_rr, callhistoryobjectfactory)
        self._main_window.database.savemajordict(self._data_key, db_rr)

    def populatefs(self, dict):
        if self.read_only:
            wx.MessageBox('You are viewing historical data which cannot be changed or saved',
                             'Cannot Save Call History Data',
                             style=wx.OK|wx.ICON_ERROR)
        else:
            self._save_to_db(dict.get(self._data_key, {}))
        return dict

    def getfromfs(self, result, timestamp=None):
        dict=self._main_window.database.\
                   getmajordictvalues(self._data_key,
                                      callhistoryobjectfactory,
                                      at_time=timestamp)
        r={}
        for k,e in dict.items():
            ce=CallHistoryEntry()
            ce.set_db_dict(e)
            r[ce.id]=ce
        result.update({ self._data_key: r})
        return result

    def merge(self, dict):
        if self.read_only:
            wx.MessageBox('You are viewing historical data which cannot be changed or saved',
                             'Cannot Save Call History Data',
                             style=wx.OK|wx.ICON_ERROR)
            return
        d=dict.get(self._data_key, {})
        l=[e for k,e in self._data.items()]
        for k,e in d.items():
            if e not in l:
                self._data[e.id]=e
        self._save_to_db(self._data)
        self._populate()

    def get_selected_data(self):
        # return a dict of selected items
        res={}
        for sel_idx in self.list_widget._item_list.GetSelections():
            k=self.list_widget._item_list.GetItemData(sel_idx)
            if k:
                res[k]=self._data[k]
        return res

    def get_data(self):
        return self._data

#-------------------------------------------------------------------------------
class CallHistoryList(wx.Panel, widgets.BitPimWidget):
    _by_type=0
    _by_date=1
    _by_number=2
    def __init__(self, mainwindow, parent, stats):
        super(CallHistoryList, self).__init__(parent, -1)
        self._main_window=mainwindow
        self._stats=stats
        self.nodes={}
        self.nodes_keys={}
        self._display_filter="All"
        # main box sizer
        vbs=wx.BoxSizer(wx.VERTICAL)
        # data date adjuster
        hbs=wx.BoxSizer(wx.HORIZONTAL)
        static_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1,
                                                 'Historical Data Status:'),
                                    wx.VERTICAL)
        self.historical_data_label=wx.StaticText(self, -1, 'Current Data')
        static_bs.Add(self.historical_data_label, 1, wx.EXPAND|wx.ALL, 5)
        hbs.Add(static_bs, 1, wx.EXPAND|wx.ALL, 5)
        vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
        # main list
        column_info=[]
        column_info.append(("Call Type", 80, False))
        column_info.append(("Date", 120, False))
        column_info.append(("Number", 100, False))
        column_info.append(("Duration", 80, False))
        column_info.append(("Name", 130, False))
        self._item_list=guiwidgets.BitPimListCtrl(self, column_info)
        self._item_list.ResetView(self.nodes, self.nodes_keys)
        vbs.Add(self._item_list, 1, wx.EXPAND|wx.ALL, 5)
        vbs.Add(wx.StaticText(self, -1, '  Note: Click column headings to sort data'), 0, wx.ALIGN_CENTRE|wx.BOTTOM, 10)
        # all done
        today.bind_notification_event(self.OnTodaySelectionIncoming,
                                      today.Today_Group_IncomingCalls)
        today.bind_notification_event(self.OnTodaySelectionMissed,
                                      today.Today_Group_MissedCalls)
        self.today_data=None
        self.SetSizer(vbs)
        self.SetAutoLayout(True)
        vbs.Fit(self)

    def OnSelected(self, node):
        for stat in self._stats.stat_list:
            if self._stats.call_history_tree_nodes[stat]==node:
                if self._display_filter!=stat:
                    self._display_filter=stat
                    # for some reason GetTopItem return 0 (instead of -1)
                    # when the list is empty
                    if self._item_list.GetItemCount():
                        item=self._item_list.GetTopItem()
                        # deselect all the items when changing view
                        while item!=-1:
                            self._item_list.Select(item, 0)
                            item=self._item_list.GetNextItem(item)
                    self.populate()
                self._on_today_selection()
                return

    def OnTodaySelectionIncoming(self, evt):
        node=self._stats.call_history_tree_nodes["Incoming"]
        self.today_data=evt.data
        self.ActivateSelf(node)

    def OnTodaySelectionMissed(self, evt):
        node=self._stats.call_history_tree_nodes["Missed"]
        self.today_data=evt.data
        self.ActivateSelf(node)

    def _on_today_selection(self):
        if self.today_data and self._item_list.GetItemCount():
            item=self._item_list.GetTopItem()
            while item!=-1:
                if self.today_data['id']==self._item_list.GetItemData(item):
                    self._item_list.Select(item, 1)
                    self._item_list.EnsureVisible(item)
                else:
                    self._item_list.Select(item, 0)
                item=self._item_list.GetNextItem(item)
        self.today_data=None

    def GetRightClickMenuItems(self, node):
        result=[]
        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EDITSELECTALL, "Select All", "Select All Items"))
        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EDITDELETEENTRY, "Delete Selected", "Delete Selected Items"))
        result.append((widgets.BitPimWidget.MENU_SPACER, 0, "", ""))
        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EXPORT_CSV_CALL_HISTORY, "Export to CSV ...", "Export the call history to a csv file"))
        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_DATAHISTORICAL, "Historical Data ...", "Display Historical Data"))
        return result

    def CanSelectAll(self):
        if self._item_list.GetItemCount():
            return True
        return False

    def OnSelectAll(self, _):
        item=self._item_list.GetTopItem()
        while item!=-1:
            self._item_list.Select(item)
            item=self._item_list.GetNextItem(item)

    def HasHistoricalData(self):
        return self._stats.HasHistoricalData()

    def OnHistoricalData(self):
        return self._stats.OnHistoricalData()

    def populate(self):
        self.nodes={}
        self.nodes_keys={}
        index=0
        for k,e in self._stats._data.items():
            if self._display_filter=="All" or e.folder==self._display_filter:
                name=e.name
                if name==None or name=="":
                    temp=self._stats._name_map.get(e.number, None)
                    if temp !=None:
                        name=temp
                    else:
                        name=""
                self.nodes[index]=(e.folder, e.get_date_time_str(),
                                   phonenumber.format(e.number),
                                   GetDurationStr(e.duration), name)
                self.nodes_keys[index]=k
                index+=1
        self._item_list.ResetView(self.nodes, self.nodes_keys)

    def CanDelete(self):
        if self._stats.read_only:
            return False
        sels_idx=self._item_list.GetFirstSelected()
        if sels_idx==-1:
            return False
        return True

    def GetDeleteInfo(self):
        return wx.ART_DEL_BOOKMARK, "Delete Call Record"

    def OnDelete(self, _):
        if self._stats.read_only:
            return
        sels_idx=self._item_list.GetSelections()
        if len(sels_idx):
            # delete them from the data list
            for i,item in sels_idx.items():
                del self._stats._data[self._item_list.GetItemData(item)]
                self._item_list.Select(item, 0)
            self.populate()
            self._stats._save_to_db(self._stats._data)
            self._stats.CalculateStats()
    def GetHelpID(self):
        return helpids.ID_TAB_CALLHISTORY


#-------------------------------------------------------------------------------
# adds full combinational filtering of history data
#-------------------------------------------------------------------------------
class CallHistoryListWithFilters(wx.Panel, widgets.BitPimWidget):
    """adds filter lists for each call history column"""
    _by_type=0
    _by_date=1
    _by_number=2
    def __init__(self, mainwindow, parent, stats, filtersVertical = True):
        super(CallHistoryListWithFilters, self).__init__(parent, -1)
        self._main_window=mainwindow
        self._stats=stats
        self.nodes={}
        self.nodes_keys={}
        self._display_filter="All"
        # main box sizer
        vbs=wx.BoxSizer(wx.VERTICAL)
        # data date adjuster
        hbs=wx.BoxSizer(wx.HORIZONTAL)
        static_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1,
                                                 'Historical Data Status (EXP):'),
                                    wx.VERTICAL)
        self.historical_data_label=wx.StaticText(self, -1, 'Current Data')
        static_bs.Add(self.historical_data_label, 1, wx.EXPAND|wx.ALL, 5)
        hbs.Add(static_bs, 1, wx.EXPAND|wx.ALL, 5)
        vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 0)

        column_info=[]                                                  
        column_info.append(("Call Type", 80, False))
        column_info.append(("Date", 120, False))
        column_info.append(("Number", 100, False))
        column_info.append(("Duration", 80, False))
        column_info.append(("Name", 130, False))

        # filter lists
        filterHeight=80
        
        self._type_list=guiwidgets.FilterListCtrl(self, [column_info[0]])
        fsize = (column_info[0][1]+10,filterHeight)
        self._type_list.SetMinSize(fsize) # set in case horizontal orient
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnFilterChanged, self._type_list)
        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnFilterChanged, self._type_list)
        self.Bind(wx.EVT_LIST_INSERT_ITEM, self.OnFilterChanged, self._type_list)
        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnFilterChanged, self._type_list)

        self._date_list=guiwidgets.FilterListCtrl(self, [column_info[1],])
        fsize = (column_info[1][1]+20,filterHeight)
        self._date_list.SetMinSize(fsize)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnFilterChanged, self._date_list)
        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnFilterChanged, self._date_list)
        self.Bind(wx.EVT_LIST_INSERT_ITEM, self.OnFilterChanged, self._date_list)
        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnFilterChanged, self._date_list)

        self._number_list=guiwidgets.FilterListCtrl(self, [column_info[2],])
        fsize = (column_info[2][1]+10,filterHeight)
        self._number_list.SetMinSize(fsize)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnFilterChanged, self._number_list)
        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnFilterChanged, self._number_list)
        self.Bind(wx.EVT_LIST_INSERT_ITEM, self.OnFilterChanged, self._number_list)
        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnFilterChanged, self._number_list)
        
        self._duration_list=guiwidgets.FilterListCtrl(self, [column_info[3],])
        fsize = (column_info[3][1]+10,filterHeight)
        self._duration_list.SetMinSize(fsize)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnFilterChanged, self._duration_list)
        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnFilterChanged, self._duration_list)
        self.Bind(wx.EVT_LIST_INSERT_ITEM, self.OnFilterChanged, self._duration_list)
        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnFilterChanged, self._duration_list)

        self._name_list = guiwidgets.FilterListCtrl(self, [column_info[4]])
        fsize = (column_info[4][1]+10,filterHeight)
        self._name_list.SetMinSize(fsize)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnFilterChanged, self._name_list)
        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnFilterChanged, self._name_list)
        self.Bind(wx.EVT_LIST_INSERT_ITEM, self.OnFilterChanged, self._name_list)
        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnFilterChanged, self._name_list)

        filter_reset_button = wx.Button(self, -1, "Clear Filters")
        self.Bind(wx.EVT_BUTTON, self.OnClearFilters, filter_reset_button)

        # main list                                                  
        self._item_list=guiwidgets.BitPimListCtrl(self, column_info)
        self._item_list.ResetView(self.nodes, self.nodes_keys)

        if filtersVertical:
            filter_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Filters:"), wx.VERTICAL)
            filter_bs.Add(filter_reset_button, 0, wx.EXPAND|wx.ALL, 5)
            filter_bs.Add(self._type_list, 1, wx.EXPAND|wx.ALL, 5)
            filter_bs.Add(self._date_list, 1, wx.EXPAND|wx.ALL, 5)
            filter_bs.Add(self._number_list, 1, wx.EXPAND|wx.ALL, 5)
            filter_bs.Add(self._duration_list, 1, wx.EXPAND|wx.ALL, 5)
            filter_bs.Add(self._name_list, 1, wx.EXPAND|wx.ALL, 5)
            list_hb=wx.BoxSizer(wx.HORIZONTAL)
            list_hb.Add(self._item_list, 1, wx.EXPAND|wx.ALL, 5)
            list_hb.Add(filter_bs, 0, wx.EXPAND|wx.ALL, 5)
            vbs.Add(list_hb, 1, wx.EXPAND|wx.ALL, 0)
        else:
            filter_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Filters:"), wx.VERTICAL)
            filter_hs=wx.BoxSizer(wx.HORIZONTAL)
            filter_bs.Add(filter_reset_button, 0, wx.ALL, 5)
            filter_bs.Add(filter_hs, 1, wx.EXPAND|wx.ALL, 0)
            filter_hs.Add(self._type_list, 0, wx.ALL, 5)
            filter_hs.Add(self._date_list, 0, wx.EXPAND|wx.ALL, 5)
            filter_hs.Add(self._number_list, 0, wx.ALL, 5)
            filter_hs.Add(self._duration_list, 0, wx.ALL, 5)
            filter_hs.Add(self._name_list, 0, wx.ALL, 5)
            vbs.Add(filter_bs, 0, wx.EXPAND|wx.ALL, 5)
            vbs.Add(self._item_list, 1, wx.EXPAND|wx.ALL, 5)

        vbs.Add(wx.StaticText(self, -1, '  Note: Click column headings to sort data'), 0, wx.ALIGN_CENTRE|wx.BOTTOM, 10)

        # all done
        today.bind_notification_event(self.OnTodaySelectionIncoming,
                                      today.Today_Group_IncomingCalls)
        today.bind_notification_event(self.OnTodaySelectionMissed,
                                      today.Today_Group_MissedCalls)
        self.today_data=None
        self.SetSizer(vbs)
        self.SetAutoLayout(True)
        vbs.Fit(self)

    def OnSelected(self, node):
        for stat in self._stats.stat_list:
            if self._stats.call_history_tree_nodes[stat]==node:
                if self._display_filter!=stat:
                    self._display_filter=stat
                    # for some reason GetTopItem return 0 (instead of -1)
                    # when the list is empty
                    if self._item_list.GetItemCount():
                        item=self._item_list.GetTopItem()
                        # deselect all the items when changing view
                        while item!=-1:
                            self._item_list.Select(item, 0)
                            item=self._item_list.GetNextItem(item)
                    self.populateFilters()
                    self.populate()
                self._on_today_selection()
                return

    def OnTodaySelectionIncoming(self, evt):
        node=self._stats.call_history_tree_nodes["Incoming"]
        self.today_data=evt.data
        self.ActivateSelf(node)

    def OnTodaySelectionMissed(self, evt):
        node=self._stats.call_history_tree_nodes["Missed"]
        self.today_data=evt.data
        self.ActivateSelf(node)

    def _on_today_selection(self):
        if self.today_data and self._item_list.GetItemCount():
            item=self._item_list.GetTopItem()
            while item!=-1:
                if self.today_data['id']==self._item_list.GetItemData(item):
                    self._item_list.Select(item, 1)
                    self._item_list.EnsureVisible(item)
                else:
                    self._item_list.Select(item, 0)
                item=self._item_list.GetNextItem(item)
        self.today_data=None

    def GetRightClickMenuItems(self, node):
        result=[]
        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EDITSELECTALL, "Select All", "Select All Items"))
        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EDITDELETEENTRY, "Delete Selected", "Delete Selected Items"))
        result.append((widgets.BitPimWidget.MENU_SPACER, 0, "", ""))
        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EXPORT_CSV_CALL_HISTORY, "Export to CSV ...", "Export the call history to a csv file"))
        result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_DATAHISTORICAL, "Historical Data ...", "Display Historical Data"))
        return result

    def CanSelectAll(self):
        if self._item_list.GetItemCount():
            return True
        return False

    def OnSelectAll(self, _):
        item=self._item_list.GetTopItem()
        while item!=-1:
            self._item_list.Select(item)
            item=self._item_list.GetNextItem(item)

    def HasHistoricalData(self):
        return self._stats.HasHistoricalData()

    def OnHistoricalData(self):
        return self._stats.OnHistoricalData()

    def populateFilters(self):
        """populates the filter lists.  I think this needs to be done separate from
           populate() since when a filter selection state changes populate gets called.
           We don't want to re-populate the filter list everytime a filter selection is 
           made.  
        """
        self._type_list.DeleteAllItems()
        self._date_list.DeleteAllItems()
        self._number_list.DeleteAllItems()
        self._duration_list.DeleteAllItems()
        self._name_list.DeleteAllItems()
        for k,e in self._stats._data.items():
            if self._display_filter != "All" and e.folder != self._display_filter:
                continue
            self._type_list.InsertStringItem(self._type_list.GetItemCount(), e.folder)
            self._date_list.InsertStringItem(self._date_list.GetItemCount(), e.get_date_time_str()[:10]) #no time
            self._number_list.InsertStringItem(self._number_list.GetItemCount(), phonenumber.format(e.number))
            self._duration_list.InsertStringItem(self._duration_list.GetItemCount(),GetDurationStr(e.duration)[:4]) #no seconds
            name=e.name
            if name==None or name=="":
                temp=self._stats._name_map.get(e.number, None)
                if temp !=None:
                    name=temp
                else:
                    name=""
            self._name_list.InsertStringItem(self._name_list.GetItemCount(), name)
    
    def populate(self):
        self.nodes={}
        self.nodes_keys={}
        index=0
        for k,e in self._stats._data.items():
            if self._display_filter=="All" or e.folder==self._display_filter:
                name=e.name
                if name==None or name=="":
                    temp=self._stats._name_map.get(e.number, None)
                    if temp !=None:
                        name=temp
                    else:
                        name=""
                # check filters, needs to happen after name lookup
                if name not in self._name_list.getSelectedOrAll() or \
                   e.folder not in self._type_list.getSelectedOrAll() or \
                   phonenumber.format(e.number) not in self._number_list.getSelectedOrAll() or \
                   GetDurationStr(e.duration)[:4] not in self._duration_list.getSelectedOrAll() or \
                   e.get_date_time_str()[:10] not in self._date_list.getSelectedOrAll():
                    continue
                
                # entry passed all filters, add to display
                self.nodes[index]=(e.folder, e.get_date_time_str(),
                                   phonenumber.format(e.number),
                                   GetDurationStr(e.duration), name)
                self.nodes_keys[index]=k
                index+=1
        self._item_list.ResetView(self.nodes, self.nodes_keys)

    def OnFilterChanged(self, event):
        """a filter list selection or inser/delete event occurred, update display"""
        self.populate()
        
    def OnClearFilters(self, event):
        """reset any filter selections, populate() gets called automatically
           by filter events fired as selection status changes"""
        self._type_list.clearAllSelections()
        self._date_list.clearAllSelections()
        self._name_list.clearAllSelections()
        self._number_list.clearAllSelections()
        self._duration_list.clearAllSelections()        
        
    def CanDelete(self):
        if self._stats.read_only:
            return False
        sels_idx=self._item_list.GetFirstSelected()
        if sels_idx==-1:
            return False
        return True

    def GetDeleteInfo(self):
        return wx.ART_DEL_BOOKMARK, "Delete Call Record"

    def OnDelete(self, _):
        if self._stats.read_only:
            return
        sels_idx=self._item_list.GetSelections()
        if len(sels_idx):
            # delete them from the data list
            for i,item in sels_idx.items():
                del self._stats._data[self._item_list.GetItemData(item)]
                self._item_list.Select(item, 0)
            self.populateFilters()
            self.populate()
            self._stats._save_to_db(self._stats._data)
            self._stats.CalculateStats()

    def GetHelpID(self):
        return helpids.ID_TAB_CALLHISTORY
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.