Re #1708 (PyGUI: New Python GUI Application based on pjsua2+SWIG API)
Initial implementation, some account functionality has been implemented. Details: - Uses Tk for GUI and pickle for object serialization. These are Python built-in modules - So far so good, everything (=Account API only so far!) seems to work, including: - STL string - STL vector - inheritance - calling callback that is implemented in Python with inheritance (the "director" feature") - Some notes though: - SIP worker threads need to be disabled because Tk GUI cannot be called from other threads (the worker thread will invoke callback which in turn will update GUI) - Features implemented in the GUI so far: - Account addition/deletion/modification - Registration - Logging to window - Persistent config (limited) git-svn-id: https://svn.pjsip.org/repos/pjproject/branches/projects/pjsua2@4640 74dad513-b988-da41-8d7b-12977e46ad98
This commit is contained in:
parent
f673a6ef92
commit
64ff3f68b4
|
@ -0,0 +1,168 @@
|
|||
# $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 random
|
||||
import pjsua2 as pj
|
||||
import _pjsua2
|
||||
import accountsetting
|
||||
import application
|
||||
|
||||
# Account class
|
||||
class Account(pj.Account):
|
||||
"""
|
||||
High level Python Account object, derived from pjsua2's Account object.
|
||||
"""
|
||||
def __init__(self, app):
|
||||
pj.Account.__init__(self)
|
||||
self.app = app
|
||||
self.randId = random.randint(1, 9999)
|
||||
self.cfg = pj.AccountConfig()
|
||||
self.cfgChanged = False
|
||||
|
||||
def statusText(self):
|
||||
status = '?'
|
||||
if self.isValid():
|
||||
ai = self.getInfo()
|
||||
if ai.regIsActive:
|
||||
if ai.onlineStatus:
|
||||
if len(ai.onlineStatusText):
|
||||
status = ai.onlineStatusText
|
||||
else:
|
||||
status = "Online"
|
||||
else:
|
||||
status = "Registered"
|
||||
else:
|
||||
if ai.regIsConfigured:
|
||||
if ai.regStatus/100 == 2:
|
||||
status = "Unregistered"
|
||||
else:
|
||||
status = ai.regStatusText
|
||||
else:
|
||||
status = "Doesn't register"
|
||||
else:
|
||||
status = '- not created -'
|
||||
return status
|
||||
|
||||
def onRegState(self, prm):
|
||||
self.app.updateAccount(self)
|
||||
|
||||
|
||||
# Account frame, to list accounts
|
||||
class AccountListFrame(ttk.Frame):
|
||||
"""
|
||||
This implements a Frame which contains account list and buttons to operate
|
||||
on them (Add, Modify, Delete, etc.).
|
||||
"""
|
||||
def __init__(self, parent, app, acc_list = []):
|
||||
ttk.Frame.__init__(self, parent, name='acclist')
|
||||
self.app = app
|
||||
self.accList = acc_list
|
||||
self.accDeletedList = []
|
||||
self.pack(expand='yes', fill='both')
|
||||
self._createWidgets()
|
||||
for acc in self.accList:
|
||||
self._showAcc(acc)
|
||||
|
||||
def _createWidgets(self):
|
||||
self.tv = ttk.Treeview(self, columns=('ID', 'Registrar', 'Default'), selectmode='browse')
|
||||
self.tv.heading('#0', text='Priority')
|
||||
self.tv.heading(0, text='ID')
|
||||
self.tv.heading(1, text='Registrar')
|
||||
self.tv.heading(2, text='Default?')
|
||||
self.tv.column('#0', width=60)
|
||||
self.tv.column(0, width=300)
|
||||
self.tv.column(1, width=200)
|
||||
self.tv.column(2, width=60)
|
||||
self.tv.grid(column=0, row=0, rowspan=4, padx=5, pady=5)
|
||||
|
||||
ttk.Button(self, text='Add..', command=self._onBtnAdd).grid(column=1, row=0, padx=5)
|
||||
ttk.Button(self, text='Settings..', command=self._onBtnSettings).grid(column=1, row=1)
|
||||
ttk.Button(self, text='Set Default', command=self._onBtnSetDefault).grid(column=1, row=2)
|
||||
ttk.Button(self, text='Delete..', command=self._onBtnDelete).grid(column=1, row=3)
|
||||
|
||||
def _showAcc(self, acc):
|
||||
is_default = 'Yes' if acc.isValid() and acc.isDefault() else ''
|
||||
values = (acc.cfg.idUri, acc.cfg.regConfig.registrarUri, is_default)
|
||||
self.tv.insert('', 0, str(acc.randId), open=True, text=str(acc.cfg.priority), values=values)
|
||||
|
||||
def updateAccount(self, acc):
|
||||
is_default = 'Yes' if acc.isValid() and acc.isDefault() else ''
|
||||
values = (acc.cfg.idUri, acc.cfg.regConfig.registrarUri, is_default)
|
||||
self.tv.item(str(acc.randId), text=str(acc.cfg.priority), values=values)
|
||||
|
||||
def _getSelectedAcc(self):
|
||||
items = self.tv.selection()
|
||||
if not items:
|
||||
return None
|
||||
iid = int(items[0])
|
||||
return [acc for acc in self.accList if acc.randId==iid][0]
|
||||
|
||||
def _onBtnAdd(self):
|
||||
cfg = pj.AccountConfig()
|
||||
dlg = accountsetting.Dialog(self.master, cfg)
|
||||
if dlg.doModal():
|
||||
acc = Account(self.app)
|
||||
acc.cfg = cfg
|
||||
self._showAcc(acc)
|
||||
self.accList.append(acc)
|
||||
self.cfgChanged = True
|
||||
|
||||
def _onBtnSettings(self):
|
||||
acc = self._getSelectedAcc()
|
||||
if not acc:
|
||||
return
|
||||
dlg = accountsetting.Dialog(self.master, acc.cfg)
|
||||
if dlg.doModal():
|
||||
self.updateAccount(acc)
|
||||
self.cfgChanged = True
|
||||
|
||||
def _onBtnDelete(self):
|
||||
acc = self._getSelectedAcc()
|
||||
if not acc:
|
||||
return
|
||||
msg = "Do you really want to delete account '%s'" % acc.cfg.idUri
|
||||
if msgbox.askquestion('Delete account?', msg, default=msgbox.NO) != u'yes':
|
||||
return
|
||||
self.accList.remove(acc)
|
||||
self.accDeletedList.append(acc)
|
||||
self.tv.delete( (str(acc.randId),) )
|
||||
|
||||
def _onBtnSetDefault(self):
|
||||
acc = self._getSelectedAcc()
|
||||
if not acc:
|
||||
return
|
||||
if acc.isValid():
|
||||
acc.setDefault()
|
||||
for acc in self.accList:
|
||||
self.updateAccount(acc)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
application.main()
|
|
@ -0,0 +1,169 @@
|
|||
# $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 endpoint
|
||||
import application
|
||||
|
||||
class Dialog(tk.Toplevel):
|
||||
"""
|
||||
This implements account settings dialog to manipulate account settings.
|
||||
"""
|
||||
def __init__(self, parent, cfg):
|
||||
tk.Toplevel.__init__(self, parent)
|
||||
self.transient(parent)
|
||||
self.parent = parent
|
||||
self.title('Account settings')
|
||||
|
||||
self.frm = ttk.Frame(self)
|
||||
self.frm.pack(expand='yes', fill='both')
|
||||
|
||||
self.isOk = False
|
||||
self.cfg = cfg
|
||||
|
||||
self.createWidgets()
|
||||
|
||||
def doModal(self):
|
||||
if self.parent:
|
||||
self.parent.wait_window(self)
|
||||
else:
|
||||
self.wait_window(self)
|
||||
return self.isOk
|
||||
|
||||
def createWidgets(self):
|
||||
# The notebook
|
||||
self.frm.rowconfigure(0, weight=1)
|
||||
self.frm.rowconfigure(1, weight=0)
|
||||
self.frm.columnconfigure(0, weight=1)
|
||||
self.frm.columnconfigure(1, weight=1)
|
||||
self.wTab = ttk.Notebook(self.frm)
|
||||
self.wTab.grid(column=0, row=0, columnspan=2, padx=5, pady=5, sticky=tk.N+tk.S+tk.W+tk.E)
|
||||
|
||||
# Main buttons
|
||||
btnOk = ttk.Button(self.frm, text='Ok', command=self.onOk)
|
||||
btnOk.grid(column=0, row=1, sticky=tk.E, padx=20, pady=10)
|
||||
btnCancel = ttk.Button(self.frm, text='Cancel', command=self.onCancel)
|
||||
btnCancel.grid(column=1, row=1, sticky=tk.W, padx=20, pady=10)
|
||||
|
||||
# Tabs
|
||||
self.createBasicTab()
|
||||
|
||||
def createBasicTab(self):
|
||||
# Prepare the variables to set/receive values from GUI
|
||||
self.cfgPriority = tk.IntVar()
|
||||
self.cfgPriority.set( self.cfg.priority )
|
||||
self.cfgAccId = tk.StringVar()
|
||||
self.cfgAccId.set( self.cfg.idUri )
|
||||
self.cfgRegistrar = tk.StringVar()
|
||||
self.cfgRegistrar.set( self.cfg.regConfig.registrarUri )
|
||||
self.cfgRegisterOnAdd = tk.IntVar()
|
||||
self.cfgRegisterOnAdd.set(self.cfg.regConfig.registerOnAdd)
|
||||
self.cfgUsername = tk.StringVar()
|
||||
self.cfgPassword = tk.StringVar()
|
||||
if len(self.cfg.sipConfig.authCreds):
|
||||
self.cfgUsername.set( self.cfg.sipConfig.authCreds[0].username )
|
||||
self.cfgPassword.set( self.cfg.sipConfig.authCreds[0].data )
|
||||
self.cfgProxy = tk.StringVar()
|
||||
if len(self.cfg.sipConfig.proxies):
|
||||
self.cfgProxy.set( self.cfg.sipConfig.proxies[0] )
|
||||
|
||||
# Build the tab page
|
||||
frm = ttk.Frame(self.frm)
|
||||
frm.columnconfigure(0, weight=1)
|
||||
frm.columnconfigure(1, weight=2)
|
||||
row = 0
|
||||
ttk.Label(frm, text='Priority:').grid(row=row, column=0, sticky=tk.E, pady=2)
|
||||
tk.Spinbox(frm, from_=0, to=9, textvariable=self.cfgPriority, width=2).grid(row=row, column=1, sticky=tk.W, padx=6)
|
||||
row += 1
|
||||
ttk.Label(frm, text='ID (URI):').grid(row=row, column=0, sticky=tk.E, pady=2)
|
||||
ttk.Entry(frm, textvariable=self.cfgAccId, width=25).grid(row=row, column=1, sticky=tk.W+tk.E, padx=6)
|
||||
row += 1
|
||||
ttk.Label(frm, text='Registrar URI:').grid(row=row, column=0, sticky=tk.E, pady=2)
|
||||
ttk.Entry(frm, textvariable=self.cfgRegistrar, width=25).grid(row=row, column=1, sticky=tk.W+tk.E, padx=6)
|
||||
row += 1
|
||||
ttk.Checkbutton(frm, text='Register on add', variable=self.cfgRegisterOnAdd).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2)
|
||||
row += 1
|
||||
ttk.Label(frm, text='Optional proxy URI:').grid(row=row, column=0, sticky=tk.E, pady=2)
|
||||
ttk.Entry(frm, textvariable=self.cfgProxy, width=25).grid(row=row, column=1, sticky=tk.W+tk.E, padx=6)
|
||||
row += 1
|
||||
ttk.Label(frm, text='Username:').grid(row=row, column=0, sticky=tk.E, pady=2)
|
||||
ttk.Entry(frm, textvariable=self.cfgUsername, width=12).grid(row=row, column=1, sticky=tk.W, padx=6)
|
||||
row += 1
|
||||
ttk.Label(frm, text='Password:').grid(row=row, column=0, sticky=tk.E, pady=2)
|
||||
ttk.Entry(frm, textvariable=self.cfgPassword, show='*', width=12).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2)
|
||||
|
||||
self.wTab.add(frm, text='Basic Settings')
|
||||
|
||||
|
||||
def onOk(self):
|
||||
# Check basic settings
|
||||
errors = "";
|
||||
if self.cfgAccId.get():
|
||||
if not endpoint.validateSipUri(self.cfgAccId.get()):
|
||||
errors += "Invalid SIP ID URI: '%s'\n" % (self.cfgAccId.get())
|
||||
if self.cfgRegistrar.get():
|
||||
if not endpoint.validateSipUri(self.cfgRegistrar.get()):
|
||||
errors += "Invalid SIP registrar URI: '%s'\n" % (self.cfgRegistrar.get())
|
||||
if self.cfgProxy.get():
|
||||
if not endpoint.validateSipUri(self.cfgProxy.get()):
|
||||
errors += "Invalid SIP proxy URI: '%s'\n" % (self.cfgProxy.get())
|
||||
|
||||
if errors:
|
||||
msgbox.showerror("Error detected:", errors)
|
||||
return
|
||||
|
||||
# Basic settings
|
||||
self.cfg.priority = self.cfgPriority.get()
|
||||
self.cfg.idUri = self.cfgAccId.get()
|
||||
self.cfg.regConfig.registrarUri = self.cfgRegistrar.get()
|
||||
self.cfg.regConfig.registerOnAdd = self.cfgRegisterOnAdd.get()
|
||||
while len(self.cfg.sipConfig.authCreds):
|
||||
self.cfg.sipConfig.authCreds.pop()
|
||||
if self.cfgUsername.get():
|
||||
cred = pj.AuthCredInfo()
|
||||
cred.scheme = "digest"
|
||||
cred.realm = "*"
|
||||
cred.username = self.cfgUsername.get()
|
||||
cred.data = self.cfgPassword.get()
|
||||
self.cfg.sipConfig.authCreds.append(cred)
|
||||
while len(self.cfg.sipConfig.proxies):
|
||||
self.cfg.sipConfig.proxies.pop()
|
||||
if self.cfgProxy.get():
|
||||
self.cfg.sipConfig.proxies.append(self.cfgProxy.get())
|
||||
|
||||
self.isOk = True
|
||||
self.destroy()
|
||||
|
||||
def onCancel(self):
|
||||
self.destroy()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
application.main()
|
|
@ -0,0 +1,396 @@
|
|||
# $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 endpoint
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import traceback
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Unfortunately SWIG classes cannot be serialized directly
|
||||
# hence need to write these config classes
|
||||
#
|
||||
class AccountConfig:
|
||||
"""
|
||||
"Proxy" for the pj.AccountConfig class, serializable
|
||||
"""
|
||||
def __init__(self, acc_cfg = None):
|
||||
if not acc_cfg:
|
||||
acc_cfg = pj.AccountConfig()
|
||||
self.priority = acc_cfg.priority
|
||||
self.idUri = acc_cfg.idUri
|
||||
self.regUri = acc_cfg.regConfig.registrarUri
|
||||
self.registerOnAdd = acc_cfg.regConfig.registerOnAdd
|
||||
self.proxies = [proxy for proxy in acc_cfg.sipConfig.proxies]
|
||||
self.userName = ""
|
||||
self.password = ""
|
||||
if len(acc_cfg.sipConfig.authCreds):
|
||||
self.userName = acc_cfg.sipConfig.authCreds[0].username
|
||||
self.password = acc_cfg.sipConfig.authCreds[0].data
|
||||
|
||||
def getAccConfig(self, acc_cfg = None):
|
||||
"""
|
||||
Convert this class to pj.AccountConfig class
|
||||
"""
|
||||
if not acc_cfg:
|
||||
acc_cfg = pj.AccountConfig()
|
||||
acc_cfg.priority = self.priority
|
||||
acc_cfg.idUri = self.idUri
|
||||
acc_cfg.regConfig.registrarUri = self.regUri
|
||||
acc_cfg.regConfig.registerOnAdd = self.registerOnAdd
|
||||
for proxy in self.proxies:
|
||||
acc_cfg.sipConfig.proxies.append(proxy)
|
||||
if self.userName:
|
||||
cred = pj.AuthCredInfo()
|
||||
cred.scheme = "digest"
|
||||
cred.realm = "*"
|
||||
cred.username = self.userName
|
||||
cred.data = self.password
|
||||
acc_cfg.sipConfig.authCreds.append(cred)
|
||||
return acc_cfg
|
||||
|
||||
class ApplicationConfig:
|
||||
"""
|
||||
Application config is serializable and contains all settings that application need.
|
||||
"""
|
||||
def __init__(self, ep_cfg = None, acc_cfgs = []):
|
||||
if not ep_cfg:
|
||||
ep_cfg = pj.EpConfig()
|
||||
|
||||
self.logFile = ep_cfg.logConfig.filename
|
||||
self.logFlags = ep_cfg.logConfig.fileFlags
|
||||
self.logLevel = ep_cfg.logConfig.consoleLevel
|
||||
|
||||
self.accCfgs = []
|
||||
for acc_cfg in acc_cfgs:
|
||||
self.accCfgs.append( AccountConfig(acc_cfg) )
|
||||
|
||||
def getEpConfig(self, ep_cfg = None):
|
||||
if not ep_cfg:
|
||||
ep_cfg = pj.EpConfig()
|
||||
ep_cfg.logConfig.filename = self.logFile
|
||||
ep_cfg.logConfig.fileFlags = self.logFlags
|
||||
ep_cfg.logConfig.consoleLevel = self.logLevel
|
||||
return ep_cfg
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
|
||||
|
||||
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')
|
||||
|
||||
# Logger
|
||||
self.logger = log.Logger()
|
||||
|
||||
# Global config, fill with default
|
||||
self.epCfg = pj.EpConfig()
|
||||
self.epCfg.uaConfig.threadCnt = 0;
|
||||
self.epCfg.logConfig.writer = self.logger
|
||||
self.epCfg.logConfig.filename = "pygui.log"
|
||||
self.epCfg.logConfig.fileFlags = pj.PJ_O_APPEND
|
||||
self.epCfg.logConfig.level = 5
|
||||
self.epCfg.logConfig.consoleLevel = 5
|
||||
|
||||
# Accounts
|
||||
self.accList = []
|
||||
|
||||
# GUI variables
|
||||
self.showLogWindow = tk.IntVar()
|
||||
self.showLogWindow.set(1)
|
||||
self.quitting = False
|
||||
|
||||
# Construct GUI
|
||||
self._createWidgets()
|
||||
|
||||
# Log window
|
||||
self.logWindow = log.LogWindow(self)
|
||||
self._onMenuShowHideLogWindow()
|
||||
|
||||
def saveConfig(self, filename='pygui.dat'):
|
||||
acc_cfgs = [acc.cfg for acc in self.accList]
|
||||
app_cfg = ApplicationConfig(self.epCfg, acc_cfgs)
|
||||
f = open(filename, 'wb')
|
||||
pickle.dump(app_cfg, f, 0)
|
||||
f.close()
|
||||
|
||||
def start(self, cfg_file='pygui.dat'):
|
||||
# Load config
|
||||
acc_cfgs = []
|
||||
if cfg_file and os.path.exists(cfg_file):
|
||||
f = open(cfg_file, 'rb')
|
||||
app_cfg = pickle.load(f)
|
||||
app_cfg.getEpConfig(self.epCfg)
|
||||
for c in app_cfg.accCfgs:
|
||||
cfg = c.getAccConfig()
|
||||
acc_cfgs.append(cfg)
|
||||
f.close()
|
||||
|
||||
# Instantiate endpoint
|
||||
self.ep = endpoint.Endpoint()
|
||||
self.epCfg.uaConfig.userAgent = "pygui-" + self.ep.libVersion().full;
|
||||
self.ep.startLib(self.epCfg)
|
||||
self.master.title('pjsua2 Demo version ' + self.ep.libVersion().full)
|
||||
|
||||
# Add accounts
|
||||
for cfg in acc_cfgs:
|
||||
self._createAcc(cfg)
|
||||
|
||||
# Start polling
|
||||
self._onTimer()
|
||||
|
||||
def updateAccount(self, acc):
|
||||
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('', 0, iid, open=True, text=text, values=values)
|
||||
self.tv.insert(iid, 0, '', open=True, text='Buddy 1', values=('Online',))
|
||||
self.tv.insert(iid, 1, '', open=True, text='Buddy 2', values=('Online',))
|
||||
|
||||
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 _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)
|
||||
|
||||
# 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 _createContextMenu(self):
|
||||
top = self.winfo_toplevel()
|
||||
self.accMenu = tk.Menu(top, tearoff=False)
|
||||
# Labels, must match with _onAccContextMenu()
|
||||
labels = ['Unregister', 'Reregister', '-', '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)
|
||||
|
||||
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)
|
||||
|
||||
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 _onTvRightClick(self, event):
|
||||
iid = self.tv.identify('item', event.x, event.y)
|
||||
if iid:
|
||||
self.tv.selection_add( (iid,) )
|
||||
acc = self._getSelectedAccount()
|
||||
if acc:
|
||||
self.accMenu.post(event.x_root, event.y_root)
|
||||
else:
|
||||
# A buddy is selected
|
||||
pass
|
||||
|
||||
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.AccountPresenceStatus()
|
||||
ps.isOnline = True
|
||||
acc.setOnlineStatus(ps)
|
||||
elif label=='Invisible':
|
||||
ps = pj.AccountPresenceStatus()
|
||||
ps.isOnline = False
|
||||
acc.setOnlineStatus(ps)
|
||||
elif label=='Away':
|
||||
ps = pj.AccountPresenceStatus()
|
||||
ps.isOnline = True
|
||||
ps.activity = pj.PJRPID_ACTIVITY_AWAY
|
||||
ps.note = "Away"
|
||||
acc.setOnlineStatus(ps)
|
||||
elif label=='Busy':
|
||||
ps = pj.AccountPresenceStatus()
|
||||
ps.isOnline = True
|
||||
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)
|
||||
del acc
|
||||
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.quitting = True
|
||||
self.ep.stopLib()
|
||||
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):
|
||||
msgbox.showinfo(self.master.title(), 'Settings')
|
||||
|
||||
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()
|
||||
app.saveConfig()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,70 @@
|
|||
# $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 application
|
||||
|
||||
|
||||
class Endpoint(pj.Endpoint):
|
||||
"""
|
||||
This is high level Python object inherited from pj.Endpoint
|
||||
"""
|
||||
instance = None
|
||||
def __init__(self):
|
||||
pj.Endpoint.__init__(self)
|
||||
Endpoint.instance = self
|
||||
|
||||
def startLib(self, ep_cfg):
|
||||
# Create lib
|
||||
self.libCreate()
|
||||
|
||||
# Init lib
|
||||
self.libInit(ep_cfg)
|
||||
|
||||
# Add transport
|
||||
tcfg = pj.TransportConfig()
|
||||
tcfg.port = 50060;
|
||||
self.transportCreate(pj.PJSIP_TRANSPORT_UDP, tcfg)
|
||||
|
||||
# Start!
|
||||
self.libStart()
|
||||
|
||||
def stopLib(self):
|
||||
self.libDestroy()
|
||||
|
||||
def validateUri(uri):
|
||||
return Endpoint.instance.utilVerifyUri(uri) == pj.PJ_SUCCESS
|
||||
|
||||
def validateSipUri(uri):
|
||||
return Endpoint.instance.utilVerifySipUri(uri) == pj.PJ_SUCCESS
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
application.main()
|
|
@ -0,0 +1,127 @@
|
|||
# $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 application
|
||||
|
||||
|
||||
class LogWindow(tk.Toplevel):
|
||||
"""
|
||||
Log window
|
||||
"""
|
||||
instance = None
|
||||
def __init__(self, app):
|
||||
tk.Toplevel.__init__(self, name='logwnd', width=640, height=480)
|
||||
LogWindow.instance = self
|
||||
self.app = app
|
||||
self.state('withdrawn')
|
||||
self.title('Log')
|
||||
self._createWidgets()
|
||||
self.protocol("WM_DELETE_WINDOW", self._onHide)
|
||||
|
||||
def addLog(self, entry):
|
||||
"""entry fields:
|
||||
int level;
|
||||
string msg;
|
||||
long threadId;
|
||||
string threadName;
|
||||
"""
|
||||
self.addLog2(entry.level, entry.msg)
|
||||
|
||||
def addLog2(self, level, msg):
|
||||
if level==5:
|
||||
tags = ('trace',)
|
||||
elif level==3:
|
||||
tags = ('info',)
|
||||
elif level==2:
|
||||
tags = ('warning',)
|
||||
elif level<=1:
|
||||
tags = ('error',)
|
||||
else:
|
||||
tags = None
|
||||
self.text.insert(tk.END, msg, tags)
|
||||
self.text.see(tk.END)
|
||||
|
||||
def _createWidgets(self):
|
||||
self.rowconfigure(0, weight=1)
|
||||
self.rowconfigure(1, weight=0)
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.columnconfigure(1, weight=0)
|
||||
|
||||
self.text = tk.Text(self, font=('Courier New', '8'), wrap=tk.NONE, undo=False, padx=4, pady=5)
|
||||
self.text.grid(row=0, column=0, sticky='nswe', padx=5, pady=5)
|
||||
|
||||
scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview)
|
||||
self.text.config(yscrollcommand=scrl.set)
|
||||
scrl.grid(row=0, column=1, sticky='nsw', padx=5, pady=5)
|
||||
|
||||
scrl = ttk.Scrollbar(self, orient=tk.HORIZONTAL, command=self.text.xview)
|
||||
self.text.config(xscrollcommand=scrl.set)
|
||||
scrl.grid(row=1, column=0, sticky='we', padx=5, pady=5)
|
||||
|
||||
self.text.bind("<Key>", self._onKey)
|
||||
|
||||
self.text.tag_configure('normal', font=('Courier New', '8'), foreground='black')
|
||||
self.text.tag_configure('trace', font=('Courier New', '8'), foreground='#777777')
|
||||
self.text.tag_configure('info', font=('Courier New', '8', 'bold'), foreground='black')
|
||||
self.text.tag_configure('warning', font=('Courier New', '8', 'bold'), foreground='cyan')
|
||||
self.text.tag_configure('error', font=('Courier New', '8', 'bold'), foreground='red')
|
||||
|
||||
def _onKey(self, event):
|
||||
# Ignore key event to make text widget read-only
|
||||
return "break"
|
||||
|
||||
def _onHide(self):
|
||||
# Hide when close ('x') button is clicked
|
||||
self.withdraw()
|
||||
self.app.showLogWindow.set(0)
|
||||
|
||||
|
||||
def writeLog2(level, msg):
|
||||
if LogWindow.instance:
|
||||
LogWindow.instance.addLog2(level, msg)
|
||||
|
||||
def writeLog(entry):
|
||||
if LogWindow.instance:
|
||||
LogWindow.instance.addLog(entry)
|
||||
|
||||
class Logger(pj.LogWriter):
|
||||
"""
|
||||
Logger to receive log messages from pjsua2
|
||||
"""
|
||||
def __init__(self):
|
||||
pj.LogWriter.__init__(self)
|
||||
|
||||
def write(self, entry):
|
||||
#print entry.msg,
|
||||
writeLog(entry)
|
||||
|
||||
if __name__ == '__main__':
|
||||
application.main()
|
Loading…
Reference in New Issue