Discussion:
unknown
1970-01-01 00:00:00 UTC
Permalink
# 11 - imported is None and existing is present
ImportDataTable.htmltemplate[11]='<font color="#aa0000">%(existing)s</font>'
# 12 - imported is present and existing is None
ImportDataTable.htmltemplate[12]='<font color="#aa0000"><font size=-1>%(imported)s</font></font>' # slightly smaller
# 13 - imported != existing
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>'
# 14 - imported equals existing
ImportDataTable.htmltemplate[14]='<font color="#aa0000">%(existing)s</font>'


class ImportDialog(wx.Dialog):
"The dialog for mixing new (imported) data with existing data"


def __init__(self, parent, existingdata, importdata):
wx.Dialog.__init__(self, parent, id=-1, title="Import Phonebook data", style=wx.CAPTION|
wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)

# the data already in the phonebook
self.existingdata=existingdata
# the data we are importing
self.importdata=importdata
# the resulting data
self.resultdata={}
# each row to display showing what happened, with ids pointing into above data
# rowdata[0]=confidence
# rowdata[1]=importdatakey
# rowdata[2]=existingdatakey
# rowdata[3]=resultdatakey
self.rowdata={}

vbs=wx.BoxSizer(wx.VERTICAL)

bg=self.GetBackgroundColour()
w=wx.html.HtmlWindow(self, -1, size=wx.Size(600,50), style=wx.html.HW_SCROLLBAR_NEVER)
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()))
vbs.Add(w, 0, wx.EXPAND|wx.ALL, 5)

hbs=wx.BoxSizer(wx.HORIZONTAL)
hbs.Add(wx.StaticText(self, -1, "Show entries"), 0, wx.EXPAND|wx.ALL,3)

self.cbunaltered=wx.CheckBox(self, wx.NewId(), "Unaltered")
self.cbadded=wx.CheckBox(self, wx.NewId(), "Added")
self.cbchanged=wx.CheckBox(self, wx.NewId(), "Merged")
self.cbdeleted=wx.CheckBox(self, wx.NewId(), "Deleted")
wx.EVT_CHECKBOX(self, self.cbunaltered.GetId(), self.OnCheckbox)
wx.EVT_CHECKBOX(self, self.cbadded.GetId(), self.OnCheckbox)
wx.EVT_CHECKBOX(self, self.cbchanged.GetId(), self.OnCheckbox)
wx.EVT_CHECKBOX(self, self.cbdeleted.GetId(), self.OnCheckbox)

for i in self.cbunaltered, self.cbadded, self.cbchanged, self.cbdeleted:
i.SetValue(True)
hbs.Add(i, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.RIGHT, 7)

t=ImportDataTable
self.show=[t.ADDED, t.UNALTERED, t.CHANGED, t.DELETED]

hbs.Add(wx.StaticText(self, -1, " "), 0, wx.EXPAND|wx.LEFT, 10)

vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)

splitter=wx.SplitterWindow(self,-1, style=wx.SP_3D|wx.SP_LIVE_UPDATE)
splitter.SetMinimumPaneSize(20)

self.grid=wx.grid.Grid(splitter, wx.NewId())
self.table=ImportDataTable(self)

# this is a work around for various wxPython/wxWidgets bugs
cr=ImportCellRenderer(self.table, self.grid)
cr.IncRef() # wxPython bug
self.grid.RegisterDataType("string", cr, None) # wxWidgets bug - it uses the string renderer rather than DefaultCellRenderer

self.grid.SetTable(self.table, False, wx.grid.Grid.wxGridSelectRows)
self.grid.SetSelectionMode(wx.grid.Grid.wxGridSelectRows)
self.grid.SetRowLabelSize(0)
self.grid.EnableDragRowSize(True)
self.grid.EnableEditing(False)
self.grid.SetMargins(1,0)
self.grid.EnableGridLines(False)

wx.grid.EVT_GRID_CELL_RIGHT_CLICK(self.grid, self.OnRightGridClick)
wx.grid.EVT_GRID_SELECT_CELL(self.grid, self.OnCellSelect)
wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self.grid, self.OnCellDClick)
wx.EVT_PAINT(self.grid.GetGridColLabelWindow(), self.OnColumnHeaderPaint)
wx.grid.EVT_GRID_LABEL_LEFT_CLICK(self.grid, self.OnGridLabelLeftClick)
wx.grid.EVT_GRID_LABEL_LEFT_DCLICK(self.grid, self.OnGridLabelLeftClick)

self.resultpreview=PhoneEntryDetailsView(splitter, -1, "styles.xy", "pblayout.xy")

splitter.SplitVertically(self.grid, self.resultpreview)

vbs.Add(splitter, 1, wx.EXPAND|wx.ALL,5)
vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5)

vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5)

self.SetSizer(vbs)
self.SetAutoLayout(True)

self.config = parent.mainwindow.config
guiwidgets.set_size("PhoneImportMergeDialog", self, screenpct=95, aspect=1.10)

self.MakeMenus()

self.sortedColumn=1
self.sortedColumnDescending=False

wx.EVT_BUTTON(self, wx.ID_HELP, lambda _: wx.GetApp().displayhelpid(helpids.ID_DLG_PBMERGEENTRIES))

# the splitter which adamantly insists it is 20 pixels wide no
# matter how hard you try to convince it otherwise. so we force it
self.splitter=splitter
wx.CallAfter(self._setthedamnsplittersizeinsteadofbeingsostupid_thewindowisnot20pixelswide_isetthesizenolessthan3times_argggh)

wx.CallAfter(self.DoMerge)


def _setthedamnsplittersizeinsteadofbeingsostupid_thewindowisnot20pixelswide_isetthesizenolessthan3times_argggh(self):
splitter=self.splitter
w,_=splitter.GetSize()
splitter.SetSashPosition(max(w/2, w-200))

# ::TODO:: this method and the copy earlier should be merged into a single mixin
def OnColumnHeaderPaint(self, evt):
w = self.grid.GetGridColLabelWindow()
dc = wx.PaintDC(w)
font = dc.GetFont()
dc.SetTextForeground(wx.BLACK)

# For each column, draw it's rectangle, it's column name,
# and it's sort indicator, if appropriate:
totColSize = -self.grid.GetViewStart()[0]*self.grid.GetScrollPixelsPerUnit()[0]
for col in range(self.grid.GetNumberCols()):
dc.SetBrush(wx.Brush("WHEAT", wx.TRANSPARENT))
colSize = self.grid.GetColSize(col)
rect = (totColSize,0,colSize,32)
# note abuse of bool to be integer 0/1
dc.DrawRectangle(rect[0] - (col!=0), rect[1], rect[2] + (col!=0), rect[3])
totColSize += colSize

if col == self.sortedColumn:
font.SetWeight(wx.BOLD)
# draw a triangle, pointed up or down, at the
# top left of the column.
left = rect[0] + 3
top = rect[1] + 3

dc.SetBrush(wx.Brush("WHEAT", wx.SOLID))
if self.sortedColumnDescending:
dc.DrawPolygon([(left,top), (left+6,top), (left+3,top+4)])
else:
dc.DrawPolygon([(left+3,top), (left+6, top+4), (left, top+4)])
else:
font.SetWeight(wx.NORMAL)

dc.SetFont(font)
dc.DrawLabel("%s" % self.grid.GetTable().GetColLabelValue(col),
rect, wx.ALIGN_CENTER | wx.ALIGN_TOP)

def OnGridLabelLeftClick(self, evt):
col=evt.GetCol()
if col==self.sortedColumn:
self.sortedColumnDescending=not self.sortedColumnDescending
else:
self.sortedColumn=col
self.sortedColumnDescending=False
self.table.OnDataUpdated()

def OnCheckbox(self, _):
t=ImportDataTable
vclist=((t.ADDED, self.cbadded), (t.UNALTERED, self.cbunaltered),
(t.CHANGED, self.cbchanged), (t.DELETED, self.cbdeleted))
self.show=[v for v,c in vclist if c.GetValue()]
if len(self.show)==0:
for v,c in vclist:
self.show.append(v)
c.SetValue(True)
self.table.OnDataUpdated()

def DoMerge(self):
if len(self.existingdata)*len(self.importdata)>200:
progdlg=wx.ProgressDialog("Merging entries", "BitPim is merging the new information into the existing information",
len(self.existingdata), parent=self, style=wx.PD_APP_MODAL|wx.PD_CAN_ABORT|wx.PD_REMAINING_TIME)
else:
progdlg=None
try:
self._DoMerge(progdlg)
finally:
if progdlg:
progdlg.Destroy()
del progdlg

DoMerge=guihelper.BusyWrapper(DoMerge)

def _DoMerge(self, progdlg):
"""Merges all the importdata with existing data

This can take quite a while!
"""

# We go to great lengths to ensure that a copy of the import
# and existing data is passed on to the routines we call and
# data structures being built. Originally the code expected
# the called routines to make copies of the data they were
# copying/modifying, but it proved too error prone and often
# ended up modifying the original/import data passed in. That
# had the terrible side effect of meaning that your original
# data got modified even if you pressed cancel!

count=0
row={}
results={}

em=EntryMatcher(self.existingdata, self.importdata)
usedimportkeys=[]
for progress,existingid in enumerate(self.existingdata.keys()):
if progdlg:
if not progdlg.Update(progress):
# user cancelled
wx.CallAfter(self.EndModal, wx.ID_CANCEL)
return
# does it match any imported entry
merged=False
for confidence, importid in em.bestmatches(existingid, limit=1):
if confidence>90:
if importid in usedimportkeys:
# someone else already used this import, lets find out who was the better match
for i in row:
if row[i][1]==importid:
break
if confidence<row[i][0]:
break # they beat us so this existing passed on an importmatch
# we beat the other existing - undo their merge
assert i==row[i][3]
row[i]=("", None, row[i][2], row[i][3])
results[i]=copy.deepcopy(self.existingdata[row[i][2]])

results[count]=self.MergeEntries(copy.deepcopy(self.existingdata[existingid]),
copy.deepcopy(self.importdata[importid]))
row[count]=(confidence, importid, existingid, count)
# update counters etc
count+=1
usedimportkeys.append(importid)
merged=True
break # we are happy with this match
if not merged:
results[count]=copy.deepcopy(self.existingdata[existingid])
row[count]=("", None, existingid, count)
count+=1

# which imports went unused?
for importid in self.importdata:
if importid in usedimportkeys: continue
results[count]=copy.deepcopy(self.importdata[importid])
row[count]=("", importid, None, count)
count+=1

# scan thru the merged ones, and see if anything actually changed
for r in row:
_, importid, existingid, resid=row[r]
if importid is not None and existingid is not None:
checkresult=copy.deepcopy(results[resid])
checkexisting=copy.deepcopy(self.existingdata[existingid])
# we don't care about serials changing ...
if "serials" in checkresult: del checkresult["serials"]
if "serials" in checkexisting: del checkexisting["serials"]

# another sort of false positive is if the name field
# has "full" defined in existing and "first", "last"
# in import, and the result ends up with "full",
# "first" and "last" which looks different than
# existing so it shows up us a change in the UI
# despite the fact that it hasn't really changed if
# full and first/last are consistent with each other.
# Currently we just ignore this situation.

if checkresult == checkexisting:
# lets pretend there was no import
row[r]=("", None, existingid, resid)

self.rowdata=row
self.resultdata=results
self.table.OnDataUpdated()

def MergeEntries(self, originalentry, importentry):
"Take an original and a merge entry and join them together return a dict of the result"
o=originalentry
i=importentry
result={}
# Get the intersection. Anything not in this is not controversial
intersect=dictintersection(o,i)
for dict in i,o:
for k in dict.keys():
if k not in intersect:
result[k]=dict[k][:]
# now only deal with keys in both
for key in intersect:
if key=="names":
# we ignore anything except the first name. fields in existing take precedence
r=i["names"][0]
for k in o["names"][0]:
r[k]=o["names"][0][k]
result["names"]=[r]
elif key=="numbers":
result['numbers']=mergenumberlists(o['numbers'], i['numbers'])
elif key=="urls":
result['urls']=mergefields(o['urls'], i['urls'], 'url', cleaner=cleanurl)
elif key=="emails":
result['emails']=mergefields(o['emails'], i['emails'], 'email', cleaner=cleanemail)
else:
result[key]=common.list_union(o[key], i[key])

return result

def OnCellSelect(self, event=None):
if event is not None:
event.Skip()
row=self.table.GetRowData(event.GetRow())
else:
gcr=self.grid.GetGridCursorRow()
if gcr>=0:
row=self.table.GetRowData(gcr)
else: # table is empty
row=None,None,None,None

confidence,importid,existingid,resultid=row
if resultid is not None:
self.resultpreview.ShowEntry(self.resultdata[resultid])
else:
self.resultpreview.ShowEntry({})

# menu and right click handling

ID_EDIT_ITEM=wx.NewId()
ID_REVERT_TO_IMPORTED=wx.NewId()
ID_REVERT_TO_EXISTING=wx.NewId()
ID_CLEAR_FIELD=wx.NewId()
ID_IMPORTED_MISMATCH=wx.NewId()

def MakeMenus(self):
menu=wx.Menu()
menu.Append(self.ID_EDIT_ITEM, "Edit...")
menu.Append(self.ID_REVERT_TO_EXISTING, "Revert field to existing value")
menu.Append(self.ID_REVERT_TO_IMPORTED, "Revert field to imported value")
menu.Append(self.ID_CLEAR_FIELD, "Clear field")
menu.AppendSeparator()
menu.Append(self.ID_IMPORTED_MISMATCH, "Imported entry mismatch...")
self.menu=menu

wx.EVT_MENU(menu, self.ID_EDIT_ITEM, self.OnEditItem)
wx.EVT_MENU(menu, self.ID_REVERT_TO_EXISTING, self.OnRevertFieldToExisting)
wx.EVT_MENU(menu, self.ID_REVERT_TO_IMPORTED, self.OnRevertFieldToImported)
wx.EVT_MENU(menu, self.ID_CLEAR_FIELD, self.OnClearField)
wx.EVT_MENU(menu, self.ID_IMPORTED_MISMATCH, self.OnImportedMismatch)

def OnRightGridClick(self, event):
row,col=event.GetRow(), event.GetCol()
self.grid.SetGridCursor(row,col)
self.grid.ClearSelection()
# enable/disable stuff in the menu
columnname=self.table.GetColLabelValue(col)
_, importkey, existingkey, resultkey=self.table.GetRowData(row)


if columnname=="Confidence":
self.menu.Enable(self.ID_REVERT_TO_EXISTING, False)
self.menu.Enable(self.ID_REVERT_TO_IMPORTED, False)
self.menu.Enable(self.ID_CLEAR_FIELD, False)
else:
resultvalue=None
if resultkey is not None:
resultvalue=getdata(columnname, self.resultdata[resultkey], None)

