[milouse] Fix identation error
[gajim.git] / plugins / whiteboard / plugin.py
blob2716c203ebbcb0260c937b390f19fbb28d0d2516
1 ## plugins/whiteboard/plugin.py
2 ##
3 ## Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com>
4 ## Copyright (C) 2010 Yann Leboulanger <asterix AT lagaule.org>
5 ##
6 ## This file is part of Gajim.
7 ##
8 ## Gajim is free software; you can redistribute it and/or modify
9 ## it under the terms of the GNU General Public License as published
10 ## by the Free Software Foundation; version 3 only.
12 ## Gajim is distributed in the hope that it will be useful,
13 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ## GNU General Public License for more details.
17 ## You should have received a copy of the GNU General Public License
18 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
21 '''
22 Whiteboard plugin.
24 :author: Yann Leboulanger <asterix@lagaule.org>
25 :since: 1st November 2010
26 :copyright: Copyright (2010) Yann Leboulanger <asterix@lagaule.org>
27 :license: GPL
28 '''
31 from common import helpers
32 from common import gajim
33 from plugins import GajimPlugin
34 from plugins.plugin import GajimPluginException
35 from plugins.helpers import log_calls, log
36 import common.xmpp
37 import gtk
38 import chat_control
39 from common import ged
40 from common.jingle_session import JingleSession
41 from common.jingle_content import JingleContent
42 from common.jingle_transport import JingleTransport, TransportType
43 import dialogs
44 from whiteboard_widget import Whiteboard, HAS_GOOCANVAS
45 from common import xmpp
46 from common import caps_cache
48 NS_JINGLE_XHTML = 'urn:xmpp:tmp:jingle:apps:xhtml'
49 NS_JINGLE_SXE = 'urn:xmpp:tmp:jingle:transports:sxe'
50 NS_SXE = 'urn:xmpp:sxe:0'
52 class WhiteboardPlugin(GajimPlugin):
53 @log_calls('WhiteboardPlugin')
54 def init(self):
55 self.config_dialog = None
56 self.events_handlers = {
57 'jingle-request-received': (ged.GUI1, self._nec_jingle_received),
58 'jingle-connected-received': (ged.GUI1, self._nec_jingle_connected),
59 'jingle-disconnected-received': (ged.GUI1,
60 self._nec_jingle_disconnected),
61 'raw-message-received': (ged.GUI1, self._nec_raw_message),
63 self.gui_extension_points = {
64 'chat_control_base' : (self.connect_with_chat_control,
65 self.disconnect_from_chat_control),
66 'chat_control_base_update_toolbar': (self.update_button_state,
67 None),
69 self.controls = []
70 self.sid = None
72 @log_calls('WhiteboardPlugin')
73 def _compute_caps_hash(self):
74 for a in gajim.connections:
75 gajim.caps_hash[a] = caps_cache.compute_caps_hash([
76 gajim.gajim_identity], gajim.gajim_common_features + \
77 gajim.gajim_optional_features[a])
78 # re-send presence with new hash
79 connected = gajim.connections[a].connected
80 if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible':
81 gajim.connections[a].change_status(gajim.SHOW_LIST[connected],
82 gajim.connections[a].status)
84 @log_calls('WhiteboardPlugin')
85 def activate(self):
86 if not HAS_GOOCANVAS:
87 raise GajimPluginException('python-pygoocanvas is missing!')
88 if NS_JINGLE_SXE not in gajim.gajim_common_features:
89 gajim.gajim_common_features.append(NS_JINGLE_SXE)
90 if NS_SXE not in gajim.gajim_common_features:
91 gajim.gajim_common_features.append(NS_SXE)
92 self._compute_caps_hash()
94 @log_calls('WhiteboardPlugin')
95 def deactivate(self):
96 if NS_JINGLE_SXE in gajim.gajim_common_features:
97 gajim.gajim_common_features.remove(NS_JINGLE_SXE)
98 if NS_SXE in gajim.gajim_common_features:
99 gajim.gajim_common_features.remove(NS_SXE)
100 self._compute_caps_hash()
102 @log_calls('WhiteboardPlugin')
103 def connect_with_chat_control(self, control):
104 if isinstance(control, chat_control.ChatControl):
105 base = Base(self, control)
106 self.controls.append(base)
108 @log_calls('WhiteboardPlugin')
109 def disconnect_from_chat_control(self, chat_control):
110 for base in self.controls:
111 base.disconnect_from_chat_control()
112 self.controls = []
114 @log_calls('WhiteboardPlugin')
115 def update_button_state(self, control):
116 for base in self.controls:
117 if base.chat_control == control:
118 if control.contact.supports(NS_JINGLE_SXE) and \
119 control.contact.supports(NS_SXE):
120 base.button.set_sensitive(True)
121 else:
122 base.button.set_sensitive(False)
124 @log_calls('WhiteboardPlugin')
125 def show_request_dialog(self, account, fjid, jid, sid, content_types):
126 def on_ok():
127 session = gajim.connections[account].get_jingle_session(fjid, sid)
128 self.sid = session.sid
129 if not session.accepted:
130 session.approve_session()
131 for content in content_types:
132 session.approve_content(content)
133 for _jid in (fjid, jid):
134 ctrl = gajim.interface.msg_win_mgr.get_control(_jid, account)
135 if ctrl:
136 break
137 if not ctrl:
138 # create it
139 gajim.interface.new_chat_from_jid(account, jid)
140 ctrl = gajim.interface.msg_win_mgr.get_control(jid, account)
141 session = session.contents[('initiator', 'xhtml')]
142 ctrl.draw_whiteboard(session)
144 def on_cancel():
145 session = gajim.connections[account].get_jingle_session(fjid, sid)
146 session.decline_session()
148 contact = gajim.contacts.get_first_contact_from_jid(account, jid)
149 if contact:
150 name = contact.get_shown_name()
151 else:
152 name = jid
153 pritext = _('Incoming Whiteboard')
154 sectext = _('%(name)s (%(jid)s) wants to start a whiteboard with '
155 'you. Do you want to accept?') % {'name': name, 'jid': jid}
156 dialog = dialogs.NonModalConfirmationDialog(pritext, sectext=sectext,
157 on_response_ok=on_ok, on_response_cancel=on_cancel)
158 dialog.popup()
160 @log_calls('WhiteboardPlugin')
161 def _nec_jingle_received(self, obj):
162 if not HAS_GOOCANVAS:
163 return
164 content_types = set(c[0] for c in obj.contents)
165 if 'xhtml' not in content_types:
166 return
167 self.show_request_dialog(obj.conn.name, obj.fjid, obj.jid, obj.sid,
168 content_types)
170 @log_calls('WhiteboardPlugin')
171 def _nec_jingle_connected(self, obj):
172 if not HAS_GOOCANVAS:
173 return
174 account = obj.conn.name
175 ctrl = (gajim.interface.msg_win_mgr.get_control(obj.fjid, account)
176 or gajim.interface.msg_win_mgr.get_control(obj.jid, account))
177 if not ctrl:
178 return
179 session = gajim.connections[obj.conn.name].get_jingle_session(obj.fjid,
180 obj.sid)
182 if ('initiator', 'xhtml') not in session.contents:
183 return
185 session = session.contents[('initiator', 'xhtml')]
186 ctrl.draw_whiteboard(session)
188 @log_calls('WhiteboardPlugin')
189 def _nec_jingle_disconnected(self, obj):
190 for base in self.controls:
191 if base.sid == obj.sid:
192 base.stop_whiteboard(reason = obj.reason)
194 @log_calls('WhiteboardPlugin')
195 def _nec_raw_message(self, obj):
196 if not HAS_GOOCANVAS:
197 return
198 if obj.stanza.getTag('sxe', namespace=NS_SXE):
199 account = obj.conn.name
201 try:
202 fjid = helpers.get_full_jid_from_iq(obj.stanza)
203 except helpers.InvalidFormat:
204 obj.conn.dispatch('ERROR', (_('Invalid Jabber ID'),
205 _('A message from a non-valid JID arrived, it has been '
206 'ignored.')))
208 jid = gajim.get_jid_without_resource(fjid)
209 ctrl = (gajim.interface.msg_win_mgr.get_control(fjid, account)
210 or gajim.interface.msg_win_mgr.get_control(jid, account))
211 if not ctrl:
212 return
213 sxe = obj.stanza.getTag('sxe')
214 if not sxe:
215 return
216 sid = sxe.getAttr('session')
217 if (jid, sid) not in obj.conn._sessions:
218 pass
219 # newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid)
220 # self.addJingle(newjingle)
222 # we already have such session in dispatcher...
223 session = obj.conn.get_jingle_session(fjid, sid)
224 cn = session.contents[('initiator', 'xhtml')]
225 error = obj.stanza.getTag('error')
226 if error:
227 action = 'iq-error'
228 else:
229 action = 'edit'
231 cn.on_stanza(obj.stanza, sxe, error, action)
232 # def __editCB(self, stanza, content, error, action):
233 #new_tags = sxe.getTags('new')
234 #remove_tags = sxe.getTags('remove')
236 #if new_tags is not None:
237 ## Process new elements
238 #for tag in new_tags:
239 #if tag.getAttr('type') == 'element':
240 #ctrl.whiteboard.recieve_element(tag)
241 #elif tag.getAttr('type') == 'attr':
242 #ctrl.whiteboard.recieve_attr(tag)
243 #ctrl.whiteboard.apply_new()
245 #if remove_tags is not None:
246 ## Delete rids
247 #for tag in remove_tags:
248 #target = tag.getAttr('target')
249 #ctrl.whiteboard.image.del_rid(target)
251 # Stop propagating this event, it's handled
252 return True
255 class Base(object):
256 def __init__(self, plugin, chat_control):
257 self.plugin = plugin
258 self.chat_control = chat_control
259 self.chat_control.draw_whiteboard = self.draw_whiteboard
260 self.contact = self.chat_control.contact
261 self.account = self.chat_control.account
262 self.jid = self.contact.get_full_jid()
263 self.create_buttons()
264 self.whiteboard = None
265 self.sid = None
267 def create_buttons(self):
268 # create juick button
269 actions_hbox = self.chat_control.xml.get_object('actions_hbox')
270 self.button = gtk.ToggleButton(label=None, use_underline=True)
271 self.button.set_property('relief', gtk.RELIEF_NONE)
272 self.button.set_property('can-focus', False)
273 img = gtk.Image()
274 img_path = self.plugin.local_file_path('whiteboard.png')
275 pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
276 iconset = gtk.IconSet(pixbuf=pixbuf)
277 factory = gtk.IconFactory()
278 factory.add('whiteboard', iconset)
279 img_path = self.plugin.local_file_path('brush_tool.png')
280 pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
281 iconset = gtk.IconSet(pixbuf=pixbuf)
282 factory.add('brush_tool', iconset)
283 img_path = self.plugin.local_file_path('line_tool.png')
284 pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
285 iconset = gtk.IconSet(pixbuf=pixbuf)
286 factory.add('line_tool', iconset)
287 img_path = self.plugin.local_file_path('oval_tool.png')
288 pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
289 iconset = gtk.IconSet(pixbuf=pixbuf)
290 factory.add('oval_tool', iconset)
291 factory.add_default()
292 img.set_from_stock('whiteboard', gtk.ICON_SIZE_BUTTON)
293 self.button.set_image(img)
294 send_button = self.chat_control.xml.get_object('send_button')
295 send_button_pos = actions_hbox.child_get_property(send_button,
296 'position')
297 actions_hbox.add_with_properties(self.button, 'position',
298 send_button_pos - 1, 'expand', False)
299 id_ = self.button.connect('toggled', self.on_whiteboard_button_toggled)
300 self.chat_control.handlers[id_] = self.button
301 self.button.show()
303 def draw_whiteboard(self, content):
304 hbox = self.chat_control.xml.get_object('chat_control_hbox')
305 if len(hbox.get_children()) == 1:
306 self.whiteboard = Whiteboard(self.account, self.contact, content,
307 self.plugin)
308 # set minimum size
309 self.whiteboard.hbox.set_size_request(300, 0)
310 hbox.pack_start(self.whiteboard.hbox, expand=False, fill=False)
311 self.whiteboard.hbox.show_all()
312 self.button.set_active(True)
313 content.control = self
314 self.sid = content.session.sid
316 def on_whiteboard_button_toggled(self, widget):
318 Popup whiteboard
320 if widget.get_active():
321 if not self.whiteboard:
322 self.start_whiteboard()
323 else:
324 self.stop_whiteboard()
326 def start_whiteboard(self):
327 conn = gajim.connections[self.chat_control.account]
328 jingle = JingleSession(conn, weinitiate=True, jid=self.jid)
329 self.sid = jingle.sid
330 conn._sessions[jingle.sid] = jingle
331 content = JingleWhiteboard(jingle)
332 content.control = self
333 jingle.add_content('xhtml', content)
334 jingle.start_session()
336 def stop_whiteboard(self, reason=None):
337 conn = gajim.connections[self.chat_control.account]
338 self.sid = None
339 session = conn.get_jingle_session(self.jid, media='xhtml')
340 if session:
341 session.end_session()
342 self.button.set_active(False)
343 if reason:
344 txt = _('Whiteboard stopped: %(reason)s') % {'reason': reason}
345 self.chat_control.print_conversation(txt, 'info')
346 if not self.whiteboard:
347 return
348 hbox = self.chat_control.xml.get_object('chat_control_hbox')
349 if self.whiteboard.hbox in hbox.get_children():
350 if hasattr(self.whiteboard, 'hbox'):
351 hbox.remove(self.whiteboard.hbox)
352 self.whiteboard = None
354 def disconnect_from_chat_control(self):
355 actions_hbox = self.chat_control.xml.get_object('actions_hbox')
356 actions_hbox.remove(self.button)
358 class JingleWhiteboard(JingleContent):
359 ''' Jingle Whiteboard sessions consist of xhtml content'''
360 def __init__(self, session, transport=None):
361 if not transport:
362 transport = JingleTransportSXE()
363 JingleContent.__init__(self, session, transport)
364 self.media = 'xhtml'
365 self.negotiated = True # there is nothing to negotiate
366 self.last_rid = 0
367 self.callbacks['session-accept'] += [self._sessionAcceptCB]
368 self.callbacks['session-terminate'] += [self._stop]
369 self.callbacks['session-terminate-sent'] += [self._stop]
370 self.callbacks['edit'] = [self._EditCB]
372 def _EditCB(self, stanza, content, error, action):
373 new_tags = content.getTags('new')
374 remove_tags = content.getTags('remove')
376 if new_tags is not None:
377 # Process new elements
378 for tag in new_tags:
379 if tag.getAttr('type') == 'element':
380 self.control.whiteboard.recieve_element(tag)
381 elif tag.getAttr('type') == 'attr':
382 self.control.whiteboard.recieve_attr(tag)
383 self.control.whiteboard.apply_new()
385 if remove_tags is not None:
386 # Delete rids
387 for tag in remove_tags:
388 target = tag.getAttr('target')
389 self.control.whiteboard.image.del_rid(target)
391 def _sessionAcceptCB(self, stanza, content, error, action):
392 log.debug('session accepted')
393 self.session.connection.dispatch('WHITEBOARD_ACCEPTED',
394 (self.session.peerjid, self.session.sid))
396 def generate_rids(self, x):
397 # generates x number of rids and returns in list
398 rids = []
399 for x in range(x):
400 rids.append(str(self.last_rid))
401 self.last_rid += 1
402 return rids
404 def send_whiteboard_node(self, items, rids):
405 # takes int rid and dict items and sends it as a node
406 # sends new item
407 jid = self.session.peerjid
408 sid = self.session.sid
409 message = xmpp.Message(to=jid)
410 sxe = message.addChild(name='sxe', attrs={'session': sid},
411 namespace=NS_SXE)
413 for x in rids:
414 if items[x]['type'] == 'element':
415 parent = x
416 attrs = {'rid': x,
417 'name': items[x]['data'][0].getName(),
418 'type': items[x]['type']}
419 sxe.addChild(name='new', attrs=attrs)
420 if items[x]['type'] == 'attr':
421 attr_name = items[x]['data']
422 chdata = items[parent]['data'][0].getAttr(attr_name)
423 attrs = {'rid': x,
424 'name': attr_name,
425 'type': items[x]['type'],
426 'chdata': chdata,
427 'parent': parent}
428 sxe.addChild(name='new', attrs=attrs)
429 self.session.connection.connection.send(message)
431 def delete_whiteboard_node(self, rids):
432 message = xmpp.Message(to=self.session.peerjid)
433 sxe = message.addChild(name='sxe', attrs={'session': self.session.sid},
434 namespace=NS_SXE)
436 for x in rids:
437 sxe.addChild(name='remove', attrs = {'target': x})
438 self.session.connection.connection.send(message)
440 def send_items(self, items, rids):
441 # recieves dict items and a list of rids of items to send
442 # TODO: is there a less clumsy way that doesn't involve passing
443 # whole list
444 self.send_whiteboard_node(items, rids)
446 def del_item(self, rids):
447 self.delete_whiteboard_node(rids)
449 def encode(self, xml):
450 # encodes it sendable string
451 return 'data:text/xml,' + urllib.quote(xml)
453 def _fill_content(self, content):
454 content.addChild(NS_JINGLE_XHTML + ' description')
456 def _stop(self, *things):
457 pass
459 def __del__(self):
460 pass
462 def get_content(desc):
463 return JingleWhiteboard
465 common.jingle_content.contents[NS_JINGLE_XHTML] = get_content
467 class JingleTransportSXE(JingleTransport):
468 def __init__(self):
469 JingleTransport.__init__(self, TransportType.streaming)
471 def make_transport(self, candidates=None):
472 transport = JingleTransport.make_transport(self, candidates)
473 transport.setNamespace(NS_JINGLE_SXE)
474 transport.setTagData('host', 'TODO')
475 return transport
477 common.jingle_transport.transports[NS_JINGLE_SXE] = JingleTransportSXE