1 |
# Copyright (C) 2006 by Martin Sevior |
2 |
# Copyright (C) 2006-2007 Marc Maurer <uwog@uwog.net> |
3 |
# Copyright (C) 2007, One Laptop Per Child |
4 |
# |
5 |
# This program is free software; you can redistribute it and/or modify |
6 |
# it under the terms of the GNU General Public License as published by |
7 |
# the Free Software Foundation; either version 2 of the License, or |
8 |
# (at your option) any later version. |
9 |
# |
10 |
# This program is distributed in the hope that it will be useful, |
11 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 |
# GNU General Public License for more details. |
14 |
# |
15 |
# You should have received a copy of the GNU General Public License |
16 |
# along with this program; if not, write to the Free Software |
17 |
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
18 |
|
19 |
from gettext import gettext as _ |
20 |
import logging |
21 |
import os |
22 |
|
23 |
# Abiword needs this to happen as soon as possible |
24 |
from gi.repository import GObject |
25 |
GObject.threads_init() |
26 |
|
27 |
from gi.repository import Gtk |
28 |
from gi.repository import GConf |
29 |
import telepathy |
30 |
import telepathy.client |
31 |
|
32 |
from sugar3.activity import activity |
33 |
from sugar3.activity.widgets import StopButton |
34 |
from sugar3.activity.widgets import ActivityToolbarButton |
35 |
from sugar3.activity.activity import get_bundle_path |
36 |
|
37 |
from sugar3.graphics.toolbutton import ToolButton |
38 |
from sugar3.graphics.toolbarbox import ToolbarButton, ToolbarBox |
39 |
from sugar3.graphics import style |
40 |
from sugar3.graphics.icon import Icon |
41 |
from sugar3.graphics.xocolor import XoColor |
42 |
|
43 |
from toolbar import EditToolbar |
44 |
from toolbar import ViewToolbar |
45 |
from toolbar import TextToolbar |
46 |
from toolbar import InsertToolbar |
47 |
from toolbar import ParagraphToolbar |
48 |
from widgets import ExportButtonFactory |
49 |
from widgets import DocumentView |
50 |
from sugar3.graphics.objectchooser import ObjectChooser |
51 |
try: |
52 |
from sugar3.graphics.objectchooser import FILTER_TYPE_GENERIC_MIME |
53 |
except: |
54 |
FILTER_TYPE_GENERIC_MIME = 'generic_mime' |
55 |
|
56 |
logger = logging.getLogger('write-activity') |
57 |
|
58 |
|
59 |
class ConnectingBox(Gtk.VBox): |
60 |
|
61 |
def __init__(self): |
62 |
Gtk.VBox.__init__(self) |
63 |
self.props.halign = Gtk.Align.CENTER |
64 |
self.props.valign = Gtk.Align.CENTER |
65 |
waiting_icon = Icon(icon_name='zoom-neighborhood', |
66 |
pixel_size=style.STANDARD_ICON_SIZE) |
67 |
waiting_icon.set_xo_color(XoColor('white')) |
68 |
self.add(waiting_icon) |
69 |
self.add(Gtk.Label(_('Connecting...'))) |
70 |
self.show_all() |
71 |
self.hide() |
72 |
|
73 |
|
74 |
class AbiWordActivity(activity.Activity): |
75 |
|
76 |
def __init__(self, handle): |
77 |
activity.Activity.__init__(self, handle) |
78 |
|
79 |
# abiword uses the current directory for all its file dialogs |
80 |
os.chdir(os.path.expanduser('~')) |
81 |
|
82 |
# create our main abiword canvas |
83 |
self.abiword_canvas = DocumentView() |
84 |
self._new_instance = True |
85 |
toolbar_box = ToolbarBox() |
86 |
|
87 |
self.activity_button = ActivityToolbarButton(self) |
88 |
toolbar_box.toolbar.insert(self.activity_button, -1) |
89 |
|
90 |
separator = Gtk.SeparatorToolItem() |
91 |
separator.show() |
92 |
self.activity_button.props.page.insert(separator, 2) |
93 |
ExportButtonFactory(self, self.abiword_canvas) |
94 |
self.activity_button.show() |
95 |
|
96 |
edit_toolbar = ToolbarButton() |
97 |
edit_toolbar.props.page = EditToolbar(self, toolbar_box) |
98 |
edit_toolbar.props.icon_name = 'toolbar-edit' |
99 |
edit_toolbar.props.label = _('Edit') |
100 |
toolbar_box.toolbar.insert(edit_toolbar, -1) |
101 |
|
102 |
view_toolbar = ToolbarButton() |
103 |
view_toolbar.props.page = ViewToolbar(self.abiword_canvas) |
104 |
view_toolbar.props.icon_name = 'toolbar-view' |
105 |
view_toolbar.props.label = _('View') |
106 |
toolbar_box.toolbar.insert(view_toolbar, -1) |
107 |
|
108 |
self.speech_toolbar_button = ToolbarButton(icon_name='speak') |
109 |
toolbar_box.toolbar.insert(self.speech_toolbar_button, -1) |
110 |
GObject.idle_add(self._init_speech) |
111 |
|
112 |
separator = Gtk.SeparatorToolItem() |
113 |
toolbar_box.toolbar.insert(separator, -1) |
114 |
|
115 |
text_toolbar = ToolbarButton() |
116 |
text_toolbar.props.page = TextToolbar(self.abiword_canvas) |
117 |
text_toolbar.props.icon_name = 'format-text' |
118 |
text_toolbar.props.label = _('Text') |
119 |
toolbar_box.toolbar.insert(text_toolbar, -1) |
120 |
|
121 |
para_toolbar = ToolbarButton() |
122 |
para_toolbar.props.page = ParagraphToolbar(self.abiword_canvas) |
123 |
para_toolbar.props.icon_name = 'paragraph-bar' |
124 |
para_toolbar.props.label = _('Paragraph') |
125 |
toolbar_box.toolbar.insert(para_toolbar, -1) |
126 |
|
127 |
insert_toolbar = ToolbarButton() |
128 |
insert_toolbar.props.page = InsertToolbar(self.abiword_canvas) |
129 |
insert_toolbar.props.icon_name = 'insert-table' |
130 |
insert_toolbar.props.label = _('Table') |
131 |
toolbar_box.toolbar.insert(insert_toolbar, -1) |
132 |
|
133 |
image = ToolButton('insert-picture') |
134 |
image.set_tooltip(_('Insert Image')) |
135 |
self._image_id = image.connect('clicked', self._image_cb) |
136 |
toolbar_box.toolbar.insert(image, -1) |
137 |
|
138 |
palette = image.get_palette() |
139 |
content_box = Gtk.VBox() |
140 |
palette.set_content(content_box) |
141 |
image_floating_checkbutton = Gtk.CheckButton(_('Floating')) |
142 |
image_floating_checkbutton.connect( |
143 |
'toggled', self._image_floating_checkbutton_toggled_cb) |
144 |
content_box.pack_start(image_floating_checkbutton, True, True, 0) |
145 |
content_box.show_all() |
146 |
self.floating_image = False |
147 |
|
148 |
separator = Gtk.SeparatorToolItem() |
149 |
separator.props.draw = False |
150 |
separator.set_size_request(0, -1) |
151 |
separator.set_expand(True) |
152 |
separator.show() |
153 |
toolbar_box.toolbar.insert(separator, -1) |
154 |
|
155 |
stop = StopButton(self) |
156 |
toolbar_box.toolbar.insert(stop, -1) |
157 |
|
158 |
toolbar_box.show_all() |
159 |
self.set_toolbar_box(toolbar_box) |
160 |
|
161 |
# add a overlay to be able to show a icon while joining a shared doc |
162 |
overlay = Gtk.Overlay() |
163 |
overlay.add(self.abiword_canvas) |
164 |
overlay.show() |
165 |
|
166 |
self._connecting_box = ConnectingBox() |
167 |
overlay.add_overlay(self._connecting_box) |
168 |
|
169 |
self.set_canvas(overlay) |
170 |
|
171 |
# we want a nice border so we can select paragraphs easily |
172 |
self.abiword_canvas.set_show_margin(True) |
173 |
|
174 |
# Read default font face and size |
175 |
client = GConf.Client.get_default() |
176 |
self._default_font_face = client.get_string( |
177 |
'/desktop/sugar/activities/write/font_face') |
178 |
if not self._default_font_face: |
179 |
self._default_font_face = 'Sans' |
180 |
self._default_font_size = client.get_int( |
181 |
'/desktop/sugar/activities/write/font_size') |
182 |
if self._default_font_size == 0: |
183 |
self._default_font_size = 12 |
184 |
|
185 |
# activity sharing |
186 |
self.participants = {} |
187 |
self.joined = False |
188 |
|
189 |
self.connect('shared', self._shared_cb) |
190 |
|
191 |
if self.shared_activity: |
192 |
# we are joining the activity |
193 |
logger.error('We are joining an activity') |
194 |
# display a icon while joining |
195 |
self._connecting_box.show() |
196 |
# disable the abi widget |
197 |
self.abiword_canvas.set_sensitive(False) |
198 |
self._new_instance = False |
199 |
self.connect('joined', self._joined_cb) |
200 |
self.shared_activity.connect('buddy-joined', |
201 |
self._buddy_joined_cb) |
202 |
self.shared_activity.connect('buddy-left', self._buddy_left_cb) |
203 |
if self.get_shared(): |
204 |
self._joined_cb(self) |
205 |
else: |
206 |
# we are creating the activity |
207 |
logger.error("We are creating an activity") |
208 |
|
209 |
self.abiword_canvas.zoom_width() |
210 |
self.abiword_canvas.show() |
211 |
self.connect_after('map-event', self.__map_activity_event_cb) |
212 |
|
213 |
self.abiword_canvas.connect('size-allocate', self.size_allocate_cb) |
214 |
|
215 |
def _init_speech(self): |
216 |
import speech |
217 |
from speechtoolbar import SpeechToolbar |
218 |
if speech.supported: |
219 |
self.speech_toolbar = SpeechToolbar(self) |
220 |
self.speech_toolbar_button.set_page(self.speech_toolbar) |
221 |
self.speech_toolbar_button.show() |
222 |
|
223 |
def size_allocate_cb(self, abi, alloc): |
224 |
GObject.idle_add(abi.queue_draw) |
225 |
|
226 |
def __map_activity_event_cb(self, event, activity): |
227 |
# set custom keybindings for Write |
228 |
# we do it later because have problems if done before - OLPC #11049 |
229 |
logger.error('Loading keybindings') |
230 |
keybindings_file = os.path.join(get_bundle_path(), 'keybindings.xml') |
231 |
self.abiword_canvas.invoke_ex( |
232 |
'com.abisource.abiword.loadbindings.fromURI', |
233 |
keybindings_file, 0, 0) |
234 |
# set default font |
235 |
if self._new_instance: |
236 |
self.abiword_canvas.select_all() |
237 |
logging.error('Setting default font to %s %d in new documents', |
238 |
self._default_font_face, self._default_font_size) |
239 |
self.abiword_canvas.set_font_name(self._default_font_face) |
240 |
self.abiword_canvas.set_font_size(str(self._default_font_size)) |
241 |
self.abiword_canvas.moveto_bod() |
242 |
self.abiword_canvas.select_bod() |
243 |
if hasattr(self.abiword_canvas, 'toggle_rulers'): |
244 |
# this is not available yet on upstream abiword |
245 |
self.abiword_canvas.view_print_layout() |
246 |
self.abiword_canvas.toggle_rulers(False) |
247 |
|
248 |
self.abiword_canvas.grab_focus() |
249 |
|
250 |
def get_preview(self): |
251 |
if not hasattr(self.abiword_canvas, 'render_page_to_image'): |
252 |
return activity.Activity.get_preview(self) |
253 |
|
254 |
from gi.repository import GdkPixbuf |
255 |
|
256 |
pixbuf = self.abiword_canvas.render_page_to_image(1) |
257 |
pixbuf = pixbuf.scale_simple(style.zoom(300), style.zoom(225), |
258 |
GdkPixbuf.InterpType.BILINEAR) |
259 |
|
260 |
preview_data = [] |
261 |
|
262 |
def save_func(buf, lenght, data): |
263 |
data.append(buf) |
264 |
return True |
265 |
|
266 |
pixbuf.save_to_callbackv(save_func, preview_data, 'png', [], []) |
267 |
preview_data = ''.join(preview_data) |
268 |
|
269 |
return preview_data |
270 |
|
271 |
def _shared_cb(self, activity): |
272 |
logger.error('My Write activity was shared') |
273 |
self._sharing_setup() |
274 |
|
275 |
self.shared_activity.connect('buddy-joined', self._buddy_joined_cb) |
276 |
self.shared_activity.connect('buddy-left', self._buddy_left_cb) |
277 |
|
278 |
channel = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES] |
279 |
logger.error('This is my activity: offering a tube...') |
280 |
id = channel.OfferDBusTube('com.abisource.abiword.abicollab', {}) |
281 |
logger.error('Tube address: %s', channel.GetDBusTubeAddress(id)) |
282 |
|
283 |
def _sharing_setup(self): |
284 |
logger.debug("_sharing_setup()") |
285 |
|
286 |
if self.shared_activity is None: |
287 |
logger.error('Failed to share or join activity') |
288 |
return |
289 |
|
290 |
self.conn = self.shared_activity.telepathy_conn |
291 |
self.tubes_chan = self.shared_activity.telepathy_tubes_chan |
292 |
self.text_chan = self.shared_activity.telepathy_text_chan |
293 |
self.tube_id = None |
294 |
self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal( |
295 |
'NewTube', self._new_tube_cb) |
296 |
|
297 |
def _list_tubes_reply_cb(self, tubes): |
298 |
for tube_info in tubes: |
299 |
self._new_tube_cb(*tube_info) |
300 |
|
301 |
def _list_tubes_error_cb(self, e): |
302 |
logger.error('ListTubes() failed: %s', e) |
303 |
|
304 |
def _joined_cb(self, activity): |
305 |
logger.error("_joined_cb()") |
306 |
if not self.shared_activity: |
307 |
self._enable_collaboration() |
308 |
return |
309 |
|
310 |
self.joined = True |
311 |
logger.error('Joined an existing Write session') |
312 |
self._sharing_setup() |
313 |
|
314 |
logger.error('This is not my activity: waiting for a tube...') |
315 |
self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( |
316 |
reply_handler=self._list_tubes_reply_cb, |
317 |
error_handler=self._list_tubes_error_cb) |
318 |
self._enable_collaboration() |
319 |
|
320 |
def _enable_collaboration(self): |
321 |
""" |
322 |
when communication established, hide the download icon |
323 |
and enable the abi widget |
324 |
""" |
325 |
self.abiword_canvas.zoom_width() |
326 |
self.abiword_canvas.set_sensitive(True) |
327 |
self._connecting_box.hide() |
328 |
|
329 |
def _new_tube_cb(self, id, initiator, type, service, params, state): |
330 |
logger.error('New tube: ID=%d initiator=%d type=%d service=%s ' |
331 |
'params=%r state=%d', id, initiator, type, service, |
332 |
params, state) |
333 |
|
334 |
if self.tube_id is not None: |
335 |
# We are already using a tube |
336 |
return |
337 |
|
338 |
if type != telepathy.TUBE_TYPE_DBUS or \ |
339 |
service != "com.abisource.abiword.abicollab": |
340 |
return |
341 |
|
342 |
channel = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES] |
343 |
|
344 |
if state == telepathy.TUBE_STATE_LOCAL_PENDING: |
345 |
channel.AcceptDBusTube(id) |
346 |
|
347 |
# look for the initiator's D-Bus unique name |
348 |
initiator_dbus_name = None |
349 |
dbus_names = channel.GetDBusNames(id) |
350 |
for handle, name in dbus_names: |
351 |
if handle == initiator: |
352 |
logger.error('found initiator D-Bus name: %s', name) |
353 |
initiator_dbus_name = name |
354 |
break |
355 |
|
356 |
if initiator_dbus_name is None: |
357 |
logger.error('Unable to get the D-Bus name of the tube initiator') |
358 |
return |
359 |
|
360 |
cmd_prefix = 'com.abisource.abiword.abicollab.olpc.' |
361 |
# pass this tube to abicollab |
362 |
address = channel.GetDBusTubeAddress(id) |
363 |
if self.joined: |
364 |
logger.error('Passing tube address to abicollab (join): %s', |
365 |
address) |
366 |
self.abiword_canvas.invoke_ex(cmd_prefix + 'joinTube', |
367 |
address, 0, 0) |
368 |
# The intiator of the session has to be the first passed |
369 |
# to the Abicollab backend. |
370 |
logger.error('Adding the initiator to the session: %s', |
371 |
initiator_dbus_name) |
372 |
self.abiword_canvas.invoke_ex(cmd_prefix + 'buddyJoined', |
373 |
initiator_dbus_name, 0, 0) |
374 |
else: |
375 |
logger.error('Passing tube address to abicollab (offer): %s', |
376 |
address) |
377 |
self.abiword_canvas.invoke_ex(cmd_prefix + 'offerTube', address, |
378 |
0, 0) |
379 |
self.tube_id = id |
380 |
|
381 |
channel.connect_to_signal('DBusNamesChanged', |
382 |
self._on_dbus_names_changed) |
383 |
|
384 |
self._on_dbus_names_changed(id, dbus_names, []) |
385 |
|
386 |
def _on_dbus_names_changed(self, tube_id, added, removed): |
387 |
""" |
388 |
We call com.abisource.abiword.abicollab.olpc.buddy{Joined,Left} |
389 |
according members of the D-Bus tube. That's why we don't add/remove |
390 |
buddies in _buddy_{joined,left}_cb. |
391 |
""" |
392 |
logger.error('_on_dbus_names_changed') |
393 |
# if tube_id == self.tube_id: |
394 |
cmd_prefix = 'com.abisource.abiword.abicollab.olpc' |
395 |
for handle, bus_name in added: |
396 |
logger.error('added handle: %s, with dbus_name: %s', |
397 |
handle, bus_name) |
398 |
self.abiword_canvas.invoke_ex(cmd_prefix + '.buddyJoined', |
399 |
bus_name, 0, 0) |
400 |
self.participants[handle] = bus_name |
401 |
|
402 |
def _on_members_changed(self, message, added, removed, local_pending, |
403 |
remote_pending, actor, reason): |
404 |
logger.error("_on_members_changed") |
405 |
for handle in removed: |
406 |
bus_name = self.participants.pop(handle, None) |
407 |
if bus_name is None: |
408 |
# FIXME: that shouldn't happen so probably hide another bug. |
409 |
# Should be investigated |
410 |
continue |
411 |
|
412 |
cmd_prefix = 'com.abisource.abiword.abicollab.olpc' |
413 |
logger.error('removed handle: %d, with dbus name: %s', handle, |
414 |
bus_name) |
415 |
self.abiword_canvas.invoke_ex(cmd_prefix + '.buddyLeft', |
416 |
bus_name, 0, 0) |
417 |
|
418 |
def _buddy_joined_cb(self, activity, buddy): |
419 |
logger.error('buddy joined with object path: %s', buddy.object_path()) |
420 |
|
421 |
def _buddy_left_cb(self, activity, buddy): |
422 |
logger.error('buddy left with object path: %s', buddy.object_path()) |
423 |
|
424 |
def read_file(self, file_path): |
425 |
logging.debug('AbiWordActivity.read_file: %s, mimetype: %s', |
426 |
file_path, self.metadata['mime_type']) |
427 |
if self._is_plain_text(self.metadata['mime_type']): |
428 |
self.abiword_canvas.load_file('file://' + file_path, 'text/plain') |
429 |
else: |
430 |
# we pass no mime/file type, let libabiword autodetect it, |
431 |
# so we can handle multiple file formats |
432 |
self.abiword_canvas.load_file('file://' + file_path, '') |
433 |
self.abiword_canvas.zoom_width() |
434 |
self._new_instance = False |
435 |
|
436 |
def write_file(self, file_path): |
437 |
logging.debug('AbiWordActivity.write_file: %s, mimetype: %s', |
438 |
file_path, self.metadata['mime_type']) |
439 |
# if we were editing a text file save as plain text |
440 |
if self._is_plain_text(self.metadata['mime_type']): |
441 |
logger.debug('Writing file as type source (text/plain)') |
442 |
self.abiword_canvas.save('file://' + file_path, 'text/plain', '') |
443 |
else: |
444 |
#if the file is new, save in .odt format |
445 |
if self.metadata['mime_type'] == '': |
446 |
self.metadata['mime_type'] = \ |
447 |
'application/vnd.oasis.opendocument.text' |
448 |
|
449 |
# Abiword can't save in .doc format, save in .rtf instead |
450 |
if self.metadata['mime_type'] == 'application/msword': |
451 |
self.metadata['mime_type'] = 'application/rtf' |
452 |
|
453 |
self.abiword_canvas.save('file://' + file_path, |
454 |
self.metadata['mime_type'], '') |
455 |
|
456 |
# due to http://bugzilla.abisource.com/show_bug.cgi?id=13585 |
457 |
if self.abiword_canvas.get_version() != '3.0': |
458 |
self.metadata['fulltext'] = self.abiword_canvas.get_content( |
459 |
'text/plain', None)[:3000] |
460 |
|
461 |
def _is_plain_text(self, mime_type): |
462 |
# These types have 'text/plain' in their mime_parents but we need |
463 |
# use it like rich text |
464 |
if mime_type in ['application/rtf', 'text/rtf', 'text/html']: |
465 |
return False |
466 |
|
467 |
from sugar3 import mime |
468 |
|
469 |
mime_parents = mime.get_mime_parents(self.metadata['mime_type']) |
470 |
return self.metadata['mime_type'] in ['text/plain', 'text/csv'] or \ |
471 |
'text/plain' in mime_parents |
472 |
|
473 |
def _image_floating_checkbutton_toggled_cb(self, checkbutton): |
474 |
self.floating_image = checkbutton.get_active() |
475 |
|
476 |
def _image_cb(self, button): |
477 |
try: |
478 |
chooser = ObjectChooser(self, what_filter='Image', |
479 |
filter_type=FILTER_TYPE_GENERIC_MIME, |
480 |
show_preview=True) |
481 |
except: |
482 |
# for compatibility with older versions |
483 |
chooser = ObjectChooser(self, what_filter='Image') |
484 |
|
485 |
try: |
486 |
result = chooser.run() |
487 |
if result == Gtk.ResponseType.ACCEPT: |
488 |
logging.debug('ObjectChooser: %r', |
489 |
chooser.get_selected_object()) |
490 |
jobject = chooser.get_selected_object() |
491 |
if jobject and jobject.file_path: |
492 |
self.abiword_canvas.insert_image(jobject.file_path, |
493 |
self.floating_image) |
494 |
finally: |
495 |
chooser.destroy() |
496 |
del chooser |