[whiteboard] Code cleanup, fixes

This commit is contained in:
Daniel Brötzmann
2019-08-19 08:22:36 +02:00
committed by Philipp Hörist
parent f16a6618e5
commit cf1e40e182
3 changed files with 418 additions and 335 deletions

View File

@@ -1,22 +1,20 @@
## plugins/whiteboard/plugin.py # Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com>
## # Copyright (C) 2010 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com> #
## Copyright (C) 2010 Yann Leboulanger <asterix AT lagaule.org> # This file is part of the Whiteboard Plugin.
## #
## 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
## Gajim is free software; you can redistribute it and/or modify # by the Free Software Foundation; version 3 only.
## 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
## Gajim is distributed in the hope that it will be useful, # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## but WITHOUT ANY WARRANTY; without even the implied warranty of # GNU General Public License for more details.
## 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/>.
## 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. Whiteboard plugin.
@@ -27,45 +25,53 @@ Whiteboard plugin.
:license: GPL :license: GPL
''' '''
from urllib.parse import quote
from gi.repository import Gio from gi.repository import Gio
from gi.repository import GLib from gi.repository import GLib
from nbxmpp import Message from nbxmpp import Message
from gajim import common from gajim import common
from gajim.common import helpers from gajim import chat_control
from gajim.common import app from gajim.common import app
from gajim.common import ged
from gajim.common import helpers
from gajim.common.jingle_session import JingleSession
from gajim.common.jingle_content import JingleContent
from gajim.common.jingle_transport import JingleTransport
from gajim.common.jingle_transport import TransportType
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.gajimplugin import GajimPluginException from gajim.plugins.gajimplugin import GajimPluginException
from gajim.plugins.helpers import log_calls, log from gajim.plugins.helpers import log_calls
from gajim.plugins.helpers import log
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from gajim import chat_control from whiteboard.whiteboard_widget import Whiteboard
from gajim.common import ged from whiteboard.whiteboard_widget import HAS_GOOCANVAS
from gajim.common.jingle_session import JingleSession
from gajim.common.jingle_content import JingleContent
from gajim.common.jingle_transport import JingleTransport, TransportType
from gajim.gtk.dialogs import NonModalConfirmationDialog
from .whiteboard_widget import Whiteboard, HAS_GOOCANVAS
NS_JINGLE_XHTML = 'urn:xmpp:tmp:jingle:apps:xhtml' NS_JINGLE_XHTML = 'urn:xmpp:tmp:jingle:apps:xhtml'
NS_JINGLE_SXE = 'urn:xmpp:tmp:jingle:transports:sxe' NS_JINGLE_SXE = 'urn:xmpp:tmp:jingle:transports:sxe'
NS_SXE = 'urn:xmpp:sxe:0' NS_SXE = 'urn:xmpp:sxe:0'
class WhiteboardPlugin(GajimPlugin): class WhiteboardPlugin(GajimPlugin):
@log_calls('WhiteboardPlugin') @log_calls('WhiteboardPlugin')
def init(self): def init(self):
self.config_dialog = None self.config_dialog = None
self.events_handlers = { self.events_handlers = {
'jingle-request-received': (ged.GUI1, self._nec_jingle_received), 'jingle-request-received': (ged.GUI1, self._nec_jingle_received),
'jingle-connected-received': (ged.GUI1, self._nec_jingle_connected), 'jingle-connected-received': (ged.GUI1,
self._nec_jingle_connected),
'jingle-disconnected-received': (ged.GUI1, 'jingle-disconnected-received': (ged.GUI1,
self._nec_jingle_disconnected), self._nec_jingle_disconnected),
'raw-message-received': (ged.GUI1, self._nec_raw_message), 'raw-message-received': (ged.GUI1, self._nec_raw_message),
} }
self.gui_extension_points = { self.gui_extension_points = {
'chat_control' : (self.connect_with_chat_control, 'chat_control': (self.connect_with_chat_control,
self.disconnect_from_chat_control), self.disconnect_from_chat_control),
'chat_control_base_update_toolbar': (self.update_button_state, 'chat_control_base_update_toolbar': (self.update_button_state,
None), None),
@@ -119,16 +125,17 @@ class WhiteboardPlugin(GajimPlugin):
def update_button_state(self, control): def update_button_state(self, control):
for base in self.controls: for base in self.controls:
if base.chat_control == control: if base.chat_control == control:
if control.contact.supports(NS_JINGLE_SXE) and \ if (control.contact.supports(NS_JINGLE_SXE) and
control.contact.supports(NS_SXE): control.contact.supports(NS_SXE)):
base.enable_action(True) base.enable_action(True)
else: else:
base.enable_action(False) base.enable_action(False)
@log_calls('WhiteboardPlugin') @log_calls('WhiteboardPlugin')
def show_request_dialog(self, account, fjid, jid, sid, content_types): def show_request_dialog(self, account, fjid, jid, sid, content_types):
def on_ok(): def _on_accept():
session = app.connections[account].get_module('Jingle').get_jingle_session(fjid, sid) session = app.connections[account].get_module(
'Jingle').get_jingle_session(fjid, sid)
self.sid = session.sid self.sid = session.sid
if not session.accepted: if not session.accepted:
session.approve_session() session.approve_session()
@@ -139,14 +146,15 @@ class WhiteboardPlugin(GajimPlugin):
if ctrl: if ctrl:
break break
if not ctrl: if not ctrl:
# create it # Create it
app.interface.new_chat_from_jid(account, jid) app.interface.new_chat_from_jid(account, jid)
ctrl = app.interface.msg_win_mgr.get_control(jid, account) ctrl = app.interface.msg_win_mgr.get_control(jid, account)
session = session.contents[('initiator', 'xhtml')] session = session.contents[('initiator', 'xhtml')]
ctrl.draw_whiteboard(session) ctrl.draw_whiteboard(session)
def on_cancel(): def _on_decline():
session = app.connections[account].get_module('Jingle').get_jingle_session(fjid, sid) session = app.connections[account].get_module(
'Jingle').get_jingle_session(fjid, sid)
session.decline_session() session.decline_session()
contact = app.contacts.get_first_contact_from_jid(account, jid) contact = app.contacts.get_first_contact_from_jid(account, jid)
@@ -154,12 +162,19 @@ class WhiteboardPlugin(GajimPlugin):
name = contact.get_shown_name() name = contact.get_shown_name()
else: else:
name = jid name = jid
pritext = _('Incoming Whiteboard')
sectext = _('%(name)s (%(jid)s) wants to start a whiteboard with ' NewConfirmationDialog(
'you. Do you want to accept?') % {'name': name, 'jid': jid} _('Incoming Whiteboard'),
dialog = NonModalConfirmationDialog(pritext, sectext=sectext, _('Incoming Whiteboard Request'),
on_response_ok=on_ok, on_response_cancel=on_cancel) _('%(name)s (%(jid)s) wants to start a whiteboard with '
dialog.popup() 'you.') % {'name': name, 'jid': jid},
[DialogButton.make('Cancel',
text=_('_Decline'),
callback=_on_decline),
DialogButton.make('OK',
text=_('_Accept'),
callback=_on_accept)],
transient_for=app.app.get_active_window()).show()
@log_calls('WhiteboardPlugin') @log_calls('WhiteboardPlugin')
def _nec_jingle_received(self, obj): def _nec_jingle_received(self, obj):
@@ -168,7 +183,11 @@ class WhiteboardPlugin(GajimPlugin):
content_types = obj.contents.media content_types = obj.contents.media
if content_types != 'xhtml': if content_types != 'xhtml':
return return
self.show_request_dialog(obj.conn.name, obj.fjid, obj.jid, obj.sid, self.show_request_dialog(
obj.conn.name,
obj.fjid,
obj.jid,
obj.sid,
content_types) content_types)
@log_calls('WhiteboardPlugin') @log_calls('WhiteboardPlugin')
@@ -176,12 +195,12 @@ class WhiteboardPlugin(GajimPlugin):
if not HAS_GOOCANVAS: if not HAS_GOOCANVAS:
return return
account = obj.conn.name account = obj.conn.name
ctrl = (app.interface.msg_win_mgr.get_control(obj.fjid, account) ctrl = (app.interface.msg_win_mgr.get_control(obj.fjid, account) or
or app.interface.msg_win_mgr.get_control(obj.jid, account)) app.interface.msg_win_mgr.get_control(obj.jid, account))
if not ctrl: if not ctrl:
return return
session = app.connections[obj.conn.name].get_module('Jingle').get_jingle_session(obj.fjid, session = app.connections[obj.conn.name].get_module(
obj.sid) 'Jingle').get_jingle_session(obj.fjid, obj.sid)
if ('initiator', 'xhtml') not in session.contents: if ('initiator', 'xhtml') not in session.contents:
return return
@@ -193,7 +212,7 @@ class WhiteboardPlugin(GajimPlugin):
def _nec_jingle_disconnected(self, obj): def _nec_jingle_disconnected(self, obj):
for base in self.controls: for base in self.controls:
if base.sid == obj.sid: if base.sid == obj.sid:
base.stop_whiteboard(reason = obj.reason) base.stop_whiteboard(reason=obj.reason)
@log_calls('WhiteboardPlugin') @log_calls('WhiteboardPlugin')
def _nec_raw_message(self, obj): def _nec_raw_message(self, obj):
@@ -205,13 +224,13 @@ class WhiteboardPlugin(GajimPlugin):
try: try:
fjid = helpers.get_full_jid_from_iq(obj.stanza) fjid = helpers.get_full_jid_from_iq(obj.stanza)
except helpers.InvalidFormat: except helpers.InvalidFormat:
obj.conn.dispatch('ERROR', (_('Invalid Jabber ID'), obj.conn.dispatch('ERROR', (_('Invalid XMPP Address'),
_('A message from a non-valid JID arrived, it has been ' _('A message from a non-valid XMPP address '
'ignored.'))) 'arrived. It has been ignored.')))
jid = app.get_jid_without_resource(fjid) jid = app.get_jid_without_resource(fjid)
ctrl = (app.interface.msg_win_mgr.get_control(fjid, account) ctrl = (app.interface.msg_win_mgr.get_control(fjid, account) or
or app.interface.msg_win_mgr.get_control(jid, account)) app.interface.msg_win_mgr.get_control(jid, account))
if not ctrl: if not ctrl:
return return
sxe = obj.stanza.getTag('sxe') sxe = obj.stanza.getTag('sxe')
@@ -220,11 +239,13 @@ class WhiteboardPlugin(GajimPlugin):
sid = sxe.getAttr('session') sid = sxe.getAttr('session')
if (jid, sid) not in obj.conn.get_module('Jingle')._sessions: if (jid, sid) not in obj.conn.get_module('Jingle')._sessions:
pass pass
# newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) # newjingle = JingleSession(con=self, weinitiate=False, jid=jid,
# sid=sid)
# self.addJingle(newjingle) # self.addJingle(newjingle)
# we already have such session in dispatcher... # We already have such session in dispatcher
session = obj.conn.get_module('Jingle').get_jingle_session(fjid, sid) session = obj.conn.get_module('Jingle').get_jingle_session(fjid,
sid)
cn = session.contents[('initiator', 'xhtml')] cn = session.contents[('initiator', 'xhtml')]
error = obj.stanza.getTag('error') error = obj.stanza.getTag('error')
if error: if error:
@@ -234,23 +255,23 @@ class WhiteboardPlugin(GajimPlugin):
cn.on_stanza(obj.stanza, sxe, error, action) cn.on_stanza(obj.stanza, sxe, error, action)
# def __editCB(self, stanza, content, error, action): # def __editCB(self, stanza, content, error, action):
#new_tags = sxe.getTags('new') # new_tags = sxe.getTags('new')
#remove_tags = sxe.getTags('remove') # remove_tags = sxe.getTags('remove')
#if new_tags is not None: # if new_tags is not None:
## Process new elements # # Process new elements
#for tag in new_tags: # for tag in new_tags:
#if tag.getAttr('type') == 'element': # if tag.getAttr('type') == 'element':
#ctrl.whiteboard.recieve_element(tag) # ctrl.whiteboard.recieve_element(tag)
#elif tag.getAttr('type') == 'attr': # elif tag.getAttr('type') == 'attr':
#ctrl.whiteboard.recieve_attr(tag) # ctrl.whiteboard.recieve_attr(tag)
#ctrl.whiteboard.apply_new() # ctrl.whiteboard.apply_new()
#if remove_tags is not None: # if remove_tags is not None:
## Delete rids # # Delete rids
#for tag in remove_tags: # for tag in remove_tags:
#target = tag.getAttr('target') # target = tag.getAttr('target')
#ctrl.whiteboard.image.del_rid(target) # ctrl.whiteboard.image.del_rid(target)
# Stop propagating this event, it's handled # Stop propagating this event, it's handled
return True return True
@@ -288,7 +309,7 @@ class Base(object):
if len(hbox.get_children()) == 1: if len(hbox.get_children()) == 1:
self.whiteboard = Whiteboard(self.account, self.contact, content, self.whiteboard = Whiteboard(self.account, self.contact, content,
self.plugin) self.plugin)
# set minimum size # Set minimum size
self.whiteboard.hbox.set_size_request(300, 0) self.whiteboard.hbox.set_size_request(300, 0)
hbox.pack_start(self.whiteboard.hbox, False, False, 0) hbox.pack_start(self.whiteboard.hbox, False, False, 0)
self.whiteboard.hbox.show_all() self.whiteboard.hbox.show_all()
@@ -321,7 +342,8 @@ class Base(object):
def stop_whiteboard(self, reason=None): def stop_whiteboard(self, reason=None):
conn = app.connections[self.chat_control.account] conn = app.connections[self.chat_control.account]
self.sid = None self.sid = None
session = conn.get_module('Jingle').get_jingle_session(self.jid, media='xhtml') session = conn.get_module('Jingle').get_jingle_session(self.jid,
media='xhtml')
if session: if session:
session.end_session() session.end_session()
self.enable_action(False) self.enable_action(False)
@@ -344,6 +366,7 @@ class Base(object):
menu.remove(i) menu.remove(i)
break break
class JingleWhiteboard(JingleContent): class JingleWhiteboard(JingleContent):
''' Jingle Whiteboard sessions consist of xhtml content''' ''' Jingle Whiteboard sessions consist of xhtml content'''
def __init__(self, session, transport=None, senders=None): def __init__(self, session, transport=None, senders=None):
@@ -351,7 +374,7 @@ class JingleWhiteboard(JingleContent):
transport = JingleTransportSXE() transport = JingleTransportSXE()
JingleContent.__init__(self, session, transport, senders) JingleContent.__init__(self, session, transport, senders)
self.media = 'xhtml' self.media = 'xhtml'
self.negotiated = True # there is nothing to negotiate self.negotiated = True # There is nothing to negotiate
self.last_rid = 0 self.last_rid = 0
self.callbacks['session-accept'] += [self._sessionAcceptCB] self.callbacks['session-accept'] += [self._sessionAcceptCB]
self.callbacks['session-terminate'] += [self._stop] self.callbacks['session-terminate'] += [self._stop]
@@ -383,11 +406,11 @@ class JingleWhiteboard(JingleContent):
@log_calls('WhiteboardPlugin') @log_calls('WhiteboardPlugin')
def _sessionAcceptCB(self, stanza, content, error, action): def _sessionAcceptCB(self, stanza, content, error, action):
log.debug('session accepted') log.debug('session accepted')
self.session.connection.dispatch('WHITEBOARD_ACCEPTED', self.session.connection.dispatch(
(self.session.peerjid, self.session.sid)) 'WHITEBOARD_ACCEPTED', (self.session.peerjid, self.session.sid))
def generate_rids(self, x): def generate_rids(self, x):
# generates x number of rids and returns in list # Generates x number of rids and returns in list
rids = [] rids = []
for x in range(x): for x in range(x):
rids.append(str(self.last_rid)) rids.append(str(self.last_rid))
@@ -396,7 +419,7 @@ class JingleWhiteboard(JingleContent):
@log_calls('WhiteboardPlugin') @log_calls('WhiteboardPlugin')
def send_whiteboard_node(self, items, rids): def send_whiteboard_node(self, items, rids):
# takes int rid and dict items and sends it as a node # Takes int rid and dict items and sends it as a node
# sends new item # sends new item
jid = self.session.peerjid jid = self.session.peerjid
sid = self.session.sid sid = self.session.sid
@@ -429,21 +452,21 @@ class JingleWhiteboard(JingleContent):
namespace=NS_SXE) namespace=NS_SXE)
for x in rids: for x in rids:
sxe.addChild(name='remove', attrs = {'target': x}) sxe.addChild(name='remove', attrs={'target': x})
self.session.connection.connection.send(message) self.session.connection.connection.send(message)
def send_items(self, items, rids): def send_items(self, items, rids):
# receives dict items and a list of rids of items to send # Receives dict items and a list of rids of items to send
# TODO: is there a less clumsy way that doesn't involve passing # TODO: Is there a less clumsy way that doesn't involve passing
# whole list # whole list?
self.send_whiteboard_node(items, rids) self.send_whiteboard_node(items, rids)
def del_item(self, rids): def del_item(self, rids):
self.delete_whiteboard_node(rids) self.delete_whiteboard_node(rids)
def encode(self, xml): def encode(self, xml):
# encodes it sendable string # Encodes it sendable string
return 'data:text/xml,' + urllib.quote(xml) return 'data:text/xml,' + quote(xml)
def _fill_content(self, content): def _fill_content(self, content):
content.addChild(NS_JINGLE_XHTML + ' description') content.addChild(NS_JINGLE_XHTML + ' description')
@@ -454,11 +477,14 @@ class JingleWhiteboard(JingleContent):
def __del__(self): def __del__(self):
pass pass
def get_content(desc): def get_content(desc):
return JingleWhiteboard return JingleWhiteboard
common.jingle_content.contents[NS_JINGLE_XHTML] = get_content common.jingle_content.contents[NS_JINGLE_XHTML] = get_content
class JingleTransportSXE(JingleTransport): class JingleTransportSXE(JingleTransport):
def __init__(self, node=None): def __init__(self, node=None):
JingleTransport.__init__(self, TransportType.SOCKS5) JingleTransport.__init__(self, TransportType.SOCKS5)
@@ -469,4 +495,5 @@ class JingleTransportSXE(JingleTransport):
transport.setTagData('host', 'TODO') transport.setTagData('host', 'TODO')
return transport return transport
common.jingle_transport.transports[NS_JINGLE_SXE] = JingleTransportSXE common.jingle_transport.transports[NS_JINGLE_SXE] = JingleTransportSXE