self.menu.Enable(self.ID_REVERT_TO_EXISTING, existingkey is not None
and getdata(columnname, self.existingdata[existingkey], None)!= resultvalue)
self.menu.Enable(self.ID_REVERT_TO_IMPORTED, importkey is not None
and getdata(columnname, self.importdata[importkey], None) != resultvalue)
self.menu.Enable(self.ID_CLEAR_FIELD, True)

self.menu.Enable(self.ID_IMPORTED_MISMATCH, importkey is not None)
# pop it up
pos=event.GetPosition()
self.grid.PopupMenu(self.menu, pos)

def OnEditItem(self,_):
self.EditEntry(self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol())

def OnRevertFieldToExisting(self, _):
row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol()
columnname=self.table.GetColLabelValue(col)
row=self.table.GetRowData(row)
reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]])
exkey,exindex=getdatainfo(columnname, self.existingdata[row[2]])
if exindex is None:
# actually need to clear the field
self.OnClearField(None)
return
if resindex is None:
self.resultdata[row[3]][reskey].append(copy.deepcopy(self.existingdata[row[2]][exkey][exindex]))
elif resindex<0:
self.resultdata[row[3]][reskey]=copy.deepcopy(self.existingdata[row[2]][exkey])
else:
self.resultdata[row[3]][reskey][resindex]=copy.deepcopy(self.existingdata[row[2]][exkey][exindex])
self.table.OnDataUpdated()

def OnRevertFieldToImported(self, _):
row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol()
columnname=self.table.GetColLabelValue(col)
row=self.table.GetRowData(row)
reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]])
imkey,imindex=getdatainfo(columnname, self.importdata[row[1]])
assert imindex is not None
if resindex is None:
self.resultdata[row[3]][reskey].append(copy.deepcopy(self.importdata[row[1]][imkey][imindex]))
elif resindex<0:
self.resultdata[row[3]][reskey]=copy.deepcopy(self.importdata[row[1]][imkey])
else:
self.resultdata[row[3]][reskey][resindex]=copy.deepcopy(self.importdata[row[1]][imkey][imindex])
self.table.OnDataUpdated()

def OnClearField(self, _):
row,col=self.grid.GetGridCursorRow(), self.grid.GetGridCursorCol()
columnname=self.table.GetColLabelValue(col)
row=self.table.GetRowData(row)
reskey,resindex=getdatainfo(columnname, self.resultdata[row[3]])
assert resindex is not None
if resindex<0:
del self.resultdata[row[3]][reskey]
else:
del self.resultdata[row[3]][reskey][resindex]
self.table.OnDataUpdated()

def OnImportedMismatch(self,_):
# what are we currently matching
row=self.grid.GetGridCursorRow()
_,ourimportkey,existingmatchkey,resultkey=self.table.GetRowData(row)
match=None
# what are the choices
choices=[]
for row in range(self.table.GetNumberRows()):
_,_,existingkey,_=self.table.GetRowData(row)
if existingkey is not None:
if existingmatchkey==existingkey:
match=len(choices)
choices.append( (getdata("Name", self.existingdata[existingkey], "<blank>"), existingkey) )

dlg=ImportedEntryMatchDialog(self, choices, match)
try:
if dlg.ShowModal()==wx.ID_OK:
confidence,importkey,existingkey,resultkey=self.table.GetRowData(self.grid.GetGridCursorRow())
assert importkey is not None
match=dlg.GetMatch()
if match is None:
# new entry
if existingkey is None:
wx.MessageBox("It is already a new entry!", wx.OK|wx.ICON_EXCLAMATION)
return
# make a new entry
for rowdatakey in xrange(100000):
if rowdatakey not in self.rowdata:
for resultdatakey in xrange(100000):
if resultdatakey not in self.resultdata:
self.rowdata[rowdatakey]=("", importkey, None, resultdatakey)
self.resultdata[resultdatakey]=copy.deepcopy(self.importdata[importkey])
# revert original one back
self.resultdata[resultkey]=copy.deepcopy(self.existingdata[existingkey])
self.rowdata[self.table.rowkeys[self.grid.GetGridCursorRow()]]=("", None, existingkey, resultkey)
self.table.OnDataUpdated()
return
assert False, "You really can't get here!"
# match an existing entry
ekey=choices[match][1]
if ekey==existingkey:
wx.MessageBox("That is already the entry matched!", wx.OK|wx.ICON_EXCLAMATION)
return
# find new match
for r in range(self.table.GetNumberRows()):
if r==self.grid.GetGridCursorRow(): continue
confidence,importkey,existingkey,resultkey=self.table.GetRowData(r)
if existingkey==ekey:
if importkey is not None:
wx.MessageBox("The new match already has an imported entry matching it!", "Already matched", wx.OK|wx.ICON_EXCLAMATION, self)
return
# clear out old match
del self.rowdata[self.table.rowkeys[self.grid.GetGridCursorRow()]]
# put in new one
self.rowdata[self.table.rowkeys[r]]=(confidence, ourimportkey, ekey, resultkey)
self.resultdata[resultkey]=self.MergeEntries(
copy.deepcopy(self.existingdata[ekey]),
copy.deepcopy(self.importdata[ourimportkey]))
self.table.OnDataUpdated()
return
assert False, "Can't get here"
finally:
dlg.Destroy()

def OnCellDClick(self, event):
self.EditEntry(event.GetRow(), event.GetCol())

def EditEntry(self, row, col=None):
row=self.table.GetRowData(row)
k=row[3]
# if k is none then this entry has been deleted. fix this ::TODO::
assert k is not None
data=self.resultdata[k]
if col is not None:
columnname=self.table.GetColLabelValue(col)
if columnname=="Confidence":
columnname="Name"
else:
columnname="Name"
datakey, dataindex=getdatainfo(columnname, data)
dlg=phonebookentryeditor.Editor(self, data, keytoopenon=datakey, dataindex=dataindex)
if dlg.ShowModal()==wx.ID_OK:
data=dlg.GetData()
self.resultdata[k]=data
self.table.OnDataUpdated()
dlg.Destroy()


class ImportedEntryMatchDialog(wx.Dialog):
"The dialog shown to select how an imported entry should match"

def __init__(self, parent, choices, match):
wx.Dialog.__init__(self, parent, id=-1, title="Select Import Entry Match", style=wx.CAPTION|
wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)

self.choices=choices
self.importdialog=parent

vbs=wx.BoxSizer(wx.VERTICAL)
hbs=wx.BoxSizer(wx.HORIZONTAL)
self.matchexisting=wx.RadioButton(self, wx.NewId(), "Matches an existing entry below", style=wx.RB_GROUP)
self.matchnew=wx.RadioButton(self, wx.NewId(), "Is a new entry")
hbs.Add(self.matchexisting, wx.NewId(), wx.ALIGN_CENTRE|wx.ALL, 5)
hbs.Add(self.matchnew, wx.NewId(), wx.ALIGN_CENTRE|wx.ALL, 5)
vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)

wx.EVT_RADIOBUTTON(self, self.matchexisting.GetId(), self.OnRBClicked)
wx.EVT_RADIOBUTTON(self, self.matchnew.GetId(), self.OnRBClicked)

splitter=wx.SplitterWindow(self, -1, style=wx.SP_3D|wx.SP_LIVE_UPDATE)
self.nameslb=wx.ListBox(splitter, wx.NewId(), choices=[name for name,id in choices], style=wx.LB_SINGLE|wx.LB_NEEDED_SB)
self.preview=PhoneEntryDetailsView(splitter, -1)
splitter.SplitVertically(self.nameslb, self.preview)
vbs.Add(splitter, 1, wx.EXPAND|wx.ALL, 5)

vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5)

vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5)

wx.EVT_LISTBOX(self, self.nameslb.GetId(), self.OnLbClicked)
wx.EVT_LISTBOX_DCLICK(self, self.nameslb.GetId(), self.OnLbDClicked)

# set values
if match is None:
self.matchexisting.SetValue(False)
self.matchnew.SetValue(True)
self.nameslb.Enable(False)
else:
self.matchexisting.SetValue(True)
self.matchnew.SetValue(False)
self.nameslb.Enable(True)
self.nameslb.SetSelection(match)
self.preview.ShowEntry(self.importdialog.existingdata[choices[match][1]])

self.SetSizer(vbs)
self.SetAutoLayout(True)
guiwidgets.set_size("PhonebookImportEntryMatcher", self, screenpct=75, aspect=0.58)

wx.EVT_MENU(self, wx.ID_OK, self.SaveSize)
wx.EVT_MENU(self, wx.ID_CANCEL, self.SaveSize)


def SaveSize(self, evt=None):
if evt is not None:
evt.Skip()
guiwidgets.save_size("PhonebookImportEntryMatcher", self.GetRect())

def OnRBClicked(self, _):
self.nameslb.Enable(self.matchexisting.GetValue())

def OnLbClicked(self,_=None):
existingid=self.choices[self.nameslb.GetSelection()][1]
self.preview.ShowEntry(self.importdialog.existingdata[existingid])

def OnLbDClicked(self,_):
self.OnLbClicked()
self.SaveSize()
self.EndModal(wx.ID_OK)

def GetMatch(self):
if self.matchnew.GetValue():
return None # new entry
return self.nameslb.GetSelection()

def dictintersection(one,two):
return filter(two.has_key, one.keys())

class EntryMatcher:
"Implements matching phonebook entries"

def __init__(self, sources, against):
self.sources=sources
self.against=against

def bestmatches(self, sourceid, limit=5):
"""Gives best matches out of against list

@return: list of tuples of (percent match, againstid)
"""

res=[]

source=self.sources[sourceid]
for i in self.against:
against=self.against[i]

# now get keys source and against have in common
intersect=dictintersection(source,against)

# overall score for this match
score=0
count=0
for key in intersect:
s=source[key]
a=against[key]
count+=1
if s==a:
score+=40*len(s)
continue

if key=="names":
score+=comparenames(s,a)
elif key=="numbers":
score+=comparenumbers(s,a)
elif key=="urls":
score+=comparefields(s,a,"url")
elif key=="emails":
score+=comparefields(s,a,"email")
elif key=="addresses":
score+=compareallfields(s,a, ("company", "street", "street2", "city", "state", "postalcode", "country"))
else:
# ignore it
count-=1

if count:
res.append( ( int(score*100/count), i ) )

res.sort()
res.reverse()
if len(res)>limit:
return res[:limit]
return res

def comparenames(s,a):
"Give a score on two names"
return (jarowinkler(nameparser.formatsimplename(s[0]), nameparser.formatsimplename(a[0]))-0.6)*10

def cleanurl(url, mode="compare"):
"""Returns lowercase url with the "http://" prefix removed and in lower case

@param mode: If the value is compare (default), it removes ""http://www.""
in preparation for comparing entries. Otherwise, if the value
is pb, the result is formatted for writing to the phonebook.
"""
if mode == "compare":
urlprefix=re.compile("^(http://)?(www.)?")
else: urlprefix=re.compile("^(http://)?")

return default_cleaner(re.sub(urlprefix, "", url).lower())

def cleanemail(email, mode="compare"):
"""Returns lowercase email
"""
return default_cleaner(email.lower())


nondigits=re.compile("[^0-9]")
def cleannumber(num):
"Returns num (a phone number) with all non-digits removed"
return re.sub(nondigits, "", num)

def comparenumbers(s,a):
"""Give a score on two phone numbers

"""

ss=[cleannumber(x['number']) for x in s]
aa=[cleannumber(x['number']) for x in a]

candidates=[]
for snum in ss:
for anum in aa:
candidates.append( (jarowinkler(snum, anum), snum, anum) )

candidates.sort()
candidates.reverse()

if len(candidates)>3:
candidates=candidates[:3]

score=0
# we now have 3 best matches
for ratio,snum,anum in candidates:
if ratio>0.9:
score+=(ratio-0.9)*10

return score

def comparefields(s,a,valuekey,threshold=0.8,lookat=3):
"""Compares the valuekey field in source and against lists returning a score for closeness of match"""
ss=[x[valuekey] for x in s if x.has_key(valuekey)]
aa=[x[valuekey] for x in a if x.has_key(valuekey)]

candidates=[]
for sval in ss:
for aval in aa:
candidates.append( (jarowinkler(sval, aval), sval, aval) )

candidates.sort()
candidates.reverse()

if len(candidates)>lookat:
candidates=candidates[:lookat]

score=0
# we now have 3 best matches
for ratio,sval,aval in candidates:
if ratio>threshold:
score+=(ratio-threshold)*10/(1-threshold)

return score

def compareallfields(s,a,fields,threshold=0.8,lookat=3):
"""Like comparefields, but for source and against lists where multiple keys have values in each item

@param fields: This should be a list of keys from the entries that are in the order the human
would write them down."""

# we do it in "write them down order" as that means individual values that move don't hurt the matching
# much (eg if the town was wrongly in address2 and then moved into town, the concatenated string would
# still be the same and it would still be an appropriate match)
args=[]
for d in s,a:
str=""
list=[]
for entry in d:
for f in fields:
# we merge together the fields space separated in order to make one long string from the values
if entry.has_key(f):
str+=entry.get(f)+" "
list.append( {'value': str} )
args.append( list )
# and then give the result to comparefields
args.extend( ['value', threshold, lookat] )
return comparefields(*args)

def mergenumberlists(orig, imp):
"""Return the results of merging two lists of numbers

We compare the sanitised numbers (ie after punctuation etc is stripped
out). If they are the same, then the original is kept (since the number
is the same, and the original most likely has the correct punctuation).

Otherwise the imported entries overwrite the originals
"""
# results start with existing entries
res=[]
res.extend(orig)
# look through each imported number
for i in imp:
num=cleannumber(i['number'])
found=False
for r in res:
if num==cleannumber(r['number']):
# an existing entry was matched so we stop
found=True
if i.has_key('speeddial'):
r['speeddial']=i['speeddial']
break
if found:
continue

# we will be replacing one of the same type
found=False
for r in res:
if i['type']==r['type']:
r['number']=i['number']
if i.has_key('speeddial'):
r['speeddial']=i['speeddial']
found=True
break
if found:
continue
# ok, just add it on the end then
res.append(i)

return res

# new jaro winkler implementation doesn't use '*' chars or similar mangling
default_cleaner=lambda x: x

def mergefields(orig, imp, field, threshold=0.88, cleaner=default_cleaner):
"""Return the results of merging two lists of fields

We compare the fields. If they are the same, then the original is kept
(since the name is the same, and the original most likely has the
correct punctuation).

Otherwise the imported entries overwrite the originals
"""
# results start with existing entries
res=[]
res.extend(orig)
# look through each imported field
for i in imp:

impfield=cleaner(i[field])

found=False
for r in res:
# if the imported entry is similar or the same as the
# original entry, then we stop

# add code for short or long lengths
# since cell phones usually have less than 16-22 chars max per field

