Web · Wiki · Activities · Blog · Lists · Chat · Meeting · Bugs · Git · Translate · Archive · People · Donate
1
# Copyright (C) 2006, Red Hat, Inc.
2
# Copyright (C) 2009 Martin Langhoff, Simon Schampijer, Daniel Drake,
3
#                    Tomeu Vizoso
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
import logging
20
from gettext import gettext as _
21
from gettext import ngettext
22
import os
23
24
from gi.repository import GObject
25
GObject.threads_init()
26
27
from gi.repository import Gtk
28
from gi.repository import Gdk
29
from gi.repository import GdkPixbuf
30
from gi.repository import WebKit
31
from gi.repository import Soup
32
from gi.repository import SoupGNOME
33
34
import base64
35
import time
36
import shutil
37
import sqlite3
38
import json
39
from gi.repository import GConf
40
import locale
41
import cairo
42
import StringIO
43
from hashlib import sha1
44
45
from sugar3.activity import activity
46
from sugar3.graphics import style
47
import telepathy
48
import telepathy.client
49
from sugar3.presence import presenceservice
50
from sugar3.graphics.tray import HTray
51
from sugar3 import profile
52
from sugar3.graphics.alert import Alert
53
from sugar3.graphics.alert import NotifyAlert
54
from sugar3.graphics.icon import Icon
55
from sugar3 import mime
56
57
from sugar3.graphics.toolbarbox import ToolbarButton
58
59
PROFILE_VERSION = 2
60
61
_profile_version = 0
62
_profile_path = os.path.join(activity.get_activity_root(), 'data/gecko')
63
_version_file = os.path.join(_profile_path, 'version')
64
_cookies_db_path = os.path.join(_profile_path, 'cookies.sqlite')
65
66
if os.path.exists(_version_file):
67
    f = open(_version_file)
68
    _profile_version = int(f.read())
69
    f.close()
70
71
if _profile_version < PROFILE_VERSION:
72
    if not os.path.exists(_profile_path):
73
        os.mkdir(_profile_path)
74
75
    shutil.copy('cert8.db', _profile_path)
76
    os.chmod(os.path.join(_profile_path, 'cert8.db'), 0660)
77
78
    f = open(_version_file, 'w')
79
    f.write(str(PROFILE_VERSION))
80
    f.close()
81
82
83
def _seed_xs_cookie(cookie_jar):
84
    """Create a HTTP Cookie to authenticate with the Schoolserver.
85
86
    Do nothing if the laptop is not registered with Schoolserver, or
87
    if the cookie already exists.
88
89
    """
90
    client = GConf.Client.get_default()
91
    backup_url = client.get_string('/desktop/sugar/backup_url')
92
    if backup_url == '':
93
        _logger.debug('seed_xs_cookie: Not registered with Schoolserver')
94
        return
95
96
    jabber_server = client.get_string(
97
        '/desktop/sugar/collaboration/jabber_server')
98
99
    soup_uri = Soup.URI()
100
    soup_uri.set_scheme('xmpp')
101
    soup_uri.set_host(jabber_server)
102
    soup_uri.set_path('/')
103
    xs_cookie = cookie_jar.get_cookies(soup_uri, for_http=False)
104
    if xs_cookie is not None:
105
        _logger.debug('seed_xs_cookie: Cookie exists already')
106
        return
107
108
    pubkey = profile.get_profile().pubkey
109
    cookie_data = {'color': profile.get_color().to_string(),
110
                   'pkey_hash': sha1(pubkey).hexdigest()}
111
112
    expire = int(time.time()) + 10 * 365 * 24 * 60 * 60
113
114
    xs_cookie = Soup.Cookie()
115
    xs_cookie.set_name('xoid')
116
    xs_cookie.set_value(json.dumps(cookie_data))
117
    xs_cookie.set_domain(jabber_server)
118
    xs_cookie.set_path('/')
119
    xs_cookie.set_max_age(expire)
