484 lines
18 KiB
Python
484 lines
18 KiB
Python
## plugins/whiteboard/plugin.py
|
|
##
|
|
## Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com>
|
|
## Copyright (C) 2010 Yann Leboulanger <asterix AT lagaule.org>
|
|
##
|
|
## This file is part of Gajim.
|
|
##
|
|
## Gajim 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; version 3 only.
|
|
##
|
|
## Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
##
|
|
|
|
'''
|
|
Whiteboard plugin.
|
|
|
|
:author: Yann Leboulanger <asterix@lagaule.org>
|
|
:since: 1st November 2010
|
|
:copyright: Copyright (2010) Yann Leboulanger <asterix@lagaule.org>
|
|
:license: GPL
|
|
'''
|
|
|
|
|
|
from common import helpers
|
|
from common import gajim
|
|
from plugins import GajimPlugin
|
|
from plugins.plugin import GajimPluginException
|
|
from plugins.helpers import log_calls, log
|
|
import common.xmpp
|
|
import gtk
|
|
import chat_control
|
|
from common import ged
|
|
from common.jingle_session import JingleSession
|
|
from common.jingle_content import JingleContent
|
|
from common.jingle_transport import JingleTransport, TransportType
|
|
import dialogs
|
|
from whiteboard_widget import Whiteboard, HAS_GOOCANVAS
|
|
from common import xmpp
|
|
from common import caps_cache
|
|
|
|
NS_JINGLE_XHTML = 'urn:xmpp:tmp:jingle:apps:xhtml'
|
|
NS_JINGLE_SXE = 'urn:xmpp:tmp:jingle:transports:sxe'
|
|
NS_SXE = 'urn:xmpp:sxe:0'
|
|
|
|
class WhiteboardPlugin(GajimPlugin):
|
|
@log_calls('WhiteboardPlugin')
|
|
def init(self):
|
|
self.description = _('Shows a whiteboard in chat.'
|
|
' python-pygoocanvas is required.')
|
|
self.config_dialog = None
|
|
self.events_handlers = {
|
|
'jingle-request-received': (ged.GUI1, self._nec_jingle_received),
|
|
'jingle-connected-received': (ged.GUI1, self._nec_jingle_connected),
|
|
'jingle-disconnected-received': (ged.GUI1,
|
|
self._nec_jingle_disconnected),
|
|
'raw-message-received': (ged.GUI1, self._nec_raw_message),
|
|
}
|
|
self.gui_extension_points = {
|
|
'chat_control_base' : (self.connect_with_chat_control,
|
|
self.disconnect_from_chat_control),
|
|
'chat_control_base_update_toolbar': (self.update_button_state,
|
|
None),
|
|
}
|
|
self.controls = []
|
|
self.sid = None
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def _compute_caps_hash(self):
|
|
for a in gajim.connections:
|
|
gajim.caps_hash[a] = caps_cache.compute_caps_hash([
|
|
gajim.gajim_identity], gajim.gajim_common_features + \
|
|
gajim.gajim_optional_features[a])
|
|
# re-send presence with new hash
|
|
connected = gajim.connections[a].connected
|
|
if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible':
|
|
gajim.connections[a].change_status(gajim.SHOW_LIST[connected],
|
|
gajim.connections[a].status)
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def activate(self):
|
|
if not HAS_GOOCANVAS:
|
|
raise GajimPluginException('python-pygoocanvas is missing!')
|
|
if NS_JINGLE_SXE not in gajim.gajim_common_features:
|
|
gajim.gajim_common_features.append(NS_JINGLE_SXE)
|
|
if NS_SXE not in gajim.gajim_common_features:
|
|
gajim.gajim_common_features.append(NS_SXE)
|
|
self._compute_caps_hash()
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def deactivate(self):
|
|
if NS_JINGLE_SXE in gajim.gajim_common_features:
|
|
gajim.gajim_common_features.remove(NS_JINGLE_SXE)
|
|
if NS_SXE in gajim.gajim_common_features:
|
|
gajim.gajim_common_features.remove(NS_SXE)
|
|
self._compute_caps_hash()
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def connect_with_chat_control(self, control):
|
|
if isinstance(control, chat_control.ChatControl):
|
|
base = Base(self, control)
|
|
self.controls.append(base)
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def disconnect_from_chat_control(self, chat_control):
|
|
for base in self.controls:
|
|
base.disconnect_from_chat_control()
|
|
self.controls = []
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def update_button_state(self, control):
|
|
for base in self.controls:
|
|
if base.chat_control == control:
|
|
if control.contact.supports(NS_JINGLE_SXE) and \
|
|
control.contact.supports(NS_SXE):
|
|
base.button.set_sensitive(True)
|
|
tooltip_text = _('Show whiteboard')
|
|
else:
|
|
base.button.set_sensitive(False)
|
|
tooltip_text = _('Client on the other side '
|
|
'does not support the whiteboard')
|
|
base.button.set_tooltip_text(tooltip_text)
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def show_request_dialog(self, account, fjid, jid, sid, content_types):
|
|
def on_ok():
|
|
session = gajim.connections[account].get_jingle_session(fjid, sid)
|
|
self.sid = session.sid
|
|
if not session.accepted:
|
|
session.approve_session()
|
|
for content in content_types:
|
|
session.approve_content(content)
|
|
for _jid in (fjid, jid):
|
|
ctrl = gajim.interface.msg_win_mgr.get_control(_jid, account)
|
|
if ctrl:
|
|
break
|
|
if not ctrl:
|
|
# create it
|
|
gajim.interface.new_chat_from_jid(account, jid)
|
|
ctrl = gajim.interface.msg_win_mgr.get_control(jid, account)
|
|
session = session.contents[('initiator', 'xhtml')]
|
|
ctrl.draw_whiteboard(session)
|
|
|
|
def on_cancel():
|
|
session = gajim.connections[account].get_jingle_session(fjid, sid)
|
|
session.decline_session()
|
|
|
|
contact = gajim.contacts.get_first_contact_from_jid(account, jid)
|
|
if contact:
|
|
name = contact.get_shown_name()
|
|
else:
|
|
name = jid
|
|
pritext = _('Incoming Whiteboard')
|
|
sectext = _('%(name)s (%(jid)s) wants to start a whiteboard with '
|
|
'you. Do you want to accept?') % {'name': name, 'jid': jid}
|
|
dialog = dialogs.NonModalConfirmationDialog(pritext, sectext=sectext,
|
|
on_response_ok=on_ok, on_response_cancel=on_cancel)
|
|
dialog.popup()
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def _nec_jingle_received(self, obj):
|
|
if not HAS_GOOCANVAS:
|
|
return
|
|
content_types = set(c[0] for c in obj.contents)
|
|
if 'xhtml' not in content_types:
|
|
return
|
|
self.show_request_dialog(obj.conn.name, obj.fjid, obj.jid, obj.sid,
|
|
content_types)
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def _nec_jingle_connected(self, obj):
|
|
if not HAS_GOOCANVAS:
|
|
return
|
|
account = obj.conn.name
|
|
ctrl = (gajim.interface.msg_win_mgr.get_control(obj.fjid, account)
|
|
or gajim.interface.msg_win_mgr.get_control(obj.jid, account))
|
|
if not ctrl:
|
|
return
|
|
session = gajim.connections[obj.conn.name].get_jingle_session(obj.fjid,
|
|
obj.sid)
|
|
|
|
if ('initiator', 'xhtml') not in session.contents:
|
|
return
|
|
|
|
session = session.contents[('initiator', 'xhtml')]
|
|
ctrl.draw_whiteboard(session)
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def _nec_jingle_disconnected(self, obj):
|
|
for base in self.controls:
|
|
if base.sid == obj.sid:
|
|
base.stop_whiteboard(reason = obj.reason)
|
|
|
|
@log_calls('WhiteboardPlugin')
|
|
def _nec_raw_message(self, obj):
|
|
if not HAS_GOOCANVAS:
|
|
return
|
|
if obj.stanza.getTag('sxe', namespace=NS_SXE):
|
|
account = obj.conn.name
|
|
|
|
try:
|
|
fjid = helpers.get_full_jid_from_iq(obj.stanza)
|
|
except helpers.InvalidFormat:
|
|
obj.conn.dispatch('ERROR', (_('Invalid Jabber ID'),
|
|
_('A message from a non-valid JID arrived, it has been '
|
|
'ignored.')))
|
|
|
|
jid = gajim.get_jid_without_resource(fjid)
|
|
ctrl = (gajim.interface.msg_win_mgr.get_control(fjid, account)
|
|
or gajim.interface.msg_win_mgr.get_control(jid, account))
|
|
if not ctrl:
|
|
return
|
|
sxe = obj.stanza.getTag('sxe')
|
|
if not sxe:
|
|
return
|
|
sid = sxe.getAttr('session')
|
|
if (jid, sid) not in obj.conn._sessions:
|
|
pass
|
|
# newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid)
|
|
# self.addJingle(newjingle)
|
|
|
|
# we already have such session in dispatcher...
|
|
session = obj.conn.get_jingle_session(fjid, sid)
|
|
cn = session.contents[('initiator', 'xhtml')]
|
|
error = obj.stanza.getTag('error')
|
|
if error:
|
|
action = 'iq-error'
|
|
else:
|
|
action = 'edit'
|
|
|
|
cn.on_stanza(obj.stanza, sxe, error, action)
|
|
# def __editCB(self, stanza, content, error, action):
|
|
#new_tags = sxe.getTags('new')
|
|
#remove_tags = sxe.getTags('remove')
|
|
|
|
#if new_tags is not None:
|
|
## Process new elements
|
|
#for tag in new_tags:
|
|
#if tag.getAttr('type') == 'element':
|
|
#ctrl.whiteboard.recieve_element(tag)
|
|
#elif tag.getAttr('type') == 'attr':
|
|
#ctrl.whiteboard.recieve_attr(tag)
|
|
#ctrl.whiteboard.apply_new()
|
|
|
|
#if remove_tags is not None:
|
|
## Delete rids
|
|
#for tag in remove_tags:
|
|
#target = tag.getAttr('target')
|
|
#ctrl.whiteboard.image.del_rid(target)
|
|
|
|
# Stop propagating this event, it's handled
|
|
return True
|
|
|
|
|
|
class Base(object):
|
|
def __init__(self, plugin, chat_control):
|
|
self.plugin = plugin
|
|
self.chat_control = chat_control
|
|
self.chat_control.draw_whiteboard = self.draw_whiteboard
|
|
self.contact = self.chat_control.contact
|
|
self.account = self.chat_control.account
|
|
self.jid = self.contact.get_full_jid()
|
|
self.create_buttons()
|
|
self.whiteboard = None
|
|
self.sid = None
|
|
|
|
def create_buttons(self):
|
|
# create whiteboard button
|
|
actions_hbox = self.chat_control.xml.get_object('actions_hbox')
|
|
self.button = gtk.ToggleButton(label=None, use_underline=True)
|
|
self.button.set_property('relief', gtk.RELIEF_NONE)
|
|
self.button.set_property('can-focus', False)
|
|
img = gtk.Image()
|
|
img_path = self.plugin.local_file_path('whiteboard.png')
|
|
pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
|
|
iconset = gtk.IconSet(pixbuf=pixbuf)
|
|
factory = gtk.IconFactory()
|
|
factory.add('whiteboard', iconset)
|
|
img_path = self.plugin.local_file_path('brush_tool.png')
|
|
pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
|
|
iconset = gtk.IconSet(pixbuf=pixbuf)
|
|
factory.add('brush_tool', iconset)
|
|
img_path = self.plugin.local_file_path('line_tool.png')
|
|
pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
|
|
iconset = gtk.IconSet(pixbuf=pixbuf)
|
|
factory.add('line_tool', iconset)
|
|
img_path = self.plugin.local_file_path('oval_tool.png')
|
|
pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
|
|
iconset = gtk.IconSet(pixbuf=pixbuf)
|
|
factory.add('oval_tool', iconset)
|
|
factory.add_default()
|
|
img.set_from_stock('whiteboard', gtk.ICON_SIZE_BUTTON)
|
|
self.button.set_image(img)
|
|
send_button = self.chat_control.xml.get_object('send_button')
|
|
send_button_pos = actions_hbox.child_get_property(send_button,
|
|
'position')
|
|
actions_hbox.add_with_properties(self.button, 'position',
|
|
send_button_pos - 1, 'expand', False)
|
|
id_ = self.button.connect('toggled', self.on_whiteboard_button_toggled)
|
|
self.chat_control.handlers[id_] = self.button
|
|
self.button.show()
|
|
|
|
def draw_whiteboard(self, content):
|
|
hbox = self.chat_control.xml.get_object('chat_control_hbox')
|
|
if len(hbox.get_children()) == 1:
|
|
self.whiteboard = Whiteboard(self.account, self.contact, content,
|
|
self.plugin)
|
|
# set minimum size
|
|
self.whiteboard.hbox.set_size_request(300, 0)
|
|
hbox.pack_start(self.whiteboard.hbox, expand=False, fill=False)
|
|
self.whiteboard.hbox.show_all()
|
|
self.button.set_active(True)
|
|
content.control = self
|
|
self.sid = content.session.sid
|
|
|
|
def on_whiteboard_button_toggled(self, widget):
|
|
"""
|
|
Popup whiteboard
|
|
"""
|
|
if widget.get_active():
|
|
if not self.whiteboard:
|
|
self.start_whiteboard()
|
|
else:
|
|
self.stop_whiteboard()
|
|
|
|
def start_whiteboard(self):
|
|
conn = gajim.connections[self.chat_control.account]
|
|
jingle = JingleSession(conn, weinitiate=True, jid=self.jid)
|
|
self.sid = jingle.sid
|
|
conn._sessions[jingle.sid] = jingle
|
|
content = JingleWhiteboard(jingle)
|
|
content.control = self
|
|
jingle.add_content('xhtml', content)
|
|
jingle.start_session()
|
|
|
|
def stop_whiteboard(self, reason=None):
|
|
conn = gajim.connections[self.chat_control.account]
|
|
self.sid = None
|
|
session = conn.get_jingle_session(self.jid, media='xhtml')
|
|
if session:
|
|
session.end_session()
|
|
self.button.set_active(False)
|
|
if reason:
|
|
txt = _('Whiteboard stopped: %(reason)s') % {'reason': reason}
|
|
self.chat_control.print_conversation(txt, 'info')
|
|
if not self.whiteboard:
|
|
return
|
|
hbox = self.chat_control.xml.get_object('chat_control_hbox')
|
|
if self.whiteboard.hbox in hbox.get_children():
|
|
if hasattr(self.whiteboard, 'hbox'):
|
|
hbox.remove(self.whiteboard.hbox)
|
|
self.whiteboard = None
|
|
|
|
def disconnect_from_chat_control(self):
|
|
actions_hbox = self.chat_control.xml.get_object('actions_hbox')
|
|
actions_hbox.remove(self.button)
|
|
|
|
class JingleWhiteboard(JingleContent):
|
|
''' Jingle Whiteboard sessions consist of xhtml content'''
|
|
def __init__(self, session, transport=None):
|
|
if not transport:
|
|
transport = JingleTransportSXE()
|
|
JingleContent.__init__(self, session, transport)
|
|
self.media = 'xhtml'
|
|
self.negotiated = True # there is nothing to negotiate
|
|
self.last_rid = 0
|
|
self.callbacks['session-accept'] += [self._sessionAcceptCB]
|
|
self.callbacks['session-terminate'] += [self._stop]
|
|
self.callbacks['session-terminate-sent'] += [self._stop]
|
|
self.callbacks['edit'] = [self._EditCB]
|
|
|
|
def _EditCB(self, stanza, content, error, action):
|
|
new_tags = content.getTags('new')
|
|
remove_tags = content.getTags('remove')
|
|
|
|
if new_tags is not None:
|
|
# Process new elements
|
|
for tag in new_tags:
|
|
if tag.getAttr('type') == 'element':
|
|
self.control.whiteboard.recieve_element(tag)
|
|
elif tag.getAttr('type') == 'attr':
|
|
self.control.whiteboard.recieve_attr(tag)
|
|
self.control.whiteboard.apply_new()
|
|
|
|
if remove_tags is not None:
|
|
# Delete rids
|
|
for tag in remove_tags:
|
|
target = tag.getAttr('target')
|
|
self.control.whiteboard.image.del_rid(target)
|
|
|
|
def _sessionAcceptCB(self, stanza, content, error, action):
|
|
log.debug('session accepted')
|
|
self.session.connection.dispatch('WHITEBOARD_ACCEPTED',
|
|
(self.session.peerjid, self.session.sid))
|
|
|
|
def generate_rids(self, x):
|
|
# generates x number of rids and returns in list
|
|
rids = []
|
|
for x in range(x):
|
|
rids.append(str(self.last_rid))
|
|
self.last_rid += 1
|
|
return rids
|
|
|
|
def send_whiteboard_node(self, items, rids):
|
|
# takes int rid and dict items and sends it as a node
|
|
# sends new item
|
|
jid = self.session.peerjid
|
|
sid = self.session.sid
|
|
message = xmpp.Message(to=jid)
|
|
sxe = message.addChild(name='sxe', attrs={'session': sid},
|
|
namespace=NS_SXE)
|
|
|
|
for x in rids:
|
|
if items[x]['type'] == 'element':
|
|
parent = x
|
|
attrs = {'rid': x,
|
|
'name': items[x]['data'][0].getName(),
|
|
'type': items[x]['type']}
|
|
sxe.addChild(name='new', attrs=attrs)
|
|
if items[x]['type'] == 'attr':
|
|
attr_name = items[x]['data']
|
|
chdata = items[parent]['data'][0].getAttr(attr_name)
|
|
attrs = {'rid': x,
|
|
'name': attr_name,
|
|
'type': items[x]['type'],
|
|
'chdata': chdata,
|
|
'parent': parent}
|
|
sxe.addChild(name='new', attrs=attrs)
|
|
self.session.connection.connection.send(message)
|
|
|
|
def delete_whiteboard_node(self, rids):
|
|
message = xmpp.Message(to=self.session.peerjid)
|
|
sxe = message.addChild(name='sxe', attrs={'session': self.session.sid},
|
|
namespace=NS_SXE)
|
|
|
|
for x in rids:
|
|
sxe.addChild(name='remove', attrs = {'target': x})
|
|
self.session.connection.connection.send(message)
|
|
|
|
def send_items(self, items, rids):
|
|
# recieves dict items and a list of rids of items to send
|
|
# TODO: is there a less clumsy way that doesn't involve passing
|
|
# whole list
|
|
self.send_whiteboard_node(items, rids)
|
|
|
|
def del_item(self, rids):
|
|
self.delete_whiteboard_node(rids)
|
|
|
|
def encode(self, xml):
|
|
# encodes it sendable string
|
|
return 'data:text/xml,' + urllib.quote(xml)
|
|
|
|
def _fill_content(self, content):
|
|
content.addChild(NS_JINGLE_XHTML + ' description')
|
|
|
|
def _stop(self, *things):
|
|
pass
|
|
|
|
def __del__(self):
|
|
pass
|
|
|
|
def get_content(desc):
|
|
return JingleWhiteboard
|
|
|
|
common.jingle_content.contents[NS_JINGLE_XHTML] = get_content
|
|
|
|
class JingleTransportSXE(JingleTransport):
|
|
def __init__(self):
|
|
JingleTransport.__init__(self, TransportType.streaming)
|
|
|
|
def make_transport(self, candidates=None):
|
|
transport = JingleTransport.make_transport(self, candidates)
|
|
transport.setNamespace(NS_JINGLE_SXE)
|
|
transport.setTagData('host', 'TODO')
|
|
return transport
|
|
|
|
common.jingle_transport.transports[NS_JINGLE_SXE] = JingleTransportSXE
|