Initial LG-PM325 support
"Brent Roettger" <[email protected]>
| Newsgroups | gmane.comp.mobile.bitpim.devel |
|---|---|
| Message-ID | <[email protected]> |
All, Here is my inital attempt at supporting my new phone. It is an LG PM 325 from Sprint. Currently only supports reading from the phonebook. My next goal is to add writing the phonebook back to the phone and then add more functionality in the near future. I have attached the two new files and the updated quiwidgets.py file. If anyone else has this phone, please test these out and let me know how it works for you. I am have noticed an annoying bug. Any of the three phones that I have to test with are all detected as COM3. I do not have a COM3 port on my system. They should correctly be detected on COM4. I haven't had a chance to investigate yet. Does anyone have any ideas? Regards, Brent Roettger
com_lgpm325.py
(text/plain, 34.8 KB)
### BITPIM ### ### Copyright (C) 2005 Brent Roettger <[email protected]> ### ### 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: com_lgpm325.py,v 1.00 2005/09/14 """Communicate with the LG PM325 (Sprint) cell phone""" # standard modules import datetime import re import time import cStringIO import sha # my modules import bpcalendar import common import commport import copy import p_lgpm325 import p_brew import com_brew import com_phone import com_lg import com_lgvx4400 import prototypes import fileinfo import call_history import sms class Phone(com_lgvx4400.Phone): "Talk to the LG PM325 cell phone" desc="LG-PM325" wallpaperindexfilename="setas/amsImageIndex.map" ringerindexfilename="setas/amsRingerIndex.map" protocolclass=p_lgpm325 serialsname='lgpm325' imagelocations=( # offset, index file, files location, type, maximumentries ( 2, wallpaperindexfilename, "brew/shared", "images", 30), ) ringtonelocations=( # offset, index file, files location, type, maximumentries ( 2, ringerindexfilename, "user/sound/ringer", "ringers", 30), ) builtinimages=('Starfish', 'Goldfish', 'Leaves', 'Bicycle', 'Speed', 'Autumn', 'Island', 'Winter', 'Bamboo', 'Yellow Flowers', 'Snow') builtinringtones=( 'Tone 1', 'Tone 2', 'Tone 3', 'Tone 4', 'Tone 5', 'Tone 6', 'Alert 1', 'Alert 2', 'Alert 3', 'Alert 4', 'Alert 5', 'Alert 6', 'Jazztic', 'Rock & Roll', 'Grand Waltz', 'Toccata and Fugue', 'Sunday Afternoon', 'Bumble Bee', 'Circus Band', 'Cuckoo Waltz', 'Latin', 'CanCan', 'Play tag', 'Eine kleine Nachtmusik', 'Symphony No.25 in G Minor', 'Capriccio a minor', 'Moonlight', 'A Nameless Girl', 'From the New World', 'They Called Me Elvis') def __init__(self, logtarget, commport): "Calls all the constructors and sets initial modes" com_phone.Phone.__init__(self, logtarget, commport) com_brew.BrewProtocol.__init__(self) com_lg.LGPhonebook.__init__(self) com_lg.LGIndexedMedia.__init__(self) self.log("Attempting to contact phone") self.mode=self.MODENONE self._cal_has_voice_id=hasattr(self.protocolclass, 'cal_has_voice_id') \ and self.protocolclass.cal_has_voice_id def getfundamentals(self, results): """Gets information fundamental to interoperating with the phone and UI. Currently this is: - 'uniqueserial' a unique serial number representing the phone - 'groups' the phonebook groups - 'wallpaper-index' map index numbers to names - 'ringtone-index' map index numbers to ringtone names This method is called before we read the phonebook data or before we write phonebook data. """ # use a hash of ESN and other stuff (being paranoid) self.log("Retrieving fundamental phone information") self.log("Phone serial number") results['uniqueserial']=sha.new(self.getfilecontents("nvm/$SYS.ESN")).hexdigest() self.log(results) # now read groups self.log("Reading group information") buf=prototypes.buffer(self.getfilecontents("pim/pbookgroup.dat")) g=self.protocolclass.pbgroups() g.readfrombuffer(buf) self.logdata("Groups read", buf.getdata(), g) groups={} for i in range(len(g.groups)): if len(g.groups[i].name): # sometimes have zero length names groups[i]={ 'group_id': g.groups[i].group_id, 'name': g.groups[i].name } results['groups']=groups self.getwallpaperindices(results) self.getringtoneindices(results) self.log("Fundamentals retrieved") return results def getwallpaperindices(self, results): return self.getmediaindex(self.builtinimages, self.imagelocations, results, 'wallpaper-index') def getringtoneindices(self, results): return self.getmediaindex(self.builtinringtones, self.ringtonelocations, results, 'ringtone-index') def getphonebook(self,result): """Reads the phonebook data. The L{getfundamentals} information will already be in result.""" # Read speed dials first speeds={} try: if self.protocolclass.NUMSPEEDDIALS: self.log("Reading speed dials") buf=prototypes.buffer(self.getfilecontents("pim/pbooksdial.dat")) sd=self.protocolclass.speeddials() sd.readfrombuffer(buf) for i in range(self.protocolclass.FIRSTSPEEDDIAL, self.protocolclass.LASTSPEEDDIAL+1): if sd.speeddials[i].entry<0 or sd.speeddials[i].entry>self.protocolclass.NUMPHONEBOOKENTRIES: continue l=speeds.get(sd.speeddials[i].entry, []) l.append((i, sd.speeddials[i].number)) speeds[sd.speeddials[i].entry]=l except com_brew.BrewNoSuchFileException: pass pbook={} # Bug in the phone. if you repeatedly read the phone book it starts # returning a random number as the number of entries. We get around # this by switching into brew mode which clears that. self.mode=self.MODENONE self.setmode(self.MODEBREW) self.log("Reading number of phonebook entries") req=self.protocolclass.pbinforequest() res=self.sendpbcommand(req, self.protocolclass.pbinforesponse) numentries=res.numentries if numentries<0 or numentries>1000: self.log("The phone is lying about how many entries are in the phonebook so we are doing it the hard way") numentries=0 firstserial=None loop=xrange(0,1000) hardway=True else: self.log("There are %d entries" % (numentries,)) loop=xrange(0, numentries) hardway=False # reset cursor self.sendpbcommand(self.protocolclass.pbinitrequest(), self.protocolclass.pbinitresponse) problemsdetected=False dupecheck={} for i in loop: if hardway: numentries+=1 req=self.protocolclass.pbreadentryrequest() res=self.sendpbcommand(req, self.protocolclass.pbreadentryresponse) self.log("Read entry "+`i`+" - "+res.entry.name) entry=self.extractphonebookentry(res.entry, speeds, result) if hardway and firstserial is None: firstserial=res.entry.serial1 pbook[i]=entry if res.entry.serial1 in dupecheck: self.log("Entry %s has same serial as entry %s. This will cause problems." % (`entry`, dupecheck[res.entry.serial1])) problemsdetected=True else: dupecheck[res.entry.serial1]=entry self.progress(i, numentries, res.entry.name) #### Advance to next entry req=self.protocolclass.pbnextentryrequest() res=self.sendpbcommand(req, self.protocolclass.pbnextentryresponse) if hardway: # look to see if we have looped if res.serial==firstserial or res.serial==0: break self.progress(numentries, numentries, "Phone book read completed") if problemsdetected: self.log("There are duplicate serial numbers. See above for details.") raise common.IntegrityCheckFailed(self.desc, "Data in phonebook is inconsistent. There are multiple entries with the same serial number. See the log.") result['phonebook']=pbook cats=[] for i in result['groups']: if result['groups'][i]['name']!='No Group': cats.append(result['groups'][i]['name']) result['categories']=cats print "returning keys",result.keys() return pbook def savegroups(self, data): groups=data['groups'] keys=groups.keys() keys.sort() g=self.protocolclass.pbgroups() for k in keys: e=self.protocolclass.pbgroup() e.icon=groups[k]['icon'] e.name=groups[k]['name'] g.groups.append(e) buffer=prototypes.buffer() g.writetobuffer(buffer) self.logdata("New group file", buffer.getvalue(), g) self.writefile("pim/pbookgroup.dat", buffer.getvalue()) def savephonebook(self, data): "Saves out the phonebook" self.savegroups(data) progressmax=len(data['phonebook'].keys()) # if we are going to write out speeddials, we have to re-read the entire # phonebook again if data.get('speeddials',None) is not None: progressmax+=len(data['phonebook'].keys()) # To write the phone book, we scan through all existing entries # and record their record number and serials. # We then delete any entries that aren't in data # We then write out our records, using overwrite or append # commands as necessary serialupdates=[] existingpbook={} # keep track of the phonebook that is on the phone self.mode=self.MODENONE self.setmode(self.MODEBREW) # see note in getphonebook() for why this is necessary self.setmode(self.MODEPHONEBOOK) # similar loop to reading req=self.protocolclass.pbinforequest() res=self.sendpbcommand(req, self.protocolclass.pbinforesponse) numexistingentries=res.numentries if numexistingentries<0 or numexistingentries>1000: self.log("The phone is lying about how many entries are in the phonebook so we are doing it the hard way") numexistingentries=0 firstserial=None loop=xrange(0,1000) hardway=True else: self.log("There are %d existing entries" % (numexistingentries,)) progressmax+=numexistingentries loop=xrange(0, numexistingentries) hardway=False progresscur=0 # reset cursor self.sendpbcommand(self.protocolclass.pbinitrequest(), self.protocolclass.pbinitresponse) for i in loop: ### Read current entry if hardway: numexistingentries+=1 progressmax+=1 req=self.protocolclass.pbreadentryrequest() res=self.sendpbcommand(req, self.protocolclass.pbreadentryresponse) entry={ 'number': res.entry.entrynumber, 'serial1': res.entry.serial1, 'name': res.entry.name} # assert entry['serial1']==entry['serial2'] # always the same self.log("Reading entry "+`i`+" - "+entry['name']) if hardway and firstserial is None: firstserial=res.entry.serial1 existingpbook[i]=entry self.progress(progresscur, progressmax, "existing "+entry['name']) #### Advance to next entry req=self.protocolclass.pbnextentryrequest() res=self.sendpbcommand(req, self.protocolclass.pbnextentryresponse) progresscur+=1 if hardway: # look to see if we have looped if res.serial==firstserial or res.serial==0: break # we have now looped around back to begining # Find entries that have been deleted pbook=data['phonebook'] dellist=[] for i in range(0, numexistingentries): ii=existingpbook[i] serial=ii['serial1'] item=self._findserial(serial, pbook) if item is None: dellist.append(i) progressmax+=len(dellist) # more work to do # Delete those entries for i in dellist: progresscur+=1 numexistingentries-=1 # keep count right ii=existingpbook[i] self.log("Deleting entry "+`i`+" - "+ii['name']) req=self.protocolclass.pbdeleteentryrequest() req.serial1=ii['serial1'] # req.serial2=ii['serial2'] req.entrynumber=ii['number'] self.sendpbcommand(req, self.protocolclass.pbdeleteentryresponse) self.progress(progresscur, progressmax, "Deleting "+ii['name']) # also remove them from existingpbook del existingpbook[i] # counter to keep track of record number (otherwise appends don't work) counter=0 # Now rewrite out existing entries keys=existingpbook.keys() existingserials=[] keys.sort() # do in same order as existingpbook for i in keys: progresscur+=1 ii=pbook[self._findserial(existingpbook[i]['serial1'], pbook)] self.log("Rewriting entry "+`i`+" - "+ii['name']) self.progress(progresscur, progressmax, "Rewriting "+ii['name']) entry=self.makeentry(counter, ii, data) counter+=1 existingserials.append(existingpbook[i]['serial1']) req=self.protocolclass.pbupdateentryrequest() req.entry=entry res=self.sendpbcommand(req, self.protocolclass.pbupdateentryresponse) serialupdates.append( ( ii["bitpimserial"], {'sourcetype': self.serialsname, 'serial1': res.serial1, 'sourceuniqueid': data['uniqueserial']}) ) assert ii['serial1']==res.serial1 # serial should stay the same # Finally write out new entries keys=pbook.keys() keys.sort() for i in keys: ii=pbook[i] if ii['serial1'] in existingserials: continue # already wrote this one out progresscur+=1 entry=self.makeentry(counter, ii, data) counter+=1 self.log("Appending entry "+ii['name']) self.progress(progresscur, progressmax, "Writing "+ii['name']) req=self.protocolclass.pbappendentryrequest() req.entry=entry res=self.sendpbcommand(req, self.protocolclass.pbappendentryresponse) serialupdates.append( ( ii["bitpimserial"], {'sourcetype': self.serialsname, 'serial1': res.newserial, 'sourceuniqueid': data['uniqueserial']}) ) data["serialupdates"]=serialupdates # deal with the speeddials if data.get("speeddials",None) is not None: # Yes, we have to read the ENTIRE phonebook again. This # is because we don't know which entry numbers actually # got assigned to the various entries, and we need the # actual numbers to assign to the speed dials newspeeds={} if len(data['speeddials']): # Move cursor to begining of phonebook self.mode=self.MODENONE self.setmode(self.MODEBREW) # see note in getphonebook() for why this is necessary self.setmode(self.MODEPHONEBOOK) self.log("Searching for speed dials") self.sendpbcommand(self.protocolclass.pbinitrequest(), self.protocolclass.pbinitresponse) for i in range(len(pbook)): ### Read current entry req=self.protocolclass.pbreadentryrequest() res=self.sendpbcommand(req, self.protocolclass.pbreadentryresponse) self.log("Scanning "+res.entry.name) progresscur+=1 # we have to turn the entry serial number into a bitpim serial serial=res.entry.serial1 found=False for bps, serials in serialupdates: if serials['serial1']==serial: # found the entry for sd in data['speeddials']: xx=data['speeddials'][sd] if xx[0]==bps: found=True if(getattr(self.protocolclass, 'SPEEDDIALINDEX', 0)==0): newspeeds[sd]=(res.entry.entrynumber, xx[1]) else: newspeeds[sd]=(res.entry.entrynumber, res.entry.numbertypes[xx[1]].numbertype) nt=self.protocolclass.numbertypetab[res.entry.numbertypes[xx[1]].numbertype] self.log("Speed dial #%d = %s (%s/%d)" % (sd, res.entry.name, nt, xx[1])) self.progress(progresscur, progressmax, "Speed dial #%d = %s (%s/%d)" % (sd, res.entry.name, nt, xx[1])) if not found: self.progress(progresscur, progressmax, "Scanning "+res.entry.name) # move to next entry self.sendpbcommand(self.protocolclass.pbnextentryrequest(), self.protocolclass.pbnextentryresponse) self.progress(progressmax, progressmax, "Finished scanning") print "new speed dials is",newspeeds req=self.protocolclass.speeddials() for i in range(self.protocolclass.NUMSPEEDDIALS): sd=self.protocolclass.speeddial() if i in newspeeds: sd.entry=newspeeds[i][0] sd.number=newspeeds[i][1] req.speeddials.append(sd) buffer=prototypes.buffer() req.writetobuffer(buffer) # We check the existing speed dial file as changes require a reboot self.log("Checking existing speed dials") if buffer.getvalue()!=self.getfilecontents("pim/pbooksdial.dat"): self.logdata("New speed dial file", buffer.getvalue(), req) self.writefile("pim/pbooksdial.dat", buffer.getvalue()) self.log("Your phone has to be rebooted due to the speed dials changing") self.progress(progressmax, progressmax, "Rebooting phone") data["rebootphone"]=True else: self.log("No changes to speed dials") return data def _findserial(self, serial, dict): """Searches dict to find entry with matching serial. If not found, returns None""" for i in dict: if dict[i]['serial1']==serial: return i return None def _normaliseindices(self, d): "turn all negative keys into positive ones for index" res={} keys=d.keys() keys.sort() keys.reverse() for k in keys: if k<0: for c in range(999999): if c not in keys and c not in res: break res[c]=d[k] else: res[k]=d[k] return res def extractphonebookentry(self, entry, speeds, fundamentals): """Return a phonebook entry in BitPim format. This is called from getphonebook.""" res={} # serials res['serials']=[ {'sourcetype': self.serialsname, 'serial1': entry.serial1, 'sourceuniqueid': fundamentals['uniqueserial']} ] # only one name res['names']=[ {'full': entry.name} ] # only one category cat=fundamentals['groups'].get(entry.group, {'name': "No Group"})['name'] if cat!="No Group": res['categories']=[ {'category': cat} ] # emails res['emails']=[] for i in entry.emails: if len(i.email): res['emails'].append( {'email': i.email} ) if not len(res['emails']): del res['emails'] # it was empty # urls if 'url' in entry.getfields() and len(entry.url): res['urls']=[ {'url': entry.url} ] # private if 'secret' in entry.getfields() and entry.secret: # we only supply secret if it is true res['flags']=[ {'secret': entry.secret } ] # memos if 'memo' in entry.getfields() and len(entry.memo): res['memos']=[ {'memo': entry.memo } ] # wallpapers if entry.wallpaper!=self.protocolclass.NOWALLPAPER: try: paper=fundamentals['wallpaper-index'][entry.wallpaper]['name'] res['wallpapers']=[ {'wallpaper': paper, 'use': 'call'} ] except: print "can't find wallpaper for index",entry.wallpaper pass # ringtones res['ringtones']=[] if 'ringtone' in entry.getfields() and entry.ringtone!=self.protocolclass.NORINGTONE: try: tone=fundamentals['ringtone-index'][entry.ringtone]['name'] res['ringtones'].append({'ringtone': tone, 'use': 'call'}) except: print "can't find ringtone for index",entry.ringtone # if 'msgringtone' in entry.getfields() and entry.msgringtone!=self.protocolclass.NOMSGRINGTONE: # try: # tone=fundamentals['ringtone-index'][entry.msgringtone]['name'] # res['ringtones'].append({'ringtone': tone, 'use': 'message'}) # except: # print "can't find ringtone for index",entry.msgringtone # if len(res['ringtones'])==0: # del res['ringtones'] if(getattr(self.protocolclass, 'SPEEDDIALINDEX', 0)==0): res=self._assignpbtypeandspeeddialsbyposition(entry, speeds, res) else: res=self._assignpbtypeandspeeddialsbytype(entry, speeds, res) return res def _assignpbtypeandspeeddialsbyposition(self, entry, speeds, res): # numbers res['numbers']=[] for i in range(self.protocolclass.NUMPHONENUMBERS): num=entry.numbers[i].number type=entry.numbertypes[i].numbertype if len(num): t=self.protocolclass.numbertypetab[type] if t[-1]=='2': t=t[:-1] res['numbers'].append({'number': num, 'type': t}) # speed dials if entry.entrynumber in speeds: for speeddial,numberindex in speeds[entry.entrynumber]: try: res['numbers'][numberindex]['speeddial']=speeddial except IndexError: print "speed dial refers to non-existent number\n",res['numbers'],"\n",numberindex,speeddial return res def _assignpbtypeandspeeddialsbytype(self, entry, speeds, res): # for some phones (e.g. vx8100) the speeddial numberindex is really the numbertype (now why would LG want to change this!) res['numbers']=[] for i in range(self.protocolclass.NUMPHONENUMBERS): num=entry.numbers[i].number type=entry.numbertypes[i].numbertype if len(num): t=self.protocolclass.numbertypetab[type] if t[-1]=='2': t=t[:-1] res['numbers'].append({'number': num, 'type': t}) # if this is a speeddial number set it if entry.entrynumber in speeds and speeds[entry.entrynumber][0][1]==entry.numbertypes[i].numbertype: res['numbers'][i]['speeddial']=speeds[entry.entrynumber][0][0] return res def _findmediainindex(self, index, name, pbentryname, type): if type=="ringtone": default=self.protocolclass.NORINGTONE elif type=="message ringtone": default=self.protocolclass.NOMSGRINGTONE elif type=="wallpaper": default=self.protocolclass.NOWALLPAPER else: assert False, "unknown type "+type if name is None: return default for i in index: if index[i]['name']==name: return i self.log("%s: Unable to find %s %s in the index. Setting to default." % (pbentryname, type, name)) return default def makeentry(self, counter, entry, data): """Creates pbentry object @param counter: The new entry number @param entry: The phonebook object (as returned from convertphonebooktophone) that we are using as the source @param data: The main dictionary, which we use to get access to media indices amongst other things """ e=self.protocolclass.pbentry() e.entrynumber=counter for k in entry: # special treatment for lists if k in ('emails', 'numbers', 'numbertypes'): l=getattr(e,k) for item in entry[k]: l.append(item) elif k=='ringtone': e.ringtone=self._findmediainindex(data['ringtone-index'], entry['ringtone'], entry['name'], 'ringtone') elif k=='msgringtone': e.msgringtone=self._findmediainindex(data['ringtone-index'], entry['msgringtone'], entry['name'], 'message ringtone') elif k=='wallpaper': e.wallpaper=self._findmediainindex(data['wallpaper-index'], entry['wallpaper'], entry['name'], 'wallpaper') elif k in e.getfields(): # everything else we just set setattr(e,k,entry[k]) return e brew_version_txt_key='brew_version.txt' brew_version_file='brew/version.txt' lgpbinfo_key='lgpbinfo' esn_file_key='esn_file' esn_file='nvm/$SYS.ESN' my_model='pm325' parentprofile=com_phone.Profile class Profile(parentprofile): protocolclass=Phone.protocolclass serialsname=Phone.serialsname BP_Calendar_Version=3 phone_manufacturer='LG Electronics Inc' phone_model='LG-LX325' WALLPAPER_WIDTH=120 WALLPAPER_HEIGHT=98 MAX_WALLPAPER_BASENAME_LENGTH=19 WALLPAPER_FILENAME_CHARS="abcdefghijklmnopqrstuvwxyz0123456789 ." WALLPAPER_CONVERT_FORMAT="bmp" MAX_RINGTONE_BASENAME_LENGTH=19 RINGTONE_FILENAME_CHARS="abcdefghijklmnopqrstuvwxyz0123456789 ." # which usb ids correspond to us usbids_straight=( ( 0x1004, 0x6000, 2), )# VID=LG Electronics, PID=LG pm325 -internal USB diagnostics interface usbids_usbtoserial=( ( 0x067b, 0x2303, None), # VID=Prolific, PID=USB to serial ( 0x0403, 0x6001, None), # VID=FTDI, PID=USB to serial ( 0x0731, 0x2003, None), # VID=Susteen, PID=Universal USB to serial ) usbids=usbids_straight+usbids_usbtoserial # which device classes we are. we are not a modem! deviceclasses=("serial",) # nb we don't allow save to camera so it isn't listed here imageorigins={} imageorigins.update(common.getkv(parentprofile.stockimageorigins, "images")) def GetImageOrigins(self): return self.imageorigins # our targets are the same for all origins imagetargets={} imagetargets.update(common.getkv(parentprofile.stockimagetargets, "wallpaper", {'width': 120, 'height': 98, 'format': "BMP"})) imagetargets.update(common.getkv(parentprofile.stockimagetargets, "pictureid", {'width': 120, 'height': 98, 'format': "BMP"})) imagetargets.update(common.getkv(parentprofile.stockimagetargets, "fullscreen", {'width': 120, 'height': 133, 'format': "BMP"})) def GetTargetsForImageOrigin(self, origin): return self.imagetargets def __init__(self): parentprofile.__init__(self) def _getgroup(self, name, groups): for key in groups: if groups[key]['name']==name: return key,groups[key] return None,None def normalisegroups(self, helper, data): "Assigns groups based on category data" pad=[] keys=data['groups'].keys() keys.sort() for k in keys: if k: # ignore key 0 which is 'No Group' name=data['groups'][k]['name'] pad.append(name) groups=helper.getmostpopularcategories(10, data['phonebook'], ["No Group"], 22, pad) # alpha sort groups.sort() # newgroups newgroups={} # put in No group newgroups[0]={'name': 'No Group', 'icon': 0} # populate for name in groups: # existing entries remain unchanged if name=="No Group": continue key,value=self._getgroup(name, data['groups']) if key is not None and key!=0: newgroups[key]=value # new entries get whatever numbers are free for name in groups: key,value=self._getgroup(name, newgroups) if key is None: for key in range(1,100000): if key not in newgroups: newgroups[key]={'name': name, 'icon': 1} break # yay, done if data['groups']!=newgroups: data['groups']=newgroups data['rebootphone']=True def convertphonebooktophone(self, helper, data): """Converts the data to what will be used by the phone @param data: contains the dict returned by getfundamentals as well as where the results go""" results={} speeds={} self.normalisegroups(helper, data) for pbentry in data['phonebook']: if len(results)==self.protocolclass.NUMPHONEBOOKENTRIES: break e={} # entry out entry=data['phonebook'][pbentry] # entry in try: # serials serial1=helper.getserial(entry.get('serials', []), self.serialsname, data['uniqueserial'], 'serial1', 0) # serial2=helper.getserial(entry.get('serials', []), self.serialsname, data['uniqueserial'], 'serial2', serial1) e['serial1']=serial1 # e['serial2']=serial2 for ss in entry["serials"]: if ss["sourcetype"]=="bitpim": e['bitpimserial']=ss assert e['bitpimserial'] # name e['name']=helper.getfullname(entry.get('names', []),1,1,22)[0] # categories/groups cat=helper.makeone(helper.getcategory(entry.get('categories', []),0,1,22), None) if cat is None: e['group']=0 else: key,value=self._getgroup(cat, data['groups']) if key is not None: e['group']=key else: # sorry no space for this category e['group']=0 # email addresses emails=helper.getemails(entry.get('emails', []) ,0,self.protocolclass.NUMEMAILS,48) e['emails']=helper.filllist(emails, self.protocolclass.NUMEMAILS, "") # url e['url']=helper.makeone(helper.geturls(entry.get('urls', []), 0,1,48), "") # memo (-1 is to leave space for null terminator - not all software puts it in, but we do) e['memo']=helper.makeone(helper.getmemos(entry.get('memos', []), 0, 1, self.protocolclass.MEMOLENGTH-1), "") # phone numbers # there must be at least one email address or phonenumber minnumbers=1 if len(emails): minnumbers=0 numbers=helper.getnumbers(entry.get('numbers', []),minnumbers,self.protocolclass.NUMPHONENUMBERS) e['numbertypes']=[] e['numbers']=[] for numindex in range(len(numbers)): num=numbers[numindex] # deal with type b4=len(e['numbertypes']) type=num['type'] for i,t in enumerate(self.protocolclass.numbertypetab): if type==t: # some voodoo to ensure the second home becomes home2 if i in e['numbertypes'] and t[-1]!='2': type+='2' continue e['numbertypes'].append(i) break if t=='none': # conveniently last entry e['numbertypes'].append(i) break if len(e['numbertypes'])==b4: # we couldn't find a type for the number continue # deal with number number=phonize(num['number']) if len(number)==0: # no actual digits in the number continue if len(number)>48: # get this number from somewhere sensible # ::TODO:: number is too long and we have to either truncate it or ignore it? number=number[:48] # truncate for moment e['numbers'].append(number) # deal with speed dial sd=num.get("speeddial", -1) if self.protocolclass.NUMSPEEDDIALS: if sd>=self.protocolclass.FIRSTSPEEDDIAL and sd<=self.protocolclass.LASTSPEEDDIAL: speeds[sd]=(e['bitpimserial'], numindex) e['numbertypes']=helper.filllist(e['numbertypes'], 5, 0) e['numbers']=helper.filllist(e['numbers'], 5, "") # ringtones, wallpaper # e['ringtone']=helper.getringtone(entry.get('ringtones', []), 'call', None) # e['msgringtone']=helper.getringtone(entry.get('ringtones', []), 'message', None) # e['wallpaper']=helper.getwallpaper(entry.get('wallpapers', []), 'call', None) # flags e['secret']=helper.getflag(entry.get('flags',[]), 'secret', False) results[pbentry]=e except helper.ConversionFailed: continue if self.protocolclass.NUMSPEEDDIALS: data['speeddials']=speeds data['phonebook']=results return data _supportedsyncs=( ('phonebook', 'read', None), # all phonebook reading # ('calendar', 'read', None), # all calendar reading # ('wallpaper', 'read', None), # all wallpaper reading # ('ringtone', 'read', None), # all ringtone reading # ('call_history', 'read', None),# all call history list reading # ('sms', 'read', None), # all SMS list reading # ('phonebook', 'write', 'OVERWRITE'), # only overwriting phonebook # ('calendar', 'write', 'OVERWRITE'), # only overwriting calendar # ('wallpaper', 'write', 'MERGE'), # merge and overwrite wallpaper # ('wallpaper', 'write', 'OVERWRITE'), # ('ringtone', 'write', 'MERGE'), # merge and overwrite ringtone # ('ringtone', 'write', 'OVERWRITE'), # ('sms', 'write', 'OVERWRITE'), # all SMS list writing )
p_lgpm325.p
(text/plain, 3.2 KB)
### BITPIM ### ### Copyright (C) 2005 Brent Roettger <[email protected]> ### ### 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: com_lgpm325.py,v 1.00 2005/09/14 %{ """Various descriptions of data specific to LG PM325 (Sprint)""" import re from prototypes import * from prototypeslg import * # Make all lg stuff available in this module as well from p_lg import * # We use LSB for all integer like fields UINT=UINTlsb BOOL=BOOLlsb NUMSPEEDDIALS=99 FIRSTSPEEDDIAL=1 LASTSPEEDDIAL=99 NUMPHONEBOOKENTRIES=200 MEMOLENGTH=33 NUMEMAILS=3 NUMPHONENUMBERS=5 NORINGTONE=0 NOWALLPAPER=0 numbertypetab=( 'Mobile', 'Home', 'Office', 'fax', 'pager' ) %} PACKET speeddial: 1 UINT {'default': 0xff} +entry 1 UINT {'default': 0xff} +number PACKET speeddials: * LIST {'length': NUMSPEEDDIALS, 'elementclass': speeddial} +speeddials PACKET pbreadentryresponse: "Results of reading one entry" * pbheader header * pbentry entry PACKET pbupdateentryrequest: * pbheader {'command': 0x04, 'flag': 0x01} +header * pbentry entry PACKET pbappendentryrequest: * pbheader {'command': 0x03, 'flag': 0x01} +header * pbentry entry # All STRINGS have raiseonterminatedread as False since the phone does # occassionally leave out the terminator byte # Note if you change the length of any of these fields, you also # need to modify com_lgpm325 to give a different truncateat parameter # in the convertphonebooktophone method PACKET pbentry: 4 UINT serial1 2 UINT {'constant': 0x026E} +entrysize 2 UINT entrynumber #is this the right length? 2 UNKNOWN unknown1 #what is this? 33 STRING {'raiseonunterminatedread': False} name 2 UINT group 1 UINT ringtone 1 UINT wallpaper #??? 1 BOOL secret * STRING {'raiseonunterminatedread': False, 'sizeinbytes': MEMOLENGTH} memo * LIST {'length': NUMEMAILS} +emails: 73 STRING {'raiseonunterminatedread': False} email 73 STRING {'raiseonunterminatedread': False} url * LIST {'length': NUMPHONENUMBERS} +numberspeeds: 1 UINT numbertype * LIST {'length': NUMPHONENUMBERS} +numbertypes: 1 UINT numbertype * LIST {'length': NUMPHONENUMBERS} +numbers: 49 STRING {'raiseonunterminatedread': False} number 1 UINT {'constant': 0x78} +EndOfRecord PACKET pbgroup: "A single group" 1 UINT group_id 1 UINT {'constant': 0x30} unknown1 #???? 3 UINT unknown2 3 UINT unknown3 32 STRING {'raiseonunterminatedread': False}name 1 UINT unk3 PACKET pbgroups: "Phonebook groups" * LIST {'elementclass': pbgroup} +groups PACKET indexentry: 2 UINT {'default': 0xffff} +index 40 STRING {'default': ""} +name PACKET indexfile: "Used for tracking wallpaper and ringtones" # A bit of a silly design again. Entries with an index of 0xffff are # 'blank'. Thus it is possible for numactiveitems and the actual # number of valid entries to be mismatched. P UINT {'constant': 30} maxitems 2 UINT numactiveitems * LIST {'length': self.maxitems, 'elementclass': indexentry, 'createdefault': True} +items
guiwidgets.py
(text/plain, 78.9 KB)
#!/usr/bin/env python ### BITPIM ### ### Copyright (C) 2003-2005 Roger Binns <[email protected]> ### ### 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: guiwidgets.py,v 1.267 2005/09/23 02:55:37 sawecw Exp $ """Most of the graphical user interface elements making up BitPim""" # standard modules import os import sys import time import copy import StringIO import getpass import sha,md5 import zlib import base64 import thread import Queue import shutil import time # wx. modules import wx import wx.html import wx.lib.mixins.listctrl import wx.lib.intctrl import wx.lib.newevent # my modules import common import helpids import comscan import usbscan import comdiagnose import analyser import guihelper import pubsub import bphtml import bitflingscan import aggregatedisplay import phone_media_codec import pubsub ### ### BitFling cert stuff ### BitFlingCertificateVerificationEvent, EVT_BITFLINGCERTIFICATEVERIFICATION = wx.lib.newevent.NewEvent() #### #### A simple text widget that does nice pretty logging. #### class LogWindow(wx.Panel): theanalyser=None def __init__(self, parent): wx.Panel.__init__(self,parent, -1) # have to use rich2 otherwise fixed width font isn't used on windows self.tb=wx.TextCtrl(self, 1, style=wx.TE_MULTILINE| wx.TE_RICH2|wx.TE_DONTWRAP|wx.TE_READONLY) f=wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL ) ta=wx.TextAttr(font=f) self.tb.SetDefaultStyle(ta) self.sizer=wx.BoxSizer(wx.VERTICAL) self.sizer.Add(self.tb, 1, wx.EXPAND) self.SetSizer(self.sizer) self.SetAutoLayout(True) self.sizer.Fit(self) wx.EVT_IDLE(self, self.OnIdle) self.outstandingtext=StringIO.StringIO() wx.EVT_KEY_UP(self.tb, self.OnKeyUp) def Clear(self): self.tb.Clear() def OnSelectAll(self, _): self.tb.SetSelection(-1, -1) def OnIdle(self,_): if self.outstandingtext.tell(): # this code is written to be re-entrant newt=self.outstandingtext.getvalue() self.outstandingtext.seek(0) self.outstandingtext.truncate() self.tb.AppendText(newt) def log(self, str, nl=True): now=time.time() t=time.localtime(now) self.outstandingtext.write("%d:%02d:%02d.%03d " % ( t[3], t[4], t[5], int((now-int(now))*1000))) self.outstandingtext.write(str) if nl: self.outstandingtext.write("\n") def logdata(self, str, data, klass=None): o=self.outstandingtext self.log(str, nl=False) if data is not None: o.write(" Data - "+`len(data)`+" bytes\n") if klass is not None: try: o.write("<#! %s.%s !#>\n" % (klass.__module__, klass.__name__)) except: klass=klass.__class__ o.write("<#! %s.%s !#>\n" % (klass.__module__, klass.__name__)) o.write(common.datatohexstring(data)) o.write("\n") def OnKeyUp(self, evt): keycode=evt.GetKeyCode() if keycode==ord('P') and evt.ControlDown() and evt.AltDown(): # analyse what was selected data=self.tb.GetStringSelection() # or the whole buffer if it was nothing if data is None or len(data)==0: data=self.tb.GetValue() try: self.theanalyser.Show() except: self.theanalyser=None if self.theanalyser is None: self.theanalyser=analyser.Analyser(data=data) self.theanalyser.Show() self.theanalyser.newdata(data) evt.Skip() ### ### Dialog asking what you want to sync ### class GetPhoneDialog(wx.Dialog): # sync sources ("Pretty Name", "name used to query profile") sources= ( ('PhoneBook', 'phonebook'), ('Calendar', 'calendar'), ('Wallpaper', 'wallpaper'), ('Ringtone', 'ringtone'), ('Memo', 'memo'), ('Todo', 'todo'), ('SMS', 'sms'), ('Call History', 'call_history')) # actions ("Pretty Name", "name used to query profile") actions = ( ("Get", "read"), ) NOTREQUESTED=0 MERGE=1 OVERWRITE=2 # type of action ("pretty name", "name used to query profile") types= ( ("Add", MERGE), ("Replace All", OVERWRITE)) HELPID=helpids.ID_GET_PHONE_DATA def __init__(self, frame, title, id=-1): wx.Dialog.__init__(self, frame, id, title, style=wx.CAPTION|wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE) gs=wx.FlexGridSizer(2+len(self.sources), 1+len(self.types),5 ,10) gs.AddGrowableCol(1) gs.AddMany( [ (wx.StaticText(self, -1, "Source"), 0, wx.EXPAND),]) for pretty,_ in self.types: gs.Add(wx.StaticText(self, -1, pretty), 0, wx.ALIGN_CENTRE) self.cb=[] self.rb=[] for desc, source in self.sources: self.cb.append(wx.CheckBox(self, wx.NewId(), desc)) wx.EVT_CHECKBOX(self, self.cb[-1].GetId(), self.DoOkStatus) gs.Add(self.cb[-1], 0, wx.EXPAND) first=True for tdesc,tval in self.types: if first: style=wx.RB_GROUP first=0 else: style=0 self.rb.append( wx.RadioButton(self, -1, "", style=style) ) if not self._dowesupport(source, self.actions[0][1], tval): self.rb[-1].Enable(False) self.rb[-1].SetValue(False) gs.Add(self.rb[-1], 0, wx.ALIGN_CENTRE) bs=wx.BoxSizer(wx.VERTICAL) bs.Add(gs, 0, wx.EXPAND|wx.ALL, 10) bs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 7) but=self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP) bs.Add(but, 0, wx.EXPAND|wx.ALL, 10) self.SetSizer(bs) self.SetAutoLayout(True) bs.Fit(self) wx.EVT_BUTTON(self, wx.ID_HELP, self.OnHelp) def _setting(self, type): for index in range(len(self.sources)): if self.sources[index][1]==type: if not self.cb[index].GetValue(): print type,"not requested" return self.NOTREQUESTED for i in range(len(self.types)): if self.rb[index*len(self.types)+i].GetValue(): print type,self.types[i][1] return self.types[i][1] assert False, "No selection for "+type assert False, "No such type "+type def GetPhoneBookSetting(self): return self._setting("phonebook") def GetCalendarSetting(self): return self._setting("calendar") def GetWallpaperSetting(self): return self._setting("wallpaper") def GetRingtoneSetting(self): return self._setting("ringtone") def GetMemoSetting(self): return self._setting("memo") def GetTodoSetting(self): return self._setting("todo") def GetSMSSetting(self): return self._setting("sms") def GetCallHistorySetting(self): return self._setting("call_history") def OnHelp(self,_): wx.GetApp().displayhelpid(self.HELPID) # this is what BitPim itself supports - the phones may support a subset _notsupported=( # ('phonebook', 'read', MERGE), # sort of is ('calendar', 'read', MERGE), ('wallpaper', 'read', MERGE), ('ringtone', 'read', MERGE), ('memo', 'read', MERGE), ('todo', 'read', MERGE)) def _dowesupport(self, source, action, type): if (source,action,type) in self._notsupported: return False return True def UpdateWithProfile(self, profile): for cs in range(len(self.sources)): source=self.sources[cs][1] # we disable the checkbox self.cb[cs].Enable(False) # are any radio buttons enabled count=0 for i in range(len(self.types)): assert len(self.types)==2 if self.types[i][1]==self.MERGE: type="MERGE" elif self.types[i][1]==self.OVERWRITE: type="OVERWRITE" else: assert False continue if self._dowesupport(source, self.actions[0][1], self.types[i][1]) and \ profile.SyncQuery(source, self.actions[0][1], type): self.cb[cs].Enable(True) self.rb[cs*len(self.types)+i].Enable(True) if self.rb[cs*len(self.types)+i].GetValue(): count+=1 else: self.rb[cs*len(self.types)+i].Enable(False) self.rb[cs*len(self.types)+i].SetValue(False) if not self.cb[cs].IsEnabled(): # ensure checkbox is unchecked if not enabled self.cb[cs].SetValue(False) else: # ensure one radio button is checked if count!=1: done=False for i in range(len(self.types)): index=cs*len(self.types)+i if self.rb[index].IsEnabled(): self.rb[index].SetValue(not done) done=False def ShowModal(self): # we ensure the OK button is in the correct state self.DoOkStatus() return wx.Dialog.ShowModal(self) def DoOkStatus(self, evt=None): # ensure the OK button is in the right state enable=False for i in self.cb: if i.GetValue(): enable=True break self.FindWindowById(wx.ID_OK).Enable(enable) if evt is not None: evt.Skip() class SendPhoneDialog(GetPhoneDialog): HELPID=helpids.ID_SEND_PHONE_DATA # actions ("Pretty Name", "name used to query profile") actions = ( ("Send", "write"), ) def __init__(self, frame, title, id=-1): GetPhoneDialog.__init__(self, frame, title, id) # this is what BitPim itself doesn't supports - the phones may support less _notsupported=( ('call_history', 'write', None),) ### ### The master config dialog ### class ConfigDialog(wx.Dialog): phonemodels={ 'LG-PM325 (Sprint)': 'com_lgpm325', 'LG-VX3200': 'com_lgvx3200', 'LG-VX4400': 'com_lgvx4400', 'LG-VX4500': 'com_lgvx4500', 'LG-VX4600 (Telus Mobility)': 'com_lgvx4600', 'LG-VX4650 (Verizon Wireless)': 'com_lgvx4650', 'LG-VX6000': 'com_lgvx6000', 'LG-VX6100': 'com_lgvx6100', 'LG-VX7000': 'com_lgvx7000', 'LG-VX8000 (Verizon Wireless)': 'com_lgvx8000', 'LG-VX8100 (Verizon Wireless)': 'com_lgvx8100', 'LG-TM520': 'com_lgtm520', 'LG-VX10': 'com_lgtm520', 'MM-7400': 'com_sanyo7400', 'PM-8200': 'com_sanyo8200', 'RL-4920': 'com_sanyo4920', 'SCP-4900': 'com_sanyo4900', 'SCP-5300': 'com_sanyo5300', 'SCP-5400': 'com_sanyo5400', 'SCP-5500': 'com_sanyo5500', 'SCP-7200': 'com_sanyo7200', 'SCP-7300': 'com_sanyo7300', 'SCP-8100': 'com_sanyo8100', 'SCP-8100 (Bell Mobility)': 'com_sanyo8100_bell', 'SCH-A310': 'com_samsungscha310', 'SPH-A460': 'com_samsungspha460', 'SPH-A620 (VGA1000)': 'com_samsungspha620', 'SPH-N200': 'com_samsungsphn200', 'SCH-A650': 'com_samsungscha650', 'SCH-A670': 'com_samsungscha670', 'SK6100 (Pelephone)' : 'com_sk6100', 'Other CDMA phone': 'com_othercdma', } if __debug__: phonemodels.update( {'Audiovox CDM-8900': 'com_audiovoxcdm8900', # phone is too fragile for normal use }) update_choices=('Never', 'Daily', 'Weekly', 'Monthly') setme="<setme>" ID_DIRBROWSE=wx.NewId() ID_COMBROWSE=wx.NewId() ID_RETRY=wx.NewId() ID_BITFLING=wx.NewId() def __init__(self, mainwindow, frame, title="BitPim Settings", id=-1): wx.Dialog.__init__(self, frame, id, title, style=wx.CAPTION|wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE) self.mw=mainwindow self.bitflingresponsequeues={} gs=wx.GridBagSizer(10, 10) gs.AddGrowableCol(1) # safemode gs.Add( wx.StaticText(self, -1, "Read Only"), pos=(0,0), flag=wx.ALIGN_CENTER_VERTICAL) self.safemode=wx.CheckBox(self, wx.NewId(), "Block writing anything to the phone") gs.Add( self.safemode, pos=(0,1), flag=wx.ALIGN_CENTER_VERTICAL) # where we store our files gs.Add( wx.StaticText(self, -1, "Disk storage"), pos=(1,0), flag=wx.ALIGN_CENTER_VERTICAL) self.diskbox=wx.TextCtrl(self, -1, self.setme, size=(400,-1)) gs.Add( self.diskbox, pos=(1,1), flag=wx.ALIGN_CENTER_VERTICAL) gs.Add( wx.Button(self, self.ID_DIRBROWSE, "Browse ..."), pos=(1,2), flag=wx.ALIGN_CENTER_VERTICAL) # phone type gs.Add( wx.StaticText(self, -1, "Phone Type"), pos=(2,0), flag=wx.ALIGN_CENTER_VERTICAL) keys=self.phonemodels.keys() keys.sort() self.phonebox=wx.ComboBox(self, -1, "LG-VX4400", style=wx.CB_DROPDOWN|wx.CB_READONLY,choices=keys) self.phonebox.SetValue("LG-VX4400") gs.Add( self.phonebox, pos=(2,1), flag=wx.ALIGN_CENTER_VERTICAL) # com port gs.Add( wx.StaticText(self, -1, "Com Port"), pos=(3,0), flag=wx.ALIGN_CENTER_VERTICAL) self.commbox=wx.TextCtrl(self, -1, self.setme, size=(200,-1)) gs.Add( self.commbox, pos=(3,1), flag=wx.ALIGN_CENTER_VERTICAL) gs.Add( wx.Button(self, self.ID_COMBROWSE, "Browse ..."), pos=(3,2), flag=wx.ALIGN_CENTER_VERTICAL) # Automatic check for update gs.Add(wx.StaticText(self, -1, 'Check for Update'), pos=(4,0), flag=wx.ALIGN_CENTER_VERTICAL) self.updatebox=wx.ComboBox(self, -1, self.update_choices[0], style=wx.CB_DROPDOWN|wx.CB_READONLY, choices=self.update_choices) gs.Add(self.updatebox, pos=(4,1), flag=wx.ALIGN_CENTER_VERTICAL) # always start with the 'Today' tab gs.Add(wx.StaticText(self, -1, 'Startup'), pos=(5,0), flag=wx.ALIGN_CENTER_VERTICAL) self.startup=wx.CheckBox(self, wx.NewId(), 'Always start with the Today tab') gs.Add(self.startup, pos=(5,1), flag=wx.ALIGN_CENTER_VERTICAL) # bitfling if bitflingscan.IsBitFlingEnabled(): self.SetupBitFlingCertVerification() gs.Add( wx.StaticText( self, -1, "BitFling"), pos=(6,0), flag=wx.ALIGN_CENTER_VERTICAL) self.bitflingenabled=wx.CheckBox(self, self.ID_BITFLING, "Enabled") gs.Add(self.bitflingenabled, pos=(6,1), flag=wx.ALIGN_CENTER_VERTICAL) gs.Add( wx.Button(self, self.ID_BITFLING, "Settings ..."), pos=(6,2), flag=wx.ALIGN_CENTER_VERTICAL) wx.EVT_BUTTON(self, self.ID_BITFLING, self.OnBitFlingSettings) wx.EVT_CHECKBOX(self, self.ID_BITFLING, self.ApplyBitFlingSettings) if self.mw.config.Read("bitfling/password","<unconfigured>") \ == "<unconfigured>": self.mw.config.WriteInt("bitfling/enabled", 0) self.bitflingenabled.SetValue(False) self.bitflingenabled.Enable(False) else: self.bitflingenabled=None # crud at the bottom bs=wx.BoxSizer(wx.VERTICAL) bs.Add(gs, 0, wx.EXPAND|wx.ALL, 10) bs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 7) but=self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP) bs.Add(but, 0, wx.CENTER|wx.ALL, 10) wx.EVT_BUTTON(self, wx.ID_HELP, self.OnHelp) wx.EVT_BUTTON(self, self.ID_DIRBROWSE, self.OnDirBrowse) wx.EVT_BUTTON(self, self.ID_COMBROWSE, self.OnComBrowse) wx.EVT_BUTTON(self, wx.ID_OK, self.OnOK) self.setdefaults() self.SetSizer(bs) self.SetAutoLayout(True) bs.Fit(self) # Retrieve saved settings... (we only care about position) set_size("ConfigDialog", self, screenpct=-1, aspect=3.5) wx.EVT_CLOSE(self, self.OnClose) def OnCancel(self, _): self.saveSize() def OnOK(self, _): self.saveSize() # validate directory dir=self.diskbox.GetValue() try: os.makedirs(dir) except: pass if os.path.isdir(dir): self.EndModal(wx.ID_OK) self.ApplyBitFlingSettings() return wx.TipWindow(self.diskbox, "No such directory - please correct") def OnHelp(self, _): wx.GetApp().displayhelpid(helpids.ID_SETTINGS_DIALOG) def OnDirBrowse(self, _): dlg=wx.DirDialog(self, defaultPath=self.diskbox.GetValue(), style=wx.DD_NEW_DIR_BUTTON) res=dlg.ShowModal() v=dlg.GetPath() dlg.Destroy() if res==wx.ID_OK: self.diskbox.SetValue(v) def OnComBrowse(self, _): self.saveSize() if self.mw.wt is not None: self.mw.wt.clearcomm() # remember its size # w=self.mw.config.ReadInt("combrowsewidth", 640) # h=self.mw.config.ReadInt("combrowseheight", 480) p=self.mw.config.ReadInt("combrowsesash", 200) dlg=CommPortDialog(self, __import__(self.phonemodels[self.phonebox.GetValue()]), defaultport=self.commbox.GetValue(), sashposition=p) # dlg.SetSize(wx.Size(w,h)) # dlg.Centre() res=dlg.ShowModal() v=dlg.GetPort() # sz=dlg.GetSize() # self.mw.config.WriteInt("combrowsewidth", sz.GetWidth()) # self.mw.config.WriteInt("combrowseheight", sz.GetHeight()) self.mw.config.WriteInt("combrowsesash", dlg.sashposition) dlg.Destroy() if res==wx.ID_OK: self.commbox.SetValue(v) def ApplyBitFlingSettings(self, _=None): if self.bitflingenabled is not None: if self.bitflingenabled.GetValue(): bitflingscan.flinger.configure(self.mw.config.Read("bitfling/username", "<unconfigured>"), bitflingscan.decode(self.mw.config.Read("bitfling/password", "<unconfigured>")), self.mw.config.Read("bitfling/host", "<unconfigured>"), self.mw.config.ReadInt("bitfling/port", 12652)) else: bitflingscan.flinger.unconfigure() def OnBitFlingSettings(self, _): dlg=BitFlingSettingsDialog(None, self.mw.config) if dlg.ShowModal()==wx.ID_OK: dlg.SaveSettings() dlg.Destroy() self.ApplyBitFlingSettings() if self.mw.config.Read("bitfling/password","<unconfigured>") \ != "<unconfigured>": self.bitflingenabled.Enable(True) def SetupBitFlingCertVerification(self): "Setup all the voodoo needed for certificate verification to happen, not matter which thread wants it" EVT_BITFLINGCERTIFICATEVERIFICATION(self, self._wrapVerifyBitFlingCert) bitflingscan.flinger.SetCertVerifier(self.dispatchVerifyBitFlingCert) bitflingscan.flinger.setthreadeventloop(wx.SafeYield) def dispatchVerifyBitFlingCert(self, addr, key): """Handle a certificate verification from any thread The request is handed to the main gui thread, and then we wait for the results""" print thread.get_ident(),"dispatchVerifyBitFlingCert called" q=self.bitflingresponsequeues.get(thread.get_ident(), None) if q is None: q=Queue.Queue() self.bitflingresponsequeues[thread.get_ident()]=q print thread.get_ident(), "Posting BitFlingCertificateVerificationEvent" wx.PostEvent(self, BitFlingCertificateVerificationEvent(addr=addr, key=key, q=q)) print thread.get_ident(), "After posting BitFlingCertificateVerificationEvent, waiting for response" res, exc = q.get() print thread.get_ident(), "Got response", res, exc if exc is not None: ex=exc[1] ex.gui_exc_info=exc[2] raise ex return res def _wrapVerifyBitFlingCert(self, evt): """Receive the event in the main gui thread for cert verification We unpack the parameters, call the verification method""" print "_wrapVerifyBitFlingCert" addr, hostkey, q = evt.addr, evt.key, evt.q self.VerifyBitFlingCert(addr, hostkey, q) def VerifyBitFlingCert(self, addr, key, q): print "VerifyBitFlingCert for", addr, "type",key.get_name() # ::TODO:: reject if not dsa # get fingerprint fingerprint=common.hexify(key.get_fingerprint()) # do we already know about it? existing=wx.GetApp().config.Read("bitfling/certificates/%s" % (addr[0],), "") if len(existing): fp=existing if fp==fingerprint: q.put( (True, None) ) return # throw up the dialog print "asking user" dlg=AcceptCertificateDialog(None, wx.GetApp().config, addr, fingerprint, q) dlg.ShowModal() def OnClose(self, evt): self.saveSize() # Don't destroy the dialong, just put it away... self.EndModal(wx.ID_CANCEL) def setfromconfig(self): if len(self.mw.config.Read("path", "")): self.diskbox.SetValue(self.mw.config.Read("path", "")) if len(self.mw.config.Read("lgvx4400port")): self.commbox.SetValue(self.mw.config.Read("lgvx4400port", "")) if self.mw.config.Read("phonetype", "") in self.phonemodels: self.phonebox.SetValue(self.mw.config.Read("phonetype")) if self.bitflingenabled is not None: self.bitflingenabled.SetValue(self.mw.config.ReadInt("bitfling/enabled", 0)) self.ApplyBitFlingSettings() self.safemode.SetValue(self.mw.config.ReadInt("Safemode", 0)) self.updatebox.SetValue(self.mw.config.Read("updaterate", self.update_choices[0])) self.startup.SetValue(self.mw.config.ReadInt("startwithtoday", 0)) def setdefaults(self): if self.diskbox.GetValue()==self.setme: if guihelper.IsMSWindows(): # we want subdir of my documents on windows # nice and painful from win32com.shell import shell, shellcon path=shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0) path=os.path.join(str(path), "bitpim") else: path=os.path.expanduser("~/.bitpim-files") self.diskbox.SetValue(path) if self.commbox.GetValue()==self.setme: comm="auto" self.commbox.SetValue(comm) def updatevariables(self): path=self.diskbox.GetValue() self.mw.configpath=path self.mw.ringerpath=self._fixup(os.path.join(path, "ringer")) self.mw.wallpaperpath=self._fixup(os.path.join(path, "wallpaper")) self.mw.phonebookpath=self._fixup(os.path.join(path, "phonebook")) self.mw.calendarpath=self._fixup(os.path.join(path, "calendar")) oldpath=self.mw.config.Read("path", "") self.mw.config.Write("path", path) self.mw.commportsetting=str(self.commbox.GetValue()) self.mw.config.Write("lgvx4400port", self.mw.commportsetting) if self.mw.wt is not None: self.mw.wt.clearcomm() # comm parameters (retry, timeouts, flow control etc) commparm={} commparm['retryontimeout']=self.mw.config.ReadInt("commretryontimeout", False) commparm['timeout']=self.mw.config.ReadInt('commtimeout', 3) commparm['hardwareflow']=self.mw.config.ReadInt('commhardwareflow', False) commparm['softwareflow']=self.mw.config.ReadInt('commsoftwareflow', False) commparm['baud']=self.mw.config.ReadInt('commbaud', 115200) self.mw.commparams=commparm # phone model self.mw.config.Write("phonetype", self.phonebox.GetValue()) self.mw.phonemodule=__import__(self.phonemodels[self.phonebox.GetValue()]) self.mw.phoneprofile=self.mw.phonemodule.Profile() pubsub.publish(pubsub.PHONE_MODEL_CHANGED, self.mw.phonemodule) # bitfling if self.bitflingenabled is not None: self.mw.bitflingenabled=self.bitflingenabled.GetValue() self.mw.config.WriteInt("bitfling/enabled", self.mw.bitflingenabled) # safemode - make sure you have to restart to disable self.mw.config.WriteInt("SafeMode", self.safemode.GetValue()) if self.safemode.GetValue(): wx.GetApp().SAFEMODE=True wx.GetApp().ApplySafeMode() # check for update rate self.mw.config.Write('updaterate', self.updatebox.GetValue()) # startup option self.mw.config.WriteInt('startwithtoday', self.startup.GetValue()) # ensure config is saved self.mw.config.Flush() self.mw.EnsureDatabase(path, oldpath) # update the status bar self.mw.SetPhoneModelStatus() # update the cache path self.mw.update_cache_path() def _fixup(self, path): # os.path.join screws up adding root directory of a drive to # a directory. eg join("c:\", "foo") gives "c:\\foo" whch # is invalid. This function fixes that if len(path)>=3: if path[1]==':' and path[2]=='\\' and path[3]=='\\': return path[0:2]+path[3:] return path def needconfig(self): # Set base config self.setfromconfig() # do we know the phone? if self.mw.config.Read("phonetype", "") not in self.phonemodels: return True # are any at unknown settings if self.diskbox.GetValue()==self.setme or \ self.commbox.GetValue()==self.setme: # fill in and set defaults self.setdefaults() self.updatevariables() # any still unset? if self.diskbox.GetValue()==self.setme or \ self.commbox.GetValue()==self.setme: return True # does data directory exist? try: os.makedirs(self.diskbox.GetValue()) except: pass if not os.path.isdir(self.diskbox.GetValue()): return True return False def ShowModal(self): self.setfromconfig() ec=wx.Dialog.ShowModal(self) if ec==wx.ID_OK: self.updatevariables() return ec def saveSize(self): save_size("ConfigDialog", self.GetRect()) ### ### The select a comm port dialog box ### class CommPortDialog(wx.Dialog): ID_LISTBOX=1 ID_TEXTBOX=2 ID_REFRESH=3 ID_SASH=4 ID_SAVE=5 def __init__(self, parent, selectedphone, id=-1, title="Choose a comm port", defaultport="auto", sashposition=0): wx.Dialog.__init__(self, parent, id, title, style=wx.CAPTION|wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) self.parent=parent self.port=defaultport self.sashposition=sashposition self.selectedphone=selectedphone p=self # parent widget # the listbox and textbox in a splitter splitter=wx.SplitterWindow(p, self.ID_SASH, style=wx.SP_3D|wx.SP_LIVE_UPDATE) self.lb=wx.ListBox(splitter, self.ID_LISTBOX, style=wx.LB_SINGLE|wx.LB_HSCROLL|wx.LB_NEEDED_SB) self.tb=wx.html.HtmlWindow(splitter, self.ID_TEXTBOX, size=wx.Size(400,400)) # default style is auto scrollbar splitter.SplitHorizontally(self.lb, self.tb, sashposition) # the buttons buttsizer=wx.GridSizer(1, 5) buttsizer.Add(wx.Button(p, wx.ID_OK, "OK"), 0, wx.ALL, 10) buttsizer.Add(wx.Button(p, self.ID_REFRESH, "Refresh"), 0, wx.ALL, 10) buttsizer.Add(wx.Button(p, self.ID_SAVE, "Save..."), 0, wx.ALL, 10) buttsizer.Add(wx.Button(p, wx.ID_HELP, "Help"), 0, wx.ALL, 10) buttsizer.Add(wx.Button(p, wx.ID_CANCEL, "Cancel"), 0, wx.ALL, 10) # vertical join of the two vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(splitter, 1, wx.EXPAND) vbs.Add(buttsizer, 0, wx.CENTER) # hook into self p.SetSizer(vbs) p.SetAutoLayout(True) vbs.Fit(p) # update dialog wx.CallAfter(self.OnRefresh) # hook in all the widgets wx.EVT_BUTTON(self, wx.ID_CANCEL, self.OnCancel) wx.EVT_BUTTON(self, wx.ID_HELP, self.OnHelp) wx.EVT_BUTTON(self, self.ID_REFRESH, self.OnRefresh) wx.EVT_BUTTON(self, self.ID_SAVE, self.OnSave) wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk) wx.EVT_LISTBOX(self, self.ID_LISTBOX, self.OnListBox) wx.EVT_LISTBOX_DCLICK(self, self.ID_LISTBOX, self.OnListBox) wx.EVT_SPLITTER_SASH_POS_CHANGED(self, self.ID_SASH, self.OnSashChange) # Retrieve saved settings... Use 40% of screen if not specified set_size("CommDialog", self, screenpct=60) wx.EVT_CLOSE(self, self.OnClose) def OnSashChange(self, _=None): self.sashposition=self.FindWindowById(self.ID_SASH).GetSashPosition() def OnRefresh(self, _=None): self.tb.SetPage("<p><b>Refreshing</b> ...") self.lb.Clear() self.Update() ports=comscan.comscan()+usbscan.usbscan() if bitflingscan.IsBitFlingEnabled(): ports=ports+bitflingscan.flinger.scan() self.portinfo=comdiagnose.diagnose(ports, self.selectedphone) if len(self.portinfo): self.portinfo=[ ("Automatic", "auto", "<p>BitPim will try to detect the correct port automatically when accessing your phone" ) ]+\ self.portinfo self.lb.Clear() sel=-1 for name, actual, description in self.portinfo: if sel<0 and self.GetPort()==actual: sel=self.lb.GetCount() self.lb.Append(name) if sel<0: sel=0 if self.lb.GetCount(): self.lb.SetSelection(sel) self.OnListBox() else: self.FindWindowById(wx.ID_OK).Enable(False) self.tb.SetPage("<html><body>You do not have any com/serial ports on your system</body></html>") def OnListBox(self, _=None): # enable/disable ok button p=self.portinfo[self.lb.GetSelection()] if p[1] is None: self.FindWindowById(wx.ID_OK).Enable(False) else: self.port=p[1] self.FindWindowById(wx.ID_OK).Enable(True) self.tb.SetPage(p[2]) def OnSave(self, _): html=StringIO.StringIO() print >>html, "<html><head><title>BitPim port listing - %s</title></head>" % (time.ctime(), ) print >>html, "<body><h1>BitPim port listing - %s</h1><table>" % (time.ctime(),) for long,actual,desc in self.portinfo: if actual is None or actual=="auto": continue print >>html, '<tr bgcolor="#77ff77"><td colspan=2>%s</td><td>%s</td></tr>' % (long,actual) print >>html, "<tr><td colspan=3>%s</td></tr>" % (desc,) print >>html, "<tr><td colspan=3><hr></td></tr>" print >>html, "</table></body></html>" dlg=wx.FileDialog(self, "Save port details as", defaultFile="bitpim-ports.html", wildcard="HTML files (*.html)|*.html", style=wx.SAVE|wx.OVERWRITE_PROMPT|wx.CHANGE_DIR) if dlg.ShowModal()==wx.ID_OK: open(dlg.GetPath(), "wt").write(html.getvalue()) dlg.Destroy() def OnCancel(self, _): self.saveSize() self.EndModal(wx.ID_CANCEL) def OnOk(self, _): self.saveSize() self.EndModal(wx.ID_OK) def OnHelp(self, _): wx.GetApp().displayhelpid(helpids.ID_COMMSETTINGS_DIALOG) def OnClose(self, evt): self.saveSize() # Don't destroy the dialong, just put it away... self.EndModal(wx.ID_CANCEL) def GetPort(self): return self.port def saveSize(self): save_size("CommDialog", self.GetRect()) ### ### Accept certificate dialog ### class AcceptCertificateDialog(wx.Dialog): def __init__(self, parent, config, addr, fingerprint, q): parent=self.FindAGoodParent(parent) wx.Dialog.__init__(self, parent, -1, "Accept certificate?", style=wx.CAPTION|wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) self.config=config self.q=q self.addr=addr self.fingerprint=fingerprint hbs=wx.BoxSizer(wx.HORIZONTAL) hbs.Add(wx.StaticText(self, -1, "Host:"), 0, wx.ALL, 5) hbs.Add(wx.StaticText(self, -1, addr[0]), 0, wx.ALL, 5) hbs.Add(wx.StaticText(self, -1, " Fingerprint:"), 0, wx.ALL, 5) hbs.Add(wx.StaticText(self, -1, fingerprint), 1, wx.ALL, 5) vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) vbs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 7) but=self.CreateButtonSizer(wx.YES|wx.NO|wx.HELP) vbs.Add(but, 0, wx.ALIGN_CENTER|wx.ALL, 10) self.SetSizer(vbs) vbs.Fit(self) wx.EVT_BUTTON(self, wx.ID_YES, self.OnYes) wx.EVT_BUTTON(self, wx.ID_NO, self.OnNo) wx.EVT_BUTTON(self, wx.ID_CANCEL, self.OnNo) def OnYes(self, _): wx.GetApp().config.Write("bitfling/certificates/%s" % (self.addr[0],), self.fingerprint) wx.GetApp().config.Flush() if self.IsModal(): self.EndModal(wx.ID_YES) else: self.Show(False) wx.CallAfter(self.Destroy) print "returning true from AcceptCertificateDialog" self.q.put( (True, None) ) def OnNo(self, _): if self.IsModal(): self.EndModal(wx.ID_NO) else: self.Show(False) wx.CallAfter(self.Destroy) print "returning false from AcceptCertificateDialog" self.q.put( (False, None) ) def FindAGoodParent(self, suggestion): win=wx.Window_FindFocus() while win is not None: try: if win.IsModal(): print "FindAGoodParent is",win return win except AttributeError: parent=win.GetParent() win=parent return suggestion ### ### BitFling settings dialog ### class BitFlingSettingsDialog(wx.Dialog): ID_USERNAME=wx.NewId() ID_PASSWORD=wx.NewId() ID_HOST=wx.NewId() ID_PORT=wx.NewId() ID_TEST=wx.NewId() passwordsentinel="@+_-3@<," def __init__(self, parent, config): wx.Dialog.__init__(self, parent, -1, "Edit BitFling settings", style=wx.CAPTION|wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) self.config=config gs=wx.FlexGridSizer(1, 2, 5, 5) gs.AddGrowableCol(1) gs.AddMany([ (wx.StaticText(self, -1, "Username"), 0, wx.ALIGN_CENTER_VERTICAL), (wx.TextCtrl(self, self.ID_USERNAME), 1, wx.EXPAND), (wx.StaticText(self, -1, "Password"), 0, wx.ALIGN_CENTER_VERTICAL), (wx.TextCtrl(self, self.ID_PASSWORD, style=wx.TE_PASSWORD), 1, wx.EXPAND), (wx.StaticText(self, -1, "Host"), 0, wx.ALIGN_CENTER_VERTICAL), (wx.TextCtrl(self, self.ID_HOST), 1, wx.EXPAND), (wx.StaticText(self, -1, "Port"), 0, wx.ALIGN_CENTER_VERTICAL), (wx.lib.intctrl.IntCtrl(self, self.ID_PORT, value=12652, min=1, max=65535), 0) ]) vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(gs, 0, wx.EXPAND|wx.ALL, 5) vbs.Add((1,1), 1, wx.EXPAND) vbs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 10) gs=wx.GridSizer(1,4, 5,5) gs.Add(wx.Button(self, wx.ID_OK, "OK")) gs.Add(wx.Button(self, self.ID_TEST, "Test")) gs.Add(wx.Button(self, wx.ID_HELP, "Help")) gs.Add(wx.Button(self, wx.ID_CANCEL, "Cancel")) vbs.Add(gs, 0, wx.ALIGN_CENTER|wx.ALL, 10) self.SetSizer(vbs) vbs.Fit(self) set_size("BitFlingConfigDialog", self, -20, 0.5) # event handlers wx.EVT_BUTTON(self, self.ID_TEST, self.OnTest) # fill in data self.FindWindowById(self.ID_USERNAME).SetValue(config.Read("bitfling/username", getpass.getuser())) if len(config.Read("bitfling/password", "")): self.FindWindowById(self.ID_PASSWORD).SetValue(self.passwordsentinel) self.FindWindowById(self.ID_HOST).SetValue(config.Read("bitfling/host", "")) self.FindWindowById(self.ID_PORT).SetValue(config.ReadInt("bitfling/port", 12652)) def ShowModal(self): res=wx.Dialog.ShowModal(self) save_size("BitFlingConfigDialog", self.GetRect()) return res def GetSettings(self): username=self.FindWindowById(self.ID_USERNAME).GetValue() pwd=self.FindWindowById(self.ID_PASSWORD).GetValue() if pwd==self.passwordsentinel: pwd=bitflingscan.decode(self.config.Read("bitfling/password", self.passwordsentinel)) host=self.FindWindowById(self.ID_HOST).GetValue() port=self.FindWindowById(self.ID_PORT).GetValue() return username, pwd, host, port def SaveSettings(self): "Copy settings from dialog fields into config object" username,pwd,host,port=self.GetSettings() self.config.Write("bitfling/username", username) self.config.Write("bitfling/password", bitflingscan.encode(pwd)) self.config.Write("bitfling/host", host) self.config.WriteInt("bitfling/port", port) def OnTest(self, _): wx.CallAfter(self._OnTest) def _OnTest(self, _=None): try: bitflingscan.flinger.configure(*self.GetSettings()) res=bitflingscan.flinger.getversion() dlg=wx.MessageDialog(self, "Succeeded. Remote version is %s" % (res,) , "Success", wx.OK|wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() except Exception,ex: res="Failed: %s: %s" % sys.exc_info()[:2] if hasattr(ex, "gui_exc_info"): print common.formatexception( ex.gui_exc_info) else: print common.formatexception() dlg=wx.MessageDialog(self, res, "Failed", wx.OK|wx.ICON_ERROR) dlg.ShowModal() dlg.Destroy() ### ### File viewer ### media_codec=phone_media_codec.codec_name class MyFileDropTarget(wx.FileDropTarget): def __init__(self, target): wx.FileDropTarget.__init__(self) self.target=target def OnDropFiles(self, x, y, filenames): return self.target.OnDropFiles(x,y,filenames) class FileView(wx.Panel): # Various DC objects used for drawing the items. We have to calculate them in the constructor as # the app object hasn't been constructed when this file is imported. item_selection_brush=None item_selection_pen=None item_line_font=None item_term="..." item_guardspace=None # Files we should ignore skiplist= ( 'desktop.ini', 'thumbs.db', 'zbthumbnail.info' ) # how much data do we want in call to getdata NONE=0 SELECTED=1 ALL=2 # maximum length of a filename maxlen=-1 # set via phone profile # acceptable characters in a filename filenamechars=None # set via phone profile def __init__(self, mainwindow, parent, watermark=None): wx.Panel.__init__(self,parent,style=wx.CLIP_CHILDREN) if not hasattr(self, "organizemenu"): self.organizemenu=None # item attributes if self.item_selection_brush is None: self.item_selection_brush=wx.TheBrushList.FindOrCreateBrush("MEDIUMPURPLE2", wx.SOLID) self.item_selection_pen=wx.ThePenList.FindOrCreatePen("MEDIUMPURPLE2", 1, wx.SOLID) f1=wx.TheFontList.FindOrCreateFont(10, wx.SWISS, wx.NORMAL, wx.BOLD) f2=wx.TheFontList.FindOrCreateFont(10, wx.SWISS, wx.NORMAL, wx.NORMAL) self.item_line_font=[f1, f2, f2, f2] dc=wx.MemoryDC() dc.SelectObject(wx.EmptyBitmap(100,100)) self.item_guardspace=dc.GetTextExtent(self.item_term)[0] del dc # no redraw ickiness # wx.EVT_ERASE_BACKGROUND(self, lambda evt: None) self.mainwindow=mainwindow self.thedir=None self.wildcard="I forgot to set wildcard in derived class|*" self.__dragging=False # use the aggregatedisplay to do the actual item display self.aggdisp=aggregatedisplay.Display(self, self, watermark) # we are our own datasource vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(self.aggdisp, 1, wx.EXPAND|wx.ALL, 2) self.SetSizer(vbs) timerid=wx.NewId() self.thetimer=wx.Timer(self, timerid) wx.EVT_TIMER(self, timerid, self.OnTooltipTimer) self.motionpos=None wx.EVT_MOUSE_EVENTS(self.aggdisp, self.OnMouseEvent) self.tipwindow=None if guihelper.IsMSWindows(): # turn on drag-and-drag for windows wx.EVT_MOTION(self.aggdisp, self.OnStartDrag) # Menus self.itemmenu=wx.Menu() self.itemmenu.Append(guihelper.ID_FV_OPEN, "Open") self.itemmenu.Append(guihelper.ID_FV_SAVE, "Save ...") self.itemmenu.AppendSeparator() if guihelper.IsMSWindows(): self.itemmenu.Append(guihelper.ID_FV_COPY, "Copy") self.itemmenu.Append(guihelper.ID_FV_DELETE, "Delete") self.itemmenu.Append(guihelper.ID_FV_RENAME, "Rename") self.itemmenu.AppendSeparator() # self.itemmenu.Append(guihelper.ID_FV_RENAME, "Rename") self.itemmenu.Append(guihelper.ID_FV_REFRESH, "Refresh") self.bgmenu=wx.Menu() if self.organizemenu is not None: self.bgmenu.AppendMenu(wx.NewId(), "Organize by", self.organizemenu) self.bgmenu.Append(guihelper.ID_FV_ADD, "Add ...") self.bgmenu.Append(guihelper.ID_FV_PASTE, "Paste") self.bgmenu.Append(guihelper.ID_FV_REFRESH, "Refresh") wx.EVT_MENU(self.itemmenu, guihelper.ID_FV_OPEN, self.OnLaunch) wx.EVT_MENU(self.itemmenu, guihelper.ID_FV_SAVE, self.OnSave) if guihelper.IsMSWindows(): wx.EVT_MENU(self.itemmenu, guihelper.ID_FV_COPY, self.OnCopy) wx.EVT_MENU(self.itemmenu, guihelper.ID_FV_DELETE, self.OnDelete) wx.EVT_MENU(self.itemmenu, guihelper.ID_FV_RENAME, self.OnRename) wx.EVT_MENU(self.itemmenu, guihelper.ID_FV_REFRESH, lambda evt: self.OnRefresh()) wx.EVT_MENU(self.bgmenu, guihelper.ID_FV_ADD, self.OnAdd) wx.EVT_MENU(self.bgmenu, guihelper.ID_FV_PASTE, self.OnPaste) wx.EVT_MENU(self.bgmenu, guihelper.ID_FV_REFRESH, lambda evt: self.OnRefresh) wx.EVT_RIGHT_UP(self.aggdisp, self.OnRightClick) aggregatedisplay.EVT_ACTIVATE(self.aggdisp, self.aggdisp.GetId(), self.OnLaunch) self.droptarget=MyFileDropTarget(self) self.SetDropTarget(self.droptarget) def OnRightClick(self, evt): """Popup the right click context menu @param widget: which widget to popup in @param position: position in widget @param onitem: True if the context menu is for an item """ if len(self.aggdisp.GetSelection()): menu=self.itemmenu item=self.GetSelectedItems()[0] menu.Enable(guihelper.ID_FV_RENAME, len(self.GetSelectedItems())==1) # we always launch on mac if not guihelper.IsMac(): menu.FindItemById(guihelper.ID_FV_OPEN).Enable(guihelper.GetOpenCommand(item.fileinfo.mimetypes, item.filename) is not None) else: menu=self.bgmenu menu.Enable(guihelper.ID_FV_PASTE, self.CanPaste()) if menu is None: return self.aggdisp.PopupMenu(menu, evt.GetPosition()) def OnLaunch(self, _): item=self.GetSelectedItems()[0] if guihelper.IsMac(): import findertools findertools.launch(item.filename) return cmd=guihelper.GetOpenCommand(item.fileinfo.mimetypes, item.filename) if cmd is None: wx.Bell() else: wx.Execute(cmd, wx.EXEC_ASYNC) if guihelper.IsMSWindows(): # drag-and-drop files only works in Windows def OnStartDrag(self, evt): evt.Skip() if not evt.LeftIsDown(): return items=self.GetSelectedItems() if not len(items): return drag_source=wx.DropSource(self) file_names=wx.FileDataObject() for item in items: file_names.AddFile(item.filename) drag_source.SetData(file_names) self.__dragging=True res=drag_source.DoDragDrop(wx.Drag_AllowMove) self.__dragging=False # check of any of the files have been removed, # can't trust result returned by DoDragDrop for item in items: if not os.access(item.filename, os.F_OK): # item has been moved, remove from index item.RemoveFromIndex() def OnMouseEvent(self, evt): self.motionpos=evt.GetPosition() evt.Skip() self.thetimer.Stop() if evt.AltDown() or evt.MetaDown() or evt.ControlDown() or evt.ShiftDown() or evt.Dragging() or evt.IsButton(): return self.thetimer.Start(1750, wx.TIMER_ONE_SHOT) def OnTooltipTimer(self, _): x,y=self.aggdisp.CalcUnscrolledPosition(*self.motionpos) res=self.aggdisp.HitTest(x,y) if res.item is not None: try: self.tipwindow.Destroy() except: pass self.tipwindow=res.item.DisplayTooltip(self.aggdisp, res.itemrectscrolled) def OnRefresh(self): self.aggdisp.UpdateItems() def GetSelectedItems(self): return [item for _,_,_,item in self.aggdisp.GetSelection()] def GetAllItems(self): return [item for _,_,_,item in self.aggdisp.GetAllItems()] def OnSelectAll(self, _): self.aggdisp.SelectAll() def EndSelectedFilesContext(self, context, deleteitems=False): # We have a fun additional problem. By default Windows # returns a code that it is copying, when in fact it is # moving. Consequently we do a delete if either the # source file is gone, or deleteitems is true if not deleteitems: for item in context: if not os.path.exists(item.filename): print "Forcing EndSelectedFilesContext to delete mode even though not specified" deleteitems=True break if deleteitems: for item in context: if os.path.exists(item.filename): os.remove(item.filename) for item in context: item.RemoveFromIndex() self.OnRefresh() def OnSave(self, _): # If one item is selected we ask for a filename to save. If # multiple then we ask for a directory, and users don't get # the choice to affect the names of files. Note that we don't # allow users to select a different format for the file - we # just copy it as is. items=self.GetSelectedItems() if len(items)==1: ext=getext(items[0].name) if ext=="": ext="*" else: ext="*."+ext dlg=wx.FileDialog(self, "Save item", wildcard=ext, defaultFile=items[0].name, style=wx.SAVE|wx.OVERWRITE_PROMPT|wx.CHANGE_DIR) if dlg.ShowModal()==wx.ID_OK: shutil.copyfile(items[0].filename, dlg.GetPath()) dlg.Destroy() else: dlg=wx.DirDialog(self, "Save items to", style=wx.DD_DEFAULT_STYLE|wx.DD_NEW_DIR_BUTTON) if dlg.ShowModal()==wx.ID_OK: for item in items: shutil.copyfile(item.filename, os.path.join(dlg.GetPath(), basename(item.filename))) dlg.Destroy() if guihelper.IsMSWindows(): def OnCopy(self, _): items=self.GetSelectedItems() if not len(items): # nothing selected return file_names=wx.FileDataObject() for item in items: file_names.AddFile(item.filename) if wx.TheClipboard.Open(): wx.TheClipboard.SetData(file_names) wx.TheClipboard.Close() def CanCopy(self): return len(self.GetSelectedItems()) def OnPaste(self, _=None): if not wx.TheClipboard.Open(): # can't access the clipboard return if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_FILENAME)): file_names=wx.FileDataObject() has_data=wx.TheClipboard.GetData(file_names) else: has_data=False wx.TheClipboard.Close() if has_data: self.OnAddFiles(file_names.GetFilenames()) 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)) wx.TheClipboard.Close() return r def OnDelete(self,_): items=self.GetSelectedItems() for item in items: os.remove(item.filename) for item in items: item.RemoveFromIndex() self.OnRefresh() def genericpopulatefs(self, dict, key, indexkey, version): try: os.makedirs(self.thedir) except: pass if not os.path.isdir(self.thedir): raise Exception("Bad directory for "+key+" '"+self.thedir+"'") # delete all files we don't know about if 'key' contains replacements if dict.has_key(key): print key,"present - updating disk" for f in os.listdir(self.thedir): # delete them all except windows magic ones which we ignore if f.lower() not in self.skiplist: os.remove(os.path.join(self.thedir, f)) d=dict[key] for i in d: open(os.path.join(self.thedir, i.encode(media_codec)), "wb").write(d[i]) d={} d[indexkey]=dict[indexkey] common.writeversionindexfile(os.path.join(self.thedir, "index.idx"), d, version) return dict def genericgetfromfs(self, result, key, indexkey, currentversion): try: os.makedirs(self.thedir) except: pass if not os.path.isdir(self.thedir): raise Exception("Bad directory for "+key+" '"+self.thedir+"'") dict={} for file in os.listdir(self.thedir): if file=='index.idx': d={} d['result']={} common.readversionedindexfile(os.path.join(self.thedir, file), d, self.versionupgrade, currentversion) result.update(d['result']) elif file.lower() in self.skiplist: # ignore windows detritus continue elif key is not None: dict[file.decode(media_codec)]=open(os.path.join(self.thedir, file), "rb").read() if key is not None: result[key]=dict if indexkey not in result: result[indexkey]={} return result def OnDropFiles(self, _, dummy, filenames): # There is a bug in that the most recently created tab # in the notebook that accepts filedrop receives these # files, not the most visible one. We find the currently # viewed tab in the notebook and send the files there if self.__dragging: # I'm the drag source, forget 'bout it ! return target=self # fallback t=self.mainwindow.nb.GetPage(self.mainwindow.nb.GetSelection()) if isinstance(t, FileView): # changing target in dragndrop target=t target.OnAddFiles(filenames) def OnAdd(self, _=None): dlg=wx.FileDialog(self, "Choose files", style=wx.OPEN|wx.MULTIPLE, wildcard=self.wildcard) if dlg.ShowModal()==wx.ID_OK: self.OnAddFiles(dlg.GetPaths()) dlg.Destroy() def CanRename(self): return len(self.GetSelectedItems())==1 # subclass needs to define this media_notification_type=None def OnRename(self, _=None): items=self.GetSelectedItems() if len(items)!=1: # either none or more than 1 items selected return old_name=items[0].name dlg=wx.TextEntryDialog(self, "Enter a new name:", "Item Rename", old_name) if dlg.ShowModal()==wx.ID_OK: new_name=dlg.GetValue() if len(new_name) and new_name!=old_name: old_file_name=items[0].filename new_file_name=self.getshortenedbasename(new_name) try: os.rename(old_file_name, new_file_name) items[0].RenameInIndex(os.path.basename( str(new_file_name).decode(media_codec))) pubsub.publish(pubsub.MEDIA_NAME_CHANGED, data={ pubsub.media_change_type: self.media_notification_type, pubsub.media_old_name: old_file_name, pubsub.media_new_name: new_file_name }) except: pass dlg.Destroy() def OnAddFiles(self,_): raise Exception("not implemented") def getshortenedbasename(self, filename, newext=''): filename=basename(filename) if not 'A' in self.filenamechars: filename=filename.lower() if not 'a' in self.filenamechars: filename=filename.upper() if len(newext): filename=stripext(filename) filename="".join([x for x in filename if x in self.filenamechars]) filename=filename.replace(" "," ").replace(" ", " ") # remove double spaces if len(newext): filename+='.'+newext if len(filename)>self.maxlen: chop=len(filename)-self.maxlen filename=stripext(filename)[:-chop].strip()+'.'+getext(filename) return os.path.join(self.thedir, filename.encode(media_codec)) def genericgetdata(self,dict,want, mediapath, mediakey, mediaindexkey): # this was originally written for wallpaper hence using the 'wp' variable dict.update(self._data) items=None if want==self.SELECTED: items=self.GetSelectedItems() if len(items)==0: want=self.ALL if want==self.ALL: items=self.GetAllItems() if items is not None: wp={} i=0 for item in items: data=open(item.filename, "rb").read() wp[i]={'name': item.name, 'data': data} v=item.origin if v is not None: wp[i]['origin']=v i+=1 dict[mediakey]=wp return dict def log(self, log_str): self.mainwindow.log(log_str) class FileViewDisplayItem(object): datakey="Someone forgot to set me" PADDING=3 def __init__(self, view, key, mediapath): self.view=view self.key=key self.thumbsize=10,10 self.mediapath=mediapath self.setvals() self.lastw=None def setvals(self): me=self.view._data[self.datakey][self.key] self.name=me['name'] self.origin=me.get('origin', None) self.filename=os.path.join(self.mediapath, self.name.encode(media_codec)) self.fileinfo=self.view.GetFileInfo(self.filename) self.size=self.fileinfo.size self.short=self.fileinfo.shortdescription() self.long=self.fileinfo.longdescription() self.thumb=None self.selbbox=None self.lines=[self.name, self.short, '%.1f kb' % (self.size/1024.0,)] if self.origin: self.lines.append(self.origin) def setthumbnailsize(self, thumbnailsize): self.thumbnailsize=thumbnailsize self.thumb=None self.selbox=None def Draw(self, dc, width, height, selected): if self.thumb is None: self.thumb=self.view.GetItemThumbnail(self.name, self.thumbnailsize[0], self.thumbnailsize[1]) redrawbbox=False if selected: if self.lastw!=width or self.selbbox is None: redrawbbox=True else: oldb=dc.GetBrush() oldp=dc.GetPen() dc.SetBrush(self.view.item_selection_brush) dc.SetPen(self.view.item_selection_pen) dc.DrawRectangle(*self.selbbox) dc.SetBrush(oldb) dc.SetPen(oldp) dc.DrawBitmap(self.thumb, self.PADDING+self.thumbnailsize[0]/2-self.thumb.GetWidth()/2, self.PADDING, True) xoff=self.PADDING+self.thumbnailsize[0]+self.PADDING yoff=self.PADDING*2 widthavailable=width-xoff-self.PADDING maxw=0 old=dc.GetFont() for i,line in enumerate(self.lines): dc.SetFont(self.view.item_line_font[i]) w,h=DrawTextWithLimit(dc, xoff, yoff, line, widthavailable, self.view.item_guardspace, self.view.item_term) maxw=max(maxw,w) yoff+=h dc.SetFont(old) self.lastw=width self.selbbox=(0,0,xoff+maxw+self.PADDING,max(yoff+self.PADDING,self.thumb.GetHeight()+self.PADDING*2)) if redrawbbox: return self.Draw(dc, width, height, selected) return self.selbbox def DisplayTooltip(self, parent, rect): res=["Name: "+self.name, "Origin: "+(self.origin, "default")[self.origin is None], 'File size: %.1f kb (%d bytes)' % (self.size/1024.0, self.size), "\n"+self.datatype+" information:\n", self.long] # tipwindow takes screen coordinates so we have to transform x,y=parent.ClientToScreen(rect[0:2]) return wx.TipWindow(parent, "\n".join(res), 1024, wx.Rect(x,y,rect[2], rect[3])) def RemoveFromIndex(self): del self.view._data[self.datakey][self.key] self.view.modified=True self.view.OnRefresh() def RenameInIndex(self, new_name): self.view._data[self.datakey][self.key]['name']=new_name self.view.modified=True self.view.OnRefresh() ### ### Various platform independent filename functions ### basename=common.basename stripext=common.stripext getext=common.getext ### ### A dialog showing a message in a fixed font, with a help button ### class MyFixedScrolledMessageDialog(wx.Dialog): """A dialog displaying a readonly text control with a fixed width font""" def __init__(self, parent, msg, caption, helpid, pos = wx.DefaultPosition, size = (850,600)): wx.Dialog.__init__(self, parent, -1, caption, pos, size, style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) text=wx.TextCtrl(self, 1, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.TE_DONTWRAP ) # Fixed width font f=wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL ) ta=wx.TextAttr(font=f) text.SetDefaultStyle(ta) text.AppendText(msg) # if i supply this in constructor then the font doesn't take text.SetInsertionPoint(0) text.ShowPosition(text.XYToPosition(0,0)) # vertical sizer vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(text, 1, wx.EXPAND|wx.ALL, 10) # buttons vbs.Add(self.CreateButtonSizer(wx.OK|wx.HELP), 0, wx.ALIGN_RIGHT|wx.ALL, 10) # plumb self.SetSizer(vbs) self.SetAutoLayout(True) wx.EVT_BUTTON(self, wx.ID_HELP, lambda _,helpid=helpid: wx.GetApp().displayhelpid(helpid)) ### ### Dialog that deals with exceptions ### import StringIO class ExceptionDialog(wx.Dialog): def __init__(self, parent, exception, title="Exception"): wx.Dialog.__init__(self, parent, title=title, style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.THICK_FRAME|wx.MAXIMIZE_BOX, size=(740, 580)) self.maintext=wx.TextCtrl(self, style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_RICH2|wx.HSCROLL) vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(self.maintext, 1, wx.EXPAND|wx.ALL, 5) buttsizer=wx.GridSizer(1, 3) buttsizer.Add(wx.Button(self, wx.ID_CANCEL, "Abort BitPim"), 0, wx.ALL, 10) buttsizer.Add(wx.Button(self, wx.ID_HELP, "Help"), 0, wx.ALL, 10) buttsizer.Add(wx.Button(self, wx.ID_OK, "Continue"), 0, wx.ALL, 10) vbs.Add(buttsizer, 0, wx.ALIGN_RIGHT|wx.ALL, 5) wx.EVT_BUTTON(self, wx.ID_CANCEL, self.abort) wx.EVT_BUTTON(self, wx.ID_HELP, lambda _: wx.GetApp().displayhelpid(helpids.ID_EXCEPTION_DIALOG)) self.SetSizer(vbs) self._text="" self.addexception(exception) def abort(self,_): import os os._exit(1) def addexception(self, exception): s=StringIO.StringIO() s.write("An unexpected exception has occurred.\nPlease see the help for details on what to do.\n\n") if hasattr(exception, 'gui_exc_info'): s.write(common.formatexception(exception.gui_exc_info)) else: s.write("Exception with no extra info.\n%s\n" % (exception.str(),)) self._text=s.getvalue() self.maintext.SetValue(self._text) def getexceptiontext(self): return self._text ### ### Too much freaking effort for a simple statusbar. Mostly copied from the demo. ### class MyStatusBar(wx.StatusBar): __total_panes=3 __version_index=2 __phone_model_index=2 __app_status_index=0 __gauge_index=1 __major_progress_index=2 __minor_progress_index=2 __help_str_index=2 __general_pane=2 __pane_width=[50, 180, -1] def __init__(self, parent, id=-1): wx.StatusBar.__init__(self, parent, id) self.__major_progress_text=self.__version_text=self.__phone_text='' self.sizechanged=False wx.EVT_SIZE(self, self.OnSize) wx.EVT_IDLE(self, self.OnIdle) self.gauge=wx.Gauge(self, 1000, 1) self.SetFieldsCount(self.__total_panes) self.SetStatusWidths(self.__pane_width) self.Reposition() def OnSize(self,_): self.sizechanged=True def OnIdle(self,_): if not len(self.GetStatusText(self.__general_pane)): self.__set_version_phone_text() if self.sizechanged: try: self.Reposition() except: # this works around a bug in wx (on Windows only) # where we get a bogus exception. See SF bug # 873155 pass def Reposition(self): self.sizeChanged = False rect=self.GetFieldRect(self.__gauge_index) self.gauge.SetPosition(wx.Point(rect.x+2, rect.y+2)) self.gauge.SetSize(wx.Size(rect.width-4, rect.height-4)) def progressminor(self, pos, max, desc=""): self.gauge.SetRange(max) self.gauge.SetValue(pos) if len(self.__major_progress_text): s=self.__major_progress_text if len(desc): s+=' - '+desc else: s=desc self.SetStatusText(s, self.__minor_progress_index) def progressmajor(self, pos, max, desc=""): if len(desc) and max: self.__major_progress_text="%d/%d %s" % (pos+1, max, desc) else: self.__major_progress_text=desc self.progressminor(0,1) def GetHelpPane(self): return self.__help_str_index def set_app_status(self, str=''): self.SetStatusText(str, self.__app_status_index) def set_phone_model(self, str=''): self.__phone_text=str self.__set_version_phone_text() def set_versions(self, current, latest=''): s='BitPim '+current if len(latest): s+='/Latest '+latest else: s+='/Latest <Unknown>' self.__version_text=s self.__set_version_phone_text() def __set_version_phone_text(self): if guihelper.IsMac(): s = self.__version_text+' '+self.__phone_text else: s = self.__version_text+'\t'+self.__phone_text self.SetStatusText(s, self.__general_pane) ### ### A MessageBox with a help button ### class AlertDialogWithHelp(wx.Dialog): """A dialog box with Ok button and a help button""" def __init__(self, parent, message, caption, helpfn, style=wx.DEFAULT_DIALOG_STYLE, icon=wx.ICON_EXCLAMATION): wx.Dialog.__init__(self, parent, -1, caption, style=style|wx.DEFAULT_DIALOG_STYLE) p=self # parent widget # horiz sizer for bitmap and text hbs=wx.BoxSizer(wx.HORIZONTAL) hbs.Add(wx.StaticBitmap(p, -1, wx.ArtProvider_GetBitmap(self.icontoart(icon), wx.ART_MESSAGE_BOX)), 0, wx.CENTER|wx.ALL, 10) hbs.Add(wx.StaticText(p, -1, message), 1, wx.CENTER|wx.ALL, 10) # the buttons buttsizer=self.CreateButtonSizer(wx.HELP|style) # Both vertical vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 10) vbs.Add(buttsizer, 0, wx.CENTER|wx.ALL, 10) # wire it in self.SetSizer(vbs) self.SetAutoLayout(True) vbs.Fit(self) wx.EVT_BUTTON(self, wx.ID_HELP, helpfn) def icontoart(self, id): if id&wx.ICON_EXCLAMATION: return wx.ART_WARNING if id&wx.ICON_INFORMATION: return wx.ART_INFORMATION # ::TODO:: rest of these # fallthru return wx.ART_INFORMATION ### ### Yet another dialog with user selectable buttons ### class AnotherDialog(wx.Dialog): """A dialog box with user supplied buttons""" def __init__(self, parent, message, caption, buttons, helpfn=None, style=wx.DEFAULT_DIALOG_STYLE, icon=wx.ICON_EXCLAMATION): """Constructor @param message: Text displayed in body of dialog @param caption: Title of dialog @param buttons: A list of tuples. Each tuple is a string and an integer id. The result of calling ShowModal() is the id @param helpfn: The function called if the user presses the help button (wx.ID_HELP) """ wx.Dialog.__init__(self, parent, -1, caption, style=style) p=self # parent widget # horiz sizer for bitmap and text hbs=wx.BoxSizer(wx.HORIZONTAL) hbs.Add(wx.StaticBitmap(p, -1, wx.ArtProvider_GetBitmap(self.icontoart(icon), wx.ART_MESSAGE_BOX)), 0, wx.CENTER|wx.ALL, 10) hbs.Add(wx.StaticText(p, -1, message), 1, wx.CENTER|wx.ALL, 10) # the buttons buttsizer=wx.BoxSizer(wx.HORIZONTAL) for label,id in buttons: buttsizer.Add( wx.Button(self, id, label), 0, wx.ALL|wx.ALIGN_CENTER, 5) if id!=wx.ID_HELP: wx.EVT_BUTTON(self, id, self.OnButton) else: wx.EVT_BUTTON(self, wx.ID_HELP, helpfn) # Both vertical vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 10) vbs.Add(buttsizer, 0, wx.CENTER|wx.ALL, 10) # wire it in self.SetSizer(vbs) self.SetAutoLayout(True) vbs.Fit(self) def OnButton(self, event): self.EndModal(event.GetId()) def icontoart(self, id): if id&wx.ICON_EXCLAMATION: return wx.ART_WARNING if id&wx.ICON_INFORMATION: return wx.ART_INFORMATION # ::TODO:: rest of these # fallthru return wx.ART_INFORMATION ### ### Utility code ### def DrawTextWithLimit(dc, x, y, text, widthavailable, guardspace, term="..."): """Draws text and if it will overflow the width available, truncates and puts ... at the end @param x: start position for text @param y: start position for text @param text: the string to draw @param widthavailable: the total amount of space available @param guardspace: if the text is longer than widthavailable then this amount of space is reclaimed from the right handside and term put there instead. Consequently this value should be at least the width of term @param term: the string that is placed in the guardspace if it gets truncated. Make sure guardspace is at least the width of this string! @returns: The extent of the text that was drawn in the end as a tuple of (width, height) """ w,h=dc.GetTextExtent(text) if w<widthavailable: dc.DrawText(text,x,y) return w,h extents=dc.GetPartialTextExtents(text) limit=widthavailable-guardspace # find out how many chars in we have to go before hitting limit for i,offset in enumerate(extents): if offset>limit: break # back off 1 in case the new text's a tad long if i: i-=1 text=text[:i]+term w,h=dc.GetTextExtent(text) assert w<=widthavailable dc.DrawText(text, x, y) return w,h ### ### Window geometry/positioning memory ### def set_size(confname, window, screenpct=50, aspect=1.0): """Sets remembered/calculated dimensions/position for window @param confname: subkey to store/get this windows's settings from @param window: the window object itself @param screenpct: percentage of the screen the window should occupy. If this value is negative then the window will not be resized, only repositioned (unless the current size is silly) @param aspect: aspect ratio. If greater than one then it is how much wider than tall the window is, and if less than one then the other way round """ confobj=wx.GetApp().config # frig confname confname="windows/"+confname # Get screen size, scale according to percentage supplied screenSize = wx.GetClientDisplayRect() if (aspect >= 1): newWidth = screenSize.width * abs(screenpct) / 100 newHeight = screenSize.height * abs(screenpct) / aspect / 100 else: newWidth = screenSize.width * abs(screenpct) * aspect / 100 newHeight = screenSize.height * abs(screenpct) / 100 if screenpct<=0: rs_width,rs_height=window.GetSizeTuple() else: # Retrieve values (if any) from config database for this config object rs_width = confobj.ReadInt(confname + "/width", int(newWidth)) rs_height = confobj.ReadInt(confname + "/height", int(newHeight)) # suitable magic number to show not configured. it is an exercise for the reader # why it isn't -65536 (hint: virtual desktops) unconfigured=-65245 rs_x = confobj.ReadInt(confname + "/x", unconfigured) rs_y = confobj.ReadInt(confname + "/y", unconfigured) # Check for small window if rs_height < 96: rs_height = newHeight if rs_width < 96: rs_width = newWidth # Make sure window is no larger than about screen size # # determine ratio of original oversized window so we keep the ratio if we resize... rs_aspect = rs_width/rs_height if rs_aspect >= 1: if rs_width > screenSize.width: rs_width = screenSize.width if rs_height > (screenSize.height): rs_height = (screenSize.height / rs_aspect) - screenSize.y else: if rs_width > screenSize.width: rs_width = screenSize.width * rs_aspect if rs_height > screenSize.height - screenSize.y: rs_height = screenSize.height - screenSize.y # Off the screen? Just pull it back a little bit so it's visible.... if rs_x!=unconfigured and rs_x > screenSize.width: rs_x = screenSize.width - 50 if rs_x!=unconfigured and rs_x + rs_width < screenSize.x: rs_x = screenSize.x if rs_y!=unconfigured and rs_y > screenSize.height: rs_y = screenSize.height - 50 if rs_y!=unconfigured and rs_y + rs_height < screenSize.y: rs_y = screenSize.y if screenpct<=0 and (rs_width,rs_height)==window.GetSizeTuple(): # set position only, and no need to resize if rs_x!=unconfigured and rs_y!=unconfigured: print "setting %s to position %d, %d" % (confname, rs_x, rs_y) window.SetPosition(wx.Point(rs_x, rs_y)) else: if rs_x==unconfigured or rs_y==unconfigured: print "setting %s to size %d x %d" % (confname, rs_width, rs_height) window.SetSize(wx.Size(rs_width, rs_height)) else: print "setting %s to position %d, %d - size %d x %d" % (confname, rs_x, rs_y, rs_width, rs_height) window.SetDimensions(rs_x, rs_y, rs_width, rs_height) def save_size(confname, myRect): """Saves size to config. L{set_size} @param confname: Same string as in set_size @param myRect: Window size you want remembered, typically window.GetRect() """ confobj=wx.GetApp().config confname="windows/"+confname x = myRect.x y = myRect.y width = myRect.width height = myRect.height confobj.WriteInt(confname + "/x", x) confobj.WriteInt(confname + "/y", y) confobj.WriteInt(confname + "/width", width) confobj.WriteInt(confname + "/height", height) confobj.Flush() class LogProgressDialog(wx.ProgressDialog): """ display log string and progress bar at the same time """ def __init__(self, title, message, maximum=100, parent=None, style=wx.PD_AUTO_HIDE|wx.PD_APP_MODAL): super(LogProgressDialog, self).__init__(title, message, maximum, parent, style) self.__progress_value=0 def Update(self, value, newmsg='', skip=None): self.__progress_value=value super(LogProgressDialog, self).Update(value, newmsg, skip) def log(self, msgstr): super(LogProgressDialog, self).Update(self.__progress_value, msgstr) class AskPhoneNameDialog(wx.Dialog): def __init__(self, parent, message, caption="Enter phone owner's name"): """ Ask a user to enter an owner's name of a phone. Similar to the wx.TextEntryDialog but has 3 buttons, Ok, No Thanks, and Maybe latter. """ super(AskPhoneNameDialog, self).__init__(parent, -1, caption) vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(wx.StaticText(self, -1, message), 0, wx.ALL, 5) self.__text_ctrl=wx.TextCtrl(self, -1, style=wx.TE_PROCESS_ENTER) vbs.Add(self.__text_ctrl, 0, wx.EXPAND|wx.ALL, 5) vbs.Add(wx.StaticLine(self), 0, wx.EXPAND|wx.ALL, 5) hbs=wx.BoxSizer(wx.HORIZONTAL) ok_btn=wx.Button(self, wx.ID_OK, 'OK') hbs.Add(ok_btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) cancel_btn=wx.Button(self, wx.ID_CANCEL, 'No Thanks') hbs.Add(cancel_btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) maybe_btn=wx.Button(self, wx.NewId(), 'Maybe next time') hbs.Add(maybe_btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) vbs.Add(hbs, 1, wx.ALL, 5) wx.EVT_BUTTON(self, maybe_btn.GetId(), self.__OnMaybe) wx.EVT_TEXT_ENTER(self, self.__text_ctrl.GetId(), self.__OnTextEnter) self.SetSizer(vbs) self.SetAutoLayout(True) vbs.Fit(self) def GetValue(self): return self.__text_ctrl.GetValue() def __OnMaybe(self, evt): self.EndModal(evt.GetId()) def __OnTextEnter(self, _): self.EndModal(wx.ID_OK) class HistoricalDataDialog(wx.Dialog): Current_Data=0 Historical_Data=1 _Historical_Date=1 _Historical_Event=2 def __init__(self, parent, caption='Historical Data Selection', current_choice=Current_Data, historical_date=None, historical_events=None): super(HistoricalDataDialog, self).__init__(parent, -1, caption) vbs=wx.BoxSizer(wx.VERTICAL) hbs=wx.BoxSizer(wx.HORIZONTAL) self.data_selector=wx.RadioBox(self, wx.NewId(), 'Data Selection:', choices=('Current', 'Historical Date', 'Historical Event'), style=wx.RA_SPECIFY_ROWS) self.data_selector.SetSelection(current_choice) wx.EVT_RADIOBOX(self, self.data_selector.GetId(), self.OnSelectData) hbs.Add(self.data_selector, 0, wx.ALL, 5) static_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, 'Historical Date:'), wx.VERTICAL) self.data_date=wx.DatePickerCtrl(self, style=wx.DP_DROPDOWN | wx.DP_SHOWCENTURY) if historical_date is not None: self.data_date.SetValue(wx.DateTimeFromTimeT(historical_date)) self.data_date.Enable(current_choice==self._Historical_Date) static_bs.Add(self.data_date, 1, wx.EXPAND, 0) hbs.Add(static_bs, 0, wx.ALL, 5) # historical events static_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1, 'Historical Events:'), wx.VERTICAL) self.hist_events=wx.ListBox(self, -1, style=wx.LB_SINGLE) if historical_events: self._populate_historical_events(historical_events) self.hist_events.Enable(current_choice==self._Historical_Event) static_bs.Add(self.hist_events, 1, wx.EXPAND, 0) hbs.Add(static_bs, 0, wx.ALL, 5) vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 5) vbs.Add(wx.StaticLine(self), 0, wx.EXPAND|wx.ALL, 5) vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL), 0, wx.ALIGN_CENTER|wx.ALL, 5) self.SetSizer(vbs) self.SetAutoLayout(True) vbs.Fit(self) def OnSelectData(self, evt): self.data_date.Enable(evt.GetInt()==self._Historical_Date) self.hist_events.Enable(evt.GetInt()==self._Historical_Event) def GetValue(self): choice=self.data_selector.GetSelection() if choice==self.Current_Data: mode=self.Current_Data time_t=None elif choice==self._Historical_Date: dt=self.data_date.GetValue() dt.SetHour(23) dt.SetMinute(59) dt.SetSecond(59) mode=self.Historical_Data time_t=dt.GetTicks() else: sel=self.hist_events.GetSelection() if sel==wx.NOT_FOUND: mode=self.Current_Data time_t=None else: mode=self.Historical_Data time_t=self.hist_events.GetClientData(sel) return mode, time_t def _populate_historical_events(self, historical_events): keys=historical_events.keys() keys.sort() keys.reverse() for k in keys: # build the string self.hist_events.Append('%s %02d-Adds %02d-Dels %02d-Mods'%\ (time.strftime('%b %d, %y %H:%M:%S', time.localtime(k)), historical_events[k]['add'], historical_events[k]['del'], historical_events[k]['mod']), k)