120
    cookie_jar.add_cookie(xs_cookie)
121
    _logger.debug('seed_xs_cookie: Updated cookie successfully')
122
123
124
def _set_char_preference(name, value):
125
    cls = components.classes["@mozilla.org/preferences-service;1"]
126
    prefService = cls.getService(components.interfaces.nsIPrefService)
127
    branch = prefService.getBranch('')
128
    branch.setCharPref(name, value)
129
130
131
from browser import TabbedView
132
from browser import ZOOM_ORIGINAL
133
from webtoolbar import PrimaryToolbar
134
from edittoolbar import EditToolbar
135
from viewtoolbar import ViewToolbar
136
import downloadmanager
137
138
# TODO: make the registration clearer SL #3087
139
140
from model import Model
141
from sugar3.presence.tubeconn import TubeConnection
142
from messenger import Messenger
143
from linkbutton import LinkButton
144
145
SERVICE = "org.laptop.WebActivity"
146
IFACE = SERVICE
147
PATH = "/org/laptop/WebActivity"
148
149
_logger = logging.getLogger('web-activity')
150
151
152
class WebActivity(activity.Activity):
153
    def __init__(self, handle):
154
        activity.Activity.__init__(self, handle)
155
156
        _logger.debug('Starting the web activity')
157
158
        session = WebKit.get_default_session()
159
        session.set_property('accept-language-auto', True)
160
        session.set_property('ssl-use-system-ca-file', True)
161
        session.set_property('ssl-strict', False)
162
163
        # By default, cookies are not stored persistently, we have to
164
        # add a cookie jar so that they get saved to disk.  We use one
165
        # with a SQlite database:
166
        cookie_jar = SoupGNOME.CookieJarSqlite(filename=_cookies_db_path,
167
                                               read_only=False)
168
        session.add_feature(cookie_jar)
169
170
        _seed_xs_cookie(cookie_jar)
171
172
        # FIXME
173
        # downloadmanager.remove_old_parts()
174
175
        self._force_close = False
176
        self._tabbed_view = TabbedView()
177
        self._tabbed_view.connect('focus-url-entry', self._on_focus_url_entry)
178
        self._tabbed_view.connect('switch-page', self.__switch_page_cb)
179
180
        self._tray = HTray()
181
        self.set_tray(self._tray, Gtk.PositionType.BOTTOM)
182
183
        self._primary_toolbar = PrimaryToolbar(self._tabbed_view, self)
184
        self._edit_toolbar = EditToolbar(self)
185
        self._view_toolbar = ViewToolbar(self)
186
187
        self._primary_toolbar.connect('add-link', self._link_add_button_cb)
188
189
        self._primary_toolbar.connect('go-home', self._go_home_button_cb)
190
191
        self._primary_toolbar.connect('go-library', self._go_library_button_cb)
192
193
        self._primary_toolbar.connect('set-home', self._set_home_button_cb)
194
195
        self._primary_toolbar.connect('reset-home', self._reset_home_button_cb)
196
197
        self._edit_toolbar_button = ToolbarButton(
198
                page=self._edit_toolbar,
199
                icon_name='toolbar-edit')
200
201
        self._primary_toolbar.toolbar.insert(
202
                self._edit_toolbar_button, 1)
203
204
        view_toolbar_button = ToolbarButton(
205
                page=self._view_toolbar,
206
                icon_name='toolbar-view')
207
        self._primary_toolbar.toolbar.insert(
208
                view_toolbar_button, 2)
209
210
        self._primary_toolbar.show_all()
211
        self.set_toolbar_box(self._primary_toolbar)
212
213
        self.set_canvas(self._tabbed_view)
214
        self._tabbed_view.show()
215
216
        self.model = Model()
217
        self.model.connect('add_link', self._add_link_model_cb)
218
219
        self.connect('key-press-event', self._key_press_cb)
220
221
        if handle.uri:
222
            self._tabbed_view.current_browser.load_uri(handle.uri)
