Index: /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/account.py
===================================================================
--- /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/account.py	(revision 4640)
+++ /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/account.py	(revision 4640)
@@ -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()
Index: /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/accountsetting.py
===================================================================
--- /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/accountsetting.py	(revision 4640)
+++ /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/accountsetting.py	(revision 4640)
@@ -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()
Index: /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/application.py
===================================================================
--- /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/application.py	(revision 4640)
+++ /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/application.py	(revision 4640)
@@ -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()
Index: /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/endpoint.py
===================================================================
--- /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/endpoint.py	(revision 4640)
+++ /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/endpoint.py	(revision 4640)
@@ -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()
Index: /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/log.py
===================================================================
--- /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/log.py	(revision 4640)
+++ /pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/log.py	(revision 4640)
@@ -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()
