Module phonebook
[hide private]
[frames] | no frames]

Source Code for Module phonebook

   1  ### BITPIM 
   2  ### 
   3  ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com> 
   4  ### Copyright (C) 2004 Adit Panchal <apanchal@bastula.org> 
   5  ### 
   6  ### This program is free software; you can redistribute it and/or modify 
   7  ### it under the terms of the BitPim license as detailed in the LICENSE file. 
   8  ### 
   9  ### $Id: phonebook.py 4775 2010-01-04 03:44:57Z djpham $ 
  10   
  11  """A widget for displaying/editting the phone information 
  12   
  13  The format for a phonebook entry is standardised.  It is a 
  14  dict with the following fields.  Each field is a list, most 
  15  important first, with each item in the list being a dict. 
  16   
  17  names: 
  18   
  19     - title      ??Job title or salutation?? 
  20     - first 
  21     - middle 
  22     - last 
  23     - full       You should specify the fullname or the 4 above 
  24     - nickname   (This could also be an alias) 
  25   
  26  categories: 
  27   
  28    - category    User defined category name 
  29    - ringtone    (optional) Ringtone name for this category 
  30   
  31  emails: 
  32   
  33    - email       Email address 
  34    - type        (optional) 'home' or 'business' 
  35    - speeddial   (optional) Speed dial for this entry 
  36    - ringtone    (optional) ringtone name for this entry 
  37    - wallpaper   (optional) wallpaper name for this entry 
  38   
  39  maillist: 
  40   
  41    - entry       string of '\x00\x00' separated of number or email entries. 
  42    - speeddial   (optional) Speed dial for this entry 
  43    - ringtone    (optional) ringtone name for this entry 
  44    - wallpaper   (optional) wallpaper name for this entry 
  45   
  46  urls: 
  47   
  48    - url         URL 
  49    - type        (optional) 'home' or 'business' 
  50   
  51  ringtones: 
  52   
  53    - ringtone    Name of a ringtone 
  54    - use         'call', 'message' 
  55   
  56  addresses: 
  57   
  58    - type        'home' or 'business' 
  59    - company     (only for type of 'business') 
  60    - street      Street part of address 
  61    - street2     Second line of street address 
  62    - city 
  63    - state 
  64    - postalcode 
  65    - country     Can also be the region 
  66   
  67  wallpapers: 
  68   
  69    - wallpaper   Name of wallpaper 
  70    - use         see ringtones.use 
  71   
  72  flags: 
  73   
  74    - secret     Boolean if record is private/secret (if not present - value is false) 
  75    - sim        Boolean if record should be stored on SIM card of GSM phones. 
  76   
  77  memos: 
  78   
  79    - memo       Note 
  80   
  81  numbers: 
  82   
  83    - number     Phone number as ascii string 
  84    - type       'home', 'office', 'cell', 'fax', 'pager', 'data', 'none'  (if you have home2 etc, list 
  85                 them without the digits.  The second 'home' is implicitly home2 etc) 
  86    - speeddial  (optional) Speed dial number 
  87    - ringtone   (optional) ringtone name for this entry 
  88    - wallpaper  (optional) wallpaper name for this entry 
  89   
  90  serials: 
  91   
  92    - sourcetype        identifies source driver in bitpim (eg "lgvx4400", "windowsaddressbook") 
  93    - sourceuniqueid    (optional) identifier for where the serial came from (eg ESN of phone, wab host/username) 
  94                        (imagine having multiple phones of the same model to see why this is needed) 
  95    - *                 other names of use to sourcetype 
  96  """ 
  97   
  98  # Standard imports 
  99  from __future__ import with_statement 
 100  import os 
 101  import cStringIO 
 102  import re 
 103  import time 
 104  import copy 
 105   
 106  # GUI 
 107  import wx 
 108  import wx.grid 
 109  import wx.html 
 110   
 111  # My imports 
 112  import common 
 113  import xyaptu 
 114  import guihelper 
 115  import phonebookentryeditor 
 116  import pubsub 
 117  import nameparser 
 118  import bphtml 
 119  import guihelper 
 120  import guiwidgets 
 121  import phonenumber 
 122  import helpids 
 123  import database 
 124  import widgets 