resfield=cleaner(r[field])

if (comparestrings(resfield, impfield) > threshold):
# an existing entry was matched so we stop
found=True

# since new item matches, we don't need to replace the
# original value, but we should update the type of item
# to reflect the imported value
# for example home --> business
if i.has_key('type'):
r['type'] = i['type']

# break out of original item loop
break

# if we have found the item to be imported, we can move to the next one
if found:
continue

# since there is no matching item, we will replace the existing item
# if a matching type exists
found=False
for r in res:
if (i.has_key('type') and r.has_key('type')):
if i['type']==r['type']:
# write the field entry in the way the phonebook expects it
r[field]=cleaner(i[field], "pb")
found=True
break
if found:
continue
# add new item on the end if there no matching type
# and write the field entry in the way the phonebook expects it
i[field] = cleaner(i[field], "pb")
res.append(i)

return res

import native.strings
jarowinkler=native.strings.jarow

def comparestrings(origfield, impfield):
""" Compares two strings and returns the score using
winkler routine from Febrl (stringcmp.py)

Return value is between 0.0 and 1.0, where 0.0 means no similarity
whatsoever, and 1.0 means the strings match exactly."""
return jarowinkler(origfield, impfield, 16)

def normalise_data(entries):
for k in entries:
# we only know about phone numbers so far ...
for n in entries[k].get("numbers", []):
n["number"]=phonenumber.normalise(n["number"])

class ColumnSelectorDialog(wx.Dialog):
"The dialog for selecting what columns you want to view"

ID_SHOW=wx.NewId()
ID_AVAILABLE=wx.NewId()
ID_UP=wx.NewId()
ID_DOWN=wx.NewId()
ID_ADD=wx.NewId()
ID_REMOVE=wx.NewId()
ID_DEFAULT=wx.NewId()

def __init__(self, parent, config, phonewidget):
wx.Dialog.__init__(self, parent, id=-1, title="Select Columns to view", style=wx.CAPTION|
wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)

self.config=config
self.phonewidget=phonewidget
hbs=wx.BoxSizer(wx.HORIZONTAL)

# the show bit
bs=wx.BoxSizer(wx.VERTICAL)
bs.Add(wx.StaticText(self, -1, "Showing"), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
self.show=wx.ListBox(self, self.ID_SHOW, style=wx.LB_SINGLE|wx.LB_NEEDED_SB, size=(250, 300))
bs.Add(self.show, 1, wx.EXPAND|wx.ALL, 5)
hbs.Add(bs, 1, wx.EXPAND|wx.ALL, 5)

# the column of buttons
bs=wx.BoxSizer(wx.VERTICAL)
self.up=wx.Button(self, self.ID_UP, "Move Up")
self.down=wx.Button(self, self.ID_DOWN, "Move Down")
self.add=wx.Button(self, self.ID_ADD, "Show")
self.remove=wx.Button(self, self.ID_REMOVE, "Don't Show")
self.default=wx.Button(self, self.ID_DEFAULT, "Default")

for b in self.up, self.down, self.add, self.remove, self.default:
bs.Add(b, 0, wx.ALL|wx.ALIGN_CENTRE, 10)

hbs.Add(bs, 0, wx.ALL|wx.ALIGN_CENTRE, 5)

# the available bit
bs=wx.BoxSizer(wx.VERTICAL)
bs.Add(wx.StaticText(self, -1, "Available"), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
self.available=wx.ListBox(self, self.ID_AVAILABLE, style=wx.LB_EXTENDED|wx.LB_NEEDED_SB, choices=AvailableColumns)
bs.Add(self.available, 1, wx.EXPAND|wx.ALL, 5)
hbs.Add(bs, 1, wx.EXPAND|wx.ALL, 5)

# main layout
vbs=wx.BoxSizer(wx.VERTICAL)
vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 5)
vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5)
vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5)

self.SetSizer(vbs)
vbs.Fit(self)

# fill in current selection
cur=self.config.Read("phonebookcolumns", "")
if len(cur):
cur=cur.split(",")
# ensure they all exist
cur=[c for c in cur if c in AvailableColumns]
else:
cur=DefaultColumns
self.show.Set(cur)

# buttons, events etc
self.up.Disable()
self.down.Disable()
self.add.Disable()
self.remove.Disable()

wx.EVT_LISTBOX(self, self.ID_SHOW, self.OnShowClicked)
wx.EVT_LISTBOX_DCLICK(self, self.ID_SHOW, self.OnShowClicked)
wx.EVT_LISTBOX(self, self.ID_AVAILABLE, self.OnAvailableClicked)
wx.EVT_LISTBOX_DCLICK(self, self.ID_AVAILABLE, self.OnAvailableDClicked)

wx.EVT_BUTTON(self, self.ID_ADD, self.OnAdd)
wx.EVT_BUTTON(self, self.ID_REMOVE, self.OnRemove)
wx.EVT_BUTTON(self, self.ID_UP, self.OnUp)
wx.EVT_BUTTON(self, self.ID_DOWN, self.OnDown)
wx.EVT_BUTTON(self, self.ID_DEFAULT, self.OnDefault)
wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)

def OnShowClicked(self, _=None):
self.up.Enable(self.show.GetSelection()>0)
self.down.Enable(self.show.GetSelection()<self.show.GetCount()-1)
self.remove.Enable(self.show.GetCount()>0)
self.FindWindowById(wx.ID_OK).Enable(self.show.GetCount()>0)

def OnAvailableClicked(self, _):
self.add.Enable(True)

def OnAvailableDClicked(self, _):
self.OnAdd()

def OnAdd(self, _=None):
items=[AvailableColumns[i] for i in self.available.GetSelections()]
for i in self.available.GetSelections():
self.available.Deselect(i)
self.add.Disable()
it=self.show.GetSelection()
if it>=0:
self.show.Deselect(it)
it+=1
else:
it=self.show.GetCount()
self.show.InsertItems(items, it)
self.remove.Disable()
self.up.Disable()
self.down.Disable()
self.show.SetSelection(it)
self.OnShowClicked()

def OnRemove(self, _):
it=self.show.GetSelection()
assert it>=0
self.show.Delete(it)
if self.show.GetCount():
if it==self.show.GetCount():
self.show.SetSelection(it-1)
else:
self.show.SetSelection(it)
self.OnShowClicked()

def OnDefault(self,_):
self.show.Set(DefaultColumns)
self.show.SetSelection(0)
self.OnShowClicked()

def OnUp(self, _):
it=self.show.GetSelection()
assert it>=1
self.show.InsertItems([self.show.GetString(it)], it-1)
self.show.Delete(it+1)
self.show.SetSelection(it-1)
self.OnShowClicked()

def OnDown(self, _):
it=self.show.GetSelection()
assert it<self.show.GetCount()-1
self.show.InsertItems([self.show.GetString(it)], it+2)
self.show.Delete(it)
self.show.SetSelection(it+1)
self.OnShowClicked()

def OnOk(self, event):
cur=[self.show.GetString(i) for i in range(self.show.GetCount())]
self.config.Write("phonebookcolumns", ",".join(cur))
self.config.Flush()
self.phonewidget.SetColumns(cur)
event.Skip()

class PhonebookPrintDialog(wx.Dialog):

ID_SELECTED=wx.NewId()
ID_ALL=wx.NewId()
ID_LAYOUT=wx.NewId()
ID_STYLES=wx.NewId()
ID_PRINT=wx.NewId()
ID_PAGESETUP=wx.NewId()
ID_PRINTPREVIEW=wx.NewId()
ID_CLOSE=wx.ID_CANCEL
ID_HELP=wx.NewId()
ID_TEXTSCALE=wx.NewId()

textscales=[ (0.4, "Teeny"), (0.6, "Tiny"), (0.8, "Small"), (1.0, "Normal"), (1.2, "Large"), (1.4, "Ginormous") ]
# we reverse the order so the slider seems more natural
textscales.reverse()

def __init__(self, phonewidget, mainwindow, config):
wx.Dialog.__init__(self, mainwindow, id=-1, title="Print PhoneBook", style=wx.CAPTION|
wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)

self.config=config
self.phonewidget=phonewidget

# sort out available layouts and styles
# first line is description
self.layoutfiles={}
for file in guihelper.getresourcefiles("pbpl-*.xy"):
f=open(file, "rt")
desc=f.readline().strip()
self.layoutfiles[desc]=f.read()
f.close()
self.stylefiles={}
for file in guihelper.getresourcefiles("pbps-*.xy"):
f=open(file, "rt")
desc=f.readline().strip()
self.stylefiles[desc]=f.read()
f.close()

# Layouts
vbs=wx.BoxSizer(wx.VERTICAL) # main vertical sizer

hbs=wx.BoxSizer(wx.HORIZONTAL) # first row

numselected=len(phonewidget.GetSelectedRows())
numtotal=len(phonewidget._data)

# selection and scale
vbs2=wx.BoxSizer(wx.VERTICAL)
bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Rows"), wx.VERTICAL)
self.selected=wx.RadioButton(self, self.ID_SELECTED, "Selected (%d)" % (numselected,), style=wx.RB_GROUP)
self.all=wx.RadioButton(self, self.ID_SELECTED, "All (%d)" % (numtotal,) )
bs.Add(self.selected, 0, wx.EXPAND|wx.ALL, 2)
bs.Add(self.all, 0, wx.EXPAND|wx.ALL, 2)
self.selected.SetValue(numselected>1)
self.all.SetValue(not (numselected>1))
vbs2.Add(bs, 0, wx.EXPAND|wx.ALL, 2)

bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Text Scale"), wx.HORIZONTAL)
for i in range(len(self.textscales)):
if self.textscales[i][0]==1.0:
sv=i
break
self.textscaleslider=wx.Slider(self, self.ID_TEXTSCALE, sv, 0, len(self.textscales)-1, style=wx.SL_VERTICAL|wx.SL_AUTOTICKS)
self.scale=1
bs.Add(self.textscaleslider, 0, wx.EXPAND|wx.ALL, 2)
self.textscalelabel=wx.StaticText(self, -1, "Normal")
bs.Add(self.textscalelabel, 0, wx.ALIGN_CENTRE)
vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2)
hbs.Add(vbs2, 0, wx.EXPAND|wx.ALL, 2)

# Sort
self.sortkeyscb=[]
bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Sorting"), wx.VERTICAL)
choices=["<None>"]+AvailableColumns
for i in range(3):
bs.Add(wx.StaticText(self, -1, ("Sort by", "Then")[i!=0]), 0, wx.EXPAND|wx.ALL, 2)
self.sortkeyscb.append(wx.ComboBox(self, wx.NewId(), "<None>", choices=choices, style=wx.CB_READONLY))
self.sortkeyscb[-1].SetSelection(0)
bs.Add(self.sortkeyscb[-1], 0, wx.EXPAND|wx.ALL, 2)
hbs.Add(bs, 0, wx.EXPAND|wx.ALL, 4)

# Layout and style
vbs2=wx.BoxSizer(wx.VERTICAL) # they are on top of each other
bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Layout"), wx.VERTICAL)
k=self.layoutfiles.keys()
k.sort()
self.layout=wx.ListBox(self, self.ID_LAYOUT, style=wx.LB_SINGLE|wx.LB_NEEDED_SB|wx.LB_HSCROLL, choices=k, size=(150,-1))
self.layout.SetSelection(0)
bs.Add(self.layout, 1, wx.EXPAND|wx.ALL, 2)
vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2)
bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Styles"), wx.VERTICAL)
k=self.stylefiles.keys()
self.styles=wx.CheckListBox(self, self.ID_STYLES, choices=k)
bs.Add(self.styles, 1, wx.EXPAND|wx.ALL, 2)
vbs2.Add(bs, 1, wx.EXPAND|wx.ALL, 2)
hbs.Add(vbs2, 1, wx.EXPAND|wx.ALL, 2)

# Buttons
vbs2=wx.BoxSizer(wx.VERTICAL)
vbs2.Add(wx.Button(self, self.ID_PRINT, "Print"), 0, wx.EXPAND|wx.ALL, 2)
vbs2.Add(wx.Button(self, self.ID_PAGESETUP, "Page Setup..."), 0, wx.EXPAND|wx.ALL, 2)
vbs2.Add(wx.Button(self, self.ID_PRINTPREVIEW, "Print Preview"), 0, wx.EXPAND|wx.ALL, 2)
vbs2.Add(wx.Button(self, self.ID_HELP, "Help"), 0, wx.EXPAND|wx.ALL, 2)
vbs2.Add(wx.Button(self, self.ID_CLOSE, "Close"), 0, wx.EXPAND|wx.ALL, 2)
hbs.Add(vbs2, 0, wx.EXPAND|wx.ALL, 2)

# wrap up top row
vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 2)

# bottom half - preview
bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Content Preview"), wx.VERTICAL)
self.preview=bphtml.HTMLWindow(self, -1)
bs.Add(self.preview, 1, wx.EXPAND|wx.ALL, 2)

# wrap up bottom row
vbs.Add(bs, 2, wx.EXPAND|wx.ALL, 2)

self.SetSizer(vbs)
vbs.Fit(self)

# event handlers
wx.EVT_BUTTON(self, self.ID_PRINTPREVIEW, self.OnPrintPreview)
wx.EVT_BUTTON(self, self.ID_PRINT, self.OnPrint)
wx.EVT_BUTTON(self, self.ID_PAGESETUP, self.OnPageSetup)
wx.EVT_RADIOBUTTON(self, self.selected.GetId(), self.UpdateHtml)
wx.EVT_RADIOBUTTON(self, self.all.GetId(), self.UpdateHtml)
for i in self.sortkeyscb:
wx.EVT_COMBOBOX(self, i.GetId(), self.UpdateHtml)
wx.EVT_LISTBOX(self, self.layout.GetId(), self.UpdateHtml)
wx.EVT_CHECKLISTBOX(self, self.styles.GetId(), self.UpdateHtml)
wx.EVT_COMMAND_SCROLL(self, self.textscaleslider.GetId(), self.UpdateSlider)
self.UpdateHtml()

def UpdateSlider(self, evt):
pos=evt.GetPosition()
if self.textscales[pos][0]!=self.scale:
self.scale=self.textscales[pos][0]
self.textscalelabel.SetLabel(self.textscales[pos][1])
self.preview.SetFontScale(self.scale)

def UpdateHtml(self,_=None):
wx.CallAfter(self._UpdateHtml)

def _UpdateHtml(self):
self.html=self.GetCurrentHTML()
self.preview.SetPage(self.html)

