0001 ### BITPIM 0002 ### 0003 ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com> 0004 ### Copyright (C) 2004 Adit Panchal <apanchal@bastula.org> 0005 ### 0006 ### This program is free software; you can redistribute it and/or modify 0007 ### it under the terms of the BitPim license as detailed in the LICENSE file. 0008 ### 0009 ### $Id: phonebook.py 4639 2008-07-22 19:50:21Z djpham $ 0010 0011 """A widget for displaying/editting the phone information 0012 0013 The format for a phonebook entry is standardised. It is a 0014 dict with the following fields. Each field is a list, most 0015 important first, with each item in the list being a dict. 0016 0017 names: 0018 0019 - title ??Job title or salutation?? 0020 - first 0021 - middle 0022 - last 0023 - full You should specify the fullname or the 4 above 0024 - nickname (This could also be an alias) 0025 0026 categories: 0027 0028 - category User defined category name 0029 - ringtone (optional) Ringtone name for this category 0030 0031 emails: 0032 0033 - email Email address 0034 - type (optional) 'home' or 'business' 0035 - speeddial (optional) Speed dial for this entry 0036 - ringtone (optional) ringtone name for this entry 0037 - wallpaper (optional) wallpaper name for this entry 0038 0039 maillist: 0040 0041 - entry string of '\x00\x00' separated of number or email entries. 0042 - speeddial (optional) Speed dial for this entry 0043 - ringtone (optional) ringtone name for this entry 0044 - wallpaper (optional) wallpaper name for this entry 0045 0046 urls: 0047 0048 - url URL 0049 - type (optional) 'home' or 'business' 0050 0051 ringtones: 0052 0053 - ringtone Name of a ringtone 0054 - use 'call', 'message' 0055 0056 addresses: 0057 0058 - type 'home' or 'business' 0059 - company (only for type of 'business') 0060 - street Street part of address 0061 - street2 Second line of street address 0062 - city 0063 - state 0064 - postalcode 0065 - country Can also be the region 0066 0067 wallpapers: 0068 0069 - wallpaper Name of wallpaper 0070 - use see ringtones.use 0071 0072 flags: 0073 0074 - secret Boolean if record is private/secret (if not present - value is false) 0075 - sim Boolean if record should be stored on SIM card of GSM phones. 0076 0077 memos: 0078 0079 - memo Note 0080 0081 numbers: 0082 0083 - number Phone number as ascii string 0084 - type 'home', 'office', 'cell', 'fax', 'pager', 'data', 'none' (if you have home2 etc, list 0085 them without the digits. The second 'home' is implicitly home2 etc) 0086 - speeddial (optional) Speed dial number 0087 - ringtone (optional) ringtone name for this entry 0088 - wallpaper (optional) wallpaper name for this entry 0089 0090 serials: 0091 0092 - sourcetype identifies source driver in bitpim (eg "lgvx4400", "windowsaddressbook") 0093 - sourceuniqueid (optional) identifier for where the serial came from (eg ESN of phone, wab host/username) 0094 (imagine having multiple phones of the same model to see why this is needed) 0095 - * other names of use to sourcetype 0096 """ 0097 0098 # Standard imports 0099 from __future__ import with_statement 0100 import os 0101 import cStringIO 0102 import re 0103 import time 0104 import copy 0105 0106 # GUI 0107 import wx 0108 import wx.grid 0109 import wx.html 0110 0111 # My imports 0112 import common 0113 import xyaptu 0114 import guihelper 0115 import phonebookentryeditor 0116 import pubsub 0117 import nameparser 0118 import bphtml 0119 import guihelper 0120 import guiwidgets 0121 import phonenumber 0122 import helpids 0123 import database 0124 import widgets 0125 0126 0127 ### 0128 ### The object we use to store a record. See detailed description of 0129 ### fields at top of file 0130 ### 0131 0132 class phonebookdataobject(database.basedataobject): 0133 # no change to _knownproperties (all of ours are list properties) 0134 _knownlistproperties=database.basedataobject._knownlistproperties.copy() 0135 _knownlistproperties.update( {'names': ['title', 'first', 'middle', 'last', 'full', 'nickname'], 0136 'categories': ['category'], 0137 'emails': ['email', 'type', 'speeddial', 0138 'ringtone', 'wallpaper' ], 0139 'urls': ['url', 'type'], 0140 'ringtones': ['ringtone', 'use'], 0141 'addresses': ['type', 'company', 'street', 'street2', 'city', 'state', 'postalcode', 'country'], 0142 'wallpapers': ['wallpaper', 'use'], 0143 'flags': ['secret', 'sim'], 0144 'memos': ['memo'], 0145 'numbers': ['number', 'type', 'speeddial', 0146 'ringtone', 'wallpaper' ], 0147 'ice': [ 'iceindex' ], 0148 ## 'maillist': ['entry', 'speeddial', 0149 ## 'ringtone', 'wallpaper' ], 0150 # serials is in parent object 0151 }) 0152 0153 phonebookobjectfactory=database.dataobjectfactory(phonebookdataobject) 0154 0155 ### 0156 ### Phonebook entry display (Derived from HTML) 0157 ### 0158 0159 class PhoneEntryDetailsView(bphtml.HTMLWindow): 0160 0161 def __init__(self, parent, id, stylesfile="styles.xy", layoutfile="pblayout.xy"): 0162 bphtml.HTMLWindow.__init__(self, parent, id) 0163 self.stylesfile=guihelper.getresourcefile(stylesfile) 0164 self.pblayoutfile=guihelper.getresourcefile(layoutfile) 0165 self.xcp=None 0166 self.xcpstyles=None 0167 self.ShowEntry({}) 0168 0169 def ShowEntry(self, entry): 0170 if self.xcp is None: 0171 template=open(self.pblayoutfile, "rt").read() 0172 self.xcp=xyaptu.xcopier(None) 0173 self.xcp.setupxcopy(template) 0174 if self.xcpstyles is None: 0175 self.xcpstyles={} 0176 try: 0177 execfile(self.stylesfile, self.xcpstyles, self.xcpstyles) 0178 except UnicodeError: 0179 common.unicode_execfile(self.stylesfile, self.xcpstyles, self.xcpstyles) 0180 self.xcpstyles['entry']=entry 0181 text=self.xcp.xcopywithdns(self.xcpstyles) 0182 try: 0183 text=bphtml.applyhtmlstyles(text, self.xcpstyles['styles']) 0184 except: 0185 if __debug__: 0186 open("debug.html", "wt").write(common.forceascii(text)) 0187 raise 0188 self.SetPage(text) 0189 0190 ### 0191 ### Functions used to get data from a record 0192 ### 0193 0194 0195 def formatcategories(cats): 0196 c=[cat['category'] for cat in cats] 0197 c.sort() 0198 return "; ".join(c) 0199 0200 def formataddress(address): 0201 l=[] 0202 for i in 'company', 'street', 'street2', 'city', 'state', 'postalcode', 'country': 0203 if i in address: 0204 l.append(address[i]) 0205 return "; ".join(l) 0206 0207 def formattypenumber(number): 0208 t=number['type'] 0209 t=t[0].upper()+t[1:] 0210 sd=number.get("speeddial", None) 0211 if sd is None: 0212 return "%s (%s)" % (phonenumber.format(number['number']), t) 0213 return "%s [%d] (%s)" % (phonenumber.format(number['number']), sd, t) 0214 0215 def formatnumber(number): 0216 sd=number.get("speeddial", None) 0217 if sd is None: 0218 return phonenumber.format(number['number']) 0219 return "%s [%d]" % (phonenumber.format(number['number']), sd) 0220 0221 def formatstorage(flag): 0222 return 'SIM' if flag.get('sim', False) else '' 0223 0224 def formatsecret(flag): 0225 return 'True' if flag.get('secret', False) else '' 0226 0227 # this is specified here as a list so that we can get the 0228 # keys in the order below for the settings UI (alpha sorting 0229 # or dictionary order would be user hostile). The data 0230 # is converted to a dict below 0231 _getdatalist=[ 0232 # column (key matchnum match func_or_field showinimport) 0233 'Name', ("names", 0, None, nameparser.formatfullname, True), 0234 'First', ("names", 0, None, nameparser.getfirst, False), 0235 'Middle', ("names", 0, None, nameparser.getmiddle, False), 0236 'Last', ("names", 0, None, nameparser.getlast, False), 0237 0238 'Category', ("categories", 0, None, "category", False), 0239 'Category2', ("categories", 1, None, "category", False), 0240 'Category3', ("categories", 2, None, "category", False), 0241 'Category4', ("categories", 3, None, "category", False), 0242 'Category5', ("categories", 4, None, "category", False), 0243 'Categories', ("categories", None, None, formatcategories, True), 0244 0245 "Phone", ("numbers", 0, None, formattypenumber, False), 0246 "Phone2", ("numbers", 1, None, formattypenumber, False), 0247 "Phone3", ("numbers", 2, None, formattypenumber, False), 0248 "Phone4", ("numbers", 3, None, formattypenumber, False), 0249 "Phone5", ("numbers", 4, None, formattypenumber, False), 0250 "Phone6", ("numbers", 5, None, formattypenumber, False), 0251 "Phone7", ("numbers", 6, None, formattypenumber, False), 0252 "Phone8", ("numbers", 7, None, formattypenumber, False), 0253 "Phone9", ("numbers", 8, None, formattypenumber, False), 0254 "Phone10", ("numbers", 9, None, formattypenumber, False), 0255 0256 # phone numbers are inserted here 0257 0258 'Email', ("emails", 0, None, "email", True), 0259 'Email2', ("emails", 1, None, "email", True), 0260 'Email3', ("emails", 2, None, "email", True), 0261 'Email4', ("emails", 3, None, "email", True), 0262 'Email5', ("emails", 4, None, "email", True), 0263 'Business Email', ("emails", 0, ("type", "business"), "email", False), 0264 'Business Email2', ("emails", 1, ("type", "business"), "email", False), 0265 'Home Email', ("emails", 0, ("type", "home"), "email", False), 0266 'Home Email2', ("emails", 1, ("type", "home"), "email", False), 0267 0268 'URL', ("urls", 0, None, "url", True), 0269 'URL2', ("urls", 1, None, "url", True), 0270 'URL3', ("urls", 2, None, "url", True), 0271 'URL4', ("urls", 3, None, "url", True), 0272 'URL5', ("urls", 4, None, "url", True), 0273 'Business URL', ("urls", 0, ("type", "business"), "url", False), 0274 'Business URL2', ("urls", 1, ("type", "business"), "url", False), 0275 'Home URL', ("urls", 0, ("type", "home"), "url", False), 0276 'Home URL2', ("urls", 1, ("type", "home"), "url", False), 0277 0278 'Ringtone', ("ringtones", 0, ("use", "call"), "ringtone", True), 0279 'Message Ringtone', ("ringtones", 0, ("use", "message"), "ringtone", True), 0280 0281 'Address', ("addresses", 0, None, formataddress, True), 0282 'Address2', ("addresses", 1, None, formataddress, True), 0283 'Address3', ("addresses", 2, None, formataddress, True), 0284 'Address4', ("addresses", 3, None, formataddress, True), 0285 'Address5', ("addresses", 4, None, formataddress, True), 0286 'Home Address', ("addresses", 0, ("type", "home"), formataddress, False), 0287 'Home Address2', ("addresses", 1, ("type", "home"), formataddress, False), 0288 'Business Address', ("addressess", 0, ("type", "business"), formataddress, False), 0289 'Business Address2', ("addressess", 1, ("type", "business"), formataddress, False), 0290 0291 "Wallpaper", ("wallpapers", 0, None, "wallpaper", True), 0292 0293 "Secret", ("flags", 0, ("secret", True), formatsecret, True), 0294 "Storage", ("flags", 0,('sim', True), formatstorage, True), 0295 "Memo", ("memos", 0, None, "memo", True), 0296 "Memo2", ("memos", 1, None, "memo", True), 0297 "Memo3", ("memos", 2, None, "memo", True), 0298 "Memo4", ("memos", 3, None, "memo", True), 0299 "Memo5", ("memos", 4, None, "memo", True), 0300 0301 "ICEindex", ("ice", 0, None, "iceindex", False), 0302 0303 ] 0304 0305 ll=[] 0306 for pretty, actual in ("Home", "home"), ("Office", "office"), ("Cell", "cell"), ("Fax", "fax"), ("Pager", "pager"), ("Data", "data"): 0307 for suf,n in ("", 0), ("2", 1), ("3", 2): 0308 ll.append(pretty+suf) 0309 ll.append(("numbers", n, ("type", actual), formatnumber, True)) 0310 _getdatalist[40:40]=ll 0311 0312 _getdatatable={} 0313 AvailableColumns=[] 0314 DefaultColumns=['Name', 'Phone', 'Phone2', 'Phone3', 'Email', 'Categories', 'Memo', 'Secret'] 0315 ImportColumns=[_getdatalist[x*2] for x in range(len(_getdatalist)/2) if _getdatalist[x*2+1][4]] 0316 0317 for n in range(len(_getdatalist)/2): 0318 AvailableColumns.append(_getdatalist[n*2]) 0319 _getdatatable[_getdatalist[n*2]]=_getdatalist[n*2+1] 0320 0321 del _getdatalist # so we don't accidentally use it 0322 0323 def getdata(column, entry, default=None): 0324 """Returns the value in a particular column. 0325 Note that the data is appropriately formatted. 0326 0327 @param column: column name 0328 @param entry: the dict representing a phonebook entry 0329 @param default: what to return if the entry has no data for that column 0330 """ 0331 key, count, prereq, formatter, _ =_getdatatable[column] 0332 0333 # do we even have that key 0334 if key not in entry: 0335 return default 0336 0337 if count is None: 0338 # value is all the fields (eg Categories) 0339 thevalue=entry[key] 0340 elif prereq is None: 0341 # no prereq 0342 if len(entry[key])<=count: 0343 return default 0344 thevalue=entry[key][count] 0345 else: 0346 # find the count instance of value matching k,v in prereq 0347 ptr=0 0348 togo=count+1 0349 l=entry[key] 0350 k,v=prereq 0351 while togo: 0352 if ptr==len(l): 0353 return default 0354 if k not in l[ptr]: 0355 ptr+=1 0356 continue 0357 if l[ptr][k]!=v: 0358 ptr+=1 0359 continue 0360 togo-=1 0361 if togo!=0: 0362 ptr+=1 0363 continue 0364 thevalue=entry[key][ptr] 0365 break 0366 0367 # thevalue now contains the dict with value we care about 0368 if callable(formatter): 0369 return formatter(thevalue) 0370 0371 return thevalue.get(formatter, default) 0372 0373 def getdatainfo(column, entry): 0374 """Similar to L{getdata} except returning higher level information. 0375 0376 Returns the key name and which index from the list corresponds to 0377 the column. 0378 0379 @param column: Column name 0380 @param entry: The dict representing a phonebook entry 0381 @returns: (keyname, index) tuple. index will be None if the entry doesn't 0382 have the relevant column value and -1 if all of them apply 0383 """ 0384 key, count, prereq, formatter, _ =_getdatatable[column] 0385 0386 # do we even have that key 0387 if key not in entry: 0388 return (key, None) 0389 0390 # which value or values do we want 0391 if count is None: 0392 return (key, -1) 0393 elif prereq is None: 0394 # no prereq 0395 if len(entry[key])<=count: 0396 return (key, None) 0397 return (key, count) 0398 else: 0399 # find the count instance of value matching k,v in prereq 0400 ptr=0 0401 togo=count+1 0402 l=entry[key] 0403 k,v=prereq 0404 while togo: 0405 if ptr==len(l): 0406 return (key,None) 0407 if k not in l[ptr]: 0408 ptr+=1 0409 continue 0410 if l[ptr][k]!=v: 0411 ptr+=1 0412 continue 0413 togo-=1 0414 if togo!=0: 0415 ptr+=1 0416 continue 0417 return (key, ptr) 0418 return (key, None) 0419 0420 class CategoryManager: 0421 0422 # this is only used to prevent the pubsub module 0423 # from being GC while any instance of this class exists 0424 __publisher=pubsub.Publisher 0425 0426 def __init__(self): 0427 self.categories=[] 0428 pubsub.subscribe(self.OnListRequest, pubsub.REQUEST_CATEGORIES) 0429 pubsub.subscribe(self.OnSetCategories, pubsub.SET_CATEGORIES) 0430 pubsub.subscribe(self.OnMergeCategories, pubsub.MERGE_CATEGORIES) 0431 pubsub.subscribe(self.OnAddCategory, pubsub.ADD_CATEGORY) 0432 0433 def OnListRequest(self, msg=None): 0434 # nb we publish a copy of the list, not the real 0435 # thing. otherwise other code inadvertently modifies it! 0436 pubsub.publish(pubsub.ALL_CATEGORIES, self.categories[:]) 0437 0438 def OnAddCategory(self, msg): 0439 name=msg.data 0440 if name in self.categories: 0441 return 0442 self.categories.append(name) 0443 self.categories.sort() 0444 self.OnListRequest() 0445 0446 def OnSetCategories(self, msg): 0447 cats=msg.data[:] 0448 self.categories=cats 0449 self.categories.sort() 0450 self.OnListRequest() 0451 0452 def OnMergeCategories(self, msg): 0453 cats=msg.data[:] 0454 newcats=self.categories[:] 0455 for i in cats: 0456 if i not in newcats: 0457 newcats.append(i) 0458 newcats.sort() 0459 if newcats!=self.categories: 0460 self.categories=newcats 0461 self.OnListRequest() 0462 0463 CategoryManager=CategoryManager() # shadow out class name 0464 0465 ### 0466 ### We use a table for speed 0467 ### 0468 0469 class PhoneDataTable(wx.grid.PyGridTableBase): 0470 0471 def __init__(self, widget, columns): 0472 self.main=widget 0473 self.rowkeys=self.main._data.keys() 0474 wx.grid.PyGridTableBase.__init__(self) 0475 self.oddattr=wx.grid.GridCellAttr() 0476 self.oddattr.SetBackgroundColour("OLDLACE") 0477 self.evenattr=wx.grid.GridCellAttr() 0478 self.evenattr.SetBackgroundColour("ALICE BLUE") 0479 self.columns=columns 0480 assert len(self.rowkeys)==0 # we can't sort here, and it isn't necessary because list is zero length 0481 0482 def GetColLabelValue(self, col): 0483 return self.columns[col] 0484 0485 def OnDataUpdated(self): 0486 newkeys=self.main._data.keys() 0487 newkeys.sort() 0488 oldrows=self.rowkeys 0489 self.rowkeys=newkeys 0490 lo=len(oldrows) 0491 ln=len(self.rowkeys) 0492 if ln>lo: 0493 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, ln-lo) 0494 elif lo>ln: 0495 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, 0, lo-ln) 0496 else: 0497 msg=None 0498 if msg is not None: 0499 self.GetView().ProcessTableMessage(msg) 0500 self.Sort() 0501 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES) 0502 self.GetView().ProcessTableMessage(msg) 0503 self.GetView().AutoSizeColumns() 0504 0505 def SetColumns(self, columns): 0506 oldcols=self.columns 0507 self.columns=columns 0508 lo=len(oldcols) 0509 ln=len(self.columns) 0510 if ln>lo: 0511 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_APPENDED, ln-lo) 0512 elif lo>ln: 0513 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_DELETED, 0, lo-ln) 0514 else: 0515 msg=None 0516 if msg is not None: 0517 self.GetView().ProcessTableMessage(msg) 0518 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES) 0519 self.GetView().ProcessTableMessage(msg) 0520 self.GetView().AutoSizeColumns() 0521 0522 def Sort(self): 0523 bycol=self.main.sortedColumn 0524 descending=self.main.sortedColumnDescending 0525 ### ::TODO:: this sorting is not stable - it should include the current pos rather than key 0526 l=[ (getdata(self.columns[bycol], self.main._data[key]), key) for key in self.rowkeys] 0527 l.sort() 0528 if descending: 0529 l.reverse() 0530 self.rowkeys=[key for val,key in l] 0531 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES) 0532 self.GetView().ProcessTableMessage(msg) 0533 0534 def IsEmptyCell(self, row, col): 0535 return False 0536 0537 def GetNumberRows(self): 0538 return len(self.rowkeys) 0539 0540 def GetNumberCols(self): 0541 return len(self.columns) 0542 0543 def GetValue(self, row, col): 0544 try: 0545 entry=self.main._data[self.rowkeys[row]] 0546 except: 0547 print "bad row", row 0548 return "<error>" 0549 0550 return getdata(self.columns[col], entry, "") 0551 0552 def GetAttr(self, row, col, _): 0553 r=[self.evenattr, self.oddattr][row%2] 0554 r.IncRef() 0555 return r 0556 0557 class PhoneWidget(wx.Panel, widgets.BitPimWidget): 0558 """Main phone editing/displaying widget""" 0559 CURRENTFILEVERSION=2 0560 # Data selector const 0561 _Current_Data=0 0562 _Historic_Data=1 0563 0564 def __init__(self, mainwindow, parent, config): 0565 wx.Panel.__init__(self, parent,-1) 0566 self.sash_pos=config.ReadInt('phonebooksashpos', -300) 0567 self.update_sash=False 0568 # keep this around while we exist 0569 self.categorymanager=CategoryManager 0570 split=wx.SplitterWindow(self, -1, style=wx.SP_3D|wx.SP_LIVE_UPDATE) 0571 split.SetMinimumPaneSize(20) 0572 self.mainwindow=mainwindow 0573 self._data={} 0574 self.parent=parent 0575 self.categories=[] 0576 self.modified=False 0577 self.table_panel=wx.Panel(split) 0578 self.table=wx.grid.Grid(self.table_panel, wx.NewId()) 0579 self.table.EnableGridLines(False) 0580 self.error_log=guihelper.MultiMessageBox(self.mainwindow , "Contact Export Errors", 0581 "Bitpim is unable to send the following data to your phone") 0582 # which columns? 0583 cur=config.Read("phonebookcolumns", "") 0584 if len(cur): 0585 cur=cur.split(",") 0586 # ensure they all exist 0587 cur=[c for c in cur if c in AvailableColumns] 0588 else: 0589 cur=DefaultColumns 0590 # column sorter info 0591 self.sortedColumn=0 0592 self.sortedColumnDescending=False 0593 0594 self.dt=PhoneDataTable(self, cur) 0595 self.table.SetTable(self.dt, False, wx.grid.Grid.wxGridSelectRows) 0596 self.table.SetSelectionMode(wx.grid.Grid.wxGridSelectRows) 0597 self.table.SetRowLabelSize(0) 0598 self.table.EnableEditing(False) 0599 self.table.EnableDragRowSize(False) 0600 self.table.SetMargins(1,0) 0601 # data date adjuster 0602 hbs=wx.BoxSizer(wx.HORIZONTAL) 0603 self.read_only=False 0604 self.historical_date=None 0605 static_bs=wx.StaticBoxSizer(wx.StaticBox(self.table_panel, -1, 0606 'Historical Data Status:'), 0607 wx.VERTICAL) 0608 self.historical_data_label=wx.StaticText(self.table_panel, -1, 0609 'Current Data') 0610 static_bs.Add(self.historical_data_label, 1, wx.EXPAND|wx.ALL, 5) 0611 hbs.Add(static_bs, 1, wx.EXPAND|wx.ALL, 5) 0612 # show the number of contacts 0613 static_bs=wx.StaticBoxSizer(wx.StaticBox(self.table_panel, -1, 0614 'Number of Contacts:'), 0615 wx.VERTICAL) 0616 self.contactcount_label=wx.StaticText(self.table_panel, -1, '0') 0617 static_bs.Add(self.contactcount_label, 1, wx.EXPAND|wx.ALL, 5) 0618 hbs.Add(static_bs, 1, wx.EXPAND|wx.ALL, 5) 0619 # main sizer 0620 vbs=wx.BoxSizer(wx.VERTICAL) 0621 vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) 0622 vbs.Add(self.table, 1, wx.EXPAND, 0) 0623 self.table_panel.SetSizer(vbs) 0624 self.table_panel.SetAutoLayout(True) 0625 vbs.Fit(self.table_panel) 0626 self.preview=PhoneEntryDetailsView(split, -1, "styles.xy", "pblayout.xy") 0627 # for some reason, preview doesn't show initial background 0628 wx.CallAfter(self.preview.ShowEntry, {}) 0629 split.SplitVertically(self.table_panel, self.preview, self.sash_pos) 0630 self.split=split 0631 bs=wx.BoxSizer(wx.VERTICAL) 0632 bs.Add(split, 1, wx.EXPAND) 0633 self.SetSizer(bs) 0634 self.SetAutoLayout(True) 0635 wx.EVT_IDLE(self, self.OnIdle) 0636 wx.grid.EVT_GRID_SELECT_CELL(self, self.OnCellSelect) 0637 wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self, self.OnCellDClick) 0638 wx.grid.EVT_GRID_CELL_RIGHT_CLICK(self, self.OnCellRightClick) 0639 wx.EVT_LEFT_DCLICK(self.preview, self.OnPreviewDClick) 0640 pubsub.subscribe(self.OnCategoriesUpdate, pubsub.ALL_CATEGORIES) 0641 pubsub.subscribe(self.OnPBLookup, pubsub.REQUEST_PB_LOOKUP) 0642 pubsub.subscribe(self.OnMediaNameChanged, pubsub.MEDIA_NAME_CHANGED) 0643 # we draw the column headers 0644 # code based on original implementation by Paul Mcnett 0645 wx.EVT_PAINT(self.table.GetGridColLabelWindow(), self.OnColumnHeaderPaint) 0646 wx.grid.EVT_GRID_LABEL_LEFT_CLICK(self.table, self.OnGridLabelLeftClick) 0647 wx.grid.EVT_GRID_LABEL_LEFT_DCLICK(self.table, self.OnGridLabelLeftClick) 0648 wx.EVT_SPLITTER_SASH_POS_CHANGED(self, self.split.GetId(), 0649 self.OnSashPosChanged) 0650 # context menu 0651 self.context_menu=wx.Menu() 0652 id=wx.NewId() 0653 self.context_menu.Append(id, 'Set to current', 0654 'Set the selected item to current data') 0655 wx.EVT_MENU(self, id, self.OnSetToCurrent) 0656 0657 def OnInit(self): 0658 # whether or not to turn on phonebook preview pane 0659 if not self.config.ReadInt("viewphonebookpreview", 1): 0660 self.OnViewPreview(False) 0661 0662 def OnColumnHeaderPaint(self, evt): 0663 w = self.table.GetGridColLabelWindow() 0664 dc = wx.PaintDC(w) 0665 font = dc.GetFont() 0666 dc.SetTextForeground(wx.BLACK) 0667 0668 # For each column, draw it's rectangle, it's column name, 0669 # and it's sort indicator, if appropriate: 0670 totColSize = -self.table.GetViewStart()[0]*self.table.GetScrollPixelsPerUnit()[0] 0671 for col in range(self.table.GetNumberCols()): 0672 dc.SetBrush(wx.Brush("WHEAT", wx.TRANSPARENT)) 0673 colSize = self.table.GetColSize(col) 0674 rect = (totColSize,0,colSize,32) 0675 dc.DrawRectangle(rect[0] - (col!=0 and 1 or 0), rect[1], rect[2] + (col!=0 and 1 or 0), rect[3]) 0676 totColSize += colSize 0677 0678 if col == self.sortedColumn: 0679 font.SetWeight(wx.BOLD) 0680 # draw a triangle, pointed up or down, at the 0681 # top left of the column. 0682 left = rect[0] + 3 0683 top = rect[1] + 3 0684 0685 dc.SetBrush(wx.Brush("WHEAT", wx.SOLID)) 0686 if self.sortedColumnDescending: 0687 dc.DrawPolygon([(left,top), (left+6,top), (left+3,top+4)]) 0688 else: 0689 dc.DrawPolygon([(left+3,top), (left+6, top+4), (left, top+4)]) 0690 else: 0691 font.SetWeight(wx.NORMAL) 0692 0693 dc.SetFont(font) 0694 dc.DrawLabel("%s" % self.table.GetTable().columns[col], 0695 rect, wx.ALIGN_CENTER | wx.ALIGN_TOP) 0696 0697 0698 def OnGridLabelLeftClick(self, evt): 0699 col=evt.GetCol() 0700 if col==self.sortedColumn: 0701 self.sortedColumnDescending=not self.sortedColumnDescending 0702 else: 0703 self.sortedColumn=col 0704 self.sortedColumnDescending=False 0705 self.dt.Sort() 0706 self.table.Refresh() 0707 0708 def OnSashPosChanged(self, _): 0709 if self.update_sash: 0710 self.sash_pos=self.split.GetSashPosition() 0711 self.config.WriteInt('phonebooksashpos', self.sash_pos) 0712 def OnPreActivate(self): 0713 self.update_sash=False 0714 def OnPostActivate(self): 0715 self.split.SetSashPosition(self.sash_pos) 0716 self.update_sash=True 0717 0718 def SetColumns(self, columns): 0719 c=self.GetColumns()[self.sortedColumn] 0720 self.dt.SetColumns(columns) 0721 if c in columns: 0722 self.sortedColumn=columns.index(c) 0723 else: 0724 self.sortedColumn=0 0725 self.sortedColumnDescending=False 0726 self.dt.Sort() 0727 self.table.Refresh() 0728 0729 def GetColumns(self): 0730 return self.dt.columns 0731 0732 def OnCategoriesUpdate(self, msg): 0733 if self.categories!=msg.data: 0734 self.categories=msg.data[:] 0735 self.modified=True 0736 0737 def OnPBLookup(self, msg): 0738 d=msg.data 0739 s=d.get('item', '') 0740 if not len(s): 0741 return 0742 d['name']=None 0743 for k,e in self._data.items(): 0744 for n in e.get('numbers', []): 0745 if s==n.get('number', None): 0746 # found a number, stop and reply 0747 d['name']=nameparser.getfullname(e['names'][0])+'('+\ 0748 n.get('type', '')+')' 0749 pubsub.publish(pubsub.RESPONSE_PB_LOOKUP, d) 0750 return 0751 for n in e.get('emails', []): 0752 if s==n.get('email', None): 0753 # found an email, stop and reply 0754 d['name']=nameparser.getfullname(e['names'][0])+'(email)' 0755 pubsub.publish(pubsub.RESPONSE_PB_LOOKUP, d) 0756 return 0757 # done and reply 0758 pubsub.publish(pubsub.RESPONSE_PB_LOOKUP, d) 0759 0760 def OnMediaNameChanged(self, msg): 0761 d=msg.data 0762 _type=d.get(pubsub.media_change_type, None) 0763 _old_name=d.get(pubsub.media_old_name, None) 0764 _new_name=d.get(pubsub.media_new_name, None) 0765 if _type is None or _old_name is None or _new_name is None: 0766 # invalid/incomplete data 0767 return 0768 if _type!=pubsub.wallpaper_type and \ 0769 _type!=pubsub.ringtone_type: 0770 # neither wallpaper nor ringtone 0771 return 0772 _old_name=common.basename(_old_name) 0773 _new_name=common.basename(_new_name) 0774 if _type==pubsub.wallpaper_type: 0775 main_key='wallpapers' 0776 element_key='wallpaper' 0777 else: 0778 main_key='ringtones' 0779 element_key='ringtone' 0780 for k,e in self._data.items(): 0781 for i,n in enumerate(e.get(main_key, [])): 0782 if _old_name==n.get(element_key, None): 0783 # found it, update the name 0784 self._data[k][main_key][i][element_key]=_new_name 0785 self.modified=True 0786 0787 def HasColumnSelector(self): 0788 return True 0789 0790 def OnViewColumnSelector(self): 0791 with guihelper.WXDialogWrapper(ColumnSelectorDialog(self.parent, self.config, self), 0792 True): 0793 pass 0794 0795 def HasPreviewPane(self): 0796 return True 0797 0798 def IsPreviewPaneEnabled(self): 0799 return self.split.IsSplit() 0800 0801 def OnViewPreview(self, preview_on): 0802 if preview_on: 0803 self.split.SplitVertically(self.table_panel, self.preview, 0804 self.sash_pos) 0805 else: 0806 if self.sash_pos is None: 0807 self.sash_pos=-300 0808 else: 0809 self.sash_pos=self.split.GetSashPosition() 0810 self.split.Unsplit(self.preview) 0811 # refresh the table view 0812 self.config.WriteInt('viewphonebookpreview', preview_on) 0813 self.dt.GetView().AutoSizeColumns() 0814 0815 def HasHistoricalData(self): 0816 return True 0817 0818 def OnHistoricalData(self): 0819 """Display current or historical data""" 0820 if self.read_only: 0821 current_choice=guiwidgets.HistoricalDataDialog.Historical_Data 0822 else: 0823 current_choice=guiwidgets.HistoricalDataDialog.Current_Data 0824 with guihelper.WXDialogWrapper(guiwidgets.HistoricalDataDialog(self, 0825 current_choice=current_choice, 0826 historical_date=self.historical_date, 0827 historical_events=\ 0828 self.mainwindow.database.getchangescount('phonebook')), 0829 True) as (dlg, retcode): 0830 if retcode==wx.ID_OK: 0831 with guihelper.MWBusyWrapper(self.mainwindow): 0832 current_choice, self.historical_date=dlg.GetValue() 0833 r={} 0834 if current_choice==guiwidgets.HistoricalDataDialog.Current_Data: 0835 self.read_only=False 0836 msg_str='Current Data' 0837 self.getfromfs(r) 0838 else: 0839 self.read_only=True 0840 msg_str='Historical Data as of %s'%\ 0841 str(wx.DateTimeFromTimeT(self.historical_date)) 0842 self.getfromfs(r, self.historical_date) 0843 self.populate(r, False) 0844 self.historical_data_label.SetLabel(msg_str) 0845 0846 def OnIdle(self, _): 0847 "We save out changed data" 0848 if self.modified: 0849 self.modified=False 0850 self.populatefs(self.getdata({})) 0851 0852 def updateserials(self, results): 0853 "update the serial numbers after having written to the phone" 0854 if not results.has_key('serialupdates'): 0855 return 0856 0857 # each item is a tuple. bpserial is the bitpim serialid, 0858 # and updserial is what to update with. 0859 for bpserial,updserial in results['serialupdates']: 0860 # find the entry with bpserial 0861 for k in self._data: 0862 entry=self._data[k] 0863 if not entry.has_key('serials'): 0864 assert False, "serials have gone horribly wrong" 0865 continue 0866 found=False 0867 for serial in entry['serials']: 0868 if bpserial==serial: 0869 found=True 0870 break 0871 if not found: 0872 # not this entry 0873 continue 0874 # we will be updating this entry 0875 # see if there is a matching serial for updserial that we will update 0876 st=updserial['sourcetype'] 0877 remove=None 0878 for serial in entry['serials']: 0879 if serial['sourcetype']!=st: 0880 continue 0881 if updserial.has_key("sourceuniqueid"): 0882 if updserial["sourceuniqueid"]!=serial.get("sourceuniqueid", None): 0883 continue 0884 remove=serial 0885 break 0886 # remove if needbe 0887 if remove is not None: 0888 for count,serial in enumerate(entry['serials']): 0889 if remove==serial: 0890 break 0891 del entry['serials'][count] 0892 # add update on end 0893 entry['serials'].append(updserial) 0894 self.modified=True 0895 0896 def CanSelectAll(self): 0897 return True 0898 0899 def OnSelectAll(self, _): 0900 self.table.SelectAll() 0901 0902 def OnCellSelect(self, event): 0903 event.Skip() 0904 row=event.GetRow() 0905 self.SetPreview(self._data[self.dt.rowkeys[row]]) # bad breaking of abstraction referencing dt! 0906 0907 def OnPreviewDClick(self, _): 0908 self.EditEntries(self.table.GetGridCursorRow(), self.table.GetGridCursorCol()) 0909 0910 def OnCellDClick(self, event): 0911 self.EditEntries(event.GetRow(), event.GetCol()) 0912 0913 def OnCellRightClick(self, evt): 0914 if not self.read_only or not self.GetSelectedRowKeys(): 0915 return 0916 self.table.PopupMenu(self.context_menu, evt.GetPosition()) 0917 0918 def OnSetToCurrent(self, _): 0919 r={} 0920 for k in self.GetSelectedRowKeys(): 0921 r[k]=self._data[k] 0922 if r: 0923 dict={} 0924 self.getfromfs(dict) 0925 dict['phonebook'].update(r) 0926 c=[e for e in self.categories if e not in dict['categories']] 0927 dict['categories']+=c 0928 self._save_db(dict) 0929 0930 def EditEntries(self, row, column): 0931 # Allow moving to next/prev entries 0932 key=self.dt.rowkeys[row] 0933 data=self._data[key] 0934 # can we get it to open on the correct field? 0935 datakey,dataindex=getdatainfo(self.GetColumns()[column], data) 0936 _keys=self.GetSelectedRowKeys() 0937 if datakey in ('categories', 'ringtones', 'wallpapers') and \ 0938 len(_keys)>1 and not self.read_only: 0939 # Edit a single field for all seleced cells 0940 with guihelper.WXDialogWrapper(phonebookentryeditor.SingleFieldEditor(self, datakey), 0941 True) as (dlg, retcode): 0942 if retcode==wx.ID_OK: 0943 _data=dlg.GetData() 0944 if _data: 0945 for r in _keys: 0946 self._data[r][datakey]=_data 0947 else: 0948 for r in _keys: 0949 del self._data[r][datakey] 0950 self.SetPreview(self._data[_keys[0]]) 0951 self.dt.OnDataUpdated() 0952 self.modified=True 0953 else: 0954 with guihelper.WXDialogWrapper(phonebookentryeditor.Editor(self, data, 0955 factory=phonebookobjectfactory, 0956 keytoopenon=datakey, 0957 dataindex=dataindex, 0958 readonly=self.read_only, 0959 datakey=key, 0960 movement=True), 0961 True) as (dlg, retcode): 0962 if retcode==wx.ID_OK: 0963 self.SaveData(dlg.GetData(), dlg.GetDataKey()) 0964 0965 def SaveData(self, data, key): 0966 self._data[key]=data 0967 self.dt.OnDataUpdated() 0968 self.SetPreview(data) 0969 self.modified=True 0970 0971 def EditEntry(self, row, column): 0972 key=self.dt.rowkeys[row] 0973 data=self._data[key] 0974 # can we get it to open on the correct field? 0975 datakey,dataindex=getdatainfo(self.GetColumns()[column], data) 0976 with guihelper.WXDialogWrapper(phonebookentryeditor.Editor(self, data, 0977 factory=phonebookobjectfactory, 0978 keytoopenon=datakey, 0979 dataindex=dataindex, 0980 readonly=self.read_only), 0981 True) as (dlg, retcode): 0982 if retcode==wx.ID_OK: 0983 data=dlg.GetData() 0984 self._data[key]=data 0985 self.dt.OnDataUpdated() 0986 self.SetPreview(data) 0987 self.modified=True 0988 0989 def GetNextEntry(self, next=True): 0990 # return the data for the next item on the list 0991 _sel_rows=self.GetSelectedRows() 0992 if not _sel_rows: 0993 return None 0994 try: 0995 row=_sel_rows[0] 0996 if next: 0997 _new_row=row+1 0998 else: 0999 _new_row=row-1 1000 _num_rows=self.table.GetNumberRows() 1001 if _new_row>=_num_rows: 1002 _new_row=0 1003 elif _new_row<0: 1004 _new_row=_num_rows-1 1005 self.table.SetGridCursor(_new_row, self.table.GetGridCursorCol()) 1006 self.table.SelectRow(_new_row) 1007 _key=self.dt.rowkeys[_new_row] 1008 return (_key,self._data[_key]) 1009 except: 1010 if __debug__: 1011 raise 1012 return None 1013 1014 def GetDeleteInfo(self): 1015 return guihelper.ART_DEL_CONTACT, "Delete Contact" 1016 1017 def GetAddInfo(self): 1018 return guihelper.ART_ADD_CONTACT, "Add Contact" 1019 1020 def CanAdd(self): 1021 if self.read_only: 1022 return False 1023 return True 1024 1025 def OnAdd(self, _): 1026 if self.read_only: 1027 return 1028 with guihelper.WXDialogWrapper(phonebookentryeditor.Editor(self, {'names': [{'full': 'New Entry'}]}, keytoopenon="names", dataindex=0), 1029 True) as (dlg, retcode): 1030 if retcode==wx.ID_OK: 1031 data=phonebookobjectfactory.newdataobject(dlg.GetData()) 1032 data.EnsureBitPimSerial() 1033 while True: 1034 key=int(time.time()) 1035 if key in self._data: 1036 continue 1037 break 1038 self._data[key]=data 1039 self.dt.OnDataUpdated() 1040 self.SetPreview(data) 1041 self.modified=True 1042 1043 def GetSelectedRows(self): 1044 rows=[] 1045 # if there is no data, there can't be any selected rows 1046 if len(self._data)==0: 1047 return rows 1048 gcr=self.table.GetGridCursorRow() 1049 set1=self.table.GetSelectionBlockTopLeft() 1050 set2=self.table.GetSelectionBlockBottomRight() 1051 if len(set1): 1052 assert len(set1)==len(set2) 1053 for i in range(len(set1)): 1054 for row in range(set1[i][0], set2[i][0]+1): # range in wx is inclusive of last element 1055 if row not in rows: 1056 rows.append(row) 1057 else: 1058 if gcr>=0: 1059 rows.append(gcr) 1060 1061 return rows 1062 1063 def GetSelectedRowKeys(self): 1064 return [self.dt.rowkeys[r] for r in self.GetSelectedRows()] 1065 1066 def CanDelete(self): 1067 if self.read_only: 1068 return False 1069 # there always seems to be something selected in the phonebook, so 1070 # there is no point testing for number of items, it just burns cycles 1071 return True 1072 1073 def OnDelete(self,_): 1074 if self.read_only: 1075 return 1076 for r in self.GetSelectedRowKeys(): 1077 del self._data[r] 1078 self.table.ClearSelection() 1079 self.dt.OnDataUpdated() 1080 self.modified=True 1081 1082 def SetPreview(self, entry): 1083 self.preview.ShowEntry(entry) 1084 1085 def CanPrint(self): 1086 return True 1087 1088 def OnPrintDialog(self, mainwindow, config): 1089 with guihelper.WXDialogWrapper(PhonebookPrintDialog(self, mainwindow, config), 1090 True): 1091 pass 1092 1093 def getdata(self, dict): 1094 dict['phonebook']=self._data.copy() 1095 dict['categories']=self.categories[:] 1096 return dict 1097 1098 def DeleteBySerial(self, bpserial): 1099 for k in self._data: 1100 entry=self._data[k] 1101 for serial in entry['serials']: 1102 if serial==bpserial: 1103 del self._data[k] 1104 self.dt.OnDataUpdated() 1105 self.modified=True 1106 return 1107 raise ValueError("No such entry with serial "+`bpserial`) 1108 1109 def UpdateSerial(self, bpserial, otherserial): 1110 try: 1111 for k in self._data: 1112 entry=self._data[k] 1113 for serial in entry['serials']: 1114 if serial==bpserial: 1115 # this is the entry we have been looking for 1116 for i,serial in enumerate(entry['serials']): 1117 if serial["sourcetype"]==otherserial["sourcetype"]: 1118 if otherserial.has_key("sourceuniqueid") and \ 1119 serial["sourceuniqueid"]==otherserial["sourceuniqueid"]: 1120 # replace 1121 entry['serials'][i]=otherserial 1122 return 1123 elif not otherserial.has_key("sourceuniqueid"): 1124 entry['serials'][i]=otherserial 1125 return 1126 entry['serials'].append(otherserial) 1127 return 1128 raise ValueError("No such entry with serial "+`bpserial`) 1129 finally: 1130 self.modified=True 1131 1132 1133 def versionupgrade(self, dict, version): 1134 """Upgrade old data format read from disk 1135 1136 @param dict: The dict that was read in 1137 @param version: version number of the data on disk 1138 """ 1139 1140 # version 0 to 1 upgrade 1141 if version==0: 1142 version=1 # they are the same 1143 1144 # 1 to 2 etc 1145 if version==1: 1146 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) 1147 version=2 1148 dict['result']['phonebook']={} 1149 dict['result']['categories']=[] 1150 1151 def clear(self): 1152 self._data={} 1153 self.dt.OnDataUpdated() 1154 1155 def getfromfs(self, dict, timestamp=None): 1156 self.thedir=self.mainwindow.phonebookpath 1157 if os.path.exists(os.path.join(self.thedir, "index.idx")): 1158 d={'result': {'phonebook': {}, 'categories': []}} 1159 common.readversionedindexfile(os.path.join(self.thedir, "index.idx"), d, self.versionupgrade, self.CURRENTFILEVERSION) 1160 pb=d['result']['phonebook'] 1161 database.ensurerecordtype(pb, phonebookobjectfactory) 1162 pb=database.extractbitpimserials(pb) 1163 self.mainwindow.database.savemajordict("phonebook", pb) 1164 self.mainwindow.database.savelist("categories", d['result']['categories']) 1165 # now that save is succesful, move file out of the way 1166 os.rename(os.path.join(self.thedir, "index.idx"), os.path.join(self.thedir, "index-is-now-in-database.bak")) 1167 # read info from the database 1168 dict['phonebook']=self.mainwindow.database.getmajordictvalues( 1169 "phonebook", phonebookobjectfactory, at_time=timestamp) 1170 dict['categories']=self.mainwindow.database.loadlist("categories") 1171 1172 1173 def _updatecount(self): 1174 # Update the count of contatcs 1175 self.contactcount_label.SetLabel('%(count)d'%{ 'count': len(self._data) }) 1176 1177 def populate(self, dict, savetodb=True): 1178 if self.read_only and savetodb: 1179 wx.MessageBox('You are viewing historical data which cannot be changed or saved', 1180 'Cannot Save Phonebook Data', 1181 style=wx.OK|wx.ICON_ERROR) 1182 return 1183 self.clear() 1184 pubsub.publish(pubsub.MERGE_CATEGORIES, dict['categories']) 1185 pb=dict['phonebook'] 1186 cats=[] 1187 for i in pb: 1188 for cat in pb[i].get('categories', []): 1189 cats.append(cat['category']) 1190 pubsub.publish(pubsub.MERGE_CATEGORIES, cats) 1191 k=pb.keys() 1192 k.sort() 1193 self.clear() 1194 self._data=pb.copy() 1195 self.dt.OnDataUpdated() 1196 self.modified=savetodb 1197 self._updatecount() 1198 1199 def _save_db(self, dict): 1200 self.mainwindow.database.savemajordict("phonebook", database.extractbitpimserials(dict["phonebook"])) 1201 self.mainwindow.database.savelist("categories", dict["categories"]) 1202 1203 def populatefs(self, dict): 1204 if self.read_only: 1205 wx.MessageBox('You are viewing historical data which cannot be changed or saved', 1206 'Cannot Save Phonebook Data', 1207 style=wx.OK|wx.ICON_ERROR) 1208 else: 1209 self._save_db(dict) 1210 self._updatecount() 1211 return dict 1212 1213 def _ensure_unicode(self, data): 1214 # convert and ensure unicode fields 1215 for _key,_entry in data.items(): 1216 for _field_key,_field_value in _entry.items(): 1217 if _field_key=='names': 1218 for _idx,_item in enumerate(_field_value): 1219 for _subkey, _value in _item.items(): 1220 if isinstance(_value, str): 1221 _item[_subkey]=_value.decode('ascii', 'ignore') 1222 1223 def importdata(self, importdata, categoriesinfo=[], merge=True): 1224 if self.read_only: 1225 wx.MessageBox('You are viewing historical data which cannot be changed or saved', 1226 'Cannot Save Phonebook Data', 1227 style=wx.OK|wx.ICON_ERROR) 1228 return 1229 if merge: 1230 d=self._data 1231 else: 1232 d={} 1233 normalise_data(importdata) 1234 self._ensure_unicode(importdata) 1235 with guihelper.WXDialogWrapper(ImportDialog(self, d, importdata), 1236 True) as (dlg, retcode): 1237 guiwidgets.save_size("PhoneImportMergeDialog", dlg.GetRect()) 1238 if retcode==wx.ID_OK: 1239 result=dlg.resultdata 1240 if result is not None: 1241 d={} 1242 database.ensurerecordtype(result, phonebookobjectfactory) 1243 database.ensurebitpimserials(result) 1244 d['phonebook']=result 1245 d['categories']=categoriesinfo 1246 self.populatefs(d) 1247 self.populate(d, False) 1248 1249 def converttophone(self, data): 1250 self.error_log.ClearMessages() 1251 self.mainwindow.phoneprofile.convertphonebooktophone(self, data) 1252 if self.error_log.MsgCount(): 1253 self.error_log.ShowMessages() 1254 return 1255 1256 ### 1257 ### The methods from here on are passed as the 'helper' to 1258 ### convertphonebooktophone in the phone profiles. One 1259 ### day they may move to a seperate class. 1260 ### 1261 1262 def add_error_message(self, msg, priority=99): 1263 self.error_log.AddMessage(msg, priority) 1264 1265 def log(self, msg): 1266 self.mainwindow.log(msg) 1267 1268 class ConversionFailed(Exception): 1269 pass 1270 1271 def _getentries(self, list, min, max, name): 1272 candidates=[] 1273 for i in list: 1274 # ::TODO:: possibly ensure that a key appears in each i 1275 candidates.append(i) 1276 if len(candidates)<min: 1277 # ::TODO:: log this 1278 raise self.ConversionFailed("Too few %s. Need at least %d but there were only %d" % (name,min,len(candidates))) 1279 if len(candidates)>max: 1280 # ::TODO:: mention this to user 1281 candidates=candidates[:max] 1282 return candidates 1283 1284 def _getfield(self,list,name): 1285 res=[] 1286 for i in list: 1287 res.append(i[name]) 1288 return res 1289 1290 def _truncatefields(self, list, truncateat): 1291 if truncateat is None: 1292 return list 1293 res=[] 1294 for i in list: 1295 if len(i)>truncateat: 1296 # ::TODO:: log truncation 1297 res.append(i[:truncateat]) 1298 else: 1299 res.append(i) 1300 return res 1301 1302 def _findfirst(self, candidates, required, key, default): 1303 """Find first match in candidates that meets required and return value of key 1304 1305 @param candidates: list of dictionaries to search through 1306 @param required: a dict of what key/value pairs must exist in an entry 1307 @param key: for a matching entry, which key's value to return 1308 @param default: what value to return if there is no match 1309 """ 1310 for dict in candidates: 1311 ok=True 1312 for k in required: 1313 if dict[k]!=required[k]: 1314 ok=False 1315 break # really want break 2 1316 if not ok: 1317 continue 1318 return dict.get(key, default) 1319 return default 1320 1321 def getfullname(self, names, min, max, truncateat=None): 1322 "Return at least min and at most max fullnames from the names list" 1323 # secret lastnamefirst setting 1324 if wx.GetApp().config.ReadInt("lastnamefirst", False): 1325 n=[nameparser.formatsimplelastfirst(nn) for nn in names] 1326 else: 1327 n=[nameparser.formatsimplename(nn) for nn in names] 1328 if len(n)<min: 1329 raise self.ConversionFailed("Too few names. Need at least %d but there were only %d" % (min, len(n))) 1330 if len(n)>max: 1331 n=n[:max] 1332 # ::TODO:: mention this 1333 return self._truncatefields(n, truncateat) 1334 1335 def getcategory(self, categories, min, max, truncateat=None): 1336 "Return at least min and at most max categories from the categories list" 1337 return self._truncatefields(self._getfield(self._getentries(categories, min, max, "categories"), "category"), truncateat) 1338 1339 def getemails(self, emails, min, max, truncateat=None): 1340 "Return at least min and at most max emails from the emails list" 1341 return self._truncatefields(self._getfield(self._getentries(emails, min, max, "emails"), "email"), truncateat) 1342 1343 def geturls(self, urls, min, max, truncateat=None): 1344 "Return at least min and at most max urls from the urls list" 1345 return self._truncatefields(self._getfield(self._getentries(urls, min, max, "urls"), "url"), truncateat) 1346 1347 1348 def getmemos(self, memos, min, max, truncateat=None): 1349 "Return at least min and at most max memos from the memos list" 1350 return self._truncatefields(self._getfield(self._getentries(memos, min, max, "memos"), "memo"), truncateat) 1351 1352 def getnumbers(self, numbers, min, max): 1353 "Return at least min and at most max numbers from the numbers list" 1354 return self._getentries(numbers, min, max, "numbers") 1355 1356 def getnumber(self, numbers, type, count=1, default=""): 1357 """Returns phone numbers of the type 1358 1359 @param numbers: The list of numbers 1360 @param type: The type, such as cell, home, office 1361 @param count: Which number to return (eg with type=home, count=2 the second 1362 home number is returned) 1363 @param default: What is returned if there is no such number""" 1364 for n in numbers: 1365 if n['type']==type: 1366 if count==1: 1367 return n['number'] 1368 count-=1 1369 return default 1370 1371 def getserial(self, serials, sourcetype, id, key, default): 1372 "Gets a serial if it exists" 1373 return self._findfirst(serials, {'sourcetype': sourcetype, 'sourceuniqueid': id}, key, default) 1374 1375 def getringtone(self, ringtones, use, default): 1376 "Gets a ringtone of type use" 1377 return self._findfirst(ringtones, {'use': use}, 'ringtone', default) 1378 1379 def getwallpaper(self, wallpapers, use, default): 1380 "Gets a wallpaper of type use" 1381 return self._findfirst(wallpapers, {'use': use}, 'wallpaper', default) 1382 1383 def getwallpaperindex(self, wallpapers, use, default): 1384 "Gets a wallpaper index of type use" 1385 return self._findfirst(wallpapers, {'use': use}, 'index', default) 1386 1387 def getflag(self, flags, name, default): 1388 "Gets value of flag named name" 1389 for i in flags: 1390 if i.has_key(name): 1391 return i[name] 1392 return default 1393 1394 def getmostpopularcategories(self, howmany, entries, reserved=[], truncateat=None, padnames=[]): 1395 """Returns the most popular categories 1396 1397 @param howmany: How many to return, including the reserved ones 1398 @param entries: A dict of the entries 1399 @param reserved: A list of reserved entries (ie must be present, no matter 1400 how popular) 1401 @param truncateat: How long to truncate the category names at 1402 @param padnames: if the list is less than howmany long, then add these on the end providing 1403 they are not already in the list 1404 @return: A list of the group names. The list starts with the members of 1405 reserved followed by the most popular groups 1406 """ 1407 # build a histogram 1408 freq={} 1409 for entry in entries: 1410 e=entries[entry] 1411 for cat in e.get('categories', []): 1412 n=cat['category'] 1413 if truncateat: n=n[:truncateat] # truncate 1414 freq[n]=1+freq.get(n,0) 1415 # sort 1416 freq=[(count,value) for value,count in freq.items()] 1417 freq.sort() 1418 freq.reverse() # most popular first 1419 # build a new list 1420 newl=reserved[:] 1421 for _, group in freq: 1422 if len(newl)==howmany: 1423 break 1424 if group not in newl: 1425 newl.append(group) 1426 # pad list out 1427 for p in padnames: 1428 if len(newl)==howmany: 1429 break 1430 if p not in newl: 1431 newl.append(p) 1432 1433 return newl 1434 1435 def makeone(self, list, default): 1436 "Returns one item long list" 1437 if len(list)==0: 1438 return default 1439 assert len(list)==1 1440 return list[0] 1441 1442 def filllist(self, list, numitems, blank): 1443 "makes list numitems long appending blank to get there" 1444 l=list[:] 1445 for dummy in range(len(l),numitems): 1446 l.append(blank) 1447 return l 1448 1449 1450 class ImportCellRenderer(wx.grid.PyGridCellRenderer): 1451 SCALE=0.8 1452 1453 COLOURS=["HONEYDEW", "WHITE", "LEMON CHIFFON", "ROSYBROWN1"] 1454 1455 def __init__(self, table, grid): 1456 wx.grid.PyGridCellRenderer.__init__(self) 1457 self.calc=False 1458 self.table=table 1459 1460 def _calcattrs(self): 1461 grid=self.table.GetView() 1462 self.font=grid.GetDefaultCellFont() 1463 self.facename=self.font.GetFaceName() 1464 self.facesize=self.font.GetPointSize() 1465 self.textcolour=grid.GetDefaultCellTextColour() 1466 self.brushes=[wx.Brush(wx.NamedColour(c)) for c in self.COLOURS] 1467 self.pens=[wx.Pen(wx.NamedColour(c),1 , wx.SOLID) for c in self.COLOURS] 1468 self.selbrush=wx.Brush(grid.GetSelectionBackground(), wx.SOLID) 1469 self.selpen=wx.Pen(grid.GetSelectionBackground(), 1, wx.SOLID) 1470 self.selfg=grid.GetSelectionForeground() 1471 self.calc=True 1472 1473 def Draw(self, grid, attr, dc, rect, row, col, isSelected): 1474 if not self.calc: self._calcattrs() 1475 1476 rowtype=self.table.GetRowType(row) 1477 dc.SetClippingRect(rect) 1478 1479 # clear the background 1480 dc.SetBackgroundMode(wx.SOLID) 1481 if isSelected: 1482 dc.SetBrush(self.selbrush) 1483 dc.SetPen(self.selpen) 1484 colour=self.selfg 1485 else: 1486 dc.SetBrush(self.brushes[rowtype]) 1487 dc.SetPen(self.pens[rowtype]) 1488 colour=self.textcolour 1489 1490 dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) 1491 1492 dc.SetBackgroundMode(wx.TRANSPARENT) 1493 dc.SetFont(self.font) 1494 1495 text = grid.GetTable().GetHtmlCellValue(row, col, colour) 1496 if len(text): 1497 bphtml.drawhtml(dc, 1498 wx.Rect(rect.x+2, rect.y+1, rect.width-4, rect.height-2), 1499 text, font=self.facename, size=self.facesize) 1500 dc.DestroyClippingRegion() 1501 1502 def GetBestSize(self, grid, attr, dc, row, col): 1503 if not self.calc: self._calcattrs() 1504 text = grid.GetTable().GetHtmlCellValue(row, col) 1505 if not len(text): return (5,5) 1506 return bphtml.getbestsize(dc, text, font=self.facename, size=self.facesize) 1507 1508 def Clone(self): 1509 return ImportCellRenderer() 1510 1511 1512 class ImportDataTable(wx.grid.PyGridTableBase): 1513 ADDED=0 1514 UNALTERED=1 1515 CHANGED=2 1516 DELETED=3 1517 1518 htmltemplate=["Not set - "+`i` for i in range(15)] 1519 1520 def __init__(self, widget): 1521 self.main=widget 1522 self.rowkeys=[] 1523 wx.grid.PyGridTableBase.__init__(self) 1524 self.columns=['Confidence']+ImportColumns 1525 1526 def GetRowData(self, row): 1527 """Returns a 4 part tuple as defined in ImportDialog.rowdata 1528 for the numbered row""" 1529 return self.main.rowdata[self.rowkeys[row]] 1530 1531 def GetColLabelValue(self, col): 1532 "Returns the label for the numbered column" 1533 return self.columns[col] 1534 1535 def IsEmptyCell(self, row, col): 1536 return False 1537 1538 def GetNumberCols(self): 1539 return len(self.columns) 1540 1541 def GetNumberRows(self): 1542 return len(self.rowkeys) 1543 1544 def GetRowType(self, row): 1545 """Returns what type the row is from DELETED, CHANGED, ADDED and UNALTERED""" 1546 row=self.GetRowData(row) 1547 if row[3] is None: 1548 return self.DELETED 1549 if row[1] is not None and row[2] is not None: 1550 return self.CHANGED 1551 if row[1] is not None and row[2] is None: 1552 return self.ADDED 1553 return self.UNALTERED 1554 1555 def GetValueWithNamedColumn(self, row, columnname): 1556 row=self.main.rowdata[self.rowkeys[row]] 1557 if columnname=='Confidence': 1558 return row[0] 1559 1560 for i,ptr in (3,self.main.resultdata), (1,self.main.importdata), (2, self.main.existingdata): 1561 if row[i] is not None: 1562 return getdata(columnname, ptr[row[i]], "") 1563 assert False, "Can't get here" 1564 return "" 1565 1566 def ShouldColumnBeShown(self, columnname, row): 1567 confidence, importedkey, existingkey, resultkey=self.GetRowData(row) 1568 if columnname=="Confidence": return True 1569 return (resultkey is not None and getdata(columnname, self.main.resultdata[resultkey], None) is not None) \ 1570 or (existingkey is not None and getdata(columnname, self.main.existingdata[existingkey], None) is not None) \ 1571 or (importedkey is not None and getdata(columnname, self.main.importdata[importedkey], None) is not None) 1572 1573 def GetHtmlCellValue(self, row, col, colour=None): 1574 try: 1575 row=self.GetRowData(row) 1576 except: 1577 print "bad row", row 1578 return ">error<" 1579 1580 if colour is None: 1581 colour="#000000" # black 1582 else: 1583 colour="#%02X%02X%02X" % (colour.Red(), colour.Green(), colour.Blue()) 1584 1585 if self.columns[col]=='Confidence': 1586 # row[0] could be a zero length string or an integer 1587 if row[0]=="": return "" 1588 return '<font color="%s">%d</font>' % (colour, row[0]) 1589 1590 # Get values - note default of None 1591 imported,existing,result=None,None,None 1592 if row[1] is not None: 1593 imported=getdata(self.columns[col], self.main.importdata[row[1]], None) 1594 if imported is not None: imported=common.strorunicode(imported) 1595 if row[2] is not None: 1596 existing=getdata(self.columns[col], self.main.existingdata[row[2]], None) 1597 if existing is not None: existing=common.strorunicode(existing) 1598 if row[3] is not None: 1599 result=getdata(self.columns[col], self.main.resultdata[row[3]], None) 1600 if result is not None: result=common.strorunicode(result) 1601 1602 # The following code looks at the combinations of imported, 1603 # existing and result with them being None and/or equalling 1604 # each other. Remember that the user could have 1605 # editted/deleted an entry so the result may not match either 1606 # the imported or existing value. Each combination points to 1607 # an index in the templates table. Assertions are used 1608 # extensively to ensure the logic is correct 1609 1610 if imported is None and existing is None and result is None: 1611 return "" # idx=9 - shortcut 1612 1613 # matching function for this column 1614 matchfn=lambda x,y: x==y 1615 1616 # if the result field is missing then value was deleted 1617 if result is None: 1618 # one of them must be present otherwise idx=9 above would have matched 1619 assert imported is not None or existing is not None 1620 if imported is not None and existing is not None: 1621 if matchfn(imported, existing): 1622 idx=14 1623 else: 1624 idx=13 1625 else: 1626 if imported is None: 1627 assert existing is not None 1628 idx=11 1629 else: 1630 assert existing is None 1631 idx=12 1632 1633 else: 1634 if imported is None and existing is None: 1635 idx=10 1636 else: 1637 # we have a result - the first 8 entries need the following 1638 # comparisons 1639 if imported is not None: 1640 imported_eq_result= matchfn(imported,result) 1641 if existing is not None: 1642 existing_eq_result= matchfn(existing,result) 1643 1644 # a table of all possible combinations of imported/exporting 1645 # being None and imported_eq_result/existing_eq_result 1646 if imported is None and existing_eq_result: 1647 idx=0 1648 elif imported is None and not existing_eq_result: 1649 idx=1 1650 elif imported_eq_result and existing is None: 1651 idx=2 1652 elif not imported_eq_result and existing is None: 1653 idx=3 1654 elif imported_eq_result and existing_eq_result: 1655 idx=4 1656 elif imported_eq_result and not existing_eq_result: 1657 idx=5 1658 elif not imported_eq_result and existing_eq_result: 1659 idx=6 1660 elif not imported_eq_result and not existing_eq_result: 1661 # neither imported or existing are the same as result 1662 # are they the same as each other? 1663 if matchfn(imported, existing): 1664 idx=7 1665 else: 1666 idx=8 1667 else: 1668 assert False, "This is unpossible!" 1669 return "FAILED" 1670 1671 if False: # set to true to debug this 1672 return `idx`+" "+self.htmltemplate[idx] % { 'imported': _htmlfixup(imported), 1673 'existing': _htmlfixup(existing), 1674 'result': _htmlfixup(result), 1675 'colour': colour} 1676 1677 1678 return self.htmltemplate[idx] % { 'imported': _htmlfixup(imported), 1679 'existing': _htmlfixup(existing), 1680 'result': _htmlfixup(result), 1681 'colour': colour} 1682 1683 @guihelper.BusyWrapper 1684 def OnDataUpdated(self): 1685 # update row keys 1686 newkeys=self.main.rowdata.keys() 1687 oldrows=self.rowkeys 1688 # rowkeys is kept in the same order as oldrows 1689 self.rowkeys=[k for k in oldrows if k in newkeys]+[k for k in newkeys if k not in oldrows] 1690 # now remove the ones that don't match checkboxes 1691 self.rowkeys=[self.rowkeys[n] for n in range(len(self.rowkeys)) if self.GetRowType(n) in self.main.show] 1692 # work out which columns we actually need 1693 colsavail=ImportColumns 1694 colsused=[] 1695 for row in range(len(self.rowkeys)): 1696 can=[] # cols available now 1697 for col in colsavail: 1698 if self.ShouldColumnBeShown(col, row): 1699 colsused.append(col) 1700 else: 1701 can.append(col) 1702 colsavail=can 1703 # colsused won't be in right order 1704 colsused=[c for c in ImportColumns if c in colsused] 1705 colsused=["Confidence"]+colsused 1706 lo=len(self.columns) 1707 ln=len(colsused) 1708 1709 try: 1710 sortcolumn=self.columns[self.main.sortedColumn] 1711 except IndexError: 1712 sortcolumn=0 1713 1714 # update columns 1715 self.columns=colsused 1716 if ln>lo: 1717 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_APPENDED, ln-lo) 1718 elif lo>ln: 1719 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_DELETED, 0, lo-ln) 1720 else: 1721 msg=None 1722 if msg is not None: 1723 self.GetView().ProcessTableMessage(msg) 1724 1725 # do sorting 1726 if sortcolumn not in self.columns: 1727 sortcolumn=1 1728 else: 1729 sortcolumn=self.columns.index(sortcolumn) 1730 1731 # we sort on lower case value, but note that not all columns are strings 1732 items=[] 1733 for row in range(len(self.rowkeys)): 1734 v=self.GetValue(row,sortcolumn) 1735 try: 1736 items.append((v.lower(), row)) 1737 except: 1738 items.append((v, row)) 1739 1740 items.sort() 1741 if self.main.sortedColumnDescending: 1742 items.reverse() 1743 self.rowkeys=[self.rowkeys[n] for _,n in items] 1744 1745 # update rows 1746 lo=len(oldrows) 1747 ln=len(self.rowkeys) 1748 if ln>lo: 1749 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, ln-lo) 1750 elif lo>ln: 1751 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, 0, lo-ln) 1752 else: 1753 msg=None 1754 if msg is not None: 1755 self.GetView().ProcessTableMessage(msg) 1756 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES) 1757 self.GetView().ProcessTableMessage(msg) 1758 self.GetView().ClearSelection() 1759 self.main.OnCellSelect() 1760 self.GetView().Refresh() 1761 1762 def _htmlfixup(txt): 1763 if txt is None: return "" 1764 return txt.replace("&", "&").replace("<", ">").replace(">", "<") \ 1765 .replace("\r\n", "<br>").replace("\r", "<br>").replace("\n", "<br>") 1766 1767 def workaroundyetanotherwxpythonbug(method, *args): 1768 # grrr 1769 try: 1770 return method(*args) 1771 except TypeError: 1772 print "swallowed a type error in workaroundyetanotherwxpythonbug" 1773 pass 1774 1775 ### 1776 ### 0 thru 8 inclusive have a result present 1777 ### 1778 1779 # 0 - imported is None, existing equals result 1780 ImportDataTable.htmltemplate[0]='<font color="%(colour)s">%(result)s</font>' 1781 # 1 - imported is None, existing not equal result 1782 ImportDataTable.htmltemplate[1]='<font color="%(colour)s"><strike>%(result)s</strike><br><b><font size=-1>Existing</font></b> %(existing)s</font>' 1783 # 2 - imported equals result, existing is None 1784 ImportDataTable.htmltemplate[2]='<font color="%(colour)s"><strike>%(result)s</strike></font>' 1785 # 3 - imported not equal result, existing is None 1786 ImportDataTable.htmltemplate[3]='<font color="%(colour)s"><strike>%(result)s</strike><br><b><font size=-1>Imported</font></b> %(imported)s</font>' 1787 # 4 - imported equals result, existing equals result 1788 ImportDataTable.htmltemplate[4]=ImportDataTable.htmltemplate[0] # just display result 1789 # 5 - imported equals result, existing not equals result 1790 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>' 1791 # 6 - imported not equal result, existing equals result 1792 ImportDataTable.htmltemplate[6]='<font color="%(colour)s">%(result)s<br><b><font size=-1>Imported</font></b> %(imported)s</font>' 1793 # 7 - imported not equal result, existing not equal result, imported equals existing 1794 ImportDataTable.htmltemplate[7]='<font color="%(colour)s"><strike>%(result)s</strike><br><b><font size=-1>Imported/Existing</font></b> %(imported)s</font>' 1795 # 8 - imported not equal result, existing not equal result, imported not equals existing 1796 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>' 1797 1798 ### 1799 ### Two special cases 1800 ### 1801 1802 # 9 - imported, existing, result are all None 1803 ImportDataTable.htmltemplate[9]="" 1804 1805 # 10 - imported, existing are None and result is present 1806 ImportDataTable.htmltemplate[10]='<font color="%(colour)s"><strike>%(result)s</strike></b></font>' 1807 1808 ### 1809 ### From 10 onwards, there is no result field, but one or both of 1810 ### imported/existing are present which means the user deleted the 1811 ### resulting value 1812 ### 1813 1814 # 11 - imported is None and existing is present 1815 ImportDataTable.htmltemplate[11]='<font color="#aa0000">%(existing)s</font>' 1816 # 12 - imported is present and existing is None 1817 ImportDataTable.htmltemplate[12]='<font color="#aa0000"><font size=-1>%(imported)s</font></font>' # slightly smaller 1818 # 13 - imported != existing 1819 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>' 1820 # 14 - imported equals existing 1821 ImportDataTable.htmltemplate[14]='<font color="#aa0000">%(existing)s</font>' 1822 1823 1824 class ImportDialog(wx.Dialog): 1825 "The dialog for mixing new (imported) data with existing data" 1826 1827 1828 def __init__(self, parent, existingdata, importdata): 1829 wx.Dialog.__init__(self, parent, id=-1, title="Import Phonebook data", style=wx.CAPTION| 1830 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) 1831 1832 # the data already in the phonebook 1833 self.existingdata=existingdata 1834 # the data we are importing 1835 self.importdata=importdata 1836 # the resulting data 1837 self.resultdata={} 1838 # each row to display showing what happened, with ids pointing into above data 1839 # rowdata[0]=confidence 1840 # rowdata[1]=importdatakey 1841 # rowdata[2]=existingdatakey 1842 # rowdata[3]=resultdatakey 1843 self.rowdata={} 1844 1845 vbs=wx.BoxSizer(wx.VERTICAL) 1846 1847 bg=self.GetBackgroundColour() 1848 w=wx.html.HtmlWindow(self, -1, size=wx.Size(600,50), style=wx.html.HW_SCROLLBAR_NEVER) 1849 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())) 1850 vbs.Add(w, 0, wx.EXPAND|wx.ALL, 5) 1851 1852 hbs=wx.BoxSizer(wx.HORIZONTAL) 1853 hbs.Add(wx.StaticText(self, -1, "Show entries"), 0, wx.EXPAND|wx.ALL,3) 1854 1855 self.cbunaltered=wx.CheckBox(self, wx.NewId(), "Unaltered") 1856 self.cbadded=wx.CheckBox(self, wx.NewId(), "Added") 1857 self.cbchanged=wx.CheckBox(self, wx.NewId(), "Merged") 1858 self.cbdeleted=wx.CheckBox(self, wx.NewId(), "Deleted") 1859 wx.EVT_CHECKBOX(self, self.cbunaltered.GetId(), self.OnCheckbox) 1860 wx.EVT_CHECKBOX(self, self.cbadded.GetId(), self.OnCheckbox) 1861 wx.EVT_CHECKBOX(self, self.cbchanged.GetId(), self.OnCheckbox) 1862 wx.EVT_CHECKBOX(self, self.cbdeleted.GetId(), self.OnCheckbox) 1863 1864 for i in self.cbunaltered, self.cbadded, self.cbchanged, self.cbdeleted: 1865 i.SetValue(True) 1866 hbs.Add(i, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.RIGHT, 7) 1867 1868 t=ImportDataTable 1869 self.show=[t.ADDED, t.UNALTERED, t.CHANGED, t.DELETED] 1870 1871 hbs.Add(wx.StaticText(self, -1, " "), 0, wx.EXPAND|wx.LEFT, 10) 1872 1873 vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) 1874 1875 splitter=wx.SplitterWindow(self,-1, style=wx.SP_3D|wx.SP_LIVE_UPDATE) 1876 splitter.SetMinimumPaneSize(20) 1877 1878 self.grid=wx.grid.Grid(splitter, wx.NewId()) 1879 self.table=ImportDataTable(self) 1880 1881 # this is a work around for various wxPython/wxWidgets bugs 1882 cr=ImportCellRenderer(self.table, self.grid) 1883 cr.IncRef() # wxPython bug 1884 self.grid.RegisterDataType("string", cr, None) # wxWidgets bug - it uses the string renderer rather than DefaultCellRenderer 1885 1886 self.grid.SetTable(self.table, False, wx.grid.Grid.wxGridSelectRows) 1887 self.grid.SetSelectionMode(wx.grid.Grid.wxGridSelectRows) 1888 self.grid.SetRowLabelSize(0) 1889 self.grid.EnableDragRowSize(True) 1890 self.grid.EnableEditing(False) 1891 self.grid.SetMargins(1,0) 1892 self.grid.EnableGridLines(False) 1893 1894 wx.grid.EVT_GRID_CELL_RIGHT_CLICK(self.grid, self.OnRightGridClick) 1895 wx.grid.EVT_GRID_SELECT_CELL(self.grid, self.OnCellSelect) 1896 wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self.grid, self.OnCellDClick) 1897 wx.EVT_PAINT(self.grid.GetGridColLabelWindow(), self.OnColumnHeaderPaint) 1898 wx.grid.EVT_GRID_LABEL_LEFT_CLICK(self.grid, self.OnGridLabelLeftClick) 1899 wx.grid.EVT_GRID_LABEL_LEFT_DCLICK(self.grid, self.OnGridLabelLeftClick) 1900 1901 self.resultpreview=PhoneEntryDetailsView(splitter, -1, "styles.xy", "pblayout.xy") 1902 1903 splitter.SplitVertically(self.grid, self.resultpreview) 1904 1905 vbs.Add(splitter, 1, wx.EXPAND|wx.ALL,5) 1906 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5) 1907 1908 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 1909 1910 self.SetSizer(vbs) 1911 self.SetAutoLayout(True) 1912 1913 self.config = parent.mainwindow.config 1914 guiwidgets.set_size("PhoneImportMergeDialog", self, screenpct=95, aspect=1.10) 1915 1916 self.MakeMenus() 1917 1918 self.sortedColumn=1 1919 self.sortedColumnDescending=False 1920 1921 wx.EVT_BUTTON(self, wx.ID_HELP, lambda _: wx.GetApp().displayhelpid(helpids.ID_DLG_PBMERGEENTRIES)) 1922 1923 # the splitter which adamantly insists it is 20 pixels wide no 1924 # matter how hard you try to convince it otherwise. so we force it 1925 self.splitter=splitter 1926 wx.CallAfter(self._setthedamnsplittersizeinsteadofbeingsostupid_thewindowisnot20pixelswide_isetthesizenolessthan3times_argggh) 1927 1928 wx.CallAfter(self.DoMerge) 1929 1930 1931 def _setthedamnsplittersizeinsteadofbeingsostupid_thewindowisnot20pixelswide_isetthesizenolessthan3times_argggh(self): 1932 splitter=self.splitter 1933 w,_=splitter.GetSize() 1934 splitter.SetSashPosition(max(w/2, w-200)) 1935 1936 # ::TODO:: this method and the copy earlier should be merged into a single mixin 1937 def OnColumnHeaderPaint(self, evt): 1938 w = self.grid.GetGridColLabelWindow() 1939 dc = wx.PaintDC(w) 1940 font = dc.GetFont() 1941 dc.SetTextForeground(wx.BLACK) 1942 1943 # For each column, draw it's rectangle, it's column name, 1944 # and it's sort indicator, if appropriate: 1945 totColSize = -self.grid.GetViewStart()[0]*self.grid.GetScrollPixelsPerUnit()[0] 1946 for col in range(self.grid.GetNumberCols()): 1947 dc.SetBrush(wx.Brush("WHEAT", wx.TRANSPARENT)) 1948 colSize = self.grid.GetColSize(col) 1949 rect = (totColSize,0,colSize,32) 1950 # note abuse of bool to be integer 0/1 1951 dc.DrawRectangle(rect[0] - (col!=0), rect[1], rect[2] + (col!=0), rect[3]) 1952 totColSize += colSize 1953 1954 if col == self.sortedColumn: 1955 font.SetWeight(wx.BOLD) 1956 # draw a triangle, pointed up or down, at the 1957 # top left of the column. 1958 left = rect[0] + 3 1959 top = rect[1] + 3 1960 1961 dc.SetBrush(wx.Brush("WHEAT", wx.SOLID)) 1962 if self.sortedColumnDescending: 1963 dc.DrawPolygon([(left,top), (left+6,top), (left+3,top+4)]) 1964 else: 1965 dc.DrawPolygon([(left+3,top), (left+6, top+4), (left, top+4)]) 1966 else: 1967 font.SetWeight(wx.NORMAL) 1968 1969 dc.SetFont(font) 1970 dc.DrawLabel("%s" % self.grid.GetTable().GetColLabelValue(col), 1971 rect, wx.ALIGN_CENTER | wx.ALIGN_TOP) 1972 1973 def OnGridLabelLeftClick(self, evt): 1974 col=evt.GetCol() 1975 if col==self.sortedColumn: 1976 self.sortedColumnDescending=not self.sortedColumnDescending 1977 else: 1978 self.sortedColumn=col 1979 self.sortedColumnDescending=False 1980 self.table.OnDataUpdated() 1981 1982 def OnCheckbox(self, _): 1983 t=ImportDataTable 1984 vclist=((t.ADDED, self.cbadded), (t.UNALTERED, self.cbunaltered), 1985 (t.CHANGED, self.cbchanged), (t.DELETED, self.cbdeleted)) 1986 self.show=[v for v,c in vclist if c.GetValue()] 1987 if len(self.show)==0: 1988 for v,c in vclist: 1989 self.show.append(v) 1990 c.SetValue(True) 1991 self.table.OnDataUpdated() 1992 1993 @guihelper.BusyWrapper 1994 def DoMerge(self): 1995 if len(self.existingdata)*len(self.importdata)>200: 1996 progdlg=wx.ProgressDialog("Merging entries", "BitPim is merging the new information into the existing information", 1997 len(self.existingdata), parent=self, style=wx.PD_APP_MODAL|wx.PD_CAN_ABORT|wx.PD_REMAINING_TIME) 1998 else: 1999 progdlg=None 2000 try: 2001 self._DoMerge(progdlg) 2002 finally: 2003 if progdlg: 2004 progdlg.Destroy() 2005 del progdlg 2006 2007 def _DoMerge(self, progdlg): 2008 """Merges all the importdata with existing data 2009 2010 This can take quite a while! 2011 """ 2012 2013 # We go to great lengths to ensure that a copy of the import 2014 # and existing data is passed on to the routines we call and 2015 # data structures being built. Originally the code expected 2016 # the called routines to make copies of the data they were 2017 # copying/modifying, but it proved too error prone and often 2018 # ended up modifying the original/import data passed in. That 2019 # had the terrible side effect of meaning that your original 2020 # data got modified even if you pressed cancel! 2021 2022 count=0 2023 row={} 2024 results={} 2025 2026 em=EntryMatcher(self.existingdata, self.importdata) 2027 usedimportkeys=[] 2028 for progress,existingid in enumerate(self.existingdata.keys()): 2029 if progdlg: 2030 if not progdlg.Update(progress): 2031 # user cancelled 2032 wx.CallAfter(self.EndModal, wx.ID_CANCEL) 2033 return 2034 # does it match any imported entry 2035 merged=False 2036 for confidence, importid in em.bestmatches(existingid, limit=1): 2037 if confidence>90: 2038 if importid in usedimportkeys: 2039 # someone else already used this import, lets find out who was the better match 2040 for i in row: 2041 if row[i][1]==importid: 2042 break 2043 if confidence<row[i][0]: 2044 break # they beat us so this existing passed on an importmatch 2045 # we beat the other existing - undo their merge 2046 assert i==row[i][3] 2047 row[i]=("", None, row[i][2], row[i][3]) 2048 results[i]=copy.deepcopy(self.existingdata[row[i][2]]) 2049 2050 results[count]=self.MergeEntries(copy.deepcopy(self.existingdata[existingid]), 2051 copy.deepcopy(self.importdata[importid])) 2052 row[count]=(confidence, importid, existingid, count) 2053 # update counters etc 2054 count+=1 2055 usedimportkeys.append(importid) 2056 merged=True 2057 break # we are happy with this match 2058 if not merged: 2059 results[count]=copy.deepcopy(self.existingdata[existingid]) 2060 row[count]=("", None, existingid, count) 2061 count+=1 2062 2063 # which imports went unused? 2064 for importid in self.importdata: 2065 if importid in usedimportkeys: continue 2066 results[count]=copy.deepcopy(self.importdata[importid]) 2067 row[count]=("", importid, None, count) 2068 count+=1 2069 2070 # scan thru the merged ones, and see if anything actually changed 2071 for r in row: 2072 _, importid, existingid, resid=row[r] 2073 if importid is not None and existingid is not None: 2074 checkresult=copy.deepcopy(results[resid]) 2075 checkexisting=copy.deepcopy(self.existingdata[existingid]) 2076 # we don't care about serials changing ... 2077 if "serials" in checkresult: del checkresult["serials"] 2078 if "serials" in checkexisting: del checkexisting["serials"] 2079 2080 # another sort of false positive is if the name field 2081 # has "full" defined in existing and "first", "last" 2082 # in import, and the result ends up with "full", 2083 # "first" and "last" which looks different than 2084 # existing so it shows up us a change in the UI 2085 # despite the fact that it hasn't really changed if 2086 # full and first/last are consistent with each other. 2087 # Currently we just ignore this situation. 2088 2089 if checkresult == checkexisting: 2090 # lets pretend there was no import 2091 row[r]=("", None, existingid, resid) 2092 2093 self.rowdata=row 2094 self.resultdata=results 2095 self.table.OnDataUpdated() 2096 2097 def MergeEntries(self, originalentry, importentry): 2098 "Take an original and a merge entry and join them together return a dict of the result" 2099 o=originalentry 2100 i=importentry 2101 result={} 2102 # Get the intersection. Anything not in this is not controversial 2103 intersect=dictintersection(o,i) 2104 for dict in i,o: 2105 for k in dict.keys(): 2106 if k not in intersect: 2107 result[k]=dict[k][:] 2108 # now only deal with keys in both 2109 for key in intersect: 2110 if key=="names": 2111 # we ignore anything except the first name. fields in existing take precedence 2112 r=i["names"][0] 2113 for k in o["names"][0]: 2114 r[k]=o["names"][0][k] 2115 result["names"]=[r] 2116 elif key=="numbers": 2117 result['numbers']=mergenumberlists(o['numbers'], i['numbers']) 2118 elif key=="urls": 2119 result['urls']=mergefields(o['urls'], i['urls'], 'url', cleaner=cleanurl) 2120 elif key=="emails": 2121 result['emails']=mergefields(o['emails'], i['emails'], 'email', cleaner=cleanemail) 2122 else: 2123 result[key]=common.list_union(o[key], i[key]) 2124 2125 return result 2126 2127 def OnCellSelect(self, event=None): 2128 if event is not None: 2129 event.Skip() 2130 row=self.table.GetRowData(event.GetRow()) 2131 else: 2132 gcr=self.grid.GetGridCursorRow() 2133 if gcr>=0 and gcr<self.grid.GetNumberRows(): 2134 row=self.table.GetRowData(gcr) 2135 else: # table is empty 2136 row=None,None,None,None 2137 2138 confidence,importid,existingid,resultid=row 2139 if resultid is not None: 2140 self.resultpreview.ShowEntry(self.resultdata[resultid]) 2141 else: 2142 self.resultpreview.ShowEntry({}) 2143 2144 # menu and right click handling 2145 2146 ID_EDIT_ITEM=wx.NewId() 2147 ID_REVERT_TO_IMPORTED=wx.NewId() 2148 ID_REVERT_TO_EXISTING=wx.NewId() 2149 ID_CLEAR_FIELD=wx.NewId() 2150 ID_IMPORTED_MISMATCH=wx.NewId() 2151 2152 def MakeMenus(self): 2153 menu=wx.Menu() 2154 menu.Append(self.ID_EDIT_ITEM, "Edit...") 2155 menu.Append(self.ID_REVERT_TO_EXISTING, "Revert field to existing value") 2156 menu.Append(self.ID_REVERT_TO_IMPORTED, "Revert field to imported value") 2157 menu.Append(self.ID_CLEAR_FIELD, "Clear field") 2158 menu.AppendSeparator() 2159 menu.Append(self.ID_IMPORTED_MISMATCH, "Imported entry mismatch...") 2160 self.menu=menu 2161 2162 wx.EVT_MENU(menu, self.ID_EDIT_ITEM, self.OnEditItem) 2163 wx.EVT_MENU(menu, self.ID_REVERT_TO_EXISTING, self.OnRevertFieldToExisting) 2164 wx.EVT_MENU(menu, self.ID_REVERT_TO_IMPORTED, self.OnRevertFieldToImported) 2165 wx.EVT_MENU(menu, self.ID_CLEAR_FIELD, self.OnClearField) 2166 wx.EVT_MENU(menu, self.ID_IMPORTED_MISMATCH, self.OnImportedMismatch) 2167 2168 def OnRightGridClick(self, event): 2169 row,col=event.GetRow(), event.GetCol() 2170 self.grid.SetGridCursor(row,col) 2171 self.grid.ClearSelection() 2172 # enable/disable stuff in the menu 2173 columnname=self.table.GetColLabelValue(col) 2174 _, importkey, existingkey, resultkey=self.table.GetRowData(row) 2175 2176 2177 if columnname=="Confidence": 2178 self.menu.Enable(self.ID_REVERT_TO_EXISTING, False) 2179 self.menu.Enable(self.ID_REVERT_TO_IMPORTED, False) 2180 self.menu.Enable(self.ID_CLEAR_FIELD, False) 2181 else: 2182 resultvalue=None 2183 if resultkey is not None: 2184 resultvalue=getdata(columnname, self.resultdata[resultkey], None) 2185 2186 self.menu.Enable(self.ID_REVERT_TO_EXISTING, existingkey is not None 2187 and getdata(columnname, self.existingdata[existingkey], None)!= resultvalue) 2188 self.menu.Enable(self.ID_REVERT_TO_IMPORTED, importkey is not None 2189 and getdata(columnname, self.importdata[importkey], None) != resultvalue) 2190 self.menu.Enable(self.ID_CLEAR_FIELD, True) 2191 2192 self.menu.Enable(self.ID_IMPORTED_MISMATCH, importkey is not None) 2193 # pop it up 2194 pos=event.GetPosition() 2195 self.grid.PopupMenu(self.menu, pos) 2196 2197 def OnEditItem(self,_): 2198 self.EditEntry(self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol()) 2199 2200 def OnRevertFieldToExisting(self, _): 2201 row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol() 2202 columnname=self.table.GetColLabelValue(col) 2203 row=self.table.GetRowData(row) 2204 reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]]) 2205 exkey,exindex=getdatainfo(columnname, self.existingdata[row[2]]) 2206 if exindex is None: 2207 # actually need to clear the field 2208 self.OnClearField(None) 2209 return 2210 if resindex is None: 2211 self.resultdata[row[3]][reskey].append(copy.deepcopy(self.existingdata[row[2]][exkey][exindex])) 2212 elif resindex<0: 2213 self.resultdata[row[3]][reskey]=copy.deepcopy(self.existingdata[row[2]][exkey]) 2214 else: 2215 self.resultdata[row[3]][reskey][resindex]=copy.deepcopy(self.existingdata[row[2]][exkey][exindex]) 2216 self.table.OnDataUpdated() 2217 2218 def OnRevertFieldToImported(self, _): 2219 row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol() 2220 columnname=self.table.GetColLabelValue(col) 2221 row=self.table.GetRowData(row) 2222 reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]]) 2223 imkey,imindex=getdatainfo(columnname, self.importdata[row[1]]) 2224 assert imindex is not None 2225 if resindex is None: 2226 self.resultdata[row[3]][reskey].append(copy.deepcopy(self.importdata[row[1]][imkey][imindex])) 2227 elif resindex<0: 2228 self.resultdata[row[3]][reskey]=copy.deepcopy(self.importdata[row[1]][imkey]) 2229 else: 2230 self.resultdata[row[3]][reskey][resindex]=copy.deepcopy(self.importdata[row[1]][imkey][imindex]) 2231 self.table.OnDataUpdated() 2232 2233 def OnClearField(self, _): 2234 row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol() 2235 columnname=self.table.GetColLabelValue(col) 2236 row=self.table.GetRowData(row) 2237 reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]]) 2238 assert resindex is not None 2239 if resindex<0: 2240 del self.resultdata[row[3]][reskey] 2241 else: 2242 del self.resultdata[row[3]][reskey][resindex] 2243 self.table.OnDataUpdated() 2244 2245 def OnImportedMismatch(self,_): 2246 # what are we currently matching 2247 row=self.grid.GetGridCursorRow() 2248 _,ourimportkey,existingmatchkey,resultkey=self.table.GetRowData(row) 2249 match=None 2250 # what are the choices 2251 choices=[] 2252 for row in range(self.table.GetNumberRows()): 2253 _,_,existingkey,_=self.table.GetRowData(row) 2254 if existingkey is not None: 2255 if existingmatchkey==existingkey: 2256 match=len(choices) 2257 choices.append( (getdata("Name", self.existingdata[existingkey], "<blank>"), existingkey) ) 2258 2259 with guihelper.WXDialogWrapper(ImportedEntryMatchDialog(self, choices, match), 2260 True) as (dlg, retcode): 2261 if retcode==wx.ID_OK: 2262 confidence,importkey,existingkey,resultkey=self.table.GetRowData(self.grid.GetGridCursorRow()) 2263 assert importkey is not None 2264 match=dlg.GetMatch() 2265 if match is None: 2266 # new entry 2267 if existingkey is None: 2268 wx.MessageBox("It is already a new entry!", wx.OK|wx.ICON_EXCLAMATION) 2269 return 2270 # make a new entry 2271 for rowdatakey in xrange(100000): 2272 if rowdatakey not in self.rowdata: 2273 for resultdatakey in xrange(100000): 2274 if resultdatakey not in self.resultdata: 2275 self.rowdata[rowdatakey]=("", importkey, None, resultdatakey) 2276 self.resultdata[resultdatakey]=copy.deepcopy(self.importdata[importkey]) 2277 # revert original one back 2278 self.resultdata[resultkey]=copy.deepcopy(self.existingdata[existingkey]) 2279 self.rowdata[self.table.rowkeys[self.grid.GetGridCursorRow()]]=("", None, existingkey, resultkey) 2280 self.table.OnDataUpdated() 2281 return 2282 assert False, "You really can't get here!" 2283 # match an existing entry 2284 ekey=choices[match][1] 2285 if ekey==existingkey: 2286 wx.MessageBox("That is already the entry matched!", wx.OK|wx.ICON_EXCLAMATION) 2287 return 2288 # find new match 2289 for r in range(self.table.GetNumberRows()): 2290 if r==self.grid.GetGridCursorRow(): continue 2291 confidence,importkey,existingkey,resultkey=self.table.GetRowData(r) 2292 if existingkey==ekey: 2293 if importkey is not None: 2294 wx.MessageBox("The new match already has an imported entry matching it!", "Already matched", wx.OK|wx.ICON_EXCLAMATION, self) 2295 return 2296 # clear out old match 2297 del self.rowdata[self.table.rowkeys[self.grid.GetGridCursorRow()]] 2298 # put in new one 2299 self.rowdata[self.table.rowkeys[r]]=(confidence, ourimportkey, ekey, resultkey) 2300 self.resultdata[resultkey]=self.MergeEntries( 2301 copy.deepcopy(self.existingdata[ekey]), 2302 copy.deepcopy(self.importdata[ourimportkey])) 2303 self.table.OnDataUpdated() 2304 return 2305 assert False, "Can't get here" 2306 2307 def OnCellDClick(self, event): 2308 self.EditEntry(event.GetRow(), event.GetCol()) 2309 2310 def EditEntry(self, row, col=None): 2311 row=self.table.GetRowData(row) 2312 k=row[3] 2313 # if k is none then this entry has been deleted. fix this ::TODO:: 2314 assert k is not None 2315 data=self.resultdata[k] 2316 if col is not None: 2317 columnname=self.table.GetColLabelValue(col) 2318 if columnname=="Confidence": 2319 columnname="Name" 2320 else: 2321 columnname="Name" 2322 datakey, dataindex=getdatainfo(columnname, data) 2323 with guihelper.WXDialogWrapper(phonebookentryeditor.Editor(self, data, keytoopenon=datakey, dataindex=dataindex), 2324 True) as (dlg, retcode): 2325 if retcode==wx.ID_OK: 2326 data=dlg.GetData() 2327 self.resultdata[k]=data 2328 self.table.OnDataUpdated() 2329 2330 class ImportedEntryMatchDialog(wx.Dialog): 2331 "The dialog shown to select how an imported entry should match" 2332 2333 def __init__(self, parent, choices, match): 2334 wx.Dialog.__init__(self, parent, id=-1, title="Select Import Entry Match", style=wx.CAPTION| 2335 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) 2336 2337 self.choices=choices 2338 self.importdialog=parent 2339 2340 vbs=wx.BoxSizer(wx.VERTICAL) 2341 hbs=wx.BoxSizer(wx.HORIZONTAL) 2342 self.matchexisting=wx.RadioButton(self, wx.NewId(), "Matches an existing entry below", style=wx.RB_GROUP) 2343 self.matchnew=wx.RadioButton(self, wx.NewId(), "Is a new entry") 2344 hbs.Add(self.matchexisting, wx.NewId(), wx.ALIGN_CENTRE|wx.ALL, 5) 2345 hbs.Add(self.matchnew, wx.NewId(), wx.ALIGN_CENTRE|wx.ALL, 5) 2346 vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) 2347 2348 wx.EVT_RADIOBUTTON(self, self.matchexisting.GetId(), self.OnRBClicked) 2349 wx.EVT_RADIOBUTTON(self, self.matchnew.GetId(), self.OnRBClicked) 2350 2351 splitter=wx.SplitterWindow(self, -1, style=wx.SP_3D|wx.SP_LIVE_UPDATE) 2352 self.nameslb=wx.ListBox(splitter, wx.NewId(), choices=[name for name,id in choices], style=wx.LB_SINGLE|wx.LB_NEEDED_SB) 2353 self.preview=PhoneEntryDetailsView(splitter, -1) 2354 splitter.SplitVertically(self.nameslb, self.preview) 2355 vbs.Add(splitter, 1, wx.EXPAND|wx.ALL, 5) 2356 2357 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5) 2358 2359 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 2360 2361 wx.EVT_LISTBOX(self, self.nameslb.GetId(), self.OnLbClicked) 2362 wx.EVT_LISTBOX_DCLICK(self, self.nameslb.GetId(), self.OnLbDClicked) 2363 2364 # set values 2365 if match is None: 2366 self.matchexisting.SetValue(False) 2367 self.matchnew.SetValue(True) 2368 self.nameslb.Enable(False) 2369 else: 2370 self.matchexisting.SetValue(True) 2371 self.matchnew.SetValue(False) 2372 self.nameslb.Enable(True) 2373 self.nameslb.SetSelection(match) 2374 self.preview.ShowEntry(self.importdialog.existingdata[choices[match][1]]) 2375 2376 self.SetSizer(vbs) 2377 self.SetAutoLayout(True) 2378 guiwidgets.set_size("PhonebookImportEntryMatcher", self, screenpct=75, aspect=0.58) 2379 2380 wx.EVT_MENU(self, wx.ID_OK, self.SaveSize) 2381 wx.EVT_MENU(self, wx.ID_CANCEL, self.SaveSize) 2382 2383 2384 def SaveSize(self, evt=None): 2385 if evt is not None: 2386 evt.Skip() 2387 guiwidgets.save_size("PhonebookImportEntryMatcher", self.GetRect()) 2388 2389 def OnRBClicked(self, _): 2390 self.nameslb.Enable(self.matchexisting.GetValue()) 2391 2392 def OnLbClicked(self,_=None): 2393 existingid=self.choices[self.nameslb.GetSelection()][1] 2394 self.preview.ShowEntry(self.importdialog.existingdata[existingid]) 2395 2396 def OnLbDClicked(self,_): 2397 self.OnLbClicked() 2398 self.SaveSize() 2399 self.EndModal(wx.ID_OK) 2400 2401 def GetMatch(self): 2402 if self.matchnew.GetValue(): 2403 return None # new entry 2404 return self.nameslb.GetSelection() 2405 2406 def dictintersection(one,two): 2407 return filter(two.has_key, one.keys()) 2408 2409 class EntryMatcher: 2410 "Implements matching phonebook entries" 2411 2412 def __init__(self, sources, against): 2413 self.sources=sources 2414 self.against=against 2415 2416 def bestmatches(self, sourceid, limit=5): 2417 """Gives best matches out of against list 2418 2419 @return: list of tuples of (percent match, againstid) 2420 """ 2421 2422 res=[] 2423 2424 source=self.sources[sourceid] 2425 for i in self.against: 2426 against=self.against[i] 2427 2428 # now get keys source and against have in common 2429 intersect=dictintersection(source,against) 2430 2431 # overall score for this match 2432 score=0 2433 count=0 2434 for key in intersect: 2435 s=source[key] 2436 a=against[key] 2437 count+=1 2438 if s==a: 2439 score+=40*len(s) 2440 continue 2441 2442 if key=="names": 2443 score+=comparenames(s,a) 2444 elif key=="numbers": 2445 score+=comparenumbers(s,a) 2446 elif key=="urls": 2447 score+=comparefields(s,a,"url") 2448 elif key=="emails": 2449 score+=comparefields(s,a,"email") 2450 elif key=="addresses": 2451 score+=compareallfields(s,a, ("company", "street", "street2", "city", "state", "postalcode", "country")) 2452 else: 2453 # ignore it 2454 count-=1 2455 2456 if count: 2457 res.append( ( int(score*100/count), i ) ) 2458 2459 res.sort() 2460 res.reverse() 2461 if len(res)>limit: 2462 return res[:limit] 2463 return res 2464 2465 def comparenames(s,a): 2466 "Give a score on two names" 2467 return (jarowinkler(nameparser.formatsimplename(s[0]), nameparser.formatsimplename(a[0]))-0.6)*10 2468 2469 def cleanurl(url, mode="compare"): 2470 """Returns lowercase url with the "http://" prefix removed and in lower case 2471 2472 @param mode: If the value is compare (default), it removes ""http://www."" 2473 in preparation for comparing entries. Otherwise, if the value 2474 is pb, the result is formatted for writing to the phonebook. 2475 """ 2476 if mode == "compare": 2477 urlprefix=re.compile("^(http://)?(www.)?") 2478 else: urlprefix=re.compile("^(http://)?") 2479 2480 return default_cleaner(re.sub(urlprefix, "", url).lower()) 2481 2482 def cleanemail(email, mode="compare"): 2483 """Returns lowercase email 2484 """ 2485 return default_cleaner(email.lower()) 2486 2487 2488 nondigits=re.compile("[^0-9]") 2489 def cleannumber(num): 2490 "Returns num (a phone number) with all non-digits removed" 2491 return re.sub(nondigits, "", num) 2492 2493 def comparenumbers(s,a): 2494 """Give a score on two phone numbers 2495 2496 """ 2497 2498 ss=[cleannumber(x['number']) for x in s] 2499 aa=[cleannumber(x['number']) for x in a] 2500 2501 candidates=[] 2502 for snum in ss: 2503 for anum in aa: 2504 candidates.append( (jarowinkler(snum, anum), snum, anum) ) 2505 2506 candidates.sort() 2507 candidates.reverse() 2508 2509 if len(candidates)>3: 2510 candidates=candidates[:3] 2511 2512 score=0 2513 # we now have 3 best matches 2514 for ratio,snum,anum in candidates: 2515 if ratio>0.9: 2516 score+=(ratio-0.9)*10 2517 2518 return score 2519 2520 def comparefields(s,a,valuekey,threshold=0.8,lookat=3): 2521 """Compares the valuekey field in source and against lists returning a score for closeness of match""" 2522 ss=[x[valuekey] for x in s if x.has_key(valuekey)] 2523 aa=[x[valuekey] for x in a if x.has_key(valuekey)] 2524 2525 candidates=[] 2526 for sval in ss: 2527 for aval in aa: 2528 candidates.append( (jarowinkler(sval, aval), sval, aval) ) 2529 2530 candidates.sort() 2531 candidates.reverse() 2532 2533 if len(candidates)>lookat: 2534 candidates=candidates[:lookat] 2535 2536 score=0 2537 # we now have 3 best matches 2538 for ratio,sval,aval in candidates: 2539 if ratio>threshold: 2540 score+=(ratio-threshold)*10/(1-threshold) 2541 2542 return score 2543 2544 def compareallfields(s,a,fields,threshold=0.8,lookat=3): 2545 """Like comparefields, but for source and against lists where multiple keys have values in each item 2546 2547 @param fields: This should be a list of keys from the entries that are in the order the human 2548 would write them down.""" 2549 2550 # we do it in "write them down order" as that means individual values that move don't hurt the matching 2551 # much (eg if the town was wrongly in address2 and then moved into town, the concatenated string would 2552 # still be the same and it would still be an appropriate match) 2553 args=[] 2554 for d in s,a: 2555 str="" 2556 list=[] 2557 for entry in d: 2558 for f in fields: 2559 # we merge together the fields space separated in order to make one long string from the values 2560 if entry.has_key(f): 2561 str+=entry.get(f)+" " 2562 list.append( {'value': str} ) 2563 args.append( list ) 2564 # and then give the result to comparefields 2565 args.extend( ['value', threshold, lookat] ) 2566 return comparefields(*args) 2567 2568 def mergenumberlists(orig, imp): 2569 """Return the results of merging two lists of numbers 2570 2571 We compare the sanitised numbers (ie after punctuation etc is stripped 2572 out). If they are the same, then the original is kept (since the number 2573 is the same, and the original most likely has the correct punctuation). 2574 2575 Otherwise the imported entries overwrite the originals 2576 """ 2577 # results start with existing entries 2578 res=[] 2579 res.extend(orig) 2580 # look through each imported number 2581 for i in imp: 2582 num=cleannumber(i['number']) 2583 found=False 2584 for r in res: 2585 if num==cleannumber(r['number']): 2586 # an existing entry was matched so we stop 2587 found=True 2588 if i.has_key('speeddial'): 2589 r['speeddial']=i['speeddial'] 2590 break 2591 if found: 2592 continue 2593 2594 # we will be replacing one of the same type 2595 found=False 2596 for r in res: 2597 if i['type']==r['type']: 2598 r['number']=i['number'] 2599 if i.has_key('speeddial'): 2600 r['speeddial']=i['speeddial'] 2601 found=True 2602 break 2603 if found: 2604 continue 2605 # ok, just add it on the end then 2606 res.append(i) 2607 2608 return res 2609 2610 # new jaro winkler implementation doesn't use '*' chars or similar mangling 2611 default_cleaner=lambda x: x 2612 2613 def mergefields(orig, imp, field, threshold=0.88, cleaner=default_cleaner): 2614 """Return the results of merging two lists of fields 2615 2616 We compare the fields. If they are the same, then the original is kept 2617 (since the name is the same, and the original most likely has the 2618 correct punctuation). 2619 2620 Otherwise the imported entries overwrite the originals 2621 """ 2622 # results start with existing entries 2623 res=[] 2624 res.extend(orig) 2625 # look through each imported field 2626 for i in imp: 2627 2628 impfield=cleaner(i[field]) 2629 2630 found=False 2631 for r in res: 2632 # if the imported entry is similar or the same as the 2633 # original entry, then we stop 2634 2635 # add code for short or long lengths 2636 # since cell phones usually have less than 16-22 chars max per field 2637 2638 resfield=cleaner(r[field]) 2639 2640 if (comparestrings(resfield, impfield) > threshold): 2641 # an existing entry was matched so we stop 2642 found=True 2643 2644 # since new item matches, we don't need to replace the 2645 # original value, but we should update the type of item 2646 # to reflect the imported value 2647 # for example home --> business 2648 if i.has_key('type'): 2649 r['type'] = i['type'] 2650 2651 # break out of original item loop 2652 break 2653 2654 # if we have found the item to be imported, we can move to the next one 2655 if found: 2656 continue 2657 2658 # since there is no matching item, we will replace the existing item 2659 # if a matching type exists 2660 found=False 2661 for r in res: 2662 if (i.has_key('type') and r.has_key('type')): 2663 if i['type']==r['type']: 2664 # write the field entry in the way the phonebook expects it 2665 r[field]=cleaner(i[field], "pb") 2666 found=True 2667 break 2668 if found: 2669 continue 2670 # add new item on the end if there no matching type 2671 # and write the field entry in the way the phonebook expects it 2672 i[field] = cleaner(i[field], "pb") 2673 res.append(i) 2674 2675 return res 2676 2677 import native.strings 2678 jarowinkler=native.strings.jarow 2679 2680 def comparestrings(origfield, impfield): 2681 """ Compares two strings and returns the score using 2682 winkler routine from Febrl (stringcmp.py) 2683 2684 Return value is between 0.0 and 1.0, where 0.0 means no similarity 2685 whatsoever, and 1.0 means the strings match exactly.""" 2686 return jarowinkler(origfield, impfield, 16) 2687 2688 def normalise_data(entries): 2689 for k in entries: 2690 # we only know about phone numbers so far ... 2691 for n in entries[k].get("numbers", []): 2692 n["number"]=phonenumber.normalise(n["number"]) 2693 2694 class ColumnSelectorDialog(wx.Dialog): 2695 "The dialog for selecting what columns you want to view" 2696 2697 ID_SHOW=wx.NewId() 2698 ID_AVAILABLE=wx.NewId() 2699 ID_UP=wx.NewId() 2700 ID_DOWN=wx.NewId() 2701 ID_ADD=wx.NewId() 2702 ID_REMOVE=wx.NewId() 2703 ID_DEFAULT=wx.NewId() 2704 2705 def __init__(self, parent, config, phonewidget): 2706 wx.Dialog.__init__(self, parent, id=-1, title="Select Columns to view", style=wx.CAPTION| 2707 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) 2708 2709 self.config=config 2710 self.phonewidget=phonewidget 2711 hbs=wx.BoxSizer(wx.HORIZONTAL) 2712 2713 # the show bit 2714 bs=wx.BoxSizer(wx.VERTICAL) 2715 bs.Add(wx.StaticText(self, -1, "Showing"), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 2716 self.show=wx.ListBox(self, self.ID_SHOW, style=wx.LB_SINGLE|wx.LB_NEEDED_SB, size=(250, 300)) 2717 bs.Add(self.show, 1, wx.EXPAND|wx.ALL, 5) 2718 hbs.Add(bs, 1, wx.EXPAND|wx.ALL, 5) 2719 2720 # the column of buttons 2721 bs=wx.BoxSizer(wx.VERTICAL) 2722 self.up=wx.Button(self, self.ID_UP, "Move Up") 2723 self.down=wx.Button(self, self.ID_DOWN, "Move Down") 2724 self.add=wx.Button(self, self.ID_ADD, "Show") 2725 self.remove=wx.Button(self, self.ID_REMOVE, "Don't Show") 2726 self.default=wx.Button(self, self.ID_DEFAULT, "Default") 2727 2728 for b in self.up, self.down, self.add, self.remove, self.default: 2729 bs.Add(b, 0, wx.ALL|wx.ALIGN_CENTRE, 10) 2730 2731 hbs.Add(bs, 0, wx.ALL|wx.ALIGN_CENTRE, 5) 2732 2733 # the available bit 2734 bs=wx.BoxSizer(wx.VERTICAL) 2735 bs.Add(wx.StaticText(self, -1, "Available"), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 2736 self.available=wx.ListBox(self, self.ID_AVAILABLE, style=wx.LB_EXTENDED|wx.LB_NEEDED_SB, choices=AvailableColumns) 2737 bs.Add(self.available, 1, wx.EXPAND|wx.ALL, 5) 2738 hbs.Add(bs, 1, wx.EXPAND|wx.ALL, 5) 2739 2740 # main layout 2741 vbs=wx.BoxSizer(wx.VERTICAL) 2742 vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 5) 2743 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5) 2744 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5) 2745 2746 self.SetSizer(vbs) 2747 vbs.Fit(self) 2748 2749 # fill in current selection 2750 cur=self.config.Read("phonebookcolumns", "") 2751 if len(cur): 2752 cur=cur.split(",") 2753 # ensure they all exist 2754 cur=[c for c in cur if c in AvailableColumns] 2755 else: 2756 cur=DefaultColumns 2757 self.show.Set(cur) 2758 2759 # buttons, events etc 2760 self.up.Disable() 2761 self.down.Disable() 2762 self.add.Disable() 2763 self.remove.Disable() 2764 2765 wx.EVT_LISTBOX(self, self.ID_SHOW, self.OnShowClicked) 2766 wx.EVT_LISTBOX_DCLICK(self, self.ID_SHOW, self.OnShowClicked) 2767 wx.EVT_LISTBOX(self, self.ID_AVAILABLE, self.OnAvailableClicked) 2768 wx.EVT_LISTBOX_DCLICK(self, self.ID_AVAILABLE, self.OnAvailableDClicked) 2769 2770 wx.EVT_BUTTON(self, self.ID_ADD, self.OnAdd) 2771 wx.EVT_BUTTON(self, self.ID_REMOVE, self.OnRemove) 2772 wx.EVT_BUTTON(self, self.ID_UP, self.OnUp) 2773 wx.EVT_BUTTON(self, self.ID_DOWN, self.OnDown) 2774 wx.EVT_BUTTON(self, self.ID_DEFAULT, self.OnDefault) 2775 wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk) 2776 2777 def OnShowClicked(self, _=None): 2778 self.up.Enable(self.show.GetSelection()>0) 2779 self.down.Enable(self.show.GetSelection()<self.show.GetCount()-1) 2780 self.remove.Enable(self.show.GetCount()>0) 2781 self.FindWindowById(wx.ID_OK).Enable(self.show.GetCount()>0) 2782 2783 def OnAvailableClicked(self, _): 2784 self.add.Enable(True) 2785 2786 def OnAvailableDClicked(self, _): 2787 self.OnAdd() 2788 2789 def OnAdd(self, _=None): 2790 items=[AvailableColumns[i] for i in self.available.GetSelections()] 2791 for i in self.available.GetSelections(): 2792 self.available.Deselect(i) 2793 self.add.Disable() 2794 it=self.show.GetSelection() 2795 if it>=0: 2796 self.show.Deselect(it) 2797 it+=1 2798 else: 2799 it=self.show.GetCount() 2800 self.show.InsertItems(items, it) 2801 self.remove.Disable() 2802 self.up.Disable() 2803 self.down.Disable() 2804 self.show.SetSelection(it) 2805 self.OnShowClicked() 2806 2807 def OnRemove(self, _): 2808 it=self.show.GetSelection() 2809 assert it>=0 2810 self.show.Delete(it) 2811 if self.show.GetCount(): 2812 if it==self.show.GetCount(): 2813 self.show.SetSelection(it-1) 2814 else: 2815 self.show.SetSelection(it) 2816 self.OnShowClicked() 2817 2818 def OnDefault(self,_): 2819 self.show.Set(DefaultColumns) 2820 self.show.SetSelection(0) 2821 self.OnShowClicked() 2822 2823 def OnUp(self, _): 2824 it=self.show.GetSelection() 2825 assert it>=1 2826 self.show.InsertItems([self.show.GetString(it)], it-1) 2827 self.show.Delete(it+1) 2828 self.show.SetSelection(it-1) 2829 self.OnShowClicked() 2830 2831 def OnDown(self, _): 2832 it=self.show.GetSelection() 2833 assert it<self.show.GetCount()-1 2834 self.show.InsertItems([self.show.GetString(it)], it+2) 2835 self.show.Delete(it) 2836 self.show.SetSelection(it+1) 2837 self.OnShowClicked() 2838 2839 def OnOk(self, event): 2840 cur=[self.show.GetString(i) for i in range(self.show.GetCount())] 2841 self.config.Write("phonebookcolumns", ",".join(cur)) 2842 self.config.Flush() 2843 self.phonewidget.SetColumns(cur) 2844 event.Skip() 2845 2846 class PhonebookPrintDialog(wx.Dialog): 2847 2848 ID_SELECTED=wx.NewId() 2849 ID_ALL=wx.NewId() 2850 ID_LAYOUT=wx.NewId() 2851 ID_STYLES=wx.NewId() 2852 ID_PRINT=wx.NewId() 2853 ID_PAGESETUP=wx.NewId() 2854 ID_PRINTPREVIEW=wx.NewId() 2855 ID_CLOSE=wx.ID_CANCEL 2856 ID_HELP=wx.NewId() 2857 ID_TEXTSCALE=wx.NewId() 2858 ID_SAVEASHTML=wx.NewId() 2859 2860 textscales=[ (0.4, "Teeny"), (0.6, "Tiny"), (0.8, "Small"), (1.0, "Normal"), (1.2, "Large"), (1.4, "Ginormous") ] 2861 # we reverse the order so the slider seems more natural 2862 textscales.reverse() 2863 2864 def __init__(self, phonewidget, mainwindow, config): 2865 wx.Dialog.__init__(self, mainwindow, id=-1, title="Print PhoneBook", style=wx.CAPTION| 2866 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) 2867 2868 self.config=config 2869 self.phonewidget=phonewidget 2870 2871 # sort out available layouts and styles 2872 # first line is description 2873 self.layoutfiles={} 2874 for _file in guihelper.getresourcefiles("pbpl-*.xy"): 2875 with file(_file, 'rt') as f: 2876 desc=f.readline().strip() 2877 self.layoutfiles[desc]=f.read() 2878 self.stylefiles={} 2879 for _file in guihelper.getresourcefiles("pbps-*.xy"): 2880 with file(_file, 'rt') as f: 2881 desc=f.readline().strip() 2882 self.stylefiles[desc]=f.read() 2883 2884 # Layouts 2885 vbs=wx.BoxSizer(wx.VERTICAL) # main vertical sizer 2886 2887 hbs=wx.BoxSizer(wx.HORIZONTAL) # first row 2888 2889 numselected=len(phonewidget.GetSelectedRows()) 2890 numtotal=len(phonewidget._data) 2891 2892 # selection and scale 2893 vbs2=wx.BoxSizer(wx.VERTICAL) 2894 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Rows"), wx.VERTICAL) 2895 self.selected=wx.RadioButton(self, self.ID_SELECTED, "Selected (%d)" % (numselected,), style=wx.RB_GROUP) 2896 self.all=wx.RadioButton(self, self.ID_SELECTED, "All (%d)" % (numtotal,) ) 2897 bs.Add(self.selected, 0, wx.EXPAND|wx.ALL, 2) 2898 bs.Add(self.all, 0, wx.EXPAND|wx.ALL, 2) 2899 self.selected.SetValue(numselected>1) 2900 self.all.SetValue(not (numselected>1)) 2901 vbs2.Add(bs, 0, wx.EXPAND|wx.ALL, 2) 2902 2903 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Text Scale"), wx.HORIZONTAL) 2904 for i in range(len(self.textscales)): 2905 if self.textscales[i][0]==1.0: 2906 sv=i 2907 break 2908 self.textscaleslider=wx.Slider(self, self.ID_TEXTSCALE, sv, 0, len(self.textscales)-1, style=wx.SL_VERTICAL|wx.SL_AUTOTICKS) 2909 self.scale=1 2910 bs.Add(self.textscaleslider, 0, wx.EXPAND|wx.ALL, 2) 2911 self.textscalelabel=wx.StaticText(self, -1, "Normal") 2912 bs.Add(self.textscalelabel, 0, wx.ALIGN_CENTRE) 2913 vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2) 2914 hbs.Add(vbs2, 0, wx.EXPAND|wx.ALL, 2) 2915 2916 # Sort 2917 self.sortkeyscb=[] 2918 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Sorting"), wx.VERTICAL) 2919 choices=["<None>"]+AvailableColumns 2920 for i in range(3): 2921 bs.Add(wx.StaticText(self, -1, ("Sort by", "Then")[i!=0]), 0, wx.EXPAND|wx.ALL, 2) 2922 self.sortkeyscb.append(wx.ComboBox(self, wx.NewId(), "<None>", choices=choices, style=wx.CB_READONLY)) 2923 self.sortkeyscb[-1].SetSelection(0) 2924 bs.Add(self.sortkeyscb[-1], 0, wx.EXPAND|wx.ALL, 2) 2925 hbs.Add(bs, 0, wx.EXPAND|wx.ALL, 4) 2926 2927 # Layout and style 2928 vbs2=wx.BoxSizer(wx.VERTICAL) # they are on top of each other 2929 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Layout"), wx.VERTICAL) 2930 k=self.layoutfiles.keys() 2931 k.sort() 2932 self.layout=wx.ListBox(self, self.ID_LAYOUT, style=wx.LB_SINGLE|wx.LB_NEEDED_SB|wx.LB_HSCROLL, choices=k, size=(150,-1)) 2933 self.layout.SetSelection(0) 2934 bs.Add(self.layout, 1, wx.EXPAND|wx.ALL, 2) 2935 vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2) 2936 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Styles"), wx.VERTICAL) 2937 k=self.stylefiles.keys() 2938 self.styles=wx.CheckListBox(self, self.ID_STYLES, choices=k) 2939 bs.Add(self.styles, 1, wx.EXPAND|wx.ALL, 2) 2940 vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2) 2941 hbs.Add(vbs2, 1, wx.EXPAND|wx.ALL, 2) 2942 2943 # Buttons 2944 vbs2=wx.BoxSizer(wx.VERTICAL) 2945 vbs2.Add(wx.Button(self, self.ID_PRINT, "Print"), 0, wx.EXPAND|wx.ALL, 2) 2946 vbs2.Add(wx.Button(self, self.ID_PAGESETUP, "Page Setup..."), 0, wx.EXPAND|wx.ALL, 2) 2947 vbs2.Add(wx.Button(self, self.ID_PRINTPREVIEW, "Print Preview"), 0, wx.EXPAND|wx.ALL, 2) 2948 vbs2.Add(wx.Button(self, self.ID_SAVEASHTML, "Save as HTML"), 0, wx.EXPAND|wx.ALL, 2) 2949 vbs2.Add(wx.Button(self, self.ID_CLOSE, "Close"), 0, wx.EXPAND|wx.ALL, 2) 2950 hbs.Add(vbs2, 0, wx.EXPAND|wx.ALL, 2) 2951 2952 # wrap up top row 2953 vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 2) 2954 2955 # bottom half - preview 2956 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Content Preview"), wx.VERTICAL) 2957 self.preview=bphtml.HTMLWindow(self, -1) 2958 bs.Add(self.preview, 1, wx.EXPAND|wx.ALL, 2) 2959 2960 # wrap up bottom row 2961 vbs.Add(bs, 2, wx.EXPAND|wx.ALL, 2) 2962 2963 self.SetSizer(vbs) 2964 vbs.Fit(self) 2965 2966 # event handlers 2967 wx.EVT_BUTTON(self, self.ID_PRINTPREVIEW, self.OnPrintPreview) 2968 wx.EVT_BUTTON(self, self.ID_PRINT, self.OnPrint) 2969 wx.EVT_BUTTON(self, self.ID_PAGESETUP, self.OnPageSetup) 2970 wx.EVT_BUTTON(self, self.ID_SAVEASHTML, self.OnSaveHTML) 2971 wx.EVT_RADIOBUTTON(self, self.selected.GetId(), self.UpdateHtml) 2972 wx.EVT_RADIOBUTTON(self, self.all.GetId(), self.UpdateHtml) 2973 for i in self.sortkeyscb: 2974 wx.EVT_COMBOBOX(self, i.GetId(), self.UpdateHtml) 2975 wx.EVT_LISTBOX(self, self.layout.GetId(), self.UpdateHtml) 2976 wx.EVT_CHECKLISTBOX(self, self.styles.GetId(), self.UpdateHtml) 2977 wx.EVT_COMMAND_SCROLL(self, self.textscaleslider.GetId(), self.UpdateSlider) 2978 self.UpdateHtml() 2979 2980 def UpdateSlider(self, evt): 2981 pos=evt.GetPosition() 2982 if self.textscales[pos][0]!=self.scale: 2983 self.scale=self.textscales[pos][0] 2984 self.textscalelabel.SetLabel(self.textscales[pos][1]) 2985 self.preview.SetFontScale(self.scale) 2986 2987 def UpdateHtml(self,_=None): 2988 wx.CallAfter(self._UpdateHtml) 2989 2990 def _UpdateHtml(self): 2991 self.html=self.GetCurrentHTML() 2992 self.preview.SetPage(self.html) 2993 2994 @guihelper.BusyWrapper 2995 def GetCurrentHTML(self): 2996 # Setup a nice environment pointing at this module 2997 vars={'phonebook': __import__(__name__) } 2998 # which data do we want? 2999 if self.all.GetValue(): 3000 rowkeys=self.phonewidget._data.keys() 3001 else: 3002 rowkeys=self.phonewidget.GetSelectedRowKeys() 3003 # sort the data 3004 # we actually sort in reverse order of what the UI shows in order to get correct results 3005 for keycb in (-1, -2, -3): 3006 sortkey=self.sortkeyscb[keycb].GetValue() 3007 if sortkey=="<None>": continue 3008 # decorate 3009 l=[(getdata(sortkey, self.phonewidget._data[key]), key) for key in rowkeys] 3010 l.sort() 3011 # undecorate 3012 rowkeys=[key for val,key in l] 3013 # finish up vars 3014 vars['rowkeys']=rowkeys 3015 vars['currentcolumns']=self.phonewidget.GetColumns() 3016 vars['data']=self.phonewidget._data 3017 # Use xyaptu 3018 xcp=xyaptu.xcopier(None) 3019 xcp.setupxcopy(self.layoutfiles[self.layout.GetStringSelection()]) 3020 html=xcp.xcopywithdns(vars) 3021 # apply styles 3022 sd={'styles': {}, '__builtins__': __builtins__ } 3023 for i in range(self.styles.GetCount()): 3024 if self.styles.IsChecked(i): 3025 exec self.stylefiles[self.styles.GetString(i)] in sd,sd 3026 try: 3027 html=bphtml.applyhtmlstyles(html, sd['styles']) 3028 except: 3029 if __debug__: 3030 with file("debug.html", "wt") as f: 3031 f.write(html) 3032 raise 3033 return html 3034 3035 def OnPrintPreview(self, _): 3036 wx.GetApp().htmlprinter.PreviewText(self.html, scale=self.scale) 3037 3038 def OnPrint(self, _): 3039 wx.GetApp().htmlprinter.PrintText(self.html, scale=self.scale) 3040 3041 def OnPrinterSetup(self, _): 3042 wx.GetApp().htmlprinter.PrinterSetup() 3043 3044 def OnPageSetup(self, _): 3045 wx.GetApp().htmlprinter.PageSetup() 3046 3047 def OnSaveHTML(self, _): 3048 with guihelper.WXDialogWrapper(wx.FileDialog(self, wildcard="Web Page (*.htm;*.html)|*.htm;*html", 3049 style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT), 3050 True) as (_dlg, _retcode): 3051 if _retcode==wx.ID_OK: 3052 file(_dlg.GetPath(), 'wt').write(self.html) 3053 3054 def htmlify(string): 3055 return common.strorunicode(string).replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "<br/>") 3056
Generated by PyXR 0.9.4