223
        elif not self._jobject.file_path:
224
            # TODO: we need this hack until we extend the activity API for
225
            # opening URIs and default docs.
226
            self._tabbed_view.load_homepage()
227
228
        self.messenger = None
229
        self.connect('shared', self._shared_cb)
230
231
        # Get the Presence Service
232
        self.pservice = presenceservice.get_instance()
233
        try:
234
            name, path = self.pservice.get_preferred_connection()
235
            self.tp_conn_name = name
236
            self.tp_conn_path = path
237
            self.conn = telepathy.client.Connection(name, path)
238
        except TypeError:
239
            _logger.debug('Offline')
240
        self.initiating = None
241
242
        if self.get_shared_activity() is not None:
243
            _logger.debug('shared: %s', self.get_shared())
244
            # We are joining the activity
245
            _logger.debug('Joined activity')
246
            self.connect('joined', self._joined_cb)
247
            if self.get_shared():
248
                # We've already joined
249
                self._joined_cb()
250
        else:
251
            _logger.debug('Created activity')
252
253
        # README: this is a workaround to remove old temp file
254
        # http://bugs.sugarlabs.org/ticket/3973
255
        self._cleanup_temp_files()
256
257
    def _cleanup_temp_files(self):
258
        """Removes temporary files generated by Download Manager that
259
        were cancelled by the user or failed for any reason.
260
261
        There is a bug in GLib that makes this to happen:
262
            https://bugzilla.gnome.org/show_bug.cgi?id=629301
263
        """
264
265
        try:
266
            uptime_proc = open('/proc/uptime', 'r').read()
267
            uptime = int(float(uptime_proc.split()[0]))
268
        except EnvironmentError:
269
            logging.warning('/proc/uptime could not be read')
270
            uptime = None
271
272
        temp_path = os.path.join(self.get_activity_root(), 'instance')
273
        now = int(time.time())
274
        cutoff = now - 24 * 60 * 60  # yesterday
275
        if uptime is not None:
276
            boot_time = now - uptime
277
            cutoff = max(cutoff, boot_time)
278
279
        for f in os.listdir(temp_path):
280
            if f.startswith('.goutputstream-'):
281
                fpath = os.path.join(temp_path, f)
282
                mtime = int(os.path.getmtime(fpath))
283
                if mtime < cutoff:
284
                    logging.warning('Removing old temporary file: %s', fpath)
285
                    try:
286
                        os.remove(fpath)
287
                    except EnvironmentError:
288
                        logging.error('Temporary file could not be '
289
                                      'removed: %s', fpath)
290
291
    def _on_focus_url_entry(self, gobject):
292
        self._primary_toolbar.entry.grab_focus()
293
294
    def _shared_cb(self, activity_):
295
        _logger.debug('My activity was shared')
296
        self.initiating = True
297
        self._setup()
298
299
        _logger.debug('This is my activity: making a tube...')
300
        self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(SERVICE,
301
                                                                    {})
302
303
    def _setup(self):
304
        if self.get_shared_activity() is None:
305
            _logger.debug('Failed to share or join activity')
306
            return
307
308
        bus_name, conn_path, channel_paths = \
309
                self.get_shared_activity().get_channels()
310
311
        # Work out what our room is called and whether we have Tubes already
312
        room = None
313
        tubes_chan = None
314
        text_chan = None
315
        for channel_path in channel_paths:
316
            channel = telepathy.client.Channel(bus_name, channel_path)
317
            htype, handle = channel.GetHandle()
318
            if htype == telepathy.HANDLE_TYPE_ROOM:
319
                _logger.debug('Found our room: it has handle#%d "%s"',
320
                              handle,
321
                              self.conn.InspectHandles(htype, [handle])[0])
322
                room = handle
323
                ctype = channel.GetChannelType()
324
                if ctype == telepathy.CHANNEL_TYPE_TUBES:
