plugin to add whiteboard feature. Fixes #2970

This commit is contained in:
Yann Leboulanger 2010-11-01 14:30:23 +01:00
parent 5a9464f0cf
commit a40bacaced
9 changed files with 1201 additions and 0 deletions

View File

@ -0,0 +1 @@
from plugin import WhiteboardPlugin

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,7 @@
[info]
name: Whiteboard
short_name: whiteboard
version: 0.1
description: Shows a whiteboard in chat.
authors = Yann Leboulanger <asterix@lagaule.org>
homepage = www.gajim.org

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

View File

@ -0,0 +1,453 @@
## 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 common import helpers
from common import gajim
from plugins import GajimPlugin
from plugins.helpers import log_calls, log
import common.xmpp
import gtk
import chat_control
from common import ged
from common.jingle_session import JingleSession
from common.jingle_content import JingleContent
from common.jingle_transport import JingleTransport, TransportType
import dialogs
from whiteboard_widget import Whiteboard
from common import xmpp
from common import caps_cache
NS_JINGLE_XHTML = 'urn:xmpp:tmp:jingle:apps:xhtml'
NS_JINGLE_SXE = 'urn:xmpp:tmp:jingle:transports:sxe'
NS_SXE = 'urn:xmpp:sxe:0'
class WhiteboardPlugin(GajimPlugin):
@log_calls('WhiteboardPlugin')
def init(self):
self.config_dialog = None
self.events_handlers = {
'jingle-request-received': (ged.GUI1, self._nec_jingle_received),
'jingle-connected-received': (ged.GUI1, self._nec_jingle_connected),
'jingle-disconnected-received': (ged.GUI1,
self._nec_jingle_disconnected),
'raw-message-received': (ged.GUI1, self._nec_raw_message),
}
self.gui_extension_points = {
'chat_control_base' : (self.connect_with_chat_control,
self.disconnect_from_chat_control),
'chat_control_base_update_toolbar': (self.update_button_state,
None),
}
self.controls = []
self.sid = None
@log_calls('WhiteboardPlugin')
def _compute_caps_hash(self):
for a in gajim.connections:
gajim.caps_hash[a] = caps_cache.compute_caps_hash([
gajim.gajim_identity], gajim.gajim_common_features + gajim.gajim_optional_features[a])
# re-send presence with new hash
connected = gajim.connections[a].connected
if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible':
gajim.connections[a].change_status(gajim.SHOW_LIST[connected],
gajim.connections[a].status)
@log_calls('WhiteboardPlugin')
def activate(self):
if NS_JINGLE_SXE not in gajim.gajim_common_features:
gajim.gajim_common_features.append(NS_JINGLE_SXE)
if NS_SXE not in gajim.gajim_common_features:
gajim.gajim_common_features.append(NS_SXE)
self._compute_caps_hash()
@log_calls('WhiteboardPlugin')
def deactivate(self):
if NS_JINGLE_SXE in gajim.gajim_common_features:
gajim.gajim_common_features.remove(NS_JINGLE_SXE)
if NS_SXE in gajim.gajim_common_features:
gajim.gajim_common_features.remove(NS_SXE)
self._compute_caps_hash()
@log_calls('WhiteboardPlugin')
def connect_with_chat_control(self, control):
if isinstance(control, chat_control.ChatControl):
base = Base(self, control)
self.controls.append(base)
@log_calls('WhiteboardPlugin')
def disconnect_from_chat_control(self, chat_control):
for base in self.controls:
base.disconnect_from_chat_control()
self.controls = []
@log_calls('WhiteboardPlugin')
def update_button_state(self, control):
for base in self.controls:
if base.chat_control == control:
if control.contact.supports(NS_JINGLE_SXE) and \
control.contact.supports(NS_SXE):
base.button.set_sensitive(True)
else:
base.button.set_sensitive(False)
@log_calls('WhiteboardPlugin')
def show_request_dialog(self, account, fjid, jid, sid, content_types):
def on_ok():
session = gajim.connections[account].get_jingle_session(fjid, sid)
self.sid = session.sid
if not session.accepted:
session.approve_session()
for content in content_types:
session.approve_content(content)
for _jid in (fjid, jid):
ctrl = gajim.interface.msg_win_mgr.get_control(_jid, account)
if ctrl:
break
if not ctrl:
# create it
gajim.interface.new_chat_from_jid(account, jid)
ctrl = gajim.interface.msg_win_mgr.get_control(jid, account)
session = session.contents[('initiator', 'xhtml')]
ctrl.draw_whiteboard(session)
def on_cancel():
session = gajim.connections[account].get_jingle_session(fjid, sid)
session.decline_session()
contact = gajim.contacts.get_first_contact_from_jid(account, jid)
if contact:
name = contact.get_shown_name()
else:
name = jid
pritext = _('Incoming Whiteboard')
sectext = _('%(name)s (%(jid)s) wants to start a whiteboard with '
'you. Do you want to accept?') % {'name': name, 'jid': jid}
dialog = dialogs.NonModalConfirmationDialog(pritext, sectext=sectext,
on_response_ok=on_ok, on_response_cancel=on_cancel)
dialog.popup()
@log_calls('WhiteboardPlugin')
def _nec_jingle_received(self, obj):
content_types = set(c[0] for c in obj.contents)
if 'xhtml' not in content_types:
return
self.show_request_dialog(obj.conn.name, obj.fjid, obj.jid, obj.sid,
content_types)
@log_calls('WhiteboardPlugin')
def _nec_jingle_connected(self, obj):
account = obj.conn.name
ctrl = (gajim.interface.msg_win_mgr.get_control(obj.fjid, account)
or gajim.interface.msg_win_mgr.get_control(obj.jid, account))
if not ctrl:
return
session = gajim.connections[obj.conn.name].get_jingle_session(obj.fjid,
obj.sid)
if ('initiator', 'xhtml') not in session.contents:
return
session = session.contents[('initiator', 'xhtml')]
ctrl.draw_whiteboard(session)
@log_calls('WhiteboardPlugin')
def _nec_jingle_disconnected(self, obj):
for base in self.controls:
if base.sid == obj.sid:
base.stop_whiteboard()
@log_calls('WhiteboardPlugin')
def _nec_raw_message(self, obj):
if obj.stanza.getTag('sxe', namespace=NS_SXE):
account = obj.conn.name
try:
fjid = helpers.get_full_jid_from_iq(obj.stanza)
except helpers.InvalidFormat:
obj.conn.dispatch('ERROR', (_('Invalid Jabber ID'),
_('A message from a non-valid JID arrived, it has been '
'ignored.')))
jid = gajim.get_jid_without_resource(fjid)
ctrl = (gajim.interface.msg_win_mgr.get_control(fjid, account)
or gajim.interface.msg_win_mgr.get_control(jid, account))
if not ctrl:
return
sxe = obj.stanza.getTag('sxe')
if not sxe:
return
sid = sxe.getAttr('session')
if (jid, sid) not in obj.conn._sessions:
pass
# newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid)
# self.addJingle(newjingle)
# we already have such session in dispatcher...
session = obj.conn.get_jingle_session(fjid, sid)
cn = session.contents[('initiator', 'xhtml')]
error = obj.stanza.getTag('error')
if error:
action = 'iq-error'
else:
action = 'edit'
cn.on_stanza(obj.stanza, sxe, error, action)
# def __editCB(self, stanza, content, error, action):
#new_tags = sxe.getTags('new')
#remove_tags = sxe.getTags('remove')
#if new_tags is not None:
## Process new elements
#for tag in new_tags:
#if tag.getAttr('type') == 'element':
#ctrl.whiteboard.recieve_element(tag)
#elif tag.getAttr('type') == 'attr':
#ctrl.whiteboard.recieve_attr(tag)
#ctrl.whiteboard.apply_new()
#if remove_tags is not None:
## Delete rids
#for tag in remove_tags:
#target = tag.getAttr('target')
#ctrl.whiteboard.image.del_rid(target)
# Stop propagating this event, it's handled
return True
class Base(object):
def __init__(self, plugin, chat_control):
self.plugin = plugin
self.chat_control = chat_control
self.chat_control.draw_whiteboard = self.draw_whiteboard
self.contact = self.chat_control.contact
self.account = self.chat_control.account
self.jid = self.contact.get_full_jid()
self.create_buttons()
self.whiteboard = None
self.sid = None
def create_buttons(self):
# create juick button
actions_hbox = self.chat_control.xml.get_object('actions_hbox')
self.button = gtk.ToggleButton(label=None, use_underline=True)
self.button.set_property('relief', gtk.RELIEF_NONE)
self.button.set_property('can-focus', False)
img = gtk.Image()
img_path = self.plugin.local_file_path('whiteboard.png')
pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
iconset = gtk.IconSet(pixbuf=pixbuf)
factory = gtk.IconFactory()
factory.add('whiteboard', iconset)
factory.add_default()
img.set_from_stock('whiteboard', gtk.ICON_SIZE_BUTTON)
self.button.set_image(img)
send_button = self.chat_control.xml.get_object('send_button')
send_button_pos = actions_hbox.child_get_property(send_button,
'position')
actions_hbox.add_with_properties(self.button, 'position',
send_button_pos - 1, 'expand', False)
id_ = self.button.connect('toggled', self.on_whiteboard_button_toggled)
self.chat_control.handlers[id_] = self.button
self.button.show()
def draw_whiteboard(self, content):
hbox = self.chat_control.xml.get_object('chat_control_hbox')
if len(hbox.get_children()) == 1:
self.whiteboard = Whiteboard(self.account, self.contact, content,
self.plugin)
# set minimum size
self.whiteboard.hbox.set_size_request(300, 0)
hbox.pack_start(self.whiteboard.hbox, expand=False, fill=False)
self.whiteboard.hbox.show_all()
self.button.set_active(True)
content.control = self
self.sid = content.session.sid
def on_whiteboard_button_toggled(self, widget):
"""
Popup whiteboard
"""
if widget.get_active():
if not self.whiteboard:
self.start_whiteboard()
else:
self.stop_whiteboard()
def start_whiteboard(self):
conn = gajim.connections[self.chat_control.account]
jingle = JingleSession(conn, weinitiate=True, jid=self.jid)
self.sid = jingle.sid
conn._sessions[jingle.sid] = jingle
content = JingleWhiteboard(jingle)
content.control = self
jingle.add_content('xhtml', content)
jingle.start_session()
def stop_whiteboard(self):
conn = gajim.connections[self.chat_control.account]
self.sid = None
session = conn.get_jingle_session(self.jid, media='xhtml')
if session:
session.end_session()
if not self.whiteboard:
return
hbox = self.chat_control.xml.get_object('chat_control_hbox')
for child in hbox.get_children():
if child == self.whiteboard.hbox:
self.button.set_active(False)
hbox.remove(child)
self.whiteboard = None
break
def disconnect_from_chat_control(self):
actions_hbox = self.chat_control.xml.get_object('actions_hbox')
actions_hbox.remove(self.button)
class JingleWhiteboard(JingleContent):
''' Jingle Whiteboard sessions consist of xhtml content'''
def __init__(self, session, transport=None):
if not transport:
transport = JingleTransportSXE()
JingleContent.__init__(self, session, transport)
self.media = 'xhtml'
self.negotiated = True # there is nothing to negotiate
self.last_rid = 0
self.callbacks['session-accept'] += [self._sessionAcceptCB]
self.callbacks['session-terminate'] += [self._stop]
self.callbacks['session-terminate-sent'] += [self._stop]
self.callbacks['edit'] = [self._EditCB]
def _EditCB(self, stanza, content, error, action):
new_tags = content.getTags('new')
remove_tags = content.getTags('remove')
if new_tags is not None:
# Process new elements
for tag in new_tags:
if tag.getAttr('type') == 'element':
self.control.whiteboard.recieve_element(tag)
elif tag.getAttr('type') == 'attr':
self.control.whiteboard.recieve_attr(tag)
self.control.whiteboard.apply_new()
if remove_tags is not None:
# Delete rids
for tag in remove_tags:
target = tag.getAttr('target')
self.control.whiteboard.image.del_rid(target)
def _sessionAcceptCB(self, stanza, content, error, action):
log.debug('session accepted')
self.session.connection.dispatch('WHITEBOARD_ACCEPTED',
(self.session.peerjid, self.session.sid))
def generate_rids(self, x):
# generates x number of rids and returns in list
rids = []
for x in range(x):
rids.append(str(self.last_rid))
self.last_rid += 1
return rids
def send_whiteboard_node(self, items, rids):
# takes int rid and dict items and sends it as a node
# sends new item
jid = self.session.peerjid
sid = self.session.sid
message = xmpp.Message(to=jid)
sxe = message.addChild(name='sxe', attrs={'session': sid},
namespace=NS_SXE)
for x in rids:
if items[x]['type'] == 'element':
parent = x
attrs = {'rid': x,
'name': items[x]['data'][0].getName(),
'type': items[x]['type']}
sxe.addChild(name='new', attrs=attrs)
if items[x]['type'] == 'attr':
attr_name = items[x]['data']
chdata = items[parent]['data'][0].getAttr(attr_name)
attrs = {'rid': x,
'name': attr_name,
'type': items[x]['type'],
'chdata': chdata,
'parent': parent}
sxe.addChild(name='new', attrs=attrs)
self.session.connection.connection.send(message)
def delete_whiteboard_node(self, rids):
message = xmpp.Message(to=self.session.peerjid)
sxe = message.addChild(name='sxe', attrs={'session': self.session.sid},
namespace=NS_SXE)
for x in rids:
sxe.addChild(name='remove', attrs = {'target': x})
self.session.connection.connection.send(message)
def send_items(self, items, rids):
# recieves dict items and a list of rids of items to send
# TODO: is there a less clumsy way that doesn't involve passing
# whole list
self.send_whiteboard_node(items, rids)
def del_item(self, rids):
self.delete_whiteboard_node(rids)
def encode(self, xml):
# encodes it sendable string
return 'data:text/xml,' + urllib.quote(xml)
def _fill_content(self, content):
content.addChild(NS_JINGLE_XHTML + ' description')
def _stop(self, *things):
pass
def __del__(self):
pass
def get_content(desc):
return JingleWhiteboard
common.jingle_content.contents[NS_JINGLE_XHTML] = get_content
class JingleTransportSXE(JingleTransport):
def __init__(self):
JingleTransport.__init__(self, TransportType.streaming)
def make_transport(self, candidates=None):
transport = JingleTransport.make_transport(self, candidates)
transport.setNamespace(NS_JINGLE_SXE)
transport.setTagData('host', 'TODO')
return transport
common.jingle_transport.transports[NS_JINGLE_SXE] = JingleTransportSXE

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,416 @@
## plugins/whiteboard/whiteboard_widget.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/>.
##
import gtk
import gtkgui_helpers
import goocanvas
from common.xmpp import Node
from common import gajim
from common import i18n
from dialogs import FileChooserDialog
'''
A whiteboard widget made for Gajim.
- Ummu
'''
class Whiteboard(object):
def __init__(self, account, contact, session, plugin):
self.plugin = plugin
file_path = plugin.local_file_path('whiteboard_widget.ui')
xml = gtk.Builder()
xml.set_translation_domain(i18n.APP)
xml.add_from_file(file_path)
self.hbox = xml.get_object('whiteboard_hbox')
self.canevas = goocanvas.Canvas()
self.hbox.pack_start(self.canevas)
self.hbox.reorder_child(self.canevas, 0)
self.canevas.set_flags(gtk.CAN_FOCUS)
self.fg_color_select_button = xml.get_object('fg_color_button')
self.root = self.canevas.get_root_item()
self.tool_buttons = []
for tool in ('brush', 'oval', 'line', 'delete'):
self.tool_buttons.append(xml.get_object(tool + '_button'))
xml.get_object('brush_button').set_active(True)
# Events
self.canevas.connect('button-press-event', self.button_press_event)
self.canevas.connect('button-release-event', self.button_release_event)
self.canevas.connect('motion-notify-event', self.motion_notify_event)
self.canevas.connect('item-created', self.item_created)
# Config
self.line_width = 2
xml.get_object('size_scale').set_value(2)
self.color = str(self.fg_color_select_button.get_color())
# SVG Storage
self.image = SVGObject(self.root, session)
xml.connect_signals(self)
# Temporary Variables for items
self.item_temp = None
self.item_temp_coords = (0, 0)
self.item_data = None
# Will be {ID: {type:'element', data:[node, goocanvas]}, ID2: {}} instance
self.recieving = {}
def on_tool_button_toggled(self, widget):
for btn in self.tool_buttons:
if btn == widget:
continue
btn.set_active(False)
def on_brush_button_toggled(self, widget):
if widget.get_active():
self.image.draw_tool = 'brush'
self.on_tool_button_toggled(widget)
def on_oval_button_toggled(self, widget):
if widget.get_active():
self.image.draw_tool = 'oval'
self.on_tool_button_toggled(widget)
def on_line_button_toggled(self, widget):
if widget.get_active():
self.image.draw_tool = 'line'
self.on_tool_button_toggled(widget)
def on_delete_button_toggled(self, widget):
if widget.get_active():
self.image.draw_tool = 'delete'
self.on_tool_button_toggled(widget)
def on_clear_button_clicked(self, widget):
self.image.clear_canvas()
def on_export_button_clicked(self, widget):
self.image.export_svg(filename)
def on_fg_color_button_color_set(self, widget):
self.color = str(self.fg_color_select_button.get_color())
def item_created(self, canvas, item, model):
print 'item created'
item.connect('button-press-event', self.item_button_press_events)
def item_button_press_events(self, item, target_item, event):
if self.image.draw_tool == 'delete':
self.image.del_item(item)
def on_size_scale_format_value(self, widget):
self.line_width = int(widget.get_value())
def button_press_event(self, widget, event):
x = event.x
y = event.y
state = event.state
self.item_temp_coords = (x, y)
if self.image.draw_tool == 'brush':
self.item_temp = goocanvas.Ellipse(parent=self.root,
center_x=x,
center_y=y,
radius_x=1,
radius_y=1,
stroke_color=self.color,
fill_color=self.color,
line_width=self.line_width)
self.item_data = 'M %s,%s L ' % (x, y)
elif self.image.draw_tool == 'oval':
self.item_data = True
if self.image.draw_tool == 'line':
self.item_data = 'M %s,%s L' % (x, y)
def motion_notify_event(self, widget, event):
x = event.x
y = event.y
state = event.state
if self.item_temp is not None:
self.item_temp.remove()
if self.item_data is not None:
if self.image.draw_tool == 'brush':
self.item_data = self.item_data + '%s,%s ' % (x, y)
self.item_temp = goocanvas.Path(parent=self.root,
data=self.item_data, line_width=self.line_width,
stroke_color=self.color)
elif self.image.draw_tool == 'oval':
self.item_temp = goocanvas.Ellipse(parent=self.root,
center_x=self.item_temp_coords[0] + (x - self.item_temp_coords[0]) / 2,
center_y=self.item_temp_coords[1] + (y - self.item_temp_coords[1]) / 2,
radius_x=abs(x - self.item_temp_coords[0]) / 2,
radius_y=abs(y - self.item_temp_coords[1]) / 2,
stroke_color=self.color,
line_width=self.line_width)
elif self.image.draw_tool == 'line':
self.item_data = 'M %s,%s L' % self.item_temp_coords
self.item_data = self.item_data + ' %s,%s' % (x, y)
self.item_temp = goocanvas.Path(parent=self.root,
data=self.item_data, line_width=self.line_width,
stroke_color=self.color)
def button_release_event(self, widget, event):
x = event.x
y = event.y
state = event.state
if self.image.draw_tool == 'brush':
self.item_data = self.item_data + '%s,%s' % (x, y)
if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]:
goocanvas.Ellipse(parent=self.root,
center_x=x,
center_y=y,
radius_x=1,
radius_y=1,
stroke_color=self.color,
fill_color=self.color,
line_width=self.line_width)
self.image.add_path(self.item_data, self.line_width, self.color)
if self.image.draw_tool == 'oval':
cx = self.item_temp_coords[0] + (x - self.item_temp_coords[0]) / 2
cy = self.item_temp_coords[1] + (y - self.item_temp_coords[1]) / 2
rx = abs(x - self.item_temp_coords[0]) / 2
ry = abs(y - self.item_temp_coords[1]) / 2
self.image.add_ellipse(cx, cy, rx, ry, self.line_width, self.color)
if self.image.draw_tool == 'line':
self.item_data = 'M %s,%s L' % self.item_temp_coords
self.item_data = self.item_data + ' %s,%s' % (x, y)
if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]:
goocanvas.Ellipse(parent=self.root,
center_x=x,
center_y=y,
radius_x=1,
radius_y=1,
stroke_color='black',
fill_color='black',
line_width=self.line_width)
self.image.add_path(self.item_data, self.line_width, self.color)
if self.image.draw_tool == 'delete':
pass
self.item_data = None
if self.item_temp is not None:
self.item_temp.remove()
self.item_temp = None
def recieve_element(self, element):
node = self.image.g.addChild(name=element.getAttr('name'))
self.image.g.addChild(node=node)
self.recieving[element.getAttr('rid')] = {'type':'element',
'data':[node],
'children':[]}
def recieve_attr(self, element):
node = self.recieving[element.getAttr('parent')]['data'][0]
node.setAttr(element.getAttr('name'), element.getAttr('chdata'))
self.recieving[element.getAttr('rid')] = {'type':'attr',
'data':element.getAttr('name'),
'parent':node}
self.recieving[element.getAttr('parent')]['children'].append(element.getAttr('rid'))
def apply_new(self):
for x in self.recieving.keys():
if self.recieving[x]['type'] == 'element':
self.image.add_recieved(x, self.recieving)
self.recieving = {}
class SvgChooserDialog(FileChooserDialog):
def __init__(self, on_response_ok=None, on_response_cancel=None):
'''
Choose in which SVG file to store the image
'''
def on_ok(widget, callback):
'''
check if file exists and call callback
'''
path_to_clientcert_file = self.get_filename()
path_to_clientcert_file = \
gtkgui_helpers.decode_filechooser_file_paths(
(path_to_clientcert_file,))[0]
if os.path.exists(path_to_clientcert_file):
callback(widget, path_to_clientcert_file)
FileChooserDialog.__init__(self,
title_text=_('Save Image as...'),
action=gtk.FILE_CHOOSER_ACTION_SAVE,
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN,
gtk.RESPONSE_OK),
current_folder='',
default_response=gtk.RESPONSE_OK,
on_response_ok=(on_ok, on_response_ok),
on_response_cancel=on_response_cancel)
filter_ = gtk.FileFilter()
filter_.set_name(_('All files'))
filter_.add_pattern('*')
self.add_filter(filter_)
filter_ = gtk.FileFilter()
filter_.set_name(_('SVG Files'))
filter_.add_pattern('*.svg')
self.add_filter(filter_)
self.set_filter(filter_)
class SVGObject():
''' A class to store the svg document and make changes to it.'''
def __init__(self, root, session, height=300, width=300):
# Will be {ID: {type:'element', data:[node, goocanvas]}, ID2: {}} instance
self.items = {}
self.root = root
self.draw_tool = 'brush'
# sxe session
self.session = session
# initialize svg document
self.svg = Node(node='<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.Path(parent=self.root, data=data,
line_width=line_width, stroke_color=color)
goocanvas_obj.connect('button-press-event', self.item_button_press_events)
node = self.g.addChild(name='path')
node.setAttr('d', data)
node.setAttr('stroke-width', str(line_width))
node.setAttr('stroke', color)
self.g.addChild(node=node)
rids = self.session.generate_rids(4)
self.items[rids[0]] = {'type':'element', 'data':[node, goocanvas_obj], 'children':rids[1:]}
self.items[rids[1]] = {'type':'attr', 'data':'d', 'parent':node}
self.items[rids[2]] = {'type':'attr', 'data':'stroke-width', 'parent':node}
self.items[rids[3]] = {'type':'attr', 'data':'stroke', 'parent':node}
self.session.send_items(self.items, rids)
def add_recieved(self, parent_rid, new_items):
''' adds the path to the items listing, both minidom node and goocanvas
object in a tuple '''
node = new_items[parent_rid]['data'][0]
self.items[parent_rid] = new_items[parent_rid]
for x in new_items[parent_rid]['children']:
self.items[x] = new_items[x]
if node.getName() == 'path':
goocanvas_obj = goocanvas.Path(parent=self.root,
data=node.getAttr('d'),
line_width=int(node.getAttr('stroke-width')),
stroke_color=node.getAttr('stroke'))
if node.getName() == 'ellipse':
goocanvas_obj = goocanvas.Ellipse(parent=self.root,
center_x=float(node.getAttr('cx')),
center_y=float(node.getAttr('cy')),
radius_x=float(node.getAttr('rx')),
radius_y=float(node.getAttr('ry')),
stroke_color=node.getAttr('stroke'),
line_width=float(node.getAttr('stroke-width')))
self.items[parent_rid]['data'].append(goocanvas_obj)
goocanvas_obj.connect('button-press-event', self.item_button_press_events)
def add_ellipse(self, cx, cy, rx, ry, line_width, stroke_color):
''' adds the ellipse to the items listing, both minidom node and goocanvas
object in a tuple '''
goocanvas_obj = goocanvas.Ellipse(parent=self.root,
center_x=cx,
center_y=cy,
radius_x=rx,
radius_y=ry,
stroke_color=stroke_color,
line_width=line_width)
goocanvas_obj.connect('button-press-event', self.item_button_press_events)
node = self.g.addChild(name='ellipse')
node.setAttr('cx', str(cx))
node.setAttr('cy', str(cy))
node.setAttr('rx', str(rx))
node.setAttr('ry', str(ry))
node.setAttr('stroke-width', str(line_width))
node.setAttr('stroke', stroke_color)
self.g.addChild(node=node)
rids = self.session.generate_rids(7)
self.items[rids[0]] = {'type':'element', 'data':[node, goocanvas_obj], 'children':rids[1:]}
self.items[rids[1]] = {'type':'attr', 'data':'cx', 'parent':node}
self.items[rids[2]] = {'type':'attr', 'data':'cy', 'parent':node}
self.items[rids[3]] = {'type':'attr', 'data':'rx', 'parent':node}
self.items[rids[4]] = {'type':'attr', 'data':'ry', 'parent':node}
self.items[rids[5]] = {'type':'attr', 'data':'stroke-width', 'parent':node}
self.items[rids[6]] = {'type':'attr', 'data':'stroke', 'parent':node}
self.session.send_items(self.items, rids)
def del_item(self, item):
rids = []
for x in self.items.keys():
if self.items[x]['type'] == 'element':
if self.items[x]['data'][1] == item:
for y in self.items[x]['children']:
rids.append(y)
self.del_rid(y)
rids.append(x)
self.del_rid(x)
break
self.session.del_item(rids)
def clear_canvas(self):
for x in self.items.keys():
if self.items[x]['type'] == 'element':
self.del_rid(x)
def del_rid(self, rid):
if self.items[rid]['type'] == 'element':
self.items[rid]['data'][1].remove()
del self.items[rid]
def export_svg(self, filename):
file = open(filename, 'w')
file.writelines(str(self.svg))
file.close()
def item_button_press_events(self, item, target_item, event):
self.del_item(item)

