diff options
author | Yann Leboulanger <yann@leboulanger.org> | 2017-09-28 23:45:59 +0300 |
---|---|---|
committer | Yann Leboulanger <yann@leboulanger.org> | 2017-09-28 23:45:59 +0300 |
commit | d564acac1974609a034a22685f0bd394429c37a0 (patch) | |
tree | 9b2fa0bba7b3b9722f932dd391f9e10685934b7c | |
parent | 72db56fda87344dcc9ccbed3cbfc58a5ec6ccd8d (diff) |
[whiteboard] Port to Py3 / GTK3
-rw-r--r-- | whiteboard/__init__.py | 1 | ||||
-rw-r--r-- | whiteboard/brush_tool.png | bin | 0 -> 806 bytes | |||
-rw-r--r-- | whiteboard/line_tool.png | bin | 0 -> 1054 bytes | |||
-rw-r--r-- | whiteboard/manifest.ini | 8 | ||||
-rw-r--r-- | whiteboard/oval_tool.png | bin | 0 -> 989 bytes | |||
-rw-r--r-- | whiteboard/plugin.py | 488 | ||||
-rw-r--r-- | whiteboard/whiteboard.png | bin | 0 -> 1550 bytes | |||
-rw-r--r-- | whiteboard/whiteboard_widget.py | 419 | ||||
-rw-r--r-- | whiteboard/whiteboard_widget.ui | 201 |
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 Binary files differnew file mode 100644 index 0000000..266c321 --- /dev/null +++ b/whiteboard/brush_tool.png diff --git a/whiteboard/line_tool.png b/whiteboard/line_tool.png Binary files differnew file mode 100644 index 0000000..151f584 --- /dev/null +++ b/whiteboard/line_tool.png 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 Binary files differnew file mode 100644 index 0000000..efd6f0c --- /dev/null +++ b/whiteboard/oval_tool.png 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 Binary files differnew file mode 100644 index 0000000..13318e3 --- /dev/null +++ b/whiteboard/whiteboard.png 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> |