325
                    _logger.debug('Found our Tubes channel at %s',
326
                                  channel_path)
327
                    tubes_chan = channel
328
                elif ctype == telepathy.CHANNEL_TYPE_TEXT:
329
                    _logger.debug('Found our Text channel at %s',
330
                                  channel_path)
331
                    text_chan = channel
332
333
        if room is None:
334
            _logger.debug("Presence service didn't create a room")
335
            return
336
        if text_chan is None:
337
            _logger.debug("Presence service didn't create a text channel")
338
            return
339
340
        # Make sure we have a Tubes channel - PS doesn't yet provide one
341
        if tubes_chan is None:
342
            _logger.debug("Didn't find our Tubes channel, requesting one...")
343
            tubes_chan = self.conn.request_channel(
344
                telepathy.CHANNEL_TYPE_TUBES, telepathy.HANDLE_TYPE_ROOM,
345
                room, True)
346
347
        self.tubes_chan = tubes_chan
348
        self.text_chan = text_chan
349
350
        tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal( \
351
                'NewTube', self._new_tube_cb)
352
353
    def _list_tubes_reply_cb(self, tubes):
354
        for tube_info in tubes:
355
            self._new_tube_cb(*tube_info)
356
357
    def _list_tubes_error_cb(self, e):
358
        _logger.debug('ListTubes() failed: %s', e)
359
360
    def _joined_cb(self, activity_):
361
        if not self.get_shared_activity():
362
            return
363
364
        _logger.debug('Joined an existing shared activity')
365
366
        self.initiating = False
367
        self._setup()
368
369
        _logger.debug('This is not my activity: waiting for a tube...')
370
        self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
371
            reply_handler=self._list_tubes_reply_cb,
372
            error_handler=self._list_tubes_error_cb)
373
374
    def _new_tube_cb(self, identifier, initiator, type, service, params,
375
                     state):
376
        _logger.debug('New tube: ID=%d initator=%d type=%d service=%s '
377
                      'params=%r state=%d', identifier, initiator, type,
378
                      service, params, state)
379
380
        if (type == telepathy.TUBE_TYPE_DBUS and
381
            service == SERVICE):
382
            if state == telepathy.TUBE_STATE_LOCAL_PENDING:
383
                self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(
384
                        identifier)
385
386
            self.tube_conn = TubeConnection(self.conn,
387
                self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
388
                identifier, group_iface=self.text_chan[
389
                    telepathy.CHANNEL_INTERFACE_GROUP])
390
391
            _logger.debug('Tube created')
392
            self.messenger = Messenger(self.tube_conn, self.initiating,
393
                                       self.model)
394
395
    def _get_data_from_file_path(self, file_path):
396
        fd = open(file_path, 'r')
397
        try:
398
            data = fd.read()
399
        finally:
400
            fd.close()
401
        return data
402
403
    def read_file(self, file_path):
404
        if self.metadata['mime_type'] == 'text/plain':
405
            data = self._get_data_from_file_path(file_path)
406
            self.model.deserialize(data)
407
408
            for link in self.model.data['shared_links']:
409
                _logger.debug('read: url=%s title=%s d=%s' % (link['url'],
410
                                                              link['title'],
411
                                                              link['color']))
412
                self._add_link_totray(link['url'],
413
                                      base64.b64decode(link['thumb']),
414
                                      link['color'], link['title'],
415
                                      link['owner'], -1, link['hash'])
416
            logging.debug('########## reading %s', data)
417
            self._tabbed_view.set_history(self.model.data['history'])
418
            for number, tab in enumerate(self.model.data['currents']):
419
                tab_page = self._tabbed_view.get_nth_page(number)
420
                tab_page.browser.set_history_index(tab['history_index'])
421
                zoom_level = tab.get('zoom_level')
422
                if zoom_level is not None:
423
                    tab_page.browser.set_zoom_level(zoom_level)
424
                tab_page.browser.grab_focus()