View File

@@ -1,39 +1,38 @@
## 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>
## 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 the Whiteboard Plugin.
## #
## 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
## Gajim is free software; you can redistribute it and/or modify # by the Free Software Foundation; version 3 only.
## 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
## Gajim is distributed in the hope that it will be useful, # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## but WITHOUT ANY WARRANTY; without even the implied warranty of # GNU General Public License for more details.
## 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/>.
## You should have received a copy of the GNU General Public License #
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##
from nbxmpp import Node from nbxmpp import Node
from gi.repository import Gtk from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.gtk.filechoosers import NativeFileChooserDialog, Filter from gajim.gtk.filechoosers import NativeFileChooserDialog
from gajim.gtk.filechoosers import Filter
from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
try: try:
import gi import gi
gi.require_version('GooCanvas', '2.0') gi.require_version('GooCanvas', '2.0')
from gi.repository import GooCanvas from gi.repository import GooCanvas
HAS_GOOCANVAS = True HAS_GOOCANVAS = True
except: except ValueError:
HAS_GOOCANVAS = False HAS_GOOCANVAS = False
@@ -42,94 +41,108 @@ class SvgSaveDialog(NativeFileChooserDialog):
_title = _('Save File as…') _title = _('Save File as…')
_filters = [Filter(_('All files'), '*', False), _filters = [Filter(_('All files'), '*', False),
Filter(_('SVG files'), '*.svg', True)] Filter(_('SVG files'), '*.svg', True)]
_action = Gtk.FileChooserAction.SAVE
''' '''
A whiteboard widget made for Gajim. A whiteboard widget made for Gajim.
- Ummu - Ummu
''' '''
class Whiteboard(object): class Whiteboard(object):
def __init__(self, account, contact, session, plugin): def __init__(self, account, contact, session, plugin):
self.plugin = plugin self.plugin = plugin
file_path = plugin.local_file_path('whiteboard_widget.ui') path = plugin.local_file_path('whiteboard_widget.ui')
xml = Gtk.Builder() self._ui = get_builder(path)
xml.set_translation_domain('gajim_plugins')
xml.add_from_file(file_path) self.canvas = GooCanvas.Canvas()
self.hbox = xml.get_object('whiteboard_hbox') self.hbox = self._ui.whiteboard_hbox
self.canevas = GooCanvas.Canvas() self._ui.whiteboard_hbox.pack_start(self.canvas, True, True, 0)
self.hbox.pack_start(self.canevas, True, True, 0) self._ui.whiteboard_hbox.reorder_child(self.canvas, 0)
self.hbox.reorder_child(self.canevas, 0) self.root = self.canvas.get_root_item()
self.fg_color_select_button = xml.get_object('fg_color_button') self.tool_buttons = [
self.root = self.canevas.get_root_item() self._ui.brush_button,
self.tool_buttons = [] self._ui.oval_button,
for tool in ('brush', 'oval', 'line', 'delete'): self._ui.line_button,
self.tool_buttons.append(xml.get_object(tool + '_button')) self._ui.delete_button
xml.get_object('brush_button').set_active(True) ]
self._ui.brush_button.set_active(True)
# Events # Events
self.canevas.connect('button-press-event', self.button_press_event) self.canvas.connect('button-press-event', self.button_press_event)
self.canevas.connect('button-release-event', self.button_release_event) self.canvas.connect('button-release-event', self.button_release_event)
self.canevas.connect('motion-notify-event', self.motion_notify_event) self.canvas.connect('motion-notify-event', self.motion_notify_event)
self.canevas.connect('item-created', self.item_created) self.canvas.connect('item-created', self.item_created)
# Config # Config
self.line_width = 2 self.line_width = 2
xml.get_object('size_scale').set_value(2) self._ui.size_scale.set_value(2)
c = self.fg_color_select_button.get_rgba() c = self._ui.fg_color_button.get_rgba()
self.color = int(c.red*255*256*256*256 + c.green*255*256*256 + \ self.color = int(c.red*255*256*256*256 +
c.green*255*256*256 +
c.blue*255*256 + 255) c.blue*255*256 + 255)
# SVG Storage # SVG Storage
self.image = SVGObject(self.root, session) self.image = SVGObject(self.root, session)
xml.connect_signals(self) self._ui.connect_signals(self)
# Temporary Variables for items # Temporary Variables for items
self.item_temp = None self.item_temp = None
self.item_temp_coords = (0, 0) self.item_temp_coords = (0, 0)
self.item_data = None self.item_data = None
# Will be {ID: {type:'element', data:[node, goocanvas]}, ID2: {}} instance # Will be instance of {ID: {type:'element'
# data:[node, goocanvas]},
# ID2: {}}
self.receiving = {} self.receiving = {}
def on_tool_button_toggled(self, widget): def _on_tool_button_toggled(self, widget):
for btn in self.tool_buttons: for btn in self.tool_buttons:
if btn == widget: if btn == widget:
continue continue
btn.set_active(False) btn.set_active(False)
def on_brush_button_toggled(self, widget): def _on_brush_button_toggled(self, widget):
if widget.get_active(): if widget.get_active():
self.image.draw_tool = 'brush' self.image.draw_tool = 'brush'
self.on_tool_button_toggled(widget) self._on_tool_button_toggled(widget)
def on_oval_button_toggled(self, widget): def _on_oval_button_toggled(self, widget):
if widget.get_active(): if widget.get_active():
self.image.draw_tool = 'oval' self.image.draw_tool = 'oval'
self.on_tool_button_toggled(widget) self._on_tool_button_toggled(widget)
def on_line_button_toggled(self, widget): def _on_line_button_toggled(self, widget):
if widget.get_active(): if widget.get_active():
self.image.draw_tool = 'line' self.image.draw_tool = 'line'
self.on_tool_button_toggled(widget) self._on_tool_button_toggled(widget)
def on_delete_button_toggled(self, widget): def _on_delete_button_toggled(self, widget):
if widget.get_active(): if widget.get_active():
self.image.draw_tool = 'delete' self.image.draw_tool = 'delete'
self.on_tool_button_toggled(widget) self._on_tool_button_toggled(widget)
def on_clear_button_clicked(self, widget): def _on_clear_button_clicked(self, widget):
self.image.clear_canvas() self.image.clear_canvas()
def on_export_button_clicked(self, widget): def _on_fg_color_button_color_set(self, widget):
SvgSaveDialog(self.image.export_svg, c = self._ui.fg_color_button.get_rgba()
transient_for=app.app.get_active_window()) self.color = int(
c.red*255*256*256*256 +
def on_fg_color_button_color_set(self, widget): c.green*255*256*256 +
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) c.blue*255*256 + 255)
def _on_size_scale_format_value(self, widget):
self.line_width = int(widget.get_value())
def _on_export_button_clicked(self, widget):
SvgSaveDialog(self.image.export_svg,
file_name=_('whiteboard.svg'),
path=app.config.get('last_save_dir'),
transient_for=app.app.get_active_window())
def item_created(self, canvas, item, model): def item_created(self, canvas, item, model):
item.connect('button-press-event', self.item_button_press_events) item.connect('button-press-event', self.item_button_press_events)
@@ -137,17 +150,14 @@ class Whiteboard(object):
if self.image.draw_tool == 'delete': if self.image.draw_tool == 'delete':
self.image.del_item(item) 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): def button_press_event(self, widget, event):
x = event.x x = event.x
y = event.y y = event.y
state = event.state
self.item_temp_coords = (x, y) self.item_temp_coords = (x, y)
if self.image.draw_tool == 'brush': if self.image.draw_tool == 'brush':
self.item_temp = GooCanvas.CanvasEllipse(parent=self.root, self.item_temp = GooCanvas.CanvasEllipse(
parent=self.root,
center_x=x, center_x=x,
center_y=y, center_y=y,
radius_x=1, radius_x=1,
@@ -166,20 +176,24 @@ class Whiteboard(object):
def motion_notify_event(self, widget, event): def motion_notify_event(self, widget, event):
x = event.x x = event.x
y = event.y y = event.y
state = event.state
if self.item_temp is not None: if self.item_temp is not None:
self.item_temp.remove() self.item_temp.remove()
if self.item_data is not None: if self.item_data is not None:
if self.image.draw_tool == 'brush': if self.image.draw_tool == 'brush':
self.item_data = self.item_data + '%s,%s ' % (x, y) self.item_data = self.item_data + '%s,%s ' % (x, y)
self.item_temp = GooCanvas.CanvasPath(parent=self.root, self.item_temp = GooCanvas.CanvasPath(
data=self.item_data, line_width=self.line_width, parent=self.root,
data=self.item_data,
line_width=self.line_width,
stroke_color_rgba=self.color) stroke_color_rgba=self.color)
elif self.image.draw_tool == 'oval': elif self.image.draw_tool == 'oval':
self.item_temp = GooCanvas.CanvasEllipse(parent=self.root, self.item_temp = GooCanvas.CanvasEllipse(
center_x=self.item_temp_coords[0] + (x - self.item_temp_coords[0]) / 2, parent=self.root,
center_y=self.item_temp_coords[1] + (y - self.item_temp_coords[1]) / 2, 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_x=abs(x - self.item_temp_coords[0]) / 2,
radius_y=abs(y - self.item_temp_coords[1]) / 2, radius_y=abs(y - self.item_temp_coords[1]) / 2,
stroke_color_rgba=self.color, stroke_color_rgba=self.color,
@@ -187,19 +201,21 @@ class Whiteboard(object):
elif self.image.draw_tool == 'line': elif self.image.draw_tool == 'line':
self.item_data = 'M %s,%s L' % self.item_temp_coords self.item_data = 'M %s,%s L' % self.item_temp_coords
self.item_data = self.item_data + ' %s,%s' % (x, y) self.item_data = self.item_data + ' %s,%s' % (x, y)
self.item_temp = GooCanvas.CanvasPath(parent=self.root, self.item_temp = GooCanvas.CanvasPath(
data=self.item_data, line_width=self.line_width, parent=self.root,
data=self.item_data,
line_width=self.line_width,
stroke_color_rgba=self.color) stroke_color_rgba=self.color)
def button_release_event(self, widget, event): def button_release_event(self, widget, event):
x = event.x x = event.x
y = event.y y = event.y
state = event.state
if self.image.draw_tool == 'brush': if self.image.draw_tool == 'brush':
self.item_data = self.item_data + '%s,%s' % (x, y) self.item_data = self.item_data + '%s,%s' % (x, y)
if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]: if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]:
GooCanvas.CanvasEllipse(parent=self.root, GooCanvas.CanvasEllipse(
parent=self.root,
center_x=x, center_x=x,
center_y=y, center_y=y,
radius_x=1, radius_x=1,
@@ -220,7 +236,8 @@ class Whiteboard(object):
self.item_data = 'M %s,%s L' % self.item_temp_coords self.item_data = 'M %s,%s L' % self.item_temp_coords
self.item_data = self.item_data + ' %s,%s' % (x, y) self.item_data = self.item_data + ' %s,%s' % (x, y)
if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]: if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]:
GooCanvas.CanvasEllipse(parent=self.root, GooCanvas.CanvasEllipse(
parent=self.root,
center_x=x, center_x=x,
center_y=y, center_y=y,
radius_x=1, radius_x=1,
@@ -241,18 +258,20 @@ class Whiteboard(object):
def recieve_element(self, element): def recieve_element(self, element):
node = self.image.g.addChild(name=element.getAttr('name')) node = self.image.g.addChild(name=element.getAttr('name'))
self.image.g.addChild(node=node) self.image.g.addChild(node=node)
self.receiving[element.getAttr('rid')] = {'type':'element', self.receiving[element.getAttr('rid')] = {'type': 'element',
'data':[node], 'data': [node],
'children':[]} 'children': []}
def recieve_attr(self, element): def recieve_attr(self, element):
node = self.receiving[element.getAttr('parent')]['data'][0] node = self.receiving[element.getAttr('parent')]['data'][0]
node.setAttr(element.getAttr('name'), element.getAttr('chdata')) node.setAttr(element.getAttr('name'), element.getAttr('chdata'))
self.receiving[element.getAttr('rid')] = {'type':'attr', self.receiving[element.getAttr('rid')] = {'type': 'attr',
'data':element.getAttr('name'), 'data': element.getAttr(
'parent':node} 'name'),
self.receiving[element.getAttr('parent')]['children'].append(element.getAttr('rid')) 'parent': node}
self.receiving[element.getAttr('parent')]['children'].append(
element.getAttr('rid'))
def apply_new(self): def apply_new(self):
for x in self.receiving.keys(): for x in self.receiving.keys():
@@ -264,34 +283,40 @@ class Whiteboard(object):
class SVGObject(): class SVGObject():
''' A class to store the svg document and make changes to it.''' ''' A class to store the svg document and make changes to it.'''
def __init__(self, root, session, height=300, width=300): def __init__(self, root, session, height=300, width=300):
# Will be {ID: {type:'element', data:[node, goocanvas]}, ID2: {}} instance # Will be instance of {ID: {type:'element',
# data:[node, goocanvas]},
# ID2: {}}
self.items = {} self.items = {}
self.root = root self.root = root
self.draw_tool = 'brush' self.draw_tool = 'brush'
# sxe session # SXE session
self.session = session self.session = session
# initialize svg document # Initialize svg document
self.svg = Node(node='<svg/>') self.svg = Node(node='<svg/>')
self.svg.setAttr('version', '1.1') self.svg.setAttr('version', '1.1')
self.svg.setAttr('height', str(height)) self.svg.setAttr('height', str(height))
self.svg.setAttr('width', str(width)) self.svg.setAttr('width', str(width))
self.svg.setAttr('xmlns', 'http://www.w3.org/2000/svg') self.svg.setAttr('xmlns', 'http://www.w3.org/2000/svg')
# TODO: make this settable # TODO: Make this settable
self.g = self.svg.addChild(name='g') self.g = self.svg.addChild(name='g')
self.g.setAttr('fill', 'none') self.g.setAttr('fill', 'none')
self.g.setAttr('stroke-linecap', 'round') self.g.setAttr('stroke-linecap', 'round')
def add_path(self, data, line_width, color): def add_path(self, data, line_width, color):
''' adds the path to the items listing, both minidom node and goocanvas '''
object in a tuple ''' 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 = GooCanvas.CanvasPath(
goocanvas_obj.connect('button-press-event', self.item_button_press_events) 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 = self.g.addChild(name='path')
node.setAttr('d', data) node.setAttr('d', data)
@@ -300,16 +325,26 @@ class SVGObject():
self.g.addChild(node=node) self.g.addChild(node=node)
rids = self.session.generate_rids(4) rids = self.session.generate_rids(4)
self.items[rids[0]] = {'type':'element', 'data':[node, goocanvas_obj], 'children':rids[1:]} self.items[rids[0]] = {'type': 'element',
self.items[rids[1]] = {'type':'attr', 'data':'d', 'parent':node} 'data': [node, goocanvas_obj],
self.items[rids[2]] = {'type':'attr', 'data':'stroke-width', 'parent':node} 'children': rids[1:]}
self.items[rids[3]] = {'type':'attr', 'data':'stroke', 'parent':node} 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) self.session.send_items(self.items, rids)
def add_recieved(self, parent_rid, new_items): def add_recieved(self, parent_rid, new_items):
''' adds the path to the items listing, both minidom node and goocanvas '''
object in a tuple ''' Adds the path to the items listing, both minidom node and goocanvas
object in a tuple
'''
node = new_items[parent_rid]['data'][0] node = new_items[parent_rid]['data'][0]
self.items[parent_rid] = new_items[parent_rid] self.items[parent_rid] = new_items[parent_rid]
@@ -317,13 +352,15 @@ class SVGObject():
self.items[x] = new_items[x] self.items[x] = new_items[x]
if node.getName() == 'path': if node.getName() == 'path':
goocanvas_obj = GooCanvas.CanvasPath(parent=self.root, goocanvas_obj = GooCanvas.CanvasPath(
parent=self.root,
data=node.getAttr('d'), data=node.getAttr('d'),
line_width=int(node.getAttr('stroke-width')), line_width=int(node.getAttr('stroke-width')),
stroke_color_rgba=int(node.getAttr('stroke'))) stroke_color_rgba=int(node.getAttr('stroke')))
if node.getName() == 'ellipse': if node.getName() == 'ellipse':
goocanvas_obj = GooCanvas.CanvasEllipse(parent=self.root, goocanvas_obj = GooCanvas.CanvasEllipse(
parent=self.root,
center_x=float(node.getAttr('cx')), center_x=float(node.getAttr('cx')),
center_y=float(node.getAttr('cy')), center_y=float(node.getAttr('cy')),
radius_x=float(node.getAttr('rx')), radius_x=float(node.getAttr('rx')),
@@ -332,20 +369,24 @@ class SVGObject():
line_width=float(node.getAttr('stroke-width'))) line_width=float(node.getAttr('stroke-width')))
self.items[parent_rid]['data'].append(goocanvas_obj) self.items[parent_rid]['data'].append(goocanvas_obj)
goocanvas_obj.connect('button-press-event', self.item_button_press_events) goocanvas_obj.connect('button-press-event',
self.item_button_press_events)
def add_ellipse(self, cx, cy, rx, ry, line_width, stroke_color): 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 ''' Adds the ellipse to the items listing, both minidom node and goocanvas
object in a tuple
goocanvas_obj = GooCanvas.CanvasEllipse(parent=self.root, '''
goocanvas_obj = GooCanvas.CanvasEllipse(
parent=self.root,
center_x=cx, center_x=cx,
center_y=cy, center_y=cy,
radius_x=rx, radius_x=rx,
radius_y=ry, radius_y=ry,
stroke_color_rgba=stroke_color, stroke_color_rgba=stroke_color,
line_width=line_width) line_width=line_width)
goocanvas_obj.connect('button-press-event', self.item_button_press_events) goocanvas_obj.connect('button-press-event',
self.item_button_press_events)
node = self.g.addChild(name='ellipse') node = self.g.addChild(name='ellipse')
node.setAttr('cx', str(cx)) node.setAttr('cx', str(cx))
@@ -357,13 +398,27 @@ class SVGObject():
self.g.addChild(node=node) self.g.addChild(node=node)
rids = self.session.generate_rids(7) rids = self.session.generate_rids(7)
self.items[rids[0]] = {'type':'element', 'data':[node, goocanvas_obj], 'children':rids[1:]} self.items[rids[0]] = {'type': 'element',
self.items[rids[1]] = {'type':'attr', 'data':'cx', 'parent':node} 'data': [node, goocanvas_obj],
self.items[rids[2]] = {'type':'attr', 'data':'cy', 'parent':node} 'children': rids[1:]}
self.items[rids[3]] = {'type':'attr', 'data':'rx', 'parent':node} self.items[rids[1]] = {'type': 'attr',
self.items[rids[4]] = {'type':'attr', 'data':'ry', 'parent':node} 'data': 'cx',
self.items[rids[5]] = {'type':'attr', 'data':'stroke-width', 'parent':node} 'parent': node}
self.items[rids[6]] = {'type':'attr', 'data':'stroke', '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) self.session.send_items(self.items, rids)

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 --> <!-- Generated with glade 3.22.1 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk+" version="3.20"/>
<object class="GtkAdjustment" id="adjustment1"> <object class="GtkAdjustment" id="adjustment1">
@@ -19,7 +19,7 @@
<placeholder/> <placeholder/>
</child> </child>
<child> <child>
<object class="GtkBox" id="vbuttonbox1"> <object class="GtkBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="border_width">6</property> <property name="border_width">6</property>
@@ -30,10 +30,10 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Brush Tool: Draw freehand lines</property> <property name="tooltip_text" translatable="yes">Draw freehand lines</property>
<signal name="toggled" handler="on_brush_button_toggled" swapped="no"/> <signal name="toggled" handler="_on_brush_button_toggled" swapped="no"/>
<child> <child>
<object class="GtkImage" id="image5"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="pixbuf">brush_tool.png</property> <property name="pixbuf">brush_tool.png</property>
@@ -51,10 +51,10 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Oval Tool: Draw circles and ellipses</property> <property name="tooltip_text" translatable="yes">Draw circles and ellipses</property>
<signal name="toggled" handler="on_oval_button_toggled" swapped="no"/> <signal name="toggled" handler="_on_oval_button_toggled" swapped="no"/>
<child> <child>
<object class="GtkImage" id="image6"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="pixbuf">oval_tool.png</property> <property name="pixbuf">oval_tool.png</property>
@@ -72,10 +72,10 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Line Tool: Draw straight lines</property> <property name="tooltip_text" translatable="yes">Draw straight lines</property>
<signal name="toggled" handler="on_line_button_toggled" swapped="no"/> <signal name="toggled" handler="_on_line_button_toggled" swapped="no"/>
<child> <child>
<object class="GtkImage" id="image7"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="pixbuf">line_tool.png</property> <property name="pixbuf">line_tool.png</property>
@@ -88,69 +88,6 @@
<property name="position">2</property> <property name="position">2</property>
</packing> </packing>
</child> </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> <child>
<object class="GtkScale" id="size_scale"> <object class="GtkScale" id="size_scale">
<property name="height_request">68</property> <property name="height_request">68</property>
@@ -162,12 +99,12 @@
<property name="inverted">True</property> <property name="inverted">True</property>
<property name="digits">0</property> <property name="digits">0</property>
<property name="value_pos">bottom</property> <property name="value_pos">bottom</property>
<signal name="value-changed" handler="on_size_scale_format_value" swapped="no"/> <signal name="value-changed" handler="_on_size_scale_format_value" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">False</property>
<property name="position">6</property> <property name="position">3</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -177,7 +114,71 @@
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Foreground color</property> <property name="tooltip_text" translatable="yes">Foreground color</property>
<property name="rgba">rgb(0,0,0)</property> <property name="rgba">rgb(0,0,0)</property>
<signal name="color-set" handler="on_fg_color_button_color_set" swapped="no"/> <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">4</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">Remove individual figures</property>
<signal name="toggled" handler="_on_delete_button_toggled" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-delete</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="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">Cleanup canvas</property>
<signal name="clicked" handler="_on_clear_button_clicked" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">6</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 to SVG file</property>
<property name="margin_top">6</property>
<signal name="clicked" handler="_on_export_button_clicked" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-as</property>
</object>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>