def GetCurrentHTML(self):
# Setup a nice environment pointing at this module
vars={'phonebook': __import__(__name__) }
# which data do we want?
if self.all.GetValue():
rowkeys=self.phonewidget._data.keys()
else:
rowkeys=self.phonewidget.GetSelectedRowKeys()
# sort the data
# we actually sort in reverse order of what the UI shows in order to get correct results
for keycb in (-1, -2, -3):
sortkey=self.sortkeyscb[keycb].GetValue()
if sortkey=="<None>": continue
# decorate
l=[(getdata(sortkey, self.phonewidget._data[key]), key) for key in rowkeys]
l.sort()
# undecorate
rowkeys=[key for val,key in l]
# finish up vars
vars['rowkeys']=rowkeys
vars['currentcolumns']=self.phonewidget.GetColumns()
vars['data']=self.phonewidget._data
# Use xyaptu
xcp=xyaptu.xcopier(None)
xcp.setupxcopy(self.layoutfiles[self.layout.GetStringSelection()])
html=xcp.xcopywithdns(vars)
# apply styles
sd={'styles': {}, '__builtins__': __builtins__ }
for i in range(self.styles.GetCount()):
if self.styles.IsChecked(i):
exec self.stylefiles[self.styles.GetString(i)] in sd,sd
try:
html=bphtml.applyhtmlstyles(html, sd['styles'])
except:
if __debug__:
f=open("debug.html", "wt")
f.write(html)
f.close()
raise
return html

GetCurrentHTML=guihelper.BusyWrapper(GetCurrentHTML)

def OnPrintPreview(self, _):
wx.GetApp().htmlprinter.PreviewText(self.html, scale=self.scale)

def OnPrint(self, _):
wx.GetApp().htmlprinter.PrintText(self.html, scale=self.scale)

def OnPrinterSetup(self, _):
wx.GetApp().htmlprinter.PrinterSetup()

def OnPageSetup(self, _):
wx.GetApp().htmlprinter.PageSetup()


def htmlify(string):
return common.strorunicode(string).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br/>")

------=_NextPart_000_0005_01C5CC00.AE4EE700
Content-Type: application/octet-stream;
name="phonebook.py.diff"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="phonebook.py.diff"

Index: phonebook.py
===================================================================
RCS file: /cvsroot/bitpim/bitpim/phonebook.py,v
retrieving revision 1.136
diff -u -r1.136 phonebook.py
--- phonebook.py 24 Jul 2005 06:07:19 -0000 1.136
+++ phonebook.py 8 Oct 2005 18:40:22 -0000
@@ -151,7 +151,10 @@
self.xcp.setupxcopy(template)
if self.xcpstyles is None:
self.xcpstyles={}
- execfile(self.stylesfile, self.xcpstyles, self.xcpstyles)
+ try:
+ execfile(self.stylesfile, self.xcpstyles, self.xcpstyles)
+ except UnicodeError:
+ common.unicode_execfile(self.stylesfile, self.xcpstyles, self.xcpstyles)
self.xcpstyles['entry']=entry
text=self.xcp.xcopywithdns(self.xcpstyles)
try:

------=_NextPart_000_0005_01C5CC00.AE4EE700
Content-Type: text/plain;
name="ringers.py"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="ringers.py"

### BITPIM
###
### Copyright (C) 2003-2004 Roger Binns <***@rogerbinns.com>
### Copyright (C) 2003-2004 Steven Palm <***@n9yty.com>
###
### This program is free software; you can redistribute it and/or modify
### it under the terms of the BitPim license as detailed in the LICENSE file.
###
### $Id: ringers.py,v 1.42 2005/09/19 22:24:35 djpham Exp $

import os
import time
import wx
from wx.lib import masked

import guiwidgets
import guihelper
import pubsub
import aggregatedisplay
import wallpaper
import common
import fileinfo
import conversions
import helpids

import rangedslider

###
### Ringers
###

class DisplayItem(guiwidgets.FileViewDisplayItem):

datakey='ringtone-index'
datatype='Audio' # used in the tooltip


class RingerView(guiwidgets.FileView):
CURRENTFILEVERSION=2

# this is only used to prevent the pubsub module
# from being GC while any instance of this class exists
__publisher=pubsub.Publisher


organizetypes=("Origin", "Audio Type", "File Size")
media_notification_type=pubsub.ringtone_type

def __init__(self, mainwindow, parent, id=-1):
self.mainwindow=mainwindow
self._data={'ringtone-index': {}}
self.updateprofilevariables(self.mainwindow.phoneprofile)
self.organizemenu=wx.Menu()
guiwidgets.FileView.__init__(self, mainwindow, parent, "ringtone-watermark")
self.wildcard="Audio files|*.wav;*.mid;*.qcp;*.mp3;*.pmd|Midi files|*.mid|Purevoice files|*.qcp|MP3 files|*.mp3|PMD/CMX files|*.pmd|All files|*.*"

self.organizeinfo={}

for k in self.organizetypes:
id=wx.NewId()
self.organizemenu.AppendRadioItem(id, k)
wx.EVT_MENU(self, id, self.OrganizeChange)
self.organizeinfo[id]=getattr(self, "organizeby_"+k.replace(" ",""))

self.modified=False
wx.EVT_IDLE(self, self.OnIdle)
pubsub.subscribe(self.OnListRequest, pubsub.REQUEST_RINGTONES)
pubsub.subscribe(self.OnDictRequest, pubsub.REQUEST_RINGTONE_INDEX)

def updateprofilevariables(self, profile):
self.maxlen=profile.MAX_RINGTONE_BASENAME_LENGTH
self.filenamechars=profile.RINGTONE_FILENAME_CHARS

def OnListRequest(self, msg=None):
l=[self._data['ringtone-index'][x]['name'] for x in self._data['ringtone-index']]
l.sort()
pubsub.publish(pubsub.ALL_RINGTONES, l)

def OnDictRequest(self, msg=None):
pubsub.publish(pubsub.ALL_RINGTONE_INDEX, self._data['ringtone-index'].copy())

def OnIdle(self, _):
"Save out changed data"
if self.modified:
self.modified=False
self.populatefs(self._data)
self.OnListRequest() # broadcast changes

def getdata(self,dict,want=guiwidgets.FileView.NONE):
return self.genericgetdata(dict, want, self.mainwindow.ringerpath, 'ringtone', 'ringtone-index')

def GetItemThumbnail(self, item, w, h):
assert w==self.thumbnail.GetWidth() and h==self.thumbnail.GetHeight()
return self.thumbnail

def OrganizeChange(self, evt):
evt.GetEventObject().Check(evt.GetId(), True)
self.OnRefresh()

def GetSections(self):
# work out section and item sizes
self.thumbnail=wx.Image(guihelper.getresourcefile('ringer.png')).ConvertToBitmap()

dc=wx.MemoryDC()
dc.SelectObject(wx.EmptyBitmap(100,100)) # unused bitmap needed to keep wxMac happy
h=dc.GetTextExtent("I")[1]
itemsize=self.thumbnail.GetWidth()+160, max(self.thumbnail.GetHeight(), h*4+DisplayItem.PADDING)+DisplayItem.PADDING*2

# get all the items
items=[DisplayItem(self, key, self.mainwindow.ringerpath) for key in self._data['ringtone-index']]
# prune out ones we don't have images for
items=[item for item in items if os.path.exists(item.filename)]

self.sections=[]

if len(items)==0:
return self.sections

# get the current sorting type
for i in range(len(self.organizetypes)):
item=self.organizemenu.FindItemByPosition(i)
if self.organizemenu.IsChecked(item.GetId()):
for sectionlabel, items in self.organizeinfo[item.GetId()](items):
sh=aggregatedisplay.SectionHeader(sectionlabel)
sh.itemsize=itemsize
for item in items:
item.thumbnailsize=self.thumbnail.GetWidth(), self.thumbnail.GetHeight()
# sort items by name
items=[(item.name.lower(), item) for item in items]
items.sort()
items=[item for name,item in items]
self.sections.append( (sh, items) )
return [sh for sh,items in self.sections]
assert False, "Can't get here"

def GetItemsFromSection(self, sectionnumber, sectionheader):
return self.sections[sectionnumber][1]

def organizeby_AudioType(self, items):
types={}
for item in items:
t=item.fileinfo.format
if t is None: t="<Unknown>"
l=types.get(t, [])
l.append(item)
types[t]=l

keys=types.keys()
keys.sort()
return [ (key, types[key]) for key in types]

def organizeby_Origin(self, items):
types={}
for item in items:
t=item.origin
if t is None: t="Default"
l=types.get(t, [])
l.append(item)
types[t]=l

keys=types.keys()
keys.sort()
return [ (key, types[key]) for key in types]

def organizeby_FileSize(self, items):

sizes={0: ('Less than 8kb', []),
8192: ('8 kilobytes', []),
16384: ('16 kilobytes', []),
32768: ('32 kilobytes', []),
65536: ('64 kilobytes', []),
131052: ('128 kilobytes', []),
524208: ('512 kilobytes', []),
1024*1024: ('One megabyte', [])}

keys=sizes.keys()
keys.sort()

for item in items:
t=item.size
if t>=keys[-1]:
sizes[keys[-1]][1].append(item)
continue
for i,k in enumerate(keys):
if t<keys[i+1]:
sizes[k][1].append(item)
break

return [sizes[k] for k in keys if len(sizes[k][1])]

def GetItemSize(self, sectionnumber, sectionheader):
return sectionheader.itemsize

def GetFileInfo(self, filename):
return fileinfo.identify_audiofile(filename)

def RemoveFromIndex(self, names):
for name in names:
wp=self._data['ringtone-index']
for k in wp.keys():
if wp[k]['name']==name:
del wp[k]
self.modified=True

def OnAddFiles(self, filenames):
for file in filenames:
if file is None: continue # failed dragdrop?
# do we want to convert file?
afi=fileinfo.identify_audiofile(file)
if afi.size<=0: continue # zero length file or other issues
newext,convertinfo=self.mainwindow.phoneprofile.QueryAudio(None, common.getext(file), afi)
if convertinfo is not afi:
filedata=None
wx.EndBusyCursor()
try:
filedata=self.ConvertFormat(file, convertinfo)
finally:
# ensure they match up
wx.BeginBusyCursor()
if filedata is None:
continue
else:
filedata=open(file, "rb").read()
# check for the size limit on the file, if specified
max_size=getattr(convertinfo, 'MAXSIZE', None)
if max_size is not None and len(filedata)>max_size:
# the data is too big
self.log('ringtone %s is too big!'%common.basename(file))
dlg=wx.MessageDialog(self,
'Ringtone %s may be too big. Do you want to proceed anway?'%common.basename(file),
'Warning',
style=wx.YES_NO|wx.ICON_ERROR)
dlg_resp=dlg.ShowModal()
dlg.Destroy()
if dlg_resp==wx.ID_NO:
continue
self.thedir=self.mainwindow.ringerpath
decoded_file=self.decodefilename(file)
target=self.getshortenedbasename(decoded_file, newext)
open(target, "wb").write(filedata)
self.AddToIndex(str(os.path.basename(target)).decode(guiwidgets.media_codec))
self.OnRefresh()

OnAddFiles=guihelper.BusyWrapper(OnAddFiles)

def AddToIndex(self, file):
for i in self._data['ringtone-index']:
if self._data['ringtone-index'][i]['name']==file:
return
keys=self._data['ringtone-index'].keys()
idx=10000
while idx in keys:
idx+=1
self._data['ringtone-index'][idx]={'name': file}
self.modified=True

def ConvertFormat(self, file, convertinfo):
dlg=ConvertDialog(self, file, convertinfo)
if dlg.ShowModal()==wx.ID_OK:
res=dlg.newfiledata
else:
res=None
dlg.Destroy()
return res

def updateindex(self, index):
if index!=self._data['ringtone-index']:
self._data['ringtone-index']=index.copy()
self.modified=True

def populatefs(self, dict):
self.thedir=self.mainwindow.ringerpath
return self.genericpopulatefs(dict, 'ringtone', 'ringtone-index', self.CURRENTFILEVERSION)

def populate(self, dict):
if self._data['ringtone-index']!=dict['ringtone-index']:
self._data['ringtone-index']=dict['ringtone-index'].copy()
self.modified=True
self.OnRefresh()

def getfromfs(self, result):
self.thedir=self.mainwindow.ringerpath
return self.genericgetfromfs(result, None, 'ringtone-index', self.CURRENTFILEVERSION)

def updateindex(self, index):
if index!=self._data['ringtone-index']:
self._data['ringtone-index']=index.copy()
self.modified=True

def versionupgrade(self, dict, version):
"""Upgrade old data format read from disk

@param dict: The dict that was read in
@param version: version number of the data on disk
"""

# version 0 to 1 upgrade
if version==0:
version=1 # the are the same

# 1 to 2 etc
if version==1:
print "converting to version 2"
version=2
d={}
input=dict.get('ringtone-index', {})
for i in input:
d[i]={'name': input[i]}
dict['ringtone-index']=d
return dict

class ConvertDialog(wx.Dialog):

ID_CONVERT=wx.NewId()
ID_PLAY=wx.NewId()
ID_PLAY_CLIP=wx.NewId()
ID_STOP=wx.NewId()
ID_TIMER=wx.NewId()
ID_SLIDER=wx.NewId()

# we need to offer all types here rather than using derived classes as the phone may support several
# alternatives

PARAMETERS={
'MP3': {
'formats': ["MP3"],
'samplerates': ["16000", "22050", "24000", "32000", "44100", "48000"],
'channels': ["1", "2"],
'bitrates': ["8", "16", "24", "32", "40", "48", "56", "64", "80", "96", "112", "128", "144", "160", "192", "224", "256", "320"],
'setup': 'mp3setup',
'convert': 'mp3convert',
'filelength': 'mp3filelength',
'final': 'mp3final',
},

'QCP': {
'formats': ["QCP"],
'samplerates': ["8000"],
'channels': ["1"],
'bitrates': ["13000"],
'optimization': ['0-Best Sound Quality', '1', '2', '3-Smallest File Size'],
'setup': 'qcpsetup',
'convert': 'qcpconvert',
'filelength': 'qcpfilelength',
'final': 'qcpfinal',
}

}



def __init__(self, parent, file, convertinfo):
wx.Dialog.__init__(self, parent, title="Convert Audio File", style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.SYSTEM_MENU|wx.MAXIMIZE_BOX)
self.file=file
self.convertinfo=convertinfo
self.afi=None
self.temporaryfiles=[]
self.wavfile=common.gettempfilename("wav") # full length wav equivalent
self.clipwavfile=common.gettempfilename("wav") # used for clips from full length wav
self.temporaryfiles.extend([self.wavfile, self.clipwavfile])

getattr(self, self.PARAMETERS[convertinfo.format]['setup'])()

vbs=wx.BoxSizer(wx.VERTICAL)
# create the covert controls
self.create_convert_pane(vbs, file, convertinfo)
# and the crop controls
self.create_crop_panel(vbs)

vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALL|wx.ALIGN_RIGHT, 5)

self.SetSizer(vbs)
vbs.Fit(self)

