diff --git a/plugins/whiteboard/__init__.py b/plugins/whiteboard/__init__.py new file mode 100644 index 000000000..802d00cdf --- /dev/null +++ b/plugins/whiteboard/__init__.py @@ -0,0 +1 @@ +from plugin import WhiteboardPlugin diff --git a/plugins/whiteboard/brush_tool.png b/plugins/whiteboard/brush_tool.png new file mode 100644 index 000000000..266c32171 Binary files /dev/null and b/plugins/whiteboard/brush_tool.png differ diff --git a/plugins/whiteboard/line_tool.png b/plugins/whiteboard/line_tool.png new file mode 100644 index 000000000..151f58484 Binary files /dev/null and b/plugins/whiteboard/line_tool.png differ diff --git a/plugins/whiteboard/manifest.ini b/plugins/whiteboard/manifest.ini new file mode 100644 index 000000000..309d372f7 --- /dev/null +++ b/plugins/whiteboard/manifest.ini @@ -0,0 +1,7 @@ +[info] +name: Whiteboard +short_name: whiteboard +version: 0.1 +description: Shows a whiteboard in chat. +authors = Yann Leboulanger +homepage = www.gajim.org diff --git a/plugins/whiteboard/oval_tool.png b/plugins/whiteboard/oval_tool.png new file mode 100644 index 000000000..efd6f0ca1 Binary files /dev/null and b/plugins/whiteboard/oval_tool.png differ diff --git a/plugins/whiteboard/plugin.py b/plugins/whiteboard/plugin.py new file mode 100644 index 000000000..4251d353d --- /dev/null +++ b/plugins/whiteboard/plugin.py @@ -0,0 +1,453 @@ +## plugins/whiteboard/plugin.py +## +## Copyright (C) 2009 Jeff Ling +## Copyright (C) 2010 Yann Leboulanger +## +## 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 . +## + +''' +Whiteboard plugin. + +:author: Yann Leboulanger +:since: 1st November 2010 +:copyright: Copyright (2010) Yann Leboulanger +:license: GPL +''' + + +from common import helpers +from common import gajim +from plugins import GajimPlugin +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 +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.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 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) + else: + base.button.set_sensitive(False) + + @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): + 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): + 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() + + @log_calls('WhiteboardPlugin') + def _nec_raw_message(self, obj): + 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 juick 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) + 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): + conn = gajim.connections[self.chat_control.account] + self.sid = None + session = conn.get_jingle_session(self.jid, media='xhtml') + if session: + session.end_session() + if not self.whiteboard: + return + hbox = self.chat_control.xml.get_object('chat_control_hbox') + for child in hbox.get_children(): + if child == self.whiteboard.hbox: + self.button.set_active(False) + hbox.remove(child) + self.whiteboard = None + break + + 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 diff --git a/plugins/whiteboard/whiteboard.png b/plugins/whiteboard/whiteboard.png new file mode 100644 index 000000000..13318e3a7 Binary files /dev/null and b/plugins/whiteboard/whiteboard.png differ diff --git a/plugins/whiteboard/whiteboard_widget.py b/plugins/whiteboard/whiteboard_widget.py new file mode 100644 index 000000000..f47835f83 --- /dev/null +++ b/plugins/whiteboard/whiteboard_widget.py @@ -0,0 +1,416 @@ +## plugins/whiteboard/whiteboard_widget.py +## +## Copyright (C) 2009 Jeff Ling +## Copyright (C) 2010 Yann Leboulanger +## +## 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 . +## + +import gtk +import gtkgui_helpers +import goocanvas +from common.xmpp import Node +from common import gajim +from common import i18n +from dialogs import FileChooserDialog + +''' +A whiteboard widget made for Gajim. +- Ummu +''' + +class Whiteboard(object): + def __init__(self, account, contact, session, plugin): + self.plugin = plugin + file_path = plugin.local_file_path('whiteboard_widget.ui') + xml = gtk.Builder() + xml.set_translation_domain(i18n.APP) + xml.add_from_file(file_path) + self.hbox = xml.get_object('whiteboard_hbox') + self.canevas = goocanvas.Canvas() + self.hbox.pack_start(self.canevas) + self.hbox.reorder_child(self.canevas, 0) + self.canevas.set_flags(gtk.CAN_FOCUS) + self.fg_color_select_button = xml.get_object('fg_color_button') + self.root = self.canevas.get_root_item() + self.tool_buttons = [] + for tool in ('brush', 'oval', 'line', 'delete'): + self.tool_buttons.append(xml.get_object(tool + '_button')) + xml.get_object('brush_button').set_active(True) + + # Events + self.canevas.connect('button-press-event', self.button_press_event) + self.canevas.connect('button-release-event', self.button_release_event) + self.canevas.connect('motion-notify-event', self.motion_notify_event) + self.canevas.connect('item-created', self.item_created) + + # Config + self.line_width = 2 + xml.get_object('size_scale').set_value(2) + self.color = str(self.fg_color_select_button.get_color()) + + # SVG Storage + self.image = SVGObject(self.root, session) + + xml.connect_signals(self) + + # Temporary Variables for items + self.item_temp = None + self.item_temp_coords = (0, 0) + self.item_data = None + + # Will be {ID: {type:'element', data:[node, goocanvas]}, ID2: {}} instance + self.recieving = {} + + def on_tool_button_toggled(self, widget): + for btn in self.tool_buttons: + if btn == widget: + continue + btn.set_active(False) + + def on_brush_button_toggled(self, widget): + if widget.get_active(): + self.image.draw_tool = 'brush' + self.on_tool_button_toggled(widget) + + def on_oval_button_toggled(self, widget): + if widget.get_active(): + self.image.draw_tool = 'oval' + self.on_tool_button_toggled(widget) + + def on_line_button_toggled(self, widget): + if widget.get_active(): + self.image.draw_tool = 'line' + self.on_tool_button_toggled(widget) + + def on_delete_button_toggled(self, widget): + if widget.get_active(): + self.image.draw_tool = 'delete' + self.on_tool_button_toggled(widget) + + def on_clear_button_clicked(self, widget): + self.image.clear_canvas() + + def on_export_button_clicked(self, widget): + self.image.export_svg(filename) + + def on_fg_color_button_color_set(self, widget): + self.color = str(self.fg_color_select_button.get_color()) + + def item_created(self, canvas, item, model): + print 'item created' + item.connect('button-press-event', self.item_button_press_events) + + def item_button_press_events(self, item, target_item, event): + if self.image.draw_tool == 'delete': + self.image.del_item(item) + + def on_size_scale_format_value(self, widget): + self.line_width = int(widget.get_value()) + + def button_press_event(self, widget, event): + x = event.x + y = event.y + state = event.state + self.item_temp_coords = (x, y) + + if self.image.draw_tool == 'brush': + self.item_temp = goocanvas.Ellipse(parent=self.root, + center_x=x, + center_y=y, + radius_x=1, + radius_y=1, + stroke_color=self.color, + fill_color=self.color, + line_width=self.line_width) + self.item_data = 'M %s,%s L ' % (x, y) + + elif self.image.draw_tool == 'oval': + self.item_data = True + + if self.image.draw_tool == 'line': + self.item_data = 'M %s,%s L' % (x, y) + + def motion_notify_event(self, widget, event): + x = event.x + y = event.y + state = event.state + if self.item_temp is not None: + self.item_temp.remove() + + if self.item_data is not None: + if self.image.draw_tool == 'brush': + self.item_data = self.item_data + '%s,%s ' % (x, y) + self.item_temp = goocanvas.Path(parent=self.root, + data=self.item_data, line_width=self.line_width, + stroke_color=self.color) + elif self.image.draw_tool == 'oval': + self.item_temp = goocanvas.Ellipse(parent=self.root, + center_x=self.item_temp_coords[0] + (x - self.item_temp_coords[0]) / 2, + center_y=self.item_temp_coords[1] + (y - self.item_temp_coords[1]) / 2, + radius_x=abs(x - self.item_temp_coords[0]) / 2, + radius_y=abs(y - self.item_temp_coords[1]) / 2, + stroke_color=self.color, + line_width=self.line_width) + elif self.image.draw_tool == 'line': + self.item_data = 'M %s,%s L' % self.item_temp_coords + self.item_data = self.item_data + ' %s,%s' % (x, y) + self.item_temp = goocanvas.Path(parent=self.root, + data=self.item_data, line_width=self.line_width, + stroke_color=self.color) + + def button_release_event(self, widget, event): + x = event.x + y = event.y + state = event.state + + if self.image.draw_tool == 'brush': + self.item_data = self.item_data + '%s,%s' % (x, y) + if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]: + goocanvas.Ellipse(parent=self.root, + center_x=x, + center_y=y, + radius_x=1, + radius_y=1, + stroke_color=self.color, + fill_color=self.color, + line_width=self.line_width) + self.image.add_path(self.item_data, self.line_width, self.color) + + if self.image.draw_tool == 'oval': + cx = self.item_temp_coords[0] + (x - self.item_temp_coords[0]) / 2 + cy = self.item_temp_coords[1] + (y - self.item_temp_coords[1]) / 2 + rx = abs(x - self.item_temp_coords[0]) / 2 + ry = abs(y - self.item_temp_coords[1]) / 2 + self.image.add_ellipse(cx, cy, rx, ry, self.line_width, self.color) + + if self.image.draw_tool == 'line': + self.item_data = 'M %s,%s L' % self.item_temp_coords + self.item_data = self.item_data + ' %s,%s' % (x, y) + if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]: + goocanvas.Ellipse(parent=self.root, + center_x=x, + center_y=y, + radius_x=1, + radius_y=1, + stroke_color='black', + fill_color='black', + line_width=self.line_width) + self.image.add_path(self.item_data, self.line_width, self.color) + + if self.image.draw_tool == 'delete': + pass + + self.item_data = None + if self.item_temp is not None: + self.item_temp.remove() + self.item_temp = None + + def recieve_element(self, element): + node = self.image.g.addChild(name=element.getAttr('name')) + self.image.g.addChild(node=node) + self.recieving[element.getAttr('rid')] = {'type':'element', + 'data':[node], + 'children':[]} + + def recieve_attr(self, element): + node = self.recieving[element.getAttr('parent')]['data'][0] + node.setAttr(element.getAttr('name'), element.getAttr('chdata')) + + self.recieving[element.getAttr('rid')] = {'type':'attr', + 'data':element.getAttr('name'), + 'parent':node} + self.recieving[element.getAttr('parent')]['children'].append(element.getAttr('rid')) + + def apply_new(self): + for x in self.recieving.keys(): + if self.recieving[x]['type'] == 'element': + self.image.add_recieved(x, self.recieving) + + self.recieving = {} + +class SvgChooserDialog(FileChooserDialog): + def __init__(self, on_response_ok=None, on_response_cancel=None): + ''' + Choose in which SVG file to store the image + ''' + def on_ok(widget, callback): + ''' + check if file exists and call callback + ''' + path_to_clientcert_file = self.get_filename() + path_to_clientcert_file = \ + gtkgui_helpers.decode_filechooser_file_paths( + (path_to_clientcert_file,))[0] + if os.path.exists(path_to_clientcert_file): + callback(widget, path_to_clientcert_file) + + FileChooserDialog.__init__(self, + title_text=_('Save Image as...'), + action=gtk.FILE_CHOOSER_ACTION_SAVE, + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, + gtk.RESPONSE_OK), + current_folder='', + default_response=gtk.RESPONSE_OK, + on_response_ok=(on_ok, on_response_ok), + on_response_cancel=on_response_cancel) + + filter_ = gtk.FileFilter() + filter_.set_name(_('All files')) + filter_.add_pattern('*') + self.add_filter(filter_) + + filter_ = gtk.FileFilter() + filter_.set_name(_('SVG Files')) + filter_.add_pattern('*.svg') + self.add_filter(filter_) + self.set_filter(filter_) + + +class SVGObject(): + ''' A class to store the svg document and make changes to it.''' + + def __init__(self, root, session, height=300, width=300): + # Will be {ID: {type:'element', data:[node, goocanvas]}, ID2: {}} instance + self.items = {} + self.root = root + self.draw_tool = 'brush' + + # sxe session + self.session = session + + # initialize svg document + self.svg = Node(node='') + self.svg.setAttr('version', '1.1') + self.svg.setAttr('height', str(height)) + self.svg.setAttr('width', str(width)) + self.svg.setAttr('xmlns', 'http://www.w3.org/2000/svg') + # TODO: make this settable + self.g = self.svg.addChild(name='g') + self.g.setAttr('fill', 'none') + self.g.setAttr('stroke-linecap', 'round') + + def add_path(self, data, line_width, color): + ''' adds the path to the items listing, both minidom node and goocanvas + object in a tuple ''' + + goocanvas_obj = goocanvas.Path(parent=self.root, data=data, + line_width=line_width, stroke_color=color) + goocanvas_obj.connect('button-press-event', self.item_button_press_events) + + node = self.g.addChild(name='path') + node.setAttr('d', data) + node.setAttr('stroke-width', str(line_width)) + node.setAttr('stroke', color) + self.g.addChild(node=node) + + rids = self.session.generate_rids(4) + self.items[rids[0]] = {'type':'element', 'data':[node, goocanvas_obj], 'children':rids[1:]} + self.items[rids[1]] = {'type':'attr', 'data':'d', 'parent':node} + self.items[rids[2]] = {'type':'attr', 'data':'stroke-width', 'parent':node} + self.items[rids[3]] = {'type':'attr', 'data':'stroke', 'parent':node} + + self.session.send_items(self.items, rids) + + def add_recieved(self, parent_rid, new_items): + ''' adds the path to the items listing, both minidom node and goocanvas + object in a tuple ''' + node = new_items[parent_rid]['data'][0] + + self.items[parent_rid] = new_items[parent_rid] + for x in new_items[parent_rid]['children']: + self.items[x] = new_items[x] + + if node.getName() == 'path': + goocanvas_obj = goocanvas.Path(parent=self.root, + data=node.getAttr('d'), + line_width=int(node.getAttr('stroke-width')), + stroke_color=node.getAttr('stroke')) + + if node.getName() == 'ellipse': + goocanvas_obj = goocanvas.Ellipse(parent=self.root, + center_x=float(node.getAttr('cx')), + center_y=float(node.getAttr('cy')), + radius_x=float(node.getAttr('rx')), + radius_y=float(node.getAttr('ry')), + stroke_color=node.getAttr('stroke'), + line_width=float(node.getAttr('stroke-width'))) + + self.items[parent_rid]['data'].append(goocanvas_obj) + goocanvas_obj.connect('button-press-event', self.item_button_press_events) + + def add_ellipse(self, cx, cy, rx, ry, line_width, stroke_color): + ''' adds the ellipse to the items listing, both minidom node and goocanvas + object in a tuple ''' + + goocanvas_obj = goocanvas.Ellipse(parent=self.root, + center_x=cx, + center_y=cy, + radius_x=rx, + radius_y=ry, + stroke_color=stroke_color, + line_width=line_width) + goocanvas_obj.connect('button-press-event', self.item_button_press_events) + + node = self.g.addChild(name='ellipse') + node.setAttr('cx', str(cx)) + node.setAttr('cy', str(cy)) + node.setAttr('rx', str(rx)) + node.setAttr('ry', str(ry)) + node.setAttr('stroke-width', str(line_width)) + node.setAttr('stroke', stroke_color) + self.g.addChild(node=node) + + rids = self.session.generate_rids(7) + self.items[rids[0]] = {'type':'element', 'data':[node, goocanvas_obj], 'children':rids[1:]} + self.items[rids[1]] = {'type':'attr', 'data':'cx', 'parent':node} + self.items[rids[2]] = {'type':'attr', 'data':'cy', 'parent':node} + self.items[rids[3]] = {'type':'attr', 'data':'rx', 'parent':node} + self.items[rids[4]] = {'type':'attr', 'data':'ry', 'parent':node} + self.items[rids[5]] = {'type':'attr', 'data':'stroke-width', 'parent':node} + self.items[rids[6]] = {'type':'attr', 'data':'stroke', 'parent':node} + + self.session.send_items(self.items, rids) + + def del_item(self, item): + rids = [] + for x in self.items.keys(): + if self.items[x]['type'] == 'element': + if self.items[x]['data'][1] == item: + for y in self.items[x]['children']: + rids.append(y) + self.del_rid(y) + rids.append(x) + self.del_rid(x) + break + self.session.del_item(rids) + + def clear_canvas(self): + for x in self.items.keys(): + if self.items[x]['type'] == 'element': + self.del_rid(x) + + def del_rid(self, rid): + if self.items[rid]['type'] == 'element': + self.items[rid]['data'][1].remove() + del self.items[rid] + + def export_svg(self, filename): + file = open(filename, 'w') + file.writelines(str(self.svg)) + file.close() + + def item_button_press_events(self, item, target_item, event): + self.del_item(item) diff --git a/plugins/whiteboard/whiteboard_widget.ui b/plugins/whiteboard/whiteboard_widget.ui new file mode 100644 index 000000000..22e13852d --- /dev/null +++ b/plugins/whiteboard/whiteboard_widget.ui @@ -0,0 +1,324 @@ + + + + + + True + 6 + + + + + + True + 6 + vertical + 6 + start + + + True + True + True + + + + True + 6 + + + True + 1 + brush_tool.png + + + 0 + + + + + True + 0 + Brush Tool + + + 1 + + + + + + + False + False + 0 + + + + + True + True + True + + + + True + 6 + + + True + 1 + oval_tool.png + + + 0 + + + + + True + 0 + Oval Tool + + + 1 + + + + + + + False + False + 1 + + + + + True + True + True + + + + True + 6 + + + True + 1 + line_tool.png + + + 0 + + + + + True + 0 + Line Tool + + + 1 + + + + + + + False + False + 2 + + + + + True + True + True + + + + True + 6 + + + True + 1 + gtk-delete + + + 0 + + + + + True + 0 + Delete Tool + + + 1 + + + + + + + False + False + 3 + + + + + True + True + True + + + + True + 6 + + + True + 1 + gtk-clear + + + 0 + + + + + True + 0 + Clear Canvas + + + 1 + + + + + + + False + False + 4 + + + + + True + True + True + + + + True + 6 + + + True + 1 + gtk-save-as + + + 0 + + + + + True + 0 + Export Image + + + 1 + + + + + + + False + False + 5 + + + + + True + True + adjustment1 + 0 + + + + False + False + 6 + True + + + + + True + + + True + 1 + 1 + Foreground +Color: + + + 0 + + + + + True + True + True + #000000000000 + + + + False + False + 1 + + + + + False + False + 7 + True + + + + + False + False + 1 + + + + + True + gtk-delete + + + 2 + 1 + 110 + 1 + 10 + 10 + +