125 126 127 ### 128 ### The object we use to store a record. See detailed description of 129 ### fields at top of file 130 ### 131 132 -class phonebookdataobject(database.basedataobject):
133 # no change to _knownproperties (all of ours are list properties) 134 _knownlistproperties=database.basedataobject._knownlistproperties.copy() 135 _knownlistproperties.update( {'names': ['title', 'first', 'middle', 'last', 'full', 'nickname'], 136 'categories': ['category'], 137 'emails': ['email', 'type', 'speeddial', 138 'ringtone', 'wallpaper' ], 139 'urls': ['url', 'type'], 140 'ringtones': ['ringtone', 'use'], 141 'addresses': ['type', 'company', 'street', 'street2', 'city', 'state', 'postalcode', 'country'], 142 'wallpapers': ['wallpaper', 'use'], 143 'flags': ['secret', 'sim'], 144 'memos': ['memo'], 145 'numbers': ['number', 'type', 'speeddial', 146 'ringtone', 'wallpaper' ], 147 'ice': [ 'iceindex' ], 148 'favorite': [ 'favoriteindex' ], 149 'ims': [ 'type', 'username' ], 150 ## 'maillist': ['entry', 'speeddial', 151 ## 'ringtone', 'wallpaper' ], 152 # serials is in parent object 153 })
154 155 phonebookobjectfactory=database.dataobjectfactory(phonebookdataobject)
156 157 ### 158 ### Phonebook entry display (Derived from HTML) 159 ### 160 161 -class PhoneEntryDetailsView(bphtml.HTMLWindow):
162
163 - def __init__(self, parent, id, stylesfile="styles.xy", layoutfile="pblayout.xy"):
164 bphtml.HTMLWindow.__init__(self, parent, id) 165 self.stylesfile=guihelper.getresourcefile(stylesfile) 166 self.pblayoutfile=guihelper.getresourcefile(layoutfile) 167 self.xcp=None 168 self.xcpstyles=None 169 self.ShowEntry({})
170
171 - def ShowEntry(self, entry):
172 if self.xcp is None: 173 template=open(self.pblayoutfile, "rt").read() 174 self.xcp=xyaptu.xcopier(None) 175 self.xcp.setupxcopy(template) 176 if self.xcpstyles is None: 177 self.xcpstyles={} 178 try: 179 execfile(self.stylesfile, self.xcpstyles, self.xcpstyles) 180 except UnicodeError: 181 common.unicode_execfile(self.stylesfile, self.xcpstyles, self.xcpstyles) 182 self.xcpstyles['entry']=entry 183 text=self.xcp.xcopywithdns(self.xcpstyles) 184 try: 185 text=bphtml.applyhtmlstyles(text, self.xcpstyles['styles']) 186 except: 187 if __debug__: 188 open("debug.html", "wt").write(common.forceascii(text)) 189 raise 190 self.SetPage(text)
191
192 ### 193 ### Functions used to get data from a record 194 ### 195 196 -def formatICE(iceindex):
197 return iceindex['iceindex']+1
198
199 -def formatFav(favoriteindex):
200 return favoriteindex['favoriteindex']+1
201
202 -def formatcategories(cats):
203 c=[cat['category'] for cat in cats] 204 c.sort() 205 return "; ".join(c)
206
207 -def formataddress(address):
208 l=[] 209 for i in 'company', 'street', 'street2', 'city', 'state', 'postalcode', 'country': 210 if i in address: 211 l.append(address[i]) 212 return "; ".join(l)
213
214 -def formattypenumber(number):
215 t=number['type'] 216 t=t[0].upper()+t[1:] 217 sd=number.get("speeddial", None) 218 if sd is None: 219 return "%s (%s)" % (phonenumber.format(number['number']), t) 220 return "%s [%d] (%s)" % (phonenumber.format(number['number']), sd, t)
221
222 -def formatnumber(number):
223 sd=number.get("speeddial", None) 224 if sd is None: 225 return phonenumber.format(number['number']) 226 return "%s [%d]" % (phonenumber.format(number['number']), sd)
227
228 -def formatstorage(flag):
229 return 'SIM' if flag.get('sim', False) else ''
230
231 -def formatsecret(flag):
232 return 'True' if flag.get('secret', False) else ''
233 234 # this is specified here as a list so that we can get the 235 # keys in the order below for the settings UI (alpha sorting 236 # or dictionary order would be user hostile). The data 237 # is converted to a dict below 238 _getdatalist=[ 239 # column (key matchnum match func_or_field showinimport) 240 'Name', ("names", 0, None, nameparser.formatfullname, True), 241 'First', ("names", 0, None, nameparser.getfirst, False), 242 'Middle', ("names", 0, None, nameparser.getmiddle, False), 243 'Last', ("names", 0, None, nameparser.getlast, False), 244 245 'Category', ("categories", 0, None, "category", False), 246 'Category2', ("categories", 1, None, "category", False), 247 'Category3', ("categories", 2, None, "category", False), 248 'Category4', ("categories", 3, None, "category", False), 249 'Category5', ("categories", 4, None, "category", False), 250 'Categories', ("categories", None, None, formatcategories, True), 251 252 "Phone", ("numbers", 0, None, formattypenumber, False), 253 "Phone2", ("numbers", 1, None, formattypenumber, False), 254 "Phone3", ("numbers", 2, None, formattypenumber, False), 255 "Phone4", ("numbers", 3, None, formattypenumber, False), 256 "Phone5", ("numbers", 4, None, formattypenumber, False), 257 "Phone6", ("numbers", 5, None, formattypenumber, False), 258 "Phone7", ("numbers", 6, None, formattypenumber, False), 259 "Phone8", ("numbers", 7, None, formattypenumber, False), 260 "Phone9", ("numbers", 8, None, formattypenumber, False), 261 "Phone10", ("numbers", 9, None, formattypenumber, False), 262 263 # phone numbers are inserted here 264 265 'Email', ("emails", 0, None, "email", True), 266 'Email2', ("emails", 1, None, "email", True), 267 'Email3', ("emails", 2, None, "email", True), 268 'Email4', ("emails", 3, None, "email", True), 269 'Email5', ("emails", 4, None, "email", True), 270 'Business Email', ("emails", 0, ("type", "business"), "email", False), 271 'Business Email2', ("emails", 1, ("type", "business"), "email", False), 272 'Home Email', ("emails", 0, ("type", "home"), "email", False), 273 'Home Email2', ("emails", 1, ("type", "home"), "email", False), 274 275 'URL', ("urls", 0, None, "url", True), 276 'URL2', ("urls", 1, None, "url", True), 277 'URL3', ("urls", 2, None, "url", True), 278 'URL4', ("urls", 3, None, "url", True), 279 'URL5', ("urls", 4, None, "url", True), 280 'Business URL', ("urls", 0, ("type", "business"), "url", False), 281 'Business URL2', ("urls", 1, ("type", "business"), "url", False), 282 'Home URL', ("urls", 0, ("type", "home"), "url", False), 283 'Home URL2', ("urls", 1, ("type", "home"), "url", False), 284 285 'Ringtone', ("ringtones", 0, ("use", "call"), "ringtone", True), 286 'Message Ringtone', ("ringtones", 0, ("use", "message"), "ringtone", True), 287 288 'Address', ("addresses", 0, None, formataddress, True), 289 'Address2', ("addresses", 1, None, formataddress, True), 290 'Address3', ("addresses", 2, None, formataddress, True), 291 'Address4', ("addresses", 3, None, formataddress, True), 292 'Address5', ("addresses", 4, None, formataddress, True), 293 'Home Address', ("addresses", 0, ("type", "home"), formataddress, False), 294 'Home Address2', ("addresses", 1, ("type", "home"), formataddress, False), 295 'Business Address', ("addressess", 0, ("type", "business"), formataddress, False), 296 'Business Address2', ("addressess", 1, ("type", "business"), formataddress, False), 297 298 "Wallpaper", ("wallpapers", 0, None, "wallpaper", True), 299 300 "Secret", ("flags", 0, ("secret", True), formatsecret, True), 301 "Storage", ("flags", 0,('sim', True), formatstorage, True), 302 "Memo", ("memos", 0, None, "memo", True), 303 "Memo2", ("memos", 1, None, "memo", True), 304 "Memo3", ("memos", 2, None, "memo", True), 305 "Memo4", ("memos", 3, None, "memo", True), 306 "Memo5", ("memos", 4, None, "memo", True), 307 308 "ICE #", ("ice", 0, None, formatICE, False), 309 "Favorite #", ("favorite", 0, None, formatFav, False) 310 311 ] 312 313 ll=[] 314 for pretty, actual in ("Home", "home"), ("Office", "office"), ("Cell", "cell"), ("Fax", "fax"), ("Pager", "pager"), ("Data", "data"): 315 for suf,n in ("", 0), ("2", 1), ("3", 2): 316 ll.append(pretty+suf) 317 ll.append(("numbers", n, ("type", actual), formatnumber, True)) 318 _getdatalist[40:40]=ll 319 320 _getdatatable={} 321 AvailableColumns=[] 322 DefaultColumns=['Name', 'Phone', 'Phone2', 'Phone3', 'Email', 'Categories', 'Memo', 'Secret'] 323 ImportColumns=[_getdatalist[x*2] for x in range(len(_getdatalist)/2) if _getdatalist[x*2+1][4]] 324 325 for n in range(len(_getdatalist)/2): 326 AvailableColumns.append(_getdatalist[n*2]) 327 _getdatatable[_getdatalist[n*2]]=_getdatalist[n*2+1] 328 329 del _getdatalist # so we don't accidentally use it
330 331 -def getdata(column, entry, default=None):
332 """Returns the value in a particular column. 333 Note that the data is appropriately formatted. 334 335 @param column: column name 336 @param entry: the dict representing a phonebook entry 337 @param default: what to return if the entry has no data for that column 338 """ 339 key, count, prereq, formatter, _ =_getdatatable[column] 340 341 # do we even have that key 342 if key not in entry: 343 return default 344 345 if count is None: 346 # value is all the fields (eg Categories) 347 thevalue=entry[key] 348 elif prereq is None: 349 # no prereq 350 if len(entry[key])<=count: 351 return default 352 thevalue=entry[key][count] 353 else: 354 # find the count instance of value matching k,v in prereq 355 ptr=0 356 togo=count+1 357 l=entry[key] 358 k,v=prereq 359 while togo: 360 if ptr==len(l): 361 return default 362 if k not in l[ptr]: 363 ptr+=1 364 continue 365 if l[ptr][k]!=v: 366 ptr+=1 367 continue 368 togo-=1 369 if togo!=0: 370 ptr+=1 371 continue 372 thevalue=entry[key][ptr] 373 break 374 375 # thevalue now contains the dict with value we care about 376 if callable(formatter): 377 return formatter(thevalue) 378 379 return thevalue.get(formatter, default)
380
381 -def getdatainfo(column, entry):
382 """Similar to L{getdata} except returning higher level information. 383 384 Returns the key name and which index from the list corresponds to 385 the column. 386 387 @param column: Column name 388 @param entry: The dict representing a phonebook entry 389 @returns: (keyname, index) tuple. index will be None if the entry doesn't 390 have the relevant column value and -1 if all of them apply 391 """ 392 key, count, prereq, formatter, _ =_getdatatable[column] 393 394 # do we even have that key 395 if key not in entry: 396 return (key, None) 397 398 # which value or values do we want 399 if count is None: 400 return (key, -1) 401 elif prereq is None: 402 # no prereq 403 if len(entry[key])<=count: 404 return (key, None) 405 return (key, count) 406 else: 407 # find the count instance of value matching k,v in prereq 408 ptr=0 409 togo=count+1 410 l=entry[key] 411 k,v=prereq 412 while togo: 413 if ptr==len(l): 414 return (key,None) 415 if k not in l[ptr]: 416 ptr+=1 417 continue 418 if l[ptr][k]!=v: 419 ptr+=1 420 continue 421 togo-=1 422 if togo!=0: 423 ptr+=1 424 continue 425 return (key, ptr) 426 return (key, None)
427
428 -class CategoryManager:
429 430 # this is only used to prevent the pubsub module 431 # from being GC while any instance of this class exists 432 __publisher=pubsub.Publisher 433
434 - def __init__(self):
435 self.categories=[] 436 self.group_wps=[] 437 438 pubsub.subscribe(self.OnListRequest, pubsub.REQUEST_CATEGORIES) 439 pubsub.subscribe(self.OnSetCategories, pubsub.SET_CATEGORIES) 440 pubsub.subscribe(self.OnMergeCategories, pubsub.MERGE_CATEGORIES) 441 pubsub.subscribe(self.OnAddCategory, pubsub.ADD_CATEGORY) 442 pubsub.subscribe(self.OnGroupWPRequest, pubsub.REQUEST_GROUP_WALLPAPERS) 443 pubsub.subscribe(self.OnSetGroupWP, pubsub.SET_GROUP_WALLPAPERS) 444 pubsub.subscribe(self.OnMergeGroupWP, pubsub.MERGE_GROUP_WALLPAPERS)
445
446 - def OnListRequest(self, msg=None):
447 # nb we publish a copy of the list, not the real 448 # thing. otherwise other code inadvertently modifies it! 449 pubsub.publish(pubsub.ALL_CATEGORIES, self.categories[:])
450
451 - def OnGroupWPRequest(self, msg=None):
452 pubsub.publish(pubsub.GROUP_WALLPAPERS, self.group_wps[:])
453
454 - def OnSetGroupWP(self, msg):
455 self.group_wps=msg.data[:] 456 self.OnGroupWPRequest()
457
458 - def OnAddCategory(self, msg):
459 name=msg.data 460 if name in self.categories: 461 return 462 self.categories.append(name) 463 self.categories.sort() 464 self.OnListRequest()
465
466 - def OnSetCategories(self, msg):
467 cats=msg.data[:] 468 self.categories=cats 469 self.categories.sort() 470 self.OnListRequest()
471
472 - def OnMergeCategories(self, msg):
473 cats=msg.data[:] 474 newcats=self.categories[:] 475 for i in cats: 476 if i not in newcats: 477 newcats.append(i) 478 newcats.sort() 479 if newcats!=self.categories: 480 self.categories=newcats 481 self.OnListRequest()
482
483 - def OnMergeGroupWP(self, msg):
484 new_groups=msg.data[:] 485 gwp=self.group_wps[:] 486 temp_dict={} 487 for entry in gwp: 488 l=entry.split(":", 1) 489 name=l[0] 490 wp=l[1] 491 temp_dict[name]=wp 492 for entry in new_groups: 493 l=entry.split(":", 1) 494 name=l[0] 495 wp=l[1] 496 temp_dict[name]=wp 497 out_list=[] 498 for k, v in temp_dict.items(): 499 out_list.append(str(k)+":"+str(v)) 500 self.group_wps=out_list 501 self.OnGroupWPRequest()
502 503 504 CategoryManager=CategoryManager() # shadow out class name
505 506 ### 507 ### We use a table for speed 508 ### 509 510 -class PhoneDataTable(wx.grid.PyGridTableBase):
511
512 - def __init__(self, widget, columns):
513 self.main=widget 514 self.rowkeys=self.main._data.keys() 515 wx.grid.PyGridTableBase.__init__(self) 516 self.oddattr=wx.grid.GridCellAttr() 517 self.oddattr.SetBackgroundColour("OLDLACE") 518 self.evenattr=wx.grid.GridCellAttr() 519 self.evenattr.SetBackgroundColour("ALICE BLUE") 520 self.columns=columns 521 assert len(self.rowkeys)==0 # we can't sort here, and it isn't necessary because list is zero length
522
523 - def GetColLabelValue(self, col):
524 return self.columns[col]
525
526 - def OnDataUpdated(self):
527 newkeys=self.main._data.keys() 528 newkeys.sort() 529 oldrows=self.rowkeys 530 self.rowkeys=newkeys 531 lo=len(oldrows) 532 ln=len(self.rowkeys) 533 if ln>lo: 534 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, ln-lo) 535 elif lo>ln: 536 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, 0, lo-ln) 537 else: 538 msg=None 539 if msg is not None: 540 self.GetView().ProcessTableMessage(msg) 541 self.Sort() 542 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES) 543 self.GetView().ProcessTableMessage(msg) 544 self.GetView().AutoSizeColumns()
545
546 - def SetColumns(self, columns):
547 oldcols=self.columns 548 self.columns=columns 549 lo=len(oldcols) 550 ln=len(self.columns) 551 if ln>lo: 552 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_APPENDED, ln-lo) 553 elif lo>ln: 554 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_DELETED, 0, lo-ln) 555 else: 556 msg=None 557 if msg is not None: 558 self.GetView().ProcessTableMessage(msg) 559 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES) 560 self.GetView().ProcessTableMessage(msg) 561 self.GetView().AutoSizeColumns()
562
563 - def Sort(self):
564 bycol=self.main.sortedColumn 565 descending=self.main.sortedColumnDescending 566 ### ::TODO:: this sorting is not stable - it should include the current pos rather than key 567 l=[ (getdata(self.columns[bycol], self.main._data[key]), key) for key in self.rowkeys] 568 l.sort() 569 if descending: 570 l.reverse() 571 self.rowkeys=[key for val,key in l] 572 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES) 573 self.GetView().ProcessTableMessage(msg)
574
575 - def IsEmptyCell(self, row, col):
576 return False
577
578 - def GetNumberRows(self):
579 return len(self.rowkeys)
580
581 - def GetNumberCols(self):
582 return len(self.columns)
583
584 - def GetValue(self, row, col):
585 try: 586 entry=self.main._data[self.rowkeys[row]] 587 except: 588 print "bad row", row 589 return "<error>" 590 591 return getdata(self.columns[col], entry, "")
592
593 - def GetAttr(self, row, col, _):
594 r=[self.evenattr, self.oddattr][row%2] 595 r.IncRef() 596 return r
597
598 -class PhoneWidget(wx.Panel, widgets.BitPimWidget):
599 """Main phone editing/displaying widget""" 600 CURRENTFILEVERSION=2 601 # Data selector const 602 _Current_Data=0 603 _Historic_Data=1 604
605 - def __init__(self, mainwindow, parent, config):
606 wx.Panel.__init__(self, parent,-1) 607 self.sash_pos=config.ReadInt('phonebooksashpos', -300) 608 self.update_sash=False 609 # keep this around while we exist 610 self.categorymanager=CategoryManager 611 split=wx.SplitterWindow(self, -1, style=wx.SP_3D|wx.SP_LIVE_UPDATE) 612 split.SetMinimumPaneSize(20) 613 self.mainwindow=mainwindow 614 self._data={} 615 self.parent=parent 616 self.categories=[] 617 self.group_wps=[] 618 self.modified=False 619 self.table_panel=wx.Panel(split) 620 self.table=wx.grid.Grid(self.table_panel, wx.NewId()) 621 self.table.EnableGridLines(False) 622 self.error_log=guihelper.MultiMessageBox(self.mainwindow , "Contact Export Errors", 623 "Bitpim is unable to send the following data to your phone") 624 # which columns? 625 cur=config.Read("phonebookcolumns", "") 626 if len(cur): 627 cur=cur.split(",") 628 # ensure they all exist 629 cur=[c for c in cur if c in AvailableColumns] 630 else: 631 cur=DefaultColumns 632 # column sorter info 633 self.sortedColumn=0 634 self.sortedColumnDescending=False 635 636 self.dt=PhoneDataTable(self, cur) 637 self.table.SetTable(self.dt, False, wx.grid.Grid.wxGridSelectRows) 638 self.table.SetSelectionMode(wx.grid.Grid.wxGridSelectRows) 639 self.table.SetRowLabelSize(0) 640 self.table.EnableEditing(False) 641 self.table.EnableDragRowSize(False) 642 self.table.SetMargins(1,0) 643 # data date adjuster 644 hbs=wx.BoxSizer(wx.HORIZONTAL) 645 self.read_only=False 646 self.historical_date=None 647 static_bs=wx.StaticBoxSizer(wx.StaticBox(self.table_panel, -1, 648 'Historical Data Status:'), 649 wx.VERTICAL) 650 self.historical_data_label=wx.StaticText(self.table_panel, -1, 651 'Current Data') 652 static_bs.Add(self.historical_data_label, 1, wx.EXPAND|wx.ALL, 5) 653 hbs.Add(static_bs, 1, wx.EXPAND|wx.ALL, 5) 654 # show the number of contacts 655 static_bs=wx.StaticBoxSizer(wx.StaticBox(self.table_panel, -1, 656 'Number of Contacts:'), 657 wx.VERTICAL) 658 self.contactcount_label=wx.StaticText(self.table_panel, -1, '0') 659 static_bs.Add(self.contactcount_label, 1, wx.EXPAND|wx.ALL, 5) 660 hbs.Add(static_bs, 1, wx.EXPAND|wx.ALL, 5) 661 # main sizer 662 vbs=wx.BoxSizer(wx.VERTICAL) 663 vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) 664 vbs.Add(self.table, 1, wx.EXPAND, 0) 665 self.table_panel.SetSizer(vbs) 666 self.table_panel.SetAutoLayout(True) 667 vbs.Fit(self.table_panel) 668 self.preview=PhoneEntryDetailsView(split, -1, "styles.xy", "pblayout.xy") 669 # for some reason, preview doesn't show initial background 670 wx.CallAfter(self.preview.ShowEntry, {}) 671 split.SplitVertically(self.table_panel, self.preview, self.sash_pos) 672 self.split=split 673 bs=wx.BoxSizer(wx.VERTICAL) 674 bs.Add(split, 1, wx.EXPAND) 675 self.SetSizer(bs) 676 self.SetAutoLayout(True) 677 wx.EVT_IDLE(self, self.OnIdle) 678 wx.grid.EVT_GRID_SELECT_CELL(self, self.OnCellSelect) 679 wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self, self.OnCellDClick) 680 wx.grid.EVT_GRID_CELL_RIGHT_CLICK(self, self.OnCellRightClick) 681 wx.EVT_LEFT_DCLICK(self.preview, self.OnPreviewDClick) 682 pubsub.subscribe(self.OnCategoriesUpdate, pubsub.ALL_CATEGORIES) 683 pubsub.subscribe(self.OnGroupWPUpdate, pubsub.GROUP_WALLPAPERS) 684 pubsub.subscribe(self.OnPBLookup, pubsub.REQUEST_PB_LOOKUP) 685 pubsub.subscribe(self.OnMediaNameChanged, pubsub.MEDIA_NAME_CHANGED) 686 # we draw the column headers 687 # code based on original implementation by Paul Mcnett 688 wx.EVT_PAINT(self.table.GetGridColLabelWindow(), self.OnColumnHeaderPaint) 689 wx.grid.EVT_GRID_LABEL_LEFT_CLICK(self.table, self.OnGridLabelLeftClick) 690 wx.grid.EVT_GRID_LABEL_LEFT_DCLICK(self.table, self.OnGridLabelLeftClick) 691 wx.EVT_SPLITTER_SASH_POS_CHANGED(self, self.split.GetId(), 692 self.OnSashPosChanged) 693 # context menu 694 self.context_menu=wx.Menu() 695 id=wx.NewId() 696 self.context_menu.Append(id, 'Set to current', 697 'Set the selected item to current data') 698 wx.EVT_MENU(self, id, self.OnSetToCurrent)
699
700 - def OnInit(self):
701 # whether or not to turn on phonebook preview pane 702 if not self.config.ReadInt("viewphonebookpreview", 1): 703 self.OnViewPreview(False)
704
705 - def OnColumnHeaderPaint(self, evt):
706 w = self.table.GetGridColLabelWindow() 707 dc = wx.PaintDC(w) 708 font = dc.GetFont() 709 dc.SetTextForeground(wx.BLACK) 710 711 # For each column, draw it's rectangle, it's column name, 712 # and it's sort indicator, if appropriate: 713 totColSize = -self.table.GetViewStart()[0]*self.table.GetScrollPixelsPerUnit()[0] 714 for col in range(self.table.GetNumberCols()): 715 dc.SetBrush(wx.Brush("WHEAT", wx.TRANSPARENT)) 716 colSize = self.table.GetColSize(col) 717 rect = (totColSize,0,colSize,32) 718 dc.DrawRectangle(rect[0] - (col!=0 and 1 or 0), rect[1], rect[2] + (col!=0 and 1 or 0), rect[3]) 719 totColSize += colSize 720 721 if col == self.sortedColumn: 722 font.SetWeight(wx.BOLD) 723 # draw a triangle, pointed up or down, at the 724 # top left of the column. 725 left = rect[0] + 3 726 top = rect[1] + 3 727 728 dc.SetBrush(wx.Brush("WHEAT", wx.SOLID)) 729 if self.sortedColumnDescending: 730 dc.DrawPolygon([(left,top), (left+6,top), (left+3,top+4)]) 731 else: 732 dc.DrawPolygon([(left+3,top), (left+6, top+4), (left, top+4)]) 733 else: 734 font.SetWeight(wx.NORMAL) 735 736 dc.SetFont(font) 737 dc.DrawLabel("%s" % self.table.GetTable().columns[col], 738 rect, wx.ALIGN_CENTER | wx.ALIGN_TOP)
739 740
741 - def OnGridLabelLeftClick(self, evt):
742 col=evt.GetCol() 743 if col==self.sortedColumn: 744 self.sortedColumnDescending=not self.sortedColumnDescending 745 else: 746 self.sortedColumn=col 747 self.sortedColumnDescending=False 748 self.dt.Sort() 749 self.table.Refresh()
750
751 - def OnSashPosChanged(self, _):
752 if self.update_sash: 753 self.sash_pos=self.split.GetSashPosition() 754 self.config.WriteInt('phonebooksashpos', self.sash_pos)
755 - def OnPreActivate(self):
756 self.update_sash=False
757 - def OnPostActivate(self):
758 self.split.SetSashPosition(self.sash_pos) 759 self.update_sash=True
760
761 - def SetColumns(self, columns):
762 c=self.GetColumns()[self.sortedColumn] 763 self.dt.SetColumns(columns) 764 if c in columns: 765 self.sortedColumn=columns.index(c) 766 else: 767 self.sortedColumn=0 768 self.sortedColumnDescending=False 769 self.dt.Sort() 770 self.table.Refresh()
771
772 - def GetColumns(self):
773 return self.dt.columns
774
775 - def OnCategoriesUpdate(self, msg):
776 if self.categories!=msg.data: 777 self.categories=msg.data[:] 778 self.modified=True
779
780 - def OnGroupWPUpdate(self, msg):
781 if self.group_wps!=msg.data: 782 self.group_wps=msg.data[:] 783 self.modified=True
784
785 - def OnPBLookup(self, msg):
786 d=msg.data 787 s=d.get('item', '') 788 if not len(s): 789 return 790 d['name']=None 791 for k,e in self._data.items(): 792 for n in e.get('numbers', []): 793 if s==n.get('number', None): 794 # found a number, stop and reply 795 d['name']=nameparser.getfullname(e['names'][0])+'('+\ 796 n.get('type', '')+')' 797 pubsub.publish(pubsub.RESPONSE_PB_LOOKUP, d) 798 return 799 for n in e.get('emails', []): 800 if s==n.get('email', None): 801 # found an email, stop and reply 802 d['name']=nameparser.getfullname(e['names'][0])+'(email)' 803 pubsub.publish(pubsub.RESPONSE_PB_LOOKUP, d) 804 return 805 # done and reply 806 pubsub.publish(pubsub.RESPONSE_PB_LOOKUP, d)
807
808 - def OnMediaNameChanged(self, msg):
809 d=msg.data 810 _type=d.get(pubsub.media_change_type, None) 811 _old_name=d.get(pubsub.media_old_name, None) 812 _new_name=d.get(pubsub.media_new_name, None) 813 if _type is None or _old_name is None or _new_name is None: 814 # invalid/incomplete data 815 return 816 if _type!=pubsub.wallpaper_type and \ 817 _type!=pubsub.ringtone_type: 818 # neither wallpaper nor ringtone 819 return 820 _old_name=common.basename(_old_name) 821 _new_name=common.basename(_new_name) 822 if _type==pubsub.wallpaper_type: 823 main_key='wallpapers' 824 element_key='wallpaper' 825 else: 826 main_key='ringtones' 827 element_key='ringtone' 828 for k,e in self._data.items(): 829 for i,n in enumerate(e.get(main_key, [])): 830 if _old_name==n.