425
426
            self._tabbed_view.set_current_page(self.model.data['current_tab'])
427
428
        elif self.metadata['mime_type'] == 'text/uri-list':
429
            data = self._get_data_from_file_path(file_path)
430
            uris = mime.split_uri_list(data)
431
            if len(uris) == 1:
432
                self._tabbed_view.props.current_browser.load_uri(uris[0])
433
            else:
434
                _logger.error('Open uri-list: Does not support'
435
                              'list of multiple uris by now.')
436
        else:
437
            file_uri = 'file://' + file_path
438
            self._tabbed_view.props.current_browser.load_uri(file_uri)
439
            self._tabbed_view.props.current_browser.grab_focus()
440
441
    def write_file(self, file_path):
442
        if not self.metadata['mime_type']:
443
            self.metadata['mime_type'] = 'text/plain'
444
445
        if self.metadata['mime_type'] == 'text/plain':
446
447
            browser = self._tabbed_view.current_browser
448
449
            if not self._jobject.metadata['title_set_by_user'] == '1':
450
                if browser.props.title is None:
451
                    self.metadata['title'] = _('Untitled')
452
                else:
453
                    self.metadata['title'] = browser.props.title
454
455
            self.model.data['history'] = self._tabbed_view.get_history()
456
            current_tab = self._tabbed_view.get_current_page()
457
            self.model.data['current_tab'] = current_tab
458
459
            self.model.data['currents'] = []
460
            for n in range(0, self._tabbed_view.get_n_pages()):
461
                tab_page = self._tabbed_view.get_nth_page(n)
462
                n_browser = tab_page.browser
463
                if n_browser != None:
464
                    uri = n_browser.get_uri()
465
                    history_index = n_browser.get_history_index()
466
                    info = {'title': n_browser.props.title, 'url': uri,
467
                            'history_index': history_index,
468
                            'zoom_level': n_browser.get_zoom_level()}
469
470
                    self.model.data['currents'].append(info)
471
472
            f = open(file_path, 'w')
473
            try:
474
                logging.debug('########## writing %s', self.model.serialize())
475
                f.write(self.model.serialize())
476
            finally:
477
                f.close()
478
479
    def _link_add_button_cb(self, button):
480
        self._add_link()
481
482
    def _go_home_button_cb(self, button):
483
        self._tabbed_view.load_homepage()
484
485
    def _go_library_button_cb(self, button):
486
        self._tabbed_view.load_homepage(ignore_gconf=True)
487
488
    def _set_home_button_cb(self, button):
489
        self._tabbed_view.set_homepage()
490
        self._alert(_('The initial page was configured'))
491
492
    def _reset_home_button_cb(self, button):
493
        self._tabbed_view.reset_homepage()
494
        self._alert(_('The default initial page was configured'))
495
496
    def _alert(self, title, text=None):
497
        alert = NotifyAlert(timeout=5)
498
        alert.props.title = title
499
        alert.props.msg = text
500
        self.add_alert(alert)
501
        alert.connect('response', self._alert_cancel_cb)
502
        alert.show()
503
504
    def _alert_cancel_cb(self, alert, response_id):
505
        self.remove_alert(alert)
506
507
    def _key_press_cb(self, widget, event):
508
        key_name = Gdk.keyval_name(event.keyval)
509
        browser = self._tabbed_view.props.current_browser
510
511
        if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
512
513
            if key_name == 'd':
514
                self._add_link()
515
            elif key_name == 'f':
516
                _logger.debug('keyboard: Find')
517
                self._edit_toolbar_button.set_expanded(True)
518
                self._edit_toolbar.search_entry.grab_focus()
519
            elif key_name == 'l':
520
                _logger.debug('keyboard: Focus url entry')
521
                self._primary_toolbar.entry.grab_focus()
522
            elif key_name == 'minus':
523
                _logger.debug('keyboard: Zoom out')
524
                browser.zoom_out()
525
            elif key_name in ['plus', 'equal']:
