1
2
3
4
5
6
7
8
9
10
11 """A widget for displaying/editting the phone information
12
13 The format for a phonebook entry is standardised. It is a
14 dict with the following fields. Each field is a list, most
15 important first, with each item in the list being a dict.
16
17 names:
18
19 - title ??Job title or salutation??
20 - first
21 - middle
22 - last
23 - full You should specify the fullname or the 4 above
24 - nickname (This could also be an alias)
25
26 categories:
27
28 - category User defined category name
29 - ringtone (optional) Ringtone name for this category
30
31 emails:
32
33 - email Email address
34 - type (optional) 'home' or 'business'
35 - speeddial (optional) Speed dial for this entry
36 - ringtone (optional) ringtone name for this entry
37 - wallpaper (optional) wallpaper name for this entry
38
39 maillist:
40
41 - entry string of '\x00\x00' separated of number or email entries.
42 - speeddial (optional) Speed dial for this entry
43 - ringtone (optional) ringtone name for this entry
44 - wallpaper (optional) wallpaper name for this entry
45
46 urls:
47
48 - url URL
49 - type (optional) 'home' or 'business'
50
51 ringtones:
52
53 - ringtone Name of a ringtone
54 - use 'call', 'message'
55
56 addresses:
57
58 - type 'home' or 'business'
59 - company (only for type of 'business')
60 - street Street part of address
61 - street2 Second line of street address
62 - city
63 - state
64 - postalcode
65 - country Can also be the region
66
67 wallpapers:
68
69 - wallpaper Name of wallpaper
70 - use see ringtones.use
71
72 flags:
73
74 - secret Boolean if record is private/secret (if not present - value is false)
75 - sim Boolean if record should be stored on SIM card of GSM phones.
76
77 memos:
78
79 - memo Note
80
81 numbers:
82
83 - number Phone number as ascii string
84 - type 'home', 'office', 'cell', 'fax', 'pager', 'data', 'none' (if you have home2 etc, list
85 them without the digits. The second 'home' is implicitly home2 etc)
86 - speeddial (optional) Speed dial number
87 - ringtone (optional) ringtone name for this entry
88 - wallpaper (optional) wallpaper name for this entry
89
90 serials:
91
92 - sourcetype identifies source driver in bitpim (eg "lgvx4400", "windowsaddressbook")
93 - sourceuniqueid (optional) identifier for where the serial came from (eg ESN of phone, wab host/username)
94 (imagine having multiple phones of the same model to see why this is needed)
95 - * other names of use to sourcetype
96 """
97
98
99 from __future__ import with_statement
100 import os
101 import cStringIO
102 import re
103 import time
104 import copy
105
106
107 import wx
108 import wx.grid
109 import wx.html
110
111
112 import common
113 import xyaptu
114 import guihelper
115 import phonebookentryeditor
116 import pubsub
117 import nameparser
118 import bphtml
119 import guihelper
120 import guiwidgets
121 import phonenumber
122 import helpids
123 import database
124 import widgets
133
134 _knownlistproperties=database.basedataobject._knownlistproperties.copy()
135 _knownlistproperties.update( {'names': ['title', 'first', 'middle', 'last', 'full', 'nickname'],
136 'categories': ['category'],
137 'emails': ['email', 'type', 'speeddial',
138 'ringtone', 'wallpaper' ],
139 'urls': ['url', 'type'],
140 'ringtones': ['ringtone', 'use'],
141 'addresses': ['type', 'company', 'street', 'street2', 'city', 'state', 'postalcode', 'country'],
142 'wallpapers': ['wallpaper', 'use'],
143 'flags': ['secret', 'sim'],
144 'memos': ['memo'],
145 'numbers': ['number', 'type', 'speeddial',
146 'ringtone', 'wallpaper' ],
147 'ice': [ 'iceindex' ],
148 'favorite': [ 'favoriteindex' ],
149 'ims': [ 'type', 'username' ],
150
151
152
153 })
154
155 phonebookobjectfactory=database.dataobjectfactory(phonebookdataobject)
156
157
158
159
160
161 -class PhoneEntryDetailsView(bphtml.HTMLWindow):
162
163 - def __init__(self, parent, id, stylesfile="styles.xy", layoutfile="pblayout.xy"):
164 bphtml.HTMLWindow.__init__(self, parent, id)
165 self.stylesfile=guihelper.getresourcefile(stylesfile)
166 self.pblayoutfile=guihelper.getresourcefile(layoutfile)
167 self.xcp=None
168 self.xcpstyles=None
169 self.ShowEntry({})
170
171 - def ShowEntry(self, entry):
172 if self.xcp is None:
173 template=open(self.pblayoutfile, "rt").read()
174 self.xcp=xyaptu.xcopier(None)
175 self.xcp.setupxcopy(template)
176 if self.xcpstyles is None:
177 self.xcpstyles={}
178 try:
179 execfile(self.stylesfile, self.xcpstyles, self.xcpstyles)
180 except UnicodeError:
181 common.unicode_execfile(self.stylesfile, self.xcpstyles, self.xcpstyles)
182 self.xcpstyles['entry']=entry
183 text=self.xcp.xcopywithdns(self.xcpstyles)
184 try:
185 text=bphtml.applyhtmlstyles(text, self.xcpstyles['styles'])
186 except:
187 if __debug__:
188 open("debug.html", "wt").write(common.forceascii(text))
189 raise
190 self.SetPage(text)
191
198
201
206
213
221
227
230
233
234
235
236
237
238 _getdatalist=[
239
240 'Name', ("names", 0, None, nameparser.formatfullname, True),
241 'First', ("names", 0, None, nameparser.getfirst, False),
242 'Middle', ("names", 0, None, nameparser.getmiddle, False),
243 'Last', ("names", 0, None, nameparser.getlast, False),
244
245 'Category', ("categories", 0, None, "category", False),
246 'Category2', ("categories", 1, None, "category", False),
247 'Category3', ("categories", 2, None, "category", False),
248 'Category4', ("categories", 3, None, "category", False),
249 'Category5', ("categories", 4, None, "category", False),
250 'Categories', ("categories", None, None, formatcategories, True),
251
252 "Phone", ("numbers", 0, None, formattypenumber, False),
253 "Phone2", ("numbers", 1, None, formattypenumber, False),
254 "Phone3", ("numbers", 2, None, formattypenumber, False),
255 "Phone4", ("numbers", 3, None, formattypenumber, False),
256 "Phone5", ("numbers", 4, None, formattypenumber, False),
257 "Phone6", ("numbers", 5, None, formattypenumber, False),
258 "Phone7", ("numbers", 6, None, formattypenumber, False),
259 "Phone8", ("numbers", 7, None, formattypenumber, False),
260 "Phone9", ("numbers", 8, None, formattypenumber, False),
261 "Phone10", ("numbers", 9, None, formattypenumber, False),
262
263
264
265 'Email', ("emails", 0, None, "email", True),
266 'Email2', ("emails", 1, None, "email", True),
267 'Email3', ("emails", 2, None, "email", True),
268 'Email4', ("emails", 3, None, "email", True),
269 'Email5', ("emails", 4, None, "email", True),
270 'Business Email', ("emails", 0, ("type", "business"), "email", False),
271 'Business Email2', ("emails", 1, ("type", "business"), "email", False),
272 'Home Email', ("emails", 0, ("type", "home"), "email", False),
273 'Home Email2', ("emails", 1, ("type", "home"), "email", False),
274
275 'URL', ("urls", 0, None, "url", True),
276 'URL2', ("urls", 1, None, "url", True),
277 'URL3', ("urls", 2, None, "url", True),
278 'URL4', ("urls", 3, None, "url", True),
279 'URL5', ("urls", 4, None, "url", True),
280 'Business URL', ("urls", 0, ("type", "business"), "url", False),
281 'Business URL2', ("urls", 1, ("type", "business"), "url", False),
282 'Home URL', ("urls", 0, ("type", "home"), "url", False),
283 'Home URL2', ("urls", 1, ("type", "home"), "url", False),
284
285 'Ringtone', ("ringtones", 0, ("use", "call"), "ringtone", True),
286 'Message Ringtone', ("ringtones", 0, ("use", "message"), "ringtone", True),
287
288 'Address', ("addresses", 0, None, formataddress, True),
289 'Address2', ("addresses", 1, None, formataddress, True),
290 'Address3', ("addresses", 2, None, formataddress, True),
291 'Address4', ("addresses", 3, None, formataddress, True),
292 'Address5', ("addresses", 4, None, formataddress, True),
293 'Home Address', ("addresses", 0, ("type", "home"), formataddress, False),
294 'Home Address2', ("addresses", 1, ("type", "home"), formataddress, False),
295 'Business Address', ("addressess", 0, ("type", "business"), formataddress, False),
296 'Business Address2', ("addressess", 1, ("type", "business"), formataddress, False),
297
298 "Wallpaper", ("wallpapers", 0, None, "wallpaper", True),
299
300 "Secret", ("flags", 0, ("secret", True), formatsecret, True),
301 "Storage", ("flags", 0,('sim', True), formatstorage, True),
302 "Memo", ("memos", 0, None, "memo", True),
303 "Memo2", ("memos", 1, None, "memo", True),
304 "Memo3", ("memos", 2, None, "memo", True),
305 "Memo4", ("memos", 3, None, "memo", True),
306 "Memo5", ("memos", 4, None, "memo", True),
307
308 "ICE #", ("ice", 0, None, formatICE, False),
309 "Favorite #", ("favorite", 0, None, formatFav, False)
310
311 ]
312
313 ll=[]
314 for pretty, actual in ("Home", "home"), ("Office", "office"), ("Cell", "cell"), ("Fax", "fax"), ("Pager", "pager"), ("Data", "data"):
315 for suf,n in ("", 0), ("2", 1), ("3", 2):
316 ll.append(pretty+suf)
317 ll.append(("numbers", n, ("type", actual), formatnumber, True))
318 _getdatalist[40:40]=ll
319
320 _getdatatable={}
321 AvailableColumns=[]
322 DefaultColumns=['Name', 'Phone', 'Phone2', 'Phone3', 'Email', 'Categories', 'Memo', 'Secret']
323 ImportColumns=[_getdatalist[x*2] for x in range(len(_getdatalist)/2) if _getdatalist[x*2+1][4]]
324
325 for n in range(len(_getdatalist)/2):
326 AvailableColumns.append(_getdatalist[n*2])
327 _getdatatable[_getdatalist[n*2]]=_getdatalist[n*2+1]
328
329 del _getdatalist
330
331 -def getdata(column, entry, default=None):
332 """Returns the value in a particular column.
333 Note that the data is appropriately formatted.
334
335 @param column: column name
336 @param entry: the dict representing a phonebook entry
337 @param default: what to return if the entry has no data for that column
338 """
339 key, count, prereq, formatter, _ =_getdatatable[column]
340
341
342 if key not in entry:
343 return default
344
345 if count is None:
346
347 thevalue=entry[key]
348 elif prereq is None:
349
350 if len(entry[key])<=count:
351 return default
352 thevalue=entry[key][count]
353 else:
354
355 ptr=0
356 togo=count+1
357 l=entry[key]
358 k,v=prereq
359 while togo:
360 if ptr==len(l):
361 return default
362 if k not in l[ptr]:
363 ptr+=1
364 continue
365 if l[ptr][k]!=v:
366 ptr+=1
367 continue
368 togo-=1
369 if togo!=0:
370 ptr+=1
371 continue
372 thevalue=entry[key][ptr]
373 break
374
375
376 if callable(formatter):
377 return formatter(thevalue)
378
379 return thevalue.get(formatter, default)
380
382 """Similar to L{getdata} except returning higher level information.
383
384 Returns the key name and which index from the list corresponds to
385 the column.
386
387 @param column: Column name
388 @param entry: The dict representing a phonebook entry
389 @returns: (keyname, index) tuple. index will be None if the entry doesn't
390 have the relevant column value and -1 if all of them apply
391 """
392 key, count, prereq, formatter, _ =_getdatatable[column]
393
394
395 if key not in entry:
396 return (key, None)
397
398
399 if count is None:
400 return (key, -1)
401 elif prereq is None:
402
403 if len(entry[key])<=count:
404 return (key, None)
405 return (key, count)
406 else:
407
408 ptr=0
409 togo=count+1
410 l=entry[key]
411 k,v=prereq
412 while togo:
413 if ptr==len(l):
414 return (key,None)
415 if k not in l[ptr]:
416 ptr+=1
417 continue
418 if l[ptr][k]!=v:
419 ptr+=1
420 continue
421 togo-=1
422 if togo!=0:
423 ptr+=1
424 continue
425 return (key, ptr)
426 return (key, None)
427
429
430
431
432 __publisher=pubsub.Publisher
433
445
450
453
455 self.group_wps=msg.data[:]
456 self.OnGroupWPRequest()
457
465
471
482
484 new_groups=msg.data[:]
485 gwp=self.group_wps[:]
486 temp_dict={}
487 for entry in gwp:
488 l=entry.split(":", 1)
489 name=l[0]
490 wp=l[1]
491 temp_dict[name]=wp
492 for entry in new_groups:
493 l=entry.split(":", 1)
494 name=l[0]
495 wp=l[1]
496 temp_dict[name]=wp
497 out_list=[]
498 for k, v in temp_dict.items():
499 out_list.append(str(k)+":"+str(v))
500 self.group_wps=out_list
501 self.OnGroupWPRequest()
502
503
504 CategoryManager=CategoryManager()
511
513 self.main=widget
514 self.rowkeys=self.main._data.keys()
515 wx.grid.PyGridTableBase.__init__(self)
516 self.oddattr=wx.grid.GridCellAttr()
517 self.oddattr.SetBackgroundColour("OLDLACE")
518 self.evenattr=wx.grid.GridCellAttr()
519 self.evenattr.SetBackgroundColour("ALICE BLUE")
520 self.columns=columns
521 assert len(self.rowkeys)==0
522
524 return self.columns[col]
525
527 newkeys=self.main._data.keys()
528 newkeys.sort()
529 oldrows=self.rowkeys
530 self.rowkeys=newkeys
531 lo=len(oldrows)
532 ln=len(self.rowkeys)
533 if ln>lo:
534 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, ln-lo)
535 elif lo>ln:
536 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, 0, lo-ln)
537 else:
538 msg=None
539 if msg is not None:
540 self.GetView().ProcessTableMessage(msg)
541 self.Sort()
542 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES)
543 self.GetView().ProcessTableMessage(msg)
544 self.GetView().AutoSizeColumns()
545
547 oldcols=self.columns
548 self.columns=columns
549 lo=len(oldcols)
550 ln=len(self.columns)
551 if ln>lo:
552 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_APPENDED, ln-lo)
553 elif lo>ln:
554 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_DELETED, 0, lo-ln)
555 else:
556 msg=None
557 if msg is not None:
558 self.GetView().ProcessTableMessage(msg)
559 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES)
560 self.GetView().ProcessTableMessage(msg)
561 self.GetView().AutoSizeColumns()
562
564 bycol=self.main.sortedColumn
565 descending=self.main.sortedColumnDescending
566
567 l=[ (getdata(self.columns[bycol], self.main._data[key]), key) for key in self.rowkeys]
568 l.sort()
569 if descending:
570 l.reverse()
571 self.rowkeys=[key for val,key in l]
572 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES)
573 self.GetView().ProcessTableMessage(msg)
574
577
579 return len(self.rowkeys)
580
582 return len(self.columns)
583
585 try:
586 entry=self.main._data[self.rowkeys[row]]
587 except:
588 print "bad row", row
589 return "<error>"
590
591 return getdata(self.columns[col], entry, "")
592
594 r=[self.evenattr, self.oddattr][row%2]
595 r.IncRef()
596 return r
597
1511
1514 SCALE=0.8
1515
1516 COLOURS=["HONEYDEW", "WHITE", "LEMON CHIFFON", "ROSYBROWN1"]
1517
1519 wx.grid.PyGridCellRenderer.__init__(self)
1520 self.calc=False
1521 self.table=table
1522
1524 grid=self.table.GetView()
1525 self.font=grid.GetDefaultCellFont()
1526 self.facename=self.font.GetFaceName()
1527 self.facesize=self.font.GetPointSize()
1528 self.textcolour=grid.GetDefaultCellTextColour()
1529 self.brushes=[wx.Brush(wx.NamedColour(c)) for c in self.COLOURS]
1530 self.pens=[wx.Pen(wx.NamedColour(c),1 , wx.SOLID) for c in self.COLOURS]
1531 self.selbrush=wx.Brush(grid.GetSelectionBackground(), wx.SOLID)
1532 self.selpen=wx.Pen(grid.GetSelectionBackground(), 1, wx.SOLID)
1533 self.selfg=grid.GetSelectionForeground()
1534 self.calc=True
1535
1536 - def Draw(self, grid, attr, dc, rect, row, col, isSelected):
1537 if not self.calc: self._calcattrs()
1538
1539 rowtype=self.table.GetRowType(row)
1540 dc.SetClippingRect(rect)
1541
1542
1543 dc.SetBackgroundMode(wx.SOLID)
1544 if isSelected:
1545 dc.SetBrush(self.selbrush)
1546 dc.SetPen(self.selpen)
1547 colour=self.selfg
1548 else:
1549 dc.SetBrush(self.brushes[rowtype])
1550 dc.SetPen(self.pens[rowtype])
1551 colour=self.textcolour
1552
1553 dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
1554
1555 dc.SetBackgroundMode(wx.TRANSPARENT)
1556 dc.SetFont(self.font)
1557
1558 text = grid.GetTable().GetHtmlCellValue(row, col, colour)
1559 if len(text):
1560 bphtml.drawhtml(dc,
1561 wx.Rect(rect.x+2, rect.y+1, rect.width-4, rect.height-2),
1562 text, font=self.facename, size=self.facesize)
1563 dc.DestroyClippingRegion()
1564
1570
1573
1576 ADDED=0
1577 UNALTERED=1
1578 CHANGED=2
1579 DELETED=3
1580
1581 htmltemplate=["Not set - "+`i` for i in range(15)]
1582
1584 self.main=widget
1585 self.rowkeys=[]
1586 wx.grid.PyGridTableBase.__init__(self)
1587 self.columns=['Confidence']+ImportColumns
1588
1590 """Returns a 4 part tuple as defined in ImportDialog.rowdata
1591 for the numbered row"""
1592 return self.main.rowdata[self.rowkeys[row]]
1593
1595 "Returns the label for the numbered column"
1596 return self.columns[col]
1597
1600
1602 return len(self.columns)
1603
1605 return len(self.rowkeys)
1606
1608 """Returns what type the row is from DELETED, CHANGED, ADDED and UNALTERED"""
1609 row=self.GetRowData(row)
1610 if row[3] is None:
1611 return self.DELETED
1612 if row[1] is not None and row[2] is not None:
1613 return self.CHANGED
1614 if row[1] is not None and row[2] is None:
1615 return self.ADDED
1616 return self.UNALTERED
1617
1619 row=self.main.rowdata[self.rowkeys[row]]
1620 if columnname=='Confidence':
1621 return row[0]
1622
1623 for i,ptr in (3,self.main.resultdata), (1,self.main.importdata), (2, self.main.existingdata):
1624 if row[i] is not None:
1625 return getdata(columnname, ptr[row[i]], "")
1626 assert False, "Can't get here"
1627 return ""
1628
1630 confidence, importedkey, existingkey, resultkey=self.GetRowData(row)
1631 if columnname=="Confidence": return True
1632 return (resultkey is not None and getdata(columnname, self.main.resultdata[resultkey], None) is not None) \
1633 or (existingkey is not None and getdata(columnname, self.main.existingdata[existingkey], None) is not None) \
1634 or (importedkey is not None and getdata(columnname, self.main.importdata[importedkey], None) is not None)
1635
1637 try:
1638 row=self.GetRowData(row)
1639 except:
1640 print "bad row", row
1641 return ">error<"
1642
1643 if colour is None:
1644 colour="#000000"
1645 else:
1646 colour="#%02X%02X%02X" % (colour.Red(), colour.Green(), colour.Blue())
1647
1648 if self.columns[col]=='Confidence':
1649
1650 if row[0]=="": return ""
1651 return '<font color="%s">%d</font>' % (colour, row[0])
1652
1653
1654 imported,existing,result=None,None,None
1655 if row[1] is not None:
1656 imported=getdata(self.columns[col], self.main.importdata[row[1]], None)
1657 if imported is not None: imported=common.strorunicode(imported)
1658 if row[2] is not None:
1659 existing=getdata(self.columns[col], self.main.existingdata[row[2]], None)
1660 if existing is not None: existing=common.strorunicode(existing)
1661 if row[3] is not None:
1662 result=getdata(self.columns[col], self.main.resultdata[row[3]], None)
1663 if result is not None: result=common.strorunicode(result)
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673 if imported is None and existing is None and result is None:
1674 return ""
1675
1676
1677 matchfn=lambda x,y: x==y
1678
1679
1680 if result is None:
1681
1682 assert imported is not None or existing is not None
1683 if imported is not None and existing is not None:
1684 if matchfn(imported, existing):
1685 idx=14
1686 else:
1687 idx=13
1688 else:
1689 if imported is None:
1690 assert existing is not None
1691 idx=11
1692 else:
1693 assert existing is None
1694 idx=12
1695
1696 else:
1697 if imported is None and existing is None:
1698 idx=10
1699 else:
1700
1701
1702 if imported is not None:
1703 imported_eq_result= matchfn(imported,result)
1704 if existing is not None:
1705 existing_eq_result= matchfn(existing,result)
1706
1707
1708
1709 if imported is None and existing_eq_result:
1710 idx=0
1711 elif imported is None and not existing_eq_result:
1712 idx=1
1713 elif imported_eq_result and existing is None:
1714 idx=2
1715 elif not imported_eq_result and existing is None:
1716 idx=3
1717 elif imported_eq_result and existing_eq_result:
1718 idx=4
1719 elif imported_eq_result and not existing_eq_result:
1720 idx=5
1721 elif not imported_eq_result and existing_eq_result:
1722 idx=6
1723 elif not imported_eq_result and not existing_eq_result:
1724
1725
1726 if matchfn(imported, existing):
1727 idx=7
1728 else:
1729 idx=8
1730 else:
1731 assert False, "This is unpossible!"
1732 return "FAILED"
1733
1734 if False:
1735 return `idx`+" "+self.htmltemplate[idx] % { 'imported': _htmlfixup(imported),
1736 'existing': _htmlfixup(existing),
1737 'result': _htmlfixup(result),
1738 'colour': colour}
1739
1740
1741 return self.htmltemplate[idx] % { 'imported': _htmlfixup(imported),
1742 'existing': _htmlfixup(existing),
1743 'result': _htmlfixup(result),
1744 'colour': colour}
1745
1746 @guihelper.BusyWrapper
1748
1749 newkeys=self.main.rowdata.keys()
1750 oldrows=self.rowkeys
1751
1752 self.rowkeys=[k for k in oldrows if k in newkeys]+[k for k in newkeys if k not in oldrows]
1753
1754 self.rowkeys=[self.rowkeys[n] for n in range(len(self.rowkeys)) if self.GetRowType(n) in self.main.show]
1755
1756 colsavail=ImportColumns
1757 colsused=[]
1758 for row in range(len(self.rowkeys)):
1759 can=[]
1760 for col in colsavail:
1761 if self.ShouldColumnBeShown(col, row):
1762 colsused.append(col)
1763 else:
1764 can.append(col)
1765 colsavail=can
1766
1767 colsused=[c for c in ImportColumns if c in colsused]
1768 colsused=["Confidence"]+colsused
1769 lo=len(self.columns)
1770 ln=len(colsused)
1771
1772 try:
1773 sortcolumn=self.columns[self.main.sortedColumn]
1774 except IndexError:
1775 sortcolumn=0
1776
1777
1778 self.columns=colsused
1779 if ln>lo:
1780 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_APPENDED, ln-lo)
1781 elif lo>ln:
1782 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_DELETED, 0, lo-ln)
1783 else:
1784 msg=None
1785 if msg is not None:
1786 self.GetView().ProcessTableMessage(msg)
1787
1788
1789 if sortcolumn not in self.columns:
1790 sortcolumn=1
1791 else:
1792 sortcolumn=self.columns.index(sortcolumn)
1793
1794
1795 items=[]
1796 for row in range(len(self.rowkeys)):
1797 v=self.GetValue(row,sortcolumn)
1798 try:
1799 items.append((v.lower(), row))
1800 except:
1801 items.append((v, row))
1802
1803 items.sort()
1804 if self.main.sortedColumnDescending:
1805 items.reverse()
1806 self.rowkeys=[self.rowkeys[n] for _,n in items]
1807
1808
1809 lo=len(oldrows)
1810 ln=len(self.rowkeys)
1811 if ln>lo:
1812 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, ln-lo)
1813 elif lo>ln:
1814 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, 0, lo-ln)
1815 else:
1816 msg=None
1817 if msg is not None:
1818 self.GetView().ProcessTableMessage(msg)
1819 msg=wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES)
1820 self.GetView().ProcessTableMessage(msg)
1821 self.GetView().ClearSelection()
1822 self.main.OnCellSelect()
1823 self.GetView().Refresh()
1824
1829
1831
1832 try:
1833 return method(*args)
1834 except TypeError:
1835 print "swallowed a type error in workaroundyetanotherwxpythonbug"
1836 pass
1837
1888 "The dialog for mixing new (imported) data with existing data"
1889
1890
1891 - def __init__(self, parent, existingdata, importdata):
1892 wx.Dialog.__init__(self, parent, id=-1, title="Import Phonebook data", style=wx.CAPTION|
1893 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1894
1895
1896 self.existingdata=existingdata
1897
1898 self.importdata=importdata
1899
1900 self.resultdata={}
1901
1902
1903
1904
1905
1906 self.rowdata={}
1907
1908 vbs=wx.BoxSizer(wx.VERTICAL)
1909
1910 bg=self.GetBackgroundColour()
1911 w=wx.html.HtmlWindow(self, -1, size=wx.Size(600,50), style=wx.html.HW_SCROLLBAR_NEVER)
1912 w.SetPage('<html><body BGCOLOR="#%02X%02X%02X">Your data is being imported and BitPim is showing what will happen below so you can confirm its actions.</body></html>' % (bg.Red(), bg.Green(), bg.Blue()))
1913 vbs.Add(w, 0, wx.EXPAND|wx.ALL, 5)
1914
1915 hbs=wx.BoxSizer(wx.HORIZONTAL)
1916 hbs.Add(wx.StaticText(self, -1, "Show entries"), 0, wx.EXPAND|wx.ALL,3)
1917
1918 self.cbunaltered=wx.CheckBox(self, wx.NewId(), "Unaltered")
1919 self.cbadded=wx.CheckBox(self, wx.NewId(), "Added")
1920 self.cbchanged=wx.CheckBox(self, wx.NewId(), "Merged")
1921 self.cbdeleted=wx.CheckBox(self, wx.NewId(), "Deleted")
1922 wx.EVT_CHECKBOX(self, self.cbunaltered.GetId(), self.OnCheckbox)
1923 wx.EVT_CHECKBOX(self, self.cbadded.GetId(), self.OnCheckbox)
1924 wx.EVT_CHECKBOX(self, self.cbchanged.GetId(), self.OnCheckbox)
1925 wx.EVT_CHECKBOX(self, self.cbdeleted.GetId(), self.OnCheckbox)
1926
1927 for i in self.cbunaltered, self.cbadded, self.cbchanged, self.cbdeleted:
1928 i.SetValue(True)
1929 hbs.Add(i, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.RIGHT, 7)
1930
1931 t=ImportDataTable
1932 self.show=[t.ADDED, t.UNALTERED, t.CHANGED, t.DELETED]
1933
1934 hbs.Add(wx.StaticText(self, -1, " "), 0, wx.EXPAND|wx.LEFT, 10)
1935
1936 vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
1937
1938 splitter=wx.SplitterWindow(self,-1, style=wx.SP_3D|wx.SP_LIVE_UPDATE)
1939 splitter.SetMinimumPaneSize(20)
1940
1941 self.grid=wx.grid.Grid(splitter, wx.NewId())
1942 self.table=ImportDataTable(self)
1943
1944
1945 cr=ImportCellRenderer(self.table, self.grid)
1946 cr.IncRef()
1947 self.grid.RegisterDataType("string", cr, None)
1948
1949 self.grid.SetTable(self.table, False, wx.grid.Grid.wxGridSelectRows)
1950 self.grid.SetSelectionMode(wx.grid.Grid.wxGridSelectRows)
1951 self.grid.SetRowLabelSize(0)
1952 self.grid.EnableDragRowSize(True)
1953 self.grid.EnableEditing(False)
1954 self.grid.SetMargins(1,0)
1955 self.grid.EnableGridLines(False)
1956
1957 wx.grid.EVT_GRID_CELL_RIGHT_CLICK(self.grid, self.OnRightGridClick)
1958 wx.grid.EVT_GRID_SELECT_CELL(self.grid, self.OnCellSelect)
1959 wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self.grid, self.OnCellDClick)
1960 wx.EVT_PAINT(self.grid.GetGridColLabelWindow(), self.OnColumnHeaderPaint)
1961 wx.grid.EVT_GRID_LABEL_LEFT_CLICK(self.grid, self.OnGridLabelLeftClick)
1962 wx.grid.EVT_GRID_LABEL_LEFT_DCLICK(self.grid, self.OnGridLabelLeftClick)
1963
1964 self.resultpreview=PhoneEntryDetailsView(splitter, -1, "styles.xy", "pblayout.xy")
1965
1966 splitter.SplitVertically(self.grid, self.resultpreview)
1967
1968 vbs.Add(splitter, 1, wx.EXPAND|wx.ALL,5)
1969 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5)
1970
1971 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
1972
1973 self.SetSizer(vbs)
1974 self.SetAutoLayout(True)
1975
1976 self.config = parent.mainwindow.config
1977 guiwidgets.set_size("PhoneImportMergeDialog", self, screenpct=95, aspect=1.10)
1978
1979 self.MakeMenus()
1980
1981 self.sortedColumn=1
1982 self.sortedColumnDescending=False
1983
1984 wx.EVT_BUTTON(self, wx.ID_HELP, lambda _: wx.GetApp().displayhelpid(helpids.ID_DLG_PBMERGEENTRIES))
1985
1986
1987
1988 self.splitter=splitter
1989 wx.CallAfter(self._setthedamnsplittersizeinsteadofbeingsostupid_thewindowisnot20pixelswide_isetthesizenolessthan3times_argggh)
1990
1991 wx.CallAfter(self.DoMerge)
1992
1993
1995 splitter=self.splitter
1996 w,_=splitter.GetSize()
1997 splitter.SetSashPosition(max(w/2, w-200))
1998
1999
2001 w = self.grid.GetGridColLabelWindow()
2002 dc = wx.PaintDC(w)
2003 font = dc.GetFont()
2004 dc.SetTextForeground(wx.BLACK)
2005
2006
2007
2008 totColSize = -self.grid.GetViewStart()[0]*self.grid.GetScrollPixelsPerUnit()[0]
2009 for col in range(self.grid.GetNumberCols()):
2010 dc.SetBrush(wx.Brush("WHEAT", wx.TRANSPARENT))
2011 colSize = self.grid.GetColSize(col)
2012 rect = (totColSize,0,colSize,32)
2013
2014 dc.DrawRectangle(rect[0] - (col!=0), rect[1], rect[2] + (col!=0), rect[3])
2015 totColSize += colSize
2016
2017 if col == self.sortedColumn:
2018 font.SetWeight(wx.BOLD)
2019
2020
2021 left = rect[0] + 3
2022 top = rect[1] + 3
2023
2024 dc.SetBrush(wx.Brush("WHEAT", wx.SOLID))
2025 if self.sortedColumnDescending:
2026 dc.DrawPolygon([(left,top), (left+6,top), (left+3,top+4)])
2027 else:
2028 dc.DrawPolygon([(left+3,top), (left+6, top+4), (left, top+4)])
2029 else:
2030 font.SetWeight(wx.NORMAL)
2031
2032 dc.SetFont(font)
2033 dc.DrawLabel("%s" % self.grid.GetTable().GetColLabelValue(col),
2034 rect, wx.ALIGN_CENTER | wx.ALIGN_TOP)
2035
2037 col=evt.GetCol()
2038 if col==self.sortedColumn:
2039 self.sortedColumnDescending=not self.sortedColumnDescending
2040 else:
2041 self.sortedColumn=col
2042 self.sortedColumnDescending=False
2043 self.table.OnDataUpdated()
2044
2055
2056 @guihelper.BusyWrapper
2058 if len(self.existingdata)*len(self.importdata)>200:
2059 progdlg=wx.ProgressDialog("Merging entries", "BitPim is merging the new information into the existing information",
2060 len(self.existingdata), parent=self, style=wx.PD_APP_MODAL|wx.PD_CAN_ABORT|wx.PD_REMAINING_TIME)
2061 else:
2062 progdlg=None
2063 try:
2064 self._DoMerge(progdlg)
2065 finally:
2066 if progdlg:
2067 progdlg.Destroy()
2068 del progdlg
2069
2071 """Merges all the importdata with existing data
2072
2073 This can take quite a while!
2074 """
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085 count=0
2086 row={}
2087 results={}
2088
2089 em=EntryMatcher(self.existingdata, self.importdata)
2090 usedimportkeys=[]
2091 for progress,existingid in enumerate(self.existingdata.keys()):
2092 if progdlg:
2093 if not progdlg.Update(progress):
2094
2095 wx.CallAfter(self.EndModal, wx.ID_CANCEL)
2096 return
2097
2098 merged=False
2099 for confidence, importid in em.bestmatches(existingid, limit=1):
2100 if confidence>90:
2101 if importid in usedimportkeys:
2102
2103 for i in row:
2104 if row[i][1]==importid:
2105 break
2106 if confidence<row[i][0]:
2107 break
2108
2109 assert i==row[i][3]
2110 row[i]=("", None, row[i][2], row[i][3])
2111 results[i]=copy.deepcopy(self.existingdata[row[i][2]])
2112
2113 results[count]=self.MergeEntries(copy.deepcopy(self.existingdata[existingid]),
2114 copy.deepcopy(self.importdata[importid]))
2115 row[count]=(confidence, importid, existingid, count)
2116
2117 count+=1
2118 usedimportkeys.append(importid)
2119 merged=True
2120 break
2121 if not merged:
2122 results[count]=copy.deepcopy(self.existingdata[existingid])
2123 row[count]=("", None, existingid, count)
2124 count+=1
2125
2126
2127 for importid in self.importdata:
2128 if importid in usedimportkeys: continue
2129 results[count]=copy.deepcopy(self.importdata[importid])
2130 row[count]=("", importid, None, count)
2131 count+=1
2132
2133
2134 for r in row:
2135 _, importid, existingid, resid=row[r]
2136 if importid is not None and existingid is not None:
2137 checkresult=copy.deepcopy(results[resid])
2138 checkexisting=copy.deepcopy(self.existingdata[existingid])
2139
2140 if "serials" in checkresult: del checkresult["serials"]
2141 if "serials" in checkexisting: del checkexisting["serials"]
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152 if checkresult == checkexisting:
2153
2154 row[r]=("", None, existingid, resid)
2155
2156 self.rowdata=row
2157 self.resultdata=results
2158 self.table.OnDataUpdated()
2159
2161 "Take an original and a merge entry and join them together return a dict of the result"
2162 o=originalentry
2163 i=importentry
2164 result={}
2165
2166 intersect=dictintersection(o,i)
2167 for dict in i,o:
2168 for k in dict.keys():
2169 if k not in intersect:
2170 result[k]=dict[k][:]
2171
2172 for key in intersect:
2173 if key=="names":
2174
2175 r=i["names"][0]
2176 for k in o["names"][0]:
2177 r[k]=o["names"][0][k]
2178 result["names"]=[r]
2179 elif key=="numbers":
2180 result['numbers']=mergenumberlists(o['numbers'], i['numbers'])
2181 elif key=="urls":
2182 result['urls']=mergefields(o['urls'], i['urls'], 'url', cleaner=cleanurl)
2183 elif key=="emails":
2184 result['emails']=mergefields(o['emails'], i['emails'], 'email', cleaner=cleanemail)
2185 else:
2186 result[key]=common.list_union(o[key], i[key])
2187
2188 return result
2189
2191 if event is not None:
2192 event.Skip()
2193 row=self.table.GetRowData(event.GetRow())
2194 else:
2195 gcr=self.grid.GetGridCursorRow()
2196 if gcr>=0 and gcr<self.grid.GetNumberRows():
2197 row=self.table.GetRowData(gcr)
2198 else:
2199 row=None,None,None,None
2200
2201 confidence,importid,existingid,resultid=row
2202 if resultid is not None:
2203 self.resultpreview.ShowEntry(self.resultdata[resultid])
2204 else:
2205 self.resultpreview.ShowEntry({})
2206
2207
2208
2209 ID_EDIT_ITEM=wx.NewId()
2210 ID_REVERT_TO_IMPORTED=wx.NewId()
2211 ID_REVERT_TO_EXISTING=wx.NewId()
2212 ID_CLEAR_FIELD=wx.NewId()
2213 ID_IMPORTED_MISMATCH=wx.NewId()
2214
2216 menu=wx.Menu()
2217 menu.Append(self.ID_EDIT_ITEM, "Edit...")
2218 menu.Append(self.ID_REVERT_TO_EXISTING, "Revert field to existing value")
2219 menu.Append(self.ID_REVERT_TO_IMPORTED, "Revert field to imported value")
2220 menu.Append(self.ID_CLEAR_FIELD, "Clear field")
2221 menu.AppendSeparator()
2222 menu.Append(self.ID_IMPORTED_MISMATCH, "Imported entry mismatch...")
2223 self.menu=menu
2224
2225 wx.EVT_MENU(menu, self.ID_EDIT_ITEM, self.OnEditItem)
2226 wx.EVT_MENU(menu, self.ID_REVERT_TO_EXISTING, self.OnRevertFieldToExisting)
2227 wx.EVT_MENU(menu, self.ID_REVERT_TO_IMPORTED, self.OnRevertFieldToImported)
2228 wx.EVT_MENU(menu, self.ID_CLEAR_FIELD, self.OnClearField)
2229 wx.EVT_MENU(menu, self.ID_IMPORTED_MISMATCH, self.OnImportedMismatch)
2230
2232 row,col=event.GetRow(), event.GetCol()
2233 self.grid.SetGridCursor(row,col)
2234 self.grid.ClearSelection()
2235
2236 columnname=self.table.GetColLabelValue(col)
2237 _, importkey, existingkey, resultkey=self.table.GetRowData(row)
2238
2239
2240 if columnname=="Confidence":
2241 self.menu.Enable(self.ID_REVERT_TO_EXISTING, False)
2242 self.menu.Enable(self.ID_REVERT_TO_IMPORTED, False)
2243 self.menu.Enable(self.ID_CLEAR_FIELD, False)
2244 else:
2245 resultvalue=None
2246 if resultkey is not None:
2247 resultvalue=getdata(columnname, self.resultdata[resultkey], None)
2248
2249 self.menu.Enable(self.ID_REVERT_TO_EXISTING, existingkey is not None
2250 and getdata(columnname, self.existingdata[existingkey], None)!= resultvalue)
2251 self.menu.Enable(self.ID_REVERT_TO_IMPORTED, importkey is not None
2252 and getdata(columnname, self.importdata[importkey], None) != resultvalue)
2253 self.menu.Enable(self.ID_CLEAR_FIELD, True)
2254
2255 self.menu.Enable(self.ID_IMPORTED_MISMATCH, importkey is not None)
2256
2257 pos=event.GetPosition()
2258 self.grid.PopupMenu(self.menu, pos)
2259
2261 self.EditEntry(self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol())
2262
2264 row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol()
2265 columnname=self.table.GetColLabelValue(col)
2266 row=self.table.GetRowData(row)
2267 reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]])
2268 exkey,exindex=getdatainfo(columnname, self.existingdata[row[2]])
2269 if exindex is None:
2270
2271 self.OnClearField(None)
2272 return
2273 if resindex is None:
2274 self.resultdata[row[3]][reskey].append(copy.deepcopy(self.existingdata[row[2]][exkey][exindex]))
2275 elif resindex<0:
2276 self.resultdata[row[3]][reskey]=copy.deepcopy(self.existingdata[row[2]][exkey])
2277 else:
2278 self.resultdata[row[3]][reskey][resindex]=copy.deepcopy(self.existingdata[row[2]][exkey][exindex])
2279 self.table.OnDataUpdated()
2280
2282 row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol()
2283 columnname=self.table.GetColLabelValue(col)
2284 row=self.table.GetRowData(row)
2285 reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]])
2286 imkey,imindex=getdatainfo(columnname, self.importdata[row[1]])
2287 assert imindex is not None
2288 if resindex is None:
2289 self.resultdata[row[3]][reskey].append(copy.deepcopy(self.importdata[row[1]][imkey][imindex]))
2290 elif resindex<0:
2291 self.resultdata[row[3]][reskey]=copy.deepcopy(self.importdata[row[1]][imkey])
2292 else:
2293 self.resultdata[row[3]][reskey][resindex]=copy.deepcopy(self.importdata[row[1]][imkey][imindex])
2294 self.table.OnDataUpdated()
2295
2297 row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol()
2298 columnname=self.table.GetColLabelValue(col)
2299 row=self.table.GetRowData(row)
2300 reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]])
2301 assert resindex is not None
2302 if resindex<0:
2303 del self.resultdata[row[3]][reskey]
2304 else:
2305 del self.resultdata[row[3]][reskey][resindex]
2306 self.table.OnDataUpdated()
2307
2309
2310 row=self.grid.GetGridCursorRow()
2311 _,ourimportkey,existingmatchkey,resultkey=self.table.GetRowData(row)
2312 match=None
2313
2314 choices=[]
2315 for row in range(self.table.GetNumberRows()):
2316 _,_,existingkey,_=self.table.GetRowData(row)
2317 if existingkey is not None:
2318 if existingmatchkey==existingkey:
2319 match=len(choices)
2320 choices.append( (getdata("Name", self.existingdata[existingkey], "<blank>"), existingkey) )
2321
2322 with guihelper.WXDialogWrapper(ImportedEntryMatchDialog(self, choices, match),
2323 True) as (dlg, retcode):
2324 if retcode==wx.ID_OK:
2325 confidence,importkey,existingkey,resultkey=self.table.GetRowData(self.grid.GetGridCursorRow())
2326 assert importkey is not None
2327 match=dlg.GetMatch()
2328 if match is None:
2329
2330 if existingkey is None:
2331 wx.MessageBox("It is already a new entry!", wx.OK|wx.ICON_EXCLAMATION)
2332 return
2333
2334 for rowdatakey in xrange(100000):
2335 if rowdatakey not in self.rowdata:
2336 for resultdatakey in xrange(100000):
2337 if resultdatakey not in self.resultdata:
2338 self.rowdata[rowdatakey]=("", importkey, None, resultdatakey)
2339 self.resultdata[resultdatakey]=copy.deepcopy(self.importdata[importkey])
2340
2341 self.resultdata[resultkey]=copy.deepcopy(self.existingdata[existingkey])
2342 self.rowdata[self.table.rowkeys[self.grid.GetGridCursorRow()]]=("", None, existingkey, resultkey)
2343 self.table.OnDataUpdated()
2344 return
2345 assert False, "You really can't get here!"
2346
2347 ekey=choices[match][1]
2348 if ekey==existingkey:
2349 wx.MessageBox("That is already the entry matched!", wx.OK|wx.ICON_EXCLAMATION)
2350 return
2351
2352 for r in range(self.table.GetNumberRows()):
2353 if r==self.grid.GetGridCursorRow(): continue
2354 confidence,importkey,existingkey,resultkey=self.table.GetRowData(r)
2355 if existingkey==ekey:
2356 if importkey is not None:
2357 wx.MessageBox("The new match already has an imported entry matching it!", "Already matched", wx.OK|wx.ICON_EXCLAMATION, self)
2358 return
2359
2360 del self.rowdata[self.table.rowkeys[self.grid.GetGridCursorRow()]]
2361
2362 self.rowdata[self.table.rowkeys[r]]=(confidence, ourimportkey, ekey, resultkey)
2363 self.resultdata[resultkey]=self.MergeEntries(
2364 copy.deepcopy(self.existingdata[ekey]),
2365 copy.deepcopy(self.importdata[ourimportkey]))
2366 self.table.OnDataUpdated()
2367 return
2368 assert False, "Can't get here"
2369
2371 self.EditEntry(event.GetRow(), event.GetCol())
2372
2373 - def EditEntry(self, row, col=None):
2374 row=self.table.GetRowData(row)
2375 k=row[3]
2376
2377 assert k is not None
2378 data=self.resultdata[k]
2379 if col is not None:
2380 columnname=self.table.GetColLabelValue(col)
2381 if columnname=="Confidence":
2382 columnname="Name"
2383 else:
2384 columnname="Name"
2385 datakey, dataindex=getdatainfo(columnname, data)
2386 with guihelper.WXDialogWrapper(phonebookentryeditor.Editor(self, data, keytoopenon=datakey, dataindex=dataindex),
2387 True) as (dlg, retcode):
2388 if retcode==wx.ID_OK:
2389 data=dlg.GetData()
2390 self.resultdata[k]=data
2391 self.table.OnDataUpdated()
2392
2394 "The dialog shown to select how an imported entry should match"
2395
2396 - def __init__(self, parent, choices, match):
2397 wx.Dialog.__init__(self, parent, id=-1, title="Select Import Entry Match", style=wx.CAPTION|
2398 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2399
2400 self.choices=choices
2401 self.importdialog=parent
2402
2403 vbs=wx.BoxSizer(wx.VERTICAL)
2404 hbs=wx.BoxSizer(wx.HORIZONTAL)
2405 self.matchexisting=wx.RadioButton(self, wx.NewId(), "Matches an existing entry below", style=wx.RB_GROUP)
2406 self.matchnew=wx.RadioButton(self, wx.NewId(), "Is a new entry")
2407 hbs.Add(self.matchexisting, wx.NewId(), wx.ALIGN_CENTRE|wx.ALL, 5)
2408 hbs.Add(self.matchnew, wx.NewId(), wx.ALIGN_CENTRE|wx.ALL, 5)
2409 vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
2410
2411 wx.EVT_RADIOBUTTON(self, self.matchexisting.GetId(), self.OnRBClicked)
2412 wx.EVT_RADIOBUTTON(self, self.matchnew.GetId(), self.OnRBClicked)
2413
2414 splitter=wx.SplitterWindow(self, -1, style=wx.SP_3D|wx.SP_LIVE_UPDATE)
2415 self.nameslb=wx.ListBox(splitter, wx.NewId(), choices=[name for name,id in choices], style=wx.LB_SINGLE|wx.LB_NEEDED_SB)
2416 self.preview=PhoneEntryDetailsView(splitter, -1)
2417 splitter.SplitVertically(self.nameslb, self.preview)
2418 vbs.Add(splitter, 1, wx.EXPAND|wx.ALL, 5)
2419
2420 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5)
2421
2422 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
2423
2424 wx.EVT_LISTBOX(self, self.nameslb.GetId(), self.OnLbClicked)
2425 wx.EVT_LISTBOX_DCLICK(self, self.nameslb.GetId(), self.OnLbDClicked)
2426
2427
2428 if match is None:
2429 self.matchexisting.SetValue(False)
2430 self.matchnew.SetValue(True)
2431 self.nameslb.Enable(False)
2432 else:
2433 self.matchexisting.SetValue(True)
2434 self.matchnew.SetValue(False)
2435 self.nameslb.Enable(True)
2436 self.nameslb.SetSelection(match)
2437 self.preview.ShowEntry(self.importdialog.existingdata[choices[match][1]])
2438
2439 self.SetSizer(vbs)
2440 self.SetAutoLayout(True)
2441 guiwidgets.set_size("PhonebookImportEntryMatcher", self, screenpct=75, aspect=0.58)
2442
2443 wx.EVT_MENU(self, wx.ID_OK, self.SaveSize)
2444 wx.EVT_MENU(self, wx.ID_CANCEL, self.SaveSize)
2445
2446
2447 - def SaveSize(self, evt=None):
2448 if evt is not None:
2449 evt.Skip()
2450 guiwidgets.save_size("PhonebookImportEntryMatcher", self.GetRect())
2451
2452 - def OnRBClicked(self, _):
2453 self.nameslb.Enable(self.matchexisting.GetValue())
2454
2455 - def OnLbClicked(self,_=None):
2456 existingid=self.choices[self.nameslb.GetSelection()][1]
2457 self.preview.ShowEntry(self.importdialog.existingdata[existingid])
2458
2459 - def OnLbDClicked(self,_):
2460 self.OnLbClicked()
2461 self.SaveSize()
2462 self.EndModal(wx.ID_OK)
2463
2464 - def GetMatch(self):
2465 if self.matchnew.GetValue():
2466 return None
2467 return self.nameslb.GetSelection()
2468
2470 return filter(two.has_key, one.keys())
2471
2473 "Implements matching phonebook entries"
2474
2475 - def __init__(self, sources, against):
2476 self.sources=sources
2477 self.against=against
2478
2479 - def bestmatches(self, sourceid, limit=5):
2480 """Gives best matches out of against list
2481
2482 @return: list of tuples of (percent match, againstid)
2483 """
2484
2485 res=[]
2486
2487 source=self.sources[sourceid]
2488 for i in self.against:
2489 against=self.against[i]
2490
2491
2492 intersect=dictintersection(source,against)
2493
2494
2495 score=0
2496 count=0
2497 for key in intersect:
2498 s=source[key]
2499 a=against[key]
2500 count+=1
2501 if s==a:
2502 score+=40*len(s)
2503 continue
2504
2505 if key=="names":
2506 score+=comparenames(s,a)
2507 elif key=="numbers":
2508 score+=comparenumbers(s,a)
2509 elif key=="urls":
2510 score+=comparefields(s,a,"url")
2511 elif key=="emails":
2512 score+=comparefields(s,a,"email")
2513 elif key=="addresses":
2514 score+=compareallfields(s,a, ("company", "street", "street2", "city", "state", "postalcode", "country"))
2515 else:
2516
2517 count-=1
2518
2519 if count:
2520 res.append( ( int(score*100/count), i ) )
2521
2522 res.sort()
2523 res.reverse()
2524 if len(res)>limit:
2525 return res[:limit]
2526 return res
2527
2531
2533 """Returns lowercase url with the "http://" prefix removed and in lower case
2534
2535 @param mode: If the value is compare (default), it removes ""http://www.""
2536 in preparation for comparing entries. Otherwise, if the value
2537 is pb, the result is formatted for writing to the phonebook.
2538 """
2539 if mode == "compare":
2540 urlprefix=re.compile("^(http://)?(www.)?")
2541 else: urlprefix=re.compile("^(http://)?")
2542
2543 return default_cleaner(re.sub(urlprefix, "", url).lower())
2544
2546 """Returns lowercase email
2547 """
2548 return default_cleaner(email.lower())
2549
2550
2551 nondigits=re.compile("[^0-9]")
2553 "Returns num (a phone number) with all non-digits removed"
2554 return re.sub(nondigits, "", num)
2555
2557 """Give a score on two phone numbers
2558
2559 """
2560
2561 ss=[cleannumber(x['number']) for x in s]
2562 aa=[cleannumber(x['number']) for x in a]
2563
2564 candidates=[]
2565 for snum in ss:
2566 for anum in aa:
2567 candidates.append( (jarowinkler(snum, anum), snum, anum) )
2568
2569 candidates.sort()
2570 candidates.reverse()
2571
2572 if len(candidates)>3:
2573 candidates=candidates[:3]
2574
2575 score=0
2576
2577 for ratio,snum,anum in candidates:
2578 if ratio>0.9:
2579 score+=(ratio-0.9)*10
2580
2581 return score
2582
2584 """Compares the valuekey field in source and against lists returning a score for closeness of match"""
2585 ss=[x[valuekey] for x in s if x.has_key(valuekey)]
2586 aa=[x[valuekey] for x in a if x.has_key(valuekey)]
2587
2588 candidates=[]
2589 for sval in ss:
2590 for aval in aa:
2591 candidates.append( (jarowinkler(sval, aval), sval, aval) )
2592
2593 candidates.sort()
2594 candidates.reverse()
2595
2596 if len(candidates)>lookat:
2597 candidates=candidates[:lookat]
2598
2599 score=0
2600
2601 for ratio,sval,aval in candidates:
2602 if ratio>threshold:
2603 score+=(ratio-threshold)*10/(1-threshold)
2604
2605 return score
2606
2608 """Like comparefields, but for source and against lists where multiple keys have values in each item
2609
2610 @param fields: This should be a list of keys from the entries that are in the order the human
2611 would write them down."""
2612
2613
2614
2615
2616 args=[]
2617 for d in s,a:
2618 str=""
2619 list=[]
2620 for entry in d:
2621 for f in fields:
2622
2623 if entry.has_key(f):
2624 str+=entry.get(f)+" "
2625 list.append( {'value': str} )
2626 args.append( list )
2627
2628 args.extend( ['value', threshold, lookat] )
2629 return comparefields(*args)
2630
2632 """Return the results of merging two lists of numbers
2633
2634 We compare the sanitised numbers (ie after punctuation etc is stripped
2635 out). If they are the same, then the original is kept (since the number
2636 is the same, and the original most likely has the correct punctuation).
2637
2638 Otherwise the imported entries overwrite the originals
2639 """
2640
2641 res=[]
2642 res.extend(orig)
2643
2644 for i in imp:
2645 num=cleannumber(i['number'])
2646 found=False
2647 for r in res:
2648 if num==cleannumber(r['number']):
2649
2650 found=True
2651 if i.has_key('speeddial'):
2652 r['speeddial']=i['speeddial']
2653 break
2654 if found:
2655 continue
2656
2657
2658 found=False
2659 for r in res:
2660 if i['type']==r['type']:
2661 r['number']=i['number']
2662 if i.has_key('speeddial'):
2663 r['speeddial']=i['speeddial']
2664 found=True
2665 break
2666 if found:
2667 continue
2668
2669 res.append(i)
2670
2671 return res
2672
2673
2674 default_cleaner=lambda x: x
2677 """Return the results of merging two lists of fields
2678
2679 We compare the fields. If they are the same, then the original is kept
2680 (since the name is the same, and the original most likely has the
2681 correct punctuation).
2682
2683 Otherwise the imported entries overwrite the originals
2684 """
2685
2686 res=[]
2687 res.extend(orig)
2688
2689 for i in imp:
2690
2691 impfield=cleaner(i[field])
2692
2693 found=False
2694 for r in res:
2695
2696
2697
2698
2699
2700
2701 resfield=cleaner(r[field])
2702
2703 if (comparestrings(resfield, impfield) > threshold):
2704
2705 found=True
2706
2707
2708
2709
2710
2711 if i.has_key('type'):
2712 r['type'] = i['type']
2713
2714
2715 break
2716
2717
2718 if found:
2719 continue
2720
2721
2722
2723 found=False
2724 for r in res:
2725 if (i.has_key('type') and r.has_key('type')):
2726 if i['type']==r['type']:
2727
2728 r[field]=cleaner(i[field], "pb")
2729 found=True
2730 break
2731 if found:
2732 continue
2733
2734
2735 i[field] = cleaner(i[field], "pb")
2736 res.append(i)
2737
2738 return res
2739
2740 import native.strings
2741 jarowinkler=native.strings.jarow
2744 """ Compares two strings and returns the score using
2745 winkler routine from Febrl (stringcmp.py)
2746
2747 Return value is between 0.0 and 1.0, where 0.0 means no similarity
2748 whatsoever, and 1.0 means the strings match exactly."""
2749 return jarowinkler(origfield, impfield, 16)
2750
2756
2758 "The dialog for selecting what columns you want to view"
2759
2760 ID_SHOW=wx.NewId()
2761 ID_AVAILABLE=wx.NewId()
2762 ID_UP=wx.NewId()
2763 ID_DOWN=wx.NewId()
2764 ID_ADD=wx.NewId()
2765 ID_REMOVE=wx.NewId()
2766 ID_DEFAULT=wx.NewId()
2767
2768 - def __init__(self, parent, config, phonewidget):
2769 wx.Dialog.__init__(self, parent, id=-1, title="Select Columns to view", style=wx.CAPTION|
2770 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2771
2772 self.config=config
2773 self.phonewidget=phonewidget
2774 hbs=wx.BoxSizer(wx.HORIZONTAL)
2775
2776
2777 bs=wx.BoxSizer(wx.VERTICAL)
2778 bs.Add(wx.StaticText(self, -1, "Showing"), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
2779 self.show=wx.ListBox(self, self.ID_SHOW, style=wx.LB_SINGLE|wx.LB_NEEDED_SB, size=(250, 300))
2780 bs.Add(self.show, 1, wx.EXPAND|wx.ALL, 5)
2781 hbs.Add(bs, 1, wx.EXPAND|wx.ALL, 5)
2782
2783
2784 bs=wx.BoxSizer(wx.VERTICAL)
2785 self.up=wx.Button(self, self.ID_UP, "Move Up")
2786 self.down=wx.Button(self, self.ID_DOWN, "Move Down")
2787 self.add=wx.Button(self, self.ID_ADD, "Show")
2788 self.remove=wx.Button(self, self.ID_REMOVE, "Don't Show")
2789 self.default=wx.Button(self, self.ID_DEFAULT, "Default")
2790
2791 for b in self.up, self.down, self.add, self.remove, self.default:
2792 bs.Add(b, 0, wx.ALL|wx.ALIGN_CENTRE, 10)
2793
2794 hbs.Add(bs, 0, wx.ALL|wx.ALIGN_CENTRE, 5)
2795
2796
2797 bs=wx.BoxSizer(wx.VERTICAL)
2798 bs.Add(wx.StaticText(self, -1, "Available"), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
2799 self.available=wx.ListBox(self, self.ID_AVAILABLE, style=wx.LB_EXTENDED|wx.LB_NEEDED_SB, choices=AvailableColumns)
2800 bs.Add(self.available, 1, wx.EXPAND|wx.ALL, 5)
2801 hbs.Add(bs, 1, wx.EXPAND|wx.ALL, 5)
2802
2803
2804 vbs=wx.BoxSizer(wx.VERTICAL)
2805 vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 5)
2806 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5)
2807 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
2808
2809 self.SetSizer(vbs)
2810 vbs.Fit(self)
2811
2812
2813 cur=self.config.Read("phonebookcolumns", "")
2814 if len(cur):
2815 cur=cur.split(",")
2816
2817 cur=[c for c in cur if c in AvailableColumns]
2818 else:
2819 cur=DefaultColumns
2820 self.show.Set(cur)
2821
2822
2823 self.up.Disable()
2824 self.down.Disable()
2825 self.add.Disable()
2826 self.remove.Disable()
2827
2828 wx.EVT_LISTBOX(self, self.ID_SHOW, self.OnShowClicked)
2829 wx.EVT_LISTBOX_DCLICK(self, self.ID_SHOW, self.OnShowClicked)
2830 wx.EVT_LISTBOX(self, self.ID_AVAILABLE, self.OnAvailableClicked)
2831 wx.EVT_LISTBOX_DCLICK(self, self.ID_AVAILABLE, self.OnAvailableDClicked)
2832
2833 wx.EVT_BUTTON(self, self.ID_ADD, self.OnAdd)
2834 wx.EVT_BUTTON(self, self.ID_REMOVE, self.OnRemove)
2835 wx.EVT_BUTTON(self, self.ID_UP, self.OnUp)
2836 wx.EVT_BUTTON(self, self.ID_DOWN, self.OnDown)
2837 wx.EVT_BUTTON(self, self.ID_DEFAULT, self.OnDefault)
2838 wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)
2839
2845
2848
2851
2852 - def OnAdd(self, _=None):
2869
2880
2885
2886 - def OnUp(self, _):
2893
2901
2902 - def OnOk(self, event):
2903 cur=[self.show.GetString(i) for i in range(self.show.GetCount())]
2904 self.config.Write("phonebookcolumns", ",".join(cur))
2905 self.config.Flush()
2906 self.phonewidget.SetColumns(cur)
2907 event.Skip()
2908
2910
2911 ID_SELECTED=wx.NewId()
2912 ID_ALL=wx.NewId()
2913 ID_LAYOUT=wx.NewId()
2914 ID_STYLES=wx.NewId()
2915 ID_PRINT=wx.NewId()
2916 ID_PAGESETUP=wx.NewId()
2917 ID_PRINTPREVIEW=wx.NewId()
2918 ID_CLOSE=wx.ID_CANCEL
2919 ID_HELP=wx.NewId()
2920 ID_TEXTSCALE=wx.NewId()
2921 ID_SAVEASHTML=wx.NewId()
2922
2923 textscales=[ (0.4, "Teeny"), (0.6, "Tiny"), (0.8, "Small"), (1.0, "Normal"), (1.2, "Large"), (1.4, "Ginormous") ]
2924
2925 textscales.reverse()
2926
2927 - def __init__(self, phonewidget, mainwindow, config):
2928 wx.Dialog.__init__(self, mainwindow, id=-1, title="Print PhoneBook", style=wx.CAPTION|
2929 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2930
2931 self.config=config
2932 self.phonewidget=phonewidget
2933
2934
2935
2936 self.layoutfiles={}
2937 for _file in guihelper.getresourcefiles("pbpl-*.xy"):
2938 with file(_file, 'rt') as f:
2939 desc=f.readline().strip()
2940 self.layoutfiles[desc]=f.read()
2941 self.stylefiles={}
2942 for _file in guihelper.getresourcefiles("pbps-*.xy"):
2943 with file(_file, 'rt') as f:
2944 desc=f.readline().strip()
2945 self.stylefiles[desc]=f.read()
2946
2947
2948 vbs=wx.BoxSizer(wx.VERTICAL)
2949
2950 hbs=wx.BoxSizer(wx.HORIZONTAL)
2951
2952 numselected=len(phonewidget.GetSelectedRows())
2953 numtotal=len(phonewidget._data)
2954
2955
2956 vbs2=wx.BoxSizer(wx.VERTICAL)
2957 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Rows"), wx.VERTICAL)
2958 self.selected=wx.RadioButton(self, self.ID_SELECTED, "Selected (%d)" % (numselected,), style=wx.RB_GROUP)
2959 self.all=wx.RadioButton(self, self.ID_SELECTED, "All (%d)" % (numtotal,) )
2960 bs.Add(self.selected, 0, wx.EXPAND|wx.ALL, 2)
2961 bs.Add(self.all, 0, wx.EXPAND|wx.ALL, 2)
2962 self.selected.SetValue(numselected>1)
2963 self.all.SetValue(not (numselected>1))
2964 vbs2.Add(bs, 0, wx.EXPAND|wx.ALL, 2)
2965
2966 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Text Scale"), wx.HORIZONTAL)
2967 for i in range(len(self.textscales)):
2968 if self.textscales[i][0]==1.0:
2969 sv=i
2970 break
2971 self.textscaleslider=wx.Slider(self, self.ID_TEXTSCALE, sv, 0, len(self.textscales)-1, style=wx.SL_VERTICAL|wx.SL_AUTOTICKS)
2972 self.scale=1
2973 bs.Add(self.textscaleslider, 0, wx.EXPAND|wx.ALL, 2)
2974 self.textscalelabel=wx.StaticText(self, -1, "Normal")
2975 bs.Add(self.textscalelabel, 0, wx.ALIGN_CENTRE)
2976 vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2)
2977 hbs.Add(vbs2, 0, wx.EXPAND|wx.ALL, 2)
2978
2979
2980 self.sortkeyscb=[]
2981 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Sorting"), wx.VERTICAL)
2982 choices=["<None>"]+AvailableColumns
2983 for i in range(3):
2984 bs.Add(wx.StaticText(self, -1, ("Sort by", "Then")[i!=0]), 0, wx.EXPAND|wx.ALL, 2)
2985 self.sortkeyscb.append(wx.ComboBox(self, wx.NewId(), "<None>", choices=choices, style=wx.CB_READONLY))
2986 self.sortkeyscb[-1].SetSelection(0)
2987 bs.Add(self.sortkeyscb[-1], 0, wx.EXPAND|wx.ALL, 2)
2988 hbs.Add(bs, 0, wx.EXPAND|wx.ALL, 4)
2989
2990
2991 vbs2=wx.BoxSizer(wx.VERTICAL)
2992 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Layout"), wx.VERTICAL)
2993 k=self.layoutfiles.keys()
2994 k.sort()
2995 self.layout=wx.ListBox(self, self.ID_LAYOUT, style=wx.LB_SINGLE|wx.LB_NEEDED_SB|wx.LB_HSCROLL, choices=k, size=(150,-1))
2996 self.layout.SetSelection(0)
2997 bs.Add(self.layout, 1, wx.EXPAND|wx.ALL, 2)
2998 vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2)
2999 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Styles"), wx.VERTICAL)
3000 k=self.stylefiles.keys()
3001 self.styles=wx.CheckListBox(self, self.ID_STYLES, choices=k)
3002 bs.Add(self.styles, 1, wx.EXPAND|wx.ALL, 2)
3003 vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2)
3004 hbs.Add(vbs2, 1, wx.EXPAND|wx.ALL, 2)
3005
3006
3007 vbs2=wx.BoxSizer(wx.VERTICAL)
3008 vbs2.Add(wx.Button(self, self.ID_PRINT, "Print"), 0, wx.EXPAND|wx.ALL, 2)
3009 vbs2.Add(wx.Button(self, self.ID_PAGESETUP, "Page Setup..."), 0, wx.EXPAND|wx.ALL, 2)
3010 vbs2.Add(wx.Button(self, self.ID_PRINTPREVIEW, "Print Preview"), 0, wx.EXPAND|wx.ALL, 2)
3011 vbs2.Add(wx.Button(self, self.ID_SAVEASHTML, "Save as HTML"), 0, wx.EXPAND|wx.ALL, 2)
3012 vbs2.Add(wx.Button(self, self.ID_CLOSE, "Close"), 0, wx.EXPAND|wx.ALL, 2)
3013 hbs.Add(vbs2, 0, wx.EXPAND|wx.ALL, 2)
3014
3015
3016 vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 2)
3017
3018
3019 bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Content Preview"), wx.VERTICAL)
3020 self.preview=bphtml.HTMLWindow(self, -1)
3021 bs.Add(self.preview, 1, wx.EXPAND|wx.ALL, 2)
3022
3023
3024 vbs.Add(bs, 2, wx.EXPAND|wx.ALL, 2)
3025
3026 self.SetSizer(vbs)
3027 vbs.Fit(self)
3028
3029
3030 wx.EVT_BUTTON(self, self.ID_PRINTPREVIEW, self.OnPrintPreview)
3031 wx.EVT_BUTTON(self, self.ID_PRINT, self.OnPrint)
3032 wx.EVT_BUTTON(self, self.ID_PAGESETUP, self.OnPageSetup)
3033 wx.EVT_BUTTON(self, self.ID_SAVEASHTML, self.OnSaveHTML)
3034 wx.EVT_RADIOBUTTON(self, self.selected.GetId(), self.UpdateHtml)
3035 wx.EVT_RADIOBUTTON(self, self.all.GetId(), self.UpdateHtml)
3036 for i in self.sortkeyscb:
3037 wx.EVT_COMBOBOX(self, i.GetId(), self.UpdateHtml)
3038 wx.EVT_LISTBOX(self, self.layout.GetId(), self.UpdateHtml)
3039 wx.EVT_CHECKLISTBOX(self, self.styles.GetId(), self.UpdateHtml)
3040 wx.EVT_COMMAND_SCROLL(self, self.textscaleslider.GetId(), self.UpdateSlider)
3041 self.UpdateHtml()
3042
3049
3052
3056
3057 @guihelper.BusyWrapper
3059
3060 vars={'phonebook': __import__(__name__) }
3061
3062 if self.all.GetValue():
3063 rowkeys=self.phonewidget._data.keys()
3064 else:
3065 rowkeys=self.phonewidget.GetSelectedRowKeys()
3066
3067
3068 for keycb in (-1, -2, -3):
3069 sortkey=self.sortkeyscb[keycb].GetValue()
3070 if sortkey=="<None>": continue
3071
3072 l=[(getdata(sortkey, self.phonewidget._data[key]), key) for key in rowkeys]
3073 l.sort()
3074
3075 rowkeys=[key for val,key in l]
3076
3077 vars['rowkeys']=rowkeys
3078 vars['currentcolumns']=self.phonewidget.GetColumns()
3079 vars['data']=self.phonewidget._data
3080
3081 xcp=xyaptu.xcopier(None)
3082 xcp.setupxcopy(self.layoutfiles[self.layout.GetStringSelection()])
3083 html=xcp.xcopywithdns(vars)
3084
3085 sd={'styles': {}, '__builtins__': __builtins__ }
3086 for i in range(self.styles.GetCount()):
3087 if self.styles.IsChecked(i):
3088 exec self.stylefiles[self.styles.GetString(i)] in sd,sd
3089 try:
3090 html=bphtml.applyhtmlstyles(html, sd['styles'])
3091 except:
3092 if __debug__:
3093 with file("debug.html", "wt") as f:
3094 f.write(html)
3095 raise
3096 return html
3097
3099 wx.GetApp().htmlprinter.PreviewText(self.html, scale=self.scale)
3100
3102 wx.GetApp().htmlprinter.PrintText(self.html, scale=self.scale)
3103
3106
3107 - def OnPageSetup(self, _):
3108 wx.GetApp().htmlprinter.PageSetup()
3109
3111 with guihelper.WXDialogWrapper(wx.FileDialog(self, wildcard="Web Page (*.htm;*.html)|*.htm;*html",
3112 style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT),
3113 True) as (_dlg, _retcode):
3114 if _retcode==wx.ID_OK:
3115 file(_dlg.GetPath(), 'wt').write(self.html)
3116
3119