"""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