| # $Id: application.py 4704 2014-01-16 05:30:46Z ming $ |
| # |
| # 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('<Control-1>', self._onTvRightClick) |
| else: |
| self.tv.bind('<3>', self._onTvRightClick) |
| self.tv.bind('<Double-Button-1>', 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() |