Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim-plugins.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYann Leboulanger <yann@leboulanger.org>2017-09-28 23:45:59 +0300
committerYann Leboulanger <yann@leboulanger.org>2017-09-28 23:45:59 +0300
commitd564acac1974609a034a22685f0bd394429c37a0 (patch)
tree9b2fa0bba7b3b9722f932dd391f9e10685934b7c
parent72db56fda87344dcc9ccbed3cbfc58a5ec6ccd8d (diff)
[whiteboard] Port to Py3 / GTK3
-rw-r--r--whiteboard/__init__.py1
-rw-r--r--whiteboard/brush_tool.pngbin0 -> 806 bytes
-rw-r--r--whiteboard/line_tool.pngbin0 -> 1054 bytes
-rw-r--r--whiteboard/manifest.ini8
-rw-r--r--whiteboard/oval_tool.pngbin0 -> 989 bytes
-rw-r--r--whiteboard/plugin.py488
-rw-r--r--whiteboard/whiteboard.pngbin0 -> 1550 bytes
-rw-r--r--whiteboard/whiteboard_widget.py419
-rw-r--r--whiteboard/whiteboard_widget.ui201
9 files changed, 1117 insertions, 0 deletions
diff --git a/whiteboard/__init__.py b/whiteboard/__init__.py
new file mode 100644
index 0000000..e6ad0ee
--- /dev/null
+++ b/whiteboard/__init__.py
@@ -0,0 +1 @@
+from .plugin import WhiteboardPlugin
diff --git a/whiteboard/brush_tool.png b/whiteboard/brush_tool.png
new file mode 100644
index 0000000..266c321
--- /dev/null
+++ b/whiteboard/brush_tool.png
Binary files differ
diff --git a/whiteboard/line_tool.png b/whiteboard/line_tool.png
new file mode 100644
index 0000000..151f584
--- /dev/null
+++ b/whiteboard/line_tool.png
Binary files differ
diff --git a/whiteboard/manifest.ini b/whiteboard/manifest.ini
new file mode 100644
index 0000000..413f8bf
--- /dev/null
+++ b/whiteboard/manifest.ini
@@ -0,0 +1,8 @@
+[info]
+name: Whiteboard
+short_name: whiteboard
+version: 0.3
+description: Shows a whiteboard in chat. python-pygoocanvas is required.
+authors = Yann Leboulanger <asterix@lagaule.org>
+homepage = https://dev.gajim.org/gajim/gajim-plugins/wikis/WhiteboardPlugin
+min_gajim_version: 0.16.11
diff --git a/whiteboard/oval_tool.png b/whiteboard/oval_tool.png
new file mode 100644
index 0000000..efd6f0c
--- /dev/null
+++ b/whiteboard/oval_tool.png
Binary files differ
diff --git a/whiteboard/plugin.py b/whiteboard/plugin.py
new file mode 100644
index 0000000..18d7b70
--- /dev/null
+++ b/whiteboard/plugin.py
@@ -0,0 +1,488 @@
+## 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 gajim import common
+from gajim.common import helpers
+from gajim.common import app
+from gajim.plugins import GajimPlugin
+from gajim.plugins.gajimplugin import GajimPluginException
+from gajim.plugins.helpers import log_calls, log
+from nbxmpp import Message
+from gi.repository import Gtk
+from gi.repository import GdkPixbuf
+from gajim import chat_control
+from gajim.common import ged
+from gajim.common.jingle_session import JingleSession
+from gajim.common.jingle_content import JingleContent
+from gajim.common.jingle_transport import JingleTransport, TransportType
+from gajim import dialogs
+from .whiteboard_widget import Whiteboard, HAS_GOOCANVAS
+from gajim.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 app.connections:
+ app.caps_hash[a] = caps_cache.compute_caps_hash([
+ app.gajim_identity], app.gajim_common_features + \
+ app.gajim_optional_features[a])
+ # re-send presence with new hash
+ connected = app.connections[a].connected
+ if connected > 1 and app.SHOW_LIST[connected] != 'invisible':
+ app.connections[a].change_status(app.SHOW_LIST[connected],
+ app.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 app.gajim_common_features:
+ app.gajim_common_features.append(NS_JINGLE_SXE)
+ if NS_SXE not in app.gajim_common_features:
+ app.gajim_common_features.append(NS_SXE)
+ self._compute_caps_hash()
+
+ @log_calls('WhiteboardPlugin')
+ def deactivate(self):
+ if NS_JINGLE_SXE in app.gajim_common_features:
+ app.gajim_common_features.remove(NS_JINGLE_SXE)
+ if NS_SXE in app.gajim_common_features:
+ app.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 = app.connections[account].get_jingle_session(fjid, sid)
+ self.sid = session.sid
+ print(session.accepted)
+ if not session.accepted:
+ session.approve_session()
+ for content in content_types:
+ session.approve_content('xhtml')
+ for _jid in (fjid, jid):
+ ctrl = app.interface.msg_win_mgr.get_control(_jid, account)
+ if ctrl:
+ break
+ if not ctrl:
+ # create it
+ app.interface.new_chat_from_jid(account, jid)
+ ctrl = app.interface.msg_win_mgr.get_control(jid, account)
+ session = session.contents[('initiator', 'xhtml')]
+ ctrl.draw_whiteboard(session)
+
+ def on_cancel():
+ session = app.connections[account].get_jingle_session(fjid, sid)
+ session.decline_session()
+
+ contact = app.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 = obj.contents.media
+ if content_types != 'xhtml':
+ 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 = (app.interface.msg_win_mgr.get_control(obj.fjid, account)
+ or app.interface.msg_win_mgr.get_control(obj.jid, account))
+ if not ctrl:
+ return
+ session = app.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 = app.get_jid_without_resource(fjid)
+ ctrl = (app.interface.msg_win_mgr.get_control(fjid, account)
+ or app.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()
+ self.button.set_property('relief', Gtk.ReliefStyle.NONE)
+ self.button.set_property('can-focus', False)
+ img = Gtk.Image()
+ img_path = self.plugin.local_file_path('whiteboard.png')
+ pixbuf = GdkPixbuf.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 = GdkPixbuf.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 = GdkPixbuf.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 = GdkPixbuf.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.IconSize.MENU)
+ 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.pack_start(self.button, False, False, 0)
+ actions_hbox.reorder_child(self.button, send_button_pos - 1)
+ 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, False, False, 0)
+ 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 = app.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 = app.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, senders=None):
+ if not transport:
+ transport = JingleTransportSXE()
+ JingleContent.__init__(self, session, transport, senders)
+ 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]
+
+ @log_calls('WhiteboardPlugin')
+ def _EditCB(self, stanza, content, error, action):
+ new_tags = content.getTags('new')
+ remove_tags = content.getTags('remove')
+ if not self.control.whiteboard:
+ return
+
+ 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)
+
+ @log_calls('WhiteboardPlugin')
+ 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
+
+ @log_calls('WhiteboardPlugin')
+ 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 = 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)
+
+ @log_calls('WhiteboardPlugin')
+ def delete_whiteboard_node(self, rids):
+ message = 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, node=None):
+ JingleTransport.__init__(self, TransportType.SOCKS5)
+
+ 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/whiteboard/whiteboard.png b/whiteboard/whiteboard.png
new file mode 100644
index 0000000..13318e3
--- /dev/null
+++ b/whiteboard/whiteboard.png
Binary files differ
diff --git a/whiteboard/whiteboard_widget.py b/whiteboard/whiteboard_widget.py
new file mode 100644
index 0000000..f816905
--- /dev/null
+++ b/whiteboard/whiteboard_widget.py
@@ -0,0 +1,419 @@
+## plugins/whiteboard/whiteboard_widget.py
+##
+## Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com>
+## Copyright (C) 2010-2017 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/>.
+##
+
+from gi.repository import Gtk
+from gajim import gtkgui_helpers
+try:
+ import gi
+ gi.require_version('GooCanvas', '2.0')
+ from gi.repository import GooCanvas
+ HAS_GOOCANVAS = True
+except:
+ HAS_GOOCANVAS = False
+from nbxmpp import Node
+from gajim.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('gajim_plugins')
+ xml.add_from_file(file_path)
+ self.hbox = xml.get_object('whiteboard_hbox')
+ self.canevas = GooCanvas.Canvas()
+ self.hbox.pack_start(self.canevas, True, True, 0)
+ self.hbox.reorder_child(self.canevas, 0)
+ 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)
+ c = self.fg_color_select_button.get_rgba()
+ self.color = int(c.red*255*256*256*256 + c.green*255*256*256 + \
+ c.blue*255*256 + 255)
+
+ # 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):
+ SvgChooserDialog(self.image.export_svg)
+
+ def on_fg_color_button_color_set(self, widget):
+ c = self.fg_color_select_button.get_rgba()
+ self.color = int(c.red*255*256*256*256 + c.green*255*256*256 + \
+ c.blue*255*256 + 255)
+
+ def item_created(self, canvas, item, model):
+ 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.CanvasEllipse(parent=self.root,
+ center_x=x,
+ center_y=y,
+ radius_x=1,
+ radius_y=1,
+ stroke_color_rgba=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.CanvasPath(parent=self.root,
+ data=self.item_data, line_width=self.line_width,
+ stroke_color_rgba=self.color)
+ elif self.image.draw_tool == 'oval':
+ self.item_temp = GooCanvas.CanvasEllipse(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_rgba=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.CanvasPath(parent=self.root,
+ data=self.item_data, line_width=self.line_width,
+ stroke_color_rgba=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.CanvasEllipse(parent=self.root,
+ center_x=x,
+ center_y=y,
+ radius_x=1,
+ radius_y=1,
+ stroke_color_rgba=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.CanvasEllipse(parent=self.root,
+ center_x=x,
+ center_y=y,
+ radius_x=1,
+ radius_y=1,
+ stroke_color_rgba=self.color,
+ fill_color_rgba=self.color,
+ 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_file = self.get_filename()
+ widget.destroy()
+ callback(path_to_file)
+
+ FileChooserDialog.__init__(self,
+ title_text=_('Save Image as...'),
+ action=Gtk.FileChooserAction.SAVE,
+ buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE,
+ Gtk.ResponseType.OK),
+ current_folder='',
+ default_response=Gtk.ResponseType.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='<svg/>')
+ 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.CanvasPath(parent=self.root, data=data,
+ line_width=line_width, stroke_color_rgba=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', str(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.CanvasPath(parent=self.root,
+ data=node.getAttr('d'),
+ line_width=int(node.getAttr('stroke-width')),
+ stroke_color_rgba=int(node.getAttr('stroke')))
+
+ if node.getName() == 'ellipse':
+ goocanvas_obj = GooCanvas.CanvasEllipse(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_rgba=int(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.CanvasEllipse(parent=self.root,
+ center_x=cx,
+ center_y=cy,
+ radius_x=rx,
+ radius_y=ry,
+ stroke_color_rgba=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', str(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 list(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 list(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):
+ f = open(filename, 'w')
+ f.writelines(str(self.svg))
+ f.close()
+
+ def item_button_press_events(self, item, target_item, event):
+ self.del_item(item)
diff --git a/whiteboard/whiteboard_widget.ui b/whiteboard/whiteboard_widget.ui
new file mode 100644
index 0000000..750c162
--- /dev/null
+++ b/whiteboard/whiteboard_widget.ui
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="lower">1</property>
+ <property name="upper">110</property>
+ <property name="value">2</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ <property name="page_size">10</property>
+ </object>
+ <object class="GtkBox" id="whiteboard_hbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">3</property>
+ <property name="spacing">6</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="vbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">6</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkToggleButton" id="brush_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Brush Tool: Draw freehand lines</property>
+ <signal name="toggled" handler="on_brush_button_toggled" swapped="no"/>
+ <child>
+ <object class="GtkImage" id="image5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">brush_tool</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="oval_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Oval Tool: Draw circles and ellipses</property>
+ <signal name="toggled" handler="on_oval_button_toggled" swapped="no"/>
+ <child>
+ <object class="GtkImage" id="image6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">oval_tool</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="line_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Line Tool: Draw straight lines</property>
+ <signal name="toggled" handler="on_line_button_toggled" swapped="no"/>
+ <child>
+ <object class="GtkImage" id="image7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">line_tool</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="delete_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Delete Tool: Remove individual figures</property>
+ <signal name="toggled" handler="on_delete_button_toggled" swapped="no"/>
+ <child>
+ <object class="GtkImage" id="image2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-delete</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="clear_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Clear Canvas: Cleanup canvas</property>
+ <signal name="clicked" handler="on_clear_button_clicked" swapped="no"/>
+ <child>
+ <object class="GtkImage" id="image3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-clear</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="export_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Export Image: Save image to svg file</property>
+ <signal name="clicked" handler="on_export_button_clicked" swapped="no"/>
+ <child>
+ <object class="GtkImage" id="image4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-save-as</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale" id="size_scale">
+ <property name="height_request">68</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="tooltip_text" translatable="yes">Line width</property>
+ <property name="orientation">vertical</property>
+ <property name="adjustment">adjustment1</property>
+ <property name="inverted">True</property>
+ <property name="digits">0</property>
+ <property name="value_pos">bottom</property>
+ <signal name="value-changed" handler="on_size_scale_format_value" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkColorButton" id="fg_color_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Foreground color</property>
+ <property name="rgba">rgb(0,0,0)</property>
+ <signal name="color-set" handler="on_fg_color_button_color_set" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <object class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-delete</property>
+ </object>
+</interface>