# diable various things
self.FindWindowById(wx.ID_OK).Enable(False)
for i in self.cropids:
self.FindWindowById(i).Enable(False)


# Events
wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)
wx.EVT_BUTTON(self, wx.ID_CANCEL, self.OnCancel)
wx.EVT_TIMER(self, self.ID_TIMER, self.OnTimer)
wx.EVT_BUTTON(self, wx.ID_HELP, lambda _: wx.GetApp().displayhelpid(helpids.ID_DLG_AUDIOCONVERT))

# timers and sounds
self.sound=None
self.timer=wx.Timer(self, self.ID_TIMER)

# wxPython wrapper on Mac raises NotImplemented for wx.Sound.Stop() so
# we hack around that by supplying a zero length wav to play instead
if guihelper.IsMac():
self.zerolenwav=guihelper.getresourcefile("zerolen.wav")


def create_convert_pane(self, vbs, file, convertinfo):
params=self.PARAMETERS[convertinfo.format]
# convert bit
bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Convert"), wx.VERTICAL)
bs.Add(wx.StaticText(self, -1, "Input File: "+file), 0, wx.ALL, 5)
gs=wx.FlexGridSizer(2, 4, 5, 5)
gs.Add(wx.StaticText(self, -1, "New Type"), 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
self.type=wx.ComboBox(self, style=wx.CB_DROPDOWN|wx.CB_READONLY, choices=params['formats'])
gs.Add(self.type, 0, wx.ALL|wx.EXPAND, 5)
gs.Add(wx.StaticText(self, -1, "Sample Rate (per second)"), 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
self.samplerate=wx.ComboBox(self, style=wx.CB_DROPDOWN|wx.CB_READONLY, choices=params['samplerates'])
gs.Add(self.samplerate, 0, wx.ALL|wx.EXPAND, 5)
gs.Add(wx.StaticText(self, -1, "Channels (Mono/Stereo)"), 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
self.channels=wx.ComboBox(self, style=wx.CB_DROPDOWN|wx.CB_READONLY, choices=params['channels'])
gs.Add(self.channels, 0, wx.ALL|wx.EXPAND, 5)
gs.Add(wx.StaticText(self, -1, "Bitrate (kbits per second)"), 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
self.bitrate=wx.ComboBox(self, style=wx.CB_DROPDOWN|wx.CB_READONLY, choices=params['bitrates'])
gs.Add(self.bitrate, 0, wx.ALL|wx.EXPAND, 5)
if params.has_key('optimization'):
gs.Add(wx.StaticText(self, -1, 'Optimization'), 0,
wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
self.optimization=wx.ComboBox(self,
style=wx.CB_DROPDOWN|wx.CB_READONLY,
choices=params['optimization'])
self.optimization.SetSelection(1)
gs.Add(self.optimization, 0, wx.ALL|wx.EXPAND, 5)
gs.AddGrowableCol(1, 1)
gs.AddGrowableCol(3, 1)
bs.Add(gs, 0, wx.EXPAND)

bs.Add(wx.Button(self, self.ID_CONVERT, "Convert"), 0, wx.ALIGN_RIGHT|wx.ALL, 5)

vbs.Add(bs, 0, wx.EXPAND|wx.ALL, 5)
# Fill out fields - we explicitly set even when not necessary due to bugs on wxMac
if self.type.GetCount()==1:
self.type.SetSelection(0)
else:
self.type.SetStringSelection(convertinfo.format)

if self.channels.GetCount()==1:
self.channels.SetSelection(0)
else:
self.channels.SetStringSelection(`convertinfo.channels`)

if self.bitrate.GetCount()==1:
self.bitrate.SetSelection(0)
else:
self.bitrate.SetStringSelection(`convertinfo.bitrate`)

if self.samplerate.GetCount()==1:
self.samplerate.SetSelection(0)
else:
self.samplerate.SetStringSelection(`convertinfo.samplerate`)

# Events
wx.EVT_BUTTON(self, self.ID_CONVERT, self.OnConvert)

def create_crop_panel(self, vbs):
# crop bit
bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Crop"), wx.VERTICAL)
hbs=wx.BoxSizer(wx.HORIZONTAL)
hbs.Add(wx.StaticText(self, -1, "Current Position"), 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
self.positionlabel=wx.StaticText(self, -1, "0 ")
hbs.Add(self.positionlabel, 0, wx.ALL, 5)
hbs.Add(wx.StaticText(self, -1, "Est. Clip File length"), 0, wx.ALL|wx.ALIGN_CENTRE_VERTICAL, 5)
self.lengthlabel=wx.StaticText(self, -1, "0 ")
hbs.Add(self.lengthlabel, 0, wx.ALL, 5)
bs.Add(hbs, 0, wx.ALL, 5)
# the start & end manual entry items
hbs=wx.GridSizer(-1, 2, 0, 0)
hbs.Add(wx.StaticText(self, -1, 'Clip Start (sec):'), 0, wx.EXPAND|wx.ALL, 5)
self.clip_start=masked.NumCtrl(self, wx.NewId(), fractionWidth=2)
hbs.Add(self.clip_start, 1, wx.EXPAND|wx.ALL, 5)
hbs.Add(wx.StaticText(self, -1, 'Clip Duration (sec):'), 0, wx.EXPAND|wx.ALL, 5)
self.clip_duration=masked.NumCtrl(self, wx.NewId(), fractionWidth=2)
hbs.Add(self.clip_duration, 1, wx.EXPAND|wx.ALL, 5)
hbs.Add(wx.StaticText(self, -1, 'Volume Adjustment (dB):'), 0,
wx.EXPAND|wx.ALL, 5)
self.clip_volume=masked.NumCtrl(self, wx.NewId(), fractionWidth=1)
hbs.Add(self.clip_volume, 1, wx.EXPAND|wx.ALL, 5)
clip_set_btn=wx.Button(self, wx.NewId(), 'Set')
hbs.Add(clip_set_btn, 0, wx.EXPAND|wx.ALL, 5)
bs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
hbs=wx.BoxSizer(wx.HORIZONTAL)
self.slider=rangedslider.RangedSlider(self, id=self.ID_SLIDER, size=(-1, 30))
hbs.Add(self.slider, 1, wx.EXPAND|wx.ALL, 5)
bs.Add(hbs, 1, wx.EXPAND|wx.ALL, 5)
hbs=wx.BoxSizer(wx.HORIZONTAL)
hbs.Add(wx.Button(self, self.ID_STOP, "Stop"), 0, wx.ALL, 5)
hbs.Add(wx.Button(self, self.ID_PLAY, "Play Position"), 0, wx.ALL, 5)
hbs.Add(wx.Button(self, self.ID_PLAY_CLIP, "Play Clip"), 0, wx.ALL, 5)
bs.Add(hbs, 0, wx.ALL|wx.ALIGN_RIGHT, 5)
vbs.Add(bs, 0, wx.EXPAND|wx.ALL, 5)
wx.EVT_BUTTON(self, self.ID_PLAY, self.OnPlayPosition)
wx.EVT_BUTTON(self, self.ID_PLAY_CLIP, self.OnPlayClip)
wx.EVT_BUTTON(self, self.ID_STOP, self.OnStop)
wx.EVT_BUTTON(self, clip_set_btn.GetId(), self.OnSetClip)

rangedslider.EVT_POS_CHANGED(self, self.ID_SLIDER, self.OnSliderCurrentChanged)
rangedslider.EVT_CHANGING(self, self.ID_SLIDER, self.OnSliderChanging)

self.cropids=[self.ID_SLIDER, self.ID_STOP, self.ID_PLAY,
self.ID_PLAY_CLIP, self.clip_start.GetId(),
self.clip_duration.GetId(), self.clip_volume.GetId(),
clip_set_btn.GetId()]


def OnConvert(self, _):
self.OnStop()
for i in self.cropids:
self.FindWindowById(i).Enable(False)
self.FindWindowById(wx.ID_OK).Enable(False)
getattr(self, self.PARAMETERS[self.convertinfo.format]['convert'])()
self.wfi=fileinfo.getpcmfileinfo(self.wavfile)
max_duration=round(self.wfi.duration, 2)+0.01
self.clip_start.SetParameters(min=0.0, max=max_duration, limited=True)
self.clip_duration.SetParameters(min=0.0, max=max_duration, limited=True)
self.UpdateCrop()
for i in self.cropids:
self.FindWindowById(i).Enable(True)
self.FindWindowById(wx.ID_OK).Enable(True)

OnConvert=guihelper.BusyWrapper(OnConvert)


def UpdateCrop(self):
self.positionlabel.SetLabel("%.1f secs" % (self.slider.GetCurrent()*self.wfi.duration),)
duration=(self.slider.GetEnd()-self.slider.GetStart())*self.wfi.duration
self.clip_start.SetValue(self.slider.GetStart()*self.wfi.duration)
self.clip_duration.SetValue(duration)
v=getattr(self, self.PARAMETERS[self.convertinfo.format]['filelength'])(duration)
self.lengthlabel.SetLabel("%s" % (v,))



def OnPlayClip(self,_):
self._Play(self.slider.GetStart(), self.slider.GetEnd(),
self.clip_volume.GetValue())

def OnPlayPosition(self, _):
self._Play(self.slider.GetCurrent(), 1.0)

def _Play(self, start, end, volume=None):
self.OnStop()
assert start<=end
self.playstart=start
self.playend=end

self.playduration=(self.playend-self.playstart)*self.wfi.duration

conversions.trimwavfile(self.wavfile, self.clipwavfile,
self.playstart*self.wfi.duration,
self.playduration, volume)
self.sound=wx.Sound(self.clipwavfile)
assert self.sound.IsOk()
res=self.sound.Play(wx.SOUND_ASYNC)
assert res
self.starttime=time.time()
self.endtime=self.starttime+self.playduration
self.timer.Start(100, wx.TIMER_CONTINUOUS)

def OnTimer(self,_):
now=time.time()
if now>self.endtime:
self.timer.Stop()
# assert wx.Sound.IsPlaying()==False
self.slider.SetCurrent(self.playend)
self.UpdateCrop()
return
# work out where the slider should go
newval=self.playstart+((now-self.starttime)/(self.endtime-self.starttime))*(self.playend-self.playstart)
self.slider.SetCurrent(newval)
self.UpdateCrop()


def OnStop(self, _=None):
self.timer.Stop()
if self.sound is not None:
if guihelper.IsMac():
# There is no stop method for Mac so we play a one centisecond wav
# which cancels the existing playing sound
self.sound=None
sound=wx.Sound(self.zerolenwav)
sound.Play(wx.SOUND_ASYNC)
else:
self.sound.Stop()
self.sound=None

def OnSliderCurrentChanged(self, evt):
self.OnStop()
wx.CallAfter(self.UpdateCrop)

def OnSliderChanging(self, _):
wx.CallAfter(self.UpdateCrop)

def _removetempfiles(self):
for file in self.temporaryfiles:
if os.path.exists(file):
os.remove(file)

def OnOk(self, evt):
self.OnStop()
# make new data
start=self.slider.GetStart()*self.wfi.duration
duration=(self.slider.GetEnd()-self.slider.GetStart())*self.wfi.duration
self.newfiledata=getattr(self, self.PARAMETERS[self.convertinfo.format]['final'])(
start, duration, self.clip_volume.GetValue())
# now remove files
self._removetempfiles()
# use normal handler to quit dialog
evt.Skip()

def OnCancel(self, evt):
self.OnStop()
self._removetempfiles()
evt.Skip()

def OnSetClip(self, _=None):
s=self.clip_start.GetValue()
d=self.clip_duration.GetValue()
e=s+d
if e<=self.wfi.duration:
self.slider.SetStart(s/self.wfi.duration)
self.slider.SetEnd(e/self.wfi.duration)
self.UpdateCrop()

###
### MP3 functions
###

def mp3setup(self):
self.mp3file=common.gettempfilename("mp3")
self.tmp_mp3file=common.gettempfilename('mp3')
self.temporaryfiles.append(self.mp3file)
self.temporaryfiles.append(self.tmp_mp3file)

def mp3convert(self):
# make mp3 to work with
open(self.mp3file, "wb").write(conversions.converttomp3(self.file, int(self.bitrate.GetStringSelection()), int(self.samplerate.GetStringSelection()), int(self.channels.GetStringSelection())))
self.afi=fileinfo.getmp3fileinfo(self.mp3file)
print "result is",len(self.afi.frames),"frames"
# and corresponding wav to play
conversions.converttowav(self.mp3file, self.wavfile)

def mp3filelength(self, duration):
# mp3 specific file length calculation
frames=self.afi.frames
self.beginframe=int(self.slider.GetStart()*len(frames))
self.endframe=int(self.slider.GetEnd()*len(frames))
length=sum([frames[frame].nextoffset-frames[frame].offset for frame in range(self.beginframe, self.endframe)])
return length

def _trim_mp3(self, start, duration):
# mp3 writing out
f=None
try:
frames=self.afi.frames
offset=frames[self.beginframe].offset
length=frames[self.endframe-1].nextoffset-offset
f=open(self.mp3file, "rb", 0)
f.seek(offset)
return f.read(length)
finally:
if f is not None:
f.close()

def _trim_and_adjust_vol_mp3(self, start, duration, volume):
# trim, adjust volume, and write mp3 out
# use the original to make a new wav, not the one that went through foo -> mp3 -> wav
conversions.converttowav(self.file, self.wavfile,
start=start, duration=duration)
# adjust the volume
conversions.adjustwavfilevolume(self.wavfile, volume)
# convert to mp3
return conversions.converttomp3(self.wavfile,
int(self.bitrate.GetStringSelection()),
int(self.samplerate.GetStringSelection()),
int(self.channels.GetStringSelection()))

def mp3final(self, start, duration, volume=None):
if volume:
# need to adjust volume
return self._trim_and_adjust_vol_mp3(start, duration, volume)
else:
return self._trim_mp3(start, duration)

###
### QCP/PureVoice functions
###

def qcpsetup(self):
self.qcpfile=common.gettempfilename("qcp")
self.temporaryfiles.append(self.qcpfile)

def qcpconvert(self):
# we verify the pvconv binary exists first
conversions.getpvconvbinary()
# convert to wav first
conversions.converttowav(self.file, self.wavfile, samplerate=8000, channels=1)
# then to qcp
conversions.convertwavtoqcp(self.wavfile, self.qcpfile,
self.optimization.GetSelection())
# and finally the wav from the qcp so we can accurately hear what it sounds like
conversions.convertqcptowav(self.qcpfile, self.wavfile)

def qcpfilelength(self, duration):
# we don't actually know unless we do a conversion as QCP is
# variable bitrate, so just assume the worst case of 13000
# bits per second (1625 bytes per second) with an additional
# 5% overhead due to the file format (I often see closer to 7%)
return int(duration*1625*1.05)

def qcpfinal(self, start, duration, volume=None):
# use the original to make a new wav, not the one that went through foo -> qcp -> wav
conversions.converttowav(self.file, self.wavfile, samplerate=8000, channels=1, start=start, duration=duration)
if volume is not None:
conversions.adjustwavfilevolume(self.wavfile, volume)
conversions.convertwavtoqcp(self.wavfile, self.qcpfile,
self.optimization.GetSelection())
return open(self.qcpfile, "rb").read()

------=_NextPart_000_0005_01C5CC00.AE4EE700
Content-Type: application/octet-stream;
name="ringers.py.diff"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="ringers.py.diff"

Index: ringers.py
===================================================================
RCS file: /cvsroot/bitpim/bitpim/ringers.py,v
retrieving revision 1.42
diff -u -r1.42 ringers.py
--- ringers.py 19 Sep 2005 22:24:35 -0000 1.42
+++ ringers.py 8 Oct 2005 06:53:51 -0000
@@ -235,7 +235,7 @@
if dlg_resp==wx.ID_NO:
continue
self.thedir=self.mainwindow.ringerpath
- decoded_file=str(file).decode(guiwidgets.media_codec)
+ decoded_file=self.decodefilename(file)
target=self.getshortenedbasename(decoded_file, newext)
open(target, "wb").write(filedata)
self.AddToIndex(str(os.path.basename(target)).decode(guiwidgets.media_codec))

------=_NextPart_000_0005_01C5CC00.AE4EE700
Content-Type: text/plain;
name="wallpaper.py"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="wallpaper.py"

### BITPIM
###
### Copyright (C) 2003-2005 Roger Binns <***@rogerbinns.com>
###
### This program is free software; you can redistribute it and/or modify
### it under the terms of the BitPim license as detailed in the LICENSE file.
###
### $Id: wallpaper.py,v 1.77 2005/09/09 05:28:24 djpham Exp $

"Deals with wallpaper and related views"

# standard modules
import os
import sys
import cStringIO
import random

# wx modules
import wx
import wx.lib.colourselect

# my modules
import conversions
import guiwidgets
import brewcompressedimage
import guihelper
import common
import helpids
import pubsub
import aggregatedisplay
import fileinfo

###
### Wallpaper pane
###

class DisplayItem(guiwidgets.FileViewDisplayItem):

datakey="wallpaper-index"
datatype="Image" # this is used in the tooltip

thewallpapermanager=None

class WallpaperView(guiwidgets.FileView):
CURRENTFILEVERSION=2
ID_DELETEFILE=2
ID_IGNOREFILE=3

# this is only used to prevent the pubsub module
# from being GC while any instance of this class exists
__publisher=pubsub.Publisher

_bitmaptypemapping={
# the extensions we use and corresponding wx types
'bmp': wx.BITMAP_TYPE_BMP,
'jpg': wx.BITMAP_TYPE_JPEG,
'png': wx.BITMAP_TYPE_PNG,
}

organizetypes=("Origin", "Image Type", "File Size") # Image Size
media_notification_type=pubsub.wallpaper_type

def __init__(self, mainwindow, parent):
global thewallpapermanager
thewallpapermanager=self
self.mainwindow=mainwindow
self.usewidth=10
self.useheight=10
wx.FileSystem_AddHandler(BPFSHandler(self))
self._data={'wallpaper-index': {}}
self.updateprofilevariables(self.mainwindow.phoneprofile)

self.organizemenu=wx.Menu()
guiwidgets.FileView.__init__(self, mainwindow, parent, "wallpaper-watermark")

self.wildcard="Image files|*.bmp;*.jpg;*.jpeg;*.png;*.gif;*.pnm;*.tiff;*.ico;*.bci;*.bit"


## self.bgmenu.Insert(1,guihelper.ID_FV_PASTE, "Paste")
## wx.EVT_MENU(self.bgmenu, guihelper.ID_FV_PASTE, self.OnPaste)

self.organizeinfo={}
last_mode=mainwindow.config.Read('imageorganizedby',
self.organizetypes[0])
for k in self.organizetypes:
id=wx.NewId()
self.organizemenu.AppendRadioItem(id, k)
wx.EVT_MENU(self, id, self.OrganizeChange)
self.organizeinfo[id]=getattr(self, "organizeby_"+k.replace(" ",""))
if k==last_mode:
self.organizemenu.Check(id, True)

self.modified=False
wx.EVT_IDLE(self, self.OnIdle)
pubsub.subscribe(self.OnListRequest, pubsub.REQUEST_WALLPAPERS)
pubsub.subscribe(self.OnPhoneModelChanged, pubsub.PHONE_MODEL_CHANGED)
self._raw_image=self._shift_down=False
wx.EVT_KEY_DOWN(self.aggdisp, self._OnKey)
wx.EVT_KEY_UP(self.aggdisp, self._OnKey)

def OnPhoneModelChanged(self, msg):
phonemodule=msg.data
self.updateprofilevariables(phonemodule.Profile)
self.OnRefresh()

def updateprofilevariables(self, profile):
self.usewidth=profile.WALLPAPER_WIDTH
self.useheight=profile.WALLPAPER_HEIGHT
self.maxlen=profile.MAX_WALLPAPER_BASENAME_LENGTH
self.filenamechars=profile.WALLPAPER_FILENAME_CHARS
self.convertextension=profile.WALLPAPER_CONVERT_FORMAT
self.convertwxbitmaptype=self._bitmaptypemapping[self.convertextension.lower()]
if hasattr(profile,"OVERSIZE_PERCENTAGE"):
self.woversize_percentage=profile.OVERSIZE_PERCENTAGE
self.hoversize_percentage=profile.OVERSIZE_PERCENTAGE
else:
self.woversize_percentage=120
self.hoversize_percentage=120

def OnListRequest(self, msg=None):
# temporaty quick-fix to not include video items in the list!
l=[self._data['wallpaper-index'][x]['name'] \
for x in self._data['wallpaper-index']\
if self._data['wallpaper-index'][x].get('origin', None)!='video' ]
l.sort()
pubsub.publish(pubsub.ALL_WALLPAPERS, l)

def OnIdle(self, _):
"Save out changed data"
if self.modified:
self.modified=False
self.populatefs(self._data)
self.OnListRequest() # broadcast changes

def _OnKey(self, evt):
self._shift_down=evt.ShiftDown()
evt.Skip()

def OnAdd(self, evt=None):
self._raw_image=self._shift_down
super(WallpaperView, self).OnAdd(evt)
# reset the fla
self._shift_down=False

def OrganizeChange(self, evt):
self.mainwindow.config.Write('imageorganizedby',
evt.GetEventObject().GetLabel(evt.GetId()))
evt.GetEventObject().Check(evt.GetId(), True)
self.OnRefresh()

def GetSections(self):
# get all the items
items=[DisplayItem(self, key, self.mainwindow.wallpaperpath) for key in self._data['wallpaper-index']]
# prune out ones we don't have images for
items=[item for item in items if os.path.exists(item.filename)]

self.sections=[]

if len(items)==0:
return self.sections

# get the current sorting type
for i in range(len(self.organizetypes)):
item=self.organizemenu.FindItemByPosition(i)
if self.organizemenu.IsChecked(item.GetId()):
for sectionlabel, items in self.organizeinfo[item.GetId()](items):
sh=aggregatedisplay.SectionHeader(sectionlabel)
sh.itemsize=(self.usewidth+120, self.useheight+DisplayItem.PADDING*2)
for item in items:
item.thumbnailsize=self.usewidth, self.useheight
# sort items by name
items=[(item.name.lower(), item) for item in items]
items.sort()
items=[item for name,item in items]
self.sections.append( (sh, items) )
return [sh for sh,items in self.sections]
assert False, "Can't get here"

def GetItemSize(self, sectionnumber, sectionheader):
return sectionheader.itemsize

def GetItemsFromSection(self, sectionnumber, sectionheader):
return self.sections[sectionnumber][1]

def organizeby_ImageType(self, items):
types={}
for item in items:
t=item.fileinfo.format
if t is None: t="<Unknown>"
l=types.get(t, [])
l.append(item)
types[t]=l

keys=types.keys()
keys.sort()
return [ (key, types[key]) for key in types]

def organizeby_Origin(self, items):
types={}
for item in items:
t=item.origin
if t is None: t="Default"
l=types.get(t, [])
l.append(item)
types[t]=l

keys=types.keys()
keys.sort()
return [ (key, types[key]) for key in types]

def organizeby_FileSize(self, items):

sizes={0: ('Less than 8kb', []),
8192: ('8 kilobytes', []),
16384: ('16 kilobytes', []),
32768: ('32 kilobytes', []),
65536: ('64 kilobytes', []),
131052: ('128 kilobytes', []),
524208: ('512 kilobytes', []),
1024*1024: ('One megabyte', [])}

keys=sizes.keys()
keys.sort()

for item in items:
t=item.size
if t>=keys[-1]:
sizes[keys[-1]][1].append(item)
continue
for i,k in enumerate(keys):
if t<keys[i+1]:
sizes[k][1].append(item)
break

return [sizes[k] for k in keys if len(sizes[k][1])]


def isBCI(self, filename):
"""Returns True if the file is a Brew Compressed Image"""
# is it a bci file?
return open(filename, "rb").read(4)=="BCI\x00"

def getdata(self,dict,want=guiwidgets.FileView.NONE):
return self.genericgetdata(dict, want, self.mainwindow.wallpaperpath, 'wallpapers', 'wallpaper-index')

def RemoveFromIndex(self, names):
for name in names:
wp=self._data['wallpaper-index']
for k in wp.keys():
if wp[k]['name']==name:
del wp[k]
self.modified=True

def GetItemThumbnail(self, name, width, height):
img,_=self.GetImage(name.encode(guiwidgets.media_codec))
if img is None or not img.Ok():
# unknown image file, display wallpaper.png
img=wx.Image(guihelper.getresourcefile('wallpaper.png'))
if width!=img.GetWidth() or height!=img.GetHeight():
# scale the image.
sfactorw=float(width)/img.GetWidth()
sfactorh=float(height)/img.GetHeight()
sfactor=min(sfactorw,sfactorh) # preserve aspect ratio
newwidth=int(img.GetWidth()*sfactor)
newheight=int(img.GetHeight()*sfactor)
img.Rescale(newwidth, newheight)
bitmap=img.ConvertToBitmap()
return bitmap

def GetImage(self, file):
"""Gets the named image

@return: (wxImage, filesize)
"""
file,cons = self.GetImageConstructionInformation(file)

return cons(file), int(os.stat(file).st_size)

# This function exists because of the constraints of the HTML
# filesystem stuff. The previous code was reading in the file to
# a wx.Image, saving it as a PNG to disk (wx.Bitmap.SaveFile
# doesn't have save to memory implemented), reading it back from
# disk and supplying it to the HTML code. Needless to say that
# involves unnecessary conversions and gets slower with larger
# images. We supply the info so that callers can make the minimum
# number of conversions possible

def GetImageConstructionInformation(self, file):
"""Gets information for constructing an Image from the file

@return: (filename to use, function to call that returns wxImage)
"""
file=os.path.join(self.mainwindow.wallpaperpath, file)
fi=self.GetFileInfo(file)
if file.endswith(".mp4") or not os.path.isfile(file):
return guihelper.getresourcefile('wallpaper.png'), wx.Image
if self.isBCI(file):
return file, lambda name: brewcompressedimage.getimage(brewcompressedimage.FileInputStream(file))
if fi is not None and fi.format=='AVI':
# return the 1st frame of the AVI file
return file, conversions.convertavitobmp
# LG phones may return a proprietary wallpaper media file, LGBIT
if fi is not None and fi.format=='LGBIT':
return file, conversions.convertfilelgbittobmp
return file, wx.Image

def GetFileInfo(self, filename):
return fileinfo.identify_imagefile(filename)

def GetImageStatInformation(self, file):
"""Returns the statinfo for file"""
file=os.path.join(self.mainwindow.wallpaperpath, file)
return statinfo(file)

def updateindex(self, index):
if index!=self._data['wallpaper-index']:
self._data['wallpaper-index']=index.copy()
self.modified=True

def populate(self, dict):
if self._data['wallpaper-index']!=dict['wallpaper-index']:
self._data['wallpaper-index']=dict['wallpaper-index'].copy()
self.modified=True
self.OnRefresh()

def OnPaste(self, evt=None):
super(WallpaperView, self).OnPaste(evt)
if not wx.TheClipboard.Open():
# can't access the clipboard
return
if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_BITMAP)):
do=wx.BitmapDataObject()
success=wx.TheClipboard.GetData(do)
else:
success=False
wx.TheClipboard.Close()
if success:
# work out a name for it
self.OnAddImage(wx.ImageFromBitmap(do.GetBitmap()), None)

def CanPaste(self):
""" Return True if can accept clipboard data, False otherwise
"""
if not wx.TheClipboard.Open():
return False
r=wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_FILENAME)) or\
wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_BITMAP))
wx.TheClipboard.Close()
return r

