# $Id$ # # pjsua Python GUI Demo # # Copyright (C)2013 Teluu Inc. (http://www.teluu.com) # # 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 # import sys if sys.version_info[0] >= 3: # Python 3 import tkinter as tk from tkinter import ttk from tkinter import messagebox as msgbox else: import Tkinter as tk import tkMessageBox as msgbox import ttk import pjsua2 as pj import log import accountsetting import account import buddy import endpoint import settings import os import traceback class Application(ttk.Frame): """ The Application main frame. """ def __init__(self): ttk.Frame.__init__(self, name='application', width=300, height=500) self.pack(expand='yes', fill='both') self.master.title('pjsua2 Demo') self.master.geometry('500x500+100+100') # Logger self.logger = log.Logger() # Accounts self.accList = [] # GUI variables self.showLogWindow = tk.IntVar(value=0) self.quitting = False # Construct GUI self._createWidgets() # Log window self.logWindow = log.LogWindow(self) self._onMenuShowHideLogWindow() # Instantiate endpoint self.ep = endpoint.Endpoint() self.ep.libCreate() # Default config self.appConfig = settings.AppConfig() self.appConfig.epConfig.uaConfig.threadCnt = 0; self.appConfig.epConfig.uaConfig.mainThreadOnly = True self.appConfig.epConfig.logConfig.writer = self.logger self.appConfig.epConfig.logConfig.filename = "pygui.log" self.appConfig.epConfig.logConfig.fileFlags = pj.PJ_O_APPEND self.appConfig.epConfig.logConfig.level = 5 self.appConfig.epConfig.logConfig.consoleLevel = 5 def saveConfig(self, filename='pygui.js'): # Save disabled accounts since they are not listed in self.accList disabled_accs = [ac for ac in self.appConfig.accounts if not ac.enabled] self.appConfig.accounts = [] # Get account configs from active accounts for acc in self.accList: acfg = settings.AccConfig() acfg.enabled = True acfg.config = acc.cfg for bud in acc.buddyList: acfg.buddyConfigs.append(bud.cfg) self.appConfig.accounts.append(acfg) # Put back disabled accounts self.appConfig.accounts.extend(disabled_accs) # Save self.appConfig.saveFile(filename) def start(self, cfg_file='pygui.js'): # Load config if cfg_file and os.path.exists(cfg_file): self.appConfig.loadFile(cfg_file) self.appConfig.epConfig.uaConfig.threadCnt = 0; self.appConfig.epConfig.uaConfig.mainThreadOnly = True self.appConfig.epConfig.logConfig.writer = self.logger self.appConfig.epConfig.logConfig.level = 5 self.appConfig.epConfig.logConfig.consoleLevel = 5 # Initialize library self.appConfig.epConfig.uaConfig.userAgent = "pygui-" + self.ep.libVersion().full; self.ep.libInit(self.appConfig.epConfig) self.master.title('pjsua2 Demo version ' + self.ep.libVersion().full) # Create transports if self.appConfig.udp.enabled: self.ep.transportCreate(self.appConfig.udp.type, self.appConfig.udp.config) if self.appConfig.tcp.enabled: self.ep.transportCreate(self.appConfig.tcp.type, self.appConfig.tcp.config) if self.appConfig.tls.enabled: self.ep.transportCreate(self.appConfig.tls.type, self.appConfig.tls.config) # Add accounts for cfg in self.appConfig.accounts: if cfg.enabled: self._createAcc(cfg.config) acc = self.accList[-1] for buddy_cfg in cfg.buddyConfigs: self._createBuddy(acc, buddy_cfg) # Start library self.ep.libStart() # Start polling self._onTimer() def updateAccount(self, acc): if acc.deleting: return # ignore iid = str(acc.randId) text = acc.cfg.idUri status = acc.statusText() values = (status,) if self.tv.exists(iid): self.tv.item(iid, text=text, values=values) else: self.tv.insert('', 'end', iid, open=True, text=text, values=values) def updateBuddy(self, bud): iid = 'buddy' + str(bud.randId) text = bud.cfg.uri status = bud.statusText() values = (status,) if self.tv.exists(iid): self.tv.item(iid, text=text, values=values) else: self.tv.insert(str(bud.account.randId), 'end', iid, open=True, text=text, values=values) def _createAcc(self, acc_cfg): acc = account.Account(self) acc.cfg = acc_cfg self.accList.append(acc) self.updateAccount(acc) acc.create(acc.cfg) acc.cfgChanged = False self.updateAccount(acc) def _createBuddy(self, acc, buddy_cfg): bud = buddy.Buddy(self) bud.cfg = buddy_cfg bud.account = acc bud.create(acc, bud.cfg) self.updateBuddy(bud) acc.buddyList.append(bud) def _createWidgets(self): self._createAppMenu() # Main pane, a Treeview self.tv = ttk.Treeview(self, columns=('Status'), show='tree') self.tv.pack(side='top', fill='both', expand='yes', padx=5, pady=5) self._createContextMenu() # Handle close event self.master.protocol("WM_DELETE_WINDOW", self._onClose) def _createAppMenu(self): # Main menu bar top = self.winfo_toplevel() self.menubar = tk.Menu() top.configure(menu=self.menubar) # File menu file_menu = tk.Menu(self.menubar, tearoff=False) self.menubar.add_cascade(label="File", menu=file_menu) file_menu.add_command(label="Add account..", command=self._onMenuAddAccount) file_menu.add_checkbutton(label="Show/hide log window", command=self._onMenuShowHideLogWindow, variable=self.showLogWindow) file_menu.add_separator() file_menu.add_command(label="Settings...", command=self._onMenuSettings) file_menu.add_command(label="Save Settings", command=self._onMenuSaveSettings) file_menu.add_separator() file_menu.add_command(label="Quit", command=self._onMenuQuit) # Window menu self.window_menu = tk.Menu(self.menubar, tearoff=False) self.menubar.add_cascade(label="Window", menu=self.window_menu) # Help menu help_menu = tk.Menu(self.menubar, tearoff=False) self.menubar.add_cascade(label="Help", menu=help_menu) help_menu.add_command(label="About", underline=2, command=self._onMenuAbout) def _showChatWindow(self, chat_inst): chat_inst.showWindow() def updateWindowMenu(self): # Chat windows self.window_menu.delete(0, tk.END) for acc in self.accList: for c in acc.chatList: cmd = lambda arg=c: self._showChatWindow(arg) self.window_menu.add_command(label=c.title, command=cmd) def _createContextMenu(self): top = self.winfo_toplevel() # Create Account context menu self.accMenu = tk.Menu(top, tearoff=False) # Labels, must match with _onAccContextMenu() labels = ['Unregister', 'Reregister', 'Add buddy...', '-', 'Online', 'Invisible', 'Away', 'Busy', '-', 'Settings...', '-', 'Delete...'] for label in labels: if label=='-': self.accMenu.add_separator() else: cmd = lambda arg=label: self._onAccContextMenu(arg) self.accMenu.add_command(label=label, command=cmd) # Create Buddy context menu # Labels, must match with _onBuddyContextMenu() self.buddyMenu = tk.Menu(top, tearoff=False) labels = ['Audio call', 'Send instant message', '-', 'Subscribe', 'Unsubscribe', '-', 'Settings...', '-', 'Delete...'] for label in labels: if label=='-': self.buddyMenu.add_separator() else: cmd = lambda arg=label: self._onBuddyContextMenu(arg) self.buddyMenu.add_command(label=label, command=cmd) if (top.tk.call('tk', 'windowingsystem')=='aqua'): self.tv.bind('<2>', self._onTvRightClick) self.tv.bind('', self._onTvRightClick) else: self.tv.bind('<3>', self._onTvRightClick) self.tv.bind('', self._onTvDoubleClick) def _getSelectedAccount(self): items = self.tv.selection() if not items: return None try: iid = int(items[0]) except: return None accs = [acc for acc in self.accList if acc.randId==iid] if not accs: return None return accs[0] def _getSelectedBuddy(self): items = self.tv.selection() if not items: return None try: iid = int(items[0][5:]) iid_parent = int(self.tv.parent(items[0])) except: return None accs = [acc for acc in self.accList if acc.randId==iid_parent] if not accs: return None buds = [b for b in accs[0].buddyList if b.randId==iid] if not buds: return None return buds[0] def _onTvRightClick(self, event): iid = self.tv.identify_row(event.y) #iid = self.tv.identify('item', event.x, event.y) if iid: self.tv.selection_set( (iid,) ) acc = self._getSelectedAccount() if acc: self.accMenu.post(event.x_root, event.y_root) else: # A buddy is selected self.buddyMenu.post(event.x_root, event.y_root) def _onTvDoubleClick(self, event): iid = self.tv.identify_row(event.y) if iid: self.tv.selection_set( (iid,) ) acc = self._getSelectedAccount() if acc: self.cfgChanged = False dlg = accountsetting.Dialog(self.master, acc.cfg) if dlg.doModal(): self.updateAccount(acc) acc.modify(acc.cfg) else: bud = self._getSelectedBuddy() acc = bud.account chat = acc.findChat(bud.cfg.uri) if not chat: chat = acc.newChat(bud.cfg.uri) chat.showWindow() def _onAccContextMenu(self, label): acc = self._getSelectedAccount() if not acc: return if label=='Unregister': acc.setRegistration(False) elif label=='Reregister': acc.setRegistration(True) elif label=='Online': ps = pj.PresenceStatus() ps.status = pj.PJSUA_BUDDY_STATUS_ONLINE acc.setOnlineStatus(ps) elif label=='Invisible': ps = pj.PresenceStatus() ps.status = pj.PJSUA_BUDDY_STATUS_OFFLINE acc.setOnlineStatus(ps) elif label=='Away': ps = pj.PresenceStatus() ps.status = pj.PJSUA_BUDDY_STATUS_ONLINE ps.activity = pj.PJRPID_ACTIVITY_AWAY ps.note = "Away" acc.setOnlineStatus(ps) elif label=='Busy': ps = pj.PresenceStatus() ps.status = pj.PJSUA_BUDDY_STATUS_ONLINE ps.activity = pj.PJRPID_ACTIVITY_BUSY ps.note = "Busy" acc.setOnlineStatus(ps) elif label=='Settings...': self.cfgChanged = False dlg = accountsetting.Dialog(self.master, acc.cfg) if dlg.doModal(): self.updateAccount(acc) acc.modify(acc.cfg) elif label=='Delete...': msg = "Do you really want to delete account '%s'?" % acc.cfg.idUri if msgbox.askquestion('Delete account?', msg, default=msgbox.NO) != u'yes': return iid = str(acc.randId) self.accList.remove(acc) acc.setRegistration(False) acc.deleting = True del acc self.tv.delete( (iid,) ) elif label=='Add buddy...': cfg = pj.BuddyConfig() dlg = buddy.SettingDialog(self.master, cfg) if dlg.doModal(): self._createBuddy(acc, cfg) else: assert not ("Unknown menu " + label) def _onBuddyContextMenu(self, label): bud = self._getSelectedBuddy() if not bud: return acc = bud.account if label=='Audio call': chat = acc.findChat(bud.cfg.uri) if not chat: chat = acc.newChat(bud.cfg.uri) chat.showWindow() chat.startCall() elif label=='Send instant message': chat = acc.findChat(bud.cfg.uri) if not chat: chat = acc.newChat(bud.cfg.uri) chat.showWindow(True) elif label=='Subscribe': bud.subscribePresence(True) elif label=='Unsubscribe': bud.subscribePresence(False) elif label=='Settings...': subs = bud.cfg.subscribe uri = bud.cfg.uri dlg = buddy.SettingDialog(self.master, bud.cfg) if dlg.doModal(): self.updateBuddy(bud) # URI updated? if uri != bud.cfg.uri: cfg = bud.cfg # del old iid = 'buddy' + str(bud.randId) acc.buddyList.remove(bud) del bud self.tv.delete( (iid,) ) # add new self._createBuddy(acc, cfg) # presence subscribe setting updated elif subs != bud.cfg.subscribe: bud.subscribePresence(bud.cfg.subscribe) elif label=='Delete...': msg = "Do you really want to delete buddy '%s'?" % bud.cfg.uri if msgbox.askquestion('Delete buddy?', msg, default=msgbox.NO) != u'yes': return iid = 'buddy' + str(bud.randId) acc.buddyList.remove(bud) del bud self.tv.delete( (iid,) ) else: assert not ("Unknown menu " + label) def _onTimer(self): if not self.quitting: self.ep.libHandleEvents(10) if not self.quitting: self.master.after(50, self._onTimer) def _onClose(self): self.saveConfig() self.quitting = True self.ep.libDestroy() self.ep = None self.update() self.quit() def _onMenuAddAccount(self): cfg = pj.AccountConfig() dlg = accountsetting.Dialog(self.master, cfg) if dlg.doModal(): self._createAcc(cfg) def _onMenuShowHideLogWindow(self): if self.showLogWindow.get(): self.logWindow.deiconify() else: self.logWindow.withdraw() def _onMenuSettings(self): dlg = settings.Dialog(self, self.appConfig) if dlg.doModal(): msgbox.showinfo(self.master.title(), 'You need to restart for new settings to take effect') def _onMenuSaveSettings(self): self.saveConfig() def _onMenuQuit(self): self._onClose() def _onMenuAbout(self): msgbox.showinfo(self.master.title(), 'About') class ExceptionCatcher: """Custom Tk exception catcher, mainly to display more information from pj.Error exception """ def __init__(self, func, subst, widget): self.func = func self.subst = subst self.widget = widget def __call__(self, *args): try: if self.subst: args = apply(self.subst, args) return apply(self.func, args) except pj.Error, error: print 'Exception:' print ' ', error.info() print 'Traceback:' print traceback.print_stack() log.writeLog2(1, 'Exception: ' + error.info() + '\n') except Exception, error: print 'Exception:' print ' ', str(error) print 'Traceback:' print traceback.print_stack() log.writeLog2(1, 'Exception: ' + str(error) + '\n') def main(): #tk.CallWrapper = ExceptionCatcher app = Application() app.start() app.mainloop() if __name__ == '__main__': main()