Index: pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/account.py
===================================================================
--- pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/account.py	(revision 4648)
+++ pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/account.py	(revision 4657)
@@ -46,4 +46,5 @@
 		self.cfg =  pj.AccountConfig()
 		self.cfgChanged = False
+		self.buddyList = []
 
 	def statusText(self):
Index: pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/accountsetting.py
===================================================================
--- pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/accountsetting.py	(revision 4648)
+++ pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/accountsetting.py	(revision 4657)
@@ -41,4 +41,5 @@
 		self.transient(parent)
 		self.parent = parent
+		self.geometry("+100+100")
 		self.title('Account settings')
 		
Index: pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/application.py
===================================================================
--- pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/application.py	(revision 4648)
+++ pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/application.py	(revision 4657)
@@ -33,4 +33,5 @@
 import accountsetting
 import account
+import buddy
 import endpoint
 
@@ -63,4 +64,5 @@
 		self.pack(expand='yes', fill='both')
 		self.master.title('pjsua2 Demo')
+		self.master.geometry('500x500+100+100')
 		
 		# Logger
@@ -72,5 +74,5 @@
 		# GUI variables
 		self.showLogWindow = tk.IntVar()
-		self.showLogWindow.set(1)
+		self.showLogWindow.set(0)
 		self.quitting = False 
 		
@@ -100,4 +102,8 @@
 		t.cfg.port = 0
 		self.transportCfgs.append(t)
+		t = SipTransportConfig()
+		t.type = pj.PJSIP_TRANSPORT_TCP
+		t.cfg.port = 0
+		self.transportCfgs.append(t)
 		
 	
@@ -116,5 +122,12 @@
 		node = json.writeNewArray("accounts")
 		for acc in self.accList:
-			node.writeObject(acc.cfg);
+			acc_node = node.writeNewContainer("Account")
+			acc_node.writeObject(acc.cfg);
+
+			# Write buddy configs
+			buddy_node = acc_node.writeNewArray("buddies")
+			for bud in acc.buddyList:
+				buddy_node.writeObject(bud.cfg)
+				
 		json.saveFile(filename)
 	
@@ -141,8 +154,19 @@
 			node = json.readArray("accounts")
 			while node.hasUnread():
-				cfg = pj.AccountConfig()
-				cfg.readObject(node)
-				acc_cfgs.append(cfg)
-		
+				acc_node = node.readContainer("Account")
+				acc_cfg = pj.AccountConfig()
+				acc_cfg.readObject(acc_node)
+		
+				# Load buddy configs
+				buddy_cfgs = []
+				buddy_node = acc_node.readArray("buddies")
+				while buddy_node.hasUnread():
+					buddy_cfg = pj.BuddyConfig()
+					buddy_cfg.readObject(buddy_node)
+					buddy_cfgs.append(buddy_cfg)
+
+				acc_cfgs.append((acc_cfg, buddy_cfgs))
+				
+				
 		# Initialize library
 		self.epCfg.uaConfig.userAgent = "pygui-" + self.ep.libVersion().full;
@@ -155,7 +179,10 @@
 			
 		# Add accounts
-		for cfg in acc_cfgs:
+		for cfg, buddy_cfgs in acc_cfgs:
 			self._createAcc(cfg)
-		
+			acc = self.accList[-1]
+			for buddy_cfg in buddy_cfgs:
+				self._createBuddy(acc, buddy_cfg)
+				
 		# Start library
 		self.ep.libStart()
@@ -173,7 +200,16 @@
 			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',))
+			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):
@@ -186,4 +222,12 @@
 		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()
@@ -222,7 +266,12 @@
 	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', '-', 'Online', 'Invisible', 'Away', 'Busy', '-', 'Settings...', '-', 'Delete...']
+		labels = ['Unregister', 'Reregister', 'Add buddy...', '-',
+			  'Online', 'Invisible', 'Away', 'Busy', '-',
+			  'Settings...', '-',
+			  'Delete...']
 		for label in labels:
 			if label=='-':
@@ -232,4 +281,18 @@
 				self.accMenu.add_command(label=label, command=cmd)
 		
