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.get(element_key, None): 831 # found it, update the name 832 self._data[k][main_key][i][element_key]=_new_name 833 self.modified=True
834
835 - def HasColumnSelector(self):
836 return True
837
838 - def OnViewColumnSelector(self):
839 with guihelper.WXDialogWrapper(ColumnSelectorDialog(self.parent, self.config, self), 840 True): 841 pass
842
843 - def HasPreviewPane(self):
844 return True
845
846 - def IsPreviewPaneEnabled(self):
847 return self.split.IsSplit()
848
849 - def OnViewPreview(self, preview_on):
850 if preview_on: 851 self.split.SplitVertically(self.table_panel, self.preview, 852 self.sash_pos) 853 else: 854 if self.sash_pos is None: 855 self.sash_pos=-300 856 else: 857 self.sash_pos=self.split.GetSashPosition() 858 self.split.Unsplit(self.preview) 859 # refresh the table view 860 self.config.WriteInt('viewphonebookpreview', preview_on) 861 self.dt.GetView().AutoSizeColumns()
862
863 - def HasHistoricalData(self):
864 return True
865
866 - def OnHistoricalData(self):
867 """Display current or historical data""" 868 if self.read_only: 869 current_choice=guiwidgets.HistoricalDataDialog.Historical_Data 870 else: 871 current_choice=guiwidgets.HistoricalDataDialog.Current_Data 872 with guihelper.WXDialogWrapper(guiwidgets.HistoricalDataDialog(self, 873 current_choice=current_choice, 874 historical_date=self.historical_date, 875 historical_events=\ 876 self.mainwindow.database.getchangescount('phonebook')), 877 True) as (dlg, retcode): 878 if retcode==wx.ID_OK: 879 with guihelper.MWBusyWrapper(self.mainwindow): 880 current_choice, self.historical_date=dlg.GetValue() 881 r={} 882 if current_choice==guiwidgets.HistoricalDataDialog.Current_Data: 883 self.read_only=False 884 msg_str='Current Data' 885 self.getfromfs(r) 886 else: 887 self.read_only=True 888 msg_str='Historical Data as of %s'%\ 889 str(wx.DateTimeFromTimeT(self.historical_date)) 890 self.getfromfs(r, self.historical_date) 891 self.populate(r, False) 892 self.historical_data_label.SetLabel(msg_str)
893
894 - def OnIdle(self, _):
895 "We save out changed data" 896 if self.modified: 897 self.modified=False 898 self.populatefs(self.getdata({}))
899
900 - def updateserials(self, results):
901 "update the serial numbers after having written to the phone" 902 if not results.has_key('serialupdates'): 903 return 904 905 # each item is a tuple. bpserial is the bitpim serialid, 906 # and updserial is what to update with. 907 for bpserial,updserial in results['serialupdates']: 908 # find the entry with bpserial 909 for k in self._data: 910 entry=self._data[k] 911 if not entry.has_key('serials'): 912 assert False, "serials have gone horribly wrong" 913 continue 914 found=False 915 for serial in entry['serials']: 916 if bpserial==serial: 917 found=True 918 break 919 if not found: 920 # not this entry 921 continue 922 # we will be updating this entry 923 # see if there is a matching serial for updserial that we will update 924 st=updserial['sourcetype'] 925 remove=None 926 for serial in entry['serials']: 927 if serial['sourcetype']!=st: 928 continue 929 if updserial.has_key("sourceuniqueid"): 930 if updserial["sourceuniqueid"]!=serial.get("sourceuniqueid", None): 931 continue 932 remove=serial 933 break 934 # remove if needbe 935 if remove is not None: 936 for count,serial in enumerate(entry['serials']): 937 if remove==serial: 938 break 939 del entry['serials'][count] 940 # add update on end 941 entry['serials'].append(updserial) 942 self.modified=True
943
944 - def CanSelectAll(self):
945 return True
946
947 - def OnSelectAll(self, _):
948 self.table.SelectAll()
949
950 - def OnCellSelect(self, event):
951 event.Skip() 952 row=event.GetRow() 953 self.SetPreview(self._data[self.dt.rowkeys[row]]) # bad breaking of abstraction referencing dt!
954
955 - def OnPreviewDClick(self, _):
956 self.EditEntries(self.table.GetGridCursorRow(), self.table.GetGridCursorCol())
957
958 - def OnCellDClick(self, event):
959 self.EditEntries(event.GetRow(), event.GetCol())
960
961 - def OnCellRightClick(self, evt):
962 if not self.read_only or not self.GetSelectedRowKeys(): 963 return 964 self.table.PopupMenu(self.context_menu, evt.GetPosition())
965
966 - def OnSetToCurrent(self, _):
967 r={} 968 for k in self.GetSelectedRowKeys(): 969 r[k]=self._data[k] 970 if r: 971 dict={} 972 self.getfromfs(dict) 973 dict['phonebook'].update(r) 974 c=[e for e in self.categories if e not in dict['categories']] 975 dict['categories']+=c 976 for i in range(0,len(self.group_wps),2): 977 grp_name = self.group_wps[i] 978 grp_wp = self.group_wps[i+1] 979 if grp_name not in dict['group_wallpapers']: 980 dict['group_wallpapers'].append(grp_name) 981 dict['group_wallpapers'].append(grp_wp) 982 self._save_db(dict)
983
984 - def EditEntries(self, row, column):
985 # Allow moving to next/prev entries 986 key=self.dt.rowkeys[row] 987 data=self._data[key] 988 # can we get it to open on the correct field? 989 datakey,dataindex=getdatainfo(self.GetColumns()[column], data) 990 _keys=self.GetSelectedRowKeys() 991 if datakey in ('categories', 'ringtones', 'wallpapers') and \ 992 len(_keys)>1 and not self.read_only: 993 # Edit a single field for all seleced cells 994 with guihelper.WXDialogWrapper(phonebookentryeditor.SingleFieldEditor(self, datakey), 995 True) as (dlg, retcode): 996 if retcode==wx.ID_OK: 997 _data=dlg.GetData() 998 if _data: 999 for r in _keys: 1000 self._data[r][datakey]=_data 1001 else: 1002 for r in _keys: 1003 del self._data[r][datakey] 1004 self.SetPreview(self._data[_keys[0]]) 1005 self.dt.OnDataUpdated() 1006 self.modified=True 1007 else: 1008 with guihelper.WXDialogWrapper(phonebookentryeditor.Editor(self, data, 1009 factory=phonebookobjectfactory, 1010 keytoopenon=datakey, 1011 dataindex=dataindex, 1012 readonly=self.read_only, 1013 datakey=key, 1014 movement=True), 1015 True) as (dlg, retcode): 1016 if retcode==wx.ID_OK: 1017 self.SaveData(dlg.GetData(), dlg.GetDataKey())
1018
1019 - def SaveData(self, data, key):
1020 self._data[key]=data 1021 self.dt.OnDataUpdated() 1022 self.SetPreview(data) 1023 self.modified=True
1024
1025 - def EditEntry(self, row, column):
1026 key=self.dt.rowkeys[row] 1027 data=self._data[key] 1028 # can we get it to open on the correct field? 1029 datakey,dataindex=getdatainfo(self.GetColumns()[column], data) 1030 with guihelper.WXDialogWrapper(phonebookentryeditor.Editor(self, data, 1031 factory=phonebookobjectfactory, 1032 keytoopenon=datakey, 1033 dataindex=dataindex, 1034 readonly=self.read_only), 1035 True) as (dlg, retcode): 1036 if retcode==wx.ID_OK: 1037 data=dlg.GetData() 1038 self._data[key]=data 1039 self.dt.OnDataUpdated() 1040 self.SetPreview(data) 1041 self.modified=True
1042
1043 - def GetNextEntry(self, next=True):
1044 # return the data for the next item on the list 1045 _sel_rows=self.GetSelectedRows() 1046 if not _sel_rows: 1047 return None 1048 try: 1049 row=_sel_rows[0] 1050 if next: 1051 _new_row=row+1 1052 else: 1053 _new_row=row-1 1054 _num_rows=self.table.GetNumberRows() 1055 if _new_row>=_num_rows: 1056 _new_row=0 1057 elif _new_row<0: 1058 _new_row=_num_rows-1 1059 self.table.SetGridCursor(_new_row, self.table.GetGridCursorCol()) 1060 self.table.SelectRow(_new_row) 1061 _key=self.dt.rowkeys[_new_row] 1062 return (_key,self._data[_key]) 1063 except: 1064 if __debug__: 1065 raise 1066 return None
1067
1068 - def GetDeleteInfo(self):
1069 return guihelper.ART_DEL_CONTACT, "Delete Contact"
1070
1071 - def GetAddInfo(self):
1072 return guihelper.ART_ADD_CONTACT, "Add Contact"
1073
1074 - def CanAdd(self):
1075 if self.read_only: 1076 return False 1077 return True
1078
1079 - def OnAdd(self, _):
1080 if self.read_only: 1081 return 1082 with guihelper.WXDialogWrapper(phonebookentryeditor.Editor(self, {'names': [{'full': 'New Entry'}]}, keytoopenon="names", dataindex=0), 1083 True) as (dlg, retcode): 1084 if retcode==wx.ID_OK: 1085 data=phonebookobjectfactory.newdataobject(dlg.GetData()) 1086 data.EnsureBitPimSerial() 1087 while True: 1088 key=int(time.time()) 1089 if key in self._data: 1090 continue 1091 break 1092 self._data[key]=data 1093 self.dt.OnDataUpdated() 1094 self.SetPreview(data) 1095 self.modified=True
1096
1097 - def GetSelectedRows(self):
1098 rows=[] 1099 # if there is no data, there can't be any selected rows 1100 if len(self._data)==0: 1101 return rows 1102 gcr=self.table.GetGridCursorRow() 1103 set1=self.table.GetSelectionBlockTopLeft() 1104 set2=self.table.GetSelectionBlockBottomRight() 1105 if len(set1): 1106 assert len(set1)==len(set2) 1107 for i in range(len(set1)): 1108 for row in range(set1[i][0], set2[i][0]+1): # range in wx is inclusive of last element 1109 if row not in rows: 1110 rows.append(row) 1111 else: 1112 if gcr>=0: 1113 rows.append(gcr) 1114 1115 return rows
1116
1117 - def GetSelectedRowKeys(self):
1118 return [self.dt.rowkeys[r] for r in self.GetSelectedRows()]
1119
1120 - def CanDelete(self):
1121 if self.read_only: 1122 return False 1123 # there always seems to be something selected in the phonebook, so 1124 # there is no point testing for number of items, it just burns cycles 1125 return True
1126
1127 - def OnDelete(self,_):
1128 if self.read_only: 1129 return 1130 for r in self.GetSelectedRowKeys(): 1131 del self._data[r] 1132 self.table.ClearSelection() 1133 self.dt.OnDataUpdated() 1134 self.modified=True
1135
1136 - def SetPreview(self, entry):
1137 self.preview.ShowEntry(entry)
1138
1139 - def CanPrint(self):
1140 return True
1141
1142 - def OnPrintDialog(self, mainwindow, config):
1143 with guihelper.WXDialogWrapper(PhonebookPrintDialog(self, mainwindow, config), 1144 True): 1145 pass
1146
1147 - def getdata(self, dict):
1148 dict['phonebook']=self._data.copy() 1149 dict['categories']=self.categories[:] 1150 dict['group_wallpapers']=self.group_wps[:] 1151 return dict
1152
1153 - def DeleteBySerial(self, bpserial):
1154 for k in self._data: 1155 entry=self._data[k] 1156 for serial in entry['serials']: 1157 if serial==bpserial: 1158 del self._data[k] 1159 self.dt.OnDataUpdated() 1160 self.modified=True 1161 return 1162 raise ValueError("No such entry with serial "+`bpserial`)
1163
1164 - def UpdateSerial(self, bpserial, otherserial):
1165 try: 1166 for k in self._data: 1167 entry=self._data[k] 1168 for serial in entry['serials']: 1169 if serial==bpserial: 1170 # this is the entry we have been looking for 1171 for i,serial in enumerate(entry['serials']): 1172 if serial["sourcetype"]==otherserial["sourcetype"]: 1173 if otherserial.has_key("sourceuniqueid") and \ 1174 serial["sourceuniqueid"]==otherserial["sourceuniqueid"]: 1175 # replace 1176 entry['serials'][i]=otherserial 1177 return 1178 elif not otherserial.has_key("sourceuniqueid"): 1179 entry['serials'][i]=otherserial 1180 return 1181 entry['serials'].append(otherserial) 1182 return 1183 raise ValueError("No such entry with serial "+`bpserial`) 1184 finally: 1185 self.modified=True
1186 1187
1188 - def versionupgrade(self, dict, version):
1189 """Upgrade old data format read from disk 1190 1191 @param dict: The dict that was read in 1192 @param version: version number of the data on disk 1193 """ 1194 1195 # version 0 to 1 upgrade 1196 if version==0: 1197 version=1 # they are the same 1198 1199 # 1 to 2 etc 1200 if version==1: 1201 wx.MessageBox("BitPim can't upgrade your old phone data stored on disk, and has discarded it. Please re-read your phonebook from the phone. If you downgrade, please delete the phonebook directory in the BitPim data directory first", "Phonebook file format not supported", wx.OK|wx.ICON_EXCLAMATION) 1202 version=2 1203 dict['result']['phonebook']={} 1204 dict['result']['categories']=[] 1205 dict['result']['group_wallpapers']=[]
1206
1207 - def clear(self):
1208 self._data={} 1209 self.dt.OnDataUpdated()
1210
1211 - def getfromfs(self, dict, timestamp=None):
1212 self.thedir=self.mainwindow.phonebookpath 1213 if os.path.exists(os.path.join(self.thedir, "index.idx")): 1214 d={'result': {'phonebook': {}, 'categories': [], 'group_wallpapers': [],}} 1215 common.readversionedindexfile(os.path.join(self.thedir, "index.idx"), d, self.versionupgrade, self.CURRENTFILEVERSION) 1216 pb=d['result']['phonebook'] 1217 database.ensurerecordtype(pb, phonebookobjectfactory) 1218 pb=database.extractbitpimserials(pb) 1219 self.mainwindow.database.savemajordict("phonebook", pb) 1220 self.mainwindow.database.savelist("categories", d['result']['categories']) 1221 self.mainwindow.database.savelist("group_wallpapers", d['result']['group_wallpapers']) 1222 # now that save is succesful, move file out of the way 1223 os.rename(os.path.join(self.thedir, "index.idx"), os.path.join(self.thedir, "index-is-now-in-database.bak")) 1224 # read info from the database 1225 dict['phonebook']=self.mainwindow.database.getmajordictvalues( 1226 "phonebook", phonebookobjectfactory, at_time=timestamp) 1227 dict['categories']=self.mainwindow.database.loadlist("categories") 1228 dict['group_wallpapers']=self.mainwindow.database.loadlist("group_wallpapers")
1229
1230 - def _updatecount(self):
1231 # Update the count of contatcs 1232 self.contactcount_label.SetLabel('%(count)d'%{ 'count': len(self._data) })
1233
1234 - def populate(self, dict, savetodb=True):
1235 if self.read_only and savetodb: 1236 wx.MessageBox('You are viewing historical data which cannot be changed or saved', 1237 'Cannot Save Phonebook Data', 1238 style=wx.OK|wx.ICON_ERROR) 1239 return 1240 self.clear() 1241 pubsub.publish(pubsub.MERGE_CATEGORIES, dict['categories']) 1242 pb=dict['phonebook'] 1243 cats=[] 1244 pb_groupwps=[] #list for groups found in phonebook 1245 for i in pb: 1246 for cat in pb[i].get('categories', []): 1247 cats.append(cat['category']) 1248 pb_groupwps.append(str(cat['category'])+":0") #the wallpaper is unknown as this point 1249 pubsub.publish(pubsub.MERGE_CATEGORIES, cats) 1250 pubsub.publish(pubsub.MERGE_GROUP_WALLPAPERS, pb_groupwps) #keep this order: pull out from phonebook FIRST 1251 pubsub.publish(pubsub.MERGE_GROUP_WALLPAPERS, dict['group_wallpapers']) #then add the ones from results dict 1252 k=pb.keys() 1253 k.sort() 1254 self.clear() 1255 self._data=pb.copy() 1256 self.dt.OnDataUpdated() 1257 self.modified=savetodb 1258 self._updatecount()
1259
1260 - def _save_db(self, dict):
1261 self.mainwindow.database.savemajordict("phonebook", database.extractbitpimserials(dict["phonebook"])) 1262 self.mainwindow.database.savelist("categories", dict["categories"]) 1263 self.mainwindow.database.savelist("group_wallpapers", dict["group_wallpapers"])
1264
1265 - def populatefs(self, dict):
1266 if self.read_only: 1267 wx.MessageBox('You are viewing historical data which cannot be changed or saved', 1268 'Cannot Save Phonebook Data', 1269 style=wx.OK|wx.ICON_ERROR) 1270 else: 1271 self._save_db(dict) 1272 self._updatecount() 1273 return dict
1274
1275 - def _ensure_unicode(self, data):
1276 # convert and ensure unicode fields 1277 for _key,_entry in data.items(): 1278 for _field_key,_field_value in _entry.items(): 1279 if _field_key=='names': 1280 for _idx,_item in enumerate(_field_value): 1281 for _subkey, _value in _item.items(): 1282 if isinstance(_value, str): 1283 _item[_subkey]=_value.decode('ascii', 'ignore')
1284
1285 - def importdata(self, importdata, categoriesinfo=[], merge=True, groupwpsinfo=[]):
1286 if self.read_only: 1287 wx.MessageBox('You are viewing historical data which cannot be changed or saved', 1288 'Cannot Save Phonebook Data', 1289 style=wx.OK|wx.ICON_ERROR) 1290 return 1291 if merge: 1292 d=self._data 1293 else: 1294 d={} 1295 normalise_data(importdata) 1296 self._ensure_unicode(importdata) 1297 with guihelper.WXDialogWrapper(ImportDialog(self, d, importdata), 1298 True) as (dlg, retcode): 1299 guiwidgets.save_size("PhoneImportMergeDialog", dlg.GetRect()) 1300 if retcode==wx.ID_OK: 1301 result=dlg.resultdata 1302 if result is not None: 1303 d={} 1304 database.ensurerecordtype(result, phonebookobjectfactory) 1305 database.ensurebitpimserials(result) 1306 d['phonebook']=result 1307 d['categories']=categoriesinfo 1308 d['group_wallpapers']=groupwpsinfo 1309 self.populatefs(d) 1310 self.populate(d, False)
1311
1312 - def converttophone(self, data):
1313 self.error_log.ClearMessages() 1314 self.mainwindow.phoneprofile.convertphonebooktophone(self, data) 1315 if self.error_log.MsgCount(): 1316 self.error_log.ShowMessages() 1317 return
1318 1319 ### 1320 ### The methods from here on are passed as the 'helper' to 1321 ### convertphonebooktophone in the phone profiles. One 1322 ### day they may move to a seperate class. 1323 ### 1324
1325 - def add_error_message(self, msg, priority=99):
1326 self.error_log.AddMessage(msg, priority)
1327
1328 - def log(self, msg):
1329 self.mainwindow.log(msg)
1330
1331 - class ConversionFailed(Exception):
1332 pass
1333
1334 - def _getentries(self, list, min, max, name):
1335 candidates=[] 1336 for i in list: 1337 # ::TODO:: possibly ensure that a key appears in each i 1338 candidates.append(i) 1339 if len(candidates)<min: 1340 # ::TODO:: log this 1341 raise self.ConversionFailed("Too few %s. Need at least %d but there were only %d" % (name,min,len(candidates))) 1342 if len(candidates)>max: 1343 # ::TODO:: mention this to user 1344 candidates=candidates[:max] 1345 return candidates
1346
1347 - def _getfield(self,list,name):
1348 res=[] 1349 for i in list: 1350 res.append(i[name]) 1351 return res
1352
1353 - def _truncatefields(self, list, truncateat):
1354 if truncateat is None: 1355 return list 1356 res=[] 1357 for i in list: 1358 if len(i)>truncateat: 1359 # ::TODO:: log truncation 1360 res.append(i[:truncateat]) 1361 else: 1362 res.append(i) 1363 return res
1364
1365 - def _findfirst(self, candidates, required, key, default):
1366 """Find first match in candidates that meets required and return value of key 1367 1368 @param candidates: list of dictionaries to search through 1369 @param required: a dict of what key/value pairs must exist in an entry 1370 @param key: for a matching entry, which key's value to return 1371 @param default: what value to return if there is no match 1372 """ 1373 for dict in candidates: 1374 ok=True 1375 for k in required: 1376 if dict[k]!=required[k]: 1377 ok=False 1378 break # really want break 2 1379 if not ok: 1380 continue 1381 return dict.get(key, default) 1382 return default
1383
1384 - def getfullname(self, names, min, max, truncateat=None):
1385 "Return at least min and at most max fullnames from the names list" 1386 # secret lastnamefirst setting 1387 if wx.GetApp().config.ReadInt("lastnamefirst", False): 1388 n=[nameparser.formatsimplelastfirst(nn) for nn in names] 1389 else: 1390 n=[nameparser.formatsimplename(nn) for nn in names] 1391 if len(n)<min: 1392 raise self.ConversionFailed("Too few names. Need at least %d but there were only %d" % (min, len(n))) 1393 if len(n)>max: 1394 n=n[:max] 1395 # ::TODO:: mention this 1396 return self._truncatefields(n, truncateat)
1397
1398 - def getcategory(self, categories, min, max, truncateat=None):
1399 "Return at least min and at most max categories from the categories list" 1400 return self._truncatefields(self._getfield(self._getentries(categories, min, max, "categories"), "category"), truncateat)
1401
1402 - def getemails(self, emails, min, max, truncateat=None):
1403 "Return at least min and at most max emails from the emails list" 1404 return self._truncatefields(self._getfield(self._getentries(emails, min, max, "emails"), "email"), truncateat)
1405
1406 - def geturls(self, urls, min, max, truncateat=None):
1407 "Return at least min and at most max urls from the urls list" 1408 return self._truncatefields(self._getfield(self._getentries(urls, min, max, "urls"), "url"), truncateat)
1409 1410
1411 - def getmemos(self, memos, min, max, truncateat=None):
1412 "Return at least min and at most max memos from the memos list" 1413 return self._truncatefields(self._getfield(self._getentries(memos, min, max, "memos"), "memo"), truncateat)
1414
1415 - def getnumbers(self, numbers, min, max):
1416 "Return at least min and at most max numbers from the numbers list" 1417 return self._getentries(numbers, min, max, "numbers")
1418
1419 - def getnumber(self, numbers, type, count=1, default=""):
1420 """Returns phone numbers of the type 1421 1422 @param numbers: The list of numbers 1423 @param type: The type, such as cell, home, office 1424 @param count: Which number to return (eg with type=home, count=2 the second 1425 home number is returned) 1426 @param default: What is returned if there is no such number""" 1427 for n in numbers: 1428 if n['type']==type: 1429 if count==1: 1430 return n['number'] 1431 count-=1 1432 return default
1433
1434 - def getserial(self, serials, sourcetype, id, key, default):
1435 "Gets a serial if it exists" 1436 return self._findfirst(serials, {'sourcetype': sourcetype, 'sourceuniqueid': id}, key, default)
1437
1438 - def getringtone(self, ringtones, use, default):
1439 "Gets a ringtone of type use" 1440 return self._findfirst(ringtones, {'use': use}, 'ringtone', default)
1441
1442 - def getwallpaper(self, wallpapers, use, default):
1443 "Gets a wallpaper of type use" 1444 return self._findfirst(wallpapers, {'use': use}, 'wallpaper', default)
1445
1446 - def getwallpaperindex(self, wallpapers, use, default):
1447 "Gets a wallpaper index of type use" 1448 return self._findfirst(wallpapers, {'use': use}, 'index', default)
1449
1450 - def getflag(self, flags, name, default):
1451 "Gets value of flag named name" 1452 for i in flags: 1453 if i.has_key(name): 1454 return i[name] 1455 return default
1456
1457 - def getmostpopularcategories(self, howmany, entries, reserved=[], truncateat=None, padnames=[]):
1458 """Returns the most popular categories 1459 1460 @param howmany: How many to return, including the reserved ones 1461 @param entries: A dict of the entries 1462 @param reserved: A list of reserved entries (ie must be present, no matter 1463 how popular) 1464 @param truncateat: How long to truncate the category names at 1465 @param padnames: if the list is less than howmany long, then add these on the end providing 1466 they are not already in the list 1467 @return: A list of the group names. The list starts with the members of 1468 reserved followed by the most popular groups 1469 """ 1470 # build a histogram 1471 freq={} 1472 for entry in entries: 1473 e=entries[entry] 1474 for cat in e.get('categories', []): 1475 n=cat['category'] 1476 if truncateat: n=n[:truncateat] # truncate 1477 freq[n]=1+freq.get(n,0) 1478 # sort 1479 freq=[(count,value) for value,count in freq.items()] 1480 freq.sort() 1481 freq.reverse() # most popular first 1482 # build a new list 1483 newl=reserved[:] 1484 for _, group in freq: 1485 if len(newl)==howmany: 1486 break 1487 if group not in newl: 1488 newl.append(group) 1489 # pad list out 1490 for p in padnames: 1491 if len(newl)==howmany: 1492 break 1493 if p not in newl: 1494 newl.append(p) 1495 1496 return newl
1497
1498 - def makeone(self, list, default):
1499 "Returns one item long list" 1500 if len(list)==0: 1501 return default 1502 assert len(list)==1 1503 return list[0]
1504
1505 - def filllist(self, list, numitems, blank):
1506 "makes list numitems long appending blank to get there" 1507 l=list[:] 1508 for dummy in range(len(l),numitems): 1509 l.append(blank) 1510 return l
1511
1512 1513 -class ImportCellRenderer(wx.grid.PyGridCellRenderer):
1514 SCALE=0.8 1515 1516 COLOURS=["HONEYDEW", "WHITE", "LEMON CHIFFON", "ROSYBROWN1"] 1517
1518 - def __init__(self, table, grid):
1519 wx.grid.PyGridCellRenderer.__init__(self) 1520 self.calc=False 1521 self.table=table
1522
1523 - def _calcattrs(self):
1524 grid=self.table.GetView() 1525 self.font=grid.GetDefaultCellFont() 1526 self.facename=self.font.GetFaceName() 1527 self.facesize=self.font.GetPointSize() 1528 self.textcolour=grid.GetDefaultCellTextColour() 1529 self.brushes=[wx.Brush(wx.NamedColour(c)) for c in self.COLOURS] 1530 self.pens=[wx.Pen(wx.NamedColour(c),1 , wx.SOLID) for c in self.COLOURS] 1531 self.selbrush=wx.Brush(grid.GetSelectionBackground(), wx.SOLID) 1532 self.selpen=wx.Pen(grid.GetSelectionBackground(), 1, wx.SOLID) 1533 self.selfg=grid.GetSelectionForeground() 1534 self.calc=True
1535
1536 - def Draw(self, grid, attr, dc, rect, row, col, isSelected):
1537 if not self.calc: self._calcattrs() 1538 1539 rowtype=self.table.GetRowType(row) 1540 dc.SetClippingRect(rect) 1541 1542 # clear the background 1543 dc.SetBackgroundMode(wx.SOLID) 1544 if isSelected: 1545 dc.SetBrush(self.selbrush) 1546 dc.SetPen(self.selpen) 1547 colour=self.selfg 1548 else: 1549 dc.SetBrush(self.brushes[rowtype]) 1550 dc.SetPen(self.pens[rowtype]) 1551 colour=self.textcolour 1552 1553 dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) 1554 1555 dc.SetBackgroundMode(wx.TRANSPARENT) 1556 dc.SetFont(self.font) 1557 1558 text = grid.GetTable().GetHtmlCellValue(row, col, colour) 1559 if len(text): 1560 bphtml.drawhtml(dc, 1561 wx.Rect(rect.x+2, rect.y+1, rect.width-4, rect.height-2), 1562 text, font=self.facename, size=self.facesize) 1563 dc.DestroyClippingRegion()
1564
1565 - def GetBestSize(self, grid, attr, dc, row, col):
1566 if not self.calc: self._calcattrs() 1567 text = grid.GetTable().GetHtmlCellValue(row, col) 1568 if not len(text): return (5,5) 1569 return bphtml.getbestsize(dc, text, font=self.facename, size=self.facesize)
1570
1571 - def Clone(self):
1572 return ImportCellRenderer()
1573
1574 1575 -class ImportDataTable(wx.grid.PyGridTableBase):
1576 ADDED=0 1577 UNALTERED=1 1578 CHANGED=2 1579 DELETED=3 1580 1581 htmltemplate=["Not set - "+`i` for i in range(15)] 1582
1583 - def __init__(self, widget):
1584 self.main=widget 1585 self.rowkeys=[] 1586 wx.grid.PyGridTableBase.__init__(self) 1587 self.columns=['Confidence']+ImportColumns
1588
1589 - def GetRowData(self, row):
1590 """Returns a 4 part tuple as defined in ImportDialog.rowdata 1591 for the numbered row""" 1592 return self.main.rowdata[self.rowkeys[row]]
1593
1594 - def GetColLabelValue(self, col):
1595 "Returns the label for the numbered column" 1596 return self.columns[col]
1597
1598 - def IsEmptyCell(self, row, col):
1599 return False
1600
1601 - def GetNumberCols(self):
1602 return len(self.columns)
1603
1604 - def GetNumberRows(self):
1605 return len(self.rowkeys)
1606
1607 - def GetRowType(self, row):
1608 """Returns what type the row is from DELETED, CHANGED, ADDED and UNALTERED""" 1609 row=self.GetRowData(row) 1610 if row[3] is None: 1611 return self.DELETED 1612 if row[1] is not None and row[2] is not None: 1613 return self.CHANGED 1614 if row[1] is not None and row[2] is None: 1615 return self.ADDED 1616 return self.UNALTERED
1617
1618 - def GetValueWithNamedColumn(self, row, columnname):
1619 row=self.main.rowdata[self.rowkeys[row]] 1620 if columnname=='Confidence': 1621 return row[0] 1622 1623 for i,ptr in (3,self.main.resultdata), (1,self.main.importdata), (2, self.main.existingdata): 1624 if row[i] is not None: 1625 return getdata(columnname, ptr[row[i]], "") 1626 assert False, "Can't get here" 1627 return ""
1628
1629 - def ShouldColumnBeShown(self, columnname, row):
1630 confidence, importedkey, existingkey, resultkey=self.GetRowData(row) 1631 if columnname=="Confidence": return True 1632 return (resultkey is not None and getdata(columnname, self.main.resultdata[resultkey], None) is not None) \ 1633 or (existingkey is not None and getdata(columnname, self.main.existingdata[existingkey], None) is not None) \ 1634 or (importedkey is not None and getdata(columnname, self.main.importdata[importedkey], None) is not None)
1635
1636 - def GetHtmlCellValue(self, row, col, colour=None):
1637 try: 1638 row=self.GetRowData(row) 1639 except: 1640 print "bad row", row 1641 return "&gt;error&lt;" 1642 1643 if colour is None: 1644 colour="#000000" # black 1645 else: 1646 colour="#%02X%02X%02X" % (colour.Red(), colour.Green(), colour.Blue()) 1647 1648 if self.columns[col]=='Confidence': 1649 # row[0] could be a zero length string or an integer 1650 if row[0]=="": return "" 1651 return '<font color="%s">%d</font>' % (colour, row[0]) 1652 1653 # Get values - note default of None 1654 imported,existing,result=None,None,None 1655 if row[1] is not None: 1656 imported=getdata(self.columns[col], self.main.importdata[row[1]], None) 1657 if imported is not None: imported=common.strorunicode(imported) 1658 if row[2] is not None: 1659 existing=getdata(self.columns[col], self.main.existingdata[row[2]], None) 1660 if existing is not None: existing=common.strorunicode(existing) 1661 if row[3] is not None: 1662 result=getdata(self.columns[col], self.main.resultdata[row[3]], None) 1663 if result is not None: result=common.strorunicode(result) 1664 1665 # The following code looks at the combinations of imported, 1666 # existing and result with them being None and/or equalling 1667 # each other. Remember that the user could have 1668 # editted/deleted an entry so the result may not match either 1669 # the imported or existing value. Each combination points to 1670 # an index in the templates table. Assertions are used 1671 # extensively to ensure the logic is correct 1672 1673 if imported is None and existing is None and result is None: 1674 return "" # idx=9 - shortcut 1675 1676 # matching function for this column 1677 matchfn=lambda x,y: x==y 1678 1679 # if the result field is missing then value was deleted 1680 if result is None: 1681 # one of them must be present otherwise idx=9 above would have matched 1682 assert imported is not None or existing is not None 1683 if imported is not None and existing is not None: 1684 if matchfn(imported, existing): 1685 idx=14 1686 else: 1687 idx=13 1688 else: 1689 if imported is None: 1690 assert existing is not None 1691 idx=11 1692 else: 1693 assert existing is None 1694 idx=12 1695 1696 else: 1697 if imported is None and existing is None: 1698 idx=10 1699 else: 1700 # we have a result - the first 8 entries need the following 1701 # comparisons 1702 if imported is not None: 1703 imported_eq_result= matchfn(imported,result) 1704 if existing is not None: 1705 existing_eq_result= matchfn(existing,result) 1706 1707 # a table of all possible combinations of imported/exporting 1708 # being None and imported_eq_result/existing_eq_result 1709 if imported is None and existing_eq_result: 1710 idx=0 1711 elif imported is None and not existing_eq_result: 1712 idx=1 1713 elif imported_eq_result and existing is None: 1714 idx=2 1715 elif not imported_eq_result and existing is None: 1716 idx=3 1717 elif imported_eq_result and existing_eq_result: 1718 idx=4 1719 elif imported_eq_result and not existing_eq_result: 1720 idx=5 1721 elif not imported_eq_result and existing_eq_result: 1722 idx=6 1723 elif not imported_eq_result and not existing_eq_result: 1724 # neither imported or existing are the same as result 1725 # are they the same as each other? 1726 if matchfn(imported, existing): 1727 idx=7 1728 else: 1729 idx=8 1730 else: 1731 assert False, "This is unpossible!" 1732 return "FAILED" 1733 1734 if False: # set to true to debug this 1735 return `idx`+" "+self.htmltemplate[idx] % { 'imported': _htmlfixup(imported), 1736 'existing': _htmlfixup(existing), 1737 'result': _htmlfixup(result), 1738 'colour': colour} 1739 1740 1741 return self.htmltemplate[idx] % { 'imported': _htmlfixup(imported), 1742 'existing': _htmlfixup(existing), 1743 'result': _htmlfixup(result), 1744 'colour': colour}
1745 1746 @guihelper.BusyWrapper
1747 - def OnDataUpdated(self):
1748 # update row keys 1749 newkeys=self.main.rowdata.keys() 1750 oldrows=self.rowkeys 1751 # rowkeys is kept in the same order as oldrows 1752 self.rowkeys=[k for k in oldrows if k in newkeys]+[k for k in newkeys if k not in oldrows] 1753 # now remove the ones that don't match checkboxes 1754 self.rowkeys=[self.rowkeys[n] for n in range(len(self.rowkeys)) if self.GetRowType(n) in self.main.show] 1755 # work out which columns we actually need 1756 colsavail=ImportColumns 1757 colsused=[] 1758 for row in range(len(self.rowkeys)): 1759 can=[] # cols available now 1760 for col in colsavail: 1761 if self.ShouldColumnBeShown(col, row): 1762 colsused.append(col) 1763 else: 1764 can.append(col) 1765 colsavail=can 1766 # colsused won't be in right order 1767 colsused=[c for c in ImportColumns if c in colsused] 1768 colsused=["Confidence"]+colsused 1769 lo=len(self.columns) 1770 ln=len(colsused) 1771 1772 try: 1773 sortcolumn=self.columns[self.main.sortedColumn] 1774 except IndexError: 1775 sortcolumn=0 1776 1777 # update columns 1778 self.columns=colsused 1779 if ln>lo: 1780 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_APPENDED, ln-lo) 1781 elif lo>ln: 1782 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_DELETED, 0, lo-ln) 1783 else: 1784 msg=None 1785 if msg is not None: 1786 self.GetView().ProcessTableMessage(msg) 1787 1788 # do sorting 1789 if sortcolumn not in self.columns: 1790 sortcolumn=1 1791 else: 1792 sortcolumn=self.columns.index(sortcolumn) 1793 1794 # we sort on lower case value, but note that not all columns are strings 1795 items=[] 1796 for row in range(len(self.rowkeys)): 1797 v=self.GetValue(row,sortcolumn) 1798 try: 1799 items.append((v.lower(), row)) 1800 except: 1801 items.append((v, row)) 1802 1803 items.sort() 1804 if self.main.sortedColumnDescending: 1805 items.reverse() 1806 self.rowkeys=[self.rowkeys[n] for _,n in items] 1807 1808 # update rows 1809 lo=len(oldrows) 1810 ln=len(self.rowkeys) 1811 if ln>lo: 1812 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, ln-lo) 1813 elif lo>ln: 1814 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, 0, lo-ln) 1815 else: 1816 msg=None 1817 if msg is not None: 1818 self.GetView().ProcessTableMessage(msg) 1819 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES) 1820 self.GetView().ProcessTableMessage(msg) 1821 self.GetView().ClearSelection() 1822 self.main.OnCellSelect() 1823 self.GetView().Refresh()
1824
1825 -def _htmlfixup(txt):
1826 if txt is None: return "" 1827 return txt.replace("&", "&amp;").replace("<", "&gt;").replace(">", "&lt;") \ 1828 .replace("\r\n", "<br>").replace("\r", "<br>").replace("\n", "<br>")
1829
1830 -def workaroundyetanotherwxpythonbug(method, *args):
1831 # grrr 1832 try: 1833 return method(*args) 1834 except TypeError: 1835 print "swallowed a type error in workaroundyetanotherwxpythonbug" 1836 pass
1837
1838 ### 1839 ### 0 thru 8 inclusive have a result present 1840 ### 1841 1842 # 0 - imported is None, existing equals result 1843 ImportDataTable.htmltemplate[0]='<font color="%(colour)s">%(result)s</font>' 1844 # 1 - imported is None, existing not equal result 1845 ImportDataTable.htmltemplate[1]='<font color="%(colour)s"><strike>%(result)s</strike><br><b><font size=-1>Existing</font></b> %(existing)s</font>' 1846 # 2 - imported equals result, existing is None 1847 ImportDataTable.htmltemplate[2]='<font color="%(colour)s"><strike>%(result)s</strike></font>' 1848 # 3 - imported not equal result, existing is None 1849 ImportDataTable.htmltemplate[3]='<font color="%(colour)s"><strike>%(result)s</strike><br><b><font size=-1>Imported</font></b> %(imported)s</font>' 1850 # 4 - imported equals result, existing equals result 1851 ImportDataTable.htmltemplate[4]=ImportDataTable.htmltemplate[0] # just display result 1852 # 5 - imported equals result, existing not equals result 1853 ImportDataTable.htmltemplate[5]='<font color="%(colour)s"><strike><font color="#00aa00">%(result)s</font></strike><br><b><font size=-1>Existing</font></b> %(existing)s</font>' 1854 # 6 - imported not equal result, existing equals result 1855 ImportDataTable.htmltemplate[6]='<font color="%(colour)s">%(result)s<br><b><font size=-1>Imported</font></b> %(imported)s</font>' 1856 # 7 - imported not equal result, existing not equal result, imported equals existing 1857 ImportDataTable.htmltemplate[7]='<font color="%(colour)s"><strike>%(result)s</strike><br><b><font size=-1>Imported/Existing</font></b> %(imported)s</font>' 1858 # 8 - imported not equal result, existing not equal result, imported not equals existing 1859 ImportDataTable.htmltemplate[8]='<font color="%(colour)s"><strike>%(result)s</strike><br><b><font size=-1>Imported</font></b> %(imported)s<br><b><font size=-1>Existing</font></b> %(existing)s</font>' 1860 1861 ### 1862 ### Two special cases 1863 ### 1864 1865 # 9 - imported, existing, result are all None 1866 ImportDataTable.htmltemplate[9]="" 1867 1868 # 10 - imported, existing are None and result is present 1869 ImportDataTable.htmltemplate[10]='<font color="%(colour)s"><strike>%(result)s</strike></b></font>' 1870 1871 ### 1872 ### From 10 onwards, there is no result field, but one or both of 1873 ### imported/existing are present which means the user deleted the 1874 ### resulting value 1875 ### 1876 1877 # 11 - imported is None and existing is present 1878 ImportDataTable.htmltemplate[11]='<font color="#aa0000">%(existing)s</font>' 1879 # 12 - imported is present and existing is None 1880 ImportDataTable.htmltemplate[12]='<font color="#aa0000"><font size=-1>%(imported)s</font></font>' # slightly smaller 1881 # 13 - imported != existing 1882 ImportDataTable.htmltemplate[13]='<font color="%(colour)s"><b><font size=-1>Existing</font></b> <font color="#aa0000">%(existing)s</font><br><b><font size=-1>Imported</font></b> <font color="#888888">%(imported)s</font></font>' 1883 # 14 - imported equals existing 1884 ImportDataTable.htmltemplate[14]='<font color="#aa0000">%(existing)s</font>' 1885 1886 1887 -class ImportDialog(wx.Dialog):
1888 "The dialog for mixing new (imported) data with existing data" 1889 1890
1891 - def __init__(self, parent, existingdata, importdata):
1892 wx.Dialog.__init__(self, parent, id=-1, title="Import Phonebook data", style=wx.CAPTION| 1893 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) 1894 1895 # the data already in the phonebook 1896 self.existingdata=existingdata 1897 # the data we are importing 1898 self.importdata=importdata 1899 # the resulting data 1900 self.resultdata={} 1901 # each row to display showing what happened, with ids pointing into above data 1902 # rowdata[0]=confidence 1903 # rowdata[1]=importdatakey 1904 # rowdata[2]=existingdatakey 1905 # rowdata[3]=resultdatakey 1906 self.rowdata={} 1907 1908 vbs=wx.BoxSizer(wx.VERTICAL) 1909 1910 bg=self.GetBackgroundColour() 1911 w=wx.html.HtmlWindow(self, -1, size=wx.Size(600,50), style=wx.html.HW_SCROLLBAR_NEVER) 1912 w.SetPage('<html><body BGCOLOR="#%02X%02X%02X">Your data is being imported and BitPim is showing what will happen below so you can confirm its actions.</body></html>' % (bg.Red(), bg.Green(), bg.Blue())) 1913 vbs.Add(w, 0, wx.EXPAND|wx.ALL, 5) 1914 1915 hbs=wx.BoxSizer(wx.HORIZONTAL) 1916 hbs.Add(wx.StaticText(self, -1, "Show entries"), 0, wx.EXPAND|wx.ALL,3) 1917 1918 self.cbunaltered=wx.CheckBox(self, wx.NewId(), "Unaltered") 1919 self.cbadded=wx.CheckBox(self, wx.NewId(), "Added") 1920 self.cbchanged=wx.CheckBox(self, wx.NewId(), "Merged") 1921 self.cbdeleted=wx.CheckBox(self, wx.NewId(), "Deleted") 1922 wx.EVT_CHECKBOX(self, self.cbunaltered.GetId(), self.OnCheckbox) 1923 wx.EVT_CHECKBOX(self, self.cbadded.GetId(), self.OnCheckbox) 1924 wx.EVT_CHECKBOX(self, self.cbchanged.GetId(), self.OnCheckbox) 1925 wx.EVT_CHECKBOX(self, self.cbdeleted.GetId(), self.OnCheckbox) 1926 1927 for i in self.cbunaltered, self.cbadded, self.cbchanged, self.cbdeleted: 1928 i.SetValue(True) 1929 hbs.Add(i, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.RIGHT, 7) 1930 1931 t=ImportDataTable 1932 self.show=[t.ADDED, t.UNALTERED, t.CHANGED, t.DELETED] 1933 1934 hbs.Add(wx.StaticText(self, -1, " "), 0, wx.EXPAND|wx.LEFT, 10) 1935 1936 vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) 1937 1938 splitter=wx.SplitterWindow(self,-1, style=wx.SP_3D|wx.SP_LIVE_UPDATE) 1939 splitter.SetMinimumPaneSize(20) 1940 1941 self.grid=wx.grid.Grid(splitter, wx.NewId()) 1942 self.table=ImportDataTable(self) 1943 1944 # this is a work around for various wxPython/wxWidgets bugs 1945 cr=ImportCellRenderer(self.table, self.grid) 1946 cr.IncRef() # wxPython bug 1947 self.grid.RegisterDataType("string", cr, None) # wxWidgets bug - it uses the string renderer rather than DefaultCellRenderer 1948 1949 self.grid.SetTable(self.table, False, wx.grid.Grid.wxGridSelectRows) 1950 self.grid.SetSelectionMode(wx.grid.Grid.wxGridSelectRows) 1951 self.grid.SetRowLabelSize(0) 1952 self.grid.EnableDragRowSize(True) 1953 self.grid.EnableEditing(False) 1954 self.grid.SetMargins(1,0) 1955 self.grid.EnableGridLines(False) 1956 1957 wx.grid.EVT_GRID_CELL_RIGHT_CLICK(self.grid, self.OnRightGridClick) 1958 wx.grid.EVT_GRID_SELECT_CELL(self.grid, self.OnCellSelect) 1959 wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self.grid, self.OnCellDClick) 1960 wx.EVT_PAINT(self.grid.GetGridColLabelWindow(), self.OnColumnHeaderPaint) 1961 wx.grid.EVT_GRID_LABEL_LEFT_CLICK(self.grid, self.OnGridLabelLeftClick) 1962 wx.grid.EVT_GRID_LABEL_LEFT_DCLICK(self.grid, self.OnGridLabelLeftClick) 1963 1964 self.resultpreview=PhoneEntryDetailsView(splitter, -1, "styles.xy", "pblayout.xy") 1965 1966 splitter.SplitVertically(self.grid, self.resultpreview) 1967 1968 vbs.Add(splitter, 1, wx.EXPAND|wx.ALL,5) 1969 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5) 1970 1971 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 1972 1973 self.SetSizer(vbs) 1974 self.SetAutoLayout(True) 1975 1976 self.config = parent.mainwindow.config 1977 guiwidgets.set_size("PhoneImportMergeDialog", self, screenpct=95, aspect=1.10) 1978 1979 self.MakeMenus() 1980 1981 self.sortedColumn=1 1982 self.sortedColumnDescending=False 1983 1984 wx.EVT_BUTTON(self, wx.ID_HELP, lambda _: wx.GetApp().displayhelpid(helpids.ID_DLG_PBMERGEENTRIES)) 1985 1986 # the splitter which adamantly insists it is 20 pixels wide no 1987 # matter how hard you try to convince it otherwise. so we force it 1988 self.splitter=splitter 1989 wx.CallAfter(self._setthedamnsplittersizeinsteadofbeingsostupid_thewindowisnot20pixelswide_isetthesizenolessthan3times_argggh) 1990 1991 wx.CallAfter(self.DoMerge)
1992 1993
1995 splitter=self.splitter 1996 w,_=splitter.GetSize() 1997 splitter.SetSashPosition(max(w/2, w-200))
1998 1999 # ::TODO:: this method and the copy earlier should be merged into a single mixin
2000 - def OnColumnHeaderPaint(self, evt):
2001 w = self.grid.GetGridColLabelWindow() 2002 dc = wx.PaintDC(w) 2003 font = dc.GetFont() 2004 dc.SetTextForeground(wx.BLACK) 2005 2006 # For each column, draw it's rectangle, it's column name, 2007 # and it's sort indicator, if appropriate: 2008 totColSize = -self.grid.GetViewStart()[0]*self.grid.GetScrollPixelsPerUnit()[0] 2009 for col in range(self.grid.GetNumberCols()): 2010 dc.SetBrush(wx.Brush("WHEAT", wx.TRANSPARENT)) 2011 colSize = self.grid.GetColSize(col) 2012 rect = (totColSize,0,colSize,32) 2013 # note abuse of bool to be integer 0/1 2014 dc.DrawRectangle(rect[0] - (col!=0), rect[1], rect[2] + (col!=0), rect[3]) 2015 totColSize += colSize 2016 2017 if col == self.sortedColumn: 2018 font.SetWeight(wx.BOLD) 2019 # draw a triangle, pointed up or down, at the 2020 # top left of the column. 2021 left = rect[0] + 3 2022 top = rect[1] + 3 2023 2024 dc.SetBrush(wx.Brush("WHEAT", wx.SOLID)) 2025 if self.sortedColumnDescending: 2026 dc.DrawPolygon([(left,top), (left+6,top), (left+3,top+4)]) 2027 else: 2028 dc.DrawPolygon([(left+3,top), (left+6, top+4), (left, top+4)]) 2029 else: 2030 font.SetWeight(wx.NORMAL) 2031 2032 dc.SetFont(font) 2033 dc.DrawLabel("%s" % self.grid.GetTable().GetColLabelValue(col), 2034 rect, wx.ALIGN_CENTER | wx.ALIGN_TOP)
2035
2036 - def OnGridLabelLeftClick(self, evt):
2037 col=evt.GetCol() 2038 if col==self.sortedColumn: 2039 self.sortedColumnDescending=not self.sortedColumnDescending 2040 else: 2041 self.sortedColumn=col 2042 self.sortedColumnDescending=False 2043 self.table.OnDataUpdated()
2044
2045 - def OnCheckbox(self, _):
2046 t=ImportDataTable 2047 vclist=((t.ADDED, self.cbadded), (t.UNALTERED, self.cbunaltered), 2048 (t.CHANGED, self.cbchanged), (t.DELETED, self.cbdeleted)) 2049 self.show=[v for v,c in vclist if c.GetValue()] 2050 if len(self.show)==0: 2051 for v,c in vclist: 2052 self.show.append(v) 2053 c.SetValue(True) 2054 self.table.OnDataUpdated()
2055 2056 @guihelper.BusyWrapper
2057 - def DoMerge(self):
2058 if len(self.existingdata)*len(self.importdata)>200: 2059 progdlg=wx.ProgressDialog("Merging entries", "BitPim is merging the new information into the existing information", 2060 len(self.existingdata), parent=self, style=wx.PD_APP_MODAL|wx.PD_CAN_ABORT|wx.PD_REMAINING_TIME) 2061 else: 2062 progdlg=None 2063 try: 2064 self._DoMerge(progdlg) 2065 finally: 2066 if progdlg: 2067 progdlg.Destroy() 2068 del progdlg
2069
2070 - def _DoMerge(self, progdlg):
2071 """Merges all the importdata with existing data 2072 2073 This can take quite a while! 2074 """ 2075 2076 # We go to great lengths to ensure that a copy of the import 2077 # and existing data is passed on to the routines we call and 2078 # data structures being built. Originally the code expected 2079 # the called routines to make copies of the data they were 2080 # copying/modifying, but it proved too error prone and often 2081 # ended up modifying the original/import data passed in. That 2082 # had the terrible side effect of meaning that your original 2083 # data got modified even if you pressed cancel! 2084 2085 count=0 2086 row={} 2087 results={} 2088 2089 em=EntryMatcher(self.existingdata, self.importdata) 2090 usedimportkeys=[] 2091 for progress,existingid in enumerate(self.existingdata.keys()): 2092 if progdlg: 2093 if not progdlg.Update(progress): 2094 # user cancelled 2095 wx.CallAfter(self.EndModal, wx.ID_CANCEL) 2096 return 2097 # does it match any imported entry 2098 merged=False 2099 for confidence, importid in em.bestmatches(existingid, limit=1): 2100 if confidence>90: 2101 if importid in usedimportkeys: 2102 # someone else already used this import, lets find out who was the better match 2103 for i in row: 2104 if row[i][1]==importid: 2105 break 2106 if confidence<row[i][0]: 2107 break # they beat us so this existing passed on an importmatch 2108 # we beat the other existing - undo their merge 2109 assert i==row[i][3] 2110 row[i]=("", None, row[i][2], row[i][3]) 2111 results[i]=copy.deepcopy(self.existingdata[row[i][2]]) 2112 2113 results[count]=self.MergeEntries(copy.deepcopy(self.existingdata[existingid]), 2114 copy.deepcopy(self.importdata[importid])) 2115 row[count]=(confidence, importid, existingid, count) 2116 # update counters etc 2117 count+=1 2118 usedimportkeys.append(importid) 2119 merged=True 2120 break # we are happy with this match 2121 if not merged: 2122 results[count]=copy.deepcopy(self.existingdata[existingid]) 2123 row[count]=("", None, existingid, count) 2124 count+=1 2125 2126 # which imports went unused? 2127 for importid in self.importdata: 2128 if importid in usedimportkeys: continue 2129 results[count]=copy.deepcopy(self.importdata[importid]) 2130 row[count]=("", importid, None, count) 2131 count+=1 2132 2133 # scan thru the merged ones, and see if anything actually changed 2134 for r in row: 2135 _, importid, existingid, resid=row[r] 2136 if importid is not None and existingid is not None: 2137 checkresult=copy.deepcopy(results[resid]) 2138 checkexisting=copy.deepcopy(self.existingdata[existingid]) 2139 # we don't care about serials changing ... 2140 if "serials" in checkresult: del checkresult["serials"] 2141 if "serials" in checkexisting: del checkexisting["serials"] 2142 2143 # another sort of false positive is if the name field 2144 # has "full" defined in existing and "first", "last" 2145 # in import, and the result ends up with "full", 2146 # "first" and "last" which looks different than 2147 # existing so it shows up us a change in the UI 2148 # despite the fact that it hasn't really changed if 2149 # full and first/last are consistent with each other. 2150 # Currently we just ignore this situation. 2151 2152 if checkresult == checkexisting: 2153 # lets pretend there was no import 2154 row[r]=("", None, existingid, resid) 2155 2156 self.rowdata=row 2157 self.resultdata=results 2158 self.table.OnDataUpdated()
2159
2160 - def MergeEntries(self, originalentry, importentry):
2161 "Take an original and a merge entry and join them together return a dict of the result" 2162 o=originalentry 2163 i=importentry 2164 result={} 2165 # Get the intersection. Anything not in this is not controversial 2166 intersect=dictintersection(o,i) 2167 for dict in i,o: 2168 for k in dict.keys(): 2169 if k not in intersect: 2170 result[k]=dict[k][:] 2171 # now only deal with keys in both 2172 for key in intersect: 2173 if key=="names": 2174 # we ignore anything except the first name. fields in existing take precedence 2175 r=i["names"][0] 2176 for k in o["names"][0]: 2177 r[k]=o["names"][0][k] 2178 result["names"]=[r] 2179 elif key=="numbers": 2180 result['numbers']=mergenumberlists(o['numbers'], i['numbers']) 2181 elif key=="urls": 2182 result['urls']=mergefields(o['urls'], i['urls'], 'url', cleaner=cleanurl) 2183 elif key=="emails": 2184 result['emails']=mergefields(o['emails'], i['emails'], 'email', cleaner=cleanemail) 2185 else: 2186 result[key]=common.list_union(o[key], i[key]) 2187 2188 return result
2189
2190 - def OnCellSelect(self, event=None):
2191 if event is not None: 2192 event.Skip() 2193 row=self.table.GetRowData(event.GetRow()) 2194 else: 2195 gcr=self.grid.GetGridCursorRow() 2196 if gcr>=0 and gcr<self.grid.GetNumberRows(): 2197 row=self.table.GetRowData(gcr) 2198 else: # table is empty 2199 row=None,None,None,None 2200 2201 confidence,importid,existingid,resultid=row 2202 if resultid is not None: 2203 self.resultpreview.ShowEntry(self.resultdata[resultid]) 2204 else: 2205 self.resultpreview.ShowEntry({})
2206 2207 # menu and right click handling 2208 2209 ID_EDIT_ITEM=wx.NewId() 2210 ID_REVERT_TO_IMPORTED=wx.NewId() 2211 ID_REVERT_TO_EXISTING=wx.NewId() 2212 ID_CLEAR_FIELD=wx.NewId() 2213 ID_IMPORTED_MISMATCH=wx.NewId() 2214
2215 - def MakeMenus(self):
2216 menu=wx.Menu() 2217 menu.Append(self.ID_EDIT_ITEM, "Edit...") 2218 menu.Append(self.ID_REVERT_TO_EXISTING, "Revert field to existing value") 2219 menu.Append(self.ID_REVERT_TO_IMPORTED, "Revert field to imported value") 2220 menu.Append(self.ID_CLEAR_FIELD, "Clear field") 2221 menu.AppendSeparator() 2222 menu.Append(self.ID_IMPORTED_MISMATCH, "Imported entry mismatch...") 2223 self.menu=menu 2224 2225 wx.EVT_MENU(menu, self.ID_EDIT_ITEM, self.OnEditItem) 2226 wx.EVT_MENU(menu, self.ID_REVERT_TO_EXISTING, self.OnRevertFieldToExisting) 2227 wx.EVT_MENU(menu, self.ID_REVERT_TO_IMPORTED, self.OnRevertFieldToImported) 2228 wx.EVT_MENU(menu, self.ID_CLEAR_FIELD, self.OnClearField) 2229 wx.EVT_MENU(menu, self.ID_IMPORTED_MISMATCH, self.OnImportedMismatch)
2230
2231 - def OnRightGridClick(self, event):
2232 row,col=event.GetRow(), event.GetCol() 2233 self.grid.SetGridCursor(row,col) 2234 self.grid.ClearSelection() 2235 # enable/disable stuff in the menu 2236 columnname=self.table.GetColLabelValue(col) 2237 _, importkey, existingkey, resultkey=self.table.GetRowData(row) 2238 2239 2240 if columnname=="Confidence": 2241 self.menu.Enable(self.ID_REVERT_TO_EXISTING, False) 2242 self.menu.Enable(self.ID_REVERT_TO_IMPORTED, False) 2243 self.menu.Enable(self.ID_CLEAR_FIELD, False) 2244 else: 2245 resultvalue=None 2246 if resultkey is not None: 2247 resultvalue=getdata(columnname, self.resultdata[resultkey], None) 2248 2249 self.menu.Enable(self.ID_REVERT_TO_EXISTING, existingkey is not None 2250 and getdata(columnname, self.existingdata[existingkey], None)!= resultvalue) 2251 self.menu.Enable(self.ID_REVERT_TO_IMPORTED, importkey is not None 2252 and getdata(columnname, self.importdata[importkey], None) != resultvalue) 2253 self.menu.Enable(self.ID_CLEAR_FIELD, True) 2254 2255 self.menu.Enable(self.ID_IMPORTED_MISMATCH, importkey is not None) 2256 # pop it up 2257 pos=event.GetPosition() 2258 self.grid.PopupMenu(self.menu, pos)
2259
2260 - def OnEditItem(self,_):
2261 self.EditEntry(self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol())
2262
2263 - def OnRevertFieldToExisting(self, _):
2264 row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol() 2265 columnname=self.table.GetColLabelValue(col) 2266 row=self.table.GetRowData(row) 2267 reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]]) 2268 exkey,exindex=getdatainfo(columnname, self.existingdata[row[2]]) 2269 if exindex is None: 2270 # actually need to clear the field 2271 self.OnClearField(None) 2272 return 2273 if resindex is None: 2274 self.resultdata[row[3]][reskey].append(copy.deepcopy(self.existingdata[row[2]][exkey][exindex])) 2275 elif resindex<0: 2276 self.resultdata[row[3]][reskey]=copy.deepcopy(self.existingdata[row[2]][exkey]) 2277 else: 2278 self.resultdata[row[3]][reskey][resindex]=copy.deepcopy(self.existingdata[row[2]][exkey][exindex]) 2279 self.table.OnDataUpdated()
2280
2281 - def OnRevertFieldToImported(self, _):
2282 row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol() 2283 columnname=self.table.GetColLabelValue(col) 2284 row=self.table.GetRowData(row) 2285 reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]]) 2286 imkey,imindex=getdatainfo(columnname, self.importdata[row[1]]) 2287 assert imindex is not None 2288 if resindex is None: 2289 self.resultdata[row[3]][reskey].append(copy.deepcopy(self.importdata[row[1]][imkey][imindex])) 2290 elif resindex<0: 2291 self.resultdata[row[3]][reskey]=copy.deepcopy(self.importdata[row[1]][imkey]) 2292 else: 2293 self.resultdata[row[3]][reskey][resindex]=copy.deepcopy(self.importdata[row[1]][imkey][imindex]) 2294 self.table.OnDataUpdated()
2295
2296 - def OnClearField(self, _):
2297 row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol() 2298 columnname=self.table.GetColLabelValue(col) 2299 row=self.table.GetRowData(row) 2300 reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]]) 2301 assert resindex is not None 2302 if resindex<0: 2303 del self.resultdata[row[3]][reskey] 2304 else: 2305 del self.resultdata[row[3]][reskey][resindex] 2306 self.table.OnDataUpdated()
2307
2308 - def OnImportedMismatch(self,_):
2309 # what are we currently matching 2310 row=self.grid.GetGridCursorRow() 2311 _,ourimportkey,existingmatchkey,resultkey=self.table.GetRowData(row) 2312 match=None 2313 # what are the choices 2314 choices=[] 2315 for row in range(self.table.GetNumberRows()): 2316 _,_,existingkey,_=self.table.GetRowData(row) 2317 if existingkey is not None: 2318 if existingmatchkey==existingkey: 2319 match=len(choices) 2320 choices.append( (getdata("Name", self.existingdata[existingkey], "<blank>"), existingkey) ) 2321 2322 with guihelper.WXDialogWrapper(ImportedEntryMatchDialog(self, choices, match), 2323 True) as (dlg, retcode): 2324 if retcode==wx.ID_OK: 2325 confidence,importkey,existingkey,resultkey=self.table.GetRowData(self.grid.GetGridCursorRow()) 2326 assert importkey is not None 2327 match=dlg.GetMatch() 2328 if match is None: 2329 # new entry 2330 if existingkey is None: 2331 wx.MessageBox("It is already a new entry!", wx.OK|wx.ICON_EXCLAMATION) 2332 return 2333 # make a new entry 2334 for rowdatakey in xrange(100000): 2335 if rowdatakey not in self.rowdata: 2336 for resultdatakey in xrange(100000): 2337 if resultdatakey not in self.resultdata: 2338 self.rowdata[rowdatakey]=("", importkey, None, resultdatakey) 2339 self.resultdata[resultdatakey]=copy.deepcopy(self.importdata[importkey]) 2340 # revert original one back 2341 self.resultdata[resultkey]=copy.deepcopy(self.existingdata[existingkey]) 2342 self.rowdata[self.table.rowkeys[self.grid.GetGridCursorRow()]]=("", None, existingkey, resultkey) 2343 self.table.OnDataUpdated() 2344 return 2345 assert False, "You really can't get here!" 2346 # match an existing entry 2347 ekey=choices[match][1] 2348 if ekey==existingkey: 2349 wx.MessageBox("That is already the entry matched!", wx.OK|wx.ICON_EXCLAMATION) 2350 return 2351 # find new match 2352 for r in range(self.table.GetNumberRows()): 2353 if r==self.grid.GetGridCursorRow(): continue 2354 confidence,importkey,existingkey,resultkey=self.table.GetRowData(r) 2355 if existingkey==ekey: 2356 if importkey is not None: 2357 wx.MessageBox("The new match already has an imported entry matching it!", "Already matched", wx.OK|wx.ICON_EXCLAMATION, self) 2358 return 2359 # clear out old match 2360 del self.rowdata[self.table.rowkeys[self.grid.GetGridCursorRow()]] 2361 # put in new one 2362 self.rowdata[self.table.rowkeys[r]]=(confidence, ourimportkey, ekey, resultkey) 2363 self.resultdata[resultkey]=self.MergeEntries( 2364 copy.deepcopy(self.existingdata[ekey]), 2365 copy.deepcopy(self.importdata[ourimportkey])) 2366 self.table.OnDataUpdated() 2367 return 2368 assert False, "Can't get here"
2369
2370 - def OnCellDClick(self, event):
2371 self.EditEntry(event.GetRow(), event.GetCol())
2372
2373 - def EditEntry(self, row, col=None):
2374 row=self.table.GetRowData(row) 2375 k=row[3] 2376 # if k is none then this entry has been deleted. fix this ::TODO:: 2377 assert k is not None 2378 data=self.resultdata[k] 2379 if col is not None: 2380 columnname=self.table.GetColLabelValue(col) 2381 if columnname=="Confidence": 2382 columnname="Name" 2383 else: 2384 columnname="Name" 2385 datakey, dataindex=getdatainfo(columnname, data) 2386 with guihelper.WXDialogWrapper(phonebookentryeditor.Editor(self, data, keytoopenon=datakey, dataindex=dataindex), 2387 True) as (dlg, retcode): 2388 if retcode==wx.ID_OK: 2389 data=dlg.GetData() 2390 self.resultdata[k]=data 2391 self.table.OnDataUpdated()
2392
2393 -class ImportedEntryMatchDialog(wx.Dialog):
2394 "The dialog shown to select how an imported entry should match" 2395
2396 - def __init__(self, parent, choices, match):
2397 wx.Dialog.__init__(self, parent, id=-1, title="Select Import Entry Match", style=wx.CAPTION| 2398 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) 2399 2400 self.choices=choices 2401 self.importdialog=parent 2402 2403 vbs=wx.BoxSizer(wx.VERTICAL) 2404 hbs=wx.BoxSizer(wx.HORIZONTAL) 2405 self.matchexisting=wx.RadioButton(self, wx.NewId(), "Matches an existing entry below", style=wx.RB_GROUP) 2406 self.matchnew=wx.RadioButton(self, wx.NewId(), "Is a new entry") 2407 hbs.Add(self.matchexisting, wx.NewId(), wx.ALIGN_CENTRE|wx.ALL, 5) 2408 hbs.Add(self.matchnew, wx.NewId(), wx.ALIGN_CENTRE|wx.ALL, 5) 2409 vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) 2410 2411 wx.EVT_RADIOBUTTON(self, self.matchexisting.GetId(), self.OnRBClicked) 2412 wx.EVT_RADIOBUTTON(self, self.matchnew.GetId(), self.OnRBClicked) 2413 2414 splitter=wx.SplitterWindow(self, -1, style=wx.SP_3D|wx.SP_LIVE_UPDATE) 2415 self.nameslb=wx.ListBox(splitter, wx.NewId(), choices=[name for name,id in choices], style=wx.LB_SINGLE|wx.LB_NEEDED_SB) 2416 self.preview=PhoneEntryDetailsView(splitter, -1) 2417 splitter.SplitVertically(self.nameslb, self.preview) 2418 vbs.Add(splitter, 1, wx.EXPAND|wx.ALL, 5) 2419 2420 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5) 2421 2422 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 2423 2424 wx.EVT_LISTBOX(self, self.nameslb.GetId(), self.OnLbClicked) 2425 wx.EVT_LISTBOX_DCLICK(self, self.nameslb.GetId(), self.OnLbDClicked) 2426 2427 # set values 2428 if match is None: 2429 self.matchexisting.SetValue(False) 2430 self.matchnew.SetValue(True) 2431 self.nameslb.Enable(False) 2432 else: 2433 self.matchexisting.SetValue(True) 2434 self.matchnew.SetValue(False) 2435 self.nameslb.Enable(True) 2436 self.nameslb.SetSelection(match) 2437 self.preview.ShowEntry(self.importdialog.existingdata[choices[match][1]]) 2438 2439 self.SetSizer(vbs) 2440 self.SetAutoLayout(True) 2441 guiwidgets.set_size("PhonebookImportEntryMatcher", self, screenpct=75, aspect=0.58) 2442 2443 wx.EVT_MENU(self, wx.ID_OK, self.SaveSize) 2444 wx.EVT_MENU(self, wx.ID_CANCEL, self.SaveSize)
2445 2446
2447 - def SaveSize(self, evt=None):
2448 if evt is not None: 2449 evt.Skip() 2450 guiwidgets.save_size("PhonebookImportEntryMatcher", self.GetRect())
2451
2452 - def OnRBClicked(self, _):
2453 self.nameslb.Enable(self.matchexisting.GetValue())
2454
2455 - def OnLbClicked(self,_=None):
2456 existingid=self.choices[self.nameslb.GetSelection()][1] 2457 self.preview.ShowEntry(self.importdialog.existingdata[existingid])
2458
2459 - def OnLbDClicked(self,_):
2460 self.OnLbClicked() 2461 self.SaveSize() 2462 self.EndModal(wx.ID_OK)
2463
2464 - def GetMatch(self):
2465 if self.matchnew.GetValue(): 2466 return None # new entry 2467 return self.nameslb.GetSelection()
2468
2469 -def dictintersection(one,two):
2470 return filter(two.has_key, one.keys())
2471
2472 -class EntryMatcher:
2473 "Implements matching phonebook entries" 2474
2475 - def __init__(self, sources, against):
2476 self.sources=sources 2477 self.against=against
2478
2479 - def bestmatches(self, sourceid, limit=5):
2480 """Gives best matches out of against list 2481 2482 @return: list of tuples of (percent match, againstid) 2483 """ 2484 2485 res=[] 2486 2487 source=self.sources[sourceid] 2488 for i in self.against: 2489 against=self.against[i] 2490 2491 # now get keys source and against have in common 2492 intersect=dictintersection(source,against) 2493 2494 # overall score for this match 2495 score=0 2496 count=0 2497 for key in intersect: 2498 s=source[key] 2499 a=against[key] 2500 count+=1 2501 if s==a: 2502 score+=40*len(s) 2503 continue 2504 2505 if key=="names": 2506 score+=comparenames(s,a) 2507 elif key=="numbers": 2508 score+=comparenumbers(s,a) 2509 elif key=="urls": 2510 score+=comparefields(s,a,"url") 2511 elif key=="emails": 2512 score+=comparefields(s,a,"email") 2513 elif key=="addresses": 2514 score+=compareallfields(s,a, ("company", "street", "street2", "city", "state", "postalcode", "country")) 2515 else: 2516 # ignore it 2517 count-=1 2518 2519 if count: 2520 res.append( ( int(score*100/count), i ) ) 2521 2522 res.sort() 2523 res.reverse() 2524 if len(res)>limit: 2525 return res[:limit] 2526 return res
2527
2528 -def comparenames(s,a):
2529 "Give a score on two names" 2530 return (jarowinkler(nameparser.formatsimplename(s[0]), nameparser.formatsimplename(a[0]))-0.6)*10
2531
2532 -def cleanurl(url, mode="compare"):
2533 """Returns lowercase url with the "http://" prefix removed and in lower case 2534 2535 @param mode: If the value is compare (default), it removes ""http://www."" 2536 in preparation for comparing entries. Otherwise, if the value 2537 is pb, the result is formatted for writing to the phonebook. 2538 """ 2539 if mode == "compare": 2540 urlprefix=re.compile("^(http://)?(www.)?") 2541 else: urlprefix=re.compile("^(http://)?") 2542 2543 return default_cleaner(re.sub(urlprefix, "", url).lower())
2544
2545 -def cleanemail(email, mode="compare"):
2546 """Returns lowercase email 2547 """ 2548 return default_cleaner(email.lower())
2549 2550 2551 nondigits=re.compile("[^0-9]")
2552 -def cleannumber(num):
2553 "Returns num (a phone number) with all non-digits removed" 2554 return re.sub(nondigits, "", num)
2555
2556 -def comparenumbers(s,a):
2557 """Give a score on two phone numbers 2558 2559 """ 2560 2561 ss=[cleannumber(x['number']) for x in s] 2562 aa=[cleannumber(x['number']) for x in a] 2563 2564 candidates=[] 2565 for snum in ss: 2566 for anum in aa: 2567 candidates.append( (jarowinkler(snum, anum), snum, anum) ) 2568 2569 candidates.sort() 2570 candidates.reverse() 2571 2572 if len(candidates)>3: 2573 candidates=candidates[:3] 2574 2575 score=0 2576 # we now have 3 best matches 2577 for ratio,snum,anum in candidates: 2578 if ratio>0.9: 2579 score+=(ratio-0.9)*10 2580 2581 return score
2582
2583 -def comparefields(s,a,valuekey,threshold=0.8,lookat=3):
2584 """Compares the valuekey field in source and against lists returning a score for closeness of match""" 2585 ss=[x[valuekey] for x in s if x.has_key(valuekey)] 2586 aa=[x[valuekey] for x in a if x.has_key(valuekey)] 2587 2588 candidates=[] 2589 for sval in ss: 2590 for aval in aa: 2591 candidates.append( (jarowinkler(sval, aval), sval, aval) ) 2592 2593 candidates.sort() 2594 candidates.reverse() 2595 2596 if len(candidates)>lookat: 2597 candidates=candidates[:lookat] 2598 2599 score=0 2600 # we now have 3 best matches 2601 for ratio,sval,aval in candidates: 2602 if ratio>threshold: 2603 score+=(ratio-threshold)*10/(1-threshold) 2604 2605 return score
2606
2607 -def compareallfields(s,a,fields,threshold=0.8,lookat=3):
2608 """Like comparefields, but for source and against lists where multiple keys have values in each item 2609 2610 @param fields: This should be a list of keys from the entries that are in the order the human 2611 would write them down.""" 2612 2613 # we do it in "write them down order" as that means individual values that move don't hurt the matching 2614 # much (eg if the town was wrongly in address2 and then moved into town, the concatenated string would 2615 # still be the same and it would still be an appropriate match) 2616 args=[] 2617 for d in s,a: 2618 str="" 2619 list=[] 2620 for entry in d: 2621 for f in fields: 2622 # we merge together the fields space separated in order to make one long string from the values 2623 if entry.has_key(f): 2624 str+=entry.get(f)+" " 2625 list.append( {'value': str} ) 2626 args.append( list ) 2627 # and then give the result to comparefields 2628 args.extend( ['value', threshold, lookat] ) 2629 return comparefields(*args)
2630
2631 -def mergenumberlists(orig, imp):
2632 """Return the results of merging two lists of numbers 2633 2634 We compare the sanitised numbers (ie after punctuation etc is stripped 2635 out). If they are the same, then the original is kept (since the number 2636 is the same, and the original most likely has the correct punctuation). 2637 2638 Otherwise the imported entries overwrite the originals 2639 """ 2640 # results start with existing entries 2641 res=[] 2642 res.extend(orig) 2643 # look through each imported number 2644 for i in imp: 2645 num=cleannumber(i['number']) 2646 found=False 2647 for r in res: 2648 if num==cleannumber(r['number']): 2649 # an existing entry was matched so we stop 2650 found=True 2651 if i.has_key('speeddial'): 2652 r['speeddial']=i['speeddial'] 2653 break 2654 if found: 2655 continue 2656 2657 # we will be replacing one of the same type 2658 found=False 2659 for r in res: 2660 if i['type']==r['type']: 2661 r['number']=i['number'] 2662 if i.has_key('speeddial'): 2663 r['speeddial']=i['speeddial'] 2664 found=True 2665 break 2666 if found: 2667 continue 2668 # ok, just add it on the end then 2669 res.append(i) 2670 2671 return res
2672 2673 # new jaro winkler implementation doesn't use '*' chars or similar mangling 2674 default_cleaner=lambda x: x
2675 2676 -def mergefields(orig, imp, field, threshold=0.88, cleaner=default_cleaner):
2677 """Return the results of merging two lists of fields 2678 2679 We compare the fields. If they are the same, then the original is kept 2680 (since the name is the same, and the original most likely has the 2681 correct punctuation). 2682 2683 Otherwise the imported entries overwrite the originals 2684 """ 2685 # results start with existing entries 2686 res=[] 2687 res.extend(orig) 2688 # look through each imported field 2689 for i in imp: 2690 2691 impfield=cleaner(i[field]) 2692 2693 found=False 2694 for r in res: 2695 # if the imported entry is similar or the same as the 2696 # original entry, then we stop 2697 2698 # add code for short or long lengths 2699 # since cell phones usually have less than 16-22 chars max per field 2700 2701 resfield=cleaner(r[field]) 2702 2703 if (comparestrings(resfield, impfield) > threshold): 2704 # an existing entry was matched so we stop 2705 found=True 2706 2707 # since new item matches, we don't need to replace the 2708 # original value, but we should update the type of item 2709 # to reflect the imported value 2710 # for example home --> business 2711 if i.has_key('type'): 2712 r['type'] = i['type'] 2713 2714 # break out of original item loop 2715 break 2716 2717 # if we have found the item to be imported, we can move to the next one 2718 if found: 2719 continue 2720 2721 # since there is no matching item, we will replace the existing item 2722 # if a matching type exists 2723 found=False 2724 for r in res: 2725 if (i.has_key('type') and r.has_key('type')): 2726 if i['type']==r['type']: 2727 # write the field entry in the way the phonebook expects it 2728 r[field]=cleaner(i[field], "pb") 2729 found=True 2730 break 2731 if found: 2732 continue 2733 # add new item on the end if there no matching type 2734 # and write the field entry in the way the phonebook expects it 2735 i[field] = cleaner(i[field], "pb") 2736 res.append(i) 2737 2738 return res
2739 2740 import native.strings 2741 jarowinkler=native.strings.jarow
2742 2743 -def comparestrings(origfield, impfield):
2744 """ Compares two strings and returns the score using 2745 winkler routine from Febrl (stringcmp.py) 2746 2747 Return value is between 0.0 and 1.0, where 0.0 means no similarity 2748 whatsoever, and 1.0 means the strings match exactly.""" 2749 return jarowinkler(origfield, impfield, 16)
2750
2751 -def normalise_data(entries):
2752 for k in entries: 2753 # we only know about phone numbers so far ... 2754 for n in entries[k].get("numbers", []): 2755 n["number"]=phonenumber.normalise(n["number"])
2756
2757 -class ColumnSelectorDialog(wx.Dialog):
2758 "The dialog for selecting what columns you want to view" 2759 2760 ID_SHOW=wx.NewId() 2761 ID_AVAILABLE=wx.NewId() 2762 ID_UP=wx.NewId() 2763 ID_DOWN=wx.NewId() 2764 ID_ADD=wx.NewId() 2765 ID_REMOVE=wx.NewId() 2766 ID_DEFAULT=wx.NewId() 2767
2768 - def __init__(self, parent, config, phonewidget):
2769 wx.Dialog.__init__(self, parent, id=-1, title="Select Columns to view", style=wx.CAPTION| 2770 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) 2771 2772 self.config=config 2773 self.phonewidget=phonewidget 2774 hbs=wx.BoxSizer(wx.HORIZONTAL) 2775 2776 # the show bit 2777 bs=wx.BoxSizer(wx.VERTICAL) 2778 bs.Add(wx.StaticText(self, -1, "Showing"), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 2779 self.show=wx.ListBox(self, self.ID_SHOW, style=wx.LB_SINGLE|wx.LB_NEEDED_SB, size=(250, 300)) 2780 bs.Add(self.show, 1, wx.EXPAND|wx.ALL, 5) 2781 hbs.Add(bs, 1, wx.EXPAND|wx.ALL, 5) 2782 2783 # the column of buttons 2784 bs=wx.BoxSizer(wx.VERTICAL) 2785 self.up=wx.Button(self, self.ID_UP, "Move Up") 2786 self.down=wx.Button(self, self.ID_DOWN, "Move Down") 2787 self.add=wx.Button(self, self.ID_ADD, "Show") 2788 self.remove=wx.Button(self, self.ID_REMOVE, "Don't Show") 2789 self.default=wx.Button(self, self.ID_DEFAULT, "Default") 2790 2791 for b in self.up, self.down, self.add, self.remove, self.default: 2792 bs.Add(b, 0, wx.ALL|wx.ALIGN_CENTRE, 10) 2793 2794 hbs.Add(bs, 0, wx.ALL|wx.ALIGN_CENTRE, 5) 2795 2796 # the available bit 2797 bs=wx.BoxSizer(wx.VERTICAL) 2798 bs.Add(wx.StaticText(self, -1, "Available"), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 2799 self.available=wx.ListBox(self, self.ID_AVAILABLE, style=wx.LB_EXTENDED|wx.LB_NEEDED_SB, choices=AvailableColumns) 2800 bs.Add(self.available, 1, wx.EXPAND|wx.ALL, 5) 2801 hbs.Add(bs, 1, wx.EXPAND|wx.ALL, 5) 2802 2803 # main layout 2804 vbs=wx.BoxSizer(wx.VERTICAL) 2805 vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 5) 2806 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5) 2807 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 2808 2809 self.SetSizer(vbs) 2810 vbs.Fit(self) 2811 2812 # fill in current selection 2813 cur=self.config.Read("phonebookcolumns", "") 2814 if len(cur): 2815 cur=cur.split(",") 2816 # ensure they all exist 2817 cur=[c for c in cur if c in AvailableColumns] 2818 else: 2819 cur=DefaultColumns 2820 self.show.Set(cur) 2821 2822 # buttons, events etc 2823 self.up.Disable() 2824 self.down.Disable() 2825 self.add.Disable() 2826 self.remove.Disable() 2827 2828 wx.EVT_LISTBOX(self, self.ID_SHOW, self.OnShowClicked) 2829 wx.EVT_LISTBOX_DCLICK(self, self.ID_SHOW, self.OnShowClicked) 2830 wx.EVT_LISTBOX(self, self.ID_AVAILABLE, self.OnAvailableClicked) 2831 wx.EVT_LISTBOX_DCLICK(self, self.ID_AVAILABLE, self.OnAvailableDClicked) 2832 2833 wx.EVT_BUTTON(self, self.ID_ADD, self.OnAdd) 2834 wx.EVT_BUTTON(self, self.ID_REMOVE, self.OnRemove) 2835 wx.EVT_BUTTON(self, self.ID_UP, self.OnUp) 2836 wx.EVT_BUTTON(self, self.ID_DOWN, self.OnDown) 2837 wx.EVT_BUTTON(self, self.ID_DEFAULT, self.OnDefault) 2838 wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)
2839
2840 - def OnShowClicked(self, _=None):
2841 self.up.Enable(self.show.GetSelection()>0) 2842 self.down.Enable(self.show.GetSelection()<self.show.GetCount()-1) 2843 self.remove.Enable(self.show.GetCount()>0) 2844 self.FindWindowById(wx.ID_OK).Enable(self.show.GetCount()>0)
2845
2846 - def OnAvailableClicked(self, _):
2847 self.add.Enable(True)
2848
2849 - def OnAvailableDClicked(self, _):
2850 self.OnAdd()
2851
2852 - def OnAdd(self, _=None):
2853 items=[AvailableColumns[i] for i in self.available.GetSelections()] 2854 for i in self.available.GetSelections(): 2855 self.available.Deselect(i) 2856 self.add.Disable() 2857 it=self.show.GetSelection() 2858 if it>=0: 2859 self.show.Deselect(it) 2860 it+=1 2861 else: 2862 it=self.show.GetCount() 2863 self.show.InsertItems(items, it) 2864 self.remove.Disable() 2865 self.up.Disable() 2866 self.down.Disable() 2867 self.show.SetSelection(it) 2868 self.OnShowClicked()
2869
2870 - def OnRemove(self, _):
2871 it=self.show.GetSelection() 2872 assert it>=0 2873 self.show.Delete(it) 2874 if self.show.GetCount(): 2875 if it==self.show.GetCount(): 2876 self.show.SetSelection(it-1) 2877 else: 2878 self.show.SetSelection(it) 2879 self.OnShowClicked()
2880
2881 - def OnDefault(self,_):
2882 self.show.Set(DefaultColumns) 2883 self.show.SetSelection(0) 2884 self.OnShowClicked()
2885
2886 - def OnUp(self, _):
2887 it=self.show.GetSelection() 2888 assert it>=1 2889 self.show.InsertItems([self.show.GetString(it)], it-1) 2890 self.show.Delete(it+1) 2891 self.show.SetSelection(it-1) 2892 self.OnShowClicked()
2893
2894 - def OnDown(self, _):
2895 it=self.show.GetSelection() 2896 assert it<self.show.GetCount()-1 2897 self.show.InsertItems([self.show.GetString(it)], it+2) 2898 self.show.Delete(it) 2899 self.show.SetSelection(it+1) 2900 self.OnShowClicked()
2901
2902 - def OnOk(self, event):
2903 cur=[self.show.GetString(i) for i in range(self.show.GetCount())] 2904 self.config.Write("phonebookcolumns", ",".join(cur)) 2905 self.config.Flush() 2906 self.phonewidget.SetColumns(cur) 2907 event.Skip()
2908
2909 -class PhonebookPrintDialog(wx.Dialog):
2910 2911 ID_SELECTED=wx.NewId() 2912 ID_ALL=wx.NewId() 2913 ID_LAYOUT=wx.NewId() 2914 ID_STYLES=wx.NewId() 2915 ID_PRINT=wx.NewId() 2916 ID_PAGESETUP=wx.NewId() 2917 ID_PRINTPREVIEW=wx.NewId() 2918 ID_CLOSE=wx.ID_CANCEL 2919 ID_HELP=wx.NewId() 2920 ID_TEXTSCALE=wx.NewId() 2921 ID_SAVEASHTML=wx.NewId() 2922 2923 textscales=[ (0.4, "Teeny"), (0.6, "Tiny"), (0.8, "Small"), (1.0, "Normal"), (1.2, "Large"), (1.4, "Ginormous") ] 2924 # we reverse the order so the slider seems more natural 2925 textscales.reverse() 2926
2927 - def __init__(self, phonewidget, mainwindow, config):
2928 wx.Dialog.__init__(self, mainwindow, id=-1, title="Print PhoneBook", style=wx.CAPTION| 2929 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) 2930 2931 self.config=config 2932 self.phonewidget=phonewidget 2933 2934 # sort out available layouts and styles 2935 # first line is description 2936 self.layoutfiles={} 2937 for _file in guihelper.getresourcefiles("pbpl-*.xy"): 2938 with file(_file, 'rt') as f: 2939 desc=f.readline().strip() 2940 self.layoutfiles[desc]=f.read() 2941 self.stylefiles={} 2942 for _file in guihelper.getresourcefiles("pbps-*.xy"): 2943 with file(_file, 'rt') as f: 2944 desc=f.readline().strip() 2945 self.stylefiles[desc]=f.read() 2946 2947 # Layouts 2948 vbs=wx.BoxSizer(wx.VERTICAL) # main vertical sizer 2949 2950 hbs=wx.BoxSizer(wx.HORIZONTAL) # first row 2951 2952 numselected=len(phonewidget.GetSelectedRows()) 2953 numtotal=len(phonewidget._data) 2954 2955 # selection and scale 2956 vbs2=wx.BoxSizer(wx.VERTICAL) 2957 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Rows"), wx.VERTICAL) 2958 self.selected=wx.RadioButton(self, self.ID_SELECTED, "Selected (%d)" % (numselected,), style=wx.RB_GROUP) 2959 self.all=wx.RadioButton(self, self.ID_SELECTED, "All (%d)" % (numtotal,) ) 2960 bs.Add(self.selected, 0, wx.EXPAND|wx.ALL, 2) 2961 bs.Add(self.all, 0, wx.EXPAND|wx.ALL, 2) 2962 self.selected.SetValue(numselected>1) 2963 self.all.SetValue(not (numselected>1)) 2964 vbs2.Add(bs, 0, wx.EXPAND|wx.ALL, 2) 2965 2966 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Text Scale"), wx.HORIZONTAL) 2967 for i in range(len(self.textscales)): 2968 if self.textscales[i][0]==1.0: 2969 sv=i 2970 break 2971 self.textscaleslider=wx.Slider(self, self.ID_TEXTSCALE, sv, 0, len(self.textscales)-1, style=wx.SL_VERTICAL|wx.SL_AUTOTICKS) 2972 self.scale=1 2973 bs.Add(self.textscaleslider, 0, wx.EXPAND|wx.ALL, 2) 2974 self.textscalelabel=wx.StaticText(self, -1, "Normal") 2975 bs.Add(self.textscalelabel, 0, wx.ALIGN_CENTRE) 2976 vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2) 2977 hbs.Add(vbs2, 0, wx.EXPAND|wx.ALL, 2) 2978 2979 # Sort 2980 self.sortkeyscb=[] 2981 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Sorting"), wx.VERTICAL) 2982 choices=["<None>"]+AvailableColumns 2983 for i in range(3): 2984 bs.Add(wx.StaticText(self, -1, ("Sort by", "Then")[i!=0]), 0, wx.EXPAND|wx.ALL, 2) 2985 self.sortkeyscb.append(wx.ComboBox(self, wx.NewId(), "<None>", choices=choices, style=wx.CB_READONLY)) 2986 self.sortkeyscb[-1].SetSelection(0) 2987 bs.Add(self.sortkeyscb[-1], 0, wx.EXPAND|wx.ALL, 2) 2988 hbs.Add(bs, 0, wx.EXPAND|wx.ALL, 4) 2989 2990 # Layout and style 2991 vbs2=wx.BoxSizer(wx.VERTICAL) # they are on top of each other 2992 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Layout"), wx.VERTICAL) 2993 k=self.layoutfiles.keys() 2994 k.sort() 2995 self.layout=wx.ListBox(self, self.ID_LAYOUT, style=wx.LB_SINGLE|wx.LB_NEEDED_SB|wx.LB_HSCROLL, choices=k, size=(150,-1)) 2996 self.layout.SetSelection(0) 2997 bs.Add(self.layout, 1, wx.EXPAND|wx.ALL, 2) 2998 vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2) 2999 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Styles"), wx.VERTICAL) 3000 k=self.stylefiles.keys() 3001 self.styles=wx.CheckListBox(self, self.ID_STYLES, choices=k) 3002 bs.Add(self.styles, 1, wx.EXPAND|wx.ALL, 2) 3003 vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2) 3004 hbs.Add(vbs2, 1, wx.EXPAND|wx.ALL, 2) 3005 3006 # Buttons 3007 vbs2=wx.BoxSizer(wx.VERTICAL) 3008 vbs2.Add(wx.Button(self, self.ID_PRINT, "Print"), 0, wx.EXPAND|wx.ALL, 2) 3009 vbs2.Add(wx.Button(self, self.ID_PAGESETUP, "Page Setup..."), 0, wx.EXPAND|wx.ALL, 2) 3010 vbs2.Add(wx.Button(self, self.ID_PRINTPREVIEW, "Print Preview"), 0, wx.EXPAND|wx.ALL, 2) 3011 vbs2.Add(wx.Button(self, self.ID_SAVEASHTML, "Save as HTML"), 0, wx.EXPAND|wx.ALL, 2) 3012 vbs2.Add(wx.Button(self, self.ID_CLOSE, "Close"), 0, wx.EXPAND|wx.ALL, 2) 3013 hbs.Add(vbs2, 0, wx.EXPAND|wx.ALL, 2) 3014 3015 # wrap up top row 3016 vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 2) 3017 3018 # bottom half - preview 3019 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Content Preview"), wx.VERTICAL) 3020 self.preview=bphtml.HTMLWindow(self, -1) 3021 bs.Add(self.preview, 1, wx.EXPAND|wx.ALL, 2) 3022 3023 # wrap up bottom row 3024 vbs.Add(bs, 2, wx.EXPAND|wx.ALL, 2) 3025 3026 self.SetSizer(vbs) 3027 vbs.Fit(self) 3028 3029 # event handlers 3030 wx.EVT_BUTTON(self, self.ID_PRINTPREVIEW, self.OnPrintPreview) 3031 wx.EVT_BUTTON(self, self.ID_PRINT, self.OnPrint) 3032 wx.EVT_BUTTON(self, self.ID_PAGESETUP, self.OnPageSetup) 3033 wx.EVT_BUTTON(self, self.ID_SAVEASHTML, self.OnSaveHTML) 3034 wx.EVT_RADIOBUTTON(self, self.selected.GetId(), self.UpdateHtml) 3035 wx.EVT_RADIOBUTTON(self, self.all.GetId(), self.UpdateHtml) 3036 for i in self.sortkeyscb: 3037 wx.EVT_COMBOBOX(self, i.GetId(), self.UpdateHtml) 3038 wx.EVT_LISTBOX(self, self.layout.GetId(), self.UpdateHtml) 3039 wx.EVT_CHECKLISTBOX(self, self.styles.GetId(), self.UpdateHtml) 3040 wx.EVT_COMMAND_SCROLL(self, self.textscaleslider.GetId(), self.UpdateSlider) 3041 self.UpdateHtml()
3042
3043 - def UpdateSlider(self, evt):
3044 pos=evt.GetPosition() 3045 if self.textscales[pos][0]!=self.scale: 3046 self.scale=self.textscales[pos][0] 3047 self.textscalelabel.SetLabel(self.textscales[pos][1]) 3048 self.preview.SetFontScale(self.scale)
3049
3050 - def UpdateHtml(self,_=None):
3051 wx.CallAfter(self._UpdateHtml)
3052
3053 - def _UpdateHtml(self):
3054 self.html=self.GetCurrentHTML() 3055 self.preview.SetPage(self.html)
3056 3057 @guihelper.BusyWrapper
3058 - def GetCurrentHTML(self):
3059 # Setup a nice environment pointing at this module 3060 vars={'phonebook': __import__(__name__) } 3061 # which data do we want? 3062 if self.all.GetValue(): 3063 rowkeys=self.phonewidget._data.keys() 3064 else: 3065 rowkeys=self.phonewidget.GetSelectedRowKeys() 3066 # sort the data 3067 # we actually sort in reverse order of what the UI shows in order to get correct results 3068 for keycb in (-1, -2, -3): 3069 sortkey=self.sortkeyscb[keycb].GetValue() 3070 if sortkey=="<None>": continue 3071 # decorate 3072 l=[(getdata(sortkey, self.phonewidget._data[key]), key) for key in rowkeys] 3073 l.sort() 3074 # undecorate 3075 rowkeys=[key for val,key in l] 3076 # finish up vars 3077 vars['rowkeys']=rowkeys 3078 vars['currentcolumns']=self.phonewidget.GetColumns() 3079 vars['data']=self.phonewidget._data 3080 # Use xyaptu 3081 xcp=xyaptu.xcopier(None) 3082 xcp.setupxcopy(self.layoutfiles[self.layout.GetStringSelection()]) 3083 html=xcp.xcopywithdns(vars) 3084 # apply styles 3085 sd={'styles': {}, '__builtins__': __builtins__ } 3086 for i in range(self.styles.GetCount()): 3087 if self.styles.IsChecked(i): 3088 exec self.stylefiles[self.styles.GetString(i)] in sd,sd 3089 try: 3090 html=bphtml.applyhtmlstyles(html, sd['styles']) 3091 except: 3092 if __debug__: 3093 with file("debug.html", "wt") as f: 3094 f.write(html) 3095 raise 3096 return html
3097
3098 - def OnPrintPreview(self, _):
3099 wx.GetApp().htmlprinter.PreviewText(self.html, scale=self.scale)
3100
3101 - def OnPrint(self, _):
3102 wx.GetApp().htmlprinter.PrintText(self.html, scale=self.scale)
3103
3104 - def OnPrinterSetup(self, _):
3105 wx.GetApp().htmlprinter.PrinterSetup()
3106
3107 - def OnPageSetup(self, _):
3108 wx.GetApp().htmlprinter.PageSetup()
3109
3110 - def OnSaveHTML(self, _):
3111 with guihelper.WXDialogWrapper(wx.FileDialog(self, wildcard="Web Page (*.htm;*.html)|*.htm;*html", 3112 style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT), 3113 True) as (_dlg, _retcode): 3114 if _retcode==wx.ID_OK: 3115 file(_dlg.GetPath(), 'wt').write(self.html)
3116
3117 -def htmlify(string):
3118 return common.strorunicode(string).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br/>")
3119