Web · Wiki · Activities · Blog · Lists · Chat · Meeting · Bugs · Git · Translate · Archive · People · Donate
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