526
                _logger.debug('keyboard: Zoom in')
527
                browser.zoom_in()
528
            elif key_name == '0':
529
                _logger.debug('keyboard: Actual size')
530
                browser.set_zoom_level(ZOOM_ORIGINAL)
531
            elif key_name == 'Left':
532
                _logger.debug('keyboard: Go back')
533
                browser.go_back()
534
            elif key_name == 'Right':
535
                _logger.debug('keyboard: Go forward')
536
                browser.go_forward()
537
            elif key_name == 'r':
538
                _logger.debug('keyboard: Reload')
539
                browser.reload()
540
            elif Gdk.keyval_name(event.keyval) == "t":
541
                self._tabbed_view.add_tab()
542
            elif key_name == 'w':
543
                _logger.debug('keyboard: close tab')
544
                self._tabbed_view.close_tab()
545
            else:
546
                return False
547
548
            return True
549
550
        elif key_name in ('KP_Up', 'KP_Down', 'KP_Left', 'KP_Right'):
551
            scrolled_window = browser.get_parent()
552
553
            if key_name in ('KP_Up', 'KP_Down'):
554
                adjustment = scrolled_window.get_vadjustment()
555
            elif key_name in ('KP_Left', 'KP_Right'):
556
                adjustment = scrolled_window.get_hadjustment()
557
            value = adjustment.get_value()
558
            step = adjustment.get_step_increment()
559
560
            if key_name in ('KP_Up', 'KP_Left'):
561
                adjustment.set_value(value - step)
562
            elif key_name in ('KP_Down', 'KP_Right'):
563
                adjustment.set_value(value + step)
564
565
            return True
566
567
        elif key_name == 'Escape':
568
            status = browser.get_load_status()
569
            loading = WebKit.LoadStatus.PROVISIONAL <= status \
570
                < WebKit.LoadStatus.FINISHED
571
            if loading:
572
                _logger.debug('keyboard: Stop loading')
573
                browser.stop_loading()
574
575
        return False
576
577
    def _add_link(self):
578
        ''' take screenshot and add link info to the model '''
579
580
        browser = self._tabbed_view.props.current_browser
581
        ui_uri = browser.get_uri()
582
583
        for link in self.model.data['shared_links']:
584
            if link['hash'] == sha1(ui_uri).hexdigest():
585
                _logger.debug('_add_link: link exist already a=%s b=%s',
586
                              link['hash'], sha1(ui_uri).hexdigest())
587
                return
588
        buf = self._get_screenshot()
589
        timestamp = time.time()
590
        self.model.add_link(ui_uri, browser.props.title, buf,
591
                            profile.get_nick_name(),
592
                            profile.get_color().to_string(), timestamp)
593
594
        if self.messenger is not None:
595
            self.messenger._add_link(ui_uri, browser.props.title,
596
                                     profile.get_color().to_string(),
597
                                     profile.get_nick_name(),
598
                                     base64.b64encode(buf), timestamp)
599
600
    def _add_link_model_cb(self, model, index):
601
        ''' receive index of new link from the model '''
602
        link = self.model.data['shared_links'][index]
603
        self._add_link_totray(link['url'], base64.b64decode(link['thumb']),
604
                              link['color'], link['title'],
605
                              link['owner'], index, link['hash'])
606
607
    def _add_link_totray(self, url, buf, color, title, owner, index, hash):
608
        ''' add a link to the tray '''
609
        item = LinkButton(buf, color, title, owner, hash)
610
        item.connect('clicked', self._link_clicked_cb, url)
611
        item.connect('remove_link', self._link_removed_cb)
612
        # use index to add to the tray
613
        self._tray.add_item(item, index)
614
        item.show()
615
        self._view_toolbar.traybutton.props.sensitive = True
616
        self._view_toolbar.traybutton.props.active = True
617
        self._view_toolbar.update_traybutton_tooltip()
618
619
    def _link_removed_cb(self, button, hash):
