PyCoCuMa-0.4.5-6/0000755000175000017500000000000010311641557014404 5ustar henninghenning00000000000000PyCoCuMa-0.4.5-6/pycocumalib/0000755000175000017500000000000010311641557016713 5ustar henninghenning00000000000000PyCoCuMa-0.4.5-6/pycocumalib/AbstractContactView.py0000644000175000017500000000251710252657644023214 0ustar henninghenning00000000000000""" Base Class for all Widgets displaying/editing a contact (vCard) """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: AbstractContactView.py 82 2004-07-11 13:01:44Z henning $ # # $Id: AbstractContactView.py 82 2004-07-11 13:01:44Z henning $ import vcard import broadcaster class AbstractContactView: def __init__(self, **kws): self._cardhandle = None self._contact = vcard.vCard() #Empty Contact self.registerAtBroadcaster() def registerAtBroadcaster(self): "Register our Widget's Callback Handlers" broadcaster.Register(self.onContactsClose, source='Contacts', title='Closed') def onContactsClose(self): self.bind_contact(None) def bind_contact(self, contact): if contact is None: self._cardhandle = None self._contact = vcard.vCard() else: self._cardhandle = contact.handle() self._contact = contact def cardhandle(self): return self._cardhandle def boundto(self): return self._contact PyCoCuMa-0.4.5-6/pycocumalib/AbstractJournalView.py0000644000175000017500000000245010252657644023227 0ustar henninghenning00000000000000""" Base Class for all Widgets displaying/editing a Journal Entry (vEvent) """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: AbstractJournalView.py 82 2004-07-11 13:01:44Z henning $ import vcalendar import broadcaster class AbstractJournalView: def __init__(self, **kws): self._jourhandle = None self._journal = vcalendar.vEvent() #Empty Calendar Entry self.registerAtBroadcaster() def registerAtBroadcaster(self): "Register our Widget's Callback Handlers" broadcaster.Register(self.onContactsClose, source='Contacts', title='Closed') def onContactsClose(self): self.bind_journal(None) def bind_journal(self, journal): if journal is None: self._jourhandle = None self._journal = vcalendar.vEvent() else: self._jourhandle = journal.handle() self._journal = journal def cardhandle(self): return self._jourhandle def boundto(self): return self._journal PyCoCuMa-0.4.5-6/pycocumalib/Bindings.py0000644000175000017500000000322510252657644021034 0ustar henninghenning00000000000000""" This file defines the menu contents and key bindings. """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: Bindings.py 90 2004-09-11 19:08:25Z henning $ import sys import string from keydefs import * menudefs = [ # underscore prefixes character to underscore ('file', [ ('C_onnect...', '<>'), ('_Disconnect', '<>'), None, ('_Import...', '<>'), ('_Export...', '<>'), None, ('Page _View', '<>'), None, ('_Close', '<>'), ]), ('contact', [ ('Create _New Contact', '<>'), ('_Delete Contact', '<>'), ('_Save Contact', '<>'), None, ('D_uplicate Contact', '<>'), ('_Export Contact', '<>'), ]), ('query', [ ('_Find Contact...', '<>'), ('Find _Next', '<>'), None, ('_Birthdays', '<>') ]), ('view', [ ('_Journal', '<>'), ('_Calendar', '<>'), ('_Letter Composer', '<>') ]), ('settings', [ ('_Edit Preferences', '<>') ]), ('help', [ ('_Help...', '<>'), None, ('_About PyCoCuMa...', '<>'), ]), ] if sys.platform == 'win32': default_keydefs = windows_keydefs else: default_keydefs = unix_keydefs del sys PyCoCuMa-0.4.5-6/pycocumalib/CalendarWidget.py0000644000175000017500000002614010252657644022155 0ustar henninghenning00000000000000""" Calendar Widget (Month View) """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: CalendarWidget.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import Pmw import time import calendar from InputWidgets import ArrowButton class CalendarWidget(Frame): cellwidth = 28 cellheight = 22 CURRENTMONTHBG = '#ffffff' CURRENTMONTHFG = '#000000' OTHERMONTHBG = '#ffffff' OTHERMONTHFG = '#bbbbbb' MARKBG = '#eeeeff' MARKFONT = ('Helvetica', -15, 'bold') SELECTBG = '#7777ee' SELECTFG = '#ffffff' CURRENTSUNDAYFG = '#ee0000' OTHERSUNDAYFG = '#eebbbb' DATEFONT = ('Helvetica', -15) WEEKLABELFONT = ('Helvetica', -12) def __init__(self, master, selectcommand=None, dblclickcommand=None): Frame.__init__(self, master, borderwidth=1, relief=RAISED) self.selectcommand = selectcommand self.dblclickcommand = dblclickcommand self.columnconfigure(2, weight=1) self.lblHeader = Label(self, font=('Helvetica', -14, 'bold')) self.lblHeader.grid(column=2, sticky=W+E) self.btnPrevYear = ArrowButton(self, direction='left', width=20, command=self.prevYear) self.btnPrevYear.grid(row=0, column=0, sticky=W) self.btnPrevMonth = ArrowButton(self, direction='left', command=self.prevMonth) self.btnPrevMonth.grid(row=0, column=1, sticky=W) self.btnNextMonth = ArrowButton(self, direction='right', command=self.nextMonth) self.btnNextMonth.grid(row=0, column=3, sticky=E) self.btnNextYear = ArrowButton(self, direction='right', width=20, command=self.nextYear) self.btnNextYear.grid(row=0, column=4, sticky=E) # Thin Separator (horiz. Line): sep = Frame(self, relief=SUNKEN, borderwidth=1, width=self.cellwidth*7, height=2) sep.grid(row=1, column=0, columnspan=5, sticky=W+E) self.canvas = Canvas(self, width=self.cellwidth*8, height=self.cellheight*7, highlightthickness=0, borderwidth=0) self.canvas.grid(row=2, column=0, columnspan=5, sticky=W+E+S+N) self.cells = [] today = time.gmtime() self.year = today.tm_year self.month = today.tm_mon self.day = today.tm_mday self.marks = {} # Create our 7x7 cell matrix: for y in range(7): row = [] for x in range(7): rect_id = self.canvas.create_rectangle((x+1)*self.cellwidth, y*self.cellheight, (x+2)*self.cellwidth, (y+1)*self.cellheight, width=0) text_id = self.canvas.create_text((x+1)*self.cellwidth + self.cellwidth/2, y*self.cellheight + self.cellheight/2) self.canvas.tag_bind(rect_id, '', lambda event, self=self, x=x, y=y: self._cellClick(x, y, event)) self.canvas.tag_bind(text_id, '', lambda event, self=self, x=x, y=y: self._cellClick(x, y, event)) row.append((rect_id, text_id)) self.cells.append(row) # Create 'Week Of Year' Labels: self.weeklabels = [] for y in range(1,7): text_id = self.canvas.create_text(self.cellwidth/2, y*self.cellheight + self.cellheight/2, font=self.WEEKLABELFONT) self.weeklabels.append(text_id) weekdays = ['Mo','Tu','We','Th','Fr','Sa','Su'] self.months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] # fill the first row in our 7x7 cell matrix with weekdays: for day_no in range(7): rect_id, text_id = self.cells[0][day_no] textcolor = '#000000' self.canvas.itemconfigure(text_id, text=weekdays[day_no], fill=textcolor, font=('Helvetica', -15)) self.update() def prevYear(self): self.setmonth(self.year -1, self.month) def nextYear(self): self.setmonth(self.year +1, self.month) def prevMonth(self): self.setmonth(self.year, self.month -1) def nextMonth(self): self.setmonth(self.year, self.month +1) _lastmouseclicktime = 0 _lastmouseclickday = None def _dayClick(self, day, event): "called by _cellClick" self.setday(day) if self.dblclickcommand\ and event.time - self._lastmouseclicktime <= 400\ and self._lastmouseclickday == day: self.dblclickcommand('%04d-%02d-%02d' % (self.year, self.month, self.day)) elif self.selectcommand: self.selectcommand('%04d-%02d-%02d' % (self.year, self.month, self.day)) self._lastmouseclicktime = event.time self._lastmouseclickday = day def _cellClick(self, cellx, celly, event): day = self.weeks[celly-1][cellx] if day: # this month's date: self._dayClick(day, event) elif celly < 3: # Clicked on a date laying in prev. month self.setmonth(self.year, self.month -1, update=False) self._dayClick(self.otherweeks[(celly-1,cellx)], event) elif celly >= 3: # Clicked on a date laying in next. month self.setmonth(self.year, self.month +1, update=False) self._dayClick(self.otherweeks[(celly-1,cellx)], event) def _getWeekNoOfMonthStart(self): # Returns 1 for January for example dayofyear = 0 wkdayyearstart = 0 for m in range(1, self.month): wkday, numdays = calendar.monthrange(self.year, m) if m == 1: wkdayyearstart = wkday dayofyear += numdays return (dayofyear + wkdayyearstart) / 7 + 1 def _renderCurrentMonthDate(self, rect_id, text_id, day, weekday, today): if (self.year, self.month, day) == (today.tm_year, today.tm_mon, today.tm_mday): borderwidth = 2 else: borderwidth = 0 if day == self.day: # Day is selected: fill = self.SELECTBG textcolor = self.SELECTFG elif weekday == 6: fill = self.CURRENTMONTHBG # Sunday in red: textcolor = self.CURRENTSUNDAYFG else: fill = self.CURRENTMONTHBG textcolor = self.CURRENTMONTHFG if self.marks.has_key('%04d-%02d-%02d' % (self.year, self.month, day)): # Mark has been set for this day: textfont = self.MARKFONT if fill == self.CURRENTMONTHBG: fill = self.MARKBG else: textfont = self.DATEFONT self.canvas.itemconfigure(rect_id, fill=fill, width=borderwidth) if borderwidth: # Raise our box to make border completely visible: self.canvas.tag_raise(rect_id, 'all') self.canvas.itemconfigure(text_id, text=str(day), fill=textcolor, font=textfont) # we must raise our text above the box otherwise we won't see it: self.canvas.tag_raise(text_id, rect_id) def _renderOtherMonthDate(self, rect_id, text_id, day, weekday): "Draw Month Date Cell for dates not in our current month" self.canvas.itemconfigure(rect_id, width=0, fill=self.OTHERMONTHBG) if weekday == 6: textcolor = self.OTHERSUNDAYFG else: textcolor = self.OTHERMONTHFG self.canvas.itemconfigure(text_id, fill=textcolor, text=str(day), font=self.DATEFONT) def update(self): "Update our Calendar Canvas" today = time.gmtime() self.lblHeader.configure( text="%s %d" % (self.months[self.month-1], self.year)) # fill 6x7 matrix with zeros: self.weeks = [[0]*7]*6 # otherweeks holds the day of month for prev. and next months, # otherweeks[(i,j)] will be set where weeks[i][j] is zero: self.otherweeks = {} calweeks = calendar.monthcalendar(self.year, self.month) self.weeks[:len(calweeks)] = calweeks wkmonstart = self._getWeekNoOfMonthStart() lastday = 0 for week, week_no in zip(self.weeks, range(len(self.weeks))): # Set 'Week Number Of Year' Label: self.canvas.itemconfigure(self.weeklabels[week_no], text=str(wkmonstart+week_no)) for day, day_no in zip(week, range(len(week))): rect_id, text_id = self.cells[week_no+1][day_no] if day: self._renderCurrentMonthDate(rect_id, text_id, day, day_no, today) lastday = day elif lastday == 0: # Our Month has not yet started: year = self.year month = self.month - 1 if month < 1: year += -1 month = 12 prevmon_firstweekday, prevmon_numberdays = calendar.monthrange(year, month) dist = 0 for w in range(week_no, len(self.weeks)): for d in range(day_no, len(week)): if self.weeks[w][d]: break dist += 1 if self.weeks[w][d]: break dayofmon = prevmon_numberdays + 1 - dist self.otherweeks[(week_no,day_no)] = dayofmon self._renderOtherMonthDate(rect_id, text_id, dayofmon, day_no) elif lastday > 0: # Next Month: if lastday > 27: lastday = 1 else: lastday += 1 self.otherweeks[(week_no,day_no)] = lastday self._renderOtherMonthDate(rect_id, text_id, lastday, day_no) def setmonth(self, year, month, update=True): "Show this month" self.year = year self.month = month if self.month < 1: self.year += -1 self.month = 12 elif self.month > 12: self.year += 1 self.month = 1 self.setday(self.day, update) def setday(self, day, update=True): "Select this day" self.day = day # Check whether our month includes this day: wkday, numdays = calendar.monthrange(self.year, self.month) if self.day > numdays: self.day = numdays if update: self.update() def setmarks(self, marks): """marks is a dictionary with 'yyyy-mm-dd' strings as keys marked days will be displayed in bold font""" self.marks = marks self.update() def getmarks(self): "Return marks dictionary (can be changed)" return self.marks if __name__ == "__main__": tk = Tk() cal = CalendarWidget(tk) cal.pack() #cal.setmonth(2001,9) #cal.setmarks({(2001,9,11):None}) tk.mainloop() PyCoCuMa-0.4.5-6/pycocumalib/CalendarWindow.py0000644000175000017500000001573710252657644022213 0ustar henninghenning00000000000000""" Calendar Toplevel Window """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: CalendarWindow.py 82 2004-07-11 13:01:44Z henning $ import sys import os import string from Tkinter import * import tkMessageBox import Pmw import debug import broadcaster import broker import time class CalendarWindow: from JournalListWidget import JournalListWidget from JournalEditWidget import JournalEditWidget def __init__(self, model, tkroot=None): if not tkroot: tkroot = Tk() tkroot.withdraw() self.tkroot = tkroot self.model = model # Handle to DateStart Mapping: self._journaldates = {} self.createWidgets() self.centerWindow() self.registerAtBroadcaster() self.onJournalsOpen() def registerAtBroadcaster(self): "Register our Callback Handlers" broadcaster.Register(self.onJournalsOpen, source='Journals', title='Opened') broadcaster.Register(self.onJournalsClose, source='Journals', title='Closed') broadcaster.Register(self.onJournalNew, source='Journal', title='Added') broadcaster.Register(self.onJournalDel, source='Journal', title='Deleted') broadcaster.Register(self.onJournalSave, source='Journal', title='Saved') # Broadcasted by JournalWindow: broadcaster.Register(self.onJournalOpen, source='Journal', title='Opened') def createWidgets(self): "create the top level window" top = self.top = Toplevel(self.tkroot, class_='CalendarWindow') top.protocol('WM_DELETE_WINDOW', self.close) top.title('Calendar') top.iconname('PyCoCuMa') try: os.chdir(os.path.dirname(sys.argv[0])) if sys.platform == "win32": top.iconbitmap("pycocuma.ico") else: top.iconbitmap("@pycocuma.xbm") top.iconmask("@pycocuma_mask.xbm") except: debug.echo("Could not set TopLevel window icon") top.withdraw() from CalendarWidget import CalendarWidget self.monthdisp = CalendarWidget(top, selectcommand=self._daySelect, dblclickcommand=self._dayDblClick) self.monthdisp.grid() def centerWindow(self, relx=0.5, rely=0.3): "Center the Main Window on Screen" widget = self.top master = self.tkroot widget.update_idletasks() # Actualize geometry information if master.winfo_ismapped(): m_width = master.winfo_width() m_height = master.winfo_height() m_x = master.winfo_rootx() m_y = master.winfo_rooty() else: m_width = master.winfo_screenwidth() m_height = master.winfo_screenheight() m_x = m_y = 0 w_width = widget.winfo_reqwidth() w_height = widget.winfo_reqheight() x = m_x + (m_width - w_width) * relx y = m_y + (m_height - w_height) * rely if x+w_width > master.winfo_screenwidth(): x = master.winfo_screenwidth() - w_width elif x < 0: x = 0 if y+w_height > master.winfo_screenheight(): y = master.winfo_screenheight() - w_height elif y < 0: y = 0 widget.geometry("+%d+%d" % (x, y)) widget.deiconify() # Become visible at the desired location def _daySelect(self, date, createnew=0): # This variable prevents a looping callback: self._indayselectcallback = 1 datadict = {'date':date} if createnew: datadict['createnew'] = createnew broadcaster.Broadcast('Calendar', 'Date Selected', data=datadict) self._indayselectcallback = 0 def _dayDblClick(self, date): self.top.event_generate('<>') if self.monthdisp.getmarks().has_key(date): self._daySelect(date, createnew=0) else: # Create New Journal Entry for a free day self._daySelect(date, createnew=1) def onJournalsOpen(self): "Callback, triggered on Broadcast" handles = self.model.ListJournalHandles() # take only the first 10 chars from dtstart, remainder could be time: dates = map(lambda x: x[:10], self.model.QueryJournalAttributes(handles, 'DateStart')) self._journaldates = dict(zip(handles, dates)) self.monthdisp.setmarks(dict(zip(dates,[None]*len(dates)))) def onJournalsClose(self): "Callback, triggered on Broadcast" self.monthdisp.setmarks({}) _indayselectcallback = 0 def onJournalOpen(self): "Callback, triggered on Broadcast by JournalWindow" if not self._indayselectcallback: # dtstart may include time, we take only the first 10 characters: year, month, day = tuple(map(int, broadcaster.CurrentData()['dtstart'][:10].split('-'))) self.monthdisp.setmonth(year, month) self.monthdisp.setday(day) def onJournalNew(self): "Callback, registered at Broadcaster" handle = broadcaster.CurrentData()['handle'] date = broadcaster.CurrentData()['dtstart'][:10] self._journaldates[handle] = date self.monthdisp.getmarks()[date] = 1 self.monthdisp.update() def onJournalDel(self): "Callback, registered at Broadcaster" handle = broadcaster.CurrentData()['handle'] date = self._journaldates[handle] del self._journaldates[handle] del self.monthdisp.getmarks()[date] self.monthdisp.update() def onJournalSave(self): "Callback, registered at Broadcaster" handle = broadcaster.CurrentData()['handle'] # Maybe the entry's date changed: # we take only the first 10 chars, because dtstart may include time: date = self.model.QueryJournalAttributes([handle], 'DateStart')[0][:10] # this mapping is for onDel only: olddate = self._journaldates[handle] if olddate != date: self._journaldates[handle] = date del self.monthdisp.getmarks()[olddate] self.monthdisp.getmarks()[date] = 1 self.monthdisp.update() def close(self, event=None): self.top.withdraw() def window(self): "Returns Tk's TopLevel Widget" return self.top def withdraw(self): "Withdraw: Forward to TopLevel Method" self.top.withdraw() def deiconify(self): "DeIconify: Forward to TopLevel Method" self.top.deiconify() def show(self): self.top.deiconify() self.top.lift() self.top.focus_set() PyCoCuMa-0.4.5-6/pycocumalib/CoCuMa_Client.py0000644000175000017500000002613510252657644021711 0ustar henninghenning00000000000000#!/usr/bin/python # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: CoCuMa_Client.py 82 2004-07-11 13:01:44Z henning $ from __version__ import __version__ import debug class CoCuMa_AbstractClient: "Abstract Base Client" def __init__(self): self.server = None self.connected = False self.errorstring = "" def getErrorString(self): return self.errorstring def Connect(self, connect_str): "Connect to Server and start Session" raise NotImplementedError def Disconnect(self): "Disconnect from Server = Exit Session" raise NotImplementedError def ListHandles(self, sortby): "Returns List of Handles" raise NotImplementedError def QueryAttributes(self, handles, attributes): "Returns a list of a list of attributes" raise NotImplementedError def GetContact(self, handle): "Returns Contact in vCard Format" raise NotImplementedError def PutContact(self, handle, data): "Store Contact and overwrite previous one" raise NotImplementedError def NewContact(self, initdict={}): "Create new Contact and return Handle" raise NotImplementedError def DelContact(self, handle): "Remove Contact" raise NotImplementedError def ListJournalHandles(self, sortby): "Returns List of Handles" raise NotImplementedError def QueryJournalAttributes(self, handles, attributes): "Returns a list of a list of attributes" raise NotImplementedError def GetJournal(self, handle): "Returns Contact in vEvent Format" raise NotImplementedError def PutJournal(self, handle, data): "Store Journal and overwrite previous one" raise NotImplementedError def NewJournal(self, initdict={}): "Create new Contact and return Handle" raise NotImplementedError def DelJournal(self, handle): "Remove Contact" raise NotImplementedError class CoCuMa_XMLRPCClient(CoCuMa_AbstractClient): "Client to the XMLRPC Server (CoCuMa_Server.py)" def __init__(self): CoCuMa_AbstractClient.__init__(self) self.cache_handles = {} self.cache_contacts = {} self.cache_journalhandles = {} self.cache_journals = {} def Connect(self, connect_str="http://localhost:8810"): "Connect to Server and start Session" self.errorstring = "" import xmlrpclib, socket retries = 4 # four connection retries for i in range(retries): try: self.server = xmlrpclib.ServerProxy(connect_str) ver = self.server.SessionInit() if ver == "CoCuMa_Server "+__version__: self.connected = True return True # Alright, we made it! else: self.errorstring = "Version Mismatch" debug.echo("CoCuMa_XMLRPCClient.Connect(): Could not connect to server: %s" % self.errorstring) # We don't need to retry, # because the server's version won't change ;-) return False except socket.error, detail: errno, self.errorstring = detail if errno != 111: return False except Exception, detail: self.errorstring = detail debug.echo("CoCuMa_XMLRPCClient.Connect(): Retrying connection to server..") import time # Exponential Backoff (wait up to 1.6 sec): time.sleep(0.2*(2**i)) return False def Disconnect(self): "Disconnect from Server = Exit Session" self.server.SessionQuit() self.server = None self.connected = False def ListHandles(self, sortby): "Returns List of Handles" if self.connected: if not self.cache_handles.has_key(sortby): self.cache_handles[sortby] = self.server.ListHandles(sortby) return self.cache_handles[sortby] else: return None def QueryAttributes(self, handles, attributes): "Returns a list of a list of attributes" if self.connected: return self.server.QueryAttributes(handles, attributes) else: return None def GetContact(self, handle): "Returns Contact in vCard Format" try: return self.cache_contacts[handle] except: if self.connected: data = self.server.GetContact(handle) self.cache_contacts[handle] = data return data else: return None def PutContact(self, handle, data): "Store Contact and overwrite previous one" if self.connected: try: self.cache_handles.clear() del self.cache_contacts[handle] except: pass return self.server.PutContact(handle, data) else: return None def NewContact(self, initdict={}): "Create new Contact and return Handle" if self.connected: self.cache_handles.clear() return self.server.NewContact(initdict) else: return None def DelContact(self, handle): "Remove Contact" if self.connected: try: del self.cache_contacts[handle] except: pass self.cache_handles.clear() return self.server.DelContact(handle) else: return None def ListJournalHandles(self, sortby): "Returns List of Handles" if self.connected: if not self.cache_journalhandles.has_key(sortby): self.cache_journalhandles[sortby] = self.server.ListJournalHandles(sortby) return self.cache_journalhandles[sortby] else: return None def QueryJournalAttributes(self, handles, attributes): "Returns a list of a list of attributes" if self.connected: return self.server.QueryJournalAttributes(handles, attributes) else: return None def GetJournal(self, handle): "Returns Contact in vEvent Format" try: return self.cache_journals[handle] except: if self.connected: data = self.server.GetJournal(handle) self.cache_journals[handle] = data return data else: return None def PutJournal(self, handle, data): "Store Journal and overwrite previous one" if self.connected: try: self.cache_journalhandles.clear() del self.cache_journals[handle] except: pass return self.server.PutJournal(handle, data) else: return None def NewJournal(self, initdict={}): "Create new Journal and return Handle" if self.connected: self.cache_journalhandles.clear() return self.server.NewJournal(initdict) else: return None def DelJournal(self, handle): "Remove Journal" if self.connected: try: del self.cache_journals[handle] except: pass self.cache_journalhandles.clear() return self.server.DelJournal(handle) else: return None class CoCuMa_FileClient(CoCuMa_AbstractClient): "Ordinary Filesystem Backend" def __init__(self): CoCuMa_AbstractClient.__init__(self) def Connect(self, connect_str): "Connect to Server and start Session" try: try: import os.path from CoCuMa_Server import CoCuMa_Server # XXX: Not very nice to use '.ics': root, ext = os.path.splitext(connect_str) self.server = CoCuMa_Server(addressbook_fname = connect_str, calendar_fname = root+'.ics') self.server.SessionInit() self.connected = True except Exception, detail: self.errorstring = detail finally: return self.connected def Disconnect(self): "Disconnect from Server = Exit Session" self.server.SessionQuit() self.server = None self.connected = False def ListHandles(self, sortby): "Returns List of Handles" if self.connected: return self.server.ListHandles(sortby) else: return None def QueryAttributes(self, handles, attributes): "Returns a list of a list of attributes" if self.connected: return self.server.QueryAttributes(handles, attributes) else: return None def GetContact(self, handle): "Returns Contact in vCard Format" if self.connected: return self.server.GetContact(handle) else: return None def PutContact(self, handle, data): "Store Contact and overwrite previous one" if self.connected: return self.server.PutContact(handle, data) else: return None def NewContact(self, initdict={}): "Create new Contact and return Handle" if self.connected: return self.server.NewContact(initdict) else: return None def DelContact(self, handle): "Remove Contact" if self.connected: return self.server.DelContact(handle) else: return None def ListJournalHandles(self, sortby): "Returns List of Handles" if self.connected: return self.server.ListJournalHandles(sortby) else: return None def QueryJournalAttributes(self, handles, attributes): "Returns a list of a list of attributes" if self.connected: return self.server.QueryJournalAttributes(handles, attributes) else: return None def GetJournal(self, handle): "Returns Journal in vCard Format" if self.connected: return self.server.GetJournal(handle) else: return None def PutJournal(self, handle, data): "Store Journal and overwrite previous one" if self.connected: return self.server.PutJournal(handle, data) else: return None def NewJournal(self, initdict={}): "Create new Journal and return Handle" if self.connected: return self.server.NewJournal(initdict) else: return None def DelJournal(self, handle): "Remove Journal" if self.connected: return self.server.DelJournal(handle) else: return None def Instantiate(con_type): "Instantiate Client Object depending on Connection Type" if con_type == 'xmlrpc': return CoCuMa_XMLRPCClient() elif con_type == 'file': return CoCuMa_FileClient() else: return None PyCoCuMa-0.4.5-6/pycocumalib/CoCuMa_Server.py0000644000175000017500000002151110252657644021732 0ustar henninghenning00000000000000#!/usr/bin/python # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: CoCuMa_Server.py 92 2004-11-28 15:34:44Z henning $ import SimpleXMLRPCServer, SocketServer from types import * import time import os import vcard import vcalendar from __version__ import __version__ import sys, signal import Preferences import debug import broadcaster class XMLRPCServer(SocketServer.TCPServer, SimpleXMLRPCServer.SimpleXMLRPCDispatcher): """Overridden SimpleXMLRPCServer We want allow_reuse_address==True""" def __init__(self, addr, requestHandler=SimpleXMLRPCServer.SimpleXMLRPCRequestHandler, logRequests=1): self.logRequests = logRequests self.allow_reuse_address = 1 SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self) SocketServer.TCPServer.__init__(self, addr, requestHandler) class CoCuMa_Server: def __init__(self, addressbook_fname, calendar_fname): self._cards_modified = False self._cal_modified = False self._addressbook_filename = addressbook_fname self._calendar_filename = calendar_fname broadcaster.Broadcast('Notification', 'Status', {'message':"Loading vCards from file '%s'..." % (self._addressbook_filename,)}) self._vcards = vcard.vCardList() self._vcards.LoadFromFile(self._addressbook_filename) broadcaster.Broadcast('Notification', 'Status', {'message':"Loading iCalendar from file '%s'..." % (self._calendar_filename,)}) self._vcalendar = vcalendar.vCalendar() self._vcalendar.LoadFromFile(self._calendar_filename) def _writeToDisk(self): "save our vcards and our vcalendar on disk" if self._cards_modified: self._vcards.SaveToFile(self._addressbook_filename) if self._cal_modified: self._vcalendar.SaveToFile(self._calendar_filename) def _shutdown(self): "Server Stop" # saving is now done on session exit: # Redundant: self._writeToDisk() pass def _signal_handler(self, signalnum, stackframe): "Trap Unix Signals" if signalnum == signal.SIGTERM: self._shutdown() sys.exit(0) def SessionInit(self): "Send My Identification to Client" return "CoCuMa_Server "+__version__ def SessionQuit(self): "Exit Session and save to disk" self._writeToDisk() return True def ListHandles(self, sortby=None): """Returns list of vCard Handles sortby is the fieldname to order by""" return self._vcards.sortedlist(sortby) def ListJournalHandles(self, sortby): "Returns list of vCalendar Handles" return self._vcalendar.sortedlist(sortby) def QueryAttributes(self, handles, attributes): """Returns a list of tuples of attribute values or a single list of strings if attributes is not a list or tuple""" return self._queryAttributes(self._vcards, handles, attributes) def QueryJournalAttributes(self, handles, attributes): """Returns a list of tuples of attribute values or a single list of strings if attributes is not a list or tuple""" return self._queryAttributes(self._vcalendar, handles, attributes) def _queryAttributes(self, vobj, handles, attributes): """Returns a list of tuples of attribute values or a single list of strings if attributes is not a list or tuple""" ret = [] try: if type(attributes)==ListType or type(attributes)==TupleType: for handle in handles: attrvals = [] for attr in attributes: attrvals.append(vobj[handle].getFieldValueStr(attr)) ret.append(tuple(attrvals)) else: for handle in handles: ret.append(vobj[handle].getFieldValueStr(attributes)) vobj.forgetLastReturnedField() finally: return ret def GetContact(self, handle): "Returns Contact as vCard" return self._vcards[handle].VCF_repr() def PutContact(self, handle, data): "Store Contact (overwrite)" self._vcards[handle] = vcard.vCard(data) # Our modified Contact lost its Handle: self._vcards[handle].sethandle(handle) # verify that our storing was correct: verify = self._vcards[handle].VCF_repr() == data if verify: # Update our Revision Date: self._vcards[handle].rev.set(time.gmtime()) self._cards_modified = True return verify def NewContact(self, initdict={}): "Create new vCard and return it's handle" card = vcard.vCard() # Set initial dictionary: for key, value in zip(initdict.keys(), initdict.values()): getattr(card, key).set(value) self._cards_modified = True return self._vcards.add(card) def DelContact(self, handle): "Delete Contact" self._cards_modified = True return self._vcards.delete(handle) def GetJournal(self, handle): "Returns Calendar Entry as vEvent" # Update our TimeStamp: self._vcalendar[handle].dtstamp.set(time.gmtime()) return self._vcalendar[handle].VCF_repr() def PutJournal(self, handle, data): "Store Calendar Entry (overwrite)" self._vcalendar[handle] = vcalendar.vEvent(data) # Our modified Journal lost its Handle: self._vcalendar[handle].sethandle(handle) # verify that our storing was correct: verify = self._vcalendar[handle].VCF_repr() == data if verify: # Update our Revision Date: self._vcalendar[handle].last_mod.set(time.gmtime()) self._cal_modified = True else: debug.echo("WARNING: CoCuMa_Server.PutJournal(): Verify Failed for #%d:" % handle) debug.echo(self._vcalendar[handle].VCF_repr()) return verify def NewJournal(self, initdict={}): "Create new vEvent and return it's handle" jour = vcalendar.vEvent() # Set initial dictionary: for key, value in zip(initdict.keys(), initdict.values()): getattr(jour, key).set(value) self._cal_modified = True return self._vcalendar.add(jour) def DelJournal(self, handle): "Delete Calendar Entry" self._cards_modified = True return self._vcalendar.delete(handle) class LogFile: "file-like class, appends to a file, or does nothing" def __init__(self, filename): self.fd = None if filename: self.fd = file(filename, 'ab') def write(self, obj): if self.fd: self.fd.write(obj) self.fd.flush() def run(): Preferences.Load() import getopt optlist, args = getopt.getopt(sys.argv[1:], "h:p:f:j:") for key, val in optlist: # Command Line Arguments override Preferences: if key == "-p": Preferences.set("server.listen_port",val) if key == "-h": Preferences.set("server.listen_host",val) if key == "-f": Preferences.set("server.addressbook_filename",val) if key == "-j": Preferences.set("server.calendar_filename",val) import socket try: xmlsrv = XMLRPCServer((Preferences.get("server.listen_host"), int(Preferences.get("server.listen_port")))) except socket.error: # Try to connect to ourself (check if we are already running): import CoCuMa_Client conn_str = "http://%s:%d" % (Preferences.get("server.listen_host"), int(Preferences.get("server.listen_port"))) client = CoCuMa_Client.CoCuMa_XMLRPCClient() if client.Connect(conn_str): sys.exit("CoCuMa_Server.run(): I'm already running. Terminating!") else: raise # Unhandled/unknown Error # Create our Server-Object: server = CoCuMa_Server(addressbook_fname = os.path.expanduser( Preferences.get("server.addressbook_filename")), calendar_fname = os.path.expanduser( Preferences.get("server.calendar_filename"))) # Register our public (XML) accessible Methods: xmlsrv.register_instance(server) # Trap UNIX-Signals: signal.signal(signal.SIGTERM, server._signal_handler) # Redirect Stderr and Stdout: log = LogFile(Preferences.get("server.log_filename")) sys.stderr = log sys.stdout = log xmlsrv.serve_forever() if __name__=='__main__': run() PyCoCuMa-0.4.5-6/pycocumalib/ConnectDialog.py0000644000175000017500000000611510252657644022011 0ustar henninghenning00000000000000#!/usr/bin/python # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: ConnectDialog.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import Pmw class ConnectDialog(Pmw.Dialog): def __init__(self, master, title="", class_=None): Pmw.Dialog.__init__(self, master=master, title=title, buttons=('Connect','Cancel'), defaultbutton='Connect') connecttypes = [("xmlrpc", "Connect to XML-RPC Server"), ("file", "Open File from Disk")] self.conn_type_var = StringVar() self.conn_type_var.set(connecttypes[0][0]) self.conn_str_var = StringVar() self.interior().columnconfigure(0, weight=1) grp = Pmw.Group(self.interior(), tag_text = "Connection Type") grp.grid(sticky=W+E, padx=2, pady=2) for type in connecttypes: lbl = Radiobutton(grp.interior(), text=type[1], value = type[0], variable = self.conn_type_var, command = self.radio_change_event) lbl.grid(sticky=W, padx=2, pady=2) grp = Pmw.Group(self.interior(), tag_text = "Connection String") grp.grid(sticky=W+E, padx=2, pady=2) grp.interior().columnconfigure(0, weight=1) lbl = Entry(grp.interior(), textvariable = self.conn_str_var, width=24) lbl.grid(sticky=W+E, padx=2, pady=2) self.btnBrowse = Button(grp.interior(), text="Browse..", command=self.browse_event) self.btnBrowse.grid(row=0, column=1, padx=2, pady=2) self.btnBrowse.grid_remove() def getvalue(self): return (self.conn_type_var.get(), self.conn_str_var.get()) def activate(self, type, defaultstrings): self.defaultstrings = defaultstrings self.conn_type_var.set(type) self.conn_str_var.set(defaultstrings[type]) if type == 'file': self.btnBrowse.grid() else: self.btnBrowse.grid_remove() Pmw.Dialog.activate(self) def radio_change_event(self): type = self.conn_type_var.get() self.conn_str_var.set(self.defaultstrings[type]) if type == 'file': self.btnBrowse.grid() else: self.btnBrowse.grid_remove() def browse_event(self): import tkFileDialog, os try: dir = os.basename(self.conn_str_var.get()) except: dir = "" dlg = tkFileDialog.Open(filetypes=[("vCard","*.vcf")], initialdir=dir, initialfile=self.conn_str_var.get()) ret = dlg.show() if ret: self.conn_str_var.set(ret) if __name__ == "__main__": # Unit Test: tk = Tk() dlg = ConnectDialog(tk, title="Connect") dlg.activate('xmlrpc', {"xmlrpc": "http://localhost:8810", "file": "~/addressbook.vcf"}) tk.destroy() PyCoCuMa-0.4.5-6/pycocumalib/ContactEditWidget.py0000644000175000017500000006322710252657644022654 0ustar henninghenning00000000000000# Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: ContactEditWidget.py 93 2004-11-28 16:05:34Z henning $ from Tkinter import * import Pmw import vcard import string import debug import IconImages import broadcaster import broker from AbstractContactView import * import ToolTip PADX = PADY = 1 ToolTips = { "pref": "Preferred", "home": "Home", "work": "Work", "intl": "International delivery address", "postal": "Postal delivery address", "parcel": "Parcel delivery address", "voice": "Voice phone", "cell": "Cellular phone", "pager": "Pager", "car": "Car-phone", "fax": "Facsimile (FAX)", "modem": "Modem connected", "msg": "Voice Messaging support", "isdn": "ISDN service telephone number", "video": "Video conferencing support"} # stores the handle of the contact under edit: affectedContact = None def _broadcast_contact_modify(): broadcaster.Broadcast('Contact', 'Modified', {'handle':affectedContact}, onceonly=1) currentEditControl = None def _setcurrentEditControl(event): global currentEditControl currentEditControl = event.widget import InputWidgets from InputWidgets import MultiRecordEdit class DateEdit(InputWidgets.DateEdit): def __init__(self, master, **kws): InputWidgets.DateEdit.__init__(self, master, **kws) self.add_save_hook(_broadcast_contact_modify) class MemoEdit(InputWidgets.MemoEdit): def __init__(self, master): InputWidgets.MemoEdit.__init__(self, master) self.add_save_hook(_broadcast_contact_modify) self.bind('', _setcurrentEditControl) class TextEdit(InputWidgets.TextEdit): def __init__(self, master): InputWidgets.TextEdit.__init__(self, master) self.add_save_hook(_broadcast_contact_modify) class MultiSelectButtons(InputWidgets.MultiSelectButtons): def __init__(self, master, buttondefs, icons, iconsgrey): InputWidgets.MultiSelectButtons.__init__(self, master, buttondefs, icons, iconsgrey) self.add_save_hook(_broadcast_contact_modify) class UTCOffsetPropEdit(InputWidgets.AbstractSingleVarEdit, Pmw.EntryField): import re _utcoffsetregex = re.compile('[-+][0-2][0-9]:[0-5][0-9]') def __init__(self, master, title, descr): InputWidgets.AbstractSingleVarEdit.__init__(self) Pmw.EntryField.__init__(self, master, entry_width = 10, entry_justify = LEFT, value = "+00:00", validate = {'validator' : self.validate}, modifiedcommand=self.save) ToolTip.ToolTip(self, descr) def toolTipMaster(self): "Returns real Tk widget for use by ToolTip" return self.component('entry') def validate(self, str): if str: if not str[0] in "0123456789+-:": return 0 #ERROR elif self._utcoffsetregex.match(str) is None: return -1 #PARTIAL else: return 1 #OK else: return -1 #PARTIAL def get(self): return self.getvalue() # inherited from Pmw.EntryField def set(self, val): self.setvalue(val) class LatLongPropEdit(InputWidgets.AbstractSingleVarEdit, Frame): def __init__(self, master, title, descr): InputWidgets.AbstractSingleVarEdit.__init__(self) Frame.__init__(self, master) self.edtLat = Pmw.EntryField(self, entry_width = 8, entry_justify = LEFT, value = "0.0", validate = {'validator' : 'real', 'min':-90.0,'max':90.0}, modifiedcommand=self.save) ToolTip.ToolTip(self.edtLat.component('entry'), "Latitude as Float (53.5)") self.edtLat.grid(row=0, column=0) self.edtLon = Pmw.EntryField(self, entry_width = 8, entry_justify = LEFT, value = "0.0", validate = {'validator' : 'real', 'min':-90.0,'max':90.0}, modifiedcommand=self.save) ToolTip.ToolTip(self.edtLon.component('entry'), "Longitude as Float (10.0)") self.edtLon.grid(row=0, column=1) self.bind('', _setcurrentEditControl) def clear(self): self.edtLat.clear() self.edtLon.clear() def get(self): ret= str(self.edtLat.getvalue())+";"+str(self.edtLon.getvalue()) if ret==";": ret = "" return ret def set(self, val): parts = val.split(";") if len(parts)<2: parts = ["",""] self.edtLat.setvalue(parts[0]) self.edtLon.setvalue(parts[1]) class UIDControl(Frame): def __init__(self, master): Frame.__init__(self, master) self._contact = None self.__createWidgets() def __createWidgets(self): master = self master.columnconfigure(1, weight=1) label = Label(master, text="UID:") label.grid() self.__ctrUID = TextEdit(master) self.__ctrUID.grid(column=1, row=0, sticky=W+E) self._btnGenerate = Button(master, image=IconImages.IconImages["generate"], command=self._generateUID) self._btnGenerate.grid(column=2, row=0, sticky=W+E) ToolTip.ToolTip(self._btnGenerate, "generate new unique identifier") def _generateUID(self): "Generate Unique Identifier: xxxx-0000-00" # where xxxx is the fst part of the DisplayName # and 0000-00 are numbers from the md5-sum if self._contact: def isascii(c): return c in string.ascii_lowercase def isdigit(c): return c in string.digits import time, md5 dispname = self._contact.getDisplayName() alphastr = filter(isascii, dispname.lower()) # Alternative: time.strftime("%Y%m%d-%H%M%S") md = md5.new(alphastr+str(self._contact.handle())) sndalstr = filter(isascii, md.hexdigest().lower()) digitstr = filter(isdigit, md.hexdigest()) fstpart = alphastr[:4] pad = sndalstr[:(4-len(fstpart))] uidstr = fstpart+pad+"-"+digitstr[:4]+"-"+digitstr[-2:] self.__ctrUID.set(uidstr) def bindto(self, contact): self._contact = contact self.__ctrUID.bindto(contact.uid) class NameEdit(Pmw.Group): def __init__(self, master): Pmw.Group.__init__(self, master, tag_text = "Name") self.__createWidgets() def __createWidgets(self): master = self.interior() master.columnconfigure(1, weight=1) master.columnconfigure(3, weight=1) label = Label(master, text="Given:") label.grid(column=0,row=0) self.__edtGiven = TextEdit(master) self.__edtGiven.grid(column=1, row=0, sticky=W+E, padx=PADX, pady=PADY) label = Label(master, text="Middle:") label.grid(column=2,row=0) self.__edtMiddle = TextEdit(master) self.__edtMiddle.grid(column=3, row=0, sticky=W+E, padx=PADX, pady=PADY) label = Label(master, text="Family:") label.grid(column=0,row=1) self.__edtFamily = TextEdit(master) self.__edtFamily.grid(column=1, row=1, sticky=W+E, padx=PADX, pady=PADY) label = Label(master, text="Nick:") label.grid(column=2,row=1) self.__edtNick = TextEdit(master) self.__edtNick.grid(column=3, row=1, sticky=W+E, padx=PADX, pady=PADY) label = Label(master, text="Prefix:") label.grid(column=0,row=2) self.__edtPrefixes = TextEdit(master) self.__edtPrefixes.grid(column=1, row=2, sticky=W+E, padx=PADX, pady=PADY) label = Label(master, text="Suffix:") label.grid(column=2,row=2) self.__edtSuffixes = TextEdit(master) self.__edtSuffixes.grid(column=3, row=2, sticky=W+E, padx=PADX, pady=PADY) def getFormattedName(self): "constructs the FormattedName from the name components" parts = [] parts.append(self.__edtPrefixes.get()) parts.append(self.__edtGiven.get()) parts.append(self.__edtMiddle.get()) parts.append(self.__edtFamily.get()) parts.append(self.__edtSuffixes.get()) parts = filter(None, parts) return string.join(parts, " ") def fromFormattedName(self, fn): "tries to split name components" parts = map(string.strip, fn.split()) if len(parts) == 3: self.__edtGiven.set(parts[0]) self.__edtMiddle.set(parts[1]) self.__edtFamily.set(parts[2]) elif len(parts) == 2: self.__edtGiven.set(parts[0]) self.__edtFamily.set(parts[1]) elif len(parts) == 1: self.__edtGiven.set(parts[0]) self.__edtFamily.set(parts[1]) def bindto(self, n, nick): self.__edtGiven.bindto(n.given) self.__edtMiddle.bindto(n.additional) self.__edtFamily.bindto(n.family) self.__edtPrefixes.bindto(n.prefixes) self.__edtSuffixes.bindto(n.suffixes) self.__edtNick.bindto(nick) class AddressEdit(MultiRecordEdit): def __init__(self, master): MultiRecordEdit.__init__(self, master, vcard.vC_adr, "Address") def createBody(self): master = self.body master.columnconfigure(1, weight=1) master.columnconfigure(3, weight=1) label = Label(master, text="PO:") label.grid(column=0,row=1) self.__edtPostOffice = TextEdit(master) self.__edtPostOffice.grid(column=1, row=1, sticky=W+E, padx=PADX, pady=PADY) self.__edtPostOffice.component('entry').bind('<1>', self.onMouseClick) ToolTip.ToolTip(self.__edtPostOffice, "Post Office box") label = Label(master, text="Extd:") label.grid(column=2,row=1) self.__edtExtended = TextEdit(master) self.__edtExtended.grid(column=3, row=1, sticky=W+E, padx=PADX, pady=PADY) self.__edtExtended.component('entry').bind('<1>', self.onMouseClick) ToolTip.ToolTip(self.__edtExtended, "Extended address") label = Label(master, text="Street:") label.grid(column=0,row=2) self.__edtStreet = TextEdit(master) self.__edtStreet.grid(column=1, columnspan=3, row=2, sticky=W+E, padx=PADX, pady=PADY) self.__edtStreet.component('entry').bind('<1>', self.onMouseClick) label = Label(master, text="Code:") label.grid(column=0,row=3) self.__edtPostalCode = TextEdit(master) self.__edtPostalCode.grid(column=1, row=3, sticky=W+E, padx=PADX, pady=PADY) self.__edtPostalCode.component('entry').bind('<1>', self.onMouseClick) label = Label(master, text="City:") label.grid(column=2,row=3) self.__edtCity = TextEdit(master) self.__edtCity.grid(column=3, row=3, sticky=W+E, padx=PADX, pady=PADY) self.__edtCity.component('entry').bind('<1>', self.onMouseClick) label = Label(master, text="Region:") label.grid(column=0,row=4) self.__edtRegion = TextEdit(master) self.__edtRegion.grid(column=1, row=4, sticky=W+E, padx=PADX, pady=PADY) self.__edtRegion.component('entry').bind('<1>', self.onMouseClick) label = Label(master, text="Country:") label.grid(column=2,row=4) self.__edtCountry = TextEdit(master) self.__edtCountry.grid(column=3, row=4, sticky=W+E, padx=PADX, pady=PADY) self.__edtCountry.component('entry').bind('<1>', self.onMouseClick) label = Label(master, text="Type:") label.grid(column=0,row=5) btndefs = [] for key in vcard.vC_adr_types: if ToolTips.has_key(key): btndefs.append((key, ToolTips[key])) self.__selType = MultiSelectButtons(master, btndefs, IconImages.IconImages, IconImages.IconImagesGrey) self.__selType.grid(column=1, columnspan=3, row=5, sticky=W) def bodyChildren(self): return [\ self.__edtPostOffice, self.__edtExtended, self.__edtStreet, self.__edtPostalCode, self.__edtCity, self.__edtRegion, self.__edtCountry, self.__selType] def onMouseClick(self, event=None): if self.state == DISABLED: self._AddRecord() def onRecordAdd(self, rec): _broadcast_contact_modify() def onRecordDel(self): _broadcast_contact_modify() def bindtorec(self, rec): adr = rec if adr is not None: pobox = adr.pobox extended = adr.extended street = adr.street postcode = adr.postcode city = adr.city region = adr.region country = adr.country type = adr.params.get("type") else: pobox = None extended = None street = None postcode = None city = None region = None country = None type = None self.__edtPostOffice.bindto(pobox) self.__edtExtended.bindto(extended) self.__edtStreet.bindto(street) self.__edtPostalCode.bindto(postcode) self.__edtCity.bindto(city) self.__edtRegion.bindto(region) self.__edtCountry.bindto(country) self.__selType.bindto(type) class PhoneEdit(MultiRecordEdit): def __init__(self, master): MultiRecordEdit.__init__(self, master, vcard.vC_tel, "Telephone", "Phone") def createBody(self): master = self.body label = Label(master, text="Number:") label.grid(column=0,row=1) master.columnconfigure(1, weight=1) self.__edtNumber = TextEdit(master) self.__edtNumber.grid(column=1, row=1, sticky=W+E, padx=PADX, pady=PADY) self.__edtNumber.component('entry').bind('<1>', self.onMouseClick) label = Label(master, text="Type:") label.grid(column=0,row=5) btndefs = [] for key in vcard.vC_tel_types: if ToolTips.has_key(key): btndefs.append((key, ToolTips[key])) self.__selType = MultiSelectButtons(master, btndefs, IconImages.IconImages, IconImages.IconImagesGrey) self.__selType.grid(column=1, columnspan=2, row=5, sticky=W, padx=PADX, pady=PADY) def bodyChildren(self): return [\ self.__edtNumber, self.__selType] def onMouseClick(self, event=None): if self.state == DISABLED: self._AddRecord() def onRecordAdd(self, rec): _broadcast_contact_modify() def onRecordDel(self): _broadcast_contact_modify() def bindtorec(self, rec): if rec is not None: type = rec.params.get("type") else: type = None self.__edtNumber.bindto(rec) self.__selType.bindto(type) class EmailEdit(MultiRecordEdit): def __init__(self, master): MultiRecordEdit.__init__(self, master, vcard.vC_email, "Email") def createBody(self): master = self.body label = Label(master, text="Email:") label.grid(column=0,row=1) btndefs = [] for key in vcard.vC_email_types: if ToolTips.has_key(key): btndefs.append((key, ToolTips[key])) self.__selType = MultiSelectButtons(master, btndefs, IconImages.IconImages, IconImages.IconImagesGrey) self.__selType.grid(column=1, row=1, sticky=W, padx=PADX, pady=PADY) master.columnconfigure(2, weight=1) self.__edtEmail = TextEdit(master) self.__edtEmail.grid(column=2, row=1, sticky=W+E, padx=PADX, pady=PADY) self.__edtEmail.component('entry').bind('<1>', self.onMouseClick) def bodyChildren(self): return \ [self.__selType, self.__edtEmail] def onMouseClick(self, event=None): if self.state == DISABLED: self._AddRecord() def onRecordAdd(self, rec): _broadcast_contact_modify() def onRecordDel(self): _broadcast_contact_modify() def bindtorec(self, rec): if rec is not None: type = rec.params.get("type") else: type = None self.__edtEmail.bindto(rec) self.__selType.bindto(type) class OrganizationEdit(Pmw.Group): def __init__(self, master): Pmw.Group.__init__(self, master, tag_text = "Organization") self.__createWidgets() def __createWidgets(self): master = self.interior() master.columnconfigure(1, weight=1) master.columnconfigure(3, weight=1) label = Label(master, text="Name:") label.grid(column=0,row=0) self.__edtName = TextEdit(master) self.__edtName.grid(column=1, columnspan=3, row=0, sticky=W+E, padx=PADX, pady=PADY) label = Label(master, text="Units:") label.grid(column=0,row=1) self.__edtUnits = TextEdit(master) self.__edtUnits.grid(column=1, columnspan=3, row=1, sticky=W+E, padx=PADX, pady=PADY) label = Label(master, text="Title:") label.grid(column=0,row=2) self.__edtTitle = TextEdit(master) self.__edtTitle.grid(column=1, row=2, sticky=W+E, padx=PADX, pady=PADY) label = Label(master, text="Role:") label.grid(column=2,row=2) self.__edtRole = TextEdit(master) self.__edtRole.grid(column=3, row=2, sticky=W+E, padx=PADX, pady=PADY) def getOrganization(self): return self.__edtName.get() def bindto(self, org, title, role): if org is None: orgname = None orgunits = None else: orgname = org.org orgunits = org.units self.__edtName.bindto(orgname) self.__edtUnits.bindto(orgunits) self.__edtTitle.bindto(title) self.__edtRole.bindto(role) class NoteEdit(Pmw.Group): def __init__(self, master): Pmw.Group.__init__(self, master, tag_text = "Note") master = self.interior() self.edtNote = MemoEdit(master) master.columnconfigure(0, weight=1) master.rowconfigure(0, weight=1) self.edtNote.grid(sticky=W+E+S+N, padx=PADX, pady=PADY) def bindto(self, var): self.edtNote.bindto(var) class CategoriesEdit(Pmw.Group): def __init__(self, master): Pmw.Group.__init__(self, master, tag_text = "Categories") master = self.interior() self.edtCategories = TextEdit(master) master.columnconfigure(0, weight=1) self.edtCategories.grid(sticky=W+E, padx=PADX, pady=PADY) ToolTip.ToolTip(self.edtCategories, "list of categories separated by comma ','") def bindto(self, var): self.edtCategories.bindto(var) class URLEdit(Pmw.Group): def __init__(self, master): Pmw.Group.__init__(self, master, tag_text = "URL") master = self.interior() self.edtURL = TextEdit(master) self.edtURL.add_save_hook(self.updatestate) self.btnGotoURL = Button(master, command=self.gotoURL, image=IconImages.IconImages["webbrowser"], state=DISABLED) self.btnGotoURL.bind("", self.updatestate) master.columnconfigure(0, weight=1) master.rowconfigure(0, weight=1) self.edtURL.grid(sticky=W+E+S+N, padx=PADX, pady=PADY) self.btnGotoURL.grid(row=0, column=1, padx=PADX, pady=PADY) def gotoURL(self): import webbrowser webbrowser.open(self.edtURL.get(), 1) def updatestate(self, event=None): if self.edtURL.get(): self.btnGotoURL["state"] = NORMAL else: self.btnGotoURL["state"] = DISABLED def bindto(self, var): self.edtURL.bindto(var) self.updatestate() class ContactEditWidget(AbstractContactView, Frame): def __init__(self, master, **kws): AbstractContactView.__init__(self, **kws) Frame.__init__(self, master, class_="ContactEdit") self.propeditor = None self.__createWidgets() broker.Register("Current Contact", lambda self=self: self._contact) # Catch every Mouse-Click: self.bind_all('<1>', self.__EditControlSave) self.bind_all('<2>', self.__EditControlSave) self.bind_all('<3>', self.__EditControlSave) def __EditControlSave(self, event=None): if currentEditControl: try: currentEditControl.save() except: pass def bind_contact(self, contact): global affectedContact if contact: affectedContact = contact.handle() else: affectedContact = None AbstractContactView.bind_contact(self, contact) self.rebindWidgets() def getAddtlFields(self, contact): "Returns list of vCard field objects suitable for PropertyEditor" return [ contact.sort_string, contact.mailer, contact.key, contact.tz, contact.geo, contact.label, contact.photo, contact.logo] def rebindWidgets(self): self.edtFormattedName.bindto(self._contact.fn) self.edtBirthday.bindto(self._contact.bday) self.ctrUID.bindto(self._contact) self.edtName.bindto(self._contact.n, self._contact.nickname) self.edtOrganization.bindto(self._contact.org, self._contact.title, self._contact.role) self.edtAddress.bindto(self._contact.adr) self.edtPhone.bindto(self._contact.tel) self.edtEmail.bindto(self._contact.email) self.edtNote.bindto(self._contact.note) self.edtCategories.bindto(self._contact.categories) self.edtURL.bindto(self._contact.url) if self.propeditor: self.propeditor.bindto(self.getAddtlFields(self._contact)) def __takeFormattedNameFromName(self): self.edtFormattedName.clear() fn = self.edtName.getFormattedName() if not fn: fn = self.edtOrganization.getOrganization() self.edtFormattedName.set(fn) def _showAdditionalFields(self): import PropertyEditor if not self.propeditor: propdefs = [ ("Text", "Sort String", "contact will be sorted by this string"), ("Text", "Mailer Software", "user's electronic mail software"), ("Memo", "Key", "public PGP-key or similar"), ("UTCOffset", "Time Zone (+01:00)", "UTC offset"), ("LatLong", "Global Position", ""), ("Memo", "Address Label", "complete formatted postal address"), ("Image", "Photo", "Photographic picture of the contact"), ("Image", "Logo", "Company logo or similar"), ] self.propeditor = PropertyEditor.PropertyEditor(self, propdefs, title="Additional Fields", save_hook=_broadcast_contact_modify, editclasses={"UTCOffset":UTCOffsetPropEdit, "LatLong":LatLongPropEdit}) self.propeditor.bindto(self.getAddtlFields(self._contact)) self.propeditor.show() self.propeditor.lift() def __createWidgets(self): self.columnconfigure(0, weight=1) self.columnconfigure(3, weight=1) # Row 0: self.edtFormattedName = TextEdit(self) self.edtFormattedName.grid(sticky=W+E, padx=PADX, pady=PADY) ToolTip.ToolTip(self.edtFormattedName, "Formatted Name (card title), right-click: copy to name") self.btnTakeFromName = Button(self, image=IconImages.IconImages["assignfn"], command=self.__takeFormattedNameFromName) ToolTip.ToolTip(self.btnTakeFromName, "take from Name") self.btnTakeFromName.grid(column=1, row=0, padx=PADX, pady=PADY) self.edtBirthday = DateEdit(self, labelpos=W, label_text="Birthday:") self.edtBirthday.grid(column=2,row=0, padx=PADX, pady=PADY) ToolTip.ToolTip(self.edtBirthday, "birthday as ISO date (YYYY-MM-DD)") self.ctrUID = UIDControl(self) self.ctrUID.grid(column=3,row=0,sticky=W+E,padx=PADX,pady=PADY) self.btnAdditionalFields = Button(self, text="Additional Fields..", command=self._showAdditionalFields) self.btnAdditionalFields.grid(column=4, row=0, sticky=W+E,padx=PADX,pady=PADY) ToolTip.ToolTip(self.btnAdditionalFields, "show additional vCard fields") # Rows 1+2: self.edtName = NameEdit(self) def fnToName(event, self=self): self.edtName.fromFormattedName(self.edtFormattedName.get()) self.edtFormattedName.component("entry").bind("<3>", fnToName) self.edtName.grid(column=0, row=1, columnspan=3,sticky=W+E+S+N, padx=PADX, pady=PADY) self.edtOrganization = OrganizationEdit(self) self.edtOrganization.grid(column=0, row=2, columnspan=3,sticky=W+E+S+N, padx=PADX, pady=PADY) self.edtAddress = AddressEdit(self) self.edtAddress.grid(column=3,row=1,rowspan=2, columnspan=2, sticky=W+E+N+S, padx=PADX, pady=PADY) # Row 3: self.rowconfigure(3, weight=1) self.edtNote = NoteEdit(self) self.edtNote.grid(column=0, row=3, columnspan=3, rowspan=1, sticky=W+E+N+S, padx=PADX, pady=PADY) self.edtPhone = PhoneEdit(self) self.edtPhone.grid(column=3,row=3, columnspan=2, sticky=W+E+S+N, padx=PADX, pady=PADY) # Rows 4+5: self.edtCategories = CategoriesEdit(self) self.edtCategories.grid(column=0, row=4, columnspan=3, rowspan=1, sticky=W+E, padx=PADX, pady=PADY) self.edtURL = URLEdit(self) self.edtURL.grid(column=0, row=5, columnspan=3, rowspan=1, sticky=W+E+S, padx=PADX, pady=PADY) self.edtEmail = EmailEdit(self) self.edtEmail.grid(column=3,row=4, rowspan=2, columnspan=2, sticky=W+E+S+N, padx=PADX, pady=PADY) PyCoCuMa-0.4.5-6/pycocumalib/ContactListWidget.py0000644000175000017500000002063110252657644022672 0ustar henninghenning00000000000000""" Contacts Listbox with filtering support """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: ContactListWidget.py 92 2004-11-28 15:34:44Z henning $ from Tkinter import * import Pmw import InputWidgets import ToolTip import string import Set import broadcaster import Preferences class ContactListWidget(Frame): DISPLAYFIELD = 'DisplayName' SORTBY = 'SortName' def __init__(self, master, model, **kws): Frame.__init__(self, master) self.model = model self.selectcommand = kws.get('selectcommand', None) self.dblclickcommand = kws.get('dblclickcommand', None) self.categories = Set.Set("All", "No Category") dispfield = Preferences.get('client.contactlist_displayfield') if dispfield: self.DISPLAYFIELD = dispfield sortby = Preferences.get('client.contactlist_sortby') if sortby: self.SORTBY = sortby self.selFilter = InputWidgets.SearchableCombobox(self, command=self.applyFilter, label_text="Filter:") self.selFilter.pack() ToolTip.ToolTip(self.selFilter, "filter contact list by category") kws = {} if sys.platform != 'win32': # Thin Scrollbars: kws['horizscrollbar_borderwidth'] = 1 kws['horizscrollbar_width'] = 10 kws['vertscrollbar_borderwidth'] = 1 kws['vertscrollbar_width'] = 10 self.listbox = Pmw.ScrolledListBox(self, scrollmargin=0, selectioncommand=self._listboxClick, dblclickcommand=self._listboxDblClick, listbox_highlightthickness=0, **kws) self.listbox.pack(fill=BOTH,expand=TRUE) self.registerAtBroadcaster() def registerAtBroadcaster(self): "Register our Widget's Callback Handlers" broadcaster.Register(self.updateList, source='Contacts', title='Opened') broadcaster.Register(self.updateList, source='Contacts', title='Closed') # Import Hook not needed because New and Save will do the job: #broadcaster.Register(self.updateList, # source='Contacts', title='Imported') broadcaster.Register(self.onContactSave, source='Contact', title='Saved') broadcaster.Register(self.onContactNew, source='Contact', title='Added') broadcaster.Register(self.onContactDel, source='Contact', title='Deleted') def getUpdatedContactData(self): """Returns handle, DisplayName and Categories of the modified contact Call only from a broadcaster callback function""" handle = broadcaster.CurrentData()['handle'] if broadcaster.CurrentTitle() != "Deleted": attrlist = self.model.QueryAttributes([handle], (self.DISPLAYFIELD,'Categories')) return (handle, attrlist[0]) else: return (handle, None) def onContactNew(self): handle, attr = self.getUpdatedContactData() self._contacthandles.insert(0, handle) self._contactdispnames.insert(0, attr[0]) self._contactcategories.insert(0, attr[1]) self._filteredhandles.insert(0, handle) self._filtereddispnames.insert(0, attr[0]) self.buildCategories() self.applyFilter() # Do not do selectContact(handle) now, # because we don't know if the contact is in our filtered list. def onContactDel(self): handle, attr = self.getUpdatedContactData() idx = self._contacthandles.index(handle) del self._contacthandles[idx] del self._contactdispnames[idx] del self._contactcategories[idx] try: filtidx = self._filteredhandles.index(handle) del self._filteredhandles[filtidx] del self._filtereddispnames[filtidx] except ValueError: pass self.applyFilter() def onContactSave(self): handle, attr = self.getUpdatedContactData() idx = self._contacthandles.index(handle) self._contactdispnames[idx] = attr[0] self._contactcategories[idx] = attr[1] # Now get the sorted list from the server: srvidx = self.model.ListHandles(self.SORTBY).index(handle) if idx != srvidx: def move(list, x, y): temp = list[x]; del list[x] list.insert(y, temp) # and move our saved contact to it's sorted place: move(self._contacthandles, idx, srvidx) move(self._contactdispnames, idx, srvidx) move(self._contactcategories, idx, srvidx) self.buildCategories() self.applyFilter() self.selectContact(handle) def _listboxClick(self): "Event triggered by clicking on the listbox" sel = self.listbox.curselection() if sel and self.selectcommand: self.selectcommand(self._filteredhandles[int(sel[0])]) def _listboxDblClick(self): "Event triggered by double-clicking on the listbox" sel = self.listbox.curselection() if sel and self.dblclickcommand: self.dblclickcommand(self._filteredhandles[int(sel[0])]) currentFilter = "None" def getCurrentCategory(self): """If a card wants to appear in our filtered list it must include the category returned by this function in it's 'categories' field""" ret = self.currentFilter if ret == "All" or ret == "No Category": ret = "" return ret def applyFilter(self, filter=None): "If called with no arguments the filter will be unchanged" if filter is None: filter = self.currentFilter else: self.currentFilter = filter if filter == "All": self._filteredhandles[:] = self._contacthandles self._filtereddispnames[:] = self._contactdispnames else: self._filteredhandles = [] self._filtereddispnames = [] for str, dispname, handle in zip(self._contactcategories, self._contactdispnames, self._contacthandles): if filter == "No Category": if str.strip() == "": self._filteredhandles.append(handle) self._filtereddispnames.append(dispname) else: if str.find(filter) != -1: self._filteredhandles.append(handle) self._filtereddispnames.append(dispname) self.listbox.setlist(self._filtereddispnames) self.selFilter.set(filter) _filteredhandles = [] _filtereddispnames = [] _contacthandles = [] _contactdispnames = [] def updateList(self): "Completely update our list" self._contacthandles[:] = self.model.ListHandles(self.SORTBY) attrlist = self.model.QueryAttributes(self._contacthandles, (self.DISPLAYFIELD,'Categories')) self._contactdispnames = [dispname for dispname, cats in attrlist] self._contactcategories = [cats for dispname, cats in attrlist] self.buildCategories() self.applyFilter("All") broadcaster.Broadcast('Notification', 'Status', {'message':'%d Contacts loaded.' % (len(self._contacthandles[:]),)}) def buildCategories(self): "Update our categories set" for str in self._contactcategories: cats = filter(None, map(string.strip, str.split(','))) for cat in cats: self.categories.add(cat) self.selFilter.setlist(zip(self.categories.items(),self.categories.items())) def selectContact(self, handle): "Set the listbox selection" try: idx = self._filteredhandles.index(handle) self.listbox.selection_clear() self.listbox.selection_set(idx) except ValueError: # The Contact does not appear in our filtered list! idx = 0 self.listbox.selection_clear() self.listbox.see(idx) PyCoCuMa-0.4.5-6/pycocumalib/ContactSelectboxWidget.py0000644000175000017500000000676110311640650023700 0ustar henninghenning00000000000000""" Searchable Combobox for Contact Display Names """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: ContactSelectboxWidget.py 142 2005-09-13 21:16:23Z henning $ import InputWidgets import ToolTip import broadcaster class ContactSelectboxWidget(InputWidgets.SearchableCombobox): def __init__(self, master, model, command, **kws): kws.setdefault('label_text', "Search/Select:") InputWidgets.SearchableCombobox.__init__(self, master, command, label_text=kws['label_text']) ToolTip.ToolTip(self, "enter the first characters from family name\n"+\ "or select a contact from the list\n"+\ "(you can also use the UP and DOWN keys)") self.model = model self.registerAtBroadcaster() def registerAtBroadcaster(self): "Register our Widget's Callback Handlers" broadcaster.Register(self.updateList, source='Contacts', title='Opened') broadcaster.Register(self.updateList, source='Contacts', title='Closed') # Import hook not needed because New and Save will do the job: #broadcaster.Register(self.updateList, # source='Contacts', title='Imported') broadcaster.Register(self.onContactSave, source='Contact', title='Saved') broadcaster.Register(self.onContactNew, source='Contact', title='Added') broadcaster.Register(self.onContactDel, source='Contact', title='Deleted') def getUpdatedContactData(self): handle = broadcaster.CurrentData()['handle'] if broadcaster.CurrentTitle() != "Deleted": attrlist = self.model.QueryAttributes([handle], 'DisplayName') return (handle, attrlist[0]) else: return (handle, None) def onContactNew(self): handle, attr = self.getUpdatedContactData() self._contacthandles.insert(0, handle) self._contactdispnames.insert(0, attr) self.setlist(zip(self._contactdispnames, self._contacthandles)) self.set(attr, "do_not_execute") def onContactDel(self): handle, attr = self.getUpdatedContactData() idx = self._contacthandles.index(handle) del self._contacthandles[idx] del self._contactdispnames[idx] self.setlist(zip(self._contactdispnames, self._contacthandles)) self.set("", "do_not_execute") def onContactSave(self): handle, attr = self.getUpdatedContactData() idx = self._contacthandles.index(handle) self._contactdispnames[idx] = attr self.setlist(zip(self._contactdispnames, self._contacthandles)) self.set(self._contactdispnames[idx], "do_not_execute") _contacthandles = [] _contactdispnames = [] def updateList(self): "completely update our list" self._contacthandles[:] = self.model.ListHandles() self._contactdispnames = self.model.QueryAttributes(self._contacthandles, 'DisplayName') self.setlist(zip(self._contactdispnames, self._contacthandles)) def selectContact(self, handle): self.set(self._contactdispnames[self._contacthandles.index(handle)], "do_not_execute") PyCoCuMa-0.4.5-6/pycocumalib/ContactViewWidget.py0000644000175000017500000002575010252657644022700 0ustar henninghenning00000000000000""" Draws Canvas with Card """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: ContactViewWidget.py 93 2004-11-28 16:05:34Z henning $ from Tkinter import * import vcard import debug import Preferences from AbstractContactView import * import IconImages import broadcaster import ToolTip # Canvas Width and Height: CANVASWD = 506 CANVASHT = 406 class ContactViewWidget(AbstractContactView, Frame): hide_fields = ["FormattedName", "DisplayName", "SortName", "Rev", "Photo", "Logo"] def __init__(self, master, **kws): self.savedialog = None self.canvas_items = [] self._selectcommand = kws.get("selectcommand", None) AbstractContactView.__init__(self) Frame.__init__(self, master, class_="ContactView") self.__createWidgets() def bind_contact(self, contact): AbstractContactView.bind_contact(self, contact) self.rebindWidgets() def renderAddress(self, adr, realadrobj, x, y, font): # adr is a dictionary (Fieldname: Value) # realadrobj is a real contact.adr[n] Object id = self.canvas.create_text( x, y, font=font, anchor=NW, text="Address:") self.canvas_items.append(id) zeilen = [adr["POBox"], adr["Extended"], adr["Street"], " ".join(filter(None, [adr["PostalCode"], adr["City"]])), ", ".join(filter(None, [adr["Region"], adr["Country"]]))] adrright = 0; adrtop = y; adrheight = 0; for zeile in filter(None, zeilen): id = self.canvas.create_text( 100, y, font=font, anchor=NW, text=zeile, width=CANVASWD-110) self.bindCopyToClipboard(id) self.canvas_items.append(id) self.makeLink(id, command=lambda x=realadrobj: self.composeLetter(x)) x1, y1, x2, y2 = self.canvas.bbox(id) y += (y2 - y1) + 2 adrright = max(adrright, x2) adrheight += (y2 - y1) +2 image_x = adrright+8; image_y = adrtop + adrheight/2 self.renderIcons(vcard.vC_adr_types, realadrobj.params.get("type"), image_x, image_y) return y def renderIcons(self, typelist, set, x, y): "Render GIF-Icons specified in set (typelist defines the ordering)" for type in typelist: if type in set: try: self.canvas_items.append(self.canvas.create_image(x, y, anchor=W, image=IconImages.IconImages[type])) x += 20 except: # Icons are not _that_ important.. print "renderIcons(): ERROR: Could not render icon '%s'" % (type,) def renderPhoto(self, imagedata, x, y): "draw photographic picture" msg = "" photoimage = None try: try: import ImageTk except: msg = "renderPhoto(): Could not import ImageTK - \n" msg += " You must install PIL (Python Imaging Library).\n" import base64 photoimage = ImageTk.PhotoImage(data=base64.decodestring(imagedata)) self.canvas_items.append(self.canvas.create_image(x, y, anchor=NE, image=photoimage)) except: msg += "renderPhoto(): Could not render PhotoImage." broadcaster.Broadcast('Notification', 'Error', {'message':msg}) return photoimage def rebindWidgets(self): "Redraw our Canvas" normalfont = ("Helvetica", -12) # delete all previous canvas items for x in self.canvas_items: self.canvas.delete(x) del self.canvas_items[:] y = 38 # first, draw picture in the background if not self._contact.photo.is_empty(): self._photoimage = self.renderPhoto(self._contact.photo.get(), CANVASWD-10, y) if self._photoimage: y += self._photoimage.height() + 6 # draw company logo if not self._contact.logo.is_empty(): self._logoimage = self.renderPhoto(self._contact.logo.get(), CANVASWD-10, y) # card title self.canvas.itemconfig(self.fn, text=self._contact.fn.get()) # revision date self.canvas.itemconfig(self.rev, text="Rev: %s" % self._contact.rev.get()) y = 40 previous_fieldname = "" addresses = [] for fieldname in vcard.FIELDNAMES: value = self._contact.getFieldValue(fieldname) valuecount = 0 while value: valuecount += 1 font = normalfont if fieldname in vcard.ADRFIELDS: # Collect all necessary address fields: if len(addresses) < valuecount: addresses.append({}) adr = addresses[valuecount-1] adr[fieldname] = value.get() if len(adr) == len(vcard.ADRFIELDS): # Now draw our complete address at once: y = self.renderAddress(adr, self._contact.adr[valuecount-1], 10, y, font) elif not value.is_empty() and fieldname not in self.hide_fields: if previous_fieldname != fieldname: self.canvas_items.append(self.canvas.create_text( 10, y, font=normalfont, anchor=NW, text=fieldname+":")) id = self.canvas.create_text( 100, y, font=font, anchor=NW, text=value.get(), width=CANVASWD-110) self.bindCopyToClipboard(id) self.canvas_items.append(id) if fieldname == "Email": self.makeLink(id, command=lambda x=value.get(): self.sendEMail(x)) elif fieldname == "URL": self.makeLink(id, command=lambda x=value.get(): self.viewURL(x)) elif fieldname == "Phone" and value.params.get("type").contains("fax"): self.makeLink(id, command=lambda name=self._contact.fn.get(),number=value.get(): self.sendFax(name, number)) x1, y1, x2, y2 = self.canvas.bbox(id) image_x = x2+8; image_y = y+(y2-y1)/2 if fieldname == "Phone": self.renderIcons(vcard.vC_tel_types, value.params.get("type"), image_x, image_y) elif fieldname == "Email": self.renderIcons(vcard.vC_email_types, value.params.get("type"), image_x, image_y) y += (y2 - y1) + 2 previous_fieldname = fieldname value = self._contact.getFieldValue(fieldname) def makeLink(self, canvas_id, command): # Special handling of hypertext links: hyperfont = ("Helvetica", -12, "underline") def MouseOver(event, canvas=self.canvas): canvas["cursor"] = "hand2" def MouseOut(event, canvas=self.canvas): canvas["cursor"] = "" def MouseClick(event, command=command): command() self.canvas.itemconfigure(canvas_id, font=hyperfont) self.canvas.tag_bind(canvas_id, "", MouseOver) self.canvas.tag_bind(canvas_id, "", MouseOut) self.canvas.tag_bind(canvas_id, "<1>", MouseClick) def bindCopyToClipboard(self, canvas_id): def copyToClipboard(event, canvas=self.canvas, id=canvas_id): text = self.canvas.itemcget(id, "text") canvas.clipboard_clear() canvas.clipboard_append(text) self.canvas.tag_bind(canvas_id, "<3>", copyToClipboard) def composeLetter(self, adr): broadcaster.Broadcast('Command', 'Compose Letter', data={'card':self._contact, 'adr':adr}) def sendEMail(self, recipient): "Start the preferred Mail Client and compose" import mailtowrapper recipient = "%s <%s>" % (self._contact.fn.get(), recipient) mailtowrapper.mailto(recipient) def sendFax(self, recipient, telnumber): "Send a Fax" import faxtowrapper faxtowrapper.faxto(recipient, telnumber) def viewURL(self, url): "Fire up the preferred browser and go to url" browserprog = Preferences.get("client.url_viewer").split(' ') if browserprog[0]: import os os.spawnvp(os.P_NOWAIT, browserprog[0], browserprog + [url]) else: import webbrowser webbrowser.open(url) def asksavefile(self): import os import tkFileDialog try: dir = os.getcwd() except os.error: dir = "" if not self.savedialog: filetypes = [ ("Encapsulated PostScript", "*.eps *.ps"), ("All files", "*"), ] self.savedialog = tkFileDialog.SaveAs(master=self, filetypes=filetypes) root, ext = os.path.splitext( self.savedialog.show(initialdir=dir)) if root and not ext: ext = ".eps" return root+ext def save_as_postscript(self): filename = self.asksavefile() if filename: self.canvas.postscript(file=filename) return "break" def __createWidgets(self): self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.canvas = Canvas(self, width=CANVASWD, height=CANVASHT, borderwidth=0, highlightthickness=0) if self._selectcommand: def selectcommand(event, self=self): self._selectcommand(self.cardhandle()) self.canvas.bind("", selectcommand) self.canvas.grid() ToolTip.ToolTip(self.canvas, "Click Right Mouse-Button to copy text to clipboard") # 'Export to EPS' is not very useful, so hide the button: # self.btnSavePostscript = Button(self, # text="Save as Encapsulated PostScript (.eps)", # command=self.save_as_postscript) # self.btnSavePostscript.grid(sticky=S+E) # Draw card shadow: self.canvas.create_rectangle(10, 10, CANVASWD, CANVASHT, outline='', fill='#666666') # Draw card paper: self.canvas.create_rectangle(2, 2, CANVASWD-6, CANVASHT-6, width=1, fill='white') # Draw head box (here goes the card title): self.canvas.create_rectangle(2, 2, CANVASWD-6, 33, width=1, fill='#ffbb88') self.fn = self.canvas.create_text(10,4, font=("Helvetica",-24,"bold"),anchor=NW) self.bindCopyToClipboard(self.fn) self.rev = self.canvas.create_text(CANVASWD-10,CANVASHT-6, font=("Helvetica",-10),anchor=SE, fill="#888888") self.bindCopyToClipboard(self.rev) PyCoCuMa-0.4.5-6/pycocumalib/FieldMappingDialog.py0000644000175000017500000000302210252657644022751 0ustar henninghenning00000000000000#!/usr/bin/python # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: FieldMappingDialog.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import PropertyEditor import vcard from InputWidgets import TextComboEdit class vCardFieldMapToPropEdit(TextComboEdit): def __init__(self, master, title, descr): TextComboEdit.__init__(self, master, nomanualedit=True, labelpos=W, label_text='->') # ToolTip.ToolTip(self.component('entry'), descr) self.setlist(vcard.FIELDNAMES) class FieldMappingDialog(PropertyEditor.PropertyEditor): def __init__(self, master, sourcefields, title="", headline=""): propdefs = [('Label', '', headline)] for field in sourcefields: propdefs.append(('vCardFieldMapTo', field, '')) PropertyEditor.PropertyEditor.__init__(self, master, propdefs, title=title, save_hook=None, editclasses={'vCardFieldMapTo':vCardFieldMapToPropEdit}, addcolon='') def getvalue(self): return map(lambda x: x.get(), self.propwidgets) if __name__ == "__main__": tk = Tk() dlg = FieldMappingDialog(tk, ['Name', 'Telefon', 'E-Mail'], title="FieldMappingDialog Test") dlg.activate() print dlg.getvalue() PyCoCuMa-0.4.5-6/pycocumalib/FindDialog.py0000644000175000017500000000433510252657644021302 0ustar henninghenning00000000000000#!/usr/bin/python # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: FindDialog.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * from InputWidgets import TextComboEdit import Pmw import vcard class FindDialog(Pmw.Dialog): def __init__(self, master, title='Find Contact', class_=None): Pmw.Dialog.__init__(self, master=master, title=title, buttons=('Ok','Cancel'), defaultbutton='Ok', activatecommand=self._onactivate) lbl = Label(self.interior(), text="Search for:") lbl.grid(padx=2, pady=2) self.findstrvar = StringVar() self.edtSearch = Entry(self.interior(), textvariable = self.findstrvar) self.edtSearch.grid(sticky=W+E,padx=2, pady=2) lbl = Label(self.interior(), text="in Field:") lbl.grid(padx=2, pady=2) self.selField = selField = TextComboEdit(self.interior(), entry_state=DISABLED) selField.setlist(vcard.FIELDNAMES) selField.selectitem(0) selField.grid(sticky=W+E, padx=2, pady=2) lbl = Label(self.interior(), text="Search Options:") lbl.grid(padx=2, pady=2) self.regexvar = IntVar() selRegex = Checkbutton(self.interior(), text="Use Regular Expressions", variable = self.regexvar) selRegex.grid(sticky=W, padx=2, pady=2) self.ignorecasevar = IntVar() self.ignorecasevar.set(1) selIgnoreCase = Checkbutton(self.interior(), text="Case Insensitive Search", variable = self.ignorecasevar) selIgnoreCase.grid(sticky=W, padx=2, pady=2) def _onactivate(self): self.edtSearch.focus_set() def getvalue(self): return map(lambda x: x.get(), (self.findstrvar, self.selField, self.regexvar, self.ignorecasevar)) if __name__ == "__main__": # Unit Test: tk = Tk() dlg = FindDialog(tk) print dlg.activate() tk.destroy() PyCoCuMa-0.4.5-6/pycocumalib/HelpWindow.py0000644000175000017500000001332410252657644021360 0ustar henninghenning00000000000000""" Help Window """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: HelpWindow.py 92 2004-11-28 15:34:44Z henning $ from Tkinter import * import Pmw class HelpWindow(Pmw.Dialog): def __init__(self, master): Pmw.Dialog.__init__(self, master=master, title='PyCoCuMa Help', buttons=('Close',), defaultbutton='Close') self.master = master self.iconbitmap(master.iconbitmap()) self.iconmask(master.iconmask()) self.text = text = Pmw.ScrolledText(self.interior(), text_width=80, text_height=24, text_wrap=WORD, text_highlightthickness=0, text_padx=5, text_pady=5, text_font=('Helvetica', -14), text_background='#ffffee', text_spacing3=5) self.text.pack(fill=BOTH, expand=1) text.tag_config('h1', font=('Helvetica', -20, 'bold'), justify=CENTER, spacing3=2) text.tag_config('h2', font=('Helvetica', -16, 'bold'), justify=LEFT, spacing1=10, spacing3=5) text.tag_config('i', font=('Helvetica', -14, 'italic')) text.tag_config('b', font=('Helvetica', -14, 'bold')) text.tag_config('a', foreground='#0000aa', underline=1) def append(text, tags=None, widget=text): parts = text.split('*') i = 0 for s in parts: if i % 2 == 1: widget.insert(END, s, 'b') else: widget.insert(END, s, tags) i += 1 from __version__ import __version__ append('PyCoCuMa %s Help\n' % (__version__,), 'h1') append('Keyboard Shortcuts:', 'h2') append(""" F1 \t switch to the 'List View' notebook page F2 \t switch to 'Card View' F3 \t open the contact for edit F4 \t open the Journal window F5 \t open the Calendar window F6 \t open the 'Compose Letter' window (commands above will also raise the respective window) CTRL-O \t connect to server / open database from file CTRL-S \t save modified contact / save modified journal entry CTRL-F \t open 'Find Contact' dialog CTRL-N \t find next contact CTRL-E \t open external editor (only in text fields) """) append('Tips:', 'h2') append(""" PyCoCuMa has many tool-tips (help balloons); e.g. leave the mouse cursor \ over an address-type button in 'Edit Contact' to get the meaning of the \ small symbolic images. Double-click on a row in List View to switch to Card View. Double-click on the card in Card View to open the contact for edit. Double-click on a free day of the Calendar to create a new journal entry for this date. Enter the first characters of a family name into the \ 'Search/Select' textbox on the top to get a list of contacts starting with \ this family name. To *add a category*, simply add the category to the comma-separated list \ in the 'Categories' textbox of a contact. After saving the card, the \ category will appear in the 'Filter' combobox. To *change the sorting order* of a contact, edit the 'Sort-String' field \ provided by the 'Additional Fields..' dialog. E.g. a contact named \ 'Niklas van Haarten' will be found under the letter 'v' at first; \ but by setting 'Sort-String' to 'Haarten' you can then move the card to the \ letter 'H', were you would probably expect it to be. Install MikTeX for Windows (""") append('http://www.miktex.org', 'a') append(""") and add 'pdflatex' to your PATH \ environment variable in order to use 'File'->'Page View' on Windows. Edit the variables 'url_viewer' and 'mailto_program' in '~/.pycocuma' to set \ them to your preferred web browser and mail-composer application. \ If you leave 'url_viewer' blank, PyCoCuMa will try to use your platform \ specific default web browser. *Bug-reports, comments and suggestions are very welcome!* Please send any bug reports to """) append("""pycocuma-bugs@srcco.de """, 'a') append("""See the README file for more information. """) append('Last-Modification: Henning Jacobs, 2004-11-28', 'i') text.configure(text_state=DISABLED) self.withdraw() _firstshow = 1 def show(self): if self._firstshow: self.centerWindow() else: self.deiconify() self._firstshow = 0 def centerWindow(self, relx=0.5, rely=0.5): "Center the Window on Screen" widget = self master = self.master widget.update_idletasks() # Actualize geometry information if master.winfo_ismapped(): m_width = master.winfo_width() m_height = master.winfo_height() m_x = master.winfo_rootx() m_y = master.winfo_rooty() else: m_width = master.winfo_screenwidth() m_height = master.winfo_screenheight() m_x = m_y = 0 w_width = widget.winfo_reqwidth() w_height = widget.winfo_reqheight() x = m_x + (m_width - w_width) * relx y = m_y + (m_height - w_height) * rely if x+w_width > master.winfo_screenwidth(): x = master.winfo_screenwidth() - w_width elif x < 0: x = 0 if y+w_height > master.winfo_screenheight(): y = master.winfo_screenheight() - w_height elif y < 0: y = 0 widget.geometry("+%d+%d" % (x, y)) widget.deiconify() # Become visible at the desired location if __name__ == '__main__': tk = Tk() win = HelpWindow(tk) tk.mainloop() PyCoCuMa-0.4.5-6/pycocumalib/HistoryWidget.py0000644000175000017500000000631610252657644022110 0ustar henninghenning00000000000000""" Searchable Combobox for Contact Display Names """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: HistoryWidget.py 89 2004-09-11 18:58:51Z henning $ from Tkinter import * import Pmw import InputWidgets import ToolTip import broadcaster class HistoryWidget(Pmw.MegaWidget): def __init__(self, parent = None, command = None, prevCommand = None, nextCommand = None, **kw): # Define the megawidget options. INITOPT = Pmw.INITOPT optiondefs = ( ('height', 16, INITOPT), ('width', 16, INITOPT), ('borderwidth', 2, INITOPT), ('padx', 0, INITOPT), ('pady', 0, INITOPT), ('sticky', 'ew', INITOPT), ) self.defineoptions(kw, optiondefs) Pmw.MegaWidget.__init__(self, parent) frame = self.interior() frame.grid(column=2,row=1, sticky=self['sticky']) self.btnPrev = InputWidgets.ArrowButton(frame, direction='left', command = self.prev) self.btnNext = InputWidgets.ArrowButton(frame, direction='right', command = self.next) self.btnPrev.grid(column=0, row=0) self.btnNext.grid(column=1, row=0) self.btnPrev.configure(state='disabled') self.btnNext.configure(state='disabled') self.prevCommand = prevCommand or command self.nextCommand = nextCommand or command ToolTip.ToolTip(self.btnPrev, "Go back in history") ToolTip.ToolTip(self.btnNext, "Go forward in history") self._list = [] self._currIndex = 0 def addHistory(self, item): # only add to history if not already present if len(self._list)==0 or item != self._list[-1]: self._list.append(item) if len(self._list) > 1: self._currIndex = len(self._list) - 1 self.btnPrev.configure(state='normal') def prev(self): if self._currIndex > 0: self._currIndex -= 1 if self.prevCommand is not None: self.prevCommand(self._list[self._currIndex]) self.btnNext.configure(state='normal') if self._currIndex < 1: self.btnPrev.configure(state='disabled') def next(self): if self._currIndex + 1 < len(self._list): self._currIndex += 1 if self.nextCommand is not None: self.nextCommand(self._list[self._currIndex]) self.btnPrev.configure(state='normal') if self._currIndex+1 >= len(self._list): self.btnNext.configure(state='disabled') def getHistory(self): return self._list def configure(self, **kws): if kws.has_key('state'): self.btnPrev.configure(state=kws['state']) self.btnNext.configure(state=kws['state']) del kws['state'] Pmw.MegaWidget.configure(self, **kws) PyCoCuMa-0.4.5-6/pycocumalib/IOBinding.py0000644000175000017500000000354510252657644021106 0ustar henninghenning00000000000000# Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: IOBinding.py 82 2004-07-11 13:01:44Z henning $ import os import tkFileDialog import tkMessageBox import debug import broker import broadcaster class IOBinding: def __init__(self, view): self.view = view self.top = view.window() self.model = view.model self.__id_import = self.top.bind("<>", self.importFile) self.__id_export = self.top.bind("<>", self.exportFile) def close(self): # Undo command bindings self.top.unbind("<>", self.__id_import) self.top.unbind("<>",self.__id_export) # Break cycles self.view = None self.top = None self.model = None def importFile(self, event): import converters data = {'handle':None} def onContactNew(data=data): data['handle'] = broadcaster.CurrentData()['handle'] # temporarily hook ourselves in the broadcast channel: broadcaster.Register(onContactNew, source="Contact", title="Added") ret = converters.Import(self.top, self.model) broadcaster.UnRegister(onContactNew, source="Contact", title="Added") if ret: # Now broadcast our success with our sniffed handle: broadcaster.Broadcast("Contacts", "Imported", data=data); return "break" def exportFile(self, event): import converters converters.Export(self.top, self.model) return "break" PyCoCuMa-0.4.5-6/pycocumalib/IconImages.py0000644000175000017500000022670110252657644021323 0ustar henninghenning00000000000000 from Tkinter import * IconImages = {} IconImagesGrey = {} car_data = '''\ R0lGODlhEAAQALMAAAAAAH8yAGBgYMwAAP8AALNmMuZNAP9mAP//cgAAzExM////l5mZ/8LCwv// /wAAACH5BAkAAA4ALAAAAAAQABAAAAhzAB0IHEiwoMGDDgA0QFhQIUOCDg8CmEhxokQGDApgzMgA gEEADBQUUEByZEeBFEMmKJCgZQGWCSYemEmzZk0ACAzo3MmT54AFBgIACNAz6FADBCg2qFhx6cQG S6NCnapQ4UIHAgAIuDow69aBUw2GfXgwIAA7 ''' pref_data = '''\ R0lGODlhEAAQANUAAMgAAM4AANYCANgHANwGAOoWAOwUAO4ZAPQVAPYgAPIpAPgsAPkuAPk4APdE APpPAOZdAPpXAPtiAPtoAPx6AP6fAP6uAP65AP/eAP/qAP/tAP/2AP//AP//M///Zv//mf//zP// /wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAACEALAAAAAAQABAAAAibAEMI HBgCwQGCCAkGcNAgQMKEBCxUIPAQ4YENGg5WFDjgAQgODwZYVECSgYQPHzdIYEBSwYECEzh08IDy A4ebOCcUCEGAAQYOHjzgvImBAUWBARL8HMoBQwKHCiMw5RABKkEADaY2AJBQAIWbGzbcpCAg4QEM GRwcOOAgAwaNAwMsuPAUaYILC6yGCADBANe4BiDoDUA4IWGoAQEAOw== ''' intl_data = '''\ R0lGODlhEAAQAOYAAAAAAABmMyteRT5yWEZ6YEl8Y0x/ZpJ4RU6CaFSIbleKcVqNdFiRdV2Qd1uU d1+TeWKWfGWZf6WMWKuRXq2UYLCWY7OZZrWcaLifa76kccGndMSqdwcHnBAQkRkZmg4OoyIioysr rCQkuTQ0tTk5uj4+vzk5zkRExUZGx0lJykxMzU5Oz0FB11FR0lRU1VdX2Fpa211d3l9f4FJS6FVV 6mJi42Vl5mho6Wtr7HBw8XNz9HZ293l5+n9//2ibgmuehXCkinOnjYmJiZKSks+1gsLCwsbGxs/P z9LS0tfX1////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5 BAkAAEoALAAAAAAQABAAAAjeAJUIVFIEgMGDRQYOLAhghpEYMFwEMJhQYEEaN5AcsSFjQQIDJgBU BODgR44kOXBEaKAAAYGJBAH4ALKDxw4gPmTAaJFigEgADH4E4dGDhw4cEBbwHDARKJEcNW8SeaAB Q4ESBwxusIEjh1ccNhpkuEChRAiDGmTYuIHjBkePFiaQAGHQxYIYMmrIgOgCQQUJITwYZOHiBYzD L1ysSDFhBIgPP1WsaEF5hYoUJ0gI6CBSCQATKFKIRnGixIjAHABYBBCghGsSJEKA6JC6YkwAIkIM EeIBcmeFtw/+FhgQADs= ''' isdn_data = '''\ R0lGODlhEAAQAJEAAAAAAMLCwv///wAAACH5BAkAAAIALAAAAAAQABAAAAhRAAUIHEiwoMGDBAEo XMiwIYAAECNKnBhAwEOLDxVWvMgxQEaPIDtaBCnwo8aOC0N6TDmypMqMIkeaXLnxpcqWMAXQxFkR 4UGHQFNSHArR58GAADs= ''' video_data = '''\ R0lGODlhEAAQAMQAAAAAAGYzM5sAAM0AAOYAAP8AAO82HsRTI/tDK95tPQCAGgCZMxqNTTOmZveG Vk2agWazmoGntNWcnuKpqrOz5prAzcLCwszM/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAA EAAQAAAIhAABWABw4UIABxMkHAhA4YJAABABVKhw0EGChREqRHwIoaMDBAYOPOj4MGKDkw0uMkC5 ceCCBQUKECAwQICCBSU36tz5sKDPnw4HRvwZgKHPlgQLHkxgNOhGpQgVGkU6seLFABlzAugI4WPI kRCQokx5YOVJrS9jzqx5EylPtxCBAo0YEAA7 ''' postal_data = '''\ R0lGODlhEAAQALMAAAAAAH9/f5KSn7KyssLCwszMzMzM5szM/////wAAAAAAAAAAAAAAAAAAAAAA AAAAACH5BAkAAAgALAAAAAAQABAAAAhmABEIHEiwoMGDCBMOJACgocOHDxkCOECxokWKDgcAMMCx o0cAGidqLECyJEmQIQ8YGGmyAMgCKQ3ABGDy5cyJMgWEbDggAMmYAmQOGDq0gM+UQVuaDLATolOH CAhInUq1qsKrVwMCADs= ''' fax_data = '''\ R0lGODlhEAAQAMQAAAAAAHd3d/8AAMwAM2aZAAD/AIaGhpaWlpmZmaCgpLKyssDAwMzMzNfX193d 3ePj4+rq6vHx8fj4+P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkA ABMALAAAAAAQABAAAAiXACcIHDjQgEGCCAtGiCDhYEKBBh5AYCjBAYCEBhs4gDDRIkKDEgIsWODg gUUAFyeAdNCggQGRCwAsCBAAgIGKDgoQYMCgQQADMmnalEC0pYABPBcYfDl0JFGeDA4cMCj0ZtIF EhAkmPqS6csGVxMgkGoQJcqXAZKKnYqy5kWZM9MmSHDW7UCUCmbWRfkQpVKgKR8KNPswIAA7 ''' voice_data = '''\ R0lGODlhEAAQALMAAAAAAGYAAIAAGZkAM9kAAP8AAP8zM7MZTMwzZv9cXP9mZsLCwv///wAAAAAA AAAAACH5BAkAAAwALAAAAAAQABAAAAh9ABkIHEiwoMGDCBkAACBwYUIACgwsjMjQIAADGDMaKFBx 4MUCBAqIBCmy48cAKFOiLKmwAEoBMGPGDMARQACYA3Lq1CkgwMICAnIiGEoUwYEDHBcAKDCgqFOJ CxgoLWAggYKrCTICiCpQ6dKRJbcWXOB14UKyCMmq5ZqwbUAAOw== ''' home_data = '''\ R0lGODlhEAAQALMAAAAAAABmM5kAAOYAAP8AAP+ZAP/MAM2aZ//Mmf/mgMLCwv///wAAAAAAAAAA AAAAACH5BAkAAAsALAAAAAAQABAAAAiCABcIHKgAAAAFAg0CGEgQAIEBBxc4XMiwIIGHEC1SFKhR IcSJDQkAOEDyAACIIjlOPICgZcuTBhdoZOnSJcyZCEqStJlxZMsDAoIKcGkSIQCiAgwOtZkQqQED S182/SnAQIKoCCgepWoVq1anV2suLKgQQNWrZQ8GWLu2gNu3bNcGBAA7 ''' new_data = '''\ R0lGODlhEAAQANUAAAAAAP9mAP+pAP/BAP/RAP/hAP/pAP/wAP/4AP8A/8LCwszMzNHR0dLS0tXV 1djY2NnZ2dzc3N3d3eDg4OXl5ejo6Onp6ezs7O3t7fDw8PHx8fT09Pf39/j4+Pv7+/39/f///wAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAAAkALAAAAAAQABAAAAibABMI HChgoMGBAQwOMJjwoEECDh1CTFBA4MSIFBMYyIgRgMcDCA54HElwAIECBhCAQGCgAIEBAgAYBAAC BMgDNT0AoEBBpkCaBUAYAFHgA4eRABT8JFAzKAgCGi48cJD0Z82nIDxswFBhAoSqAWjWHGA0g4UA EagqTSAWhIAOUSlIeMCgKturWrl6bbDALtK/SNcmUEC4sOHCAQEAOw== ''' cell_data = '''\ R0lGODlhEAAQANUAAAAAAAAAZjNmZkxMTFBQUFRUVFdXV2JiYmVlZWlpaWxsbHBwcHR0dHd3d3t7 e39/f/8AAAD/AGaZmYKCgoaGhomJiY2NjZCQkJSUlJiYmJubm5+fn6KioqampqqqqrGxsbS0tLi4 uJnMzMLCwsPDw8bGxtHR0dXV1cz//+Dg4Ofn5+7u7vj4+Pz8/P///wAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAAC4ALAAAAAAQABAAAAiGAF0I HDgQAIARBBMSNIhQoUKGDl0YnAjxoUAWK1SkOHHwYQsBIAVIKNExIYAVAkSoRBGi5MIUElDIROHB ZUETEUDohKDBpkAAJD4E2BDAggWfEkF44KDhQoUHSAF0CIAhwIQAC6JmsEDhAQMFB6JWCNAgAIIA BaI6WJDggAECA5BKpFhRYEAAOw== ''' parcel_data = '''\ R0lGODlhEAAQALMAAAAAACoqKv8AAP9mAP+ZAP/MAIaGhv/mgMLCwsnJyf///wAAAAAAAAAAAAAA AAAAACH5BAkAAAoALAAAAAAQABAAAAiFABUIHIgAAIKBCBEiCBCgAMODCRUsDHCgQMUDDwkyrCjg ogCLGQFYHCmg5MiKARQAMGCAI0mLLFOKLGDg48UCAgwMICCzgM+cLVkK2Dmg50+fCXYKILDTKE6f B5gOZer0Y4GkBJY2VenzqUWpRFNO9Ip1aUaBY78WDQAx4cQEZyMSNCg3IAA7 ''' work_data = '''\ R0lGODlhEAAQALMAAAAAAH8ZGWYzM4wMDKZZM+V/M//MAP/mgMLCwv///wAAAAAAAAAAAAAAAAAA AAAAACH5BAkAAAkALAAAAAAQABAAAAhgABMIHEiwoMEEAhIKQHBwIICHEBk2BIBAIsWCAA4YgMgR gIEDFB8WGEmy5EgCIgkoXLkygEiWMAW4BFAgJsuZNW0qxKlz58ueMn/2HPCQQICjSJMi7ciUaYKK UKNKRRAQADs= ''' pager_data = '''\ R0lGODlhEAAQANUAAAAAABgYGC8vLzMzMz8/PwAAZjNmZkZGRktLS1ZWVk1mZmJiYm1tbXl5eQCZ MwD/ADMzmWZmzGaZmXOZmYSEhI6OjpmZmZ+fn6ampqurq7Gxsba2tpmZ/5nMzMLCws3NzdjY2Mz/ /+Pj4+/v7/T09Pr6+v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAACYALAAAAAAQABAAAAicAE0I HEiwoMGDCBMWBMCwoUOGAwGMIDFCBIgPHjZkuFCBAQATEksYGEnSgAQMFRosYCjCQIeXMDuEwGCB w0oAIFwq6LBzgswLHCJAYOhBp4KdO0NYiFCgAEMNOnlKDUHhQIEADC1ICMG1K1UGCwIMYNiAgtmz DcAmIDBgrAcACRw8ELAgQQIEbNsC8GDi7cO/fAV6GEy48GCFCAMCADs= ''' msg_data = '''\ R0lGODlhEAAQANUAAAAAAAQEAAsLCw0NDRISABoaABkZGRsbGywsKDIyMjMzM19fElxcF1xcLgAA ZkxMTFZWQlxcRVNTU2FhYWZmZnZ2dnd3d3p6daioAJCQP729P8TEF8/PEd/fF9bWKNfXKdvbL4CA fM7OY87Ob9vbc/T0Y///bfPzev//f4GBgY+Pj5aWlpmZmb6+vt/fiP//kPHxsdjY2P//zOXl5f// /wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAADQALAAAAAAQABAAAAibAGkI HAgAwYSBCBMCAAEhBYCECj9ouBDjIUQaAAB4MBGhxYwZFhECIDHiBIoGFT6GFJiRg4sSLxhYWAHy wYOMODGIgMFRAk0AN3EK3XAyJUihGVmoGECgAwOPIFlIZZGUxc0AGULUBCq1KgAZAAoscPgVQFez VoEeACAggQGuVNFOZUFBgQIKc5M62Mu37964cwMHfoi0ME4aAQEA ''' trash_data = '''\ R0lGODlhEAAQALMAAAAAAIAAAACAAICAAAAAgIAAgACAgH9/f7+/v/8AAAD/AP//AAAA//8A/wD/ /////yH5BAkAAAMALAAAAAAQABAAAAhcAAcIFAigYMGBCBECOMAQQMKBBiNGTAjgAYKLDDM6VChR 4sOFIA+AfDigIgAEITdyfHCypUiVJUuyRClSZEyFM13ChJiz5k6COWn+lKnT5segI49m1Eiyo0GE AQEAOw== ''' card_data = '''\ R0lGODlhIAAgAOYAAAAAAGYzM5lmZsZrRfeGVgAAgFBQlnBwqf8A/wCAgICAgICAs5CQvf+5iaCg xrCw0MDAwMHBwMLCwcPDwcTEwcXFwcbGwcfHwsjIwsnJwsrKwsvLwszMwszMw83Nw87Ow8/Pw9DQ w9HRw9HRxNLSxNPTxNTUxNXVxNbWxNfXxdjYxdnZxdraxdvbxcDA2v//zv//0P//0f//0v//0/// 1P//1f//1v//1///2P//2f//2v//2///3P//3f//3v//39DQ48zM/+Dg7f//4P//4f//4v//4/// 5P//5f//5v//5///6P//6f//6v//6///7P//7f//7v//7/Dw9v//8P//8f//8v//8///9P//9f// 9v//9wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5 BAkAAAgALAAAAAAgACAAAAj+ABEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLDBVo3Mixo8ePGwEo gEASQgQJEzioxJBhi5YsWK5YqUJFShQoT5w0YbJEiQKREIIEAUCUqFAOGl66WOCAps0oT540aSJE SJKfI4cK2LoVQBANHLQ8YHAFyAIqLoBAEQJkCpAHLpBghQCAK4AGeINs6JDlgEyaDpaadbDgwQEX R+YCCBDgbgMABIJw8IDlwEwqQpYaWMpkgRIXLoxgjbC4MQG8BAh4+HBlLNoFB54sWLrkwBQGLoqM Lj0gdd4PIK5UWeogitm0LpbcdiBkCFYJQ4n2Ph0ERAgrBbJr3879B9YJQgH+DBhANHIIEVW4q9/u AyuF8OLJCxVBoubNnDt7JkFihMgPHzxgVcFQCSRAlIFBkFACFVFAhR9P+xlRxBAA7oCVBQAUqGGB AJBggk046cSTEkgcMeEPPeyQA1YXYKDBXqudp+AJURCFlY0i/ZQjDlixBFaM9JVgAgoh5qdEhP4B qAMON2CVwYurWaegCSeg8ERROt7YUQ1YvdgBcCGM4GGVKjzYU4kT+pBiDjfYQANWMH4gYwlVprCC EziKpKeNG82A1WRyBlmnCisYieR/PCxpQw0zxIBVlEFSmQKhLDShH39pJsomozHAgBWYU5K5Agst jIgphWveUAMNMsDwAlYOIMUqa0hF1WrrrbjiGhAAOw== ''' delcontact_data = '''\ R0lGODlhIAAgAPcAAAAAAAAODgAcHBcXHQAqKi4uKy0tOTk5OWYzMwAAVTk5UAAAZAAAcgBVVURE QVVVUVVVVVxcV1VVd1tbcmRkZHJybXNzbXR0bXJycpkAAMwAAP8AAMwzM/9mAJlmZv+ZMwAAgFBQ lmtrhHBwqf8A/wCAgICAgImJgouLgqKimKOjmIiIqoCAs5CQvZ2duaqqoaqqora2rba2rri4rrm5 rrq6rru7r6KiscfHtMfHtaCgxqurwrCw0MDAwMHBwMLCwcPDwcTEwcXFwcbGwcfHwsjIwsnJwsrK wsvLwszMwszMw83Nw87Ow8/Pw9DQw9HRw9HRxNLSxNPTxNTUxNXVxNbWxNfXxdjYxdnZxdraxdvb xcDA2uPjy+PjzOPjzv//zuPj0uPj1uPj1+Pj2f//0P//0f//0v//0///1P//1f//1v//1///2P// 2f//2v//2///3P//3f//3v//39DQ48zM/+Dg7f//4P//4f//4v//4///5P//5f//5v//5///6P// 6f//6v//6///7P//7f//7v//7/Dw9v//8P//8f//8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAACQALAAAAAAgACAA AAj+AEkIHEiwoMGDCBMqXMiwIUIPGjR4AOCwoYYPHTZkoHgQAAANHBl+xKgx5MCPGyuS+HixJEGU HlWuZOlypYaNJit6/FgSZs6EJoIKxUDBowaNNz0eOAABg9CnQgGY6EG1h48fQGIY7VmhwgsxiAwV IjRIUCBAf/yYkNqjTp2dO49mKKlABFixhQYNChTIjp0+a6e+9UB4olyPJW+wQLSFDiE7dA7R4bGF T+AeAApvxYl4YxgdW1jQYaGDBY8RW/ZcBoAAgdEOOAs86AxgB4sQoQGx8LNli57APli7vpi0gAMY LmiDYRH6z4hDLbbkAc4aAIcPPQtYGJNoC2IAEnb+0NnyB7oOO3cC/3h7vUPSARFSKAJB//sC+vjp zwkMhH1GnAZcUEMi+e2UAAP50SdHYEGw19MAE6gQRVhjlQXABgB40QcfeuAxhxxwBCbEWwDgFAAA K9ggBSKF5FVWIADkgEMXeuRxB4hvBDYEACXsJAABDQAQxRRikWUWWl50wYWNc8TxRhuBEVHEEUjI cIIFKNQQhRRUFOJRYB5hAIBTX5rARmBFGHFEEkvMQIOWUkxRhZFnpbVhhx/C4QYbawSmJhJLMNGE E1tOQUUVg+y0llSMQpVGYFQqIagTUAx56BUvouUHH3vYKIeTbayhBhqBIaFEoE48seWhVmAhyJdZ sC4qlVBnBMYmE6muWoUVV2BRpx93egjinmqkcUYZgQXahKpxUsErFlkEYieHnuoZqrFlkBHYpIVe Cq0WmlJ7I6hrpIGGGWR8ERhU7Lbr7lNwxSvvvPTuFBAAOw== ''' car_grey_data = '''\ R0lGODlhEAAQALMAAAAAAENDQ2BgYD09PUxMTHd3d3JycoiIiO/v7xYWFl9fX/Pz86SkpMHBwf// /wAAACH5BAkAAA4ALAAAAAAQABAAAAhzAB0IHEiwoMGDDgA0QFhQIUOCDg8CmEhxokQGDApgzMgA gEEADBQUUEByZEeBFEMmKJCgZQGWCSYemEmzZk0ACAzo3MmT54AFBgIACNAz6FADBCg2qFhx6cQG S6NCnapQ4UIHAgAIuDow69aBUw2GfXgwIAA7 ''' cell_grey_data = '''\ R0lGODlhEAAQAPcAAAAAAAsLC1ZWVkxMTFBQUFRUVFdXV2JiYmRkZGlpaWxsbHBwcHR0dHd3d3t7 e39/f0xMTJaWlomJiYKCgoaGhomJiY2NjZCQkJSUlJiYmJubm56enqKioqampqqqqrGxsbS0tLe3 t7y8vMHBwcPDw8bGxtDQ0NXV1e/v7+Dg4Ofn5+7u7vj4+Pv7+////wAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAAC4ALAAAAAAQABAA AAiGAF0IHDgQAIARBBMSNIhQoUKGDl0YnAjxoUAWK1SkOHHwYQsBIAVIKNExIYAVAkSoRBGi5MIU ElDIROHBZUETEUDohKDBpkAAJD4E2BDAggWfEkF44KDhQoUHSAF0CIAhwIQAC6JmsEDhAQMFB6JW CNAgAIIABaI6WJDggAECA5BKpFhRYEAAOw== ''' fax_grey_data = '''\ R0lGODlhEAAQAPcAAAAAAHd3d0xMTEJCQnh4eJaWloaGhpaWlpmZmaCgoLKyssDAwMzMzNfX193d 3ePj4+rq6vHx8fj4+P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABMALAAAAAAQABAA AAiXACcIHDjQgEGCCAtGiCDhYEKBBh5AYCjBAYCEBhs4gDDRIkKDEgIsWODggUUAFyeAdNCggQGR CwAsCBAAgIGKDgoQYMCgQQADMmnalEC0pYABPBcYfDl0JFGeDA4cMCj0ZtIFEhAkmPqS6csGVxMg kGoQJcqXAZKKnYqy5kWZM9MmSHDW7UCUCmbWRfkQpVKgKR8KNPswIAA7 ''' home_grey_data = '''\ R0lGODlhEAAQALMAAAAAAEFBQS0tLUVFRUxMTKampsTExKOjo9XV1eLi4sHBwf///wAAAAAAAAAA AAAAACH5BAkAAAsALAAAAAAQABAAAAiCABcIHKgAAAAFAg0CGEgQAIEBBxc4XMiwIIGHEC1SFKhR IcSJDQkAOEDyAACIIjlOPICgZcuTBhdoZOnSJcyZCEqStJlxZMsDAoIKcGkSIQCiAgwOtZkQqQED S182/SnAQIKoCCgepWoVq1anV2suLKgQQNWrZQ8GWLu2gNu3bNcGBAA7 ''' intl_grey_data = '''\ R0lGODlhEAAQAPcAAAAAAEFBQUtLS19fX2dnZ2lpaWxsbHp6em9vb3V1dXd3d3p6enx8fH19fX9/ f4CAgIODg4aGho2NjZOTk5WVlZiYmJubm52dnaCgoKampqmpqaysrBcXFx4eHicnJx4eHjAwMDk5 OTQ0NEJCQkdHR0xMTElJSVJSUlRUVFdXV1paWlxcXFFRUV9fX2JiYmVlZWhoaGtra21tbWJiYmVl ZXBwcHNzc3Z2dnl5eX5+foGBgYSEhIeHh42NjYiIiIuLi5GRkZSUlImJiZKSkre3t8HBwcbGxs/P z9LS0tfX1////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAAEoALAAAAAAQABAA AAjeAJUIVFIEgMGDRQYOLAhghpEYMFwEMJhQYEEaN5AcsSFjQQIDJgBUBODgR44kOXBEaKAAAYGJ BAH4ALKDxw4gPmTAaJFigEgADH4E4dGDhw4cEBbwHDARKJEcNW8SeaABQ4ESBwxusIEjh1ccNhpk uEChRAiDGmTYuIHjBkePFiaQAGHQxYIYMmrIgOgCQQUJITwYZOHiBYzDL1ysSDFhBIgPP1WsaEF5 hYoUJ0gI6CBSCQATKFKIRnGixIjAHABYBBCghGsSJEKA6JC6YkwAIkIMEeIBcmeFtw/+FhgQADs= ''' isdn_grey_data = '''\ R0lGODlhEAAQALMAAAAAAMHBwf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAACH5BAkAAAIALAAAAAAQABAAAAhRAAUIHEiwoMGDBAEoXMiwIYAAECNKnBhAwEOLDxVWvMgx QEaPIDtaBCnwo8aOC0N6TDmypMqMIkeaXLnxpcqWMAXQxFkR4UGHQFNSHArR58GAADs= ''' modem_grey_data = '''\ R0lGODlhEAAQALMAAAAAAExMTGpqapaWlpeXl8TExNra2uLi4rGxsejo6ISEhMbGxvn5+eXl5f// /wAAACH5BAkAAA4ALAAAAAAQABAAAAiQAB0IHKigoIKBCBEqSMAwQcGEBA8QAFDgQIIDDxMqOACA Y8UDBgwcVNjxgEeKIQFERECApckCKFU62Miy5oECE3Gq3MiAgUQENwkIEACg6EyTB3wGHcp0Z8mk OJkSLaqgQYOiIKNOXbAAgIIFAawCMKATANeCRc0OCJv2rAKjAosuGNDALdyEctHKhDgwLd+AADs= ''' msg_grey_data = '''\ R0lGODlhEAAQAPcAAAAAAAMDAwsLCw0NDRAQEBcXFxkZGRsbGysrKzIyMjMzM1ZWVlRUVFZWVgsL C0xMTFNTU1lZWVNTU2BgYGZmZnZ2dnd3d3l5eZWVlYeHh6+vr7CwsLq6usnJycLCwsPDw8jIyH9/ f8LCwsPDw8/Pz+Tk5O7u7uXl5fDw8IGBgY+Pj5aWlpmZmb6+vtXV1fLy8unp6djY2Pn5+eXl5f// /wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAADQALAAAAAAQABAA AAibAGkIHAgAwYSBCBMCAAEhBYCECj9ouBDjIUQaAAB4MBGhxYwZFhECIDHiBIoGFT6GFJiRg4sS LxhYWAHywYOMODGIgMFRAk0AN3EK3XAyJUihGVmoGECgAwOPIFlIZZGUxc0AGULUBCq1KgAZAAos cPgVQFezVoEeACAggQGuVNFOZUFBgQIKc5M62Mu37964cwMHfoi0ME4aAQEAOw== ''' pager_grey_data = '''\ R0lGODlhEAAQAPcAAAAAABgYGC8vLzMzMz4+PgsLC1ZWVkZGRktLS1ZWVl5eXmJiYm1tbXl5eV9f X5aWlj4+PnFxcYmJiY2NjYSEhI6OjpmZmZ6enqampqurq7Gxsba2tqSkpLy8vMHBwc3NzdjY2O/v 7+Pj4+/v7/T09Pr6+v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAACYALAAAAAAQABAA AAicAE0IHEiwoMGDCBMWBMCwoUOGAwGMIDFCBIgPHjZkuFCBAQATEksYGEnSgAQMFRosYCjCQIeX MDuEwGCBw0oAIFwq6LBzgswLHCJAYOhBp4KdO0NYiFCgAEMNOnlKDUHhQIEADC1ICMG1K1UGCwIM YNiAgtmzDcAmIDBgrAcACRw8ELAgQQIEbNsC8GDi7cO/fAV6GEy48GCFCAMCADs= ''' parcel_grey_data = '''\ R0lGODlhEAAQALMAAAAAACoqKkxMTIiIiKampsTExIaGhuLi4sHBwcnJyf///wAAAAAAAAAAAAAA AAAAACH5BAkAAAoALAAAAAAQABAAAAiFABUIHIgAAIKBCBEiCBCgAMODCRUsDHCgQMUDDwkyrCjg ogCLGQFYHCmg5MiKARQAMGCAI0mLLFOKLGDg48UCAgwMICCzgM+cLVkK2Dmg50+fCXYKILDTKE6f B5gOZer0Y4GkBJY2VenzqUWpRFNO9Ip1aUaBY78WDQAx4cQEZyMSNCg3IAA7 ''' postal_grey_data = '''\ R0lGODlhEAAQALMAAAAAAH9/f5OTk7KyssHBwczMzM7OztHR0f///wAAAAAAAAAAAAAAAAAAAAAA AAAAACH5BAkAAAgALAAAAAAQABAAAAhmABEIHEiwoMGDCBMOJACgocOHDxkCOECxokWKDgcAMMCx o0cAGidqLECyJEmQIQ8YGGmyAMgCKQ3ABGDy5cyJMgWEbDggAMmYAmQOGDq0gM+UQVuaDLATolOH CAhInUq1qsKrVwMCADs= ''' pref_grey_data = '''\ R0lGODlhEAAQAPcAAAAAAAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4O Dg8PDxAQEBERERISEhMTExQUFBUVFRYWFhcXFxgYGBkZGRoaGhsbGxwcHB0dHR4eHh8fHyAgICEh ISIiIiMjIyQkJCUlJSYmJicnJygoKCkpKSoqKisrKywsLC0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0 NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PT4+Pj8/P0BAQEFBQUJCQkNDQ0REREVFRUZGRkdH R0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFFRUVJSUlNTU1RUVFVVVVZWVldXV1hYWFlZWVpa WltbW1xcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqamtra2xsbG1t bW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5+fn9/f4CA gIGBgYKCgoODg4SEhIWFhYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo+Pj5CQkJGRkZKSkpOT k5SUlJWVlZaWlpeXl5iYmJmZmZqampubm5ycnJ2dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaam pqenp6ioqKmpqaqqqqurq6ysrK2tra6urq+vr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5 ubq6uru7u7y8vL29vb6+vr+/v8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJycrKysvLy8zM zM3Nzc7Ozs/Pz9DQ0NHR0dLS0tPT09TU1NXV1dbW1tfX19jY2NnZ2dra2tvb29zc3N3d3d7e3t/f 3+Dg4OHh4eLi4uPj4+Tk5OXl5ebm5ufn5+jo6Onp6erq6uvr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy 8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+vv7+/z8/P39/f7+/v///yH5BAkAAP8ALAAAAAAQABAA AAibAP8JHPivihWCCAn2kMOmR8KERWitKvIQoRVw2g5WFFhET75yeigStBKmpBlD9D6CM2SmZBgr UxKVU/cuJb1yOHMmmvKviJlo5d69y4kzmhmR/3p0AUq0XLQuDhX+aVruT1SCPNhQZcMjYRBKOMGB w0kpSEIr0bDJsWJFDrZoGgf2ILMLqkClu8hcTcpHSle5Uvjs7UE4IeGoAQEAOw== ''' video_grey_data = '''\ R0lGODlhEAAQAPcAAAAAAEJCQi4uLj09PUVFRUxMTGpqam9vb3d3d4mJiU5OTl9fX2NjY3x8fKKi ooCAgJmZmZ2dna2trbq6uri4uLa2tsHBwdHR0QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAEAAQAAAIhAABWABw 4UIABxMkHAhA4YJAABABVKhw0EGChREqRHwIoaMDBAYOPOj4MGKDkw0uMkC5ceCCBQUKECAwQICC BSU36tz5sKDPnw4HRvwZgKHPlgQLHkxgNOhGpQgVGkU6seLFABlzAugI4WPIkRCQokx5YOVJrS9j zqx5EylPtxCBAo0YEAA7 ''' voice_grey_data = '''\ R0lGODlhEAAQALMAAAAAAB4eHikpKTMzM0FBQUxMTHBwcExMTGZmZoyMjJOTk8HBwf///wAAAAAA AAAAACH5BAkAAAwALAAAAAAQABAAAAh8ABkIHEiwoMGDCBkAACBwYUIACgwsjMjQIAADGDMaKFBx 4MUCBAqIBCmy48cAKFOiLKmwAEoBMGPGDMARQACYA3Lq1CkgwMICAnIiGEoUwUgACwAUGFC0qcQF DJIWMJBAgdUEGZEOTKp0ZEmtBBdwXbhQLEKxaKEmXMsgIAA7 ''' work_grey_data = '''\ R0lGODlhEAAQALMAAAAAADc3N0JCQjIyMmtra5WVlcTExOLi4sHBwf///wAAAAAAAAAAAAAAAAAA AAAAACH5BAkAAAkALAAAAAAQABAAAAhgABMIHEiwoMEEAhIKQHBwIICHEBk2BIBAIsWCAA4YgMgR gIEDFB8WGEmy5EgCIgkoXLkygEiWMAW4BFAgJsuZNW0qxKlz58ueMn/2HPCQQICjSJMi7ciUaYKK UKNKRRAQADs= ''' webbrowser_data = '''\ R0lGODlhEAAQAPcAAAAAAAEAAAEBAAIAAAIBAAsAAA4AACIAACgPADcAACshAEIAAFsAAChlKixd RJkAAJ4AANw8J4FqAOJzHNVVLNdWLd1YK81aONtfM4ATXooXX5wQU5wcXbopQYQjc44kcIsqergw ar1oQZh3d7FoaKVie+hKSsNnQ/9mZlyNclqRc12UddmHH/qJBvSADvaIDu2FEvKKFfO6DOaVK9S+ NOPWAP7XAfrXBvXfAPnaA//dAPfZCOPiBerjCe3jCfXjAvDnDPzhCvTtCPDpDv73AfvwBvv4Av/6 AP37Av//APjxCfHkGvHkG//gKf//M5eBfZyDe8Oncsaqdf//ZgcHnBAQkR8YlhkZmjIWiSghnw4O oyIiozEppz8xq04kkFUrl1IxomI8pH9ctWpgq3ZnoUNB1VZU01lX1lxa2V9d3F5e3XVTwWNa0Gxd 1WFf3ndf0lNT51dV6Gdl5Gpo521r6nJw73h29Xt5+Ko8hKVImZNQrYNpxY1yz4Bl2IVr3ZR61Wqb gG2eg3KkiHWni5yAg9OOjv+bm9G1gM7InP//mcPBwcPDwcLCwsTCwMfHxdDQztTS0NnX1f//zP// /wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAAJMALAAAAAAQABAA AAj2ACcJnLRIgAAACAEwGjiwoAA4jtSwgZJEQoGFAhsRiDMH0iM5YoRIMhImAEYCKwLViVRnDxBJ iZL4aBCAIAFAguzc4TNEEswkSXogUCRARaBBd/4s8ZloCtAkPBAUPVTHD5NETp5qrSFAihw6ffLM iPHCRhIdLWCwYCAgihs5b/TgCYHhSBIXEThkWCDATIo0btqsKVEkSQ4LIrxgMSCgjJkzaNgQUpKE yIUTXbZoAYBIwRgyT2ggqVwBBBgHVRRqfXqDwocvXK5QATCpSRAcP3bImNDBg5csVWYvNITCBIQH GzSQGGFls0KGAgslOGBgAEKMAgMCADs= ''' assignfn_data = '''\ R0lGODlhEAAQAPUAAAAAACtfmS1inS1kniptpCpupSxooSd2qSZ7rCV+rih1qR2XvRyZviKGsySB sCCOuCCPuBeqyBeryRqgwxmlxhSzzhO50hK80xG/1Q/G2Q3L3AzN3gzP3xDA1gfd5wvR4AvS4QrV 4gnX5AnY5Ajb5gfg6Qbi6gXk6wPs8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACkALAAAAAAQABAAAAbmwFQq lUoBUqlUKpVKpVKpVCqVAgBSqVQqlUqlUqlUKgUYAQAAACCVSqVSqVQK4BFpMhhLBZBKpYCpVApw 8nwAAAAAEAGkUqkUAFUacQCpVCoFmABSqVQKUAppAAAAIJUCLACpVCoF+GQuFQkFkEoBARBAKpVK ATAAAADAAKRSgAYglUqlAIBUKgWAAFIAByCVSqVSgFQqBWgAUgAEIJVKpVKpVCoFACYAKYACkEql UqlUKpUCHAApQAGQSqVSqVQqlQIUACmAAZBKpVKpVCqVAgwAKYAAkEqlUqlUKgVMAQKAFCAACAIA Ow== ''' modem_data = '''\ R0lGODlhEAAQALMAAAAAAP8AAJlmAAD/AMyZAP/MAP/wAP//AMDAP///M4SEhMbGxv//zOXl5f// /wAAACH5BAkAAA4ALAAAAAAQABAAAAiQAB0IHKigoIKBCBEqSMAwQcGEBA8QAFDgQIIDDxMqOACA Y8UDBgwcVNjxgEeKIQFERECApckCKFU62Miy5oECE3Gq3MiAgUQENwkIEACg6EyTB3wGHcp0Z8mk OJkSLaqgQYOiIKNOXbAAgIIFAawCMKATANeCRc0OCJv2rAKjAosuGNDALdyEctHKhDgwLd+AADs= ''' newcontact_data = '''\ R0lGODlhIAAgAPf/AAICAg4ODhISBRwaBhYWFiIeCiIiCiomCi4qCjIuDjY2Djo2Djw6Di4uKjo2 MgICfgYGfgoKfg4OfhAOehISehoWeh4aeiIediIieiYmeComdjIucjY2cjo2fk5KElpKOmReHGpW PnZaPmJiHm5qGnp2KlpSSkZGfnpmVgICgk5GknZyknJyqhaKehqKehqOeh6OeiaSdopeNoJiPppy Np56PoJ+npKKJpaOMqaaKoKCfoaGfoqGeoqKeo6Keo6OepKSdpaWepmWc5qadqqKSqKeer62Xqai cqymfv6KEv6aFv6eGv6iJtCCQtKGQtaSQs6uVsaiYtK6dvqmTvqqTvquUvqyTvq2Sv7KHv7SFv7a Ev7eFv7WIP7SLP7eJP7aKv7WOv7mD/7mFv7iPv7mOP7qOtLKetrCev7eWurefv7GYv7Obv7WYv7S aP7efv7mQv7mTv7qQuriev7icv7yav7yfYKCgpqWmpqaloaGro6KpoqKqoKCspKSop6arpKSvqKe nq6miqiinLq2nqqmqq6urqKixL6+wrKy0sK+ktrSitrSltrWlt7alt7ans7KosrKrMrKssrKts7K tsrKvc7Kvc7Ous7OvtLOttLOutTSqNbSrtrWptrWrNLSttbSttbWttPSutLSvtbSvtbWutbWvtrW s9rWut7Wut7Wvt7atObeguLejuLemurWluLequbeturikvbumP7yhv72hv7yiv72jf7ykv72kv72 mObiqu7ipurmsvTuovXup/burv72ov72qf72rv76qPbytv72tvryvv76sv76tv76uv76vsbCzsbG zsnGxsrKws7OwsLC1sbG0sLC2tLSwtLSxtbWwtbWxtrWxtraxtXS09bW0tLS2tbS2tbS3tLO7tLS 49bS5tLS6tjW4OLewubexubizvbuxvr2zv76wv76xv76zP760v761v7+0f7+1v762v763v7+2v7+ 3uLi4uLi7vLu8v7+4v7+5v7+6v7+7vLy9v7+8v7+9v7+/v///wAAACH5BAEAAP8ALAAAAAAgACAA AAj+AP/9+/fv379///6J+ffv379///79+/fv379///79+/fv379///79+/fv379///5t+ffv379/ //79+/fv379///79+/fv379///79+/fv379///5l+ffv379///79+/fv379///79+/fv379///79 +5fk379///51+ffv379/Sf79+/fv379///79+/fv379///79+/dvyb9///6N+ffv3z8l//79+/fv 379///79+/fv379///79+/fv379///7V+ffv379///79+/fv379///79+/fv379///79+/fv3784 /3LdovWvzL9///79+/fv379///79+/fv379///7+/fv379+/f7OCFbM169+/f//+/fv379+/f//+ /fv3798/gP/+/fv379+/f+jSwdtnT5itf//+/fv379+/f//+/fv379+/f//+hdGSBYsXMnRw4fPH jx2wOm+6ZNESRgiQHz147NChQ4cdO3YAHOH0StcqX71kveN3j5ivXLaCIUOXTp27ePLs2buHDx++ e3YAIFFFDhaOHGbqKEp3DlivYegYBRqkrp27ePLs2buHjx69e3YAFNGUCsQZV2R2mQvmS1YtW4sS tRt3p12zbPLmbav3DZG0e3YACHk0IooUKEbQzKn1y5gcZEiOKUv37hAzPdvyGOKDiIW0e3YADNH+ BKKGEhJt2NyAEyvNqmFqkqFTV66ZnxXM8tjjc0+aNHt2AATZtCAJjRJW3LQBc4UVr2OomKSr1MdG vD3R8txjoe8PQGn27AAIAimBDCJPqFRZ80VVq0bo0E0iVMhdtj3LtkGzp++PIXr27AD4AambBwUM nkyZ4uWaI0frOGzQ0MFCBQoSIkB4kCJFihTy7ADwEQmcOAROnCxgIEYbJ07qTmS4gCGDCgkSIDxI kSJFihTy7ADoISncNwMHmjRR0I1bp07t5MWLZ0+ePXv47N27Z8+ePHny7ADgIcnbABgwChiIgeCb KVPx4smTZ8+evXv57t2zZ8+ePHnx7ADYQUn+QIsWLly8gAFjAChT8uTZk3fv3r17+PDds2dPnrx4 8ewA0EGJkiVLmTB5+vSJFKlT9gQI2DFDUAMHglDYsQMAgB078ewA0EGJUiVLlkKFGkWKFKlT9uzZ s3fv3r589+7ZkycPoLx48dzZAWDHGaVKly6JEjWqVKlSpe4BAABABwAAeEwACmHHjh07dtzZAWDH mbNnz56JmjatWrVq2PDhw4cP37179+zZu2cvXjx37tzZAWDH2bNnz6ZNmzatWjVr2PABAGAHAAA7 AOwAIBAAjx07dtrZAWDn2bNn06ZRo2bNmjVs2PDhw3fv3j179uTJs2cPnzt37drZAWDn2bOPZ9Oo UbNmzRo2bNbw4bt37549e/bkyYsXz527du3a2QFg59mzadSoWbNmDRs2bNbw4bt3z549e/LkxYvn zp27du3a2QFgx44dO3bs2LFjx44dO3bs2LFjx44dO3bs2LFjx44dO3bs2AEAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAEAAAAAAAfBABICAAOw== ''' savecontact_data = '''\ R0lGODlhIAAgAPfLAAICAg4ODhISEgICOgICPgI6OgI+PiIiIioqKjIyMjo6Oj4+PgICXgICdgIC fjo6Ujo6bgJeXkJCQkZGRk5OTlJSUlJaVlpaWl5eXlpaZl5edmJiWmJiYmZmZmpqam5ubnZ2bnp6 bnJycnZ2cnZ2dnp6en5+fgICgl5egm5uinJyqn5+sn6qqnbqznrq0nrq1n7q2n7q3oaGeoKCgoaG hoqKio6Ojo6OnpqajpKSkpaWlpaampqamp6enoKCspqappKSvoKqqoauro62spK6uqamlqKioqam oqampqaqqqqqqqqurq6urrq6rr6+rrKysra2tra+urq6srq6tr6+sr6+trq6ur6+ur6+vqKixrKy yrKy0r6+0r6+1prGvpbWxp7eyob+zor+0or+1o7+2o7+3oLq4oLq5obq6o7+4pL+5pL+6pb+7pr+ 8pr+9p7++p7+/qLOxq7OyqbWyq7e0rbGxrbS0r7a1qLq0qry1rLi1rrq2r7u3rL63rb+4rb+5rr+ 6rr+7r7+8sLCssLCtsbGttLSvsLCwsbGwsbGxsrKws7OwsrKys7OzsLC2sLW1sLe2sba2sra2sre 3s7e3tLSwtLSxtbWxtraxtLS0tbW1tLS4t7e6srm3s7q4sL24tLi4tbi4tbm5tbu6trm5t7u6tr2 7t768urq0u7u1urq2v7+zvLy2v7+0v7+1vr63v7+2v7+3uLi4ubm5uLi7uLq6ubu7uLy7ub+8ub+ 9ury8ur28u7y8u769v7+4v7+5v7+6v7+7vLy8vLy9vL29vb29vL++vb6+vr+/v7+/v///wAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAMsALAAAAAAgACAA AAj+AJfNmDFjxowZM2bMmDFjxowZM2bMmLFs2bJly5YtW7Zs2bJly2bI0oQGDRo0aNDEMBMDxgsX LSwYuQBg2bJly5YtW7Zs2bJly2ZoOqRMmTJlypQpU6ZMmTJlySrkuABg2bJly5YtW7Zs2bJly2Zo OgTHjZs2bNaoSVOGzBgxYaJAuQBg2bJly5YtW7Zs2bJly2ZoOpRMWbJkyZQpU6ZMGbJjxKA8uQBg 2bJly5YtW7Zs2bJly2ZoOiRIUCBAgP786dMnDx4wX5YouQBg2bJly5YtW7Zs2bJly2ZoOmTMmDFj xoz12nWrlKhNnZIYuQBg2bJly5YtW7Zs2bJly2b+aDqUK1cuXKdMjfLUCdIdO3KM9LgAYNmyZcuW LVu2bNkygMuWzdB06BOfPXrozInjhcgQIUFY7NBxAcCyZcuWLVu2bNmyZcuWzdB0KNmxYbps1SIV itKkSI8e6bBxAYACDCNMzJgxY8aMGTMAzNCEJREvW7VIhaIkKdKjR4/q1KBxAYCIJqqABQMGDBiw XzMAzMhkBcoTJkqQGOnBQ0eOHDVo0JhxAQCIJqqABQMGjBatXzMAzGj0hIkSJEqQIFGiREmPGSZM mChxAUCGH6o4bRq2aYujXzMAzEikBIkRJlgOyVKmTFmjGTNqlBBxAUAGFFxWZPGxRYWjXzMAzDj+ hMRIDyVPFGBQpqyYEg8dRozwcAHAAwhaVgDz8cuRI18zAMzA0oOHjieaMORQxouRjgkSMHzocAEA CBRaVgBTMQwIQEe+ZgCYYUVHDhtYZkmYMesQFh0IDkzwgOECgAc3uHQBNgxIFlq+ZgCYAcWGjRpW imVidAgLFh0CAkjgcOECgAEMGjg4ceLEiROxZgAYceHChQsJAAAAAAAAAAAULly4cOEBAAIMGjg4 ceLEiROxZgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmDUv365ctXrFixZgDwMCMFAAMGAAAoAEDD hg0iQoAIASKEjCKGWPny5ctXrFiwZgAggQT+QIQIESJEiBABAA4cTZo0adLEySBDqF758uUrVixY sGYAMHFlihQpVahQoTKIEKFCqgAAGAFgBAAAJgDMmAEAwIwZsGYAMIEIkSJFihQtWrSoUqVKwIAB +/UL2K9fv3z5ihULYCxYsFzNADBDkSJFixYtqmTJ0qVLl4IBAABgBgAAMwDMADBjxowZM1zNADBD kaJFixZVsmTp0qVLmIIBAwYM2K9fv3z5ihULFixXrlzNADBD0aJFiypVsmTp0qVLmIABADADAIAZ AGYAAABgxowZM1rNADBj0aJFlSpZsnTp0iVMmIABA/br1y9fvmLFigULlitXrVrNADBj0aKPRZUs Wbp06RImTJiAAfv165cvX75ixYIFy5WrVq1azQAwY9GiSpYsXbp0CRMmTJiAAfv1y5cvX7FiwYLl ypWrVq1WzQAwY8aMGTNmzJgxY8aMGTNmzJgxY8aMGTNmzJgxY8aMGTNmzAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAICAAOw== ''' pycocuma_title_data = '''\ R0lGODlh8ABkAPf/AAICAhpTdzqWVrSaovJoge5YaM4tPr1eZ6fU6KoeLhU+WV9eYM/S2DTePhox SB5qnM7p9XaTqZ8aJg0mPXSnx1FUVZoVIOno6aS2wzlymGJ0iBkiKNm3s+by+JqmtXZJUzh9qqjA zpZ1gRceIwYaJZSZpjxXcTdBR61bZ7/ByIbE4bfT5ZU0Q12MrHa63ja2RvTYuPCimcuKj1V6kub2 /MHn9hIWGpi1yILagmCVuYemu6NibjZXcoyWpLtGVG10hjbLRi12pcvW3t2jpbq+xkVJTj81Qrfi 802OtZDJ4cl5gdfY3JYaKEGAq6TG25dfanWiwC9beMLU4UpedsE+Sw0SFvTCq5YeLmSixkbKSu5j ef369zsqK89YZnKGmSswM9CeoBBek0q6Voe0z9bn8JTS6ypAXf7s4Cc1QNVxfXyy0l+Gn7HN3snM 00CGsolaadc7U/KOjJIaJkduidTh7DBIYqmp9WmewGiNpmx9j7s6Rq6yvF16kKx/hcPb6cDGzlam XsmjqFSBnlxkeCIpL4YiMnq+3nuetudNXIGFkJW81S5xoCFkkuju81RgdLbe7wYOErS090RNdJfO 5ra/yczf61KmYpqa+YaUpdxodand8v7+9lqexujAsBVkmHoeLjtHTt3f4NFHVnmuzypxor0iMkx2 k0mIsn7I6I2gsXY5Qaeqsm6VsUdUbSMuS9/p8brO2/rfy+5ecZigrcvL9YbP7mBsgHV/j0ROZnSM oCZVdEhmflSNs2yozISMmxgoPqasxoKVp4aitvfy72qYuD1jffKGjFiUuZnY8OXSzehUZCg3U3yp xjF7qsvHw9vOzfX2999MXby5xIq71jpKZI60zXJyyIYqOlJthMDM1oWtyOlfc8Pi8J680KfL4DVA W2Save/u78CnpcDA9KkzQ25sf5esvHmask59m7ba6v7+/SRvoIKiubrF0O52gIzW8VCUvuXg4GKA le/y9ODy+RdIaPrp3tZOX5vE25NGUrG4whFajhQaHhQiPW2iwx8pRyH5BAAAAAAALAAAAADwAGQA AAj+ANWpU7dly5Yt0KBBgzZv3rx54cI1unDhwoULF17FexUvVKhQoUKFWrJkyZIlS5YwYMCAQRsG bdq0adOmzZ8/f/6kSJEiBREiRIhIk6ZPmj59+vbs2bNnD7BVwFatWrXKwyoPHmbNmjVr1qxZJUqU KFGiR48ePXr08NXDly9fvnz58pUoUaJEiW4lunXr1q1bt279uPXjx48fP378KKehnC1btmzZGjRo 0KBBgwYNcuTIkaMpjipMqdCqVatWrVrhwoULFy5q1KgVoQaK2rdv3759+/Zt2bdly5YtW7ZsGRo0 rly5cuXK1b9///79+/WrX79+/fr169evX79+/fr+9evXr1+/fv369evXr1+/fv3Uqduibgs0aFug QRs2b9g8gOHmhbsQrlGjC6/ixXsV71WoeKFCLVkSKtSSJUuWMFiyhEEbBm0YtGnT5s+fNm3+pPiT IgWRFESISKOkT5o+ffr26NO3Z8+qPXv2rALmYdWqVbNWzVo1a9asWSVmzSpRokSJEj169PDVw1cP X758+fKVyFeiRIkSJbqV6NatW7d+/Lj140eeHz9+2CqnoZytcrbK2Rpky9agQYMGOXLkaIGjBa0q OGpVYUqFVq0q4GrVClcRSUUkFaFW5Nu3It9Anfj27du3b9++LTOybJmrZa6WoUHjytU/V//+/ev+ 96/fv379+vXr169fv379+vXr169fv379+vXr169fv379+m1RtwXaFmjQoEGDNmxeuHnhwoW7cOFC owsXLsR7Fe8VwFDxQoUKtWQJnSVLGCxhwIDBkjYM2rRp06ZNmz/O/vxJ8edPCiJEiBCRRkmfPn3S 9O3Zs2fPHmCrVu1ZtWeVh1WrPKya5WFViVmzSpSYVaJEiRI9evTo4auErx6+fPny5SuRLy+JEt1K dOvWrVu3bt36cevWjx8/fvz4UU7DD1vlbNkqN2iQrUGDBg1a4MiRo1aOHFVw1GpBq1YVWrXChQtX K0m4qBWhRo1akW/Uvn379u2bkWVGln1btmz+mZFly764cuXKlat/rv79+/ev379+/fr169evX79+ /fr169evX79+/fr169evX79+/fr127JFHbQt0LZAmzcP2rB58+Y1atQo3IULFy68uhDvVbxQoUKF ChVqyZIlS5YwWMIAIIM2DNowYNCmjTNnbf44+/MnRYo/KVIQoUREHxF90vTt0bdnz549e/bsWQVs 1apVHlZ5GLBq1qxVs2bNmlWiRIkSJUr06FHCVw9fPXz58uUrka9EibwkSpToVqJEt27lEXHrx48f t378+KHhR7lyGsoNsmWrnK1BgwYNGjRogaMFjhY4clTBUYVWrVpVkFQBFy5cRSRJ+ibpm6T+b0W+ ffv27du3b9+WfVtmZNmyZa6WuVrmypUrV67+/fvX71+/f//69evXr1+/fv369evXr1+/fv369evX r1+/fv369eu3BdqWLdCgQdsCbd68YfPCzQsXrlG4CxdeXXh1IV6oV/FCxQtFJ9SSJUuWLFmyZMkS BgzatGHQpk2bNs7+/HEG8E+KFClSECFChIg0afqk6ZOmT9+ePXtW7Vm1Z9WqVatWeVjlYdaqAbNm zZo1q0SJEiVK9CjRo0SPHr56+Orhy5cvX74SJUqUKFGiW7cS3bp169atWz9u/fjx48cPDeU0/ChX zpatcrYGDRo0aNAgR4McOXLkqJUjRxX+KrRqVaFVq1atcOHChQuXpCLUJFEr8u3bt2/fvn379m3Z NyPLli0zsmwZGleuXLly5erfv3//+v3r96/fv37/+v3r96/fv37/+v3r96/fv37/+v3r9w8aNHXQ tkCDNg8atGHzws0LFy5cuAuNLly4cCHeqwvxXsULtSTUklChlixZImQJAwYMGDBg0KZNmzZt2rRp 88fZnz9/UqQgQoQIEUrS9OkDqE/aHn369ujbswcYMGCrVq1atcrDqlmzVs2aNWvWrFklSpQoUaKE rxIlfJXo4ctXD1++fPny5StRokSJbiW6devWrVs/bt36cSvPjx8/fvywpcGWrXK2bNn+smVr0KBB gwYNcuRogSNHjhw5cuSolaNWFVq1wtUKlyRJkqhJolZEErVv1L59+/bt27dv3759W7Zs2bJly5a5 QuPKlStX/1z9+/fv379///79+/fv379///79+/fv379///79+/fv1xZo0KBBgwZt2Dxo0OaFCxeu UaNGFxpduPDq1YVQr0KFohMvVKhQS+gsWbJkyZIlDBgsYcCgTZs2bdq0+dPmT5s/Kf6kSJEiBREi lIhIk6ZPmj59+vbs2aNvzx5ge4ABXAVs1apVq1bNWjVrlodZs0qUKFGiRIkSJXqU8NWjRw9fvnz5 8uXLl69EvhL5SpQoUaJbiW7dupX+58etPD9+/Pjx48cPDeVslbNly5atQYMGDRo0aNCgQY4GOXLk yFGrCq1atWpVoRWuVrhw4ZIkiZokakWoUaP27du3b9++ffv27duyb8uWLVu2bJkrV8tcufr379+/ f//+/fv379+/f//+/fv379+/f//+/fv375+rLdCgQdsCDRo0aPPmzZs3b164C+EaXbhw4UK8Vxfi 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 5QvTqlXmVplb5WGVBw8ePHjw4MGDBw+zUs0qUaJHiRIlevTo0aMHJkyYMGHC5MuXL1++fOXy5SVR olxebnnxcuvWrVu35N3KkydPnjx58uQBqEGDBg0abGmwpUGDLVu2bNnCNgibLWy7bDna5ciRoymO pkyZMsXElFatWpn+MNGqlYlWrUzgosaDGjVqdahRq1OnTp06ZszUMWOmjhkzdcyYqWPGTB0zZuqY MVPHjJk6dcyYMVPnmzp1oZyFChUqVLwloULFC+XswislWggQIECAAIFt95Q92bRJnTp16tSpUxdv 3KVLdi7Ek3Qh0pY9W2htURfuwgV1mzap26RO3ZYtmzalIEfOmbM2zpw5c9bGWZtNm7Zs2bRJ3aZN mzZt2bRp06Ytm7Zs2bRJnTp16tSpU6dOnbotW9Rt2aJOnbotW7Zs2bJF3ZYtW9Sp26JOnbotW7Zs 2rRly5YtW7ZsUaduy5Yt6rZs2rJFnbot6rYA3KJuy5YtW7Zs2bL+ZcuWLVu2bNmyZcuWLZs2qdui bsuWLVu2bNm0ZcsmdZvUbdqyZQu0LVs2bdmyacuWTVu2bNmyZdOWLZu2bNm0ZdOmLVu2qFO3ZcuW LVv2xAsV6tmqWav0hQoVakmbeBy2ESDgjgABAgTuiRIVTp06derUhaMVyY4dO3Yu0bJmjYG0LerU bQkXbos6derUqVOnbpM6dZu2bBq2o9QAZ86cOXPmzJmzNtC2bNmyBRo0aNCgQdsCDRo0aNCgbYEG DRo0aNCgQYMGDRo0aNCgQYMGDRo0aNCgQYMGDRo0aNCgQYMGDRo0aNCgQYMGDRo0aNCgQZs3bxg0 aNCgQZtHA9r+vA7Q5s2DNm/evHnz5kGbBw3aPGjzAEKDNm8etHnQ5s2bN2/evHkd5s2bN29eh3nz 5nWYN2/evHnz5s2bN2/evHnz5s2bN2/evHnz5s2bN2/evHnz5s2bN2/evHlb5jkLFSpUiVXiVg3Y EypeKGehnN3TQoAAAQJatBQQdU/GFnVbQtGKNI4WrXGRIkWiNW5JOHVbLoSbp06dOnXq1KnbtEnd pk3qNm3ZtGlAgj5//vz58+fPnz9toEGjMQ8aNGjQoEGbBw0aNBrQoHWARgMaDWg05s2bN28eDWgd 5s2b12Fehw4dOnSY16HDvA4d5nWY16FDh0aNOsxr1KhRo0b+jRo16jCvQ6NGjRo16tCoUaNGjRo1 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 ''' generate_data = '''\ R0lGODlhEAAQAPUgAAAAACMjIzAwMDs7O0tLS0xMTFZWVmFhYWZmZm5ubnBwcHV1dX9/fw/G2Q3L 3AzP3xDA1gfd5wvR4AvS4QrV4gnX5AnY5Ajb5gfg6Qbi6gXk6wPs8IODg5GRkZSUlJWVlQAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACAALAAAAAAQABAAAAbmQBAI BAIBQAAACAQCgUAgEAgEAgBAAgEIBAKBQCAQCACwAAAIBAAEAgBAIBAIEKEADJ3OYgAoAIAgEAig iUgAic/n82EAQCAQYIOxPAAHhYLz+QxAIBAIgKE4AAAAAMD5LAAgECAAkDQAABAIoPgAPwgBACAg ACAAAAgEUHw+CAEABAIYAAAAAADgfBYAEAgEAg0SgINCwfl8BiAQCAQCARiJxOfzAX4YABAIBAIB CoCBodNZDAAFAAgEAgEAIBAAgQCAQAAACAQCgUAgEEggAIFAIBAIBAKBQCAQCAAEgEAgEAgECgIA Ow== ''' savejournal_data = '''\ R0lGODlhFAAUAPeEAAICAg4ODhISEiIiIioqKjIyMjo6Oj4+PkJCQkZGRk5OTlJSUlJaVlpaWl5e XmJiYmZmZmpqam5ubnJycnZ2dnp6en5+fn6qqnbqznrq0nrq1n7q2n7q3oKCgoaGhoqKio6OjpKS kpaWlpaampqamp6enoKqqoauro62spK6uqKioqampqaqqqqqqqqurq6urrKysra2tra+urq6ur6+ vprGvpbWxp7eyob+zor+0or+1o7+2o7+3oLq4oLq5obq6o7+4pL+5pL+6pb+7pr+8pr+9p7++p7+ /qLOxq7OyqbWyq7e0rbGxrbS0r7a1qLq0qry1rLi1rrq2r7u3rL63rb+4rb+5rr+6rr+7r7+8sLC wsbGxsrKys7OzsLW1sLe2sba2sra2sre3s7e3tLS0tbW1srm3s7q4sL24tLi4tbi4tbm5tbu6trm 5t7u6tr27t768uLi4ubm5uLq6ubu7uLy7ub+8ub+9ury8ur28u7y8u769vLy8vL29vb29vL++vb6 +vr+/v7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAIQALAAAAAAUABQA AAj+AAl16NChQ4cOHTp06NChQ4cOHTp06ECoQ5wyP378+PHjh48eHDZoyICBgYoGADqU0TJIkCBB ggQJEiRIkCBBghaEaACgQxktR4wUISJESBAgPHboyIFDRowGADqU0TJo0KBBgwYNGjRIUCBAfWLA aACgQxktWbJcuXLFipUqVKA8uWHDRYsGADqU0fLnz58/f/bswVPHzZo0YlioaACgQxktd+7csQPn DZszZr44aZJERYkGADqU0YJmipQlS5QgqZECxQkTF0iIaACgQxktgwD10UNnThs1Y8SA8eJFBIgG ADqUobFFD505bdSMCQPGixcvTD54aACgA5kZMWDIvGixQkUJEiJChPjgwUOHBgA6dIHhosWKFitW rGjRokQHgBYsWKjQAECHLS1WqHBBQ0ucQYMGdenQ4UOFCQ0AdNCyQkWJFjAMOBg0yE+LCBAoUIjQ AEAHGiVIiIBRxkGIQXy6iEiAwIEECA0AdJghIgQIGnIQdJCjhYYIAgMSRHDQAECHGCBAfJjhhwwX LTRoiBAQAMGDBg0AUGjQoEGDAgAAAAAAAAAABQ0aNGhwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BAQAOw== ''' deljournal_data = '''\ R0lGODlhFAAUAPMJAAAAAJkAAJlmZswAAMwzM/8AAP9mAP+ZM////wAAAAAAAAAAAAAAAAAAAAAA AAAAACH5BAEAAAkALAAAAAAUABQAAAT+MKWUUkoppZRSgimllFISYwiQEkwppZRSSimlNOAxJYCU UkopJQDBACmBcUwJICWYUkpgBJBSSmDAcUoAKaUERgAAppRSAmCMEkBKEIwAQEoppZQAgGCUAMAI AKSUEkwppZQAGCWMACBIKaWUUkopJQDBKAGAlFJKKSWYUkoJjBJKACnBlFJKKaWUwCgBAlACSCml lFJKMIFRAgAAlABSgimllBIYJgCQEgSgBJBSSimBceAIIKWUACgBpARTAuSUAEBKKSUIQAEpJUCO GQHAlFJKKQFQQEoAElMCACmllFJKEACQEiAlAJBSgimllFICBSQAAgQgpZRSSimllCAPSCkBAFJK KaUEU0oppZQiADs= ''' newjournal_data = '''\ R0lGODlhFAAUAPU1AP6KEv6aFv6eGv6iJv7KHv7SFv7aEv7eFv7WIP7SLP7eJP7aKv7WOv7mD/7m Fv7iPv7mOP7qOv7GYv7mQv7mTv7qQv7yav7yffbumP7yhv72hv7yiv72jf7ykv72kv72mPTuovXu p/72ov72qf72rvbytv72tvbuxvr2zv76wv76xv761v762v763v7+4v7+5v7+6v7+8v7+9v7+/v// /wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAADUALAAAAAAUABQAAAb+wFqt VqvVarVao1ar1Wq1Wq1Wq9VqtVqtVqsdarVarVar1Wq1Wq1Wq9VqtRqiVqvVarVarVar1YA1QK1W q9UStVqtVgPUarVarVarCWq1Wu1Rq9VqgVqtVqvVarVarVar1S61Wq1Wq9VqtVqtVqsBa7VK7dPR 1CK1Wq1Wq9VqtVqtVqtlRiZOplar1Wq1Wq1Wq9VqtZSqFXOROLVarVar1RqGAkEBAVo8MJpsJbpM EgVDo1ar1Wq1GgjDkr1KoVqtVqvVarVarVar1S4oFUpUq9VqtVqtVqvVarUapAY8jUK1Ta1Wq9Vq tVqtVqvVarXaplar1Wq1Wq1Wq9VqtUBnrVarUWq1Wk1Sq9VqtVqtBqjVarUaA1ir1Wq1Qa1Wq9Vq tVqtVqvVFrVarVar1Wq1Wq1Wq9VqtVpNUavVarVarVar1Wq1Wq1Wq9UctRqwVqvVarVarVar1Wq1 Wq1Wq9VqtVqtVqvVggA7 ''' dupcontact_data = '''\ R0lGODlhIAAgAPfnAAICAgYGBgoKCg4ODg4eGhoODhISEh4WEh4eHgICMgICNhoaIh4eJhISOhIi HgI+PhoyOiISEi4WFjYeEjYeHjoeHiYmHiIiIiYmJjAuKDY2Ljo6NCI+Rj42QgJCQgJqagJ2dgJ6 ei5SXlYuHk4mJkIyMlY6OmI2Im46JmYyMlY+QmZKNm5OPnxCKkZGQE5ORk5EVFJSTFZWS1paUV5e WFhYanZOTmJiWWZmXmpqVm5uWm5uXmJiYWpqYW5uY3Z2bX5+bnJydnp6cn5+dn5+fgICgioqjjIy ljY2ljo6mipqoiZ+rip2qkJCnkZGolJSpkp2nm5uimJ+mnZ2lmpqrh6YvCKGsiKOthauzBqkxBay zha20BK60hK+1g7G2g7K3A7O3hLC1grW4gra5gbi6gbm7gLq8gLw8nqGgppSNopeXpZiYr5mQuJ6 ToKCeoiIeo6Oetaacu6CUu6GVvqSYvqaaoKCgoaGhoqKgo6Oh4qKioCAkI6OkpKSi5aWipqahpqa ipqaj56ejpaWkZaWlp6eloqKrIKCtIaGto6OsoKWopqaop6upp66tKaml6KimaqqnrKymqamqqqq pq6upaqqrq6utqK+tqq2srKypbi4pL6+qra2srKyurq6tr6+tpqawp6exqKixqamyqqqyq6uzrKy zra2zr6+zrKy3rq6076+1u6qgv60hOKuosbGrsLCtsbGssbGtsLCvsbGu8rKvsrKws7OwsrKzsLC 0sbG0sLC2sbG2srK0srK3M7O3tLSwtLSxtbWxtraxt7exsTE9M7O8tLG4tbW4uLiwubmxuLizebm yurq0e7u2PLyyvLyzvLy3fb23v7+0vr62P7+2eLi5ubm6u7u4urq6O7u7Pb24vb25vLy7vb26/r6 5v7+4P7+5vr67f7+6v7+7v7+8////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAOcALAAAAAAgACAA AAj+AM+dO3fu3LlzduzYsWPHjh07duzYsWPHjh07duzYsWPHjh07GM6dO3fu3LlzQ1Dl6qNHTx5c uGh1K3fNmjhy5MiNGxfOGzdaF86dO3fu3LlzbnIlUmHChIkaxmx1+1Vqlypv1rxpy5bNFzJaGM6d O3fODhEeRAipWKOGBas4vm55O+RNXLZTo3glEkXqkK9aF86dO3eOCKpFRPRQSHGg1Qo5x255O+RN HLJSU0YhQtZrVy9ZFs6dO3fOjaoodvRUSHGCTqs6rm5582UKVSpRqVaBIqYqFLJaF86dO3fODZ8S RDrBiDCiDZ1Wxm55e4LkyJMmR5w0eZLkCBVZGM7+nTt3jsgdCXZ0FWPApkWAOcZucStSpEiRIkWK FClSpIgRgLIunDt37pybOxKI9EoFwUGaDKCMBeNGjty4ceTIjRsXDhw4cLEwnDt37hwRSx3sVBIB IoSHDx72AONGbty4ceHChQsHDly1arUunDt37hwRVKCIeGLUqNGlS5coBfu2gQaNQRt6xCg0ZEMf SK8snDt37pwdVIbs2LJl69atYMCECeMWLdq2aNGiSYMmTZo0arUunDt37pwbSRyI2LJ169atYMCE CeMWIwaeGTjcDJoBCRIkTa8snDt37hwRTmjs2Lp1KxgwYMKECduGw80QHI/e/LgBJFKkTbUu2LH+ QwQHkVmFiNi6dSsYMGDChg3btqwZM2bOmClLpsyZs2evLAxBpSqIHVqF7Ny6FQwYMGHChA3bNm5c uHDgwFWrNm0awGnTal1wo8tQCSKzCtmZNGkSJUqUMmXKBItWrViyar2qVavWq1evIFkg0seEDSK2 Hl3AYAEDBgwWLlywgMHCBQsWLli4cOGCBQsXLgiwo4cCCSK0KAmZMWPGjBk0evTo0cNHjx46cujQ keMGgAHnzp0joocCCTuTHvHRowcPHjx4BhUSJChQIEGABP3xAwfAGQDnzp0j0glGgQsXLFy4cOHC hQsWLly4cMHChQsXLlxAAMCMmTMAzp2zs6v+2AIUEwCcqCGjhwIFChQoUKBAQQIACRq8AAAAQBkA AACcc6OrFAQCLSxMEfXoEy1asmjRkgUrEwAxADS9MXAOABkA586dI1JJBAgQDz54EHILGzdu3LZJ k7YMgJcvYAA8WnAOIIAxAM6dO0fEE6NGjRpdukRJ2LcNNGgM0nADAAAAYQAAAIDgHAAxAM6dO2dH Fy1btm7dChZMGLdo0bZFczbM0Q4AXQDoeHPhHAAwAM6dO+fGlq1bt4IBAwYMWLQYMe7MuPHjhwYA WwD4EHThHIAvAM6dO0fk1q1bwYABgyQkRgYAAAAAAAAAAAAAWAAAAAAAAIAwAM6dO0fE1q2PW7eC AcOkSAoUJUyWWLlypUqWLFmwaNGyhUuXLgDOnTtn59atYMGECXvkw8UFAAAAAAAAAAAAAAAAAAAA AAAAAADOnTtnZ9IkSpQoUYIkqE+fPG5+9KARI0YMGTFixHAx4Ny5c+fOnTt3AQMGCxcwWLhw4cIF BAgQLECAAAGCBQgQIFgAMMC5c+fOnTt3LiAAOw== ''' exportcontact_data = '''\ R0lGODlhIAAgAPfpAAICAg4GBgoKCgoKDgICHgoKFhYSEh4WEh4eGh4eJhIiHhoyOiISEjoeHiYm JioqLjIuJjo6MgICXgICcgICfj4+SiYmagJCQgJubgJ2dgJ6ei5SXlYuHk42IlY6OmI2ImYyMno+ IlY+QlJKPmJGMmZKNm5OPn5CKkJCQkJCSkpKRk5OSk5GVlZOQlJSSlZWTlZWUlpaUkpKblJSYlpW bmJiVn5iTmpqYm5uZmpqamZmfnJyZnZ2bnpyZn5+bn5+dn5+fgICgioqjj4+jjY2lj4+mj4+nlJS pnp6impqrppSGp5SGppSNqJWHr5mEr5qEq5yNq5yOrZuJopeXpZiYqZ2Sqp6Tq5+Tr5mQsJqFsJu FtZuBt52Bup6AuJ6Tup+UoKCdqaCXq6KZqaOcrKafvKCAv6GAv6OAv6WAv6aAv6iAv6qAv6yAv66 Av6+AtKWcuqCVvaaavqSYv7GAv7OAv7WAv7eAv7iAoKCgoKCho6OhoqKio6OipKShpKSipKSjpaW ip6ejpaWlpqalp6eko6OroKCsoKCto6Oso6OupaWvp6upp66sp66tq6eiqqqmqqqnq6unrqqkrqq lr6umrqynqqqpq6upqqqrqamvqqquqK+sqK+trKyorKypra2qrq6pr6+qrKyurq6tpqawp6exqKi xqamyrKy3rq61u6qgvq2hv6ygv62hsbGrsLCtsbGssbGtsbGus7GssrKus7OutLSvsLCxsbGysrK ws7OwsLC0sbG0sLC2srK0srK2srK3tLSwtLSxtbWwtbWxtraxtraytbW1sbG9tbW4uLiwubmxuLi yuLizubmzuLi0uLi3urq0u7u0u7u1u7u2vLyyvLyzvr61v7+0v7+1vr62v7+2v7+3urq5urq6u7u 6vb26vr64vr65v7+4v7+5vr66v7+6v7+7v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAOkALAAAAAAgACAA AAj+ANOlS5cuXbp06dKlS5cuXbp0AACkS5cuXbp06dKlS5cuXbp06dKlS5cuXbp06dKlS5cuXToA d+4ASJcuXbp06dKlS5cuXbp06dKlS5cuXbp06dKlS5cuHQA7duzYAZAuXbp06dKlS5cuXbp06dKl S5cuXbp06dKlS5cOQJ06derUqQMgXbp06dKlS5cuXbp06dKlS5cuXbp06dKlSweADh06dOjQoUMH QLp06dKlS5cuXbp06dKlS5cuXbp06dKlAzBnzpw5c+bMmTNnDoB06dKlS5cuXbp06dKlS5cuXbp0 6dIBcOPGjRs3bty4cePGjRsA6dKlS5cuXbp06dL+pUuXLl26dOkAAAAAAECbNm3atGkDAAAAAADS pUuXLl06gOnSpUuXLl26dOnSpUuXLh0ANmzYsGHDBkC6dOnSpUuXLl26dOnSpUuXLl26dOnSpUuX Lh2ANWvWrFmzBkC6dOnSpUuXLl26dOnSpUuXLl26dOnSpUuXLh0ANWrUqFGjBkC6dOnSpUuXLl26 dOnSpUuXLl26dOnSpUuXLh2ANGnSpEmTBkC6dOnSpUuXLl26dOnSpUuXLl26dOnSpUuXLh0ANGjQ oEGDBkC6dOnSpUuXLl26dOnSpUuXLl26dOnSpUuXLh2AM2fOnDlzBkC6dOnSpUuXLl26dOnSpUv+ ly4dHjx48ODBgwcIADNmzJgxYwYAihxA8ODBgwePg3Tp0qVLly4dnlu7BO3h8wcXgDJlypQpUwaA HmLizpkDV04WQAfp0qVLly5dOjy7EInw4MEDDQBdunTp0qULgDzCunn7BiyZLAfp0qVLly5dOjyC RFCZYkLVGwBcuHDhwoULAB2aEpk6dQiYLAfp0qVLly5dOjx7GoA40KrEFwBbtmzZsmULABmFDB3z 1etXLAfp0qVLly5dOjx7GoD4IIdVHANatGRx8sTJACSZSEFLVSpZLAfp0qVLly5dOjyiWDDg4EXO qgdSpDRZoqRJAQtDihwxQiRJLAfp0qVLly7+XTo8vJAlwHIiAJwKUKKECBEiBAEJEygECRJESCwH 6dKlS5cuXTo8vFAtUMAEgqIZVaxcuXLliotY0caRG8eNGywH6dKlS5cuXTo8mDZk0HABw4UUYcSI ESNGzI1a08aNG8cN4DZYDtKlS5cuXbp0eEYtYtSIE6dNOMaQ6UCChA0EMFQM+hHBTyRYDtKlS5cu Xbp0eHLl0qVLV7BghBxRkiRpkqRAw6JJ06bt2jVYDtKlS5cuXbp0eHLl0qUrmLBgnyrNGjGiR4sV PPq8gAQJEihXDtKlS5cuXbp0eHLp0hUsmLBhtmg9i8GDRwxAYHbU8NGpUyhXDtKlS5cuXbqPdHh0 6dIVTBgxYsOCUTPWjFkxZ8yWKVtmzVo1Vw7SpUuXLl26dHh06QomjBgxYsWKhTNHjty4cdy4bduW DRs2Vw7SpUuXLl26dHgsXbp06ZInT548vZIlK1asWLFgwYLlypWrRw7SpUuXLl26dA4cOHDgwIED Bw4cOHDgwIEDBw4cOHAA0IEDBw4EpEuXLiAAOw== ''' conntocard_data = '''\ R0lGODlhFAAUAPQAAAAAAGYzM0VFRUhISGBgYHJycnh4eACAgJlmZsZrRfeGVpSUlJ6enqqqqr+/ v/+5iQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEA ABAALAAAAAAUABQAAAX+IARBEARBEARBEARBEARBEARBEARBEASBEARBEARBEAQAAAAAAABAEARB EARBEARCRLEsy1IQAAAAAAAAAAAAAAAAAAAABEgATdM0TdNAEARBEARBAAAQAAAAAAAAIARBEARB ECAABABBEARBEARBEARBEGCAAAFAEARBEARBEARBEAQwwABAEARBEARCEARBEAQBjQMAAAAAAAAA AAAAAAAAANg4AIIgCAJAEEEQBEEQAwAACIIAz/MAIAAAAAAAAABBQBAAzwMoEARBEARBEASBEBAE gPI8igJBEARBEARBEBAEiaI8DwRCEARBEARBEAQBAJAoygNBEARBEAQ6QRAIQRCQJACgQBAEQRAE QRAEQRAAJAkAQSAEQRAEQRAEQRBwHABwABAEQRAEQRAEgRBwHMdxHAcQAgA7 ''' def createIconImages(): if not IconImages: for key,value in zip(globals().keys(), globals().values()): if key[-5:] == "_data": icon = key[:-5] if icon[-5:] == "_grey": icon = icon[:-5] IconImagesGrey[icon] = PhotoImage(data=value) else: IconImages[icon] = PhotoImage(data=value) PyCoCuMa-0.4.5-6/pycocumalib/ImportExportDialog.py0000644000175000017500000000361410252657644023075 0ustar henninghenning00000000000000#!/usr/bin/python # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: ImportExportDialog.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import Pmw from RadioSelectDialog import RadioSelectDialog from InputWidgets import TextComboEdit class ImportExportDialog(RadioSelectDialog): # exporttargets e.g.: [("csv","Comma Separated Values"),("latex","LaTeX)] def __init__(self, master, exporttargets, title="", headline=""): RadioSelectDialog.__init__(self, master, exporttargets, title, headline) self.combo = TextComboEdit(self.interior(), labelpos="w", label_text="Encoding: ", nomanualedit=True) self.combo.setlist([ "UTF-8", "UTF-16", "ISO-8859-1", "ISO-8859-2", "ISO-8859-3", "ISO-8859-4", "ISO-8859-5", "ISO-8859-6", "ISO-8859-7", "ISO-8859-8", "ISO-8859-9", "ISO-8859-10", "ISO-8859-13", "ISO-8859-14", "ISO-8859-15", "Base64", "Quoted-Printable" ]) self.combo.set("UTF-8") self.combo.grid(padx=2, pady=2) def getvalue(self): "Return selected RadioButton and Encoding as Tuple" return (self.targetvar.get(), self.combo.get()) if __name__ == "__main__": # Unit Test: tk = Tk() exporttargets = [("csv","Comma Separated Values"), ("dat","Future Data Format")] dlg = ImportExportDialog(tk, exporttargets, title="Test", headline="Export To:") print dlg.activate() tk.destroy() PyCoCuMa-0.4.5-6/pycocumalib/InputWidgets.py0000644000175000017500000010272510252657644021732 0ustar henninghenning00000000000000""" Edit Boxes, Combobox, etc """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: InputWidgets.py 92 2004-11-28 15:34:44Z henning $ import sys, os from Tkinter import * import Pmw import debug import IconImages import ToolTip class ArrowButton(Canvas): def __init__(self, master, direction='down', command=None, **kws): kws.setdefault('width', 16) kws.setdefault('height', 16) kws.setdefault('borderwidth', 2) Canvas.__init__(self, master, relief=RAISED, highlightthickness=0, **kws) self._disabled = False self.direction = direction self.command = command self.bind('', self._btnPress) self.bind('', self._btnRelease) self._drawArrow() _arrowRelief = RAISED def _drawArrow(self, sunken=0): if sunken: self._arrowRelief = self.cget('relief') self.configure(relief = SUNKEN) else: self.configure(relief = self._arrowRelief) if self._disabled: color = '#aaaaaa' else: color = '#000000' Pmw.drawarrow(self, color, self.direction, 'arrow') def _btnPress(self, event): if not self._disabled: self._drawArrow(sunken=1) def _btnRelease(self, event): if not self._disabled: if self.command: self.command() self._drawArrow() def setdirection(self, dir): self.direction = dir self._drawArrow() def configure(self, **kws): if kws.has_key('state'): self._disabled = kws['state'] == 'disabled' self._drawArrow() Canvas.configure(self, **kws) class AbstractSingleVarEdit: def __init__(self): self._state = NORMAL self._var = None self._save_hooks = [] # Descendants should implement the following: #def get(self): # pass #def clear(self): # pass #def set(self): # pass def save(self, event=None): if self._state != DISABLED and self._var is not None: if self._var.get() != self.get(): self._var.set(self.get()) for hook in self._save_hooks: hook() def add_save_hook(self, save_hook): self._save_hooks.append(save_hook) def bindto(self, var): self.save() self._var = None self.clear() if var is not None: self._var = var self.set(var.get()) class LabelPseudoEdit(AbstractSingleVarEdit, Label): "Fake Edit Widget, polymorphic to TextEdit and Pmw.EntryField" def __init__(self, master, **kws): AbstractSingleVarEdit.__init__(self) kws.setdefault('width', 16) Label.__init__(self, master, **kws) def set(self, val): self.configure(text=val) setentry = set def get(self): return self.cget('text') getvalue = get def clear(self): self.set('') def component(self, name): return self def cget(self, opt): return Label.cget(self, opt.split('_')[-1]) class TextEdit(AbstractSingleVarEdit, Pmw.EntryField): def __init__(self, master, **kws): AbstractSingleVarEdit.__init__(self) kws.setdefault('entry_width', 16) kws.setdefault('modifiedcommand', self.save) Pmw.EntryField.__init__(self, master, **kws) def toolTipMaster(self): "Returns real Tk widget for use by ToolTip" return self.component('entry') def clear(self): self.delete(0, END) def set(self, text): self.delete(0, END) self.insert(END, text) self.save() def disable(self): self._state = DISABLED self.clear() self.configure(entry_state = DISABLED) def enable(self): self._state = NORMAL self.configure(entry_state = NORMAL) def focus_set(self): self.component('entry').focus_set() class CheckboxEdit(AbstractSingleVarEdit, Pmw.RadioSelect): def __init__(self, master, **kws): AbstractSingleVarEdit.__init__(self) kws.setdefault('buttontype', 'checkbutton') kws.setdefault('command', self.saveCallback) Pmw.RadioSelect.__init__(self, master, **kws) self.add('yes') def saveCallback(self, nothing=None, nothing2=None): self.save() def toolTipMaster(self): "Returns real Tk widget for use by ToolTip" return self.component('yes') def clear(self): self.setvalue([]) def get(self): if 'yes' in self.getvalue(): return 'yes' return 'no' def set(self, text): if text == 'yes': self.setvalue(['yes']) else: self.setvalue([]) self.save() def disable(self): self._state = DISABLED self.clear() def enable(self): self._state = NORMAL def focus_set(self): self.component('yes').focus_set() class TextComboEdit(AbstractSingleVarEdit, Pmw.ComboBox): def __init__(self, master, **kws): AbstractSingleVarEdit.__init__(self) if sys.platform != 'win32': # Thin Scrollbars: kws['scrolledlist_horizscrollbar_borderwidth'] = 1 kws['scrolledlist_horizscrollbar_width'] = 10 kws['scrolledlist_vertscrollbar_borderwidth'] = 1 kws['scrolledlist_vertscrollbar_width'] = 10 self.nomanualedit = False if kws.has_key('nomanualedit'): self.nomanualedit = kws['nomanualedit'] del kws['nomanualedit'] modifiedcommand = self.save if kws.has_key('modifiedcommand'): modifiedcommand = kws['modifiedcommand'] del kws['modifiedcommand'] if self.nomanualedit: kws['entryfield_pyclass'] = LabelPseudoEdit kws['entryfield_relief'] = SUNKEN kws['entryfield_borderwidth'] = 1 kws['selectioncommand'] = modifiedcommand else: kws['entryfield_modifiedcommand'] = modifiedcommand Pmw.ComboBox.__init__(self, master, scrolledlist_scrollmargin=0, history=0, unique=0, buttonaspect=0.75, hull_borderwidth=0, arrowbutton_highlightthickness=0, **kws) def toolTipMaster(self): "Returns real Tk widget for use by ToolTip" return self.component('entry') def clear(self): self.component("entryfield").clear() def get(self): return self.component("entryfield").getvalue() def set(self, val): self.setentry(val) def setlist(self, items): self.component("scrolledlist").setlist(items) def getlist(self): return self.component("scrolledlist").get() class FilenameEdit(AbstractSingleVarEdit, Frame): def __init__(self, master, **kws): AbstractSingleVarEdit.__init__(self) Frame.__init__(self, master, borderwidth=0) self.type = kws.get('type', 'open') self.filetypes = kws.get('filetypes', [("All Files", "*")]) self.showbasenameonly = kws.get('showbasenameonly', False) self.columnconfigure(0, weight=1) if self.showbasenameonly: self.edtFilename = LabelPseudoEdit(self, borderwidth=1, relief=SUNKEN) self._fullpath = '' else: self.edtFilename = TextEdit(self) self.edtFilename.grid(sticky=W+E) self.btnBrowse = Button(self, text='...', command=self._browse, padx=0, pady=0) ToolTip.ToolTip(self.btnBrowse, 'browse files') self.btnBrowse.grid(row=0, column=1) _browsedlg = None def _browse(self): if not self._browsedlg: dir, fname = os.path.split(self.get()) import tkFileDialog if self.type == 'saveas': self._browsedlg = tkFileDialog.SaveAs(filetypes = self.filetypes, initialfile=fname, initialdir=dir) else: self._browsedlg = tkFileDialog.Open(filetypes = self.filetypes, initialfile=fname, initialdir=dir) ret = self._browsedlg.show() if ret: self.set(ret) def toolTipMaster(self): "Returns real Tk widget for use by ToolTip" return self.edtFilename.toolTipMaster() def clear(self): self.edtFilename.clear() self._fullpath = '' def bindto(self, var): if self.showbasenameonly: AbstractSingleVarEdit.bindto(self, var) else: self.edtFilename.bindto(var) def set(self, val): if self.showbasenameonly: self._fullpath = val self.edtFilename.set(os.path.basename(val)) self.save() else: self.edtFilename.set(val) def get(self): if self.showbasenameonly: return self._fullpath else: return self.edtFilename.get() class FontEdit(AbstractSingleVarEdit, Frame): def __init__(self, master, **kws): AbstractSingleVarEdit.__init__(self) Frame.__init__(self, master, borderwidth=0) self.columnconfigure(0, weight=1) self.edtFontFamily = TextComboEdit(self, nomanualedit=True, modifiedcommand=self.save) import tkFont fontfamilies = list(tkFont.families()) fontfamilies.sort() self.edtFontFamily.setlist(fontfamilies) self.edtFontFamily.grid(rowspan=2, sticky=W+E) self.edtFontSize = Pmw.EntryField(self, validate={'validator':'integer', 'min':1, 'max':72}, entry_width=3, entry_justify=RIGHT, modifiedcommand=self.save) self.edtFontSize.grid(row=0, rowspan=2, column=1) btn = ArrowButton(self, direction='up', borderwidth=1, width=8, height=8, command=self._sizeIncr) btn.grid(row=0, column=2) btn = ArrowButton(self, direction='down', borderwidth=1, width=8, height=8, command=self._sizeDecr) btn.grid(row=1, column=2) self.edtFontSizeUnit = TextComboEdit(self, nomanualedit=True, modifiedcommand=self.save, entryfield_width=2) self.edtFontSizeUnit.setlist(['pt','px']) self.edtFontSizeUnit.grid(row=0, rowspan=2, column=3) self.edtFontModifiers = TextComboEdit(self, nomanualedit=True, modifiedcommand=self.save, entryfield_width=8) self.edtFontModifiers.setlist(['normal', 'bold', 'italic', 'bold italic']) self.edtFontModifiers.grid(row=0, rowspan=2, column=4) def _sizeIncr(self): text = self.edtFontSize.getvalue() if text != '': size = int(text); size += 1 else: size = 10 self.edtFontSize.setvalue(size) def _sizeDecr(self): text = self.edtFontSize.getvalue() if text != '': size = int(text); size += -1 else: size = 10 self.edtFontSize.setvalue(size) def clear(self): pass def set(self, val): try: family, size, mod = val except: debug.echo('FontEdit.set(): Illegal font specification.') family, size, mod = ('', '', '') self.edtFontFamily.set(family) if size[:1] == '-': size = size[1:] self.edtFontSizeUnit.set('px') else: self.edtFontSizeUnit.set('pt') self.edtFontSize.setvalue(size) if mod == '': mod = 'normal' self.edtFontModifiers.set(mod) def get(self): size = self.edtFontSize.getvalue() if self.edtFontSizeUnit.get() == 'px': size = '-'+size mod = self.edtFontModifiers.get() if mod == 'normal': mod = '' return (self.edtFontFamily.get(), size, mod) class MemoEdit(AbstractSingleVarEdit, Pmw.ScrolledText): def __init__(self, master, **kws): AbstractSingleVarEdit.__init__(self) kws.setdefault('text_width', 20) kws.setdefault('text_height', 4) kws.setdefault('text_wrap', WORD) # File extension for temp-File to open in # external editor: kws.setdefault('file_extension', '.txt') self._file_extension = kws['file_extension'] del kws['file_extension'] if sys.platform != 'win32': # Thin Scrollbars: kws['horizscrollbar_borderwidth'] = 1 kws['horizscrollbar_width'] = 10 kws['vertscrollbar_borderwidth'] = 1 kws['vertscrollbar_width'] = 10 Pmw.ScrolledText.__init__(self, master, scrollmargin=0, **kws) self.component('text').bind("", self.save) self.component('text').bind("", self.save) self.component('text').bind('', self.editInExternalEditor) def toolTipMaster(self): "Returns real Tk widget for use by ToolTip" return self.component('text') def get(self): return Pmw.ScrolledText.get(self, "1.0", END)[:-1] def clear(self): self.delete("1.0", END) def set(self, text): self.clear() self.insert(END, text, 'default') def editInExternalEditor(self, event=None): import tempfile fhandle, fname = tempfile.mkstemp(self._file_extension) os.write(fhandle, self.get().encode('utf-8', 'replace')) os.close(fhandle) try: # open external editor os.spawnlp(os.P_WAIT, 'gvim', 'gvim', '-f', fname) # read modified temporary file fileobj = open(fname, 'rb') self.set(unicode(fileobj.read(), 'utf-8', 'replace')) fileobj.close() finally: # finally delete the temporary file os.unlink(fname) class DateEdit(AbstractSingleVarEdit, Pmw.Counter): def __init__(self, master, **kws): AbstractSingleVarEdit.__init__(self) if kws.has_key('label_text'): kws['frame_borderwidth']=0 Pmw.Counter.__init__(self, master, entry_width = 10, entry_justify = LEFT, buttonaspect=0.5, datatype={'counter':'date', 'separator':'-', 'format':'ymd', 'yyyy':1}, entryfield_value = "0000-00-00", entryfield_validate = {'validator' : 'date', 'min' : '1700-01-01', 'max' : '2100-12-31', 'minstrict' : 0, 'maxstrict' : 0, 'format' : 'ymd', 'separator' : '-'}, entryfield_modifiedcommand=self.save, hull_borderwidth=0, #downarrow_borderwidth=1, downarrow_highlightthickness=0, #uparrow_borderwidth=1, uparrow_highlightthickness=0, **kws) def toolTipMaster(self): "Returns real Tk widget for use by ToolTip" return self.component('entry') def get(self): return self.getvalue() # inherited from Pmw.Counter / Pmw.EntryField def set(self, val): self.setvalue(val) class TimeEdit(AbstractSingleVarEdit, Pmw.Counter): def __init__(self, master, **kws): AbstractSingleVarEdit.__init__(self) if kws.has_key('label_text'): kws['frame_borderwidth']=0 Pmw.Counter.__init__(self, master, entry_width=8, entry_justify = LEFT, buttonaspect=0.5, datatype={'counter':'time', 'separator':':', 'time24':1}, entryfield_value = "00:00:00", entryfield_validate = {'validator' : 'time', 'separator' : ':'}, entryfield_modifiedcommand=self.save, hull_borderwidth=0, #downarrow_borderwidth=1, downarrow_highlightthickness=0, #uparrow_borderwidth=1, uparrow_highlightthickness=0, **kws) def toolTipMaster(self): "Returns real Tk widget for use by ToolTip" return self.component('entry') def get(self): return self.getvalue() # inherited from Pmw.Counter / Pmw.EntryField def set(self, val): self.setvalue(val) class MultiSelectButtons(Frame): from Set import Set def __init__(self, master, buttondefs, icons, iconsgrey): self.__state = NORMAL self.__var = None self._save_hooks = [] self.__set = self.Set() self.__buttondefs = buttondefs self.__icons = icons self.__iconsgrey = iconsgrey self.__buttons = {} Frame.__init__(self, master) self.__createWidgets() def __createWidgets(self): import ToolTip for name, descr in self.__buttondefs: def command(self=self, name=name): if name in self.__set: self.__set.remove(name) self.save() else: self.__set.add(name) self.save() self.__update() self.__buttons[name] = btn = Button(self, image=self.__iconsgrey[name], command=command, relief=FLAT) btn.pack(side=LEFT, fill=X, expand=TRUE) tooltip = ToolTip.ToolTip(btn, descr) def MouseEnter(event, self=self, name=name): if not name in self.__set: self.__buttons[name]["image"] = self.__icons[name] def MouseLeave(event, self=self, name=name): if not name in self.__set: self.__buttons[name]["image"] = self.__iconsgrey[name] btn.bind("", MouseEnter, add="+") btn.bind("", MouseLeave, add="+") def __update(self): for name, btn in zip(self.__buttons.keys(), self.__buttons.values()): if name in self.__set: btn["relief"] = SUNKEN btn["image"] = self.__icons[name] else: btn["relief"] = FLAT btn["image"] = self.__iconsgrey[name] def save(self, event=None): if self.__state != DISABLED and self.__var is not None: self.__var.assign(self.__set) for hook in self._save_hooks: hook() def add_save_hook(self, save_hook): self._save_hooks.append(save_hook) def bindto(self, var): if var: self.__var = var self.__set.assign(var) else: self.__set.assign(self.Set()) self.__var = None self.__update() def disable(self): self.__state = DISABLED for btn in self.__buttons.values(): btn["state"] = DISABLED self.__set.assign(self.Set()) self.__update() def enable(self): self.__state = NORMAL for btn in self.__buttons.values(): btn["state"] = NORMAL class MultiRecordEdit(Pmw.Group): "Edit Control e.g. for vCard.adr/tel/email" def __init__(self, master, recclass, title, rectitle=None): self.state = NORMAL self.__records = [] self.__recidx = 0 self.__recclass = recclass if not rectitle: rectitle = title self.__rectitle = rectitle Pmw.Group.__init__(self, master, tag_text = title) self.__createWidgets() def __createWidgets(self): master = self.interior() topbar = Frame(master) topbar.pack(side=TOP, fill=X) self.body = Frame(master) self.body.pack(side=BOTTOM, fill=BOTH, expand=1) topbar.columnconfigure(0, weight=1) self.__selRecord = Pmw.OptionMenu(topbar, items=[], command=self.__SelectRecordCallback, menubutton_padx=2, menubutton_pady=1) self.__selRecord.grid(row=0, column=0, sticky=W+E) self.__lblRecordCount = Label(topbar, text="(0)") self.__lblRecordCount.grid(row=0, column=1) self.__btnAddRecord = Button(topbar, image=IconImages.IconImages["new"], command=self._AddRecord) self.__btnAddRecord.grid(row=0, column=2) self.__btnDelRecord = Button(topbar, image=IconImages.IconImages["trash"], command=self._DelRecord) self.__btnDelRecord.grid(row=0, column=3) self.createBody() self.disableAll() def createBody(self): """Create Edit Widgets in self.body for selected Record To Be overwritten by SubClass""" raise NotImplementedError def bodyChildren(self): """return list of widgets created in createBody To Be overwritten by SubClass""" return [] def disableAll(self): for x in self.bodyChildren(): try: x.disable() except: pass self.__btnDelRecord["state"] = DISABLED self.state = DISABLED def enableAll(self): for x in self.bodyChildren(): try: x.enable() except: pass self.__btnDelRecord["state"] = NORMAL self.state = NORMAL def __updateMenu(self): items = [] for x in range(1, len(self.__records)+1): items.append(self.__rectitle + " %d" % x) self.__selRecord.setitems(items) self.__lblRecordCount["text"] = "(%d)" % len(items) def __SelectRecordCallback(self, str): self.__recidx = int(str.split()[1])-1 self.bindtorec(self.__records[self.__recidx]) def __Select(self, idx): self.__selRecord.invoke(idx) onRecordAdd = None def _AddRecord(self): self.__records.append(self.__recclass()) self.__updateMenu() self.__Select(len(self.__records)-1) self.enableAll() # Callback can be set by SubClass: if self.onRecordAdd: self.onRecordAdd(self.__records[-1]) onRecordDel = None def _DelRecord(self): del self.__records[self.__recidx] self.__updateMenu() if self.__records == []: self.disableAll() else: self.__Select(0) # Callback can be set by SubClass: if self.onRecordDel: self.onRecordDel() def bindto(self, records): if self.__records: # Save Old Binding (Rebind old): self.bindtorec(self.__records[self.__recidx]) if records is None: self.__records = [] self.__updateMenu() self.disableAll() self.bindtorec(None) else: self.__records = records self.__updateMenu() if len(self.__records)>0: self.enableAll() prefidx = 0 # Find the preferred record: for i in range(len(self.__records)): if self.__records[i].is_pref(): prefidx = i break self.__Select(prefidx) else: self.disableAll() self.bindtorec(None) def bindtorec(self, rec): "To be overridden by SubClass" pass class UnicodeVar(Variable): """Value holder for unicode string variables.""" _default = "" def __init__(self, master=None): """Construct a string variable. MASTER can be given as master widget.""" Variable.__init__(self, master) def get(self): """Return value of variable as unicode string.""" # Tk uses UTF8 => convert to Python Unicode String #return unicode(self._tk.globalgetvar(self._name), 'utf8') # At least in Python 2.3 this conversion is not needed: return self._tk.globalgetvar(self._name) class SearchableCombobox(Frame): def __init__(self, master, command, label_text=""): self.do_not_execute = 0 self._isPosted = 0 self.buttonaspect = 0.75 # list of (dispname, value, idx) tuples: self.list = [] # current index of self.list: self.curidx = 0 # list of (dispname, value, idx) tuples: self.__droplist = [] self.command = command Frame.__init__(self, master) self.columnconfigure(1, weight=1) self.label = label = Label(self, text=label_text) label.grid() self.entryvar = UnicodeVar() self.entryvar.trace("w", self.__entrychanged) self.entry = entry = Entry(self, textvariable=self.entryvar) self.normalBackground = self.entry['background'] entry.grid(column=1, row=0, sticky=W+E) self.button = button = Canvas(self, borderwidth=2, relief=RAISED, width=16, height=16, highlightthickness=0) button.grid(column=2, row=0) self._arrowRelief = self.button.cget('relief') self.popup = popup = Toplevel(self, borderwidth=1) popup.withdraw() popup.overrideredirect(1) kws = {} if sys.platform != 'win32': # Thin Scrollbars: kws['horizscrollbar_borderwidth'] = 1 kws['horizscrollbar_width'] = 10 kws['vertscrollbar_borderwidth'] = 1 kws['vertscrollbar_width'] = 10 self.scrolledlist = scrolledlist = Pmw.ScrolledListBox(popup, scrollmargin=0, hull_height=240, usehullsize=1, listbox_highlightthickness=0, **kws) scrolledlist.pack(fill=BOTH, expand=TRUE) self.listbox = listbox = scrolledlist.component("listbox") # Bind events to the arrow button. button.bind('<1>', self.invoke) button.bind('', self._drawArrow) button.bind('<3>', self._next) button.bind('', self._previous) button.bind('', self._next) button.bind('', self._previous) button.bind('', self.invoke) button.bind('', self.invoke) button.bind('', self.invoke) button.bind('', self.invoke) button.bind('', self.invoke) # Bind events to the dropdown window. popup.bind('', self._unpostList) popup.bind('', self._selectUnpost) popup.bind('', self._selectUnpost) popup.bind('', self._dropdownBtnRelease) popup.bind('', self._unpostOnNextRelease) # Bind events to the Tk listbox. listbox.bind('', self._unpostOnNextRelease) # Bind events to the Tk entry widget. entry.bind('<1>', self._unpostList) entry.bind('', self._entryfocusin) entry.bind('', self._resizeArrow) entry.bind('', self.invoke) entry.bind('', self.invoke) entry.bind('', self.invoke) entry.bind('', self.invoke) entry.bind('', self._next) entry.bind('', self._previous) # Need to unpost the popup if the entryfield is unmapped (eg: # its toplevel window is withdrawn) while the popup list is # displayed. entry.bind('', self._unpostList) def _next(self, event): if self._isPosted: self.listbox.focus_set() # Select (blue bg) first item: self.listbox.select_set(0) # Activate (underline) first item: self.listbox.activate(0) else: self.__select(self.curidx +1 ) self.entry.select_range(0, END) def _previous(self, event): self.__select(self.curidx -1 ) self.entry.select_range(0, END) def _entryfocusin(self, event): self.entry.select_range(0, END) def _drawArrow(self, event=None, sunken=0): arrow = self.button if sunken: self._arrowRelief = arrow.cget('relief') arrow.configure(relief = 'sunken') else: arrow.configure(relief = self._arrowRelief) direction = 'down' Pmw.drawarrow(arrow, self.entry['foreground'], direction, 'arrow') def invoke(self, event=None): self.__setdroplist() self._postList() def _postList(self, event = None): self._isPosted = 1 self._drawArrow(sunken=1) # Make sure that the arrow is displayed sunken. self.update_idletasks() x = self.entry.winfo_rootx() y = self.entry.winfo_rooty() + \ self.entry.winfo_height() w = self.entry.winfo_width() + self.button.winfo_width() h = self.listbox.winfo_height() sh = self.winfo_screenheight() if y + h > sh and y > sh / 2: y = self.entry.winfo_rooty() - h self.scrolledlist.configure(hull_width=w) Pmw.setgeometryanddeiconify(self.popup, '+%d+%d' % (x, y)) # Grab the popup, so that all events are delivered to it, and # set focus to the listbox, to make keyboard navigation # easier. #Pmw.pushgrab(self.popup, 1, self._unpostList) #self.listbox.focus_set() self._drawArrow() # Ignore the first release of the mouse button after posting the # dropdown list, unless the mouse enters the dropdown list. self._ignoreRelease = 1 def _dropdownBtnRelease(self, event): if (event.widget == self.scrolledlist.component('vertscrollbar') or event.widget == self.scrolledlist.component('horizscrollbar')): return if self._ignoreRelease: self._unpostOnNextRelease() return self._unpostList() if (event.x >= 0 and event.x < self.listbox.winfo_width() and event.y >= 0 and event.y < self.listbox.winfo_height()): self.__listboxselect() def _unpostOnNextRelease(self, event = None): self._ignoreRelease = 0 def _resizeArrow(self, event): bw = (int(self.button['borderwidth']) + int(self.button['highlightthickness'])) newHeight = self.entry.winfo_reqheight() - 2 * bw newWidth = int(newHeight * self.buttonaspect) self.button.configure(width=newWidth, height=newHeight) self._drawArrow() def _unpostList(self, event=None): if not self._isPosted: # It is possible to get events on an unposted popup. For # example, by repeatedly pressing the space key to post # and unpost the popup. The event may be # delivered to the popup window even though # Pmw.popgrab() has set the focus away from the # popup window. (Bug in Tk?) return # Restore the focus before withdrawing the window, since # otherwise the window manager may take the focus away so we # can't redirect it. Also, return the grab to the next active # window in the stack, if any. #Pmw.popgrab(self.popup) self.popup.withdraw() self._isPosted = 0 self._drawArrow() def _selectUnpost(self, event): self._unpostList() self.__listboxselect() def focus_set(self): self.entry.focus_set() def __find(self, searchstr): def matches(item, searchstr=searchstr.lower()): return item[0].lower()[:len(searchstr)] == searchstr # Return list of (dispname, value, listidx) tuples: ret = filter(matches, self.list) return ret def __setdroplist(self, list=None): if list is not None: # Droplist must be (dispname, value, listidx) tuples! self.__droplist = list else: # Default Droplist: All Items self.__droplist = self.list self.scrolledlist.setlist([name for name, value, idx in self.__droplist]) def __entrychanged(self, x, y, z): text = self.entryvar.get() if not text: return res = self.__find(text) if len(res) == 1 or (len(res) > 1 and res[0][0] == text): self.entry['background'] = self.normalBackground self.update_idletasks() dispname, value, idx = res[0] # Only one match found --> execute command: if not self.do_not_execute: self.__select(idx) self._unpostList() elif res: # More than one match self.entry['background'] = 'pink' self.update_idletasks() dispname, value, idx = res[0] self.curidx = idx # Display popup listbox: self.__setdroplist(res) self._postList() else: # No match self.entry['background'] = 'pink' self._unpostList() def __select(self, listidx): if listidx >= len(self.list): listidx = 0 elif listidx < 0: listidx = len(self.list)-1 dispname, value, idx = self.list[listidx] if self.command: self.command(value) self.do_not_execute = 1 self.entryvar.set(dispname) self.do_not_execute = 0 self.curidx = listidx def __listboxselect(self): sels = self.listbox.curselection() if len(sels) > 0: dispname, value, listidx = self.__droplist[int(sels[0])] self.__select(listidx) def set(self, text, do_not_execute=None): if do_not_execute: self.do_not_execute = 1 self.entryvar.set(text) self.do_not_execute = 0 def setlist(self, list): # list must be a list of (dispname, value, idx) tuples: self.list = zip([name for name, value in list], [value for name, value in list], xrange(len(list))) self.entryvar.set("") self.__setdroplist() PyCoCuMa-0.4.5-6/pycocumalib/JournalEditWidget.py0000644000175000017500000003071710252657644022671 0ustar henninghenning00000000000000# Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: JournalEditWidget.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import Pmw import string import debug import IconImages import broadcaster import broker from AbstractJournalView import AbstractJournalView import ToolTip PADX = PADY = 1 # stores the handle of the journal under edit: affectedJournal = None def _broadcast_journal_modify(): broadcaster.Broadcast('Journal', 'Modified', {'handle':affectedJournal}, onceonly=1) currentEditControl = None def _setcurrentEditControl(event): global currentEditControl currentEditControl = event.widget import InputWidgets from InputWidgets import MultiRecordEdit class DateEdit(InputWidgets.DateEdit): def __init__(self, master, **kws): InputWidgets.DateEdit.__init__(self, master, **kws) self.add_save_hook(_broadcast_journal_modify) self.component('entry').bind('', _setcurrentEditControl) class TimeWrapperVar: "Wraps a vC_datetime var" def __init__(self, datetime): self.datetime = datetime def set(self, time): self.datetime.setTime(time) def get(self): return self.datetime.getTime() class DateWrapperVar: "Wraps a vC_datetime var" def __init__(self, datetime): self.datetime = datetime def set(self, date): self.datetime.setDate(date) def get(self): return self.datetime.getDate() class TimeEdit(InputWidgets.TimeEdit): def __init__(self, master, **kws): InputWidgets.TimeEdit.__init__(self, master, **kws) self.add_save_hook(_broadcast_journal_modify) class MemoEdit(InputWidgets.MemoEdit): def __init__(self, master): InputWidgets.MemoEdit.__init__(self, master) self.add_save_hook(_broadcast_journal_modify) self.bind('', _setcurrentEditControl) class TextEdit(InputWidgets.TextEdit): def __init__(self, master, **kws): InputWidgets.TextEdit.__init__(self, master, **kws) self.add_save_hook(_broadcast_journal_modify) self.bind('', _setcurrentEditControl) class TextComboEdit(InputWidgets.TextComboEdit): def __init__(self, master, **kws): InputWidgets.TextComboEdit.__init__(self, master, **kws) self.add_save_hook(_broadcast_journal_modify) self.component('entry').bind('', _setcurrentEditControl) class MultiSelectButtons(InputWidgets.MultiSelectButtons): def __init__(self, master, buttondefs, icons, iconsgrey): InputWidgets.MultiSelectButtons.__init__(self, master, buttondefs, icons, iconsgrey) self.add_save_hook(_broadcast_journal_modify) class UTCOffsetPropEdit(InputWidgets.AbstractSingleVarEdit, Pmw.EntryField): import re _utcoffsetregex = re.compile('[-+][0-2][0-9]:[0-5][0-9]') def __init__(self, master, title, descr): InputWidgets.AbstractSingleVarEdit.__init__(self) Pmw.EntryField.__init__(self, master, entry_width = 10, entry_justify = LEFT, value = "+00:00", validate = {'validator' : self.validate}, modifiedcommand=self.save) ToolTip.ToolTip(self, descr) def validate(self, str): if str: if not str[0] in "0123456789+-:": return 0 #ERROR elif self._utcoffsetregex.match(str) is None: return -1 #PARTIAL else: return 1 #OK else: return -1 #PARTIAL def get(self): return self.getvalue() # inherited from Pmw.EntryField def set(self, val): self.setvalue(val) class LatLongPropEdit(InputWidgets.AbstractSingleVarEdit, Frame): def __init__(self, master, title, descr): InputWidgets.AbstractSingleVarEdit.__init__(self) Frame.__init__(self, master) self.edtLat = Pmw.EntryField(self, entry_width = 8, entry_justify = LEFT, value = "0.0", validate = {'validator' : 'real', 'min':-90.0,'max':90.0}, modifiedcommand=self.save) ToolTip.ToolTip(self.edtLat, "Latitude as Float (53.5)") self.edtLat.grid(row=0, column=0) self.edtLon = Pmw.EntryField(self, entry_width = 8, entry_justify = LEFT, value = "0.0", validate = {'validator' : 'real', 'min':-90.0,'max':90.0}, modifiedcommand=self.save) ToolTip.ToolTip(self.edtLon, "Longitude as Float (10.0)") self.edtLon.grid(row=0, column=1) def clear(self): self.edtLat.clear() self.edtLon.clear() def get(self): ret= str(self.edtLat.getvalue())+";"+str(self.edtLon.getvalue()) if ret==";": ret = "" return ret def set(self, val): parts = val.split(";") if len(parts)<2: parts = ["",""] self.edtLat.setvalue(parts[0]) self.edtLon.setvalue(parts[1]) class URLEdit(Pmw.Group): def __init__(self, master): Pmw.Group.__init__(self, master, tag_text = "URL") master = self.interior() self.edtURL = TextEdit(master) self.edtURL.add_save_hook(self.updatestate) self.btnGotoURL = Button(master, command=self.gotoURL, image=IconImages.IconImages["webbrowser"], state=DISABLED) self.btnGotoURL.bind("", self.updatestate) master.columnconfigure(0, weight=1) master.rowconfigure(0, weight=1) self.edtURL.grid(sticky=W+E+S+N, padx=PADX, pady=PADY) self.btnGotoURL.grid(row=0, column=1, padx=PADX, pady=PADY) def gotoURL(self): import webbrowser webbrowser.open(self.edtURL.get(), 1) def updatestate(self, event=None): if self.edtURL.get(): self.btnGotoURL["state"] = NORMAL else: self.btnGotoURL["state"] = DISABLED def bindto(self, var): self.edtURL.bindto(var) self.updatestate() class AttendeeEdit(MultiRecordEdit): from vcalendar import vC_attendee def __init__(self, master): MultiRecordEdit.__init__(self, master, self.vC_attendee, "Attendees", "Attendee") defaultlabel = "*Click here to add current contact as attendee*" def createBody(self): master = self.body self.__lblContact = Label(master, cursor='hand2', text=self.defaultlabel) self.__lblContact.grid(column=0,row=1,sticky=W+E) self.__lblContact.bind('<1>', self.onMouseClick) def onRecordAdd(self, rec): contact = broker.Request('Current Contact') # Assign Attendee's Values from vCard: rec.assignFromCard(contact) if not contact.uid.is_empty(): uid = "[%s]" % (contact.uid.get()) else: uid = '' self.__lblContact["text"] = "%s <%s> %s" % (contact.fn.get(), rec.get(), uid) _broadcast_journal_modify() def onRecordDel(self): if self.state == DISABLED: self.bindtorec(None) _broadcast_journal_modify() def onMouseClick(self, event=None): if self.state == DISABLED: self._AddRecord() elif self._attendee_uid or self._attendee_fn: broadcaster.Broadcast('Journal', 'Attendee Clicked', data={'uid':self._attendee_uid, 'fn':self._attendee_fn}) _attendee_uid = None _attendee_fn = None def bindtorec(self, rec): if rec is None: self._attendee_uid = None self.__lblContact["text"] = self.defaultlabel else: fn = rec.params.get('cn') if fn is None: fn = "" else: fn = fn[0] uid = rec.getUID() if not uid: uidstr = "" else: uidstr = "[%s]" % uid self._attendee_uid = uid self._attendee_fn = fn self.__lblContact["text"] = "%s <%s> %s" % (fn, rec.get(), uidstr) class JournalEditWidget(AbstractJournalView, Frame): def __init__(self, master, **kws): AbstractJournalView.__init__(self, **kws) Frame.__init__(self, master, class_="JournalEdit") self.__createWidgets() # Catch every Mouse-Click: self.bind_all('<1>', self.__EditControlSave) self.bind_all('<2>', self.__EditControlSave) self.bind_all('<3>', self.__EditControlSave) def __EditControlSave(self, event=None): if currentEditControl: try: currentEditControl.save() except: pass def bind_journal(self, journal): global affectedJournal if journal: affectedJournal = journal.handle() else: affectedJournal = None AbstractJournalView.bind_journal(self, journal) self.rebindWidgets() def rebindWidgets(self): self.edtDateStart.bindto(DateWrapperVar(self._journal.dtstart)) self.edtTimeStart.bindto(TimeWrapperVar(self._journal.dtstart)) self.edtTimeEnd.bindto(TimeWrapperVar(self._journal.dtend)) self.edtSummary.bindto(self._journal.summary) self.edtCategories.bindto(self._journal.categories) self.edtLocation.bindto(self._journal.location) self.edtDescr.bindto(self._journal.description) self.edtAttendee.bindto(self._journal.attendee) self.lblRev["text"] = "Created: %s Last-Modification: %s" % (self._journal.created.get(), self._journal.last_mod.get()) def _onTimeEndSave(self): if self._journal.dtend.getTime(): # If End-Time is set: # Set End-Date to Start-Date: self._journal.dtend.setDate(self._journal.dtstart.getDate()) else: self._journal.dtend.set(None) def __createWidgets(self): self.columnconfigure(1, weight=1) self.rowconfigure(1, weight=1) # Row 0: self.edtDateStart = DateEdit(self) ToolTip.ToolTip(self.edtDateStart, 'Date') self.edtDateStart.grid(padx=PADX, pady=PADY) self.edtSummary = TextEdit(self) ToolTip.ToolTip(self.edtSummary, 'Summary') self.edtSummary.grid(row=0, column=1, sticky=W+E, padx=PADX, pady=PADY) # Row 1: self.edtCategories = TextComboEdit(self, entry_width=10) ToolTip.ToolTip(self.edtCategories, 'Categories (separate with \',\')') self.edtCategories.grid(row=1, column=0, sticky=W+E, padx=PADX, pady=PADY) self.edtCategories.setlist(['Appointment','Birthday','Business', 'Charge','Education','Holiday','Invoice','Meeting', 'Miscellaneous','Personal', 'Phone Call','Special Occasion','Travel','Vacation']) self.edtDescr = MemoEdit(self) ToolTip.ToolTip(self.edtDescr, 'Description') # MemoEdit is Pmw.ScrolledText: self.edtDescr["text_height"] = 2 self.edtDescr.grid(row=1, rowspan=2, column=1, sticky=W+E+S+N, padx=PADX, pady=PADY) # Row 2: self.edtLocation = TextEdit(self, entry_width=10) ToolTip.ToolTip(self.edtLocation, 'Location') self.edtLocation.grid(row=2, column=0, sticky=W+E, padx=PADX, pady=PADY) # Row 3: timeframe = Pmw.Group(self, tag_text='Start/End Time') self.edtTimeStart = TimeEdit(timeframe.interior(), increment=900) # increment by 15 min ToolTip.ToolTip(self.edtTimeStart, 'Time Start') self.edtTimeStart.grid(row=0, column=0, padx=PADX, pady=PADY) self.edtTimeEnd = TimeEdit(timeframe.interior(), increment=900) # increment by 15 min self.edtTimeEnd.add_save_hook(self._onTimeEndSave) ToolTip.ToolTip(self.edtTimeEnd, 'Time End') self.edtTimeEnd.grid(row=1, column=0, padx=PADX, pady=PADY) timeframe.grid(row=3, column=0, sticky=W+E+S+N, padx=PADX, pady=PADY) self.edtAttendee = AttendeeEdit(self) self.edtAttendee.grid(row=3, column=1, sticky=W+E+S+N, padx=PADX, pady=PADY) # Row 4: self.lblRev = Label(self, anchor=W) self.lblRev.grid(row=4, column=0, columnspan=2, sticky=W+E, padx=PADX, pady=PADY) def focus_set(self): self.edtSummary.selection_range(0,END) self.edtSummary.focus_set() if __name__ == "__main__": tk = Tk() wdgt = JournalEditWidget(tk) wdgt.pack(fill=BOTH) tk.mainloop() PyCoCuMa-0.4.5-6/pycocumalib/JournalListWidget.py0000644000175000017500000001402710252657644022713 0ustar henninghenning00000000000000""" Journal List """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: JournalListWidget.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import Pmw import InputWidgets import ToolTip import string import Set import broadcaster from MultiColumnTextList import MultiColumnTextList class JournalListWidget(Frame): def __init__(self, master, model, selcommand): Frame.__init__(self, master) self.model = model self.selcommand = selcommand self.textlist = MultiColumnTextList(self, selectcommand=self._textlistClick, autocolresize=1, text_height=10) self.textlist.pack(fill=BOTH,expand=TRUE) self.textlist.setcolwidthsfromstr(["0000-00-00", "Appointment", "Summary"]) self.registerAtBroadcaster() def textwidget(self): return self.textlist.text def registerAtBroadcaster(self): "Register our Widget's Callback Handlers" broadcaster.Register(self.updateList, source='Journals', title='Opened') broadcaster.Register(self.updateList, source='Journals', title='Closed') # Import Hook not needed because New and Save will do the job: #broadcaster.Register(self.updateList, # source='Journals', title='Imported') broadcaster.Register(self.onJournalSave, source='Journal', title='Saved') broadcaster.Register(self.onJournalNew, source='Journal', title='Added') broadcaster.Register(self.onJournalDel, source='Journal', title='Deleted') def getUpdatedJournalData(self): """Returns handle, AttendeeIds of the modified journal Call only from a broadcaster callback function""" handle = broadcaster.CurrentData()['handle'] if broadcaster.CurrentTitle() != "Deleted": attr = self.model.QueryJournalAttributes([handle], 'AttendeeIds') return (handle, attr[0]) else: return (handle, None) def onJournalNew(self): handle, attr = self.getUpdatedJournalData() self._journalhandles.append(handle) self._journalattendees.append(attr) #self.textlist.append(attr) idx = len(self._journalhandles)-1 srvidx = self.model.ListJournalHandles().index(handle) if idx != srvidx: def move(list, x, y): temp = list[x]; del list[x] list.insert(y, temp) # and move our new journal to it's sorted place: move(self._journalhandles, idx, srvidx) move(self._journalattendees, idx, srvidx) #self.textlist.remove(idx) #self.textlist.insert(srvidx, attr) self.applyFilter() def onJournalDel(self): handle, attr = self.getUpdatedJournalData() idx = self._journalhandles.index(handle) del self._journalhandles[idx] del self._journalattendees[idx] try: idx = self._filteredhandles.index(handle) del self._filteredhandles[idx] except: self._updateTextList() else: self.textlist.remove(idx) def onJournalSave(self): handle, attr = self.getUpdatedJournalData() idx = self._journalhandles.index(handle) self._journalattendees[idx] = attr # Now get the sorted list from the server: srvidx = self.model.ListJournalHandles().index(handle) if idx != srvidx: def move(list, x, y): temp = list[x]; del list[x] list.insert(y, temp) # and move our saved journal to it's sorted place: move(self._journalhandles, idx, srvidx) move(self._journalattendees, idx, srvidx) self.applyFilter() #self.textlist.remove(idx) #self.textlist.insert(srvidx, attr) self.selectJournal(handle) def _textlistClick(self): "Event triggered by clicking on the listbox" sel = self.textlist.selected() if sel is not None: self.selcommand(self._filteredhandles[sel]) currentFilter = 'All' def applyFilter(self, filter=None): "If called with no arguments the filter will be unchanged" if filter is None: filter = self.currentFilter else: self.currentFilter = filter if filter == "All": self._filteredhandles[:] = self._journalhandles else: self._filteredhandles = [] for str, handle in zip(self._journalattendees, self._journalhandles): if str.find(filter) != -1: self._filteredhandles.append(handle) self._updateTextList() def _updateTextList(self): rows = self.model.QueryJournalAttributes(self._filteredhandles, ('DateStart','Categories','Summary')) self.textlist.clear() for row in rows: self.textlist.append(row) _journalhandles = [] _journalattendees = [] _filteredhandles = [] def updateList(self): "Completely update our list" self._journalhandles[:] = self._filteredhandles[:] = self.model.ListJournalHandles() self._journalattendees = self.model.QueryJournalAttributes(self._journalhandles, 'AttendeeIds') self._updateTextList() def listFilteredHandles(self): return self._filteredhandles def selectJournal(self, handle): "Set the listbox selection" try: idx = self._filteredhandles.index(handle) self.textlist.select(idx) except ValueError: # The Journal does not appear in our filtered list! # => We can't select it => Do nothing return self.textlist.see(idx) PyCoCuMa-0.4.5-6/pycocumalib/JournalWindow.py0000644000175000017500000003374410252657644022112 0ustar henninghenning00000000000000""" Journal Toplevel Window """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: JournalWindow.py 82 2004-07-11 13:01:44Z henning $ import sys import os import string from Tkinter import * import tkMessageBox import debug import broadcaster import broker import time class JournalWindow: from JournalListWidget import JournalListWidget from JournalEditWidget import JournalEditWidget def __init__(self, model, tkroot=None): if not tkroot: tkroot = Tk() tkroot.withdraw() self.tkroot = tkroot self.model = model self.createWidgets() self.centerWindow() self.registerAtBroadcaster() self.lstJournal.updateList() self.onJournalsOpen() def registerAtBroadcaster(self): "Register our Callback Handlers" # Show Message Box on Notification Broadcast: broadcaster.Register(self.onJournalsOpen, source='Journals', title='Opened') broadcaster.Register(self.onJournalsClose, source='Journals', title='Closed') broadcaster.Register(self.onJournalModify, source='Journal', title='Modified') # The CalendarWindow broadcasts the following # to make us open the specified Day: broadcaster.Register(self.onCalendarDateSelect, source='Calendar', title='Date Selected') # broadcasted by MainView: broadcaster.Register(self.onContactOpen, source='Contact', title='Opened') def createWidgets(self): "create the top level window" top = self.top = Toplevel(self.tkroot, class_='JournalWindow') top.protocol('WM_DELETE_WINDOW', self.close) top.title('Journal') top.iconname('PyCoCuMa') try: os.chdir(os.path.dirname(sys.argv[0])) if sys.platform == "win32": top.iconbitmap("pycocuma.ico") else: top.iconbitmap("@pycocuma.xbm") top.iconmask("@pycocuma_mask.xbm") except: debug.echo("Could not set TopLevel window icon") top.bind('', self.saveJournal) top.rowconfigure(0, weight=3) top.rowconfigure(1, weight=1) top.columnconfigure(0, weight=1) top.withdraw() import ToolTip import IconImages from InputWidgets import ArrowButton IconImages.createIconImages() self.lstJournal = self.JournalListWidget(top, self.model, self.openJournal) self.lstJournal.grid(row=0, columnspan=2, sticky=W+E+S+N) #self.lstJournal.textwidget().bind("", self.popup_menu, add="+") self.journaledit = self.JournalEditWidget(top) self.journaledit.grid(row=1, rowspan=2, column=0, sticky=W+E+S+N) self.btnbar = btnbar = Frame(top) self.btnNewJournal = Button(btnbar, image=IconImages.IconImages["newjournal"], command=self.newJournal) ToolTip.ToolTip(self.btnNewJournal, "Add New Journal Entry") self.btnNewJournal.pack() self.btnDelJournal = Button(btnbar, image=IconImages.IconImages["deljournal"], command=self.delJournal, state=DISABLED) ToolTip.ToolTip(self.btnDelJournal, "Delete this Journal Entry") self.btnDelJournal.pack() self.btnSaveJournal = Button(btnbar, image=IconImages.IconImages["savejournal"], command=self.saveJournal) ToolTip.ToolTip(self.btnSaveJournal, "Save this Journal Entry to server") self.btnSaveJournal.pack() self.btnFilterJournal = Button(btnbar, image=IconImages.IconImages["conntocard"], command=self.toggleConnectedToContact) ToolTip.ToolTip(self.btnFilterJournal, "Connect View to current Contact\n(show only journal entries with current contact as attendee)") self.btnFilterJournal.pack() btnbar.grid(row=1, column=1, sticky=N) self.btnHideJournalEdit = ArrowButton(top, direction='up', command=self.hideJournalEdit, width=24, height=10) self.btnHideJournalEdit.grid(row=2, column=1, sticky=W+E+S) ToolTip.ToolTip(self.btnHideJournalEdit, "hide/show Journal Edit") popup = None def popup_menu(self, event=None): if not self.popup: self.popup = Toplevel(self.top) self.popup.withdraw() self.popup.wm_overrideredirect(1) self.popup.bind("", self.popup_close) btn = Button(self.popup, text="Add New Journal Entry", command=self.newJournal) btn.pack() x = event.x_root y = event.y_root self.popup.geometry("+%d+%d" % (x, y)) self.popup.deiconify() def popup_close(self, event=None): self.popup.withdraw() def centerWindow(self, relx=0.5, rely=0.3): "Center the Main Window on Screen" widget = self.top master = self.tkroot widget.update_idletasks() # Actualize geometry information if master.winfo_ismapped(): m_width = master.winfo_width() m_height = master.winfo_height() m_x = master.winfo_rootx() m_y = master.winfo_rooty() else: m_width = master.winfo_screenwidth() m_height = master.winfo_screenheight() m_x = m_y = 0 w_width = widget.winfo_reqwidth() w_height = widget.winfo_reqheight() x = m_x + (m_width - w_width) * relx y = m_y + (m_height - w_height) * rely if x+w_width > master.winfo_screenwidth(): x = master.winfo_screenwidth() - w_width elif x < 0: x = 0 if y+w_height > master.winfo_screenheight(): y = master.winfo_screenheight() - w_height elif y < 0: y = 0 widget.geometry("+%d+%d" % (x, y)) widget.deiconify() # Become visible at the desired location def newJournal(self, initdict=None): "Add new (empty) journal" if initdict is None: initdict = {} initdict.setdefault('dtstart', time.strftime("%Y-%m-%d", time.gmtime())) initdict.setdefault('summary', '(no summary)') newhandle = self.model.NewJournal(initdict) if self._connectedToContact: # Add current Contact as Attendee: contact = broker.Request('Current Contact') import vcalendar event = vcalendar.vEvent() for key, val in zip(initdict.keys(), initdict.values()): getattr(event, key).set(val) event.attendee.append(vcalendar.vC_attendee()) event.attendee[0].assignFromCard(contact) self.model.PutJournal(newhandle, event.VCF_repr()) self.editJournal(newhandle) def askDelJournal(self, date, summary): "Display Dialog asking if user really wants to delete the journal entry" m = tkMessageBox.Message( title="Confirm Delete", message="Do You really want to delete the journal entry '%s'?" % (summary), icon=tkMessageBox.QUESTION, type=tkMessageBox.YESNO, master=self.top) return m.show() def delJournal(self): "Delete journal currently editing" date = self.journaledit.boundto().dtstart.get() summary = self.journaledit.boundto().summary.get() answer = self.askDelJournal(date, summary) # Sometimes (esp. after Import) the answer is True # instead of 'yes': Why??? if answer == 'yes' or answer == True: handle = self.journaledit.cardhandle() self.model.DelJournal(handle) self.journal_modified = 0 self.openJournal() def askSaveJournal(self): "Display Dialog asking if user wants to save changes" m = tkMessageBox.Message( title="Save Changes?", message="This Journal has been modified.\nSave the Changes?", icon=tkMessageBox.QUESTION, type=tkMessageBox.YESNOCANCEL, master=self.top) return m.show() def saveJournal(self, event=None): "Upload the modified Journal to the Server" # To get the latest changes from all textedit widgets: self.journaledit.rebindWidgets() self.btnSaveJournal["state"] = DISABLED handle = self.journaledit.cardhandle() if handle is None: handle = self.model.NewJournal() jour = self.journaledit.boundto() if jour.summary.is_empty(): jour.summary.set('(no summary)') self.model.PutJournal(handle, jour.VCF_repr()) self.journal_modified = 0 _connectedToContact = False def toggleConnectedToContact(self): if self._connectedToContact: self._connectedToContact = False self.btnFilterJournal["relief"] = RAISED self.lstJournal.applyFilter("All") self.openJournal() else: contact = broker.Request('Current Contact') uid = contact.uid self.lstJournal.applyFilter(uid.get()) self._connectedToContact = True self.btnFilterJournal["relief"] = SUNKEN self.openJournal() if uid.is_empty(): broadcaster.Broadcast('Notification', 'Warning', data={'message':"The current contact '%s' has no UID. " % contact.fn.get()+ "You must set the UID before you can connect the Journal to an contact."}) def openJournal(self, handle=None): "Open journal by handle or default (first)" if self.journal_modified: # Sometimes (esp. after Import) the answer is True # instead of 'yes': Why??? answer = self.askSaveJournal() if answer == 'yes' or answer == True: self.saveJournal() if handle is None: handles = self.lstJournal.listFilteredHandles() if handles: journal = self.model.GetJournal(handles[-1]) else: journal = None else: journal = self.model.GetJournal(handle) self.journaledit.bind_journal(journal) if journal: # Inform other widgets of newly opened journal: self.lstJournal.selectJournal(journal.handle()) self.btnDelJournal["state"]=NORMAL # This is esp. for the CalendarWindow: broadcaster.Broadcast('Journal', 'Opened', data={'dtstart':journal.dtstart.get()}) else: self.btnDelJournal["state"]=DISABLED self.btnSaveJournal["state"]=DISABLED self.journal_modified = 0 def viewJournal(self, handle=None): "Open the 'View Journal' Tab" self.openJournal(handle) def editJournal(self, handle=None): "Open the 'Edit Journal' Tab" self.openJournal(handle) self.journaledit.focus_set() def onJournalsOpen(self): "Callback, triggered on Broadcast" self.btnNewJournal["state"] = NORMAL self.btnDelJournal["state"] = NORMAL self.btnSaveJournal["state"] = DISABLED self.openJournal() def onJournalsClose(self): "Callback, triggered on Broadcast" self.btnNewJournal["state"] = DISABLED self.btnDelJournal["state"] = DISABLED self.btnSaveJournal["state"] = DISABLED journal_modified = 0 def onJournalModify(self): "File was modified since last save" self.journal_modified = 1 self.btnSaveJournal["state"] = NORMAL def onCalendarDateSelect(self): "A Day was selected in the CalendarWindow" date = broadcaster.CurrentData()['date'] createnew = broadcaster.CurrentData().get('createnew') if createnew: self.newJournal(initdict={'dtstart':date}) else: handles = self.model.ListJournalHandles() # Calendar delivers date as YYYY-MM-DD, but dtstart may # include time, this means we cut off after 10th char: dates = map(lambda x: x[:10], self.model.QueryJournalAttributes(handles, 'DateStart')) try: idx = dates.index(date) except ValueError: return self.openJournal(handles[idx]) def onContactOpen(self): if self._connectedToContact: contact = broker.Request('Current Contact') self.lstJournal.applyFilter(contact.uid.get()) self.openJournal() _journaledit_hidden = 0 def hideJournalEdit(self): if self._journaledit_hidden: self.journaledit.grid() self.btnbar.grid() self._journaledit_hidden = 0 self.btnHideJournalEdit.setdirection('up') else: self.journaledit.grid_remove() self.btnbar.grid_remove() self._journaledit_hidden = 1 self.btnHideJournalEdit.setdirection('down') def close(self, event=None): reply = 'none' if self.journal_modified: reply = self.askSaveJournal() if reply == 'yes' or reply == True: self.saveJournal() # We need str() here, because reply is a tcl_Obj: if str(reply) != 'cancel': self._close() def _close(self): self.top.withdraw() def window(self): "Returns Tk's TopLevel Widget" return self.top def withdraw(self): "Withdraw: Forward to TopLevel Method" self.top.withdraw() def deiconify(self): "DeIconify: Forward to TopLevel Method" self.top.deiconify() def show(self): self.top.deiconify() self.top.lift() self.top.focus_set() PyCoCuMa-0.4.5-6/pycocumalib/LetterComposer.py0000644000175000017500000003440210252657644022247 0ustar henninghenning00000000000000""" Letter Composer for TeX-Letters """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: LetterComposer.py 92 2004-11-28 15:34:44Z henning $ from Tkinter import * import Pmw import ToolTip import os, sys, types from InputWidgets import TextEdit, TextComboEdit, FilenameEdit, MemoEdit import Preferences import ConfigParser import __version__ PADX = PADY = 2 class Letter: "Encapsulates Letter-Data" def __init__(self): self._metasect = "MetaInfo" self._datasect = "LetterData" self._dict = { 'MySignature': "", 'MyPlace': "", 'MyAddress': "", 'MyBackAddress': "", 'MyBottomText': "", 'Date': "", 'Address': "", 'YourMail': "", 'Subject': "", 'Opening': "", 'Body': "", 'Closing': "", 'Enclosing': "" } def getDict(self): return self._dict def setDict(self, _dict): self._dict = _dict def saveToFile(self, filename): cp = ConfigParser.SafeConfigParser() # Make OptionNames case-sensitive: cp.optionxform = str # Add Meta-Info: sec = self._metasect if not cp.has_section(sec): cp.add_section(sec) import time cp.set(sec, "Creator", "PyCoCuMa-%s" % __version__.__version__) cp.set(sec, "CreationDate", time.strftime('%Y%m%dT%H%M%S')) # Add Data section: sec = self._datasect if not cp.has_section(sec): cp.add_section(sec) for key, val in zip(self._dict.keys(), self._dict.values()): cp.set(sec, key, val.encode('utf-8', 'replace')) cp.write(open(filename, 'wb')) def loadFromFile(self, filename): cp = ConfigParser.SafeConfigParser() # Make OptionNames case-sensitive: cp.optionxform = str cp.read([filename]) for opt in cp.options(self._datasect): self._dict[opt] = unicode(cp.get(self._datasect, opt), 'utf-8', 'replace') class LetterComposer(Pmw.MegaToplevel): def __init__(self, master): Pmw.MegaToplevel.__init__(self, master=master, title='Compose Letter') self.userdeletefunc(self.withdraw) self.master = master self.letter = Letter() self.createWidgets() self.fillDefaultValues() self.withdraw() def fillDefaultValues(self): "Get initial values from Preferences" myaddress = Preferences.get('lettercomposer.myaddress') self.edtMyAddress.clear() if myaddress: self.edtMyAddress.set(myaddress) templatefname = Preferences.get('lettercomposer.template_filename') if not templatefname: templatefname = os.path.dirname(sys.argv[0])+'/templates/DinBrief.tex' self.edtTemplateFilename.set(templatefname) self.edtSubject.clear() opening = Preferences.get('lettercomposer.opening', types.ListType) if opening is not None: self.edtOpening.setlist(opening) if len(opening)>0: self.edtOpening.set(opening[0]) self.edtBody.clear() closing = Preferences.get('lettercomposer.closing', types.ListType) if closing is not None: self.edtClosing.setlist(closing) if len(closing)>0: self.edtClosing.set(closing[0]) # Now fill the combobox lists: import time self.edtDate.setlist([ time.strftime('%d. %B %Y'), time.strftime('%x'), time.strftime('%Y-%m-%d')]) def createWidgets(self): hull = self.component('hull') hull.rowconfigure(0, weight=1) hull.columnconfigure(0, weight=1) top = Frame(hull, borderwidth=1, relief=RAISED) top.grid(sticky=W+E+S+N) sidepanel = Frame(hull, borderwidth=1, relief=RAISED) sidepanel.grid(row=0, column=1, sticky=N+S) self.edtTemplateFilename = FilenameEdit(sidepanel, showbasenameonly=True, filetypes=[('LaTeX Templates','*.tex'),('All Files','*')]) self.edtTemplateFilename.grid(sticky=W+E, padx=PADX, pady=PADY) self.edtTeXFilename = FilenameEdit(sidepanel, type='saveas', filetypes=[('LaTeX Files','*.tex'),('All Files','*')]) ToolTip.ToolTip(self.edtTeXFilename, 'TeX Filename to save to') self.edtTeXFilename.grid(sticky=W+E, padx=PADX, pady=PADY) btn = Button(sidepanel, text='Save Letter', command=self.saveLetterToFile) btn.grid(sticky=W+E, padx=PADX, pady=PADY) btn = Button(sidepanel, text='Load Letter', command=self.loadLetterFromFile) btn.grid(sticky=W+E, padx=PADX, pady=PADY) btn = Button(sidepanel, text='Save TeX', command=self.saveTeXToFile) btn.grid(sticky=W+E, padx=PADX, pady=PADY) btn = Button(sidepanel, text='Preview PDF', command=self.previewPdfFile) btn.grid(sticky=W+E, padx=PADX, pady=PADY) btn = Button(sidepanel, text='Close', command=self.close) btn.grid(sticky=W+E, padx=PADX, pady=PADY) self.use_signature = IntVar() self.use_signature.set(1) chk = Checkbutton(sidepanel, text='Use Signature', variable=self.use_signature) chk.grid(sticky=W+E, padx=PADX, pady=PADY) top.columnconfigure(0, weight=1) # Expand Body: top.rowconfigure(4, weight=1) self.edtAddress = MemoEdit(top, labelpos=N, label_text='Address to:') ToolTip.ToolTip(self.edtAddress, "recipient's address: exactly four lines are used") self.edtAddress.grid(sticky=W+E+S+N, padx=PADX, pady=PADY) self.edtMyAddress = MemoEdit(top, labelpos=N, label_text='My Address:') ToolTip.ToolTip(self.edtMyAddress, "sender's address: first line is also signature\n"+ 'first three lines are used as backaddress\n'+ 'you can specify as many lines as you need (e.g. for phone, email)') self.edtMyAddress.set(' ') self.edtMyAddress.tag_add('default', '0.0', END) self.edtMyAddress.tag_config('default', justify=RIGHT) self.edtMyAddress.grid(row=0, column=1, sticky=W+E+S+N, padx=PADX, pady=PADY) self.edtYourMail = TextEdit(top, labelpos=W, label_text='Your Mail:') ToolTip.ToolTip(self.edtYourMail, "refer to recipient's mail (usually by date)") self.edtYourMail.grid(sticky=W+E, padx=PADX, pady=PADY) self.edtDate = TextComboEdit(top, labelpos=W, label_text='Date:') ToolTip.ToolTip(self.edtDate, "leave empty to use today's date") self.edtDate.grid(row=1, column=1, sticky=W+E, padx=PADX, pady=PADY) self.edtSubject = TextEdit(top, labelpos=W, label_text='Subject:') self.edtSubject.grid(columnspan=2, sticky=W+E, padx=PADX, pady=PADY) self.edtOpening = TextComboEdit(top, labelpos=W, label_text='Opening:') self.edtOpening.grid(columnspan=2, sticky=W+E, padx=PADX, pady=PADY) self.edtBody = MemoEdit(top, text_height=8, file_extension='.tex') ToolTip.ToolTip(self.edtBody, 'text body: you can use LaTeX commands here\n' + '(Press CTRL-E to open external editor)') self.edtBody.grid(columnspan=2, sticky=W+E+S+N, padx=PADX, pady=PADY) self.edtClosing = TextComboEdit(top, labelpos=W, label_text='Closing:') self.edtClosing.grid(columnspan=2, sticky=W+E, padx=PADX, pady=PADY) self.edtEnclosing = TextEdit(top, labelpos=W, label_text='Enclosing:') ToolTip.ToolTip(self.edtEnclosing, 'list of attachments, separate by LaTeX linebreak (\\\\)') self.edtEnclosing.grid(columnspan=2, sticky=W+E, padx=PADX, pady=PADY) def createLetterFromWidgets(self): address_lines = self.edtAddress.get().splitlines() # We want exactly four lines: for i in range(4-len(address_lines)): address_lines.append('') myaddress_lines = self.edtMyAddress.get().splitlines() if self.use_signature.get() and len(myaddress_lines)>0: signature = myaddress_lines[0] else: signature = '' letterdict = { 'MySignature':signature, 'MyPlace':'', 'MyAddress':"\n".join(myaddress_lines), 'MyBackAddress':"\n".join(myaddress_lines[:3]), 'MyBottomText':'', 'Date':self.edtDate.get(), 'Address':"\n".join(address_lines), 'YourMail':self.edtYourMail.get(), 'Subject':self.edtSubject.get(), 'Opening':self.edtOpening.get(), 'Body':self.edtBody.get(), 'Closing':self.edtClosing.get(), 'Enclosing':self.edtEnclosing.get() } self.letter.setDict(letterdict) def saveLetterToFile(self): self.createLetterFromWidgets() import tkFileDialog dlg = tkFileDialog.SaveAs(self.master, filetypes=[("PyCoCuMa Letter Files", "*.letter")]) letter_dir = Preferences.get('lettercomposer.letter_dir') letter_fname, file_ext = os.path.splitext(os.path.basename(self.edtTeXFilename.get())) letter_fname += '.letter' fname = dlg.show(initialdir=letter_dir, initialfile=letter_fname) if fname: self.letter.saveToFile(fname) def loadLetterFromFile(self): import tkFileDialog dlg = tkFileDialog.Open(self.master, filetypes=[("PyCoCuMa Letter Files", "*.letter"), ("All Files", "*")]) letter_dir = Preferences.get('lettercomposer.letter_dir') fname = dlg.show(initialdir=letter_dir) if fname: self.letter.loadFromFile(fname) d = self.letter.getDict() self.edtMyAddress.set(d["MyAddress"]) self.edtDate.set(d["Date"]) self.edtAddress.set(d["Address"]) self.edtYourMail.set(d["YourMail"]) self.edtSubject.set(d["Subject"]) self.edtOpening.set(d["Opening"]) self.edtBody.set(d["Body"]) self.edtClosing.set(d["Closing"]) self.edtEnclosing.set(d["Enclosing"]) def saveTeXToFile(self): self.createLetterFromWidgets() def enclatin1(str): return str.encode('latin-1', 'replace') from TemplateProcessor import TemplateProcessor proc = TemplateProcessor(postproc=enclatin1) outfile = file(self.edtTeXFilename.get(), 'wb') d = self.letter.getDict().copy() d["MyAddress"] = d["MyAddress"].splitlines() d["MyBackAddress"] = d["MyBackAddress"].splitlines() d["Address"] = d["Address"].splitlines() proc.process(file(self.edtTemplateFilename.get()).readlines(), outfile.write, d) outfile.close() self.savePreferences() def savePreferences(self): Preferences.set('lettercomposer.myaddress', self.edtMyAddress.get()) opening = self.edtOpening.get() openings = list(self.edtOpening.getlist()) if opening and not opening in openings: openings.insert(0, opening) Preferences.set('lettercomposer.opening', openings[:10]) closing = self.edtClosing.get() closings = list(self.edtClosing.getlist()) if closing and not closing in closings: closings.insert(0, closing) Preferences.set('lettercomposer.closing', closings[:10]) Preferences.set('lettercomposer.template_filename', self.edtTemplateFilename.get()) Preferences.set('lettercomposer.letter_dir', os.path.dirname(self.edtTeXFilename.get())) def previewPdfFile(self): import texwrapper texfilename = self.edtTeXFilename.get() if not os.access(texfilename, os.F_OK): import tkMessageBox tkMessageBox.showerror("Error", "File not found: '%s'\n You must save the TeX-file first!" % texfilename) return texwrapper.run_pdflatex(texfilename) texwrapper.view_pdf(texfilename) def close(self): self.withdraw() def composeTo(self, addressee, adr): self.fillDefaultValues() adrstreet = adr.street.get() adrcity = '%s %s' % (adr.postcode.get(), adr.city.get()) address = '\n%s\n%s\n%s' % (addressee.fn.get(), adrstreet, adrcity) self.edtAddress.clear() self.edtAddress.set(address) texfname = addressee.uid.get() if not texfname: texfname = addressee.fn.get().replace(' ', '_') texdir = Preferences.get('lettercomposer.letter_dir') if not texdir: texdir = os.path.dirname(sys.argv[0]) import time texfname = '%s/%s-%s.tex' % (texdir, texfname, time.strftime('%Y%m%d')) self.edtTeXFilename.set(texfname) self.show() _firstshow = 1 def show(self): if self._firstshow: self.centerWindow() else: self.deiconify() self.lift() self._firstshow = 0 def centerWindow(self, relx=0.5, rely=0.3): "Center the Window on Screen" widget = self master = self.master widget.update_idletasks() # Actualize geometry information if master.winfo_ismapped(): m_width = master.winfo_width() m_height = master.winfo_height() m_x = master.winfo_rootx() m_y = master.winfo_rooty() else: m_width = master.winfo_screenwidth() m_height = master.winfo_screenheight() m_x = m_y = 0 w_width = widget.winfo_reqwidth() w_height = widget.winfo_reqheight() x = m_x + (m_width - w_width) * relx y = m_y + (m_height - w_height) * rely if x+w_width > master.winfo_screenwidth(): x = master.winfo_screenwidth() - w_width elif x < 0: x = 0 if y+w_height > master.winfo_screenheight(): y = master.winfo_screenheight() - w_height elif y < 0: y = 0 widget.geometry("+%d+%d" % (x, y)) widget.deiconify() # Become visible at the desired location if __name__ == "__main__": import vcard import testvcard tk = Tk() dlg = LetterComposer(tk) card = vcard.vCard() testvcard.fillDemoCard(card) dlg.composeTo(card, card.adr[0]) tk.mainloop() PyCoCuMa-0.4.5-6/pycocumalib/ListViewWidget.py0000644000175000017500000001700710252657644022214 0ustar henninghenning00000000000000# Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: ListViewWidget.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import Preferences import broadcaster import debug import ToolTip import vcard import types DEFAULT_FIELDS_SHOWN = [ "FormattedName", "Organization", "Phone"] class ListViewWidget(Frame): fields_shown = DEFAULT_FIELDS_SHOWN sortby = "SortName" def __init__(self, master, model, **kws): self.handles = [] self.model = model self._dblclickcommand = kws.get("dblclickcommand", None) self._selectcommand = kws.get("selectcommand", None) Frame.__init__(self, master, class_="ListView") if Preferences.has_key('client.listview_columns'): self.fields_shown = Preferences.get('client.listview_columns', types.ListType) if Preferences.has_key('client.listview_sortby'): self.sortby = Preferences.get('client.listview_sortby') self.__createWidgets() self.registerAtBroadcaster() def registerAtBroadcaster(self): broadcaster.Register(self.updateList, source='Contacts', title='Opened') broadcaster.Register(self.updateList, source='Contacts', title='Closed') broadcaster.Register(self.onContactDel, source='Contact', title='Deleted') broadcaster.Register(self.onContactNew, source='Contact', title='Added') broadcaster.Register(self.onContactSave, source='Contact', title='Saved') def getUpdatedContactData(self): """Returns handle and fields_shown from the modified contact Call only from a broadcaster callback function""" handle = broadcaster.CurrentData()['handle'] if broadcaster.CurrentTitle() != "Deleted": attrlist = self.model.QueryAttributes([handle], self.fields_shown) return (handle, attrlist[0]) else: return (handle, None) def onContactNew(self): handle, attr = self.getUpdatedContactData() self.handles.append(handle) self.rows.append(attr) self.textlist.append(attr) idx = len(self.handles)-1 srvidx = self.model.ListHandles(self.sortby).index(handle) if idx != srvidx: def move(list, x, y): temp = list[x]; del list[x] list.insert(y, temp) # and move our new Contact to it's sorted place: move(self.handles, idx, srvidx) move(self.rows, idx, srvidx) self.textlist.remove(idx) self.textlist.insert(srvidx, attr) def onContactDel(self): handle, attr = self.getUpdatedContactData() idx = self.handles.index(handle) del self.handles[idx] del self.rows[idx] self.textlist.remove(idx) def onContactSave(self): handle, attr = self.getUpdatedContactData() idx = self.handles.index(handle) self.rows[idx] = attr # Now get the sorted list from the server: srvidx = self.model.ListHandles(self.sortby).index(handle) if idx != srvidx: def move(list, x, y): temp = list[x]; del list[x] list.insert(y, temp) # and move our saved contact to it's sorted place: move(self.handles, idx, srvidx) move(self.rows, idx, srvidx) self.textlist.remove(idx) self.textlist.insert(srvidx, attr) self.selectContact(handle) def __createWidgets(self): from MultiColumnTextList import MultiColumnTextList def dblclickcommand(self=self): idx = self.textlist.selected() self._dblclickcommand(self.handles[idx]) def selectcommand(self=self): idx = self.textlist.selected() if idx is not None: self._selectcommand(self.handles[idx]) self.rowconfigure(1, weight=1) self.columnconfigure(1, weight=1) self._btnSortby = Button(self, text="Sort by...", command=self.__Sortby) ToolTip.ToolTip(self._btnSortby, "select column to order by") self._btnSortby.grid(sticky=NW) self._btnConfigure = Button(self, text="Configure Columns...", command=self.__ConfigureColumns) ToolTip.ToolTip(self._btnConfigure, "select columns to show") self._btnConfigure.grid(row=0,column=1,sticky=NW) self.textlist = MultiColumnTextList(self, selectcommand=selectcommand, dblclickcommand=dblclickcommand, autocolresize=1, columnheader=1) self.textlist.setcolheader(self.fields_shown) self.textlist.setcolwidthsfromstr(self.fields_shown) self.textlist.grid(columnspan=2,sticky=W+S+E+N) def __tableKeyPress(self, event): key = event.keysym_num if (key > 65) and (key < 122): for fn in self.fns: if fn[0].upper() == chr(key).upper(): self.select_contact(fn) def __Sortby(self): import Pmw dialog = Pmw.SelectionDialog(self, title = 'Sort by', buttons = ('Ok', 'Cancel'), defaultbutton = 'Ok', scrolledlist_labelpos = 'n', label_text = 'Select Sort Column:', scrolledlist_items = vcard.FIELDNAMES) # We use str() here, because self.sortby could be unicode: dialog.setvalue(str(self.sortby)) dialog.see(vcard.FIELDNAMES.index(self.sortby)) def applysortby(result, dlg=dialog, self=self): sels = dlg.getcurselection() if (result == "Ok") and (len(sels) > 0): debug.echo("Sortby %s" % sels[0]) self.sortby = sels[0] Preferences.set("client.listview_sortby", self.sortby) self.updateList() dlg.deactivate(result) dialog["command"] = applysortby dialog.activate() def __ConfigureColumns(self): from StringSetDialog import StringSetDialog dlg = StringSetDialog(self, title="Configure Columns", available_strings = vcard.FIELDNAMES, selected_strings = self.fields_shown, allowmultiselect=1) def cfgcolumns(btn, self=self, dlg=dlg): if btn == 'Ok': self.fields_shown = dlg.getvalue() Preferences.set('client.listview_columns', dlg.getvalue()) self.textlist.setcolheader(dlg.getvalue()) self.textlist.setcolwidths([]) self.updateList() dlg.deactivate() dlg['command'] = cfgcolumns dlg.activate() def selectContact(self, contact): try: handle = contact.handle() except: handle = contact try: idx = self.handles.index(handle) self.textlist.select(idx) self.textlist.see(idx) except ValueError: pass rows = [] def updateList(self): self.handles = self.model.ListHandles(sortby=self.sortby) self.rows = self.model.QueryAttributes(self.handles, self.fields_shown) self.textlist.clear() for row in self.rows: self.textlist.append(row) PyCoCuMa-0.4.5-6/pycocumalib/MainController.py0000644000175000017500000002057210311640650022214 0ustar henninghenning00000000000000""" This is the Main Controller. It starts the server, shows splash screen and cleans up afterwards. MainController is imported and run by the 'pycocuma' executable script. """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: MainController.py 142 2005-09-13 21:16:23Z henning $ import Preferences import broker from __version__ import __version__ from MainModel import MainModel CMDLINE_HELP = """\ PyCoCuMa: Pythonic Contact and Customer Management version %s Usage: pycocuma [options] [connection-string] connection-string addressbook filename / server url -h or --help show this help -c or --config location of configuration file (default: ~/.pycocuma) --noconf do not save configuration / preferences -t or --type connection type: xmlrpc or file (default: file) --finder open QuickFinder --import import from file/stdin (-) (e.g. vcard:/tmp/John.vcf) --export export to file or stdout (e.g. latex:-) Examples: pycocuma --type=xmlrpc http://localhost:8810 pycocuma --config=/tmp/pycocumatest.conf --type=file ./addressbook.vcf pycocuma --import=csv:myaddresses.csv See homepage: http://www.srcco.de/pycocuma/""" % __version__ class MainController: def __init__(self, nogui=False, nouserpref=False): # Don't create any GUI-windows (e.g. MainView): self.nogui = nogui # Start QuickFinder only: self.startquickfinder = False # Do not load nor save preferences to ~/.pycocuma: self.nouserpref = nouserpref # import from this file self.importfile = None # export to this file self.exportfile = None optlist, args, preffile = self.parseCommandLineOptions() # Load Preferences: if not nouserpref: Preferences.Load(preffile) self.applyCommandLineOptions(optlist, args) # importing and exporting disables the GUI: if self.importfile or self.exportfile: self.nogui = True # Display Splash-Screen: self.splash = None if not self.nogui: from SplashScreen import SplashScreen # The Tk-Root will be created by SplashScreen: self.splash = SplashScreen() self.splash.show() import locale locale.setlocale(locale.LC_ALL, '') self.model = MainModel() if not self.nogui: tkroot = None if self.splash: tkroot = self.splash.tkroot if self.startquickfinder: from QuickFinder import QuickFinder self.view = view = QuickFinder(__version__, self.model, tkroot) else: from MainView import MainView self.view = view = MainView(__version__, self.model, tkroot) def parseCommandLineOptions(self): "Parse Command-Line Options" import getopt, sys try: optlist, args = getopt.getopt(sys.argv[1:], "c:ht:", ['help', 'config=', 'noconf', 'type=', 'finder', 'import=', 'export=']) except getopt.GetoptError, msg: print "ERROR: " + str(msg) self.showCommandLineHelp() preffile = None for key, val in optlist: if key == "-c" or key == "--config": preffile = val return (optlist, args, preffile) def applyCommandLineOptions(self, optlist, args): "Overwrite Preferences with command-line options" if len(args) >= 1: Preferences.set("client.connection_string", args[0]) Preferences.set("client.connection_type", 'file') for key, val in optlist: if key == "-h" or key == "--help": self.showCommandLineHelp() elif key == "--noconf": self.nouserpref = True elif key == "--finder": self.startquickfinder = True elif key == "--import": self.importfile = val elif key == "--export": self.exportfile = val # Command Line Arguments override Preferences: elif key == "-t" or key == "--type": Preferences.set("client.connection_type",val) def showCommandLineHelp(self): "Print Help Screen and Exit" import sys print CMDLINE_HELP sys.exit(0) def startServer(self): "start the XML-RPC Server" import os, sys # Where to look for the server executable: serverpaths = [ os.path.dirname(sys.argv[0])+'/pycocuma-server', 'pycocuma-server', 'CoCuMa_Server.py' ] for path in serverpaths: serverfilename = path if os.access(serverfilename, os.R_OK): break # Run our Server Daemon: os.spawnv(os.P_NOWAIT, sys.executable, [sys.executable, serverfilename, '-h', Preferences.get('server.listen_host'), '-p', Preferences.get('server.listen_port'), '-f', Preferences.get('server.addressbook_filename'), '-j', Preferences.get('server.calendar_filename') ]) def doImport(self, filename): "Import into model from file or stdin" import converters, sys, os parts = filename.split(':') if len(parts) > 1: # format specified (e.g. vcard or csv) srcformat = parts[0] filename = parts[1] else: # try to guess by file extension root, ext = os.path.splitext(filename) if ext == "csv": srcformat = ext else: srcformat = "vcard" if filename == "-": # use StdIn: fd = sys.stdin else: # read file: fd = file(filename, 'rb') try: converters.importvcards(self.model, srcformat, fd, 'utf-8') finally: fd.close() def doExport(self, filename): "Export from model to file or stdout" import converters, sys, os parts = filename.split(':') if len(parts) > 1: # format specified (e.g. vcard or csv) dstformat = parts[0] filename = parts[1] else: # try to guess by file extension root, ext = os.path.splitext(filename) if ext == "csv": dstformat = ext elif ext == "xml": dstformat = "xmlrdf" else: dstformat = "vcard" if filename == "-": # use StdOut: fd = sys.stdout else: # read file: fd = file(filename, 'wb') outfile = converters.EncodedFileWriter(fd, 'utf-8') try: converters.exportvcards(self.model, dstformat, outfile, 'utf-8') finally: fd.close() def Run(self): "Fire up the server and the client's main window" if Preferences.get("client.connection_type").lower() == "xmlrpc"\ and Preferences.get("client.autostart_server").lower() == "yes": self.startServer() if self.splash: self.splash.update("Connecting...") self.model.Open(Preferences.get('client.connection_type'), Preferences.get('client.connection_string')) if not self.nogui: if self.splash: self.splash.destroy() self.splash = None self.view.tkroot.mainloop() if self.importfile: self.doImport(self.importfile) if self.exportfile: self.doExport(self.exportfile) if self.model.isConnected(): Preferences.set('client.connection_type', broker.Request("Connection Type")) Preferences.set('client.connection_string', broker.Request("Connection String")) def CleanUp(self): "Close the connection to server and save user preferences" self.model.Close(final=1) if not self.nogui: self.view.tkroot.destroy() if not self.nouserpref: Preferences.Save() PyCoCuMa-0.4.5-6/pycocumalib/MainModel.py0000644000175000017500000001256610252657644021154 0ustar henninghenning00000000000000#!/usr/bin/python """ Middle-Layer between Frontend (MainView) and CoCuMa_Client. """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: MainModel.py 82 2004-07-11 13:01:44Z henning $ from __version__ import __version__ from vcard import vCard from vcalendar import vEvent #from Customers import Customers import broadcaster import broker import debug import CoCuMa_Client class MainModel: "This is the middle-layer link between the various Frontends and Backends" def __init__(self): # self.client is None when not connected: self.client = None self.connection_type = 'none' self.connection_string = '' broker.UnRegister('Connection Type') broker.UnRegister('Connection String') broker.Register('Connection Type', lambda self=self: self.connection_type) broker.Register('Connection String', lambda self=self: self.connection_string) def Open(self, con_type, con_str): "Connect to Backend Client" self.client = CoCuMa_Client.Instantiate(con_type) if self.client and self.client.Connect(con_str): self.connection_type = con_type self.connection_string = con_str broadcaster.Broadcast('Contacts', 'Opened') broadcaster.Broadcast('Journals', 'Opened') else: self.connection_type = 'none' self.connection_string = '' if self.client: errorstr = self.client.getErrorString() else: errorstr = "" self.client = None msgstr = "Connection to %s (%s) failed.\n%s" % (con_str, con_type, errorstr) debug.echo("MainModel.Open(): "+msgstr) broadcaster.Broadcast('Notification', 'Error', {'message':msgstr}) def Close(self, final=0): "Disconnect from Backend" self.connection_type = 'none' self.connection_string = '' if self.client: self.client.Disconnect() self.client = None if not final: # 'final' signals that the application is shutting down, # therefore we don't broadcast in this case broadcaster.Broadcast('Contacts', 'Closed') broadcaster.Broadcast('Journals', 'Closed') def isConnected(self): return self.client is not None #### The following Methods just call their Backend Equivalents: def GetContact(self, handle): if self.client: card = vCard(self.client.GetContact(handle)) card.sethandle(handle) return card else: return None def PutContact(self, handle, data): ret = None if self.client: ret = self.client.PutContact(handle, data) broadcaster.Broadcast('Contact', 'Saved', {'handle':handle}) return ret def NewContact(self, initdict={}): # initdict is a dictionary with initial values for the vcard # e.g. initdict={'fn':'New Untitled Card'} ret = None if self.client: ret = self.client.NewContact(initdict) broadcaster.Broadcast('Contact', 'Added', {'handle':ret}) return ret def DelContact(self, handle): ret = None if self.client: ret = self.client.DelContact(handle) broadcaster.Broadcast('Contact', 'Deleted', {'handle':handle}) return ret def ListHandles(self, sortby=""): if self.client: return self.client.ListHandles(sortby) else: return [] def QueryAttributes(self, handles, attributes): if self.client: return self.client.QueryAttributes(handles, attributes) else: return [] # Now the same with vEvent (we call it Journal here): def GetJournal(self, handle): if self.client: jour = vEvent(self.client.GetJournal(handle)) jour.sethandle(handle) return jour else: return None def PutJournal(self, handle, data): ret = None if self.client: ret = self.client.PutJournal(handle, data) broadcaster.Broadcast('Journal', 'Saved', data={'handle':handle}) return ret def NewJournal(self, initdict={}): # initdict is a dictionary with initial values for the vEvent # e.g. initdict={'summary':'Important Meeting'} ret = None if self.client: ret = self.client.NewJournal(initdict) datadict = {'handle':ret} datadict.update(initdict) broadcaster.Broadcast('Journal', 'Added', data=datadict) return ret def DelJournal(self, handle): ret = None if self.client: ret = self.client.DelJournal(handle) broadcaster.Broadcast('Journal', 'Deleted', {'handle':handle}) return ret def ListJournalHandles(self, sortby=""): if self.client: return self.client.ListJournalHandles(sortby) else: return [] def QueryJournalAttributes(self, handles, attributes): if self.client: return self.client.QueryJournalAttributes(handles, attributes) else: return [] PyCoCuMa-0.4.5-6/pycocumalib/MainView.py0000644000175000017500000007315510252657644021027 0ustar henninghenning00000000000000"""Main PyCoCuMa widget. This window provides the basic decorations, primarily including the menubar. It is used to bring up other windows. """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: MainView.py 92 2004-11-28 15:34:44Z henning $ import sys import os import string from Tkinter import * import tkMessageBox import Pmw import IconImages import debug import broadcaster import broker import types import Preferences class MainView: from ContactSelectboxWidget import ContactSelectboxWidget from ContactListWidget import ContactListWidget from ContactEditWidget import ContactEditWidget from ContactViewWidget import ContactViewWidget #from ContactJournalWidget import ContactJournalWidget from ListViewWidget import ListViewWidget from IOBinding import IOBinding import Query import Bindings def __init__(self, version, model, tkroot=None, slavewindow=False): self.isslavewindow = slavewindow if not tkroot: tkroot = Pmw.initialise() tkroot.withdraw() self.tkroot = tkroot self.__version = version self.model = model self.initOptionDatabase() self.createWidgets() self.centerWindow() self.createMenubar() self.applyBindings() self.registerAtBroadcaster() # Only Import and Export: self.io = self.IOBinding(self) # Test whether this is the first start-up with this version: if Preferences.get('client.version') != self.__version: # if true, show our fancy help window: self.showHelpWindow() Preferences.set('client.version', self.__version) def applyBindings(self, keydefs=None): "Apply Key-Bindings / Add, Connect to Events" if keydefs is None: keydefs = self.Bindings.default_keydefs self.keydefs = keydefs top = self.top for event, keylist in keydefs.items(): if keylist: top.event_add(event, *keylist) top.bind("<>", self.close) top.bind("<>", self.showPreferencesDialog) top.bind("<>", self.showAboutDialog) top.bind("<>", self.showHelpWindow) top.bind("<>", self.connect_to_server_event) top.bind("<>", self.disconnect_from_server_event) # Application-wide Bindings (default: F1-F5 Keys): top.bind_all("<>", self.view_list_event) top.bind_all("<>", self.view_card_event) top.bind_all("<>", self.edit_contact_event) top.bind_all("<>", self.view_journal) top.bind_all("<>", self.view_calendar) top.bind_all("<>", self.view_lettercomposer) top.bind("<>", self.new_contact_event) top.bind("<>", self.del_contact_event) top.bind("<>", self.save_contact_event) top.bind("<>", self.dup_contact_event) top.bind("<>", self.export_contact_event) top.bind("<>", self.find_contact_event) top.bind("<>", self.query_find_contact) top.bind("<>", self.query_find_next) top.bind("<>", self.query_birthdays) top.bind("<>", self.page_view) def registerAtBroadcaster(self): "Register our Callback Handlers" # Show Message Box on Notification Broadcast: broadcaster.Register(self.onNotification, source='Notification') broadcaster.Register(self.onContactsOpen, source='Contacts', title='Opened') broadcaster.Register(self.onContactsClose, source='Contacts', title='Closed') broadcaster.Register(self.onContactsImport, source='Contacts', title='Imported') broadcaster.Register(self.onContactModify, source='Contact', title='Modified') # Broadcasted by JournalEditWidget: broadcaster.Register(self.onJournalAttendeeClick, source='Journal', title='Attendee Clicked') broadcaster.Register(self.onCommandComposeLetter, source='Command', title='Compose Letter') menu_specs = [ ("file", "_File"), ("contact", "_Contact"), ("query", "_Query"), ("view", "_View"), ("settings", "_Settings"), ("help", "_Help"), ] def createMenubar(self): "Create the Menus and fill them" mbar = self._menubar self.menudict = menudict = {} for name, label in self.menu_specs: underline, label = prepstr(label) menudict[name] = menu = Menu(mbar, name=name) mbar.add_cascade(label=label, menu=menu, underline=underline) self.fillMenus() def fillMenus(self, defs=None, keydefs=None): """Fill the menus. Menus that are absent or None in self.menudict are ignored.""" if defs is None: defs = self.Bindings.menudefs if keydefs is None: keydefs = self.Bindings.default_keydefs menudict = self.menudict for mname, itemlist in defs: menu = menudict.get(mname) if not menu: continue for item in itemlist: if not item: menu.add_separator() else: label, event = item checkbutton = (label[:1] == '!') if checkbutton: label = label[1:] underline, label = prepstr(label) accelerator = get_accelerator(keydefs, event) def command(win=self.top, event=event): win.event_generate(event) if checkbutton: var = self.getrawvar(event, BooleanVar) menu.add_checkbutton(label=label, underline=underline, command=command, accelerator=accelerator, variable=var) else: menu.add_command(label=label, underline=underline, command=command, accelerator=accelerator) def initOptionDatabase(self): try: family, size, mod = Preferences.get('client.font', types.ListType) except: family = None if family: self.tkroot.option_add("*font", (family, size, mod)) if sys.platform != 'win32': # Enable 'MouseOver'-highlighting of buttons, etc: self.tkroot.option_add("*activeBackground", "#ececec") self.tkroot.option_add("*activeForeground", "black") # I prefer windows-alike look on unix: # (Listbox, Entry and Text widgets have grey background # on unix per default) self.tkroot.option_add("*Listbox*background", "white") # 2004-02-27: I no longer want flat listboxes: # self.tkroot.option_add("*Listbox*relief", "flat") self.tkroot.option_add("*Entry*background", "white") self.tkroot.option_add("*Text*background", "white") def createWidgets(self): "create the top level window" self._menubar = Menu(self.tkroot) top = self.top = Toplevel(self.tkroot, class_='PyCoCuMa', menu=self._menubar) top.protocol('WM_DELETE_WINDOW', self.close) top.title('PyCoCuMa %s' % self.__version) top.iconname('PyCoCuMa') try: os.chdir(os.path.dirname(sys.argv[0])) if sys.platform == "win32": top.iconbitmap("pycocuma.ico") else: top.iconbitmap("@pycocuma.xbm") top.iconmask("@pycocuma_mask.xbm") except: debug.echo("Could not set TopLevel window icon") top.rowconfigure(1, weight=1) top.columnconfigure(1, weight=1) top.withdraw() import ToolTip ## Top Bar: topbar = Frame(top) topbar.grid(columnspan=2,sticky=W+E) topbar.columnconfigure(0, weight=1) topbar.columnconfigure(7, weight=1) self.selContact = self.ContactSelectboxWidget(topbar, self.model, self.openContact) self.selContact.grid(sticky=W+E, padx=2, pady=2) # create icons (should already have been done by SplashScreen!) IconImages.createIconImages() # create buttons self.btnNewContact = Button(topbar, image=IconImages.IconImages["newcontact"], command=self.newContact) ToolTip.ToolTip(self.btnNewContact, "Create New Contact") self.btnDelContact = Button(topbar, image=IconImages.IconImages["delcontact"], command=self.delContact, state=DISABLED) ToolTip.ToolTip(self.btnDelContact, "Delete Contact") self.btnSaveContact = Button(topbar, image=IconImages.IconImages["savecontact"], command=self.saveContact, state=DISABLED) ToolTip.ToolTip(self.btnSaveContact, "Save Contact to Server") self.btnDuplicateContact = Button(topbar, image=IconImages.IconImages["dupcontact"], command=self.duplicateContact, state=DISABLED) ToolTip.ToolTip(self.btnDuplicateContact, "Duplicate this Contact") self.btnExportContact = Button(topbar, image=IconImages.IconImages["exportcontact"], command=self.exportContact, state=DISABLED) ToolTip.ToolTip(self.btnExportContact, "Export this Contact to file") # selectively display buttons btnColumn = 1 for btn in Preferences.get("client.topbar", astype=types.ListType): if btn == "newContact": self.btnNewContact.grid(column=btnColumn,row=0, padx=2, pady=2) btnColumn += 1 elif btn == "delContact": self.btnDelContact.grid(column=btnColumn,row=0, padx=2, pady=2) btnColumn += 1 elif btn == "saveContact": self.btnSaveContact.grid(column=btnColumn,row=0, padx=2, pady=2) btnColumn += 1 elif btn == "duplicateContact": self.btnDuplicateContact.grid(column=btnColumn,row=0, padx=2, pady=2) btnColumn += 1 elif btn == "exportContact": self.btnExportContact.grid(column=btnColumn,row=0, padx=2, pady=2) btnColumn += 1 elif btn == "SEP": # Separator Line: sep = Frame(topbar, width=2, borderwidth=1, relief=SUNKEN, height=32) sep.grid(column=btnColumn, row=0, padx=4, pady=2) btnColumn += 1 dummy = Frame(topbar) dummy.grid(column=7, row=0, sticky=W+E) #### ## List of Contacts (left): self.lstContacts = self.ContactListWidget(top, self.model, selectcommand=self.openContact, dblclickcommand=self.viewContact) self.lstContacts.grid(row=1,column=0,sticky=N+S+W+E) #### self._notebook = Pmw.NoteBook(top, pagemargin=2, lowercommand=self.notebookLower, raisecommand=self.notebookRaise) self._notebook.grid(row=1,column=1,sticky=W+E+N+S) self.listview = None page = self._notebook.add('List View') self.listview = self.ListViewWidget(page, self.model, selectcommand=self.openContact, dblclickcommand=self.viewContact) self.listview.pack(fill=BOTH, expand=1) self.contactview = None page = self._notebook.add('View Card') self.contactview = self.ContactViewWidget(page, selectcommand=self.editContact) self.contactview.grid(sticky=W+E+N+S) page = self._notebook.add('Edit Contact') self.contactedit = self.ContactEditWidget(page) self.contactedit.pack(fill=BOTH, expand=1) self._notebook.setnaturalsize() # Add Message/Status bar at bottom of window: self.messagebar = Pmw.MessageBar(top, silent=True, entry_relief=SUNKEN, entry_borderwidth=1, entry_highlightthickness=0) self.messagebar.grid(columnspan=2, sticky=W+E) def centerWindow(self, relx=0.5, rely=0.3): "Center the Main Window on Screen" widget = self.top master = self.tkroot widget.update_idletasks() # Actualize geometry information if master.winfo_ismapped(): m_width = master.winfo_width() m_height = master.winfo_height() m_x = master.winfo_rootx() m_y = master.winfo_rooty() else: m_width = master.winfo_screenwidth() m_height = master.winfo_screenheight() m_x = m_y = 0 w_width = widget.winfo_reqwidth() w_height = widget.winfo_reqheight() x = m_x + (m_width - w_width) * relx y = m_y + (m_height - w_height) * rely if x+w_width > master.winfo_screenwidth(): x = master.winfo_screenwidth() - w_width elif x < 0: x = 0 if y+w_height > master.winfo_screenheight(): y = master.winfo_screenheight() - w_height elif y < 0: y = 0 widget.geometry("+%d+%d" % (x, y)) widget.deiconify() # Become visible at the desired location def notebookLower(self, page): "Event triggered when lowering a page of the notebook" pass def notebookRaise(self, page): "Event triggered when raising another page of the notebook" if page == "List View" and self.listview: #try: # self.listview.partial_update(self.listview_update_contacts) #except: # pass self.listview_update_contacts.clear() elif page == "View Card" and self.contactview and self.contactview_must_update: self.contactview.rebindWidgets() self.contactview_must_update = 0 def newContact(self): "Add new (empty) contact" newhandle = self.model.NewContact({ 'fn':'*New Untitled Card*', 'categories':self.lstContacts.getCurrentCategory()}) self.editContact(newhandle) self.set_saved(0) def askDelContact(self, fn): "Display Dialog asking if user really wants to delete the card" m = tkMessageBox.Message( title="Confirm Delete", message="Do You really want to delete the card '%s'?" % (fn), icon=tkMessageBox.QUESTION, type=tkMessageBox.YESNO, master=self.top) return m.show() def delContact(self): "Delete contact currently editing" answer = self.askDelContact(self.contactedit.boundto().fn.get()) # Sometimes (esp. after Import) the answer is True # instead of 'yes': Why??? if answer == 'yes' or answer == True: handle = self.contactedit.cardhandle() self.model.DelContact(handle) self.contact_modified = 0 self.openContact() self.set_saved(0) def askSaveContact(self): "Display Dialog asking if user wants to save changes" m = tkMessageBox.Message( title="Save Changes?", message="This Contact has been modified.\nSave the Changes?", icon=tkMessageBox.QUESTION, type=tkMessageBox.YESNOCANCEL, master=self.top) return m.show() def saveContact(self): "Upload the modified Contact to the Server" # To get the latest changes from all textedit widgets: self.contactedit.rebindWidgets() # this must come after rebindWidgets, # because rebindWidgets could trigger a Modify-Broadcast: self.btnSaveContact["state"] = DISABLED handle = self.contactedit.cardhandle() if handle is None: handle = self.model.NewContact() self.model.PutContact(handle, self.contactedit.boundto().VCF_repr()) self.contact_modified = 0 def duplicateContact(self): "Duplicate the current Card" import vcard newhandle = self.model.NewContact() card = vcard.vCard(self.contactedit.boundto().VCF_repr()) card.fn.set(card.fn.get()+" (Copy)") self.model.PutContact(newhandle, card.VCF_repr()) self.editContact(newhandle) self.set_saved(0) def exportContact(self): "Export the current card to file" import converters converters.Export(self.top, self.model, targetformat=None, cardhandles=[self.contactedit.cardhandle()]) def openContact(self, handle=None): "Open contact by handle or default (first)" if self.contact_modified: # Sometimes (esp. after Import) the answer is True # instead of 'yes': Why??? answer = self.askSaveContact() if answer == 'yes' or answer == True: self.saveContact() if handle is None: handles = self.model.ListHandles() if handles: contact = self.model.GetContact(handles[0]) else: contact = None else: contact = self.model.GetContact(handle) self.contactview.bind_contact(contact) self.contactedit.bind_contact(contact) if contact: # Inform other widgets of newly opened contact: self.selContact.selectContact(contact.handle()) self.lstContacts.selectContact(contact.handle()) self.listview.selectContact(contact.handle()) self.btnDelContact["state"]=NORMAL self.btnDuplicateContact["state"]=NORMAL self.btnExportContact["state"]=NORMAL broadcaster.Broadcast('Contact', 'Opened', data={'handle':contact.handle()}) else: self.btnDelContact["state"]=DISABLED self.btnDuplicateContact["state"]=DISABLED self.btnExportContact["state"]=DISABLED self.btnSaveContact["state"]=DISABLED self.contact_modified = 0 def viewContact(self, handle=None): "Open the 'View Contact' Tab" self.openContact(handle) self._notebook.selectpage("View Card") def editContact(self, handle=None): "Open the 'Edit Contact' Tab" self.openContact(handle) self._notebook.selectpage("Edit Contact") def connect_to_server_event(self, event=None): "Show Connect Dialog" import ConnectDialog dlg = ConnectDialog.ConnectDialog(self.top, title="Connect") defaultstrings = {'xmlrpc':'http://localhost:8810', 'file':os.path.expanduser("~/addressbook.vcf")} type = broker.Request('Connection Type') str = broker.Request('Connection String') if type == 'none': type = Preferences.get('client.connection_type') str = Preferences.get('client.connection_string') defaultstrings[type] = str def doconnect(btn, self=self, dlg=dlg): if btn == 'Connect': type, str = dlg.getvalue() self.model.Close() self.model.Open(type, str) dlg.deactivate() dlg['command'] = doconnect dlg.activate(type, defaultstrings) def disconnect_from_server_event(self, event=None): "Disconnect From Server (close Model)" self.model.Close() def view_list_event(self, event=None): self._notebook.selectpage("List View") self.top.deiconify() self.top.lift() def view_card_event(self, event=None): self._notebook.selectpage("View Card") self.top.deiconify() self.top.lift() def edit_contact_event(self, event=None): self._notebook.selectpage("Edit Contact") self.top.deiconify() self.top.lift() def new_contact_event(self, event=None): self.newContact() def del_contact_event(self, event=None): self.delContact() def save_contact_event(self, event=None): self.saveContact() def dup_contact_event(self, event=None): self.duplicateContact() def export_contact_event(self, event=None): self.exportContact() def find_contact_event(self, event=None): self.selContact.focus_set() def onNotification(self): "Show Messagebox on Notification" title = broadcaster.CurrentTitle() showdialog = False if title == 'Error': self.messagebar.message('systemerror', 'ERROR: ' + broadcaster.CurrentData()['message']) icon = tkMessageBox.ERROR showdialog = True elif title == 'Status': self.messagebar.message('state', broadcaster.CurrentData()['message']) elif title == 'Event': self.messagebar.message('systemevent', broadcaster.CurrentData()['message']) else: self.messagebar.message('usererror', 'WARNING: ' + broadcaster.CurrentData()['message']) icon = tkMessageBox.WARNING showdialog = True if showdialog: m = tkMessageBox.Message( title=title, message=broadcaster.CurrentData()['message'], icon=icon, type=tkMessageBox.OK, master=self.top) m.show() def onContactsOpen(self): "Callback, triggered on Broadcast" self.set_saved(1) self.btnNewContact["state"] = NORMAL self.btnSaveContact["state"] = DISABLED # (Re-)Enable all Menu Items: for menu in ["file", "query"]: for i in range(self.menudict[menu].index(END)+1): if self.menudict[menu].type(i) == 'command': self.menudict[menu].entryconfigure(i, state=NORMAL) self.openContact() self.selContact.focus_set() def onContactsClose(self): "Callback, triggered on Broadcast" self.set_saved(1) self.btnNewContact["state"] = DISABLED self.btnDelContact["state"] = DISABLED self.btnDuplicateContact["state"] = DISABLED self.btnExportContact["state"] = DISABLED self.btnSaveContact["state"] = DISABLED # Disable all Menu Items except Connect and Close: for menu in ["file", "query"]: for i in range(self.menudict[menu].index(END)+1): if self.menudict[menu].type(i) == 'command'\ and self.menudict[menu].entrycget(i, 'label') != 'Connect...'\ and self.menudict[menu].entrycget(i, 'label') != 'Close': self.menudict[menu].entryconfigure(i, state=DISABLED) def onContactsImport(self): "Callback, triggered on Broadcast" self.set_saved(0) self.openContact(broadcaster.CurrentData()['handle']) listview_update_contacts = {} contactview_must_update = 0 contact_modified = 0 def onContactModify(self): "File was modified since last save" self.listview_update_contacts[broadcaster.CurrentData()['handle']] = 1 self.contactview_must_update = 1 self.contact_modified = 1 self.set_saved(0) self.btnSaveContact["state"] = NORMAL self.updateTitlebar() def onJournalAttendeeClick(self): # First try UID, and only if uid is not defined, # search by FormattedName: fieldval = broadcaster.CurrentData().get('uid') if fieldval: fieldname = 'UID' else: fieldval = broadcaster.CurrentData().get('fn') fieldname = 'FormattedName' handles = self.model.ListHandles() attrs = self.model.QueryAttributes(handles, fieldname) try: idx = attrs.index(fieldval) except ValueError: broadcaster.Broadcast('Notification', 'Error', data={'message':"A contact with %s '%s' could not be found." % (fieldname, fieldval)}) return self.openContact(handles[idx]) def onCommandComposeLetter(self): self.view_lettercomposer() card = broadcaster.CurrentData()['card'] adr = broadcaster.CurrentData()['adr'] self.lettercomposer.composeTo(card, adr) _is_saved = 0 def set_saved(self, flag): self._is_saved = flag self.updateTitlebar() def get_saved(self): return self._is_saved def close(self, event=None): reply = 'none' if self.contact_modified: reply = self.askSaveContact() if reply == 'yes' or reply == True: self.saveContact() # We need str() here, because reply is a tcl_Obj: if str(reply) != 'cancel': self._close() def _close(self): if self.isslavewindow: self.withdraw() else: self.tkroot.quit() self.io.close(); self.io = None self.top.destroy() def window(self): "Returns Tk's TopLevel Widget" return self.top def updateTitlebar(self): "Update the Window's Titlebar" con_str = broker.Request('Connection String') if con_str: title = "PyCoCuMa - %s" % con_str if not self.get_saved(): title = "*%s*" % title else: title = "PyCoCuMa %s" % self.__version self.top.wm_title(title) _prefwin = None def showPreferencesDialog(self, event=None): "Open Preference Editor" if not self._prefwin: import PreferencesDialog self._prefwin = PreferencesDialog.PreferencesDialog(self.top) self._prefwin.show() def showAboutDialog(self, event=None): "Show a simple About Dialog" tkMessageBox.showinfo('About PyCoCuMa ' + self.__version, """PyCoCuMa %s (Pythonic Contact and Customer Management) Copyright 2003-2004 Henning Jacobs This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. """ % self.__version) _helpwin = None def showHelpWindow(self, event=None): "Open Help Window" if not self._helpwin: import HelpWindow self._helpwin = HelpWindow.HelpWindow(self.top) self._helpwin.show() def withdraw(self): "Withdraw: Forward to TopLevel Method" self.top.withdraw() def deiconify(self): "DeIconify: Forward to TopLevel Method" self.top.deiconify() def query_birthdays(self, event=None): "Display List of upcoming Birthdays" self.Query.Birthdays(self.top, self.model) def query_find_contact(self, event=None): "Find/Search Dialog" start = self.contactedit.cardhandle() handle = self.Query.FindContact(self.top, self.model, start) if handle != None and handle != start: self.openContact(handle) def query_find_next(self, event=None): "Find the Next Contact with same Searchoptions" start = self.contactedit.cardhandle() handle = self.Query.FindContact(self.top, self.model, start, 'search_next') if handle != None and handle != start: self.openContact(handle) def page_view(self, event=None): "View PDF Document (uses PDFLaTeX)" import tempfile import converters filename = tempfile.mktemp('.tex') fd = converters.EncodedFileWriter(file(filename, "wb"), 'latin-1') converters.export2latex(self.model, fd) fd.close() import texwrapper texwrapper.run_pdflatex(filename) texwrapper.view_pdf(filename) journalwin = None def view_journal(self, event=None): "Open Journal Window" from JournalWindow import JournalWindow if not self.journalwin: self.journalwin = JournalWindow(self.model, self.top) self.journalwin.show() lettercomposer = None def view_lettercomposer(self, event=None): "Open Letter Composer Window" from LetterComposer import LetterComposer if not self.lettercomposer: self.lettercomposer = LetterComposer(self.top) self.lettercomposer.show() calendarwin = None def view_calendar(self, event=None): "Open Calendar Window" from CalendarWindow import CalendarWindow if not self.calendarwin: self.calendarwin = CalendarWindow(self.model, self.top) self.calendarwin.show() keynames = { 'bracketleft': '[', 'bracketright': ']', 'slash': '/', } def prepstr(s): # Helper to extract the underscore from a string, e.g. # prepstr("Co_py") returns (2, "Copy"). i = string.find(s, '_') if i >= 0: s = s[:i] + s[i+1:] return i, s def get_accelerator(keydefs, event): import re keylist = keydefs.get(event) if not keylist: return "" s = keylist[0] s = re.sub(r"-[a-z]\b", lambda m: string.upper(m.group()), s) s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s) s = re.sub("Key-", "", s) s = re.sub("Control-", "Ctrl-", s) s = re.sub("-", "+", s) s = re.sub("><", " ", s) s = re.sub("<", "", s) s = re.sub(">", "", s) return s PyCoCuMa-0.4.5-6/pycocumalib/MultiColumnTextList.py0000644000175000017500000002240010252657644023244 0ustar henninghenning00000000000000#!/usr/bin/python # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: MultiColumnTextList.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import Pmw import tkFont import sys class MultiColumnTextList(Frame): "Tkinter.Text wird hier als Tabelle missbraucht" colsepwidth = 20 def __init__(self, master, **kws): Frame.__init__(self, master) self.rows = [] self.columnwidths = [] self.columnstringwidths = {} options = ['selectcommand','dblclickcommand','autocolresize'] self.selectcommand = kws.get('selectcommand', None) self.dblclickcommand = kws.get('dblclickcommand', None) self.autocolresize = kws.get('autocolresize', 1) if kws.get('columnheader', 0): kws['columnheader'] = 1 kws['columnheader_state'] = DISABLED kws['columnheader_takefocus'] = 0 kws['columnheader_highlightthickness'] = 0 kws['columnheader_cursor'] = self.cget('cursor') kws['columnheader_background'] = self.cget('background') kws['columnheader_borderwidth'] = 1 # Remove our own Options from Dict: for opt in options: try: del kws[opt] except: pass if sys.platform != 'win32': # Thin Scrollbars: kws['horizscrollbar_borderwidth'] = 1 kws['horizscrollbar_width'] = 10 kws['vertscrollbar_borderwidth'] = 1 kws['vertscrollbar_width'] = 10 self.scrolledtext = Pmw.ScrolledText(self, # no distance between text and scrollbars: scrollmargin=0, text_wrap=NONE, text_state=DISABLED, text_takefocus=0, text_highlightthickness=0, text_cursor=self.cget('cursor'), **kws) self.text = self.scrolledtext.component('text') if kws.get('columnheader', 0): self.columnheader = self.scrolledtext.component('columnheader') else: self.columnheader = None self.text.bind("", self._resize_columns) self.text.bind("", self._text_click) self.text.bind("", self._text_click) self.text.bind("", self._text_click) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.scrolledtext.grid(sticky=W+E+N+S) # Create Tag for selected Row (Line): self.text.tag_config("rowsel", foreground=self.text.cget('selectforeground'), background=self.text.cget('selectbackground'), borderwidth=self.text.cget('selectborderwidth'), relief=RAISED) # Create Tag for Odd Rows (Lines): self.text.tag_config("odd", background='#eeeeff') # Make Selection invisible: self.text.config( selectforeground=self.text.cget('foreground'), selectbackground=self.text.cget('background'), selectborderwidth=0) self.font = tkFont.Font(font = self.text.cget('font')) def append(self, row): "Append a new row" self.insert(-1, row) def insert(self, idx, row): "Insert row before idx" self.rows.insert(idx, row) colwidths_havechanged = 0 if self.autocolresize: for i in range(len(row)): # First, we compare the string length.. if len(row[i]) > self.columnstringwidths.get(i, 0): self.columnstringwidths[i] = len(row[i]) # we measure only the longest string, # because font.measure is VERY TIME CONSUMING!! wd = self.font.measure(row[i])+self.colsepwidth if i > len(self.columnwidths)-1: self.columnwidths.append(wd) colwidths_havechanged = 1 elif wd > self.columnwidths[i]: self.columnwidths[i] = wd colwidths_havechanged = 1 if idx == -1: CharIndex = END absidx = len(self.rows)-1 else: CharIndex = "%d.0" % (idx+1) absidx = idx self.text["state"] = NORMAL self.text.insert(CharIndex, self._row2textline(row)) self.text["state"] = DISABLED self._updateRowTags(range(absidx-1, len(self.rows))) if colwidths_havechanged: self._resize_columns() if len(self.rows) == 1: self.select(0) def remove(self, idx): "Delete Row at idx" self.text["state"] = NORMAL self.text.delete("%d.0" % (idx+1), "%d.0" % (idx+2)) self.text["state"] = DISABLED del self.rows[idx] if len(self.rows) == 0: self.select(None) else: self._updateRowTags(range(idx-1, len(self.rows))) def clear(self): "Delete all Rows." self.text["state"] = NORMAL self.text.delete("1.0", END) self.text["state"] = DISABLED self.rows[:] = [] self.select(None) def get(self, idx=None): "Return row at idx or list of all rows" if idx == None: return self.rows else: try: return self.rows[idx] except: return None def length(self): "Return number of rows." return len(self.rows) def selected(self): "Return index of selected row." return self.selectedrow def _row2textline(self, row): return "\t".join(row)+"\t\n" def _resize_columns(self, event=None): "Do column resizing" tabs = [] prevcolend = 0 for i in range(len(self.columnwidths)-1): tabs.append(self.columnwidths[i]+prevcolend) prevcolend = tabs[i] # to make the line appear as a complete row, # append another tab-stopp at widget width: if self.columnwidths: lasttab=max(self.columnwidths[-1]+prevcolend,self.text.winfo_width()) else: lasttab = self.text.winfo_width() tabs.append(lasttab) tabs = tuple(map(str, tabs)) self.text["tabs"] = tabs if self.columnheader: self.columnheader["tabs"] = tabs def _updateRowTags(self, range): for i in range: if i is not None: if i == self.selectedrow: self.text.tag_remove("odd", "%s.0" % (i+1), "%s.end" % (i+1)) self.text.tag_add("rowsel", "%s.0" % (i+1), "%s.end" % (i+1)) elif i % 2 == 1: self.text.tag_remove("rowsel", "%s.0" % (i+1), "%s.end" % (i+1)) self.text.tag_add("odd", "%s.0" % (i+1), "%s.end" % (i+1)) else: self.text.tag_remove("rowsel", "%s.0" % (i+1), "%s.end" % (i+1)) self.text.tag_remove("odd", "%s.0" % (i+1), "%s.end" % (i+1)) def setcolwidths(self, widths): self.columnwidths = widths self.columnstringwidths.clear() self._resize_columns() def setcolwidthsfromstr(self, row): self.columnwidths[:] = [] for i in range(len(row)): wd = self.font.measure(row[i])+self.colsepwidth self.columnwidths.append(wd) self.columnstringwidths.clear() self._resize_columns() def setcolheader(self, header): self.columnheader["state"] = NORMAL self.columnheader.delete('1.0',END) self.columnheader.insert('1.0',self._row2textline(header)) self.columnheader["state"] = DISABLED selectedrow = None def select(self, idx): "Select Row or None." if idx >= len(self.rows): idx = None lastsel = self.selectedrow self.selectedrow = idx self._updateRowTags([idx, lastsel]) def see(self, idx): "Scroll to line number idx" self.text.see("%d.0" % idx) _lastmouseclicktime = 0 _lastmouseclickrowidx = None def _text_click(self, event): "User selected a row by click" line, char = self.text.index("@%d,%d" % (event.x, event.y)).split('.') idx = int(line)-1 self.select(idx) if self.dblclickcommand\ and event.time - self._lastmouseclicktime <= 400\ and self._lastmouseclickrowidx == idx: self.dblclickcommand() elif self.selectcommand: self.selectcommand() self._lastmouseclicktime = event.time self._lastmouseclickrowidx = idx if __name__ == "__main__": tk = Tk() list = MultiColumnTextList(tk) list.pack(fill=BOTH,expand=1) list.append(('Datum','Typ','Vorgang','?')) list.append(('2003-10-10','Kommentar','Ja, Dies ist mein Test-Kommentar!','What''s that?')) list.append(('2003-10-11','Note','Note how the column-widths grow with the content!','Another Cell')) list.append(('This','Line','should','HAVE BEEN DELETED!')) list.append(('2003-10-11','Test','123','456')) list.insert(1, ('This','should appear','as the','second line!')) list.remove(4) tk.mainloop() PyCoCuMa-0.4.5-6/pycocumalib/MultiStatusBar.py0000644000175000017500000000222210252657644022216 0ustar henninghenning00000000000000# Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: MultiStatusBar.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * class MultiStatusBar(Frame): def __init__(self, master=None, **kw): if master is None: master = Tk() apply(Frame.__init__, (self, master), kw) self.labels = {} def set_label(self, name, text='', side=LEFT): if not self.labels.has_key(name): label = Label(self, bd=1, relief=SUNKEN, anchor=W) label.pack(side=side) self.labels[name] = label else: label = self.labels[name] label.config(text=text) def _test(): b = Frame() c = Text(b) c.pack(side=TOP) a = MultiStatusBar(b) a.set_label("one", "hello") a.set_label("two", "world") a.pack(side=BOTTOM, fill=X) b.pack() b.mainloop() if __name__ == '__main__': _test() PyCoCuMa-0.4.5-6/pycocumalib/Preferences.py0000644000175000017500000001113210252657644021534 0ustar henninghenning00000000000000# Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: Preferences.py 87 2004-09-07 17:29:34Z henning $ import sys import os.path import types import debug import ConfigParser _preffile = os.path.expanduser('~/.pycocuma') if sys.platform == "win32": # Windows now uses FileClient per default: _config = { "client.connection_type": "file", "client.connection_string": os.path.expanduser("~/addressbook.vcf"), # Use default mailto-App from Windows-Registry: "client.mailto_program": "", "client.url_viewer": "", "client.autostart_server": "no", "client.topbar": "newContact, delContact, saveContact, SEP, duplicateContact, exportContact", # not used when conn_type is 'file': "server.addressbook_filename": "~/addressbook.vcf", "server.calendar_filename": "~/addressbook.ics", "server.listen_host": "localhost", "server.listen_port": "8810", # No logging per default: "server.log_filename": "" } else: # Use XML-RPC Server: _config = { "client.connection_type": "xmlrpc", "client.connection_string": "http://localhost:8810", "client.mailto_program": "x-terminal-emulator -e pine %1", "client.url_viewer": "", "client.autostart_server": "yes", "client.topbar": "newContact, delContact, saveContact, SEP, duplicateContact, exportContact", "server.addressbook_filename": "~/addressbook.vcf", "server.calendar_filename": "~/addressbook.ics", "server.listen_host": "localhost", "server.listen_port": "8810", # No logging per default: "server.log_filename": "" } def Load(filename=None): "Load Configuration Settings From File" global _preffile if filename: _preffile = os.path.expanduser(filename) cp = ConfigParser.ConfigParser() try: cp.read([_preffile]) except: debug.echo("Errors while reading '"+_preffile+"'") for sec in cp.sections(): name = sec.lower() for opt in cp.options(sec): _config[name + "." + opt.lower()] = unicode(cp.get(sec, opt), 'utf-8', 'replace') def Save(): "Write Configuration to File" cp = ConfigParser.ConfigParser() for key, val in zip(_config.keys(), _config.values()): sec, opt = key.split('.') if not cp.has_section(sec): cp.add_section(sec) cp.set(sec, opt, val.encode('utf-8', 'replace')) cp.write(open(_preffile, 'wb')) def get(key, astype=None): "Return Configuration Option as given Type" # returns None if key is not set: ret = _config.get(key) if ret is not None and astype == types.ListType: ret = _strToList(ret) return ret def set(key, value): "Set Configuration Option" if type(value) == types.ListType or type(value) == types.TupleType: valstr = _listToStr(value) else: valstr = value if not isinstance(valstr, types.StringTypes): valstr = str(valstr) _config[key] = valstr def has_key(key): "Test for Configuration Option existance" return _config.has_key(key) def _listToStr(list): "Convert List to String for storing in Config-File" def escape(str): return str.replace('\\',r'\\').replace(',','\,') return ', '.join(map(escape, list)) def _strToList(str): "Convert List String from Config-File to List" def deescape(str): return str.replace('\,',',').replace(r'\\', '\\') # The last space char is stripped but we need it here: if str[-2:] != '\,' and str[-1:] == ',': str = str + ' ' ret = str.split(', ') i = 0 while i # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: PreferencesDialog.py 89 2004-09-11 18:58:51Z henning $ from Tkinter import * from PropertyEditor import PropertyEditor from InputWidgets import TextEdit, TextComboEdit, FilenameEdit, FontEdit import Preferences import ToolTip import vcard import types class PrefWrapperVar: "Wraps a Preference Option" def __init__(self, optname, default='', type=None): self.optname = optname self.default = default self.type = type def set(self, val): Preferences.set(self.optname, val) def get(self): val = Preferences.get(self.optname, self.type) if val is None: return self.default else: return val class FilenamePropEdit(FilenameEdit): def __init__(self, master, title, descr): FilenameEdit.__init__(self, master) ToolTip.ToolTip(self, descr) class FontPropEdit(FontEdit): def __init__(self, master, title, descr): FontEdit.__init__(self, master) ToolTip.ToolTip(self, descr) class vCardFieldPropEdit(TextComboEdit): def __init__(self, master, title, descr): TextComboEdit.__init__(self, master, nomanualedit=True) ToolTip.ToolTip(self.component('entry'), descr) self.setlist(vcard.FIELDNAMES) class PreferencesDialog(PropertyEditor): # (Type, Title, Helptext) propdefs = [ ('Label', '', 'You must restart PyCoCuMa for the changes to take effect.'), ('Page', 'General', ''), ('vCardField', 'Contact List Displayfield', 'default: DisplayName'), ('vCardField', 'Contact List Sortby', 'default: SortName'), ('Page', 'Look & Feel', ''), ('Font', 'General Font', 'application wide font'), ('Text', 'Speed Buttons', 'These little icons on top of the window'), ('Page', 'QuickFinder', ''), ('Checkbox', 'Start centered', 'Start QuickFinder centered on desktop'), ('Label', '', '(Start the Finder with the "--finder" commandline option.)'), ('Page', 'External Programs', ''), ('Filename', 'URL Viewer Program (Web Browser)', 'leave empty to use platform\'s default browser'), ('Filename', 'Mail-To Program', 'your email composer application'), ] def __init__(self, master): PropertyEditor.__init__(self, master=master, propdefs=self.propdefs, title='Edit Preferences', editclasses={'vCardField':vCardFieldPropEdit, 'Filename':FilenamePropEdit, 'Font':FontPropEdit}) self.bindto([ PrefWrapperVar('client.contactlist_displayfield', default='DisplayName'), PrefWrapperVar('client.contactlist_sortby', default='SortName'), PrefWrapperVar('client.font', default=('',''), type=types.ListType), PrefWrapperVar('client.topbar', default='newContact, delContact, saveContact, SEP, duplicateContact, exportContact'), PrefWrapperVar('client.finder_centered', default='yes'), PrefWrapperVar('client.url_viewer', default=''), PrefWrapperVar('client.mailto_program', default='') ]) if __name__ == "__main__": tk = Tk() dlg = PreferencesDialog(tk) dlg.show() tk.mainloop() PyCoCuMa-0.4.5-6/pycocumalib/PropertyEditor.py0000644000175000017500000002323010252657644022270 0ustar henninghenning00000000000000""" Property Editor Dialog """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: PropertyEditor.py 93 2004-11-28 16:05:34Z henning $ from Tkinter import * import Pmw import ToolTip from InputWidgets import AbstractSingleVarEdit, TextEdit, MemoEdit, CheckboxEdit PADX = PADY = 2 class MemoPropEdit(Pmw.Group): def __init__(self, master, title, descr): Pmw.Group.__init__(self, master, tag_text = title) master = self.interior() self.edtMemo = MemoEdit(master) master.columnconfigure(0, weight=1) master.rowconfigure(0, weight=1) self.edtMemo.grid(sticky=W+E+S+N, padx=PADX, pady=PADY) ToolTip.ToolTip(self.edtMemo, descr) def bindto(self, var): self.edtMemo.bindto(var) def add_save_hook(self, save_hook): self.edtMemo.add_save_hook(save_hook) class TextPropEdit(TextEdit): def __init__(self, master, title, descr): TextEdit.__init__(self, master) ToolTip.ToolTip(self, descr) class CheckboxPropEdit(CheckboxEdit): def __init__(self, master, title, descr): CheckboxEdit.__init__(self, master) ToolTip.ToolTip(self, descr) class ImagePropEdit(Pmw.Group, AbstractSingleVarEdit): def __init__(self, master, title, descr): Pmw.Group.__init__(self, master, tag_text = title) AbstractSingleVarEdit.__init__(self) # imagedata is base64 encoded! self._imagedata = "" self._imageformat = "" master = self.interior() ToolTip.ToolTip(master, descr) self.btnLoad = Button(master, text='Load...', command=self.loadImageFromFile) self.btnLoad.grid(column=0, columnspan=2, row=0, sticky=W+E+S+N, padx=PADX, pady=PADY) ToolTip.ToolTip(self.btnLoad, "Load Image from file") self.btnSave = Button(master, text='Save...', command=self.saveImageToFile) self.btnSave.grid(column=2, columnspan=2, row=0, sticky=W+E+S+N, padx=PADX, pady=PADY) ToolTip.ToolTip(self.btnSave, "Save Image to file") self.lblSize = Label(master) self.lblSize.grid(columnspan=3, row=1, sticky=W+E, padx=PADX, pady=PADY) self.btnClear = Button(master, text="Clear", command=self.clearImage, padx=0, pady=0) self.btnClear.grid(column=3, row=1, padx=PADX, pady=PADY) ToolTip.ToolTip(self.btnClear, "Clear Image (remove)") self.updateLabel() def clearImage(self): self.clear() self.save() self.updateLabel() def updateLabel(self): if not self._imageformat and hasattr(self._var, 'params'): # get vCard specific parameters tempset = self._var.params.get('type') if tempset and len(tempset) > 0: self._imageformat = tempset[0] else: self._imageformat = "UNKNOWN" size = len(self._imagedata) if size > 0: self.lblSize["text"] = "%d Bytes (base64 encoded %s)" % (size, self._imageformat) else: self.lblSize["text"] = "" def get(self): return self._imagedata def clear(self): self._imagedata = "" self._imageformat = "" def set(self, data): self._imagedata = data self.updateLabel() def loadImageFromFile(self): import tkFileDialog, base64 dlg = tkFileDialog.Open(self.interior(), filetypes=[("Image Files", "*.jpg *.jpeg *.png *.gif"), ("All Files", "*")]) fname = dlg.show() if fname: # determine image format import os fbase, fext = os.path.splitext(fname) fext = fext.lower() if fext == '.jpg' or fext == '.jpeg': self._imageformat = 'JPEG' elif fext == '.png': self._imageformat = 'PNG' elif fext == '.gif': self._imageformat = 'GIF' else: self._imageformat = 'UNKNOWN' self.set(base64.encodestring(open(fname, "rb").read())) if hasattr(self._var, 'params'): # set vCard specific parameters import Set self._var.params.set('encoding', Set.Set('b')) self._var.params.set('type', Set.Set(self._imageformat)) self.save() def saveImageToFile(self): import tkFileDialog, base64 dlg = tkFileDialog.SaveAs(self.interior(), filetypes=[("Image Files", "*.jpg *.jpeg *.png *.gif"), ("All Files", "*")]) fname = dlg.show() if fname: fd = open(fname, "wb") fd.write(base64.decodestring(self.get())) fd.close() class PropertyEditor(Pmw.Dialog): # propdefs = [(type, title, descr)] def __init__(self, master, propdefs, **kws): Pmw.Dialog.__init__(self, master=master, title=kws.get('title', ''), buttons=('Close',), defaultbutton='Close') self.master = master self.propdefs = propdefs self._save_hook = kws.get('save_hook') self._editclasses = kws.get('editclasses', {}) self._addcolon = kws.get('addcolon', ':') self.notebook = None self.createWidgets() def createWidgets(self): self.propwidgets = [] row = 1 page = self.interior() self.interior().columnconfigure(0, weight=1) for type, title, descr in self.propdefs: if type == "Page": row = 0 if not self.notebook: self.interior().rowconfigure(row, weight=1) self.notebook = Pmw.NoteBook(self.interior()) self.notebook.grid(row=row, columnspan=2, sticky=W+E+S+N, padx=PADX, pady=PADY) page = self.notebook.add(title) page.columnconfigure(0, weight=1) page.columnconfigure(1, weight=2) continue elif type == "Label": lbl = Label(page, text=descr) lbl.grid(row=row, column=0, columnspan=2, padx=PADX, pady=PADY) else: if type == "Memo": page.rowconfigure(row, weight=1) widget = MemoPropEdit(page, title, descr) widget.grid(row=row, columnspan=2, sticky=W+E+N+S, padx=PADX, pady=PADY) elif type == "Image": page.rowconfigure(row, weight=1) widget = ImagePropEdit(page, title, descr) widget.grid(row=row, columnspan=2, sticky=W+E+N+S, padx=PADX, pady=PADY) else: lbl = Label(page, text=title+self._addcolon) lbl.grid(row=row, column=0, padx=PADX, pady=PADY) if type == "Text": widget = TextPropEdit(page, title, descr) elif type == "Checkbox": widget = CheckboxPropEdit(page, title, descr) else: widget = self._editclasses[type](page, title, descr) widget.grid(row=row, column=1, sticky=W+E, padx=PADX, pady=PADY) if self._save_hook: widget.add_save_hook(self._save_hook) self.propwidgets.append(widget) row += 1 if self.notebook: self.notebook.setnaturalsize() def bindto(self, varlist): for var, widget in zip(varlist, self.propwidgets): widget.bindto(var) _firstshow = 1 def show(self): if self._firstshow: self.centerWindow() else: self.deiconify() self._firstshow = 0 def centerWindow(self, relx=0.5, rely=0.3): "Center the Window on Screen" widget = self master = self.master widget.update_idletasks() # Actualize geometry information if master.winfo_ismapped(): m_width = master.winfo_width() m_height = master.winfo_height() m_x = master.winfo_rootx() m_y = master.winfo_rooty() else: m_width = master.winfo_screenwidth() m_height = master.winfo_screenheight() m_x = m_y = 0 w_width = widget.winfo_reqwidth() w_height = widget.winfo_reqheight() x = m_x + (m_width - w_width) * relx y = m_y + (m_height - w_height) * rely if x+w_width > master.winfo_screenwidth(): x = master.winfo_screenwidth() - w_width elif x < 0: x = 0 if y+w_height > master.winfo_screenheight(): y = master.winfo_screenheight() - w_height elif y < 0: y = 0 widget.geometry("+%d+%d" % (x, y)) widget.deiconify() # Become visible at the desired location if __name__ == "__main__": import InputWidgets propdefs = [ ("Text", "Test", "Test Descr"), ("Text", "Color", "Choose Color"), ("Label", "", "Here comes a DateEdit:"), ("Date", "Date", "My Date Edit"), ("Memo", "Your Ideas", "Please enter your Ideas")] class MyDateEdit(InputWidgets.DateEdit): def __init__(self, master, title, descr): InputWidgets.DateEdit.__init__(self, master) tk = Tk() dlg = PropertyEditor(tk, propdefs, title="Property Editor Test", editclasses={"Date":MyDateEdit}) dlg.show() tk.mainloop() PyCoCuMa-0.4.5-6/pycocumalib/Query.py0000644000175000017500000000753610252657644020415 0ustar henninghenning00000000000000""" Find Contact and Query Birthdays """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: Query.py 82 2004-07-11 13:01:44Z henning $ import vcard import Pmw import MultiColumnTextList from Tkinter import * import time import debug # Max number of tel/emails/adrs to be searched through: MAX_N_SEARCH_FIELDS = 5 last_searchdef = None def FindContact(master, model, start=None, searchnext=0): global last_searchdef import FindDialog import re if (not searchnext) or (not last_searchdef): dlg = FindDialog.FindDialog(master) if dlg.activate() == 'Ok': searchdef = dlg.getvalue() else: return None else: searchdef = last_searchdef searchstr, fieldname, useregex, ignorecase = searchdef handles = model.ListHandles() if start: pos = handles.index(start) if pos < len(handles)-1: pos += 1 else: # Restart Search at First Contact: pos = 0 else: pos = 0 # Tk deals with UTF-8 strings, but we want Unicode: searchstr = unicode(searchstr, 'utf-8', 'replace') regex_flags = None if ignorecase: searchstr = searchstr.lower() regex_flags = re.IGNORECASE for handle in handles[pos:]+handles[:pos]: # Search in max. MAX_N_SEARCH_FIELDS fields of the same name: vals = model.QueryAttributes([handle], [fieldname]*MAX_N_SEARCH_FIELDS) for val in filter(None, vals[0]): if useregex: # The User wants to Use Regular Expressions: if re.search(searchstr, val, regex_flags): last_searchdef = searchdef return handle else: # Standard String Search: if ignorecase: val = val.lower() if val.find(searchstr) <> -1: last_searchdef = searchdef return handle def Birthdays(master, model): attrlist = model.QueryAttributes(model.ListHandles(), ('FormattedName','Birthday')) bdays = [] for fn, bday in attrlist: if bday: isodate = bday year = int(isodate[:4]) month = int(isodate[5:7]) day = int(isodate[8:]) curtime = time.localtime() curyear = curtime[0] curmonth = curtime[1] curday = curtime[2] if month > curmonth or (month == curmonth and day >= curday): y = curyear else: y = curyear+1 secs = time.mktime((y, month, day, 0, 0, 0, 0, 1, -1)) sortidx = secs - time.time() tuple = (sortidx, year, month, day, fn, y) bdays.append(tuple) bdays.sort() #text = "" rows = [] for bday in bdays: no = str(bday[5]-bday[1]) if no[-1:] == "1": ext = "st" elif no[-1:] == "2": ext = "nd" elif no[-1:] == "3": ext = "rd" else: ext = "th" no = no+ext rows.append(("%(y)04d-%(m)02d-%(d)02d" % { "y":bday[5], "m":bday[2], "d":bday[3]}, "%(no)s Birthday of %(name)s" % { "name":bday[4], "no":no})) dlg = Pmw.Dialog(master, title="Birthdays", buttons=('Close',), defaultbutton='Close') dlg.withdraw() textlist = MultiColumnTextList.MultiColumnTextList(dlg.interior(), autocolresize=0) textlist.setcolwidthsfromstr(['2004-12-31','Birthday']) textlist.pack(fill=BOTH, expand=1) for row in rows: textlist.append(row) dlg.show() PyCoCuMa-0.4.5-6/pycocumalib/QuickFinder.py0000644000175000017500000002277110311640650021473 0ustar henninghenning00000000000000""" QuickFinder """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: QuickFinder.py 142 2005-09-13 21:16:23Z henning $ import sys import os import string from Tkinter import * import tkMessageBox import Pmw import IconImages import debug import broadcaster import broker import types import Preferences class QuickFinder: from ContactSelectboxWidget import ContactSelectboxWidget from ContactViewWidget import ContactViewWidget from HistoryWidget import HistoryWidget import Query import Bindings def __init__(self, version, model, tkroot=None): if not tkroot: tkroot = Pmw.initialise() tkroot.withdraw() self.tkroot = tkroot self.__version = version self.model = model self.initOptionDatabase() self.createWidgets() self.centerWindow() self.applyBindings() self.registerAtBroadcaster() def applyBindings(self, keydefs=None): "Apply Key-Bindings / Add, Connect to Events" if keydefs is None: keydefs = self.Bindings.default_keydefs self.keydefs = keydefs top = self.top for event, keylist in keydefs.items(): if keylist: top.event_add(event, *keylist) top.bind("<>", self.close) def registerAtBroadcaster(self): "Register our Callback Handlers" # Show Message Box on Notification Broadcast: broadcaster.Register(self.onNotification, source='Notification') broadcaster.Register(self.onContactsOpen, source='Contacts', title='Opened') broadcaster.Register(self.onCommandComposeLetter, source='Command', title='Compose Letter') def initOptionDatabase(self): try: family, size, mod = Preferences.get('client.font', types.ListType) except: family = None if family: self.tkroot.option_add("*font", (family, size, mod)) if sys.platform != 'win32': # Enable 'MouseOver'-highlighting of buttons, etc: self.tkroot.option_add("*activeBackground", "#ececec") self.tkroot.option_add("*activeForeground", "black") # I prefer windows-alike look on unix: # (Listbox, Entry and Text widgets have grey background # on unix per default) self.tkroot.option_add("*Listbox*background", "white") # 2004-02-27: I no longer want flat listboxes: # self.tkroot.option_add("*Listbox*relief", "flat") self.tkroot.option_add("*Entry*background", "white") self.tkroot.option_add("*Text*background", "white") def createWidgets(self): "create the top level window" #self._menubar = Menu(self.tkroot) top = self.top = Toplevel(self.tkroot, class_='PyCoCuMa') top.protocol('WM_DELETE_WINDOW', self.close) top.title('PyCoCuMa %s' % self.__version) top.iconname('PyCoCuMa') try: os.chdir(os.path.dirname(sys.argv[0])) if sys.platform == "win32": top.iconbitmap("pycocuma.ico") else: top.iconbitmap("@pycocuma.xbm") top.iconmask("@pycocuma_mask.xbm") except: debug.echo("Could not set TopLevel window icon") top.rowconfigure(1, weight=1) top.columnconfigure(0, weight=1) top.withdraw() import ToolTip ## Top Bar: topbar = Frame(top) topbar.grid(sticky=W+E) topbar.columnconfigure(0, weight=1) self.selContact = self.ContactSelectboxWidget(topbar, self.model, self.openContact, label_text='') self.selContact.grid(sticky=W+E, padx=2, pady=2) self.history = self.HistoryWidget(topbar, command = self.openContactFromHistory) self.history.grid(column=1, row=0, sticky=W+E+N+S, padx=2, pady=2) self.history.configure(state="disabled") self.btnCloseContactView = Button(topbar, text="Collapse", command=self.closeContactView) self.btnCloseContactView.grid(column=2, row=0, sticky=W+E, padx=2, pady=2) self.btnCloseContactView["state"]=DISABLED self.btnOpenMainView = Button(topbar, text="Main Window", command=self.openMainView) self.btnOpenMainView.grid(column=3, row=0, sticky=W+E, padx=2, pady=2) self.contactview = None self.contactview = self.ContactViewWidget(top, selectcommand=self.openMainViewForEdit) self.contactview.grid(sticky=W+E+N+S) self.contactview.grid_forget() def centerWindow(self, relx=0.5, rely=0.3): "Center the Main Window on Screen" widget = self.top master = self.tkroot widget.update_idletasks() # Actualize geometry information if Preferences.get("client.finder_centered") != "no": if master.winfo_ismapped(): m_width = master.winfo_width() m_height = master.winfo_height() m_x = master.winfo_rootx() m_y = master.winfo_rooty() else: m_width = master.winfo_screenwidth() m_height = master.winfo_screenheight() m_x = m_y = 0 w_width = widget.winfo_reqwidth() w_height = widget.winfo_reqheight() x = m_x + (m_width - w_width) * relx y = m_y + (m_height - w_height) * rely if x+w_width > master.winfo_screenwidth(): x = master.winfo_screenwidth() - w_width elif x < 0: x = 0 if y+w_height > master.winfo_screenheight(): y = master.winfo_screenheight() - w_height elif y < 0: y = 0 widget.geometry("+%d+%d" % (x, y)) widget.deiconify() # Become visible at the desired location def openContactFromHistory(self, handle): self.openContact(handle, addToHistory=False) def openContact(self, handle=None, addToHistory=True): "Open contact by handle or default (first)" if handle is None: handles = self.model.ListHandles() if handles: contact = self.model.GetContact(handles[0]) else: contact = None else: contact = self.model.GetContact(handle) self.contactview.bind_contact(contact) if contact: # Inform other widgets of newly opened contact: self.selContact.selectContact(contact.handle()) broadcaster.Broadcast('Contact', 'Opened', data={'handle':contact.handle()}) self.contactview.grid() self.btnCloseContactView["text"]="Collapse" self.btnCloseContactView["state"]=NORMAL if contact and addToHistory: # Store in history self.history.addHistory(contact.handle()) def closeContactView(self): "Close Contact Window" if self.btnCloseContactView["text"] == "Show": self.contactview.grid() self.btnCloseContactView["text"]="Collapse" else: self.contactview.grid_forget() self.btnCloseContactView["text"]="Show" mainview = None def createMainView(self): if not self.mainview: from MainView import MainView self.mainview = MainView(self.__version, self.model, tkroot=self.tkroot, slavewindow=True) broadcaster.Broadcast("Contacts", "Opened") def openMainView(self): "Open the Main Window" self.createMainView() self.mainview.openContact(self.contactview.cardhandle()) self.mainview.deiconify() def openMainViewForEdit(self, cardhandle): "Open the Main Window and edit Contact" self.createMainView() self.mainview.editContact(cardhandle) self.mainview.deiconify() def onNotification(self): "Show Messagebox on Notification" title = broadcaster.CurrentTitle() showdialog = False if title == 'Error': icon = tkMessageBox.ERROR showdialog = True elif title == 'Status': pass elif title == 'Event': pass else: icon = tkMessageBox.WARNING showdialog = True if showdialog: m = tkMessageBox.Message( title=title, message=broadcaster.CurrentData()['message'], icon=icon, type=tkMessageBox.OK, master=self.top) m.show() def onContactsOpen(self): "Callback, triggered on Broadcast" self.selContact.focus_set() def onCommandComposeLetter(self): "Redirect to MainView" self.createMainView() self.mainview.onCommandComposeLetter() def close(self, event=None): self.tkroot.quit() self.top.destroy() def window(self): "Returns Tk's TopLevel Widget" return self.top def withdraw(self): "Withdraw: Forward to TopLevel Method" self.top.withdraw() def deiconify(self): "DeIconify: Forward to TopLevel Method" self.top.deiconify() PyCoCuMa-0.4.5-6/pycocumalib/RadioSelectDialog.py0000644000175000017500000000276410252657644022624 0ustar henninghenning00000000000000#!/usr/bin/python # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: RadioSelectDialog.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import Pmw class RadioSelectDialog(Pmw.Dialog): # exporttargets e.g.: [("csv","Comma Separated Values"),("latex","LaTeX)] def __init__(self, master, exporttargets, title="", headline="", class_=None): Pmw.Dialog.__init__(self, master=master, title=title, buttons=('Ok','Cancel'), defaultbutton='Ok') lbl = Label(self.interior(), text=headline) lbl.grid(padx=2, pady=2) self.targetvar = StringVar() self.targetvar.set(exporttargets[0][0]) for target in exporttargets: lbl = Radiobutton(self.interior(), text=target[1], value = target[0], variable = self.targetvar) lbl.grid(sticky=W, padx=2, pady=2) def getvalue(self): return self.targetvar.get() if __name__ == "__main__": # Unit Test: tk = Tk() exporttargets = [("csv","Comma Separated Values"), ("dat","Future Data Format")] dlg = RadioSelectDialog(tk, exporttargets, title="Test", headline="Export To:") print dlg.activate() tk.destroy() PyCoCuMa-0.4.5-6/pycocumalib/Set.py0000644000175000017500000000404310252657644020031 0ustar henninghenning00000000000000""" Simple Set Class Note that we cannot use the sets.Set class of Python2.3 here, because we want items() as sorted list """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: Set.py 82 2004-07-11 13:01:44Z henning $ def union(s1, s2): import copy result = copy.copy(s1) for item in s2: result.add(item) return result class Set: def __init__(self, *args): self._dict = {} for arg in args: self.add(arg) def __repr__(self): import string elems = map(repr, self._dict.keys()) elems.sort() return "%s(%s)" % (self.__class__.__name__, string.join(elems, ", ")) def assign(self, other): import copy self._dict = copy.copy(other._dict) def extend(self, args): "Add several items at once." for arg in args: self.add(arg) def add(self, item): "Add one item to the set." self._dict[item] = item def remove(self, item): "Remove an item from the set." del self._dict[item] def contains(self, item): "Check whether the set contains a certain item." return self._dict.has_key(item) # High-Performance membership test for Python 2.0+ __contains__ = contains def __getitem__(self, index): "Support the 'for item in set:' proto." return self._dict.keys()[index] def __iter__(self): "Better support 'for item in set:' via Py2.2 iterators" return iter(self._dict.copy()) def __len__(self): "Return the number of items in the set." return len(self._dict) def items(self): "Return a list containing all items in sorted order, if possible." result = self._dict.keys() try: result.sort() except: pass return result PyCoCuMa-0.4.5-6/pycocumalib/SplashScreen.py0000644000175000017500000000630010252657644021666 0ustar henninghenning00000000000000""" Splash Screen for Start-Up """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: SplashScreen.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import IconImages import __version__ import Pmw class SplashScreen: def __init__(self, tkroot=None, title="PyCoCuMa Splash Screen", class_=None): if not tkroot: tkroot = Pmw.initialise() tkroot.withdraw() self.tkroot = tkroot if class_: self.top = Toplevel(tkroot, class_=class_) else: self.top = Toplevel(tkroot) if title: self.top.title(title) self.top.iconname(title) bgcolor = "#14143c" fgcolor = "#eaeafc" self.top["bg"] = bgcolor IconImages.createIconImages() frm = Frame(self.top, borderwidth=2, bg=bgcolor) frm.pack() lbl = Label(frm, image=IconImages.IconImages["pycocuma_title"], padx=0, pady=0, borderwidth=0) lbl.grid(columnspan=2) self.lblStatus = Label(frm, text="Loading...", justify=LEFT, font=("Helvetica", -12), bg=bgcolor, fg=fgcolor) self.lblStatus.grid(row=1,column=0,sticky=W) lbl = Label(frm, text="version %s" % __version__.__version__, justify=RIGHT, font=("Helvetica", -12), bg=bgcolor, fg=fgcolor) lbl.grid(row=1,column=1,sticky=E) self.top.overrideredirect(1) self.top.withdraw() def centerWindow(self, relx=0.5, rely=0.3): "Center the Main Window on Screen" widget = self.top master = self.tkroot widget.update_idletasks() # Actualize geometry information if master.winfo_ismapped(): m_width = master.winfo_width() m_height = master.winfo_height() m_x = master.winfo_rootx() m_y = master.winfo_rooty() else: m_width = master.winfo_screenwidth() m_height = master.winfo_screenheight() m_x = m_y = 0 w_width = widget.winfo_reqwidth() w_height = widget.winfo_reqheight() x = m_x + (m_width - w_width) * relx y = m_y + (m_height - w_height) * rely if x+w_width > master.winfo_screenwidth(): x = master.winfo_screenwidth() - w_width elif x < 0: x = 0 if y+w_height > master.winfo_screenheight(): y = master.winfo_screenheight() - w_height elif y < 0: y = 0 widget.geometry("+%d+%d" % (x, y)) widget.deiconify() # Become visible at the desired location def show(self): self.centerWindow(relx=0.5,rely=0.5) self.top.update() def update(self, statusstr): self.lblStatus["text"] = statusstr self.top.update() def destroy(self): self.top.destroy() if __name__ == "__main__": # Unit Test: dlg = SplashScreen() dlg.show() dlg.tkroot.mainloop() PyCoCuMa-0.4.5-6/pycocumalib/StringSetDialog.py0000644000175000017500000000524710252657644022347 0ustar henninghenning00000000000000# Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: StringSetDialog.py 82 2004-07-11 13:01:44Z henning $ from Tkinter import * import Pmw class StringSetDialog(Pmw.Dialog): def __init__(self, master, title="", available_strings=[], selected_strings=[], allowmultiselect=0, **kws): Pmw.Dialog.__init__(self, master=master, title=title, buttons=('Ok','Cancel'), defaultbutton='Ok', **kws) self.allowmultiselect = allowmultiselect self.interior().columnconfigure(0, weight=1) self.interior().columnconfigure(2, weight=1) self.interior().rowconfigure(1, weight=1) self.interior().rowconfigure(2, weight=1) lbl = Label(self.interior(), text="Available:") lbl.grid(padx=2, pady=2) lbl = Label(self.interior(), text="Selected:") lbl.grid(row=0, column=2, padx=2, pady=2) b = Button(self.interior(), text="Add >>", command=self.add) b.grid(row=1, column=1, padx=2, pady=2, sticky=W+E+S) b = Button(self.interior(), text="<< Remove", command=self.remove) b.grid(row=2, column=1, padx=2, pady=2, sticky=W+E+N) self.avail_listbox = avail = Pmw.ScrolledListBox(self.interior(), listbox_highlightthickness=0) avail.grid(row=1, rowspan=2, column=0, padx=2, pady=2, sticky=W+E+S+N) self.sel_listbox = sel = Pmw.ScrolledListBox(self.interior(), listbox_highlightthickness=0) sel.grid(row=1, rowspan=2, column=2, padx=2, pady=2, sticky=W+E+S+N) avail.setlist(available_strings[:]) sel.setlist(selected_strings[:]) def add(self): list = self.sel_listbox.get() item = self.avail_listbox.get(ACTIVE) if not item in list or self.allowmultiselect: self.sel_listbox.insert(END, item) def remove(self): self.sel_listbox.delete(ACTIVE) def getvalue(self): return self.sel_listbox.get() if __name__ == "__main__": # Unit Test: tk = Tk() avail = ["Orange", "Cherry", "Apple", "Banana"] sel = ["Orange", "Apple"] dlg = StringSetDialog(tk, title="Test", available_strings=avail, selected_strings=sel) def printres(btn, dlg=dlg): if btn=='Ok': print dlg.getvalue() dlg.deactivate() dlg['command'] = printres dlg.activate() tk.destroy() PyCoCuMa-0.4.5-6/pycocumalib/TemplateProcessor.py0000644000175000017500000001062310252657644022752 0ustar henninghenning00000000000000#!/usr/bin/python """ Process Template Files (based on Yet Another Python Templating Utility by Alex Martelli) """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: TemplateProcessor.py 82 2004-07-11 13:01:44Z henning $ import re, sys import types class _nevermatch: def match(self, line): return None def sub(self, repl, line): return line _never = _nevermatch() def identity(string, why=None): return string def nohandle(string, kind): import traceback traceback.print_exc() return "*** Exception raised in %s '%s'\n" % (kind, string) class TemplateProcessor: def __init__(self, postproc=identity, preproc=identity, handle=nohandle): self.reexpr = re.compile('<\?(=|!|#)([^?]+)\?>') self.restart = re.compile('\s*<\?([^}][^?]+){\?>') self.reend = re.compile('\s*<\?}\?>') self.recont = re.compile('\s*<\?}([^{]+){\?>') self.locals = {'_pb':self.process_block} self.postproc = postproc self.preproc = preproc self.handle = handle def process(self, block, outfunc, globals={}): self.outfunc = outfunc self.globals = globals self.locals['_bl'] = block self.process_block() def process_block(self, i=0, last=None): "Process lines [i, last) of block" def repl(match, self=self): cmd = match.group(1) if cmd == '#': # Comment return '' else: expr = self.preproc(match.group(2), 'eval') try: ret = eval(expr, self.globals, self.locals) except: return self.postproc(self.handle(expr, 'eval')) if cmd == '=': # Ensure that our Expression is a String: if not isinstance(ret, types.StringTypes): ret = str(ret) return self.postproc(ret) else: # ! return '' block = self.locals['_bl'] if last is None: last=len(block) while i < last: line = block[i] match = self.restart.search(line) if match: # a statement starts 'here' # i is the last line NOT to process stat = match.group(1)+':' j = i+1 nest = 1 while j < last: line = block[j] if self.reend.match(line): nest += -1 if nest == 0: break elif self.restart.match(line): nest += 1 elif nest == 1: match = self.recont.match(line) if match: # found a continued statement nestat = match.group(1)+':' stat = '%s _pb(%s,%s)\n%s' % (stat, i+1, j, nestat) i = j # i is the last line NOT to process j += 1 stat = self.preproc(stat, 'exec') stat = '%s _pb(%s,%s)' % (stat, i+1, j) try: exec stat in self.globals, self.locals except: self.outfunc(self.postproc(self.handle(stat, 'exec'))) i = j+1 else: self.outfunc(self.reexpr.sub(repl, line)) i += 1 if __name__ == "__main__": dummytemplate = """ a: , b: , foo: A Comment (invisible): A will be changed now! a: , b: , foo: Hallo? i: Zwei Drei Sonst Continues, but here is the END. """.splitlines(True) globals = {} def changeA(globals=globals): globals['a'] = 2004 def foo(): return 'foo returned by foo() function' globals.update({'Name':'A Dummy Template', 'a':42, 'b':'\xdcmlaute', 'changeA':changeA, 'foo':foo}) proc = TemplateProcessor() proc.process(dummytemplate, sys.stdout.write, globals) PyCoCuMa-0.4.5-6/pycocumalib/ToolTip.py0000644000175000017500000000606110252657644020672 0ustar henninghenning00000000000000""" Balloon Help Ideas gleaned from PySol """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: ToolTip.py 82 2004-07-11 13:01:44Z henning $ import os from Tkinter import * class ToolTipBase: def __init__(self, button): if hasattr(button, 'toolTipMaster'): # toolTipMaster returns widget to use for balloon help: self.button = button.toolTipMaster() else: self.button = button self.tipwindow = None self.id = None self.x = self.y = 0 self._id1 = self.button.bind("", self.enter) self._id2 = self.button.bind("", self.leave) self._id3 = self.button.bind("", self.leave) def enter(self, event=None): self.schedule() def leave(self, event=None): self.unschedule() self.hidetip() def schedule(self): self.unschedule() self.id = self.button.after(1000, self.showtip) def unschedule(self): id = self.id self.id = None if id: self.button.after_cancel(id) def showtip(self): if self.tipwindow: return # The tip window must be completely outside the button; # otherwise when the mouse enters the tip window we get # a leave event and it disappears, and then we get an enter # event and it reappears, and so on forever :-( x = self.button.winfo_rootx() + 20 y = self.button.winfo_rooty() + self.button.winfo_height() + 1 self.tipwindow = tw = Toplevel(self.button) tw.wm_overrideredirect(1) tw.wm_geometry("+%d+%d" % (x, y)) self.showcontents() def showcontents(self, text="Your text here"): # Override this in derived class label = Label(self.tipwindow, text=text, justify=LEFT, background="#ffffe0", relief=SOLID, borderwidth=1) label.pack() def hidetip(self): tw = self.tipwindow self.tipwindow = None if tw: tw.destroy() class ToolTip(ToolTipBase): def __init__(self, button, text): ToolTipBase.__init__(self, button) self.text = text def showcontents(self): ToolTipBase.showcontents(self, self.text) class ListboxToolTip(ToolTipBase): def __init__(self, button, items): ToolTipBase.__init__(self, button) self.items = items def showcontents(self): listbox = Listbox(self.tipwindow, background="#ffffe0") listbox.pack() for item in self.items: listbox.insert(END, item) def main(): # Test code root = Tk() b = Button(root, text="Hello", command=root.destroy) b.pack() root.update() tip = ListboxToolTip(b, ["Hello", "world"]) # root.mainloop() # not in idle if __name__ == "__main__": main() PyCoCuMa-0.4.5-6/pycocumalib/__init__.py0000644000175000017500000000013310252657644021031 0ustar henninghenning00000000000000# $Id: __init__.py 82 2004-07-11 13:01:44Z henning $ # Dummy file to make this a package PyCoCuMa-0.4.5-6/pycocumalib/__version__.py0000644000175000017500000000012310311641375021540 0ustar henninghenning00000000000000# $Id: __version__.py 143 2005-09-13 21:22:05Z henning $ __version__ = "0.4.5-6" PyCoCuMa-0.4.5-6/pycocumalib/broadcaster.py0000644000175000017500000000401010252657644021561 0ustar henninghenning00000000000000# Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: broadcaster.py 82 2004-07-11 13:01:44Z henning $ __all__ = ['Register', 'UnRegister', 'Broadcast', 'CurrentSource', 'CurrentTitle', 'CurrentData'] import debug listeners = {} currentSources = [] currentTitles = [] currentData = [] def Register(listener, arguments=(), source=None, title=None): if not listeners.has_key((source, title)): listeners[(source, title)] = [] listeners[(source, title)].append((listener, arguments)) def UnRegister(listener, arguments=(), source=None, title=None): try: lst = listeners[(source, title)] del lst[lst.index((listener, arguments))] except: debug.echo("broadcaster.UnRegister(): failed to unregister listener") lastbroadcast = None def Broadcast(source, title, data={}, onceonly=0): "Broadcast an Event / Notification to all registered Listeners" global lastbroadcast if onceonly and lastbroadcast == (source, title, data): # We don't want to let this broadcast be repeated return debug.echo("broadcaster.Broadcast(): %s %s %s" % (source, title, str(data))) currentSources.append(source) currentTitles.append(title) currentData.append(data) listenerList = listeners.get((source, title), [])[:] if source != None: listenerList += listeners.get((None, title), []) if title != None: listenerList += listeners.get((source, None), []) for listener, arguments in listenerList: listener(*arguments) currentSources.pop() currentTitles.pop() currentData.pop() lastbroadcast = (source, title, data) def CurrentSource(): return currentSource[-1] def CurrentTitle(): return currentTitles[-1] def CurrentData(): return currentData[-1] PyCoCuMa-0.4.5-6/pycocumalib/broker.py0000644000175000017500000000214010252657644020556 0ustar henninghenning00000000000000# Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: broker.py 82 2004-07-11 13:01:44Z henning $ __all__ = ['Register', 'UnRegister', 'Request', 'CurrentTitle', 'CurrentData'] import debug providers = {} currentTitles = [] currentData= [] def Register(title, provider, arguments=()): assert not providers.has_key(title) providers[title] = (provider, arguments) def UnRegister(title): try: del providers[title] except: debug.echo("broker.UnRegister(): failed to unregister provider for '%s'" % title) def Request(title, data={}): currentTitles.append(title) currentData.append(data) result = apply(apply, providers.get(title)) currentTitles.pop() currentData.pop() return result def CurrentTitle(): return currentTitles[-1] def CurrentData(): return currentData[-1] PyCoCuMa-0.4.5-6/pycocumalib/converters.py0000644000175000017500000003130310252657644021467 0ustar henninghenning00000000000000#!/usr/bin/python """ Import and Export model """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: converters.py 82 2004-07-11 13:01:44Z henning $ import vcard import debug import types class EncodedFileWriter: "Takes Unicode Strings and writes them encoded to the real file." def __init__(self, realfile, encoding): self._fd = realfile self._encoding = encoding def write(self, val): if isinstance(val, types.UnicodeType): self._fd.write(val.encode(self._encoding, 'replace')) else: self._fd.write(val) def close(self): self._fd.close() def export2vcard(model, outfile, encoding, cardhandles=None): "Simply fetch raw vCard Data from Server and write to File" if cardhandles is None: cardhandles=model.ListHandles() cardlist = vcard.vCardList() for handle in cardhandles: cardlist.add(model.GetContact(handle)) outfile.write(cardlist.VCF_repr()) def export2xmlrdf(model, outfile, encoding, cardhandles=None): "Export to vCard-RDF (XML) Data" if cardhandles is None: cardhandles=model.ListHandles() cardlist = vcard.vCardList() for handle in cardhandles: cardlist.add(model.GetContact(handle)) outfile.write(cardlist.XML_repr()) def export2csv(model, outfile, encoding, cardhandles=None, fieldnames=vcard.FIELDNAMES): "Export to Comma Separated Values" import csv writer = csv.writer(outfile) # local copy: fieldnames = fieldnames[:] # Hack: for field in ["Phone", "Email"]: idx = fieldnames.index(field) for i in range(3): fieldnames.insert(idx+i+1, "%s %d" % (field, i+2)) writer.writerow(fieldnames) if cardhandles is None: cardhandles=model.ListHandles() for handle in cardhandles: row = [] for field in fieldnames: val = model.GetContact(handle).getFieldValueStr(field).encode(encoding, 'replace') # Newline break things: val = val.replace('\n', r'\n').replace('\r', r'\r') row.append(val) writer.writerow(row) def export2latex(model, outfile, encoding='latin-1', cardhandles=None): "Export to Fancy LaTeX Pages" # Subset of vcard.FIELDNAMES: fields2export = [ "FormattedName", #"DisplayName", #"Family", #"Given", #"Additional", #"Prefixes", #"Suffixes", #"NickName", "Birthday", "Organization", "Units", "Title", "Role", "Phone", "Email", #"Mailer", "POBox", "Extended", "Street", "PostalCode", "City", "Region", "Country", #"Label", "Note", "Categories", #"SortName", #"SortString", "URL", #"Key", "TimeZone", "GlobalPosition", #"Rev", "UID" ] template = r"""\documentclass[10pt]{article} \usepackage[a4paper]{geometry} \usepackage[latin1]{inputenc} \usepackage{color} \definecolor{light-gray}{gray}{0.75} \usepackage{multicol} \usepackage{fancyhdr} \usepackage{times} % Select Helvetica as default font: \renewcommand{\familydefault}{phv} \pagestyle{fancy} \rhead{\tt\scriptsize vCards converted to LaTeX by PyCoCuMa v on } \cfoot{\thepage} \def\begincard#1{ \noindent\begin{minipage}{\linewidth} \parindent = 0mm \dimen1=\linewidth \advance\dimen1 by -2\fboxsep \setbox0=\hbox to \dimen1{\small #1 \hfill} \dp0=0pt \colorbox{light-gray}{\box0} \vskip 1mm \bgroup\scriptsize} \def\endcard{\egroup \end{minipage}\vskip 2mm} \begin{document} \begin{multicols}{3} \begincard{} \endcard \end{multicols} \end{document} """.splitlines(True) def texescape(str): "Escape special TeX characters" return str.replace('&','\&').replace('_','\_').replace('$','\$').replace('#','\#') if cardhandles is None: cardhandles=model.ListHandles() globals = {'ret':'', 'cardidx':0} def nextCard(globals=globals, cardhandles=cardhandles): idx = globals['cardidx'] if idx >= len(cardhandles): return False else: globals['card'] = model.GetContact(cardhandles[idx]) globals['cardidx'] += 1 return True def getFieldValue(str, globals=globals): return globals['card'].getFieldValueStr(str) def printLines(globals=globals): card = globals['card'] ret = '' addresses = [] for field in fields2export: valcount = 0 val = card.getFieldValueStr(field, default=None) while val is not None: valcount += 1 if field in vcard.ADRFIELDS: if len(addresses) < valcount: addresses.append({}) adr = addresses[valcount-1] adr[field] = val if len(adr) >= len(vcard.ADRFIELDS): zeilen = [adr["POBox"], adr["Extended"], adr["Street"], " ".join(filter(None, [adr["PostalCode"], adr["City"]])), ", ".join(filter(None, [adr["Region"], adr["Country"]]))] for z in filter(None, zeilen): ret = ret + " "+ z + "\par\n" elif val: ret = ret + " "+ val + "\par\n" val = card.getFieldValueStr(field, default=None) return ret import __version__ import time globals.update({'Version': __version__.__version__, 'DateTime':time.asctime(), 'NextCard':nextCard, 'GetFieldValue':getFieldValue, 'PrintLines':printLines}) def outfunc(str, outfile=outfile): outfile.write(str) from TemplateProcessor import TemplateProcessor proc = TemplateProcessor(postproc=texescape) proc.process(template, outfunc, globals) def export2pine(model, outfile, encoding, cardhandles=None): "Export to Pine Mail Addressbook" if cardhandles is None: cardhandles=model.ListHandles() # Support up to three defined email-addresses per contact: rows = model.QueryAttributes(cardhandles, ('NickName','DisplayName','Email 1','Email 2','Email 3')) for row in rows: # rows are immutable tuples, make them mutable: row = list(row) if row[2]: # do split()[0] on each email, because there may follow # params: (pref, internet) for example: emails = map(lambda x: x.split()[0], filter(None, row[2:])) row[2] = ', '.join(emails) # More than one Email-Addr, so enclose with brackets: if len(emails)>1: row[2] = "(%s)" % row[2] outfile.write('\t'.join(row[:3]) + '\n') def importFvcard(model, infile, encoding): "Import from VCF-File" importcards = vcard.vCardList() importcards.LoadFromStream(infile, encoding) for cardhdl in importcards.sortedlist(): handle = model.NewContact() model.PutContact(handle, importcards[cardhdl].VCF_repr()) def importFcsv(model, infile, encoding): "Import from CSV-Table" import csv reader = csv.reader(infile) firstrow = None for row in reader: if firstrow is None: firstrow = row unknownfields = filter(lambda x: x[0] not in vcard.FIELDNAMES, zip(firstrow, range(len(firstrow)))) if unknownfields: from FieldMappingDialog import FieldMappingDialog dlg = FieldMappingDialog(None, [field for field, no in unknownfields], title='Map Fields', headline='Please map the unknown fields to vCard fields.\n'+ 'Unmapped fields will be ignored.') dlg.activate() for mappedto, i in zip(dlg.getvalue(), [no for field, no in unknownfields]): firstrow[i] = mappedto else: handle = model.NewContact() try: card = model.GetContact(handle) for i in range(len(row)): # firstrow must have at least len(row) elements: if row[i] and firstrow[i]: card.setFieldValueStr(firstrow[i], unicode(row[i], encoding, 'replace')) model.PutContact(handle, card.VCF_repr()) except: # Something bad happened, don't leave our DB inconsistent: model.DelContact(handle) raise exportfunctions = { "vcard":export2vcard, "xmlrdf":export2xmlrdf, "csv": export2csv, "latex":export2latex, "pine": export2pine} def exportvcards(model, targetformat, outfile, encoding, cardhandles=None): "Chooses the right exportfunction according to targetformat" func = exportfunctions.get(targetformat) if func: return func(model, outfile, encoding, cardhandles) else: return None exportfiletypes = { "vcard":("vCard", "*.vcf"), "xmlrdf":("XML Files", "*.xml"), "csv": ("Comma Separated Text", "*.csv"), "latex":("LaTeX Document", "*.tex"), "pine": ("Pine Addressbook", "*")} def askexportfile(targetformat, master=None): "Ask for Export Filename" import tkFileDialog, os try: dir = os.getcwd() except: dir = "" dlg = tkFileDialog.SaveAs(master, filetypes=[exportfiletypes[targetformat]]) fname = dlg.show(initialdir=dir, initialfile="") if fname: root, ext = os.path.splitext(fname) if root and not ext: ext = exportfiletypes[targetformat][1][1:] return root+ext else: return None importfunctions = { "vcard":importFvcard, "csv": importFcsv} def importvcards(model, srcformat, infile, encoding): "Chooses the right importfunction according to srcformat" func = importfunctions.get(srcformat) if func: return func(model, infile, encoding) else: return None importfiletypes = { "vcard":("vCard", "*.vcf"), "csv": ("Comma Separated Text", "*.csv")} def askimportfile(srcformat, master=None): "Ask for Import Filename" import tkFileDialog, os try: dir = os.getcwd() except: dir = "" dlg = tkFileDialog.Open(master, filetypes=[importfiletypes[srcformat], ("All Files", "*")], initialdir=dir, initialfile="") return dlg.show() importformats = [ ("vcard","vCard 3.0 File (*.vcf)"), ("csv", "Comma Separated Values (*.csv)")] def Import(master, model, srcformat=None): "Show Import Dialog and do importing" if srcformat is None: from ImportExportDialog import ImportExportDialog dlg = ImportExportDialog(master, importformats, title="Import Contacts", headline="Import from:") res = dlg.activate() if res == 'Ok': dlg.deactivate() srcformat, srcencoding = dlg.getvalue() else: return filename = askimportfile(srcformat, master) if filename: fd = file(filename, 'rb') importvcards(model, srcformat, fd, srcencoding) fd.close() return True else: return False exporttargets = [ ("vcard","vCard 3.0 File (*.vcf)"), ("xmlrdf","XML-RDF (*.xml)"), ("csv", "Comma Separated Values (*.csv), MSExcel compatible"), ("latex","LaTeX Document (*.tex)"), ("pine", "Pine Addressbook")] def Export(master, model, targetformat=None, cardhandles=None): "Show Export Dialog and do exporting" if targetformat is None: from ImportExportDialog import ImportExportDialog if cardhandles and len(cardhandles) == 1: title = "Export Single Contact" else: title = "Export Contacts" dlg = ImportExportDialog(master, exporttargets, title=title, headline="Export to:") res = dlg.activate() if res == 'Ok': dlg.deactivate() targetformat, targetencoding = dlg.getvalue() else: return # For py2exe ModuleFinder: import codecs from encodings import latin_1 filename = askexportfile(targetformat, master) if filename: fd = open(filename, "wb") outfile = EncodedFileWriter(fd, targetencoding) exportvcards(model, targetformat, outfile, targetencoding, cardhandles) fd.close() PyCoCuMa-0.4.5-6/pycocumalib/createIconImages.py0000755000175000017500000000251110252657644022501 0ustar henninghenning00000000000000#!/usr/bin/python """ Creates IconImages.py from ../images/*.gif """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: createIconImages.py 82 2004-07-11 13:01:44Z henning $ import base64 import glob import os imagefilenames = glob.glob("../images/*.gif") fd = open("IconImages.py", "w") fd.write(""" from Tkinter import * IconImages = {} IconImagesGrey = {} """) for fname in imagefilenames: iconname = os.path.splitext(os.path.basename(fname.lower()))[0] fd.write("\n"+ iconname + "_data = '''\\\n" + # Note: On Windows: 'rb' (binary read) is required! base64.encodestring(open(fname, "rb").read()) + "'''") fd.write(""" def createIconImages(): if not IconImages: for key,value in zip(globals().keys(), globals().values()): if key[-5:] == "_data": icon = key[:-5] if icon[-5:] == "_grey": icon = icon[:-5] IconImagesGrey[icon] = PhotoImage(data=value) else: IconImages[icon] = PhotoImage(data=value) """) fd.close() PyCoCuMa-0.4.5-6/pycocumalib/debug.py0000644000175000017500000000403210252657644020362 0ustar henninghenning00000000000000""" Debugging Helper Functions """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: debug.py 82 2004-07-11 13:01:44Z henning $ import types, string, sys from traceback import * class nullOutput: "A do-nothing file (output) class" def write(self, var): pass stdout = sys.stdout or nullOutput() traceOutput = stdout watchOutput = stdout rawOutput = stdout def watch(variableName): if __debug__: stack = extract_stack()[-2:][0] actualCall = stack[3] if actualCall is None: actualCall = "watch([unknown])" left = string.find(actualCall, "(") right = string.rfind(actualCall, ")") paramDict = {} paramDict["varName"] = string.strip( actualCall[left+1:right]) paramDict["varType"] = str(type(variableName))[7:-2] paramDict["value"] = repr(variableName) paramDict["methodName"] = stack[2] paramDict["lineNumber"] = stack[1] paramDict["fileName"] = stack[0] outStr = 'File "%(fileName)s", line %(lineNumber)d, in' \ ' %(methodName)s\n %(varName)s <%(varType)s>' \ ' = %(value)s\n\n' watchOutput.write(outStr % paramDict) def trace(text): if __debug__: stack = extract_stack()[-2:][0] paramDict = {} paramDict["methodName"] = stack[2] paramDict["lineNumber"] = stack[1] paramDict["fileName"] = stack[0] paramDict["text"] = text outStr = 'File "%(fileName)s", line %(lineNumber)d, in' \ ' %(methodName)s\n %(text)s\n\n' traceOutput.write(outStr % paramDict) def raw(text): if __debug__: try: rawOutput.write(text) except IOError: pass def echo(text): raw(text+"\n") PyCoCuMa-0.4.5-6/pycocumalib/faxtowrapper.py0000644000175000017500000000352410252657644022023 0ustar henninghenning00000000000000#!/usr/bin/python """ Extension to send a Fax """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: faxtowrapper.py 82 2004-07-11 13:01:44Z henning $ import sys import os import Preferences def faxto(recipient, telnumber): faxprog = Preferences.get("client.faxto_program") if sys.platform == 'win32': if not faxprog: # Get default fax-application from Windows-Registry: # TODO: Howto fax on Windows???? import _winreg handle = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\Classes\\faxto\shell\open\command') valname, faxprog, valtype = _winreg.EnumValue(handle, 0) args = faxprog.split(' ') # Remove dbl-quotes if any: if args[0][0] == '"' and args[0][-1] == '"': args[0] = args[0][1:-1] args[0] = os.path.abspath(args[0]) for i in range(1, len(args)): args[i] = args[i].replace('%1', recipient) args[i] = args[i].replace('%2', telnumber) os.spawnv(os.P_NOWAIT, args[0], args) else: if not faxprog: # Ugly fallback: # (xterm, vi and fax should be available on every UNIX-machine): faxprog = 'xterm | -e | sh | -c | vi /tmp/pycocuma_fax.txt && fax send %2 /tmp/pycocuma_fax.txt' args = faxprog.split(' | ') else: args = faxprog.split(' ') for i in range(1, len(args)): args[i] = args[i].replace('%1', recipient) args[i] = args[i].replace('%2', telnumber) os.spawnvp(os.P_NOWAIT, args[0], args) PyCoCuMa-0.4.5-6/pycocumalib/keydefs.py0000644000175000017500000000146110311640650020712 0ustar henninghenning00000000000000# Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: keydefs.py 142 2005-09-13 21:16:23Z henning $ windows_keydefs = \ {'<>': [''], '<>': [''], '<>': [''], '<>': [''], '<>': [''], '<>': [''], '<>': [''], '<>': [''], '<>': [''], '<>': [''], '<>': ['']} unix_keydefs = windows_keydefs PyCoCuMa-0.4.5-6/pycocumalib/mailtowrapper.py0000644000175000017500000000320510252657644022163 0ustar henninghenning00000000000000#!/usr/bin/python """ Extension to send Email """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: mailtowrapper.py 82 2004-07-11 13:01:44Z henning $ import sys import os import Preferences def mailto(recipient): # recipient should be a string like: 'Henning Jacobs ' mailprog = Preferences.get("client.mailto_program") if sys.platform == 'win32': if not mailprog: # Get default mail-application from Windows-Registry: import _winreg handle = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\Classes\mailto\shell\open\command') valname, mailprog, valtype = _winreg.EnumValue(handle, 0) args = mailprog.split(' ') # Remove dbl-quotes if any: if args[0][0] == '"' and args[0][-1] == '"': args[0] = args[0][1:-1] args[0] = os.path.abspath(args[0]) for i in range(1, len(args)): args[i] = args[i].replace('%1', recipient) os.spawnv(os.P_NOWAIT, args[0], args) else: if not mailprog: # Ugly fallback: # (xterm and mail should be available on every UNIX-machine): mailprog = 'xterm -e mail %1' args = mailprog.split(' ') for i in range(1, len(args)): args[i] = args[i].replace('%1', recipient) os.spawnvp(os.P_NOWAIT, args[0], args) PyCoCuMa-0.4.5-6/pycocumalib/pycocuma.py0000644000175000017500000000163010252657644021115 0ustar henninghenning00000000000000#!/usr/bin/python """\ PyCoCuMa (Pythonic Contact and Customer Management) provides an personal information system for addresses, telephone numbers and other data associated with personal contacts. PyCoCuMa stores it's data in compatible vCard files (*.vcf). PyCoCuMa was programmed by Henning Jacobs . PyCoCuMa's Homepage: http://www.srcco.de """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: pycocuma.py 82 2004-07-11 13:01:44Z henning $ def run(): from pycocumalib.MainController import MainController mainctrl = MainController() mainctrl.Run() mainctrl.CleanUp() if __name__ == "__main__": run() PyCoCuMa-0.4.5-6/pycocumalib/testCoCuMa_Server.py0000644000175000017500000001022710252657644022634 0ustar henninghenning00000000000000""" Unit Test Case """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: testCoCuMa_Server.py 82 2004-07-11 13:01:44Z henning $ import unittest from CoCuMa_Server import CoCuMa_Server from vcard import vCard from vcalendar import vEvent from testvcard import fillDemoCard from testvcalendar import fillDemoJournal class CoCuMa_ServerTestCase(unittest.TestCase): TESTFN = '*CoCuMa_Server TestCase Card*' TESTCATEGORIES = 'TestCase, CoCuMa_Server, Private' TESTFN2 = 'Will be deleted soon..' TESTCATEGORIES2 = u'Delete me, TestCase, Umlaut\xe4' def setUp(self): import tempfile self.tmpabkfilename = tempfile.mktemp() self.tmpcalfilename = tempfile.mktemp() self.democard = vCard() fillDemoCard(self.democard) self.demojour = vEvent() fillDemoJournal(self.demojour) self.server = CoCuMa_Server(self.tmpabkfilename, self.tmpcalfilename) self.server.SessionInit() def tearDown(self): "delete our temporary addressbook and calendar files" self.server.SessionQuit() self.server._shutdown() import os try: os.unlink(self.tmpabkfilename) os.unlink(self.tmpcalfilename) except OSError: # Ignore non-existing files pass def testEmptyDB(self): "operations on empty database" handles = self.server.ListHandles() self.assertEqual(handles, []) jourhdl = self.server.NewJournal() self.server.DelJournal(jourhdl) handle = self.server.NewContact() self.server.PutContact(handle, self.democard.VCF_repr()) self.server.DelContact(handle) handles = self.server.ListHandles() self.assertEqual(handles, []) def testLoadSave(self): "saving and loading database" # Create new card with some inital values set: handle = self.server.NewContact({ 'fn' : self.TESTFN, 'categories': self.TESTCATEGORIES}) # Now Add a Calendar Entry: jourhdl = self.server.NewJournal() self.server.PutJournal(jourhdl, self.demojour.VCF_repr()) # Create another contact: temphandle = self.server.NewContact({ 'fn' : self.TESTFN2, 'categories': self.TESTCATEGORIES2}) # handles are sorted (by DisplayName): handles = self.server.ListHandles() self.assertEqual([handle, temphandle], handles) # Query single field: attrs = self.server.QueryAttributes(handles, "FormattedName") self.assertEqual(attrs[0], self.TESTFN) # Query multiple fields: attrs = self.server.QueryAttributes(handles, ("FormattedName", "Categories")) self.assertEqual(attrs[0][0], self.TESTFN) self.assertEqual(attrs[0][1], self.TESTCATEGORIES) # Push our Demo Card to the self.server: self.server.PutContact(handle, self.democard.VCF_repr()) # Save to file: self.server.SessionQuit() self.server._shutdown() self.server = None # Now try again (this time the file has to be loaded from disk): self.server = CoCuMa_Server(self.tmpabkfilename, self.tmpcalfilename) self.server.SessionInit() handles = self.server.ListHandles() # Query multiple fields: attrs = self.server.QueryAttributes(handles, ("FormattedName", "Categories")) self.assertEqual(attrs[1][0], self.TESTFN2) self.assertEqual(attrs[1][1], self.TESTCATEGORIES2) # Delete our second card: self.server.DelContact(handles[1]) # now try our first one: data = self.server.GetContact(handles[0]) self.assertEqual(data, self.democard.VCF_repr()) # Check if Journal is still there: data = self.server.GetJournal(jourhdl) self.assertEqual(data, self.demojour.VCF_repr()) if __name__ == "__main__": unittest.main() PyCoCuMa-0.4.5-6/pycocumalib/testMainController.py0000644000175000017500000000704010252657644023126 0ustar henninghenning00000000000000""" Unit Test Case """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: testMainController.py 82 2004-07-11 13:01:44Z henning $ import unittest from MainController import MainController from vcard import vCard, FIELDNAMES from testvcard import fillDemoCard import Preferences class MainControllerTestCase(unittest.TestCase): TESTFN = '*MainController TestCase Card %d*' TESTCATEGORIES = 'TestCase, MainController, Private' TESTFN2 = 'Will be deleted soon..' TESTCATEGORIES2 = u'Delete me, TestCase, Umlaut\xe4' def setUp(self): self.mainctrl = MainController(nogui=1, nouserpref=1) import tempfile self.tempfilename = tempfile.mktemp() self.democard = vCard() fillDemoCard(self.democard) def tearDown(self): self.mainctrl.CleanUp() import os try: os.unlink(self.tempfilename) os.unlink(self.tempfilename+'.ics') except OSError: pass def testContactOperationsXMLRPC(self): "some random operations on MainModel (stress test) using XMLRPC" # Use port 8811 instead of 8810 to avoid collision with # any running non-testing server: Preferences.set("client.connection_type", "xmlrpc") Preferences.set("client.connection_string", "http://localhost:8811") Preferences.set("client.autostart_server", "yes") Preferences.set("server.listen_host", "localhost") Preferences.set("server.listen_port", "8811") Preferences.set("server.addressbook_filename", self.tempfilename) Preferences.set("server.calendar_filename", self.tempfilename+'.ics') self.mainctrl.Run() self._testContactOperations() print "The test server is still running." print "Please kill the process manually!" def testContactOperationsFile(self): "some random operations on MainModel (stress test) using direct file" Preferences.set("client.connection_type", "file") Preferences.set("client.connection_string", self.tempfilename) Preferences.set("client.autostart_server", "no") self.mainctrl.Run() self._testContactOperations() def _testContactOperations(self): "some random operations on MainModel (stress test)" import random for j in range(3): handles = [] for i in range(10): hdl = self.mainctrl.model.NewContact({'fn':self.TESTFN % i}) handles.append(hdl) card = self.mainctrl.model.GetContact(hdl) self.assertEquals(card.fn.get(), self.TESTFN % i) for i in range(10): hdl = random.choice(handles) field = random.choice(FIELDNAMES) # Revision-DateTime will be altered by server: while field == "Rev": field = random.choice(FIELDNAMES) self.mainctrl.model.PutContact(hdl, self.democard.VCF_repr()) card = self.mainctrl.model.GetContact(hdl) self.assertEquals(card.getFieldValueStr(field), self.democard.getFieldValueStr(field)) for i in range(random.randint(0, 10)): self.mainctrl.model.DelContact(handles[i]) if __name__ == "__main__": unittest.main() PyCoCuMa-0.4.5-6/pycocumalib/testall.py0000755000175000017500000000164610252657644020757 0ustar henninghenning00000000000000#!/usr/bin/python """ Run all PyCoCuMa Unit Tests """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: testall.py 82 2004-07-11 13:01:44Z henning $ import unittest import testvcard import testvcalendar import testCoCuMa_Server import testMainController if __name__ == "__main__": suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(testvcard.vCardTestCase)) suite.addTest(unittest.makeSuite(testvcalendar.vCalendarTestCase)) suite.addTest(unittest.makeSuite(testCoCuMa_Server.CoCuMa_ServerTestCase)) suite.addTest(unittest.makeSuite(testMainController.MainControllerTestCase)) unittest.TextTestRunner(verbosity=2).run(suite) PyCoCuMa-0.4.5-6/pycocumalib/testvcalendar.py0000644000175000017500000001016710252657644022141 0ustar henninghenning00000000000000""" Unit Test Case """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: testvcalendar.py 82 2004-07-11 13:01:44Z henning $ import unittest from vcalendar import vEvent, vCalendar DEMOCOMMENT = u"""This is a simple Test Case for my vCalendar Python Module. Here comes german umlauts: \xe4\xc4\xfc\xdc\xf6\xd6\xdf (this string is Unicode and will be written to disk as Latin-1) Now some special vCalendar chars: \t (tab) ; : , ? (note the escaping!) """ RFCTESTDESCR1 = """Project xyz Review Meeting Minutes Agenda\n1. Review of project version 1.0 requirements. 2. Definition of project processes. 3. Review of project schedule. Participants: John Smith, Jane Doe, Jim Dandy -It was decided that the requirements need to be signed off by product marketing. -Project processes were accepted. -Project schedule needs to account for scheduled holidays and employee vacation time. Check with HR for specific dates. -New schedule will be distributed by Friday. -Next weeks meeting is cancelled. No meeting until 3/23.""" RFCTESTCAL1 = r"""BEGIN:VCALENDAR VERSION:2.0 PRODID:-//ABC Corporation//NONSGML My Product//EN BEGIN:VJOURNAL DTSTAMP:19970324T120000Z UID:uid5@host1.com ORGANIZER:MAILTO:jsmith@host.com STATUS:DRAFT CLASS:PUBLIC CATEGORIES:Project Report, XYZ, Weekly Meeting DESCRIPTION:Project xyz Review Meeting Minutes\n Agenda\n1. Review of project version 1.0 requirements.\n2. Definition of project processes.\n3. Review of project schedule.\n Participants: John Smith, Jane Doe, Jim Dandy\n-It was decided that the requirements need to be signed off by product marketing.\n-Project processes were accepted.\n -Project schedule needs to account for scheduled holidays and employee vacation time. Check with HR for specific dates.\n-New schedule will be distributed by Friday.\n- Next weeks meeting is cancelled. No meeting until 3/23. END:VJOURNAL END:VCALENDAR""" def fillDemoJournal(jour): "Completely fill a vEvent with demo values" jour.summary.set("Summary") jour.description.set("Description") jour.categories.set("Demo, Business, Private, TestCase") jour.comment.set(DEMOCOMMENT) jour.location.set("Somewhere on terra") jour.contact.set("John Smith, Ocean Drive 3, Atlantis") jour.dtstart.set("2004-02-22") jour.url.set("http://www.mydomain.nowhere") jour.uid.set("-//TEST//TestvEvent@mydomain.nowhere//123456789") class vCalendarTestCase(unittest.TestCase): def setUp(self): self.calendar = vCalendar() self.demojournal = vEvent() fillDemoJournal(self.demojournal) def testReadWrite(self): "writing and reading vCalendar from stream" self.calendar.add(self.demojournal) jourbefore = self.demojournal import StringIO stream = StringIO.StringIO() self.calendar.SaveToStream(stream) stream.seek(0) self.calendar.clear() self.calendar.LoadFromStream(stream) handle = self.calendar.sortedlist()[0] jourafter = self.calendar[handle] self.assertEqual(jourbefore.VCF_repr(), jourafter.VCF_repr()) def testFieldValue(self): "compare journal values with our demo values" self.assertEqual(self.demojournal.getFieldValueStr("Comment"), DEMOCOMMENT) def testRFCConformity(self): "rfc conformity" self.calendar.add(vEvent(RFCTESTCAL1)) self.assertEqual(self.calendar[1].description.get(), RFCTESTDESCR1) def testSort(self): "vCalendar sorting by Date" jour = vEvent() jour.dtstart.set("2004-02-22") hdl1 = self.calendar.add(jour) jour = vEvent() jour.dtstart.set("2001-09-11") hdl2 = self.calendar.add(jour) jour = vEvent() jour.dtstart.set("2004-08-19") hdl3 = self.calendar.add(jour) self.assertEqual(self.calendar.sortedlist(), [hdl2, hdl1, hdl3]) if __name__ == "__main__": unittest.main() PyCoCuMa-0.4.5-6/pycocumalib/testvcard.py0000644000175000017500000001342410252657644021300 0ustar henninghenning00000000000000""" Unit Test Case """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: testvcard.py 82 2004-07-11 13:01:44Z henning $ import unittest from vcard import vCard, vCardList, vC_adr, vC_tel, vC_email DEMONOTE = u"""This is a simple Test Case for my vCard Python Module. Here comes german umlauts: \xe4\xc4\xfc\xdc\xf6\xd6\xdf (this string is Unicode and will be written to disk as Latin-1) Now some special vCard chars: \t (tab) ; : , ? (note the escaping!) """ def fillDemoCard(card): "Completely fill a vCard with demo values" card.fn.set("*Demo Card*") card.nickname.set("Nickname") card.bday.set("1983-08-19") card.n.prefixes.set("Prefixes") card.n.given.set("Given") card.n.additional.set("Additional") card.n.family.set("Family") card.n.suffixes.set("Suffixes") card.org.org.set("MyCompany Ltd.") card.org.units.set("My Division, My Office; Another Dep.") card.title.set("Title") card.role.set("Role") card.adr.append(vC_adr()) card.adr[0].pobox.set("POBox") card.adr[0].extended.set("Extended") card.adr[0].street.set("Street") card.adr[0].postcode.set("Postal Code") card.adr[0].city.set("City") card.adr[0].region.set("Region") card.adr[0].country.set("Country") card.adr[0].params.get("type").add("pref") card.adr.append(vC_adr()) card.adr[1].pobox.set("POBox2") card.adr[1].extended.set("Extended2") card.adr[1].street.set("Street2") card.adr[1].postcode.set("Postal Code2") card.adr[1].city.set("City2") card.adr[1].region.set("Region2") card.adr[1].country.set("Country2") card.label.set("My Address Label\nStreet Somewhere\nNoCity") card.tel.append(vC_tel()) card.tel[0].set("02323-92348223-9 34") card.tel[0].params.get("type").extend(["isdn","home","msg"]) card.tel.append(vC_tel()) card.tel[1].set("01234-56789") card.tel[1].params.get("type").extend(["work","cell"]) card.email.append(vC_email()) card.email[0].set("email@my.domain.nowhere") card.email[0].params.get("type").add("pref") card.email.append(vC_email()) card.email[1].set("email2@my.2nddomain.nowhere") card.note.set(DEMONOTE) card.categories.set("Demo, Business, Private, TestCase") card.url.set("http://www.mydomain.nowhere") card.uid.set("-//TEST//TestvCard@mydomain.nowhere//123456789") card.key.set("""-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.0.4e (MingW32) Comment: Weitere Infos: siehe http://www.gnupg.org mQGiBD46zoIRBADiXv3YlcimwxS1H9THTuzkcP1Ozry88Xvja36ct9uMsjYTl8HK 1sYNBCEnjBKkqIEC4RW1iOfMgLhYeix4stSR7aa+09ZmOkSwE/nq/bNjKaGOjIRE QHQEQQaXeD6DPm9kc9XHLMpYZoC4Ix8JKOrQeBUXekXgWJ0xaFXNEg7zewCgsowY W/BC4Cl9AX8t6SBPWRr0RjkD/A3iZc8J8WMSrVa8XFbcpPh/RddiNCMc8u3V/Lll Gym6LUvjcTDdoAuZLcT04/w3HcvJvroHhy4yLuo6wk8xODYEFsmQS12XsMQe5ifr GlE3om9HboqfiLjeizU1vG/FP2WLI19fsKtVnfKbsNhkFR6ar9j1+kxYAFjWOLP7 LWiuBACClxFUDLl0SyXC0C2b3/Lv/VYpnwO6B7n5s1k559OfQHK1alEK88js4vEe m0jD2n1vJZZlJZei8gzZFJeGh6XdMmcidRp8g5TnEvs84xM9QsU0Q64Uoqbcu6u0 rUp/zEnTANmghz2riBsIwj3Ne32FQyYCVx+d27hj/zqL41syfbQjSGVubmluZyBK YWNvYnMgPGhlbm5pbmdAamFjb2JzMS5kZT6IVwQTEQIAFwUCPjrOggULBwoDBAMV AwIDFgIBAheAAAoJEAiNyhwAo48FstgAn0tRSNClh76+Rg9SghQk6w+gixnpAKCb lW0d9ncBts2z/YYT3Ve0xiPI6LkBDQQ+Os6XEAQAyVV8OTs2WbUv4/MtI9Gb5mY/ 4uAHZU4+8fTToKb6P8IJzEe05hIMJcREvUOI8m6zsKxoGitC1KuNYATZsIQ3Ul7D 2Vf016zScaPwHtfkgMkE+6VOUP1lt+nZTLwa5NUxs236L4WA4cdRs+20EQtYk8A+ NXwZOJpbAP4Hcny8QA8ABA0EALJ9953d1Z654g1cWNM/qjLhWR8GzhYBFEI1hWGT r4hS4NMhxBSWLTCW1COw+7Z8PhTnYZwyW/Zi4SIOmwkwpQkG3r3+Ao5jjmX/m4yX SM+sfv6KArwDuXvGGtpZ7tH/MJcMZd7TJgGxxbVz18HYtr7u471V1XD3Z6dsRD1i zxpuiEYEGBECAAYFAj46zpcACgkQCI3KHACjjwUtlQCfQAxUyDfEoQH5F2W8CRDz iYZtIf4AoJtFvYGBxWRa3ZzkV9mOnlY1t/q0 =TPAr -----END PGP PUBLIC KEY BLOCK-----""") card.mailer.set("Pine for Linux ver. 4.9") card.tz.set("+01:00") card.geo.set("53.5;10.0") class vCardTestCase(unittest.TestCase): def setUp(self): self.cardlist = vCardList() self.democard = vCard() fillDemoCard(self.democard) def testReadWrite(self): "writing and reading vCard from stream" self.cardlist.add(self.democard) cardbefore = self.democard import StringIO stream = StringIO.StringIO() self.cardlist.SaveToStream(stream) stream.seek(0) self.cardlist.clear() self.cardlist.LoadFromStream(stream) handle = self.cardlist.sortedlist()[0] cardafter = self.cardlist[handle] self.assertEqual(cardbefore.VCF_repr(), cardafter.VCF_repr()) def testSort(self): "vCardList sorting" card = vCard() card.n.family.set("Aaaa") hdl1 = self.cardlist.add(card) card = vCard() card.fn.set("Bbbb") card.sort_string.set("0000") hdl2 = self.cardlist.add(card) card = vCard() card.fn.set("Cccc") hdl3 = self.cardlist.add(card) self.assertEqual(self.cardlist.sortedlist(), [hdl2, hdl1, hdl3]) def testFieldValue(self): "compare card values with our demo values" self.assertEqual(self.democard.getFieldValueStr("Note"), DEMONOTE) self.assertEqual(self.democard.getDisplayName(), "Family, Given") self.assertEqual(self.democard.getFieldValueStr("Street 1"), "Street") self.assertEqual(self.democard.getFieldValueStr("Phone 2"), "01234-56789 (cell, voice, work)") vals = [] for i in range(2): vals.append(self.democard.getFieldValueStr("PostalCode")) self.assertEqual(tuple(vals), ("Postal Code", "Postal Code2")) if __name__ == "__main__": unittest.main() PyCoCuMa-0.4.5-6/pycocumalib/texwrapper.py0000644000175000017500000000664310252657644021507 0ustar henninghenning00000000000000#!/usr/bin/python """ Extension to call (La)TeX and view PDF written by Henning Jacobs , 2003 """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: texwrapper.py 82 2004-07-11 13:01:44Z henning $ import sys import os import tkMessageBox from Tkinter import * import ToolTip class OutputWindow(Toplevel): def __init__(self, root, title="OutputWindow"): Toplevel.__init__(self, root) self.title(title) self.vbar = vbar = Scrollbar(self, name='vbar') self.text_frame = text_frame = Frame(self) self.text = text = Text(text_frame, wrap="none", background="#bbbbbb", foreground="black") ToolTip.ToolTip(text, 'There were errors while running LaTeX.\n'+ 'Errors are shown in red.') text.tag_configure('error', foreground='#ee0000') vbar['command'] = text.yview vbar.pack(side=RIGHT, fill=Y) text['yscrollcommand'] = vbar.set text_frame.pack(side=LEFT, fill=BOTH, expand=1) text.pack(side=TOP, fill=BOTH, expand=1) text.focus_set() def appendtext(self, text, tags=None): self.text.insert(END, text, tags) self.text.see(END) self.text.update() def view_pdf(texfilename): if not texfilename: return pdfname = os.path.splitext(texfilename)[0] + ".pdf" if not os.access(pdfname, os.R_OK): tkMessageBox.showerror("Error", "File not found: '%s'\n Run TeX first!" % pdfname) if sys.platform == "win32": # Explorer File-Handling: os.startfile(pdfname) else: os.spawnv(os.P_NOWAIT, '/usr/bin/xpdf', ['xpdf', pdfname]) def run_pdflatex(texfilename): if not texfilename: return oldpwd = os.getcwd() # TODO: Not working when texfilename includes space characters! cmd = 'pdflatex -interaction=nonstopmode %s' % texfilename os.chdir(os.path.dirname(texfilename)) # Check whether .aux and .log files were there before we came: root, ext = os.path.splitext(texfilename) isOurAuxFile = not os.access(root+'.aux', os.F_OK) isOurLogFile = not os.access(root+'.log', os.F_OK) # Now run pdfLaTeX: pipe = os.popen(cmd, 'r') outlines = pipe.readlines() pipe.close() showoutwin = False for line in outlines: if line[:1] == '!': showoutwin = True break # Restore Working Directory: os.chdir(oldpwd) pdfname = os.path.splitext(texfilename)[0] + ".pdf" if os.access(pdfname, os.R_OK): # PDF seems to be written -> OK # Delete .aux and .log files if they were created by us: if isOurAuxFile: os.unlink(root+'.aux') if isOurLogFile: os.unlink(root+'.log') else: showoutwin = True if showoutwin: # There were Errors while running pdfLaTeX -> show Output Log: outwin = OutputWindow(None, title="TeX Output from %s" % os.path.basename(texfilename)) outwin.appendtext("PyCoCuMa: Calling '%s'\n" % cmd) for line in outlines: if line[:1] == '!': outwin.appendtext(line, 'error') else: outwin.appendtext(line) PyCoCuMa-0.4.5-6/pycocumalib/vcalendar.py0000644000175000017500000003210610252657644021236 0ustar henninghenning00000000000000#!/usr/bin/python """ iCalendar 2.0 Implementation as described in RFC 2445 We use the former name 'vCalendar' here. WARNING: This iCalendar implementation is VERY incomplete and should not be used for any real-world calendar applications! """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: vcalendar.py 82 2004-07-11 13:01:44Z henning $ import re, time from types import * import string import debug from vcore import * import Set FIELDNAMES = [ "Summary", "Description", "Categories", "Comment", "Location", "Attendee", # special PyCoCuMa virtual field: "AttendeeIds", "Organizer", "Contact", "DateStart", "DateEnd", "LastModification", "Created", "TimeStamp", "URL", "UID"] FIELDNAME2ATTR = { "Summary" : "summary", "Description" : "description", "Categories" : "categories", "Comment" : "comment", "Location" : "location", "Attendee" : "attendee", "AttendeeIds" : "getAttendeeIds", "Organizer" : "organizer", "Contact" : "contact", "DateStart" : "dtstart", "DateEnd" : "dtend", "LastModification": "last_mod", "Created" : "created", "TimeStamp" : "dtstamp", "URL" : "url", "UID" : "uid" } SUPPORTED_VERSIONS = ["2.0"] class vC_caladdress(vC_text): def __init__(self, value="", params=None): parts = value.split(':') if len(parts) > 1: value = ':'.join(parts[1:]) vC_text.__init__(self, value, params) # PyCoCuMa's Contact UID is wrapped in the DIR URI Param: # E.g: http://pycocuma.localhost:8810/xmlrpc/vCard?UID=wxyz-1234-56 uid = self.params.get('dir') if uid is None: self._uid = '' else: parts = uid[0].split('=') self._uid = parts[-1] def getUID(self): return self._uid def setUID(self, uid): self._uid = uid def assignFromCard(self, card): "We set our Calendar Address from a vCard" fn = card.fn.get() if card.email: # Find the preferred Email-Address: prefmail = 0 for i in range(len(card.email)): if card.email[i].is_pref(): prefmail = i self.set(card.email[prefmail].get()) else: # Sorry, the card has no email: self.set("nobody@nowhere") # Set our Common Name to the card's FormattedName: self.params.set('cn', Set.Set(fn)) # Set our UID: if not card.uid.is_empty(): self.setUID(card.uid.get()) else: self.setUID('') def is_pref(self): # This Function is required by InputWidgets.MultiRecordEdit return False def VCF_repr(self): if self._uid: # This is a fake URI to wrap the vCard's UID: dir = "http://pycocuma.localhost:8810/xmlrpc/vCard?UID=%s" % self._uid self.params.set('dir', Set.Set(dir)) return self.params.VCF_repr() + ":MAILTO:" + self.value.VCF_repr() class vC_attendee(vC_caladdress): "Event Attendee" def __init__(self, value="", params=None): vC_caladdress.__init__(self, value, params) class vC_organizer(vC_caladdress): "Event Organizer" def __init__(self, value="", params=None): vC_caladdress.__init__(self, value, params) # Stores information about last call to getFieldValue(): # _lastreturnedfield = "%d_%s" % (vevent.handle(), fieldname) _lastreturnedfield = "" _lastreturnedvalueidx = 0 _uidcounter = 0 class vEvent: """vEvent Implementation as described in RFC 2445""" def _generateUID(self): global _uidcounter dtstr = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime()) _uidcounter += 1 return "%s-%04X@pycocuma.srcco.de" % (dtstr, _uidcounter) def _name2attr(self, name): return name.lower().replace("-", "_") def _attr2name(self, attr): return attr.upper().replace("_", "-") def __init__(self, block_of_lines=[]): self.__dict__["_handle"] = None # default values: self.__dict__["data"] = { "summary" : vC_text(), "description": vC_text(), "categories": vC_categories(), "comment" : vC_text(), "location" : vC_text(), "attendee" : [], "organizer" : vC_organizer(), "contact" : vC_text(), "dtstart" : vC_datetime(), "dtend" : vC_datetime(), "last_mod" : vC_datetime(time.gmtime()), "created" : vC_datetime(time.gmtime()), "dtstamp" : vC_datetime(time.gmtime()), "url" : vC_text(), # RFC-2445 says that UID MUST BE present: "uid" : vC_text(self._generateUID())} # Name to function mapping: self.__dict__["name_func"] = { "SUMMARY" : vC_text, "DESCRIPTION": vC_text, "CATEGORIES": vC_categories, "COMMENT" : vC_text, "LOCATION" : vC_text, "ATTENDEE" : vC_attendee, "ORGANIZER" : vC_organizer, "CONTACT" : vC_text, "DTSTART" : vC_datetime, "DTEND" : vC_datetime, "LAST-MOD" : vC_datetime, "CREATED" : vC_datetime, # Timestamp will only be set once on object creation: "DTSTAMP" : vC_datetime, "URL" : vC_text, "UID" : vC_text} if type(block_of_lines) != ListType: # Input was a string? # Delete empty lines: block_of_lines = filter(isNonEmptyLine, block_of_lines.split('\n')) # de-fold: makeloglines(block_of_lines) block_of_lines = map(vC_contentline, block_of_lines) for line in block_of_lines: self._insertline(line) global _lastreturnedvalueidx _lastreturnedvalueidx = 0 def _insertline(self, line): if self.name_func.has_key(line.name): attr = self._name2attr(line.name) if hasattr(self, attr) and type(self.__getattr__(attr)) == ListType: # Multi-Value: self.__getattr__(attr).append(\ self.name_func[line.name](line.value, line.params)) else: # Single-Value: self.__setattr__(attr,\ self.name_func[line.name](line.value, line.params)) def __getattr__(self, name): try: return self.data[name] except: raise AttributeError, "No Attribute '%s'" % (name) def __setattr__(self, name, value): try: self.data[name] = value except: raise AttributeError, "No Attribute '%s'" % (name) def __repr__(self): ret = "<%s: " % self.__class__ for key, value in zip(self.data.keys(), self.data.values()): ret = ret + "%s: %s\n" % (repr(key), repr(value)) return ret+">" def handle(self): return self.__dict__["_handle"] def sethandle(self, handle): "Handle can be set only once in a lifetime!" if self.__dict__["_handle"] == None: self.__dict__["_handle"] = handle def getAttendeeIds(self): "Return comma separated list of attendee UIDs" return ', '.join(map(vC_caladdress.getUID, self.attendee)) def getFieldValue(self, field_and_idx): """Returns content of field as vC_value field_and_idx => e.g. 'Street 1' return => vC_value or None On multiple calls it returns further items from value array (Call this functions until it returns None)""" global _lastreturnedfield, _lastreturnedvalueidx def lastretfieldstr(field, self=self): handle = self.handle() if handle is None: return "None_%s" % (field) else: return "%d_%s" % (handle, field) parts = field_and_idx.split(" ") field = parts[0] try: idx = int(parts[1])-1 except: if lastretfieldstr(field) == _lastreturnedfield: idx = _lastreturnedvalueidx + 1 else: idx = 0 attr = FIELDNAME2ATTR[field] attrobj = getsubattr(self, attr) if type(attrobj) == ListType: if len(attrobj) > idx: value = attrobj[idx] else: value = None elif idx >0: value = None else: value = attrobj _lastreturnedvalueidx = idx _lastreturnedfield = lastretfieldstr(field) return value def getFieldValueStr(self, field_and_idx, default=""): """Returns content of field as string field_and_idx => e.g. 'Street 1' return => e.g. 'Fifth-Avenue 89'""" val = self.getFieldValue(field_and_idx) if val: return flattenattr(val) else: # the field does not exist # (NOTE: this does not mean empty fields!): return default def VCF_repr(self): ret = "BEGIN:VEVENT\n" Dict = self.data for name, val in Dict.items(): if type(val) == ListType: for itm in val: ret = ret + log2phylines(self._attr2name(name) + itm.VCF_repr() + "\n") elif val != None: if not val.is_empty(): ret = ret + log2phylines(self._attr2name(name) + val.VCF_repr() + "\n") return ret + "END:VEVENT\n" class vCalendar: """List of vEvents""" def __init__(self): self.handlecounter = 0 self.data = {} def sortedlist(self, sortby=""): "Return list of event handles" if not sortby: sortby = "DateStart" events = self.data.values() handles = self.data.keys() def getfieldvaluestr(event, field=sortby): return event.getFieldValueStr(field) decorated = zip(map(getfieldvaluestr, events), handles) decorated.sort() self.forgetLastReturnedField() return [ handle for sortstr, handle in decorated ] def LoadFromFile(self, fname): "Load from *.ics file" try: fd = open(fname, "rb") self.LoadFromStream(fd) fd.close() except: # Loading failed return False return True def LoadFromStream(self, stream, encoding='utf8'): "Load from any text stream with file-like methods" vEventBlock = None lines = stream.readlines() makeloglines(lines) for line in filter(isNonEmptyLine, lines): if type(line) != UnicodeType: line = unicode(line, encoding, 'replace') line = vC_contentline(line) if line.name == "BEGIN": if line.value.upper() == "VEVENT": vEventBlock = [] if vEventBlock is not None: vEventBlock.append(line) if line.name == "END" \ and line.value.upper() == "VEVENT": self.add(vEvent(vEventBlock)) def SaveToFile(self, fname): "Save to *.ics file" fd = open(fname, "wb") self.SaveToStream(fd) fd.close() def SaveToStream(self, stream, encoding='utf8'): "Save to Stream (FileDescriptor)" stream.write(self.VCF_repr().encode(encoding, 'replace')) def __str__(self): return str(zip(self.data.keys(), map(str,self.data.values()))) def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value def clear(self): "Removes all vEvents" self.data.clear() def add(self, event=None): "Add new vEvent to list" if event == None: event = vEvent() self.handlecounter += 1 newhandle = self.handlecounter # Handle must not have been set before: event.sethandle(newhandle) self.data[newhandle] = event return newhandle def delete(self, handle): "Removes vEvent with handle from list" if self.data.has_key(handle): del self.data[handle] return True else: return False def forgetLastReturnedField(self): "Reset the lastreturnedfield variable" global _lastreturnedfield, _lastreturnedvalueidx _lastreturnedfield = "" _lastreturnedvalueidx = 0 def VCF_repr(self): "Representation for file output" from __version__ import __version__ ret = "BEGIN:VCALENDAR\nVERSION:2.0\n" ret = ret + "PRODID:-//Henning Jacobs//NONSGML PyCoCuMa Calendar Version %s//EN\n\n" % __version__ for event in self.data.values(): ret = ret + event.VCF_repr() + "\n" # add blank line between events return ret+"END:VCALENDAR\n" PyCoCuMa-0.4.5-6/pycocumalib/vcard.py0000644000175000017500000005144010252657644020400 0ustar henninghenning00000000000000#!/usr/bin/python """ vCard 3.0 Implementation as described in RFC 2426 """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: vcard.py 94 2004-12-06 21:10:31Z henning $ import re, time from types import * import string import debug from vcore import * # For py2exe ModuleFinder: import codecs from encodings import utf_8 FIELDNAMES = [ "FormattedName", "DisplayName", # virtual (does not map to a real vCard field) "Family", "Given", "Additional", "Prefixes", "Suffixes", "NickName", "Birthday", "Organization", "Units", "Title", "Role", "Phone", "Email", "Mailer", "POBox", "Extended", "Street", "PostalCode", "City", "Region", "Country", "Label", "Note", "Categories", "SortName", # virtual "SortString", "URL", "Key", "TimeZone", "GlobalPosition", "Photo", "Logo", "Rev", "UID"] ADRFIELDS = ["POBox", "Extended", "Street", "PostalCode", "City", "Region", "Country"] FIELDNAME2ATTR = {"FormattedName":"fn", "DisplayName":"getDisplayName", "Family":"n.family", "Given":"n.given", "Additional":"n.additional", "Prefixes":"n.prefixes", "Suffixes":"n.suffixes", "NickName":"nickname", "Birthday":"bday", "Organization":"org.org", "Units":"org.units", "Title":"title", "Role":"role", "Phone":"tel", "Email":"email", "Mailer":"mailer", "POBox":"adr.pobox", "Extended":"adr.extended", "Street":"adr.street", "City":"adr.city", "Region":"adr.region", "PostalCode":"adr.postcode", "Country":"adr.country", "Label":"label", "Note":"note", "Categories":"categories", "SortName":"getSortName", "SortString":"sort_string", "URL":"url", "Key":"key", "TimeZone":"tz", "GlobalPosition":"geo", "Photo":"photo", "Logo":"logo", "Rev":"rev", "UID":"uid" } vC_adr_types = ["pref", "home", "work", "intl", "postal", "parcel", "dom"] # DOMestic is currently not supported in PyCoCuMa vC_adr_types_default = ["intl", "postal", "parcel", "work"] vC_tel_types = ["pref", "home", "work", "voice", "cell", "pager", "car", "fax", "modem", "msg", "isdn", "video", "bbs", "pcs"] # BBS and PCS are currently not supported in PyCoCuMa vC_tel_types_default = ["voice"] vC_email_types = ["pref", "internet", "x400"] # These will not display in PyCoCuMa vC_email_types_default = ["internet"] SUPPORTED_VERSIONS = ["2.1", "3.0"] class vC_n(vC_AbstractCompoundValue): "vCard Name record" def __init__(self, value="", params=None): if params is None: params = vC_params() self.params = params parts = VALUE_DELIM_RE.split(value) self.family,\ self.given,\ self.additional,\ self.prefixes,\ self.suffixes = gettuple(map(vC_value, parts), 5, vC_value) def is_empty(self): return False def getFamilyGiven(self): fam = self.family.get() giv = self.given.get() if fam and giv: return fam + ", " + giv else: return fam or giv or "" def VCF_repr(self): ret = self.params.VCF_repr() + ":" ret = ret + ";".join(map(getvcfrepr, [self.family,\ self.given,\ self.additional,\ self.prefixes,\ self.suffixes])) return ret def XML_repr(self): ret = "%s%s%s%s" ret = ret + "%s" ret = ret % tuple(map(lambda x: x.XML_repr(), [self.family,\ self.given,\ self.additional,\ self.prefixes,\ self.suffixes])) return ret class vC_tel(vC_text): "Single Telephone Entry" def __init__(self, value="", params=None): vC_text.__init__(self, value, params) import Set # Set default TEL-Type if not defined: if self.params.get("type") is None: defaultset = Set.Set() defaultset.extend(vC_tel_types_default) self.params.set("type", defaultset) def is_pref(self): "Is this Entry preferred?" return "pref" in self.params.get("type") class vC_email(vC_text): "Single Email Address" def __init__(self, value="", params=None): vC_text.__init__(self, value, params) import Set # Set default EMAIL-Type if not defined: if self.params.get("type") is None: defaultset = Set.Set() defaultset.extend(vC_email_types_default) self.params.set("type", defaultset) def is_pref(self): "Is this Entry preferred?" return "pref" in self.params.get("type") class vC_adr(vC_AbstractCompoundValue): "Single Adress Record" def __init__(self, value="", params=None): if params is None: params = vC_params() self.params = params import Set # Set default ADR-Type if not defined: if self.params.get("type") is None: defaultset = Set.Set() defaultset.extend(vC_adr_types_default) self.params.set("type", defaultset) parts = VALUE_DELIM_RE.split(value) self.pobox,\ self.extended,\ self.street,\ self.city,\ self.region,\ self.postcode,\ self.country = gettuple(map(vC_value, parts), 7, vC_value) def is_empty(self): return False def is_pref(self): "Is this Entry preferred?" return "pref" in self.params.get("type") def VCF_repr(self): ret = self.params.VCF_repr() + ":" ret = ret + ";".join(map(getvcfrepr, [self.pobox,\ self.extended,\ self.street,\ self.city,\ self.region,\ self.postcode,\ self.country])) return ret def XML_repr(self): ret = "%s%s%s" ret = ret + "%s%s%s" ret = ret + "%s" ret = ret % tuple(map(lambda x: x.XML_repr(), [self.pobox,\ self.extended,\ self.street,\ self.city,\ self.region,\ self.postcode,\ self.country])) return ret+self.params.XML_repr() class vC_orgunits(vC_AbstractBaseValue): "Contains List of Organizational Units" def __init__(self): self.list = [] def get(self): return ";".join(self.list) def set(self, value): if type(value) is ListType: self.list = value else: self.list = map(string.strip, value.split(";")) def __repr__(self): return "<%s: %s>" % (self.__class__, repr(self.list)) def is_empty(self): return self.list == [] def VCF_repr(self): return ";".join(map(escape, self.list)) def XML_repr(self): ret = "" for unit in self.list: ret = ret + "%s" % escapexml(unit) return ret+'' class vC_org(vC_AbstractCompoundValue): "Organizational Information" def __init__(self, value="", params=None): if params is None: params = vC_params() self.params = params parts = VALUE_DELIM_RE.split(value) self.org = vC_value(getitem(parts, 0, "")) self.units = vC_orgunits() if len(parts) > 1: self.units.list = map(deescape, filter(isNonEmptyLine, parts[1:])) def is_empty(self): return self.org.get() == "" and self.units.is_empty() def VCF_repr(self): ret = self.params.VCF_repr() + ":" ret = ret + ";".join(map(getvcfrepr, [self.org, self.units])) return ret def XML_repr(self): ret = "%s%s" ret = ret % tuple(map(lambda x: x.XML_repr(), [self.org, self.units])) return ret # Stores information about last call to getFieldValue(): # _lastreturnedfield = "%d_%s" % (vcard.handle(), fieldname) _lastreturnedfield = "" _lastreturnedvalueidx = 0 class vCard: """vCard 3.0 Implementation as described in RFC 2426""" def _name2attr(self, name): return name.lower().replace("-", "_") def _attr2name(self, attr): return attr.upper().replace("_", "-") def __init__(self, block_of_lines=[]): self.__dict__["_handle"] = None # default values: self.__dict__["data"] = { "version": vC_text("3.0"), "n" : vC_n(), "fn" : vC_text(), "nickname": vC_text(), "bday" : vC_datetime(), "tel" : [], "adr" : [], "label" : vC_text(), "email" : [], "mailer": vC_text(), "org" : vC_org(), "title" : vC_text(), "role" : vC_text(), "note" : vC_text(), "categories" : vC_categories(), "sort_string" : vC_text(), "url" : vC_text(), "key" : vC_text(), "rev" : vC_datetime(time.gmtime()), "uid" : vC_text(), # Geographical Types: "tz" : vC_text(), "geo" : vC_geo(), # Photographic Picture: "photo" : vC_text(), # company logo image: "logo" : vC_text()} # Name to function mapping: self.__dict__["name_func"] = { "N" : vC_n, "FN" : vC_text, "NICKNAME": vC_text, "BDAY" : vC_datetime, "TEL" : vC_tel, "ADR" : vC_adr, "LABEL" : vC_text, "EMAIL" : vC_email, "MAILER": vC_text, "ORG" : vC_org, "TITLE" : vC_text, "ROLE" : vC_text, "NOTE" : vC_text, "CATEGORIES" : vC_categories, "SORT-STRING" : vC_text, "URL" : vC_text, "KEY" : vC_text, "REV" : vC_datetime, "UID" : vC_text, "TZ" : vC_text, "GEO" : vC_geo, "PHOTO" : vC_text, "LOGO" : vC_text} if type(block_of_lines) != ListType: # Input was a string? # Delete empty lines: block_of_lines = filter(isNonEmptyLine, block_of_lines.split('\n')) # de-fold: makeloglines(block_of_lines) block_of_lines = map(vC_contentline, block_of_lines) for line in block_of_lines: if line.name == "VERSION" \ and not line.value in SUPPORTED_VERSIONS : debug.echo("WARNING: Unsupported vCard version (should be \"3.0\" or \"2.1\").") self._insertline(line) global _lastreturnedvalueidx _lastreturnedvalueidx = 0 def _insertline(self, line): if self.name_func.has_key(line.name): attr = self._name2attr(line.name) if hasattr(self, attr) and type(self.__getattr__(attr)) == ListType: # Multi-Value (adr, tel and email): self.__getattr__(attr).append(\ self.name_func[line.name](line.value, line.params)) else: # Single-Value: self.__setattr__(attr,\ self.name_func[line.name](line.value, line.params)) def __getattr__(self, name): try: return self.data[name] except: raise AttributeError, "No Attribute '%s'" % (name) def __setattr__(self, name, value): try: self.data[name] = value except: raise AttributeError, "No Attribute '%s'" % (name) def __repr__(self): ret = "<%s: " % self.__class__ for key, value in zip(self.data.keys(), self.data.values()): ret = ret + "%s: %s\n" % (repr(key), repr(value)) return ret+">" def handle(self): return self.__dict__["_handle"] def sethandle(self, handle): "Handle can be set only once in a lifetime!" if self.__dict__["_handle"] == None: self.__dict__["_handle"] = handle def getSortName(self): "usually the same as getDisplayName, differs when sort-string is set" return self.sort_string.get() or\ self.n.getFamilyGiven() or\ self.fn.get() def getDisplayName(self): "Get DisplayName (this is: 'Family, Given' or the FormattedName)" return self.n.getFamilyGiven() or\ self.fn.get() def getFieldValue(self, field_and_idx): """Returns content of field as vC_value field_and_idx => e.g. 'Street 1' return => vC_value or None On multiple calls it returns further items from value array (Call this functions until it returns None)""" global _lastreturnedfield, _lastreturnedvalueidx def lastretfieldstr(field, self=self): handle = self.handle() if handle is None: return "None_%s" % (field) else: return "%d_%s" % (handle, field) parts = field_and_idx.split(" ") field = parts[0] try: idx = int(parts[1])-1 except: if lastretfieldstr(field) == _lastreturnedfield: idx = _lastreturnedvalueidx + 1 else: idx = 0 attr = FIELDNAME2ATTR[field] attrobj = getsubattr(self, attr) if type(attrobj) == ListType: if len(attrobj) > idx: value = attrobj[idx] else: value = None elif idx >0: value = None else: value = attrobj _lastreturnedvalueidx = idx _lastreturnedfield = lastretfieldstr(field) return value def getFieldValueStr(self, field_and_idx, default=""): """Returns content of field as string field_and_idx => e.g. 'Street 1' return => e.g. 'Fifth-Avenue 89'""" val = self.getFieldValue(field_and_idx) if val: return flattenattr(val) else: # the field does not exist # (NOTE: this does not mean empty fields!): return default def setFieldValueStr(self, field_and_idx, val): "Set Field Value" parts = field_and_idx.split(" ") field = parts[0] try: idx = int(parts[1])-1 except: idx = 0 attr = FIELDNAME2ATTR[field] attrobj = getsubattr(self, attr) if type(attrobj) == ListType: if len(attrobj) > idx: value = attrobj[idx] else: # XXX: This is ugly hard-coded! if field in ADRFIELDS: self.__getattr__('adr').append(self.name_func['ADR']()) elif field == 'Phone': self.__getattr__('tel').append(self.name_func['TEL']()) elif field == 'Email': self.__getattr__('email').append(self.name_func['EMAIL']()) else: raise ValueError attrobj = getsubattr(self, attr) value = attrobj[-1] elif idx >0: raise ValueError else: value = attrobj value.set(val) def VCF_repr(self): "Native vCard Representation" ret = "BEGIN:VCARD\n" for name, val in self.data.items(): if type(val) == ListType: for itm in val: ret = ret + log2phylines(self._attr2name(name) + itm.VCF_repr() + "\n") elif val != None: if not val.is_empty(): ret = ret + log2phylines(self._attr2name(name) + val.VCF_repr() + "\n") return ret + "END:VCARD\n" def XML_repr(self): "XML Representation" ret = '\n' % escapexml(self.uid.get()) for name, val in self.data.items(): if name != 'version': # We don't need to specify the Version with XML! if type(val) == ListType: if len(val) > 1: ret = ret + "" % self._attr2name(name) ret = ret + '' for itm in val: ret = ret + '' + itm.XML_repr() + '' ret = ret + '' ret = ret + "\n" % self._attr2name(name) elif len(val) > 0: ret = ret + "" % self._attr2name(name) ret = ret + val[0].XML_repr() ret = ret + "\n" % self._attr2name(name) elif val != None: if not val.is_empty(): ret = ret + "" % self._attr2name(name) ret = ret + val.XML_repr() ret = ret + "\n" % self._attr2name(name) return ret + "\n" class vCardList: """List of vCards""" def __init__(self): self.handlecounter = 0 self.data = {} def sortedlist(self, sortby=""): "Return list of card handles" if not sortby: sortby = "SortName" cards = self.data.values() handles = self.data.keys() def getfieldvaluestr(card, field=sortby): return card.getFieldValueStr(field) decorated = zip(map(getfieldvaluestr, cards), handles) decorated.sort() self.forgetLastReturnedField() return [ handle for sortstr, handle in decorated ] def LoadFromFile(self, fname): "Load from *.vcf file" try: fd = open(fname, "rb") self.LoadFromStream(fd) fd.close() except: # Loading failed return False return True def LoadFromStream(self, stream, encoding='utf8'): "Load from any text stream with file-like methods" vCardBlock = None lines = stream.readlines() makeloglines(lines) for line in filter(isNonEmptyLine, lines): if type(line) != UnicodeType: line = unicode(line, encoding, 'replace') line = vC_contentline(line) if line.name == "BEGIN" \ and line.value.upper() == "VCARD": vCardName = "default" vCardBlock = [] if vCardBlock == None: debug.echo("ERROR: Missing 'BEGIN:VCARD' ?") else: vCardBlock.append(line) if line.name == "END" \ and line.value.upper() == "VCARD": self.add(vCard(vCardBlock)) def SaveToFile(self, fname): "Save to *.vcf file" fd = open(fname, "wb") self.SaveToStream(fd) fd.close() def SaveToStream(self, stream, encoding='utf8'): "Save to Stream (FileDescriptor)" stream.write(self.VCF_repr().encode(encoding, 'replace')) def __str__(self): return str(zip(self.data.keys(), map(str,self.data.values()))) def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value def clear(self): "Removes all vCards" self.data.clear() def add(self, card=None): "Add new vCard to list" if card == None: card = vCard() self.handlecounter += 1 newhandle = self.handlecounter # Handle must not have been set before: card.sethandle(newhandle) self.data[newhandle] = card return newhandle def delete(self, handle): "Removes vCard with handle from list" if self.data.has_key(handle): del self.data[handle] return True else: return False def forgetLastReturnedField(self): "Reset the lastreturnedfield variable" global _lastreturnedfield, _lastreturnedvalueidx _lastreturnedfield = "" _lastreturnedvalueidx = 0 def VCF_repr(self): "Representation for file output" ret = "" for card in self.data.values(): ret = ret + card.VCF_repr() + "\n" # add blank line return ret def XML_repr(self): ret = '\n' ret = ret + '' for card in self.data.values(): ret = ret + card.XML_repr() + "\n" # add blank line return ret + '\n' PyCoCuMa-0.4.5-6/pycocumalib/vcore.py0000644000175000017500000003243310252657644020420 0ustar henninghenning00000000000000#!/usr/bin/python """ vCard 3.0 and iCalendar 2.0 Core Types and Functions as described in RFC 2425 (A MIME Content-Type for Directory Information) """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: vcore.py 82 2004-07-11 13:01:44Z henning $ import re from types import * import debug VALUE_DELIM_RE = re.compile(r"(?", ">") def quoteParamValue(str): # Has to be quoted? # Check for non-safe chars: if str.find('=') != -1 \ or str.find(';') != -1\ or str.find(':') != -1\ or str.find(',') != -1: return "\""+str+"\"" else: return str def chop_line(line): "Truncate line breaks" while len(line) and line[-1] in ["\r","\n"]: line=line[:-1] return line def isNonEmptyLine(line): """are there characters other than space? used especially with filter() """ line = chop_line(line) if line: ret = not line.isspace() else: ret = False return ret def makeloglines(physical_lines): "Line De-Folding (RFC 2425): turn physical lines into logical ones" i = 0 while i < len(physical_lines): if i > 0 and len(physical_lines[i]) >= 2: # Line is continued if next phy. line starts with # single space or tab: if physical_lines[i][0] in " \t": physical_lines[i-1] =\ chop_line(physical_lines[i-1]) + physical_lines[i][1:] del physical_lines[i] else: i += 1 else: i += 1 def log2phylines(logical_line): "Fold(break) Lines longer than 75-Chars" # Max-Length of physical Line: i = 75 result = logical_line while True: if i < len(logical_line): # Break with CR and start next line with single space character: result = result[:i] + "\n " + result[i:] else: break i += 76 return result def getitem(seq, idx, default=None): "Return List Item or default if idx is out of range" if len(seq) > idx: return seq[idx] else: return default def gettuple(seq, length, default=None): "Fills seq up to length with default and return tuple" if len(seq) < length: if callable(default): ret = seq for i in range(length - len(seq)): ret.append(default()) return tuple(ret) else: return tuple(seq) + tuple(((length - len(seq))*[default])) else: return tuple(seq) class vC_params: "Parameters used by all vCard/vCalendar records" from Set import Set def __init__(self, text=""): self.dict = {} self.parse(text) def parse(self, text): params = VALUE_DELIM_RE.split(text) for param in params: matchobj = PARAM_RE.match(param) if matchobj: param_name = matchobj.group(1).lower() param_value = matchobj.group(2) if param_value != "": # Compat: Version 2 of vCard had no TYPE param: if param_name == "": param_name = "type" # Is the Value a quoted String? (DQUOTE str DQUOTE) if param_value[0] == "\"" and param_value[-1] == "\"" \ and len(param_value) > 1: param_value = param_value[1:-1] # DeQuote self.dict.setdefault(param_name, self.Set()).extend(param_value.split(",")) def get(self, param_name): return self.dict.get(param_name, None) def set(self, param_name, param_value): self.dict[param_name] = param_value def VCF_repr(self): ret = "" for key, value in zip(self.dict.keys(), self.dict.values()): ret = ret + ";" + key.upper() + "=" + ",".join(map(quoteParamValue,value.items())) return ret def XML_repr(self): if not self.dict: return '' ret = "" # XXX: This is only for vCard-RDF! for item in self.dict.get('type', []): ret = ret + '' return ret def __repr__(self): return "<%s: %s>" % (self.__class__, repr(self.dict)) class vC_contentline: "Logical vCard/vCalendar Content Line" def __init__(self, text): self.name = "" self.params = "" self.value = "" self.parse(text) def parse(self, text): matchobj = CONTENT_LINE_RE.match(chop_line(text)) if matchobj: self.group = matchobj.group(1)[:-1].upper() self.name = matchobj.group(2).upper() self.params = vC_params(matchobj.group(3)) self.value = matchobj.group(4) else: debug.echo("WARNING: Illegal vCard/vCalendar contentline: '%s'" % text) def VCF_repr(self): ret = "" if self.group: ret = ret + self.group + "." ret = ret + self.name + str(self.params) + ":" + self.value return ret class vC_AbstractBaseValue: "Abstract Base Class for all vCard/vCalendar leaf nodes" def VCF_repr(self): "Return VCF representation of value" raise NotImplementedError def XML_repr(self): raise NotImplementedError def get(self): "Return value as String" raise NotImplementedError def set(self, value): "Set value (value should be a string)" raise NotImplementedError def is_empty(self): "Test value for emptieness" raise NotImplementedError class vC_AbstractCompoundValue: "Abstract Base Class for all vCard compound values (n, adr, org)" def __repr__(self): return "<%s: %s>" % (self.__class__, repr(self.__dict__)) def is_empty(self): "return True if value is empty" raise NotImplementedError def VCF_repr(self): "Return VCF representation of value" raise NotImplementedError def XML_repr(self): raise NotImplementedError class vC_value(vC_AbstractBaseValue): "Raw Value used by vC_text" def __init__(self, value=""): self.__data = deescape(value) def VCF_repr(self): return escape(self.__data) def XML_repr(self): return escapexml(self.__data) def set(self, value): self.__data = value def get(self): return self.__data def is_empty(self): return self.__data == "" def __repr__(self): return "<%s: %s>" % (self.__class__, repr(self.__data)) class vC_text(vC_AbstractBaseValue): "vCard/vCalendar Text Value (encapsulates vC_value and vC_params)" def __init__(self, value="", params=None): if params is None: params = vC_params() self.params = params self.value = vC_value(value) def get(self): return self.value.get() def set(self, val): self.value.set(val) def is_empty(self): return self.value.is_empty() def VCF_repr(self): return self.params.VCF_repr() + ":" + self.value.VCF_repr() def XML_repr(self): paramxml = self.params.XML_repr() if paramxml: return ""+self.value.XML_repr()+"" + paramxml else: return self.value.XML_repr() def __repr__(self): return "<%s: %s %s>" % (self.__class__, repr(self.value), repr(self.params)) class vC_categories(vC_text): def VCF_repr(self): # value includes ',' therefore do not escape in a whole: ret = self.params.VCF_repr() + ":" + ','.join(\ map(escape, self.value.get().split(','))) return ret class vC_geo(vC_text): def VCF_repr(self): # value includes ';' therefore do not escape: ret = self.params.VCF_repr() + ":" + self.value.get() return ret class vC_datetime(vC_AbstractBaseValue): "vCard/vCalendar DateTime Value Type" # TODO: Datetime is UTC, support TimeZones! def __init__(self, value=None, params=vC_params()): self.params = params self.dateonly = False self.value = None self.set(value) def __repr__(self): return "<%s: %s>" % (self.__class__, self.get()) def is_empty(self): return self.value is None def getDate(self): "get date as string (YYYY-MM-DD)" if self.value is not None: return "%04d-%02d-%02d" % self.value[0:3] else: return "" def getTime(self): "get time as string (HH:MM:SS)" if self.value is not None and not self.dateonly: return "%02d:%02d:%02d" % self.value[3:6] else: return "" def setDate(self, d): "set date from given string (YYYY-MM-DD)" if self.value is None: self.value = (0,0,0,0,0,0,0,0,0) try: if d.find("-") != -1: d = gettuple(map(int, d.split("-")), 3, 0) else: d = tuple(map(int, (d[0:4], d[4:6], d[6:8]))) self.value = d + self.value[3:] except ValueError: # Date-String was probably incorrect: We clear ourself: self.value = None def setTime(self, t): "set time from given string (HH:MM:SS)" if self.value is None: self.value = (0,0,0,0,0,0,0,0,0) debug.echo('vC_datetime.setTime(): Setting Time, but Date not set!') try: if t.find(":") != -1: if t[-1:] == "Z": t = t[:-1] t = gettuple(map(int, t.split(":")), 3, 0) else: t = tuple(map(int, (t[0:2], t[2:4], t[4:6]))) self.value = self.value[0:3] + t + self.value[6:] self.dateonly = False except ValueError: # Something has gone wrong: Set Time to Zero: self.value = self.value[0:3] + (0,0,0) + self.value[6:] self.dateonly = True def get(self): if self.value is not None: if self.dateonly: ret = self.getDate() else: ret = self.getDate() + ' ' + self.getTime() else: ret = "" return ret def set(self, value): if (type(value) is StringType or\ type(value) is UnicodeType): # Unpack datetime string (YYYY-MM-DDTHH:MM:SS[Z]): d, t = gettuple(value.split('T'), 2) if not t: # try wspace-char as date-time separator: d, t = gettuple(value.split(' '), 2) self.setDate(d) if t: self.setTime(t) else: self.dateonly = True elif value: # value is (hopefully) a time.struct_time tuple: self.value = value else: self.value = None def VCF_repr(self): if self.value is not None: # To comply with RFC 2445 (iCalendar) # DateTime does not include "-" nor ":": if self.dateonly: datetimestr = "%04d%02d%02d" % self.value[0:3] else: datetimestr = "%04d%02d%02dT%02d%02d%02dZ" % self.value[0:6] else: datetimestr = "" return self.params.VCF_repr() + ":" + datetimestr def XML_repr(self): if self.value is not None: if self.dateonly: datetimestr = "%04d%02d%02d" % self.value[0:3] else: datetimestr = "%04d%02d%02dT%02d%02d%02dZ" % self.value[0:6] else: datetimestr = "" return datetimestr PyCoCuMa-0.4.5-6/templates/0000755000175000017500000000000010311641557016402 5ustar henninghenning00000000000000PyCoCuMa-0.4.5-6/templates/DinBrief.tex0000644000175000017500000000212710252657641020615 0ustar henninghenning00000000000000 ?> \documentclass[12pt]{dinbrief} \usepackage{ngerman} %% Umlaute richtig bersetzen: \usepackage[T1]{fontenc} \usepackage[latin1]{inputenc} %% Times New Roman as roman font and Courier as typewriter font: \usepackage{times} %% Select Helvetica as default font: \renewcommand{\familydefault}{phv} \signature{} \place{} \address{\centerline{\scshape }} \backaddress{} \bottomtext{} \date{} \begin{document} \begin{letter}{\\ \\ \\[\medskipamount] {\bf }} \yourmail{} \subject{\bf } \opening{} \closing{} \encl{} \end{letter} \end{document} PyCoCuMa-0.4.5-6/templates/Letter.tex0000644000175000017500000000150510252657641020371 0ustar henninghenning00000000000000 ?> \documentclass[12pt]{letter} \usepackage[T1]{fontenc} \usepackage[latin1]{inputenc} %% Times New Roman as roman font and Courier as typewriter font: \usepackage{times} %% Select Helvetica as default font: \renewcommand{\familydefault}{phv} \signature{} \address{} \date{} \begin{document} \begin{letter}{\\ \\ \\[\medskipamount] {\bf }} \opening{} \closing{} \encl{} \end{letter} \end{document} PyCoCuMa-0.4.5-6/COPYING0000644000175000017500000004330210252657644015451 0ustar henninghenning00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. PyCoCuMa-0.4.5-6/ChangeLog0000644000175000017500000001742610311641350016157 0ustar henninghenning000000000000002005-09-13 Henning Jacobs * ADDED: --import and --export commandline switches * CHANGED: reduced window size of QuickFinder * ADDED: CTRL-Q closes the application * NEW VERSION: 0.4.5-6 2004-12-06 Henning Jacobs * ADDED: Py2Exe Win32 binary distribution * CHANGED: A "frozen" Pmw module is now included in the source distribution. * FIXED: Bug where contacts with empty FN disappeared. 2004-11-28 Henning Jacobs * ADDED: "Photo" and "Logo" image support. You can stick a photographic image to each contact (look under "Addtl.Fields") - this requires the Python Imaging Library (PIL). * ADDED: A status bar at the bottom of MainView. 2004-09-11 Henning Jacobs * ADDED: History in QuickFinder, Extended PreferenceDialog (Patch contributed by Stephan Helma ) 2004-09-07 Henning Jacobs * ADDED: Configurable speed buttons (Patch contributed by Stephan Helma ) 2004-05-22 Henning Jacobs * ADDED: XML-RDF Export of vCards, simple server-logging (enable in "~/pycocuma") * ADDED: Encoding-selection for Import and Export, all data is now written to disk as UTF-8! * NEW VERSION: 0.4.5 2004-05-08 Henning Jacobs * ADDED: "QuickFinder", try "pycocuma --finder" * FIXED: vC_datetime does not depend on the limited time module anymore 2004-04-04 Henning Jacobs * FIXED: converters.py: Import and Export; you can now import from CSV and map unknown fields to vCard fields. * NEW VERSION: 0.4.4-5 * ADDED: vCard XML-RDF representation 2004-03-28 Henning Jacobs * FIXED: Windows: There was a bug in debug.py: pythonw (noconsole) does not provide a correct sys.stdout file-object 2004-03-20 Henning Jacobs * ADDED: Preference Option: 'General Font'; changes font application wide, this is esp. useful on WinXP where a ugly fixed-width font is used by default. 2004-03-19 Henning Jacobs * ADDED: '--config=' commandline option * ENHANCED: texwrapper.py uses nonstopmode and cleans up afterwards (deletes .aux and .log files); Outputwin is only showed when errors occured while running LaTeX 2004-03-17 Henning Jacobs * NEW VERSION: 0.4.4-4 * ADDED: LetterComposer.py and TemplateProcessor.py * ENHANCED: converters.py: export2latex uses TemplateProcessor 2004-03-08 Henning Jacobs * FIXED: Export to Pine Addressbook * FIXED: Removed all TAB-characters from all .py-files, indent by 4 spaces (TABs in the source should be treated as BUGs), changed all 0's and 1's to Booleans where appropriate * ADDED: '--noconf' commandline switch * NEW VERSION: 0.4.4-3 * ENHANCED: On Unix: thin scrollbars, 'MouseOver'-button highlighting * FIXED: Windows: Click on an email-address to start your default mail-application (usually Outlook) 2004-03-06 Henning Jacobs * FIXED: PyCoCuMa wouldn't start with empty db * ADDED: PreferencesDialog.py 2004-03-05 Henning Jacobs * ENHANCED: CalendarWidget: displays week-number and adjoining days of prev. and next month 2004-03-04 Henning Jacobs * NEW VERSION: 0.4.4-2 * ADDED: Journal: TimeStart and TimeEnd; fixed vC_datetime to change date and time independently (Warning: Time is UTC!) 2004-03-02 Henning Jacobs * ADDED: btnDuplicateContact and btnExportContact = duplicate a card and export a single card to file * ADDED: PyCoCuMa now recognizes command-line options, try "pycocuma --help" for details * NEW VERSION: 0.4.4-1 * ENHANCED: You can now 'connect' the Journal with the current contact this means: only journal entries with this contact as an attendee are shown and when you create a new journal entry, this contact will be added to the attendee-list. 2004-02-29 Henning Jacobs * FIXED: TextEdit is now Pmw.EntryField and saving is done via 'modifiedcommand' 2004-02-28 Henning Jacobs * FIXED: searching (Find Contact) * REMOVED: some old, unused modules * ADDED: Calendar(Window|Widget).py displays a nice month calendar and is connected with the JournalWindow * ENHANCED: The F1-F5 keys operate application-wide and also raise the respective window (gives the user a convenient switching system) * NEW VERSION: 0.4.4 2004-02-27 Henning Jacobs * FIXED: MultiColumnTextList.py: tag/selection update was very buggy * NEW VERSION: 0.4.3-1 * ENHANCED: revised the HelpWindow and replaced readme text with new help text (using Tk's text tags to define headlines, bold text, etc) The help window will display on first startup automatically. * NEW VERSION: 0.4.3-2 2004-02-26 Henning Jacobs * ADDED: List View of contacts, using MultiColumnTextList (enhanced) * NEW VERSION: 0.4.3 * FIXED: Made all dialogs inherit from Pmw.Dialog (common look&feel), replaced Tk() calls with Pmw.initialise(); Use Pmw more often! * REMOVED: RFCs from dist-package: You can download rfc2425, rfc2426 (vCard) and rfc2446 (iCal) from http://www.imc.org 2004-02-25 Henning Jacobs * ADDED: Journal * FIXED: some problems with vC_params 2004-02-22 Henning Jacobs * FIXED: vCard Line Folding was incorrect: continue a logical line by only one single space character (or tab) (see rfc2425.txt) * ENHANCED: RFC-2425 core types and functions are now in vcore.py * ADDED: vcalendar.py module (based on vcard.py) and introduced new calendar functions in CoCuMa_Server.py. vcalendar.py is NOT A FULL FLEDGED iCalendar implementation! 2004-02-21 Henning Jacobs * ADDED: You can Disconnect and Connect from the File menu. Direct file access (instead of xmlrpc) is now the default on win32. * NEW VERSION: 0.4.2-4 2004-02-18 Henning Jacobs * ADDED: A basic Test Suite (test* files), run testall.py to test the most important components (vCard, server) 2004-02-17 Henning Jacobs * NEW VERSION: 0.4.2-3 * FIXED: If you add a new contact, it automatically gets the current filter category * FIXED: The address and email types are now displayed on the card as icons, too. The card's size is now 500x400 px. 2004-02-15 Henning Jacobs * NEW VERSION: 0.4.2-2 * ADDED: ContactEditWidget.py: Edit Additional vCard Fields: Sort-String, Mailer, Key, Timezone, Global Position, Address Label * ADDED: ContactEditWidget.py: Generate Unique Identifier (Button) * FIXED: on first startup (with empty DB): if you edited the empty card and tried to save it, there occured a "cannot marshal None" error 2004-02-13 Henning Jacobs * FIXED: A lot of bugs (I hope); cleaned up (deleted old code) and more events are handled by broadcaster (Contact Save/New/Del) * ENHANCED: I have done some optimizations in ContactListWidget.py and ContactSelectboxWidget.py; the list will not be created completely new anymore * NEW VERSION: 0.4.2-1 2004-02-12 Henning Jacobs * ENHANCED: MainView.py: iconmask * FIXED: Help: show docstring when README ist not available * ADDED: I created a nice SplashScreen Image with The Gimp! 2004-02-11 Henning Jacobs * NEW VERSION: 0.4.2 * ENHANCED: ContactEditWidget.py: New Icon "assignfn" for Button "Take From Name" * FIXED: vcard.py: vCardList.forgetLastReturnedField() fixes bug: when only one card was in the list it didn't show up because the same field was queried two times (returns empty string on snd query!) * ADDED: MainView.py: New Button: btnSaveContact * ADDED: MainView.py: askDelContact() * ADDED: ChangeLog File (this one!) PyCoCuMa-0.4.5-6/INSTALL0000644000175000017500000000273510252657644015454 0ustar henninghenning00000000000000# $Id: INSTALL 83 2004-07-11 13:12:44Z henning $ Installation Instructions: -------------------------- Prerequisites: You need at least Python 2.3 with Tk 8.3 and Tkinter. You will also need the Pmw Megawidget package (Pmw 1.2) and the Python Imaging Library (PIL). Tk and Tkinter are shipped with Python by default, so for Windows users: Just download the current Python Distribution from http://www.python.org . PyCoCuMa was tested on the following platforms: - GNU/Linux Debian (Sarge), Python 2.3 - Windows 2000 with Python 2.3.3 and Pmw 1.2 INSTALLATION ON UNIX/LINUX: 1.) Unpack the tarball: ~/tmp$ tar -xzvf PyCoCuMa-.tar.gz 2.) Enter the newly created directory: ~/tmp$ cd PyCoCuMa- 3.) Simply run the setup script: ~/tmp/PyCoCuMa-$ python setup.py install Where is the version number of your downloaded package, e.g. 0.3.1. This will install the PyCoCuMa libraries into your local Python installation (usually something like /usr/lib/python2.3/site-packages/) and copies the two executable scripts 'pycocuma' and 'pycocuma-server' to your /usr/bin directory (or /usr/local/bin, see below). Try 'setup.py --help install' for a list of available install options. If you prefer installing to /usr/local rather than /usr, try 'setup.py install --prefix=/usr/local' for example. To start PyCoCuMa run 'pycocuma'. INSTALLATION ON WINDOWS: 1.) Unpack the tarball to any directory you like (e.g. c:\Program Files\) 2.) Double-clicking 'pycocuma.pyw' should do. PyCoCuMa-0.4.5-6/README0000644000175000017500000000520210311640650015254 0ustar henninghenning00000000000000# $Id: README 142 2005-09-13 21:16:23Z henning $ PyCoCuMa README --------------- PyCoCuMa (Pythonic Contact and Customer Management) provides an personal information system for addresses, telephone numbers and other data associated with personal contacts. PyCoCuMa is purely written in Python with a Tk graphical interface. PyCoCuMa is based on an XML-RPC client-server architecture. The server stores it's data in compatible vCard (ver. 3.0) files (*.vcf) which can be read by all modern address programs (Evolution, KAddressbook, Outlook, GnomeCard, etc). For Installation Instructions see the file named 'INSTALL'. You can.. ..open vCard files directly or over PyCoCuMa's XML-RPC server ..define unlimited mail/email-addresses and tel-numbers for a contact ..designate addresses/tel-numbers as 'preferred', 'home', 'work', 'mobile', etc ..filter the contact list by category (e.g. 'Business', 'Personal', ..) ..search in all vCard fields for regular expressions ("Query"->"Find Contact.." or CTRL-F) ..start your favourite mail-agent or web-browser by clicking on an Email/URL ..get a list of persons birthdays ("Query"->"Birthdays") ..export your contacts to CSV files (KSpread and Microsoft Excel compatible). ..import from CSV files ..create a PDF document (for print-out) via LaTeX ("File"->"Page View") ..keep a journal of actions (Appointments, Phone Calls, etc; with calendar) ..compose letters (requires pdfLaTeX) ..stick photo images (e.g. JPEG) to vCards (requires PIL) ..do a lot more ;-) CONFIGURATION: PyCoCuMa stores it's preferences in ~/.pycocuma (INI-File) by default. Leave the option "url_viewer" blank to use Python's internal 'Webbrowser' interface-module. Set "autostart_server" to "yes" (this is the default) if you are using PyCoCuMa only locally and don't wan't to hassle with the server (you could also start the server at boot time or run it on another machine). COMMAND-LINE OPTIONS: Usage: pycocuma [options] [connection-string] connection-string addressbook filename / server url -h or --help show this help -c or --config location of configuration file (default: ~/.pycocuma) --noconf do not save configuration / preferences -t or --type connection type: xmlrpc or file (default: file) --finder open QuickFinder --import import from file/stdin (-) (e.g. vcard:/tmp/John.vcf) --export export to file or stdout (e.g. latex:-) Example: 'pycocuma --type=file ./addressbook.vcf' will open the file ./addressbook.vcf from disk without starting any server. PyCoCuMa was programmed by Henning Jacobs . PyCoCuMa's Homepage: http://www.srcco.de PyCoCuMa-0.4.5-6/TODO0000644000175000017500000000156310252657644015111 0ustar henninghenning00000000000000# $Id: TODO 92 2004-11-28 15:34:44Z henning $ TODO (Wish List): ----------------- - better journal/calendar support - customer handling, invoice printing - server security (host-/ password authentication) - integration with other programs like GnuCash - address syncronization - import XML files (?) - world map for TimeZone and GlobalPosition - needs a lot of optimization (filtering, server comm., ..) - import from CSV (*done!*) - export to vCard (*done! 2004-01-29*) - filter contact list by category (*done! 2004-02-06*) - contact/customer journal (entries like Appointment, Phone Call, ..) (*done! 2004-02-26*) - settings dialog (fonts, colors, sorting, and other preferences) (*done! 2004-03-06*) - integrated letter composer with LaTeX printing and auto. address handling (*done! 2004-03-17*) - support picture(s) in vCards (e.g. photo, company logo) (*done! 2004-11-28*) PyCoCuMa-0.4.5-6/pycocuma0000755000175000017500000000100210252657644016153 0ustar henninghenning00000000000000#!/usr/bin/python """\ PyCoCuMa (Pythonic Contact and Customer Management) provides an personal information system for addresses, telephone numbers and other data associated with personal contacts. PyCoCuMa stores it's data in compatible vCard files (*.vcf). PyCoCuMa was programmed by Henning Jacobs . PyCoCuMa's Homepage: http://www.srcco.de """ # $Id: pycocuma 83 2004-07-11 13:12:44Z henning $ if __name__ == "__main__": import pycocumalib.pycocuma pycocumalib.pycocuma.run() PyCoCuMa-0.4.5-6/pycocuma-server0000755000175000017500000000120310252657644017462 0ustar henninghenning00000000000000#!/usr/bin/python """\ PyCoCuMa (Pythonic Contact and Customer Management) provides an personal information system for addresses, telephone numbers and other data associated with personal contacts. PyCoCuMa stores it's data in compatible vCard files (*.vcf). This is the PyCoCuMa-XMLRPCServer startup file. This file is usually executed by pycocuma automagically when run the first time since system boot. PyCoCuMa was programmed by Henning Jacobs . PyCoCuMa's Homepage: http://www.srcco.de """ # $Id: pycocuma-server 83 2004-07-11 13:12:44Z henning $ import pycocumalib.CoCuMa_Server pycocumalib.CoCuMa_Server.run() PyCoCuMa-0.4.5-6/pycocuma.ico0000644000175000017500000000157610252657644016741 0ustar henninghenning00000000000000h(    m  g 0( wy|&'0' vw S$#%ooq-+=,0%#-3*VRwRN#,>3Ѿ/'!$s1*! !`Y:1~P3(%t'  \vWT}VXSBALeeeccd. {)~۸ GRRR\\\ddeSJ1>2wȄar666WSxda'#QSWͻ\S0()$;>567[]_^_`˹(TDDDl|׼HKL@?[meR\\SUcntw쉐8<>𙜞!##ب?PyCoCuMa-0.4.5-6/pycocuma.pyw0000755000175000017500000000107510252657644017003 0ustar henninghenning00000000000000#!/usr/bin/python """\ PyCoCuMa (Pythonic Contact and Customer Management) provides an personal information system for addresses, telephone numbers and other data associated with personal contacts. PyCoCuMa stores it's data in compatible vCard files (*.vcf). PyCoCuMa was programmed by Henning Jacobs . PyCoCuMa's Homepage: http://www.srcco.de This is the Windows (without-console) executable, should run by a simple double-click from windows explorer. """ if __name__ == "__main__": import pycocumalib.pycocuma pycocumalib.pycocuma.run() PyCoCuMa-0.4.5-6/pycocuma.xbm0000644000175000017500000000050010252657644016737 0ustar henninghenning00000000000000/* Erzeugt mit Gimp */ #define pycocuma_width 16 #define pycocuma_height 16 static unsigned char pycocuma_bits[] = { 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 0x00, 0x94, 0x1e, 0xbc, 0x37, 0xfc, 0x29, 0xfc, 0x21, 0xfc, 0x20, 0xfc, 0x41, 0xfc, 0x60, 0xfc, 0x4c, 0xfc, 0x63, 0xfc, 0x39, 0xf8, 0x01, 0xe0, 0x00 }; PyCoCuMa-0.4.5-6/pycocuma_mask.xbm0000644000175000017500000000051710252657644017762 0ustar henninghenning00000000000000/* Erzeugt mit Gimp */ #define pycocuma_mask_width 16 #define pycocuma_mask_height 16 static unsigned char pycocuma_mask_bits[] = { 0x80, 0x01, 0xf0, 0x03, 0xf8, 0x07, 0xf8, 0x0f, 0xfc, 0x1f, 0xfc, 0x3f, 0xfc, 0x3f, 0xfc, 0x3f, 0xfc, 0x3f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x7f, 0xfc, 0x3f, 0xf8, 0x01, 0xe0, 0x00 }; PyCoCuMa-0.4.5-6/setup.py0000755000175000017500000000436510252657644016141 0ustar henninghenning00000000000000#!/usr/bin/python # setup.py # # $Id: setup.py 95 2004-12-11 20:50:34Z henning $ from distutils.core import setup import pycocumalib.__version__ import sys if sys.platform == "win32": try: import py2exe except: print "INFO: py2exe not found." class Win32Target: def __init__(self, **kw): self.__dict__.update(kw) self.version = pycocumalib.__version__.__version__.replace("-", ".") self.company_name = "Henning Jacobs" self.copyright = "(c)2004 Henning Jacobs" self.name = "PyCoCuMa" setup(name="PyCoCuMa", version = pycocumalib.__version__.__version__, description = "Pythonic Contact and Customer Management", long_description = """PyCoCuMa (Pythonic Contact and Customer Management) provides an personal information system for addresses, telephone numbers and other data associated with personal contacts (also supports photographic pictures). PyCoCuMa is purely written in Python with a Tk graphical interface. PyCoCuMa is based on an XML-RPC client-server architecture. The server stores it's data in compatible vCard (ver. 3.0) files (*.vcf) which can be read by all modern address programs (Evolution, KAddressbook, Outlook, GnomeCard, etc).""", author = "Henning Jacobs", author_email = "henning@srcco.de", url = "http://www.srcco.de", scripts = ["pycocuma","pycocuma-server"], packages = ["pycocumalib"], classifiers = ["Development Status :: 3 - Alpha", "Environment :: Win32 (MS Windows)", "Environment :: X11 Applications", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: GNU General Public License (GPL)", "Natural Language :: English", "Operating System :: Microsoft :: Windows :: Windows 95/98/2000", "Operating System :: Microsoft :: Windows :: Windows NT/2000", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Topic :: Office/Business"], # for py2exe: windows = [Win32Target(script = "pycocuma.pyw", icon_resources=[(1, "pycocuma.ico")])], zipfile = "pycocumalib.zip", options = {"py2exe": {"compressed": 1, "optimize": 2, "dist_dir": "dist/win32", "includes": "JpegImagePlugin,PngImagePlugin,BmpImagePlugin,GifImagePlugin"}}, ) PyCoCuMa-0.4.5-6/PKG-INFO0000644000175000017500000000260110311641557015500 0ustar henninghenning00000000000000Metadata-Version: 1.0 Name: PyCoCuMa Version: 0.4.5-6 Summary: Pythonic Contact and Customer Management Home-page: http://www.srcco.de Author: Henning Jacobs Author-email: henning@srcco.de License: UNKNOWN Description: PyCoCuMa (Pythonic Contact and Customer Management) provides an personal information system for addresses, telephone numbers and other data associated with personal contacts (also supports photographic pictures). PyCoCuMa is purely written in Python with a Tk graphical interface. PyCoCuMa is based on an XML-RPC client-server architecture. The server stores it's data in compatible vCard (ver. 3.0) files (*.vcf) which can be read by all modern address programs (Evolution, KAddressbook, Outlook, GnomeCard, etc). Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Win32 (MS Windows) Classifier: Environment :: X11 Applications Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Natural Language :: English Classifier: Operating System :: Microsoft :: Windows :: Windows 95/98/2000 Classifier: Operating System :: Microsoft :: Windows :: Windows NT/2000 Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Topic :: Office/Business