def AddToIndex(self, file, origin):
for i in self._data['wallpaper-index']:
if self._data['wallpaper-index'][i]['name']==file:
self._data['wallpaper-index'][i]['origin']=origin
return
keys=self._data['wallpaper-index'].keys()
idx=10000
while idx in keys:
idx+=1
self._data['wallpaper-index'][idx]={'name': file, 'origin': origin}
self.modified=True

def OnAddFiles(self, filenames):
for file in filenames:
if self._raw_image:
decoded_file=self.decodefilename(file)
targetfilename=self.getshortenedbasename(decoded_file)
open(targetfilename, 'wb').write(open(file, 'rb').read())
self.AddToIndex(str(os.path.basename(targetfilename)).decode(guiwidgets.media_codec),
'images')
else:
# :::TODO:: do i need to handle bci specially here?
# The proper way to handle custom image types, e.g. BCI and LGBIT,
# is to add a wx.ImageHandler for it. Unfortunately wx.Image_AddHandler
# is broken in the current wxPython, so . . .
fi=self.GetFileInfo(file)
if fi is not None and fi.format in ('LGBIT', 'BCI'):
if fi.format=='LGBIT':
img=conversions.convertfilelgbittobmp(file)
else:
img=brewcompressedimage.getimage(brewcompressedimage.FileInputStream(file))
else:
img=wx.Image(file)
if not img.Ok():
dlg=wx.MessageDialog(self, "Failed to understand the image in '"+file+"'",
"Image not understood", style=wx.OK|wx.ICON_ERROR)
dlg.ShowModal()
continue
self.OnAddImage(img,file,refresh=False)
self.OnRefresh()


def OnAddImage(self, img, file, refresh=True):
# ::TODO:: if file is None, find next basename in our directory for
# clipboard99 where 99 is next unused number

dlg=ImagePreviewDialog(self, img, file, self.mainwindow.phoneprofile)
if dlg.ShowModal()!=wx.ID_OK:
dlg.Destroy()
return