620
        ''' remove a link from tray and delete it in the model '''
621
        self.model.remove_link(hash)
622
        self._tray.remove_item(button)
623
        if len(self._tray.get_children()) == 0:
624
            self._view_toolbar.traybutton.props.sensitive = False
625
            self._view_toolbar.traybutton.props.active = False
626
            self._view_toolbar.update_traybutton_tooltip()
627
628
    def _link_clicked_cb(self, button, url):
629
        ''' an item of the link tray has been clicked '''
630
        self._tabbed_view.props.current_browser.load_uri(url)
631
632
    def _get_screenshot(self):
633
        browser = self._tabbed_view.props.current_browser
634
        window = browser.get_window()
635
        width, height = window.get_width(), window.get_height()
636
637
        thumb_width, thumb_height = style.zoom(100), style.zoom(80)
638
639
        thumb_surface = Gdk.Window.create_similar_surface(window,
640
            cairo.CONTENT_COLOR, thumb_width, thumb_height)
641
642
        cairo_context = cairo.Context(thumb_surface)
643
        thumb_scale_w = thumb_width * 1.0 / width
644
        thumb_scale_h = thumb_height * 1.0 / height
645
        cairo_context.scale(thumb_scale_w, thumb_scale_h)
646
        Gdk.cairo_set_source_window(cairo_context, window, 0, 0)
647
        cairo_context.paint()
648
649
        thumb_str = StringIO.StringIO()
650
        thumb_surface.write_to_png(thumb_str)
651
        return thumb_str.getvalue()
652
653
    def can_close(self):
654
        if self._force_close:
655
            return True
656
        elif downloadmanager.can_quit():
657
            return True
658
        else:
659
            alert = Alert()
660
            alert.props.title = ngettext('Download in progress',
661
                                         'Downloads in progress',
662
                                         downloadmanager.num_downloads())
663
            message = ngettext('Stopping now will erase your download',
664
                               'Stopping now will erase your downloads',
665
                               downloadmanager.num_downloads())
666
            alert.props.msg = message
667
            cancel_icon = Icon(icon_name='dialog-cancel')
668
            cancel_label = ngettext('Continue download', 'Continue downloads',
669
                                    downloadmanager.num_downloads())
670
            alert.add_button(Gtk.ResponseType.CANCEL, cancel_label,
671
                             cancel_icon)
672
            stop_icon = Icon(icon_name='dialog-ok')
673
            alert.add_button(Gtk.ResponseType.OK, _('Stop'), stop_icon)
674
            stop_icon.show()
675
            self.add_alert(alert)
676
            alert.connect('response', self.__inprogress_response_cb)
677
            alert.show()
678
            self.present()
679
            return False
680
681
    def __inprogress_response_cb(self, alert, response_id):
682
        self.remove_alert(alert)
683
        if response_id is Gtk.ResponseType.CANCEL:
684
            logging.debug('Keep on')
685
        elif response_id == Gtk.ResponseType.OK:
686
            logging.debug('Stop downloads and quit')
687
            self._force_close = True
688
            downloadmanager.remove_all_downloads()
689
            self.close()
690
691
    def __switch_page_cb(self, tabbed_view, page, page_num):
692
        browser = page._browser
693
        status = browser.get_load_status()
694
695
        if status in (WebKit.LoadStatus.COMMITTED,
696
                      WebKit.LoadStatus.FIRST_VISUALLY_NON_EMPTY_LAYOUT):
697
            self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH))
698
        elif status in (WebKit.LoadStatus.PROVISIONAL,
699
                        WebKit.LoadStatus.FAILED,
700
                        WebKit.LoadStatus.FINISHED):
701
            self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
702
703
    def get_document_path(self, async_cb, async_err_cb):
704
        browser = self._tabbed_view.props.current_browser
705
        browser.get_source(async_cb, async_err_cb)
706
707
    def get_canvas(self):
708
        return self._tabbed_view