558 lines
20 KiB
Python
558 lines
20 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
wix: a SAT frontend
|
|
Copyright (C) 2009 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
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 3 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, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
|
|
import wx
|
|
from chat import Chat
|
|
from param import Param
|
|
from form import Form
|
|
from gateways import GatewaysManager
|
|
from profile import Profile
|
|
import gobject
|
|
import os.path
|
|
import pdb
|
|
from tools.jid import JID
|
|
from logging import debug, info, error
|
|
from quick_frontend.quick_chat_list import QuickChatList
|
|
from quick_frontend.quick_contact_list import QuickContactList
|
|
from quick_frontend.quick_app import QuickApp
|
|
from quick_frontend.quick_contact_management import QuickContactManagement
|
|
from cgi import escape
|
|
import sys
|
|
|
|
IMAGE_DIR = sys.path[0]+'/images'
|
|
|
|
msgOFFLINE = "offline"
|
|
msgONLINE = "online"
|
|
idCONNECT,\
|
|
idDISCONNECT,\
|
|
idEXIT,\
|
|
idPARAM,\
|
|
idADD_CONTACT,\
|
|
idREMOVE_CONTACT,\
|
|
idSHOW_PROFILE,\
|
|
idFIND_GATEWAYS = range(8)
|
|
const_DEFAULT_GROUP = "Unclassed"
|
|
const_STATUS = [("", "Online", None),
|
|
("chat", "Free for chat", "green"),
|
|
("away", "AFK", "brown"),
|
|
("dnd", "DND", "red"),
|
|
("xa", "Away", "red")]
|
|
|
|
class ChatList(QuickChatList):
|
|
"""This class manage the list of chat windows"""
|
|
|
|
def __init__(self, host):
|
|
QuickChatList.__init__(self, host)
|
|
|
|
def createChat(self, target):
|
|
return Chat(target, self.host)
|
|
|
|
|
|
class ContactList(wx.SimpleHtmlListBox, QuickContactList):
|
|
"""Customized control to manage contacts."""
|
|
|
|
def __init__(self, parent, CM):
|
|
wx.SimpleHtmlListBox.__init__(self, parent, -1)
|
|
QuickContactList.__init__(self, CM)
|
|
self.host = parent
|
|
self.groups = {} #list contacts in each groups, key = group
|
|
self.Bind(wx.EVT_LISTBOX, self.onSelected)
|
|
self.Bind(wx.EVT_LISTBOX_DCLICK, self.onActivated)
|
|
|
|
def __find_idx(self, entity, reverse=False):
|
|
"""Find indexes of given jid (or groups) in contact list
|
|
@return: list of indexes"""
|
|
result=[]
|
|
for i in range(self.GetCount()):
|
|
if (type(entity) == JID and type(self.GetClientData(i)) == JID and self.GetClientData(i).short == entity.short) or\
|
|
self.GetClientData(i) == entity:
|
|
result.append(i)
|
|
return result
|
|
|
|
def replace(self, jid):
|
|
debug("update %s" % jid)
|
|
if not self.__find_idx(jid):
|
|
self.add(jid)
|
|
else:
|
|
for i in self.__find_idx(jid):
|
|
self.SetString(i, self.__presentItem(jid))
|
|
|
|
def disconnect(self, jid):
|
|
self.remove(jid) #for now, we only show online contacts
|
|
|
|
def __eraseGroup(self, group):
|
|
"""Erase all contacts in group
|
|
@param group: group to erase
|
|
@return: True if something as been erased"""
|
|
erased = False
|
|
indexes = self.__find_idx(group)
|
|
for idx in indexes:
|
|
while idx<self.GetCount()-1 and type(self.GetClientData(idx+1)) == JID:
|
|
erased = True
|
|
self.Delete(idx+1)
|
|
return erased
|
|
|
|
|
|
def __presentGroup(self, group):
|
|
"""Make a nice presentation for the contact groups"""
|
|
html = """-- [%s] --""" % group
|
|
|
|
return html
|
|
|
|
def __presentItem(self, jid):
|
|
"""Make a nice presentation of the contact in the list."""
|
|
name = self.CM.getAttr(jid,'name')
|
|
nick = self.CM.getAttr(jid,'nick')
|
|
show = filter(lambda x:x[0]==self.CM.getAttr(jid,'show'), const_STATUS)[0]
|
|
#show[0]==shortcut
|
|
#show[1]==human readable
|
|
#show[2]==color (or None)
|
|
show_html = "<font color='%s'>[%s]</font>" % (show[2], show[1]) if show[2] else ""
|
|
status = self.CM.getAttr(jid,'status') or ''
|
|
avatar = self.CM.getAttr(jid,'avatar') or IMAGE_DIR+'/empty_avatar.png'
|
|
|
|
#XXX: yes table I know :) but wxHTML* doesn't support CSS
|
|
html = """
|
|
<table border='0'>
|
|
<td>
|
|
<img height='64' width='64' src='%s' />
|
|
</td>
|
|
<td>
|
|
<b>%s</b> %s<br />
|
|
<i>%s</i>
|
|
</td>
|
|
</table>
|
|
""" % (avatar,
|
|
escape(nick or name or jid.node or jid.short),
|
|
show_html,
|
|
escape(status))
|
|
|
|
return html
|
|
|
|
def clear_contacts(self):
|
|
"""Clear all the contact list"""
|
|
self.Clear()
|
|
|
|
def add(self, jid):
|
|
"""add a contact to the list"""
|
|
debug ("adding %s",jid)
|
|
groups = self.CM.getAttr(jid, 'groups')
|
|
if not groups:
|
|
idx = self.Append(self.__presentItem(jid), jid)
|
|
else:
|
|
for group in groups:
|
|
indexes = self.__find_idx(group)
|
|
gp_idx = 0
|
|
if not indexes: #this is a new group, we have to create it
|
|
gp_idx = self.Append(self.__presentGroup(group), group)
|
|
else:
|
|
gp_idx = indexes[0]
|
|
|
|
self.Insert(self.__presentItem(jid), gp_idx+1, jid)
|
|
|
|
|
|
|
|
def remove(self, jid):
|
|
"""remove a contact from the list"""
|
|
debug ("removing %s",jid)
|
|
list_idx = self.__find_idx(jid)
|
|
list_idx.reverse() #we me make some deletions, we have to reverse the order
|
|
for i in list_idx:
|
|
self.Delete(i)
|
|
|
|
def onSelected(self, event):
|
|
"""Called when a contact is selected."""
|
|
data = self.getSelection()
|
|
if type(data) == JID:
|
|
event.Skip()
|
|
else:
|
|
group = self.GetClientData(self.GetSelection())
|
|
erased = self.__eraseGroup(group)
|
|
if not erased: #the group was already erased, we can add again the contacts
|
|
contacts = self.CM.getContFromGroup(group)
|
|
contacts.sort()
|
|
id_insert = self.GetSelection()+1
|
|
for contact in contacts:
|
|
self.Insert(self.__presentItem(contact), id_insert, contact)
|
|
self.SetSelection(wx.NOT_FOUND)
|
|
event.Skip(False)
|
|
|
|
def onActivated(self, event):
|
|
"""Called when a contact is clicked or activated with keyboard."""
|
|
data = self.getSelection()
|
|
self.onActivatedCB(data)
|
|
event.Skip()
|
|
|
|
def getSelection(self):
|
|
"""Return the selected contact, or an empty string if there is not"""
|
|
if self.GetSelection() == wx.NOT_FOUND:
|
|
return None
|
|
data = self.GetClientData(self.GetSelection())
|
|
if type(data) != JID:
|
|
return None
|
|
return data
|
|
|
|
def registerActivatedCB(self, cb):
|
|
"""Register a callback with manage contact activation."""
|
|
self.onActivatedCB=cb
|
|
|
|
class MainWindow(wx.Frame, QuickApp):
|
|
"""main app window"""
|
|
|
|
def __init__(self):
|
|
wx.Frame.__init__(self,None, title="SAT Wix", size=(300,500))
|
|
self.CM = QuickContactManagement() #gof:
|
|
|
|
|
|
#Frame elements
|
|
self.contactList = ContactList(self, self.CM)
|
|
self.contactList.registerActivatedCB(self.onContactActivated)
|
|
self.chat_wins=ChatList(self)
|
|
self.CreateStatusBar()
|
|
self.createMenus()
|
|
|
|
#ToolBar
|
|
self.tools=self.CreateToolBar()
|
|
self.statusBox = wx.ComboBox(self.tools, -1, "Online", choices=[status[1] for status in const_STATUS],
|
|
style=wx.CB_DROPDOWN | wx.CB_READONLY)
|
|
self.tools.AddControl(self.statusBox)
|
|
self.tools.AddSeparator()
|
|
self.statusTxt=wx.TextCtrl(self.tools, -1, style = wx.TE_PROCESS_ENTER)
|
|
self.tools.AddControl(self.statusTxt)
|
|
self.Bind(wx.EVT_COMBOBOX, self.onStatusChange, self.statusBox)
|
|
self.Bind(wx.EVT_TEXT_ENTER, self.onStatusChange, self.statusTxt)
|
|
self.tools.Disable()
|
|
|
|
#tray icon
|
|
ticon = wx.Icon(IMAGE_DIR+'/crystal/tray_icon.xpm', wx.BITMAP_TYPE_XPM)
|
|
self.tray_icon = wx.TaskBarIcon()
|
|
self.tray_icon.SetIcon(ticon, "Wix jabber client")
|
|
wx.EVT_TASKBAR_LEFT_UP(self.tray_icon, self.onTrayClick)
|
|
|
|
|
|
#events
|
|
self.Bind(wx.EVT_CLOSE, self.onClose, self)
|
|
|
|
QuickApp.__init__(self)
|
|
|
|
self.Show()
|
|
|
|
def createMenus(self):
|
|
info("Creating menus")
|
|
connectMenu = wx.Menu()
|
|
connectMenu.Append(idCONNECT, "&Connect CTRL-c"," Connect to the server")
|
|
connectMenu.Append(idDISCONNECT, "&Disconnect CTRL-d"," Disconnect from the server")
|
|
connectMenu.Append(idPARAM,"&Parameters"," Configure the program")
|
|
connectMenu.AppendSeparator()
|
|
connectMenu.Append(idEXIT,"E&xit"," Terminate the program")
|
|
contactMenu = wx.Menu()
|
|
contactMenu.Append(idADD_CONTACT, "&Add contact"," Add a contact to your list")
|
|
contactMenu.Append(idREMOVE_CONTACT, "&Remove contact"," Remove the selected contact from your list")
|
|
contactMenu.AppendSeparator()
|
|
contactMenu.Append(idSHOW_PROFILE, "&Show profile", " Show contact's profile")
|
|
communicationMenu = wx.Menu()
|
|
communicationMenu.Append(idFIND_GATEWAYS, "&Find Gateways"," Find gateways to legacy IM")
|
|
menuBar = wx.MenuBar()
|
|
menuBar.Append(connectMenu,"&General")
|
|
menuBar.Append(contactMenu,"&Contacts")
|
|
menuBar.Append(communicationMenu,"&Communication")
|
|
self.SetMenuBar(menuBar)
|
|
|
|
#events
|
|
wx.EVT_MENU(self, idCONNECT, self.onConnectRequest)
|
|
wx.EVT_MENU(self, idDISCONNECT, self.onDisconnectRequest)
|
|
wx.EVT_MENU(self, idPARAM, self.onParam)
|
|
wx.EVT_MENU(self, idEXIT, self.onExit)
|
|
wx.EVT_MENU(self, idADD_CONTACT, self.onAddContact)
|
|
wx.EVT_MENU(self, idREMOVE_CONTACT, self.onRemoveContact)
|
|
wx.EVT_MENU(self, idSHOW_PROFILE, self.onShowProfile)
|
|
wx.EVT_MENU(self, idFIND_GATEWAYS, self.onFindGateways)
|
|
|
|
|
|
def newMessage(self, from_jid, msg, type, to_jid):
|
|
QuickApp.newMessage(self, from_jid, msg, type, to_jid)
|
|
|
|
def showAlert(self, message):
|
|
# TODO: place this in a separate class
|
|
popup=wx.PopupWindow(self)
|
|
### following code come from wxpython demo
|
|
popup.SetBackgroundColour("CADET BLUE")
|
|
st = wx.StaticText(popup, -1, message, pos=(10,10))
|
|
sz = st.GetBestSize()
|
|
popup.SetSize( (sz.width+20, sz.height+20) )
|
|
x=(wx.DisplaySize()[0]-popup.GetSize()[0])/2
|
|
popup.SetPosition((x,0))
|
|
popup.Show()
|
|
wx.CallLater(5000,popup.Destroy)
|
|
|
|
def showDialog(self, message, title="", type="info"):
|
|
if type == 'info':
|
|
flags = wx.OK | wx.ICON_INFORMATION
|
|
elif type == 'error':
|
|
flags = wx.OK | wx.ICON_ERROR
|
|
elif type == 'yes/no':
|
|
flags = wx.YES_NO | wx.ICON_QUESTION
|
|
else:
|
|
flags = wx.OK | wx.ICON_INFORMATION
|
|
error('unmanaged dialog type: %s', type)
|
|
dlg = wx.MessageDialog(self, message, title, flags)
|
|
answer = dlg.ShowModal()
|
|
dlg.Destroy()
|
|
return True if (answer == wx.ID_YES or answer == wx.ID_OK) else False
|
|
|
|
def setStatusOnline(self, online=True):
|
|
"""enable/disable controls, must be called when local user online status change"""
|
|
if online:
|
|
self.SetStatusText(msgONLINE)
|
|
self.tools.Enable()
|
|
else:
|
|
self.SetStatusText(msgOFFLINE)
|
|
self.tools.Disable()
|
|
return
|
|
|
|
|
|
def presenceUpdate(self, jabber_id, show, priority, statuses):
|
|
QuickApp.presenceUpdate(self, jabber_id, show, priority, statuses)
|
|
|
|
def askConfirmation(self, type, id, data):
|
|
#TODO: refactor this in QuickApp
|
|
debug ("Confirmation asked")
|
|
answer_data={}
|
|
if type == "FILE_TRANSFERT":
|
|
debug ("File transfert confirmation asked")
|
|
dlg = wx.MessageDialog(self, "The contact %s wants to send you the file %s\nDo you accept ?" % (data["from"], data["filename"]),
|
|
'File Request',
|
|
wx.YES_NO | wx.ICON_QUESTION
|
|
)
|
|
answer=dlg.ShowModal()
|
|
if answer==wx.ID_YES:
|
|
filename = wx.FileSelector("Where do you want to save the file ?", flags = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
|
|
if filename:
|
|
answer_data["dest_path"] = filename
|
|
self.bridge.confirmationAnswer(id, True, answer_data)
|
|
self.waitProgress(id, "File Transfer", "Copying %s" % os.path.basename(filename))
|
|
else:
|
|
answer = wx.ID_NO
|
|
if answer==wx.ID_NO:
|
|
self.bridge.confirmationAnswer(id, False, answer_data)
|
|
|
|
dlg.Destroy()
|
|
|
|
elif type == "YES/NO":
|
|
debug ("Yes/No confirmation asked")
|
|
dlg = wx.MessageDialog(self, data["message"],
|
|
'Confirmation',
|
|
wx.YES_NO | wx.ICON_QUESTION
|
|
)
|
|
answer=dlg.ShowModal()
|
|
if answer==wx.ID_YES:
|
|
self.bridge.confirmationAnswer(id, True, {})
|
|
if answer==wx.ID_NO:
|
|
self.bridge.confirmationAnswer(id, False, {})
|
|
|
|
dlg.Destroy()
|
|
|
|
def actionResult(self, type, id, data):
|
|
debug ("actionResult: type = [%s] id = [%s] data = [%s]" % (type, id, data))
|
|
if not id in self.current_action_ids:
|
|
debug ('unknown id, ignoring')
|
|
return
|
|
if type == "SUPPRESS":
|
|
self.current_action_ids.remove(id)
|
|
elif type == "SUCCESS":
|
|
self.current_action_ids.remove(id)
|
|
dlg = wx.MessageDialog(self, data["message"],
|
|
'Success',
|
|
wx.OK | wx.ICON_INFORMATION
|
|
)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
elif type == "ERROR":
|
|
self.current_action_ids.remove(id)
|
|
dlg = wx.MessageDialog(self, data["message"],
|
|
'Error',
|
|
wx.OK | wx.ICON_ERROR
|
|
)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
elif type == "FORM":
|
|
self.current_action_ids.remove(id)
|
|
debug ("Form received")
|
|
form=Form(self, title='Registration', target = data['target'], type = data['type'], xml_data = data['xml'])
|
|
elif type == "RESULT":
|
|
self.current_action_ids.remove(id)
|
|
if self.current_action_ids_cb.has_key(id):
|
|
callback = self.current_action_ids_cb[id]
|
|
del self.current_action_ids_cb[id]
|
|
callback(data)
|
|
elif type == "DICT_DICT":
|
|
self.current_action_ids.remove(id)
|
|
if self.current_action_ids_cb.has_key(id):
|
|
callback = self.current_action_ids_cb[id]
|
|
del self.current_action_ids_cb[id]
|
|
callback(data)
|
|
else:
|
|
error ("FIXME FIXME FIXME: type [%s] not implemented" % type)
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def progressCB(self, id, title, message):
|
|
data = self.bridge.getProgress(id)
|
|
if data:
|
|
if not data['position']:
|
|
data['position'] = '0'
|
|
if not self.pbar:
|
|
#first answer, we must construct the bar
|
|
self.pbar = wx.ProgressDialog(title, message, int(data['size']), None,
|
|
wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_ESTIMATED_TIME | wx.PD_REMAINING_TIME)
|
|
self.pbar.finish_value = int(data['size'])
|
|
|
|
self.pbar.Update(int(data['position']))
|
|
elif self.pbar:
|
|
self.pbar.Update(self.pbar.finish_value)
|
|
return
|
|
|
|
wx.CallLater(10, self.progressCB, id, title, message)
|
|
|
|
def waitProgress (self, id, title, message):
|
|
self.pbar = None
|
|
wx.CallLater(10, self.progressCB, id, title, message)
|
|
|
|
|
|
|
|
### events ###
|
|
|
|
def onContactActivated(self, jid):
|
|
debug ("onContactActivated: %s", jid)
|
|
if self.chat_wins[jid.short].IsShown():
|
|
self.chat_wins[jid.short].Hide()
|
|
else:
|
|
self.chat_wins[jid.short].Show()
|
|
|
|
def onConnectRequest(self, e):
|
|
self.bridge.connect()
|
|
|
|
def onDisconnectRequest(self, e):
|
|
self.bridge.disconnect()
|
|
|
|
def __updateStatus(self):
|
|
show = filter(lambda x:x[1] == self.statusBox.GetValue(), const_STATUS)[0][0]
|
|
status = self.statusTxt.GetValue()
|
|
self.bridge.setPresence(show=show, statuses={'default':status}) #FIXME: manage multilingual statuses
|
|
|
|
def onStatusChange(self, e):
|
|
debug("Status change request")
|
|
self.__updateStatus()
|
|
|
|
def onParam(self, e):
|
|
debug("Param request")
|
|
param=Param(self)
|
|
|
|
def onExit(self, e):
|
|
self.Close()
|
|
|
|
def onAddContact(self, e):
|
|
debug("Add contact request")
|
|
dlg = wx.TextEntryDialog(
|
|
self, 'Please enter new contact JID',
|
|
'Adding a contact', 'name@server.tld')
|
|
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
jid=JID(dlg.GetValue())
|
|
if jid.is_valid():
|
|
self.bridge.addContact(jid.short)
|
|
else:
|
|
error ("'%s' is an invalid JID !", jid)
|
|
#TODO: notice the user
|
|
|
|
dlg.Destroy()
|
|
|
|
def onRemoveContact(self, e):
|
|
debug("Remove contact request")
|
|
target = self.contactList.getSelection()
|
|
if not target:
|
|
dlg = wx.MessageDialog(self, "You haven't selected any contact !",
|
|
'Error',
|
|
wx.OK | wx.ICON_ERROR
|
|
)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
return
|
|
|
|
dlg = wx.MessageDialog(self, "Are you sure you want to delete %s from your roster list ?" % target.short,
|
|
'Contact suppression',
|
|
wx.YES_NO | wx.ICON_QUESTION
|
|
)
|
|
|
|
if dlg.ShowModal() == wx.ID_YES:
|
|
info("Unsubscribing %s presence", target.short)
|
|
self.bridge.delContact(target.short)
|
|
|
|
dlg.Destroy()
|
|
|
|
def onShowProfile(self, e):
|
|
debug("Show contact's profile request")
|
|
target = self.contactList.getSelection()
|
|
if not target:
|
|
dlg = wx.MessageDialog(self, "You haven't selected any contact !",
|
|
'Error',
|
|
wx.OK | wx.ICON_ERROR
|
|
)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
return
|
|
id = self.bridge.getProfile(target.short)
|
|
self.current_action_ids.add(id)
|
|
self.current_action_ids_cb[id] = self.onProfileReceived
|
|
|
|
def onProfileReceived(self, data):
|
|
"""Called when a profile is received"""
|
|
debug ('Profile received: [%s]' % data)
|
|
profile=Profile(self, data)
|
|
|
|
|
|
def onFindGateways(self, e):
|
|
debug("Find Gateways request")
|
|
id = self.bridge.findGateways(self.whoami.domain)
|
|
self.current_action_ids.add(id)
|
|
self.current_action_ids_cb[id] = self.onGatewaysFound
|
|
|
|
def onGatewaysFound(self, data):
|
|
"""Called when SàT has found the server gateways"""
|
|
target = data['__private__']['target']
|
|
del data['__private__']
|
|
gatewayManager = GatewaysManager(self, data, server=target)
|
|
|
|
def onClose(self, e):
|
|
info("Exiting...")
|
|
e.Skip()
|
|
|
|
def onTrayClick(self, e):
|
|
debug("Tray Click")
|
|
if self.IsShown():
|
|
self.Hide()
|
|
else:
|
|
self.Show()
|
|
self.Raise()
|
|
e.Skip()
|
|
|