img=dlg.GetResultImage()
imgparams=dlg.GetResultParams()
origin=dlg.GetResultOrigin()

# ::TODO:: temporary hack - this should really be an imgparam
extension={'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'}[imgparams['format']]

# munge name
decoded_file=self.decodefilename(file)
targetfilename=self.getshortenedbasename(decoded_file, extension)

res=getattr(self, "saveimage_"+imgparams['format'])(
img,
targetfilename, imgparams)
if not res:
try: os.remove(targetfilename)
except: pass
dlg=wx.MessageDialog(self, "Failed to convert the image in '"+file+"'",
"Image not converted", style=wx.OK|wx.ICON_ERROR)
dlg.ShowModal()
return

self.AddToIndex(str(os.path.basename(targetfilename)).decode(guiwidgets.media_codec), origin)
if refresh:
self.OnRefresh()

def saveimage_BMP(self, img, targetfilename, imgparams):
if img.ComputeHistogram(wx.ImageHistogram())<=236: # quantize only does 236 or less
img.SetOptionInt(wx.IMAGE_OPTION_BMP_FORMAT, wx.BMP_8BPP)
return img.SaveFile(targetfilename, wx.BITMAP_TYPE_BMP)

def saveimage_JPEG(self, img, targetfilename, imgparams):
img.SetOptionInt("quality", 100)
return img.SaveFile(targetfilename, wx.BITMAP_TYPE_JPEG)

def saveimage_PNG(self, img, targetfilename, imgparams):
# ::TODO:: this is where the file size constraints should be examined
# and obeyed
return img.SaveFile(targetfilename, wx.BITMAP_TYPE_PNG)

def populatefs(self, dict):
self.thedir=self.mainwindow.wallpaperpath
return self.genericpopulatefs(dict, 'wallpapers', 'wallpaper-index', self.CURRENTFILEVERSION)

def getfromfs(self, result):
self.thedir=self.mainwindow.wallpaperpath
return self.genericgetfromfs(result, None, 'wallpaper-index', self.CURRENTFILEVERSION)

def versionupgrade(self, dict, version):
"""Upgrade old data format read from disk

@param dict: The dict that was read in
@param version: version number of the data on disk
"""

# version 0 to 1 upgrade
if version==0:
version=1 # they are the same

# 1 to 2 etc
if version==1:
print "converting to version 2"
version=2
d={}
input=dict.get('wallpaper-index', {})
for i in input:
d[i]={'name': input[i]}
dict['wallpaper-index']=d
return dict

class WallpaperPreview(wx.PyWindow):

def __init__(self, parent, image=None, id=1, size=wx.DefaultSize, pos=wx.DefaultPosition, style=0):
wx.PyWindow.__init__(self, parent, id=id, size=size, pos=pos, style=style|wx.FULL_REPAINT_ON_RESIZE)
self.bg=wx.Brush(parent.GetBackgroundColour())
self._bufbmp=None

wx.EVT_ERASE_BACKGROUND(self, lambda evt: None)
wx.EVT_PAINT(self, self.OnPaint)

self.SetImage(image)

def SetImage(self, name):
if name is None:
self.theimage=None
else:
self.theimage, _=thewallpapermanager.GetImage(name)
self.thesizedbitmap=None
self.Refresh(False)

def OnPaint(self, _):
sz=self.GetClientSize()
if self._bufbmp is None or sz.width>self._bufbmp.GetWidth() or sz.height>self._bufbmp.GetHeight():
self._bufbmp=wx.EmptyBitmap((sz.width+64)&~8, (sz.height+64)&~8)
dc=wx.BufferedPaintDC(self, self._bufbmp, style=wx.BUFFER_VIRTUAL_AREA)
dc.SetBackground(self.bg)
dc.Clear()
if self.theimage is None: return
# work out what size the scaled bitmap should be to retain its aspect ratio and fit within sz
sfactorw=float(sz.width)/self.theimage.GetWidth()
sfactorh=float(sz.height)/self.theimage.GetHeight()
sfactor=min(sfactorw,sfactorh)
newwidth=int(self.theimage.GetWidth()*sfactor)
newheight=int(self.theimage.GetHeight()*sfactor)
if self.thesizedbitmap is None or self.thesizedbitmap.GetWidth()!=newwidth or \
self.thesizedbitmap.GetHeight()!=newheight:
self.thesizedbitmap=self.theimage.Scale(newwidth, newheight).ConvertToBitmap()
dc.DrawBitmap(self.thesizedbitmap, sz.width/2-newwidth/2, sz.height/2-newheight/2, True)




def ScaleImageIntoBitmap(img, usewidth, useheight, bgcolor=None, valign="center"):
"""Scales the image and returns a bitmap

@param usewidth: the width of the new image
@param useheight: the height of the new image
@param bgcolor: the background colour as a string ("ff0000" is red etc). If this
is none then the background is made transparent"""
if bgcolor is None:
bitmap=wx.EmptyBitmap(usewidth, useheight, 24) # have to use 24 bit for transparent background
else:
bitmap=wx.EmptyBitmap(usewidth, useheight)
mdc=wx.MemoryDC()
mdc.SelectObject(bitmap)
# scale the source.
sfactorw=usewidth*1.0/img.GetWidth()
sfactorh=useheight*1.0/img.GetHeight()
sfactor=min(sfactorw,sfactorh) # preserve aspect ratio
newwidth=int(img.GetWidth()*sfactor/1.0)
newheight=int(img.GetHeight()*sfactor/1.0)

img.Rescale(newwidth, newheight)
# deal with bgcolor/transparency
if bgcolor is not None:
transparent=None
assert len(bgcolor)==6
red=int(bgcolor[0:2],16)
green=int(bgcolor[2:4],16)
blue=int(bgcolor[4:6],16)
mdc.SetBackground(wx.TheBrushList.FindOrCreateBrush(wx.Colour(red,green,blue), wx.SOLID))
else:
transparent=wx.Colour(*(img.FindFirstUnusedColour()[1:]))
mdc.SetBackground(wx.TheBrushList.FindOrCreateBrush(transparent, wx.SOLID))
mdc.Clear()
mdc.SelectObject(bitmap)
# figure where to place image to centre it
posx=usewidth-(usewidth+newwidth)/2
if valign in ("top", "clip"):
posy=0
elif valign=="center":
posy=useheight-(useheight+newheight)/2
else:
assert False, "bad valign "+valign
posy=0
# draw the image
mdc.DrawBitmap(img.ConvertToBitmap(), posx, posy, True)
# clean up
mdc.SelectObject(wx.NullBitmap)
# deal with transparency
if transparent is not None:
mask=wx.Mask(bitmap, transparent)
bitmap.SetMask(mask)
if valign=="clip" and newheight!=useheight:
return bitmap.GetSubBitmap( (0,0,usewidth,newheight) )
return bitmap

###
### Virtual filesystem where the images etc come from for the HTML stuff
###

statinfo=common.statinfo

class BPFSHandler(wx.FileSystemHandler):

CACHELOWWATER=80
CACHEHIGHWATER=100

def __init__(self, wallpapermanager):
wx.FileSystemHandler.__init__(self)
self.wpm=wallpapermanager
self.cache={}

def _GetCache(self, location, statinfo):
"""Return the cached item, or None

Note that the location value includes the filename and the parameters such as width/height
"""
if statinfo is None:
print "bad location",location
return None
return self.cache.get( (location, statinfo), None)

def _AddCache(self, location, statinfo, value):
"Add the item to the cache"
# we also prune it down in size if necessary
if len(self.cache)>=self.CACHEHIGHWATER:
print "BPFSHandler cache flush"
# random replacement - almost as good as LRU ...
while len(self.cache)>self.CACHELOWWATER:
del self.cache[random.choice(self.cache.keys())]
self.cache[(location, statinfo)]=value

def CanOpen(self, location):

# The call to self.GetProtocol causes an exception if the
# location starts with a pathname! This typically happens
# when the help file is opened. So we work around that bug
# with this quick check.

if location.startswith("/"):
return False
proto=self.GetProtocol(location)
if proto=="bpimage" or proto=="bpuserimage":
return True
return False

def OpenFile(self,filesystem,location):
try:
res=self._OpenFile(filesystem,location)
except:
res=None
print "Exception in getting image file - you can't do that!"
print common.formatexception()
if res is not None:
# we have to seek the file object back to the begining and make a new
# wx.FSFile each time as wxPython doesn't do the reference counting
# correctly
res[0].seek(0)
args=(wx.InputStream(res[0]),)+res[1:]
res=wx.FSFile(*args)
return res

def _OpenFile(self, filesystem, location):
proto=self.GetProtocol(location)
r=self.GetRightLocation(location)
params=r.split(';')
r=params[0]
params=params[1:]
p={}
for param in params:
x=param.find('=')
key=str(param[:x])
value=param[x+1:]
if key=='width' or key=='height':
p[key]=int(value)
else:
p[key]=value
if proto=="bpimage":
return self.OpenBPImageFile(location, r, **p)
elif proto=="bpuserimage":
return self.OpenBPUserImageFile(location, r, **p)
return None

def OpenBPUserImageFile(self, location, name, **kwargs):
si=self.wpm.GetImageStatInformation(name)
res=self._GetCache(location, si)
if res is not None: return res
file,cons=self.wpm.GetImageConstructionInformation(name)
if cons == wx.Image:
res=BPFSImageFile(self, location, file, **kwargs)
else:
res=BPFSImageFile(self, location, img=cons(file), **kwargs)
self._AddCache(location, si, res)
return res

def OpenBPImageFile(self, location, name, **kwargs):
f=guihelper.getresourcefile(name)
if not os.path.isfile(f):
print f,"doesn't exist"
return None
si=statinfo(f)
res=self._GetCache(location, si)
if res is not None: return res
res=BPFSImageFile(self, location, name=f, **kwargs)
self._AddCache(location, si, res)
return res

def BPFSImageFile(fshandler, location, name=None, img=None, width=-1, height=-1, valign="center", bgcolor=None):
"""Handles image files

If we have to do any conversion on the file then we return PNG
data. This used to be a class derived from wx.FSFile, but due to
various wxPython bugs it instead returns the parameters to make a
wx.FSFile since a new one has to be made every time.
"""
# special fast path if we aren't resizing or converting image
if img is None and width<0 and height<0:
mime=guihelper.getwxmimetype(name)
# wxPython 2.5.3 has a new bug and fails to read bmp files returned as a stream
if mime not in (None, "image/x-bmp"):
return (open(name, "rb"), location, mime, "", wx.DateTime_Now())

if img is None:
img=wx.Image(name)

if width>0 and height>0:
b=ScaleImageIntoBitmap(img, width, height, bgcolor, valign)
else:
b=img.ConvertToBitmap()

f=common.gettempfilename("png")
if not b.SaveFile(f, wx.BITMAP_TYPE_PNG):
raise Exception, "Saving to png failed"

data=open(f, "rb").read()
os.remove(f)

return (cStringIO.StringIO(data), location, "image/png", "", wx.DateTime_Now())


class ImageCropSelect(wx.ScrolledWindow):

def __init__(self, parent, image, previewwindow=None, id=1, resultsize=(100,100), size=wx.DefaultSize, pos=wx.DefaultPosition, style=0):
wx.ScrolledWindow.__init__(self, parent, id=id, size=size, pos=pos, style=style|wx.FULL_REPAINT_ON_RESIZE)
self.previewwindow=previewwindow
self.bg=wx.Brush(wx.WHITE)
self.parentbg=wx.Brush(parent.GetBackgroundColour())
self._bufbmp=None

self.anchors=None

wx.EVT_ERASE_BACKGROUND(self, lambda evt: None)
wx.EVT_PAINT(self, self.OnPaint)

self.image=image
self.origimage=image
self.setresultsize(resultsize)

# cursors for outside, inside, on selection, pressing bad mouse button
self.cursors=[wx.StockCursor(c) for c in (wx.CURSOR_ARROW, wx.CURSOR_HAND, wx.CURSOR_SIZING, wx.CURSOR_NO_ENTRY)]
self.clickpoint=None
wx.EVT_MOTION(self, self.OnMotion)
wx.EVT_LEFT_DOWN(self, self.OnLeftDown)
wx.EVT_LEFT_UP(self, self.OnLeftUp)

def SetPreviewWindow(self, previewwindow):
self.previewwindow=previewwindow

def OnPaint(self, _):
sz=self.thebmp.GetWidth(), self.thebmp.GetHeight()
sz2=self.GetClientSize()
sz=max(sz[0],sz2[0])+32,max(sz[1],sz2[1])+32
if self._bufbmp is None or self._bufbmp.GetWidth()<sz[0] or self._bufbmp.GetHeight()<sz[1]:
self._bufbmp=wx.EmptyBitmap((sz[0]+64)&~8, (sz[1]+64)&~8)
dc=wx.BufferedPaintDC(self, self._bufbmp, style=wx.BUFFER_VIRTUAL_AREA)
if sz2[0]<sz[0] or sz2[1]<sz[1]:
dc.SetBackground(self.parentbg)
dc.Clear()
dc.DrawBitmap(self.thebmp, 0, 0, False)
# draw bounding box next
l,t,r,b=self.anchors
points=(l,t), (r,t), (r,b), (l,b)
dc.DrawLines( points+(points[0],) )
for x,y in points:
dc.DrawRectangle(x-5, y-5, 10, 10)

OUTSIDE=0
INSIDE=1
HANDLE_LT=2
HANDLE_RT=3
HANDLE_RB=4
HANDLE_LB=5

def _hittest(self, evt):
l,t,r,b=self.anchors
within=lambda x,y,l,t,r,b: l<=x<=r and t<=y<=b
x,y=self.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
for i,(ptx,pty) in enumerate(((l,t), (r,t), (r,b), (l,b))):
if within(x,y,ptx-5, pty-5, ptx+5,pty+5):
return self.HANDLE_LT+i
if within(x,y,l,t,r,b):
return self.INSIDE
return self.OUTSIDE

def OnMotion(self, evt):
if evt.Dragging():
return self.OnMotionDragging(evt)
self.UpdateCursor(evt)

def UpdateCursor(self, evt):
ht=self._hittest(evt)
self.SetCursor(self.cursors[min(2,ht)])

def OnMotionDragging(self, evt):
if not evt.LeftIsDown() or self.clickpoint is None:
self.SetCursor(self.cursors[3])
return
xx,yy=self.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
deltax=xx-self.origevtpos[0]
deltay=yy-self.origevtpos[1]

