From e56ea14ab8531ee3cec375460577d1b89bf62e26 Mon Sep 17 00:00:00 2001 From: Liong Sauw Ming Date: Thu, 16 Jan 2014 05:30:46 +0000 Subject: Closed #1723: Merging pjsua2 branch into trunk git-svn-id: http://svn.pjsip.org/repos/pjproject/trunk@4704 74dad513-b988-da41-8d7b-12977e46ad98 --- pjsip-apps/src/pygui/application.py | 510 ++++++++++++++++++++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 pjsip-apps/src/pygui/application.py (limited to 'pjsip-apps/src/pygui/application.py') diff --git a/pjsip-apps/src/pygui/application.py b/pjsip-apps/src/pygui/application.py new file mode 100644 index 00000000..0dd5fa50 --- /dev/null +++ b/pjsip-apps/src/pygui/application.py @@ -0,0 +1,510 @@ +# $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() -- cgit v1.2.3