View File

@ -0,0 +1,324 @@
<?xml version="1.0"?>
<interface>
<requires lib="gtk+" version="2.16"/>
<!-- interface-naming-policy project-wide -->
<object class="GtkHBox" id="whiteboard_hbox">
<property name="visible">True</property>
<property name="spacing">6</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkVButtonBox" id="vbuttonbox1">
<property name="visible">True</property>
<property name="border_width">6</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<property name="layout_style">start</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>
<signal name="toggled" handler="on_brush_button_toggled"/>
<child>
<object class="GtkHBox" id="hbox4">
<property name="visible">True</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="image5">
<property name="visible">True</property>
<property name="xalign">1</property>
<property name="pixbuf">brush_tool.png</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Brush Tool</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</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>
<signal name="toggled" handler="on_oval_button_toggled"/>
<child>
<object class="GtkHBox" id="hbox5">
<property name="visible">True</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="image6">
<property name="visible">True</property>
<property name="xalign">1</property>
<property name="pixbuf">oval_tool.png</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label5">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Oval Tool</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</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>
<signal name="toggled" handler="on_line_button_toggled"/>
<child>
<object class="GtkHBox" id="hbox6">
<property name="visible">True</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="image7">
<property name="visible">True</property>
<property name="xalign">1</property>
<property name="pixbuf">line_tool.png</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label6">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Line Tool</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</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>
<signal name="toggled" handler="on_delete_button_toggled"/>
<child>
<object class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="image2">
<property name="visible">True</property>
<property name="xalign">1</property>
<property name="stock">gtk-delete</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Delete Tool</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</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>
<signal name="clicked" handler="on_clear_button_clicked"/>
<child>
<object class="GtkHBox" id="hbox2">
<property name="visible">True</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="xalign">1</property>
<property name="stock">gtk-clear</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Clear Canvas</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</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>
<signal name="clicked" handler="on_export_button_clicked"/>
<child>
<object class="GtkHBox" id="hbox3">
<property name="visible">True</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="image4">
<property name="visible">True</property>
<property name="xalign">1</property>
<property name="stock">gtk-save-as</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Export Image</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkHScale" id="size_scale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">adjustment1</property>
<property name="digits">0</property>
<signal name="value_changed" handler="on_size_scale_format_value"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">6</property>
<property name="secondary">True</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="fg_color_hbox">
<property name="visible">True</property>
<child>
<object class="GtkLabel" id="fg_color_label">
<property name="visible">True</property>
<property name="xpad">1</property>
<property name="ypad">1</property>
<property name="label" translatable="yes">Foreground
Color:</property>
</object>
<packing>
<property name="position">0</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="color">#000000000000</property>
<signal name="color_set" handler="on_fg_color_button_color_set"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">7</property>
<property name="secondary">True</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="stock">gtk-delete</property>
</object>
<object class="GtkAdjustment" id="adjustment1">
<property name="value">2</property>
<property name="lower">1</property>
<property name="upper">110</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
<property name="page_size">10</property>
</object>
</interface>