if self.clickpoint==self.INSIDE:
newanchors=self.origanchors[0]+deltax, self.origanchors[1]+deltay, \
self.origanchors[2]+deltax, self.origanchors[3]+deltay
iw=self.dimensions[0]
ih=self.dimensions[1]
# would box be out of bounds?
if newanchors[0]<0:
newanchors=0,newanchors[1], self.origanchors[2]-self.origanchors[0], newanchors[3]
if newanchors[1]<0:
newanchors=newanchors[0], 0, newanchors[2], self.origanchors[3]-self.origanchors[1]
if newanchors[2]>iw:
newanchors=iw-(self.origanchors[2]-self.origanchors[0]),newanchors[1],iw, newanchors[3]
if newanchors[3]>ih:
newanchors=newanchors[0],ih-(self.origanchors[3]-self.origanchors[1]), newanchors[2],ih
self.anchors=newanchors
self.Refresh(False)
self.updatepreview()
return
# work out how to do this with left top and then expand code
if self.clickpoint==self.HANDLE_LT:
aa=0,1,-1,-1
elif self.clickpoint==self.HANDLE_RT:
aa=2,1,+1,-1
elif self.clickpoint==self.HANDLE_RB:
aa=2,3,+1,+1
elif self.clickpoint==self.HANDLE_LB:
aa=0,3,-1,+1
else:
assert False, "can't get here"

na=[self.origanchors[0],self.origanchors[1],self.origanchors[2],self.origanchors[3]]
na[aa[0]]=na[aa[0]]+deltax
na[aa[1]]=na[aa[1]]+deltay
neww=na[2]-na[0]
newh=na[3]-na[1]
ar=float(neww)/newh
if ar<self.aspectratio:
na[aa[0]]=na[aa[0]]+(self.aspectratio*newh-neww)*aa[2]
elif ar>self.aspectratio:
na[aa[1]]=na[aa[1]]+(neww/self.aspectratio-newh)*aa[3]

# ignore if image would be smaller than 10 pixels in any direction
if neww<10 or newh<10:
return
# if any point is off screen, we need to fix things up
if na[0]<0:
xdiff=-na[0]
ydiff=xdiff/self.aspectratio
na[0]=0
na[1]+=ydiff
if na[1]<0:
ydiff=-na[1]
xdiff=ydiff*self.aspectratio
na[1]=0
na[0]-=xdiff
if na[2]>self.dimensions[0]:
xdiff=na[2]-self.dimensions[0]
ydiff=xdiff/self.aspectratio
na[2]=na[2]-xdiff
na[3]=na[3]-ydiff
if na[3]>self.dimensions[1]:
ydiff=na[3]-self.dimensions[1]
xdiff=ydiff*self.aspectratio
na[2]=na[2]-xdiff
na[3]=na[3]-ydiff
if na[0]<0 or na[1]<0 or na[2]>self.dimensions[0] or na[3]>self.dimensions[1]:
print "offscreen fixup not written yet"
return

# work out aspect ratio
self.anchors=na
self.Refresh(False)
self.updatepreview()
return


def OnLeftDown(self, evt):
ht=self._hittest(evt)
if ht==self.OUTSIDE:
self.SetCursor(self.cursors[3])
return
self.clickpoint=ht
xx,yy=self.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
self.origevtpos=xx,yy
self.origanchors=self.anchors

def OnLeftUp(self, evt):
self.clickpoint=None
self.UpdateCursor(evt)

def setlbcolour(self, colour):
self.bg=wx.Brush(colour)
self.remakebitmap()

def SetZoom(self, factor):
curzoom=float(self.image.GetWidth())/self.origimage.GetWidth()
self.anchors=[a*factor/curzoom for a in self.anchors]
self.image=self.origimage.Scale(self.origimage.GetWidth()*factor, self.origimage.GetHeight()*factor)
self.setresultsize(self.resultsize)

def setresultsize(self, (w,h)):
self.resultsize=w,h
self.aspectratio=ratio=float(w)/h
imgratio=float(self.image.GetWidth())/self.image.GetHeight()

neww=self.image.GetWidth()
newh=self.image.GetHeight()
if imgratio<ratio:
neww*=ratio/imgratio
elif imgratio>ratio:
newh*=imgratio/ratio

# ensure a minimum size
neww=max(neww, 50)
newh=max(newh, 50)

# update anchors if never set
if self.anchors==None:
self.anchors=0.1 * neww, 0.1 * newh, 0.9 * neww, 0.9 * newh

# fixup anchors
l,t,r,b=self.anchors
l=min(neww-40, l)
r=min(neww-10, r)
if r-l<20: r=40
t=min(newh-40, t)
b=min(newh-10, b)
if b-t<20: b=40
aratio=float(r-l)/(b-t)
if aratio<ratio:
b=t+(r-l)/ratio
elif aratio>ratio:
r=l+(b-t)*ratio
self.anchors=l,t,r,b

self.dimensions=neww,newh
self.thebmp=wx.EmptyBitmap(neww, newh)

self.remakebitmap()


def remakebitmap(self):
w,h=self.dimensions
dc=wx.MemoryDC()
dc.SelectObject(self.thebmp)
dc.SetBackground(self.bg)
dc.Clear()
dc.DrawBitmap(self.image.ConvertToBitmap(), w/2-self.image.GetWidth()/2, h/2-self.image.GetHeight()/2, True)
dc.SelectObject(wx.NullBitmap)
self.imageofthebmp=None
self.SetVirtualSize( (w, h) )
self.SetScrollRate(1,1)

self.updatepreview()
self.Refresh(False)

# updating the preview is expensive so it is done on demand. We
# tell the preview window there has been an update and it calls
# back from its paint method
def updatepreview(self):
if self.previewwindow:
self.previewwindow.SetUpdated(self.GetPreview)

def GetPreview(self):
w,h=self.resultsize
l,t,r,b=self.anchors
scale=max(float(w+0.99999)/(r-l), float(h+0.99999)/(b-t))

# we are fine using this to scale down
if True and scale<1:
sub=wx.EmptyBitmap(w,h)
mdcsub=wx.MemoryDC()
mdcsub.SelectObject(sub)
mdcsub.SetUserScale(scale, scale)
mdc=wx.MemoryDC()
mdc.SelectObject(self.thebmp)
mdcsub.Blit(0,0,r-l,b-t,mdc,l,t)

mdc.SelectObject(wx.NullBitmap)
mdcsub.SelectObject(wx.NullBitmap)
return sub

sub=self.thebmp.GetSubBitmap( (l,t,(r-l),(b-t)) )
sub=sub.ConvertToImage()
sub.Rescale(w,h)
return sub.ConvertToBitmap()

class ImagePreview(wx.PyWindow):

def __init__(self, parent):
wx.PyWindow.__init__(self, parent)
wx.EVT_ERASE_BACKGROUND(self, lambda evt: None)
wx.EVT_PAINT(self, self.OnPaint)
self.bmp=wx.EmptyBitmap(1,1)
self.updater=None

def SetUpdated(self, updater):
self.updater=updater
self.Refresh(True)

def OnPaint(self, _):
if self.updater is not None:
self.bmp=self.updater()
self.updater=None
dc=wx.PaintDC(self)
dc.DrawBitmap(self.bmp, 0, 0, False)


class ImagePreviewDialog(wx.Dialog):

SCALES=[ (0.25, "1/4"),
(0.5, "1/2"),
(1, "1"),
(2, "2"),
(4, "4")]

def __init__(self, parent, image, filename, phoneprofile):
wx.Dialog.__init__(self, parent, -1, "Image Preview", style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.SYSTEM_MENU|wx.MAXIMIZE_BOX|wx.MINIMIZE_BOX)
self.phoneprofile=phoneprofile
self.filename=filename
self.image=image

vbsouter=wx.BoxSizer(wx.VERTICAL)

hbs=wx.BoxSizer(wx.HORIZONTAL)
self.cropselect=ImageCropSelect(self, image)

vbs=wx.BoxSizer(wx.VERTICAL)
self.colourselect=wx.lib.colourselect.ColourSelect(self, wx.NewId(), "Background ...", (255,255,255))
vbs.Add(self.colourselect, 0, wx.ALL|wx.EXPAND, 5)
wx.lib.colourselect.EVT_COLOURSELECT(self, self.colourselect.GetId(), self.OnBackgroundColour)
vbs.Add(wx.StaticText(self, -1, "Origin"), 0, wx.ALL, 5)
self.originbox=wx.ListBox(self, size=(-1, 100))
vbs.Add(self.originbox, 0, wx.ALL|wx.EXPAND, 5)
vbs.Add(wx.StaticText(self, -1, "Target"), 0, wx.ALL, 5)
self.targetbox=wx.ListBox(self, size=(-1,100))
vbs.Add(self.targetbox, 0, wx.EXPAND|wx.ALL, 5)
vbs.Add(wx.StaticText(self, -1, "Scale"), 0, wx.ALL, 5)

for one,(s,_) in enumerate(self.SCALES):
if s==1: break
self.slider=wx.Slider(self, -1, one, 0, len(self.SCALES)-1, style=wx.HORIZONTAL|wx.SL_AUTOTICKS)
wx.EVT_SCROLL(self, self.SetZoom)
vbs.Add(self.slider, 0, wx.ALL|wx.EXPAND, 5)
self.zoomlabel=wx.StaticText(self, -1, self.SCALES[one][1])
vbs.Add(self.zoomlabel, 0, wx.ALL|wx.ALIGN_CENTRE_HORIZONTAL, 5)

vbs.Add(wx.StaticText(self, -1, "Preview"), 0, wx.ALL, 5)
self.imagepreview=ImagePreview(self)
self.cropselect.SetPreviewWindow(self.imagepreview)
vbs.Add(self.imagepreview, 0, wx.ALL, 5)


hbs.Add(vbs, 0, wx.ALL, 5)
hbs.Add(self.cropselect, 1, wx.ALL|wx.EXPAND, 5)

vbsouter.Add(hbs, 1, wx.EXPAND|wx.ALL, 5)

vbsouter.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5)
vbsouter.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTRE|wx.ALL, 5)

wx.EVT_LISTBOX(self, self.originbox.GetId(), self.OnOriginSelect)
wx.EVT_LISTBOX_DCLICK(self, self.originbox.GetId(), self.OnOriginSelect)

wx.EVT_LISTBOX(self, self.targetbox.GetId(), self.OnTargetSelect)
wx.EVT_LISTBOX_DCLICK(self, self.targetbox.GetId(), self.OnTargetSelect)

wx.EVT_BUTTON(self, wx.ID_HELP, lambda _:
wx.GetApp().displayhelpid(helpids.ID_DLG_IMAGEPREVIEW))
self.originbox.Set(phoneprofile.GetImageOrigins().keys())
self.originbox.SetSelection(0)
self.OnOriginSelect(None)

self.SetSizer(vbsouter)
vbsouter.Fit(self)
guiwidgets.set_size("wallpaperpreview", self, 80, 1.0)

def ShowModal(self):
res=wx.Dialog.ShowModal(self)
guiwidgets.save_size("wallpaperpreview", self.GetRect())
return res

def SetZoom(self, evt):
self.cropselect.SetZoom(self.SCALES[evt.GetPosition()][0])
self.zoomlabel.SetLabel(self.SCALES[evt.GetPosition()][1])
return

def OnBackgroundColour(self, evt):
self.cropselect.setlbcolour(evt.GetValue())

def OnOriginSelect(self, _):
v=self.originbox.GetStringSelection()
assert v is not None
t=self.targetbox.GetStringSelection()
self.targets=self.phoneprofile.GetTargetsForImageOrigin(v)
keys=self.targets.keys()
keys.sort()
self.targetbox.Set(keys)
if t in keys:
self.targetbox.SetSelection(keys.index(t))
else:
self.targetbox.SetSelection(0)
self.OnTargetSelect(None)

def OnTargetSelect(self, _):
v=self.targetbox.GetStringSelection()
print "target is",v
w,h=self.targets[v]['width'],self.targets[v]['height']
self.imagepreview.SetSize( (w,h) )
self.cropselect.setresultsize( (w, h) )
sz=self.GetSizer()
if sz is not None:
# sizer doesn't autmatically size when we change preview size
# so this forces that, as well as the repaint due to screen corruption
sz.Layout()
self.Refresh(True)

def GetResultImage(self):
return self.imagepreview.bmp.ConvertToImage()

def GetResultParams(self):
return self.targets[self.targetbox.GetStringSelection()]

def GetResultOrigin(self):
return self.originbox.GetStringSelection()



if __name__=='__main__':

if __debug__:
def profile(filename, command):
import hotshot, hotshot.stats, os
file=os.path.abspath(filename)
profile=hotshot.Profile(file)
profile.run(command)
profile.close()
del profile
howmany=100
stats=hotshot.stats.load(file)
stats.strip_dirs()
stats.sort_stats('time', 'calls')
stats.print_stats(100)
stats.sort_stats('cum', 'calls')
stats.print_stats(100)
stats.sort_stats('calls', 'time')
stats.print_stats(100)
sys.exit(0)

class FakeProfile:

def GetImageOrigins(self):
return {"images": {'description': 'General images'},
"mms": {'description': 'Multimedia Messages'},
"camera": {'description': 'Camera images'}}

def GetTargetsForImageOrigin(self, origin):
return {"wallpaper": {'width': 100, 'height': 200, 'description': 'Display as wallpaper'},
"photoid": {'width': 100, 'height': 150, 'description': 'Display as photo id'},
"outsidelcd": {'width': 90, 'height': 80, 'description': 'Display on outside screen'}}

def run():
app=wx.PySimpleApp()
dlg=ImagePreviewDialog(None, wx.Image("test.jpg"), "foobar.png", FakeProfile())
dlg.ShowModal()

if __debug__ and True:
profile("wp.prof", "run()")
run()




------=_NextPart_000_0005_01C5CC00.AE4EE700
Content-Type: application/octet-stream;
name="wallpaper.py.diff"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="wallpaper.py.diff"

Index: wallpaper.py
===================================================================
RCS file: /cvsroot/bitpim/bitpim/wallpaper.py,v
retrieving revision 1.77
diff -u -r1.77 wallpaper.py
--- wallpaper.py 9 Sep 2005 05:28:24 -0000 1.77
+++ wallpaper.py 8 Oct 2005 06:53:38 -0000
@@ -363,7 +363,7 @@
def OnAddFiles(self, filenames):
for file in filenames:
if self._raw_image:
- decoded_file=str(file).decode(guiwidgets.media_codec)
+ decoded_file=self.decodefilename(file)
targetfilename=self.getshortenedbasename(decoded_file)
open(targetfilename, 'wb').write(open(file, 'rb').read())
self.AddToIndex(str(os.path.basename(targetfilename)).decode(guiwidgets.media_codec),
@@ -407,7 +407,7 @@
extension={'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'}[imgparams['format']]

# munge name
- decoded_file=str(file).decode(guiwidgets.media_codec)
+ decoded_file=self.decodefilename(file)
targetfilename=self.getshortenedbasename(decoded_file, extension)

res=getattr(self, "saveimage_"+imgparams['format'])(

------=_NextPart_000_0005_01C5CC00.AE4EE700--

Loading...