+		# Create Buddy context menu
+		# Labels, must match with _onAccContextMenu()
+		self.buddyMenu = tk.Menu(top, tearoff=False)
+		labels = ['Video call', 'Audio call', 'Send instant message', '-',
+			  '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)
@@ -251,8 +314,29 @@
 		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('item', event.x, event.y)
+		iid = self.tv.identify_row(event.y)
+		#iid = self.tv.identify('item', event.x, event.y)
 		if iid:
-			self.tv.selection_add( (iid,) )
+			self.tv.selection_set( (iid,) )
 			acc = self._getSelectedAccount()
 			if acc:
@@ -260,5 +344,5 @@
 			else:
 				# A buddy is selected
-				pass
+				self.buddyMenu.post(event.x_root, event.y_root)
 	
 	def _onAccContextMenu(self, label):
@@ -272,13 +356,13 @@
 			acc.setRegistration(True)
 		elif label=='Online':
-			ps = pj.AccountPresenceStatus()
+			ps = pj.PresenceStatus()
 			ps.isOnline = True
 			acc.setOnlineStatus(ps)
 		elif label=='Invisible':
-			ps = pj.AccountPresenceStatus()
+			ps = pj.PresenceStatus()
 			ps.isOnline = False
 			acc.setOnlineStatus(ps)
 		elif label=='Away':
-			ps = pj.AccountPresenceStatus()
+			ps = pj.PresenceStatus()
 			ps.isOnline = True
 			ps.activity = pj.PJRPID_ACTIVITY_AWAY
@@ -286,5 +370,5 @@
 			acc.setOnlineStatus(ps)
 		elif label=='Busy':
-			ps = pj.AccountPresenceStatus()
+			ps = pj.PresenceStatus()
 			ps.isOnline = True
 			ps.activity = pj.PJRPID_ACTIVITY_BUSY
@@ -305,7 +389,54 @@
 			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=='Video call':
+			pass
+		elif label=='Audio call':
+			pass
+		elif label=='Send instant message':
+			pass
+		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:
Index: pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/buddy.py
===================================================================
--- pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/buddy.py	(revision 4657)
+++ pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/buddy.py	(revision 4657)
@@ -0,0 +1,152 @@
+# $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 endpoint
+import application
+
+# Buddy class
+class Buddy(pj.Buddy):
+	"""
+	High level Python Buddy object, derived from pjsua2's Buddy object.
+	"""
+	def __init__(self, app):
+		pj.Buddy.__init__(self)
+		self.app = app
+		self.randId = random.randint(1, 9999)
+		self.cfg = None
+		self.account = None
+
+	def statusText(self):
+		bi = self.getInfo()
+		status = ''
+		if bi.subState == pj.PJSIP_EVSUB_STATE_ACTIVE:
+			if bi.presStatus.status == pj.PJSUA_BUDDY_STATUS_ONLINE:
+				status = bi.presStatus.statusText
+				if not status:
+					status = 'Online'
+			elif bi.presStatus.status == pj.PJSUA_BUDDY_STATUS_OFFLINE:
+				status = 'Offline'
+			else:
+				status = 'Unknown'
+		return status
+	
+	def onBuddyState(self):
+		self.app.updateBuddy(self)
+
+class SettingDialog(tk.Toplevel):
+	"""
+	This implements buddy settings dialog to manipulate buddy settings.
+	"""
+	def __init__(self, parent, cfg):
+		tk.Toplevel.__init__(self, parent)
+		self.transient(parent)
+		self.parent = parent
+		self.geometry("+100+100")
+		self.title('Buddy 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.cfgUri = tk.StringVar()
+		self.cfgUri.set( self.cfg.uri )
+		self.cfgSubscribe = tk.IntVar()
+		self.cfgSubscribe.set(self.cfg.subscribe)
+		
+		# 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='URI:').grid(row=row, column=0, sticky=tk.E, pady=2)
+		ttk.Entry(frm, textvariable=self.cfgUri, width=40).grid(row=row, column=1, sticky=tk.W+tk.E, padx=6)
+		row += 1
+		ttk.Checkbutton(frm, text='Subscribe presence', variable=self.cfgSubscribe).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.cfgUri.get():
+			if not endpoint.validateSipUri(self.cfgUri.get()):
+				errors += "Invalid Buddy URI: '%s'\n" % (self.cfgUri.get())
+				
+		if errors:
+			msgbox.showerror("Error detected:", errors)
+			return
+		
+		# Basic settings
+		self.cfg.uri = self.cfgUri.get()
+		self.cfg.subscribe = self.cfgSubscribe.get()
+		
+		self.isOk = True
+		self.destroy()
+		
+	def onCancel(self):
+		self.destroy()
+
+
+if __name__ == '__main__':
+	application.main()
