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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.