Ticket #41: webactivity.py

File webactivity.py, 21.1 KB (added by davidkofler, 15 years ago)

Modifies webacivity.py that enables the tooltips

Line 
1# Copyright (C) 2006, Red Hat, Inc.
2# Copyright (C) 2009 Martin Langhoff, Simon Schampijer, Daniel Drake
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
18import os
19import logging
20from gettext import gettext as _
21
22import gobject
23gobject.threads_init()
24
25import gtk
26import sha
27import base64
28import time
29import shutil
30import sqlite3
31import json
32import gconf
33
34from urlparse import urlparse
35
36# HACK: Needed by http://dev.sugarlabs.org/ticket/456
37import gnome
38gnome.init('Hulahop', '1.0')
39
40from sugar.activity import activity
41from sugar.graphics import style
42import telepathy
43import telepathy.client
44from sugar.presence import presenceservice
45from sugar.graphics.tray import HTray
46from sugar import profile
47from sugar.graphics.alert import Alert
48from sugar.graphics.icon import Icon
49from sugar import mime
50
51PROFILE_VERSION = 1
52
53_profile_version = 0
54_profile_path = os.path.join(activity.get_activity_root(), 'data/gecko')
55_version_file = os.path.join(_profile_path, 'version')
56
57if os.path.exists(_version_file):
58    f = open(_version_file)
59    _profile_version = int(f.read())
60    f.close()
61
62if _profile_version < PROFILE_VERSION:
63    if not os.path.exists(_profile_path):
64        os.mkdir(_profile_path)
65
66    shutil.copy('cert8.db', _profile_path)
67    os.chmod(os.path.join(_profile_path, 'cert8.db'), 0660)
68
69    f = open(_version_file, 'w')
70    f.write(str(PROFILE_VERSION))
71    f.close()
72
73def _seed_xs_cookie():
74    ''' Create a HTTP Cookie to authenticate with the Schoolserver
75    '''
76    client = gconf.client_get_default()
77    backup_url = client.get_string('/desktop/sugar/backup_url')
78    if not backup_url:
79        _logger.debug('seed_xs_cookie: Not registered with Schoolserver')
80        return
81
82    jabber_server = client.get_string(
83        '/desktop/sugar/collaboration/jabber_server')
84
85    pubkey = profile.get_profile().pubkey
86    cookie_data = {'color': profile.get_color().to_string(),
87                   'pkey_hash': sha.new(pubkey).hexdigest()}
88
89    db_path = os.path.join(_profile_path, 'cookies.sqlite')
90    try:
91        cookies_db = sqlite3.connect(db_path)
92        c = cookies_db.cursor()
93
94        c.execute('''CREATE TABLE IF NOT EXISTS
95                     moz_cookies
96                     (id INTEGER PRIMARY KEY,
97                      name TEXT,
98                      value TEXT,
99                      host TEXT,
100                      path TEXT,
101                      expiry INTEGER,
102                      lastAccessed INTEGER,
103                      isSecure INTEGER,
104                      isHttpOnly INTEGER)''')
105
106        c.execute('''SELECT id
107                     FROM moz_cookies
108                     WHERE name=? AND host=? AND path=?''',
109                  ('xoid', jabber_server, '/'))
110       
111        if c.fetchone():
112            _logger.debug('seed_xs_cookie: Cookie exists already')
113            return
114
115        expire = int(time.time()) + 10*365*24*60*60
116        c.execute('''INSERT INTO moz_cookies (name, value, host,
117                                              path, expiry, lastAccessed,
118                                              isSecure, isHttpOnly)
119                     VALUES(?,?,?,?,?,?,?,?)''',
120                  ('xoid', cjson.encode(cookie_data), jabber_server,
121                   '/', expire, 0, 0, 0 ))
122        cookies_db.commit()
123        cookies_db.close()
124    except sqlite3.Error, e:
125        _logger.error('seed_xs_cookie: %s' % e)
126    else:
127        _logger.debug('seed_xs_cookie: Updated cookie successfully')
128
129
130import hulahop
131hulahop.set_app_version(os.environ['SUGAR_BUNDLE_VERSION'])
132hulahop.startup(_profile_path)
133
134from xpcom import components
135
136def _set_accept_languages():
137    ''' Set intl.accept_languages based on the locale
138    '''
139    try:
140        lang = os.environ['LANG'].strip('\n') # e.g. es_UY.UTF-8
141    except KeyError:
142        return
143
144    if (not lang.endswith(".utf8") or not lang.endswith(".UTF-8")) \
145            and lang[2] != "_":
146        _logger.debug("Set_Accept_language: unrecognised LANG format")
147        return
148
149    # e.g. es-uy, es
150    pref = lang[0:2] + "-" + lang[3:5].lower()  + ", " + lang[0:2]
151    cls = components.classes["@mozilla.org/preferences-service;1"]
152    prefService = cls.getService(components.interfaces.nsIPrefService)
153    branch = prefService.getBranch('')
154    branch.setCharPref('intl.accept_languages', pref)
155    logging.debug('LANG set')
156
157from browser import Browser
158from edittoolbar import EditToolbar
159from webtoolbar import WebToolbar
160from viewtoolbar import ViewToolbar
161import downloadmanager
162import globalhistory
163import filepicker
164
165_LIBRARY_PATH = '/usr/share/library-common/index.html'
166
167from model import Model
168from sugar.presence.tubeconn import TubeConnection
169from messenger import Messenger
170from linkbutton import LinkButton
171
172SERVICE = "org.laptop.WebActivity"
173IFACE = SERVICE
174PATH = "/org/laptop/WebActivity"
175
176_TOOLBAR_EDIT = 1
177_TOOLBAR_BROWSE = 2
178
179_logger = logging.getLogger('web-activity')
180
181class WebActivity(activity.Activity):
182    def __init__(self, handle):
183        activity.Activity.__init__(self, handle)
184
185        _logger.debug('Starting the web activity')
186
187        self._browser = Browser()
188
189        _set_accept_languages()
190        _seed_xs_cookie()
191       
192        # don't pick up the sugar theme - use the native mozilla one instead
193        cls = components.classes['@mozilla.org/preferences-service;1']
194        pref_service = cls.getService(components.interfaces.nsIPrefService)
195        branch = pref_service.getBranch("mozilla.widget.")
196        branch.setBoolPref("disable-native-theme", True)
197
198        toolbox = activity.ActivityToolbox(self)
199
200        self._edit_toolbar = EditToolbar(self._browser)
201        toolbox.add_toolbar(_('Edit'), self._edit_toolbar)
202        self._edit_toolbar.show()
203
204        self._web_toolbar = WebToolbar(self._browser)
205        toolbox.add_toolbar(_('Browse'), self._web_toolbar)
206        self._web_toolbar.show()
207       
208        self._tray = HTray()
209        self.set_tray(self._tray, gtk.POS_BOTTOM)
210        self._tray.show()
211       
212        self._view_toolbar = ViewToolbar(self)
213        toolbox.add_toolbar(_('View'), self._view_toolbar)
214        self._view_toolbar.show()
215
216        self.set_toolbox(toolbox)
217        toolbox.show()
218
219        self.set_canvas(self._browser)
220        self._browser.show()
221                 
222        self._browser.history.connect('session-link-changed',
223                                      self._session_history_changed_cb)
224        self._web_toolbar.connect('add-link', self._link_add_button_cb)
225
226        self._browser.connect("notify::title", self._title_changed_cb)
227
228        self.model = Model()
229        self.model.connect('add_link', self._add_link_model_cb)
230
231        self.current = _('blank')
232        self.webtitle = _('blank')
233        self.connect('key-press-event', self._key_press_cb)
234                     
235        self.toolbox.set_current_toolbar(_TOOLBAR_BROWSE)
236       
237        if handle.uri:
238            self._browser.load_uri(handle.uri)       
239        elif not self._jobject.file_path:
240            # TODO: we need this hack until we extend the activity API for
241            # opening URIs and default docs.
242            self._load_homepage()
243
244        self.messenger = None
245        self.connect('shared', self._shared_cb)
246
247        # Get the Presence Service       
248        self.pservice = presenceservice.get_instance()
249        try:
250            name, path = self.pservice.get_preferred_connection()
251            self.tp_conn_name = name
252            self.tp_conn_path = path
253            self.conn = telepathy.client.Connection(name, path)
254        except TypeError:
255            _logger.debug('Offline')
256        self.initiating = None
257           
258        if self._shared_activity is not None:
259            _logger.debug('shared:  %s' %self._shared_activity.props.joined)
260
261        if self._shared_activity is not None:
262            # We are joining the activity
263            _logger.debug('Joined activity')                     
264            self.connect('joined', self._joined_cb)
265            if self.get_shared():
266                # We've already joined
267                self._joined_cb()
268        else:   
269            _logger.debug('Created activity')
270   
271    def _shared_cb(self, activity_):
272        _logger.debug('My activity was shared')       
273        self.initiating = True                       
274        self._setup()
275
276        _logger.debug('This is my activity: making a tube...')
277        self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(SERVICE, {})
278               
279    def _setup(self):
280        if self._shared_activity is None:
281            _logger.debug('Failed to share or join activity')
282            return
283
284        bus_name, conn_path, channel_paths = \
285                self._shared_activity.get_channels()
286
287        # Work out what our room is called and whether we have Tubes already
288        room = None
289        tubes_chan = None
290        text_chan = None
291        for channel_path in channel_paths:
292            channel = telepathy.client.Channel(bus_name, channel_path)
293            htype, handle = channel.GetHandle()
294            if htype == telepathy.HANDLE_TYPE_ROOM:
295                _logger.debug('Found our room: it has handle#%d "%s"'
296                    %(handle, self.conn.InspectHandles(htype, [handle])[0]))
297                room = handle
298                ctype = channel.GetChannelType()
299                if ctype == telepathy.CHANNEL_TYPE_TUBES:
300                    _logger.debug('Found our Tubes channel at %s'%channel_path)
301                    tubes_chan = channel
302                elif ctype == telepathy.CHANNEL_TYPE_TEXT:
303                    _logger.debug('Found our Text channel at %s'%channel_path)
304                    text_chan = channel
305
306        if room is None:
307            _logger.debug("Presence service didn't create a room")
308            return
309        if text_chan is None:
310            _logger.debug("Presence service didn't create a text channel")
311            return
312
313        # Make sure we have a Tubes channel - PS doesn't yet provide one
314        if tubes_chan is None:
315            _logger.debug("Didn't find our Tubes channel, requesting one...")
316            tubes_chan = self.conn.request_channel(telepathy.CHANNEL_TYPE_TUBES,
317                                                   telepathy.HANDLE_TYPE_ROOM,
318                                                   room, True)
319
320        self.tubes_chan = tubes_chan
321        self.text_chan = text_chan
322
323        tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal( \
324                'NewTube', self._new_tube_cb)
325
326    def _list_tubes_reply_cb(self, tubes):
327        for tube_info in tubes:
328            self._new_tube_cb(*tube_info)
329
330    def _list_tubes_error_cb(self, e):
331        _logger.debug('ListTubes() failed: %s'%e)
332
333    def _joined_cb(self, activity_):
334        if not self._shared_activity:
335            return
336
337        _logger.debug('Joined an existing shared activity')
338       
339        self.initiating = False
340        self._setup()
341               
342        _logger.debug('This is not my activity: waiting for a tube...')
343        self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
344            reply_handler=self._list_tubes_reply_cb,
345            error_handler=self._list_tubes_error_cb)
346
347    def _new_tube_cb(self, identifier, initiator, type, service, params, state):
348        _logger.debug('New tube: ID=%d initator=%d type=%d service=%s '
349                      'params=%r state=%d' %(identifier, initiator, type,
350                                             service, params, state))
351
352        if (type == telepathy.TUBE_TYPE_DBUS and
353            service == SERVICE):
354            if state == telepathy.TUBE_STATE_LOCAL_PENDING:
355                self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(
356                        identifier)
357
358            self.tube_conn = TubeConnection(self.conn,
359                self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
360                identifier, group_iface = self.text_chan[
361                    telepathy.CHANNEL_INTERFACE_GROUP])
362           
363            _logger.debug('Tube created')
364            self.messenger = Messenger(self.tube_conn, self.initiating,
365                                       self.model)         
366
367             
368    def _load_homepage(self):
369        if os.path.isfile(_LIBRARY_PATH):
370            self._browser.load_uri('file://' + _LIBRARY_PATH)
371        else:
372            default_page = os.path.join(activity.get_bundle_path(),
373                                        "data/index.html")
374            self._browser.load_uri(default_page)
375
376    def _session_history_changed_cb(self, session_history, link):
377        _logger.debug('NewPage: %s.' %link)
378        self.current = link
379       
380    def _title_changed_cb(self, embed, pspec):
381        if embed.props.title is not '':
382            _logger.debug('Title changed=%s' % embed.props.title)
383            self.webtitle = embed.props.title
384
385    def _get_data_from_file_path(self, file_path):
386        fd = open(file_path, 'r')
387        try:
388            data = fd.read()
389        finally:
390            fd.close()
391        return data
392
393    def read_file(self, file_path):
394        if self.metadata['mime_type'] == 'text/plain':
395            data = self._get_data_from_file_path(file_path)
396            self.model.deserialize(data)
397           
398            for link in self.model.data['shared_links']:
399                _logger.debug('read: url=%s title=%s d=%s' % (link['url'],
400                                                              link['title'],
401                                                              link['color']))
402                self._add_link_totray(link['url'],
403                                      base64.b64decode(link['thumb']),
404                                      link['color'], link['title'],
405                                      link['owner'], -1, link['hash'])     
406            self._browser.set_session(self.model.data['history'])
407        elif self.metadata['mime_type'] == 'text/uri-list':
408            data = self._get_data_from_file_path(file_path)
409            uris = mime.split_uri_list(data)
410            if len(uris) == 1:
411                self._browser.load_uri(uris[0])
412            else:
413                _logger.error('Open uri-list: Does not support'
414                              'list of multiple uris by now.')
415        else:
416            self._browser.load_uri(file_path)
417       
418    def write_file(self, file_path):
419        if not self.metadata['mime_type']:
420            self.metadata['mime_type'] = 'text/plain'
421       
422        if self.metadata['mime_type'] == 'text/plain':
423            if not self._jobject.metadata['title_set_by_user'] == '1':
424                if self._browser.props.title:
425                    self.metadata['title'] = self._browser.props.title
426
427            self.model.data['history'] = self._browser.get_session()
428
429            f = open(file_path, 'w')
430            try:
431                f.write(self.model.serialize())
432            finally:
433                f.close()
434
435    def _link_add_button_cb(self, button):
436        _logger.debug('button: Add link: %s.' % self.current)               
437        self._add_link()
438           
439    def _key_press_cb(self, widget, event):
440        if event.state & gtk.gdk.CONTROL_MASK:
441            if gtk.gdk.keyval_name(event.keyval) == "d":
442                _logger.debug('keyboard: Add link: %s.' % self.current)     
443                self._add_link()               
444                return True
445            elif gtk.gdk.keyval_name(event.keyval) == "f":
446                _logger.debug('keyboard: Find')
447                self.toolbox.set_current_toolbar(_TOOLBAR_EDIT)
448                self._edit_toolbar.search_entry.grab_focus()
449                return True
450            elif gtk.gdk.keyval_name(event.keyval) == "l":
451                _logger.debug('keyboard: Focus url entry')
452                self.toolbox.set_current_toolbar(_TOOLBAR_BROWSE)
453                self._web_toolbar.entry.grab_focus()
454                return True
455            elif gtk.gdk.keyval_name(event.keyval) == "minus":
456                _logger.debug('keyboard: Zoom out')
457                self._browser.zoom_out()
458                return True
459            elif gtk.gdk.keyval_name(event.keyval) == "plus" \
460                     or gtk.gdk.keyval_name(event.keyval) == "equal" :
461                _logger.debug('keyboard: Zoom in')
462                self._browser.zoom_in()
463                return True
464        return False
465
466    def _add_link(self):
467        ''' take screenshot and add link info to the model '''
468        for link in self.model.data['shared_links']:
469            if link['hash'] == sha.new(self.current).hexdigest():
470                _logger.debug('_add_link: link exist already a=%s b=%s' %(
471                    link['hash'], sha.new(self.current).hexdigest()))
472                return
473        buf = self._get_screenshot()
474        timestamp = time.time()
475        self.model.add_link(self.current, self.webtitle, buf,
476                            profile.get_nick_name(),
477                            profile.get_color().to_string(), timestamp)
478
479        if self.messenger is not None:
480            self.messenger._add_link(self.current, self.webtitle,       
481                                     profile.get_color().to_string(),
482                                     profile.get_nick_name(),
483                                     base64.b64encode(buf), timestamp)
484
485    def _add_link_model_cb(self, model, index):
486        ''' receive index of new link from the model '''
487        link = self.model.data['shared_links'][index]
488        self._add_link_totray(link['url'], base64.b64decode(link['thumb']),
489                              link['color'], link['title'],
490                              link['owner'], index, link['hash'])
491
492    def _add_link_totray(self, url, buf, color, title, owner, index, hash):
493        ''' add a link to the tray '''
494        item = LinkButton(url, buf, color, title, owner, index, hash)
495        item.connect('clicked', self._link_clicked_cb, url)
496        item.connect('remove_link', self._link_removed_cb)
497        info = ToolTip.get_info(url)
498        tooltip = ToolTip(item, text=info)
499        self._tray.add_item(item, index) # use index to add to the tray
500        item.show()
501        if self._tray.props.visible is False:
502            self._tray.show()       
503        self._view_toolbar.traybutton.props.sensitive = True
504       
505    def _link_removed_cb(self, button, hash):
506        ''' remove a link from tray and delete it in the model '''
507        self.model.remove_link(hash)
508        self._tray.remove_item(button)
509        if len(self._tray.get_children()) == 0:
510            self._view_toolbar.traybutton.props.sensitive = False
511
512    def _link_clicked_cb(self, button, url):
513        ''' an item of the link tray has been clicked '''
514        self._browser.load_uri(url)
515
516    def _pixbuf_save_cb(self, buf, data):
517        data[0] += buf
518        return True
519
520    def get_buffer(self, pixbuf):
521        data = [""]
522        pixbuf.save_to_callback(self._pixbuf_save_cb, "png", {}, data)
523        return str(data[0])
524
525    def _get_screenshot(self):
526        window = self._browser.window
527        width, height = window.get_size()
528
529        screenshot = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=False,
530                                    bits_per_sample=8, width=width,
531                                    height=height)
532        screenshot.get_from_drawable(window, window.get_colormap(), 0, 0, 0, 0,
533                                     width, height)
534
535        screenshot = screenshot.scale_simple(style.zoom(100),
536                                                 style.zoom(80),
537                                                 gtk.gdk.INTERP_BILINEAR)
538
539        buf = self.get_buffer(screenshot)
540        return buf
541
542    def can_close(self):
543        if downloadmanager.can_quit():
544            return True
545        else:
546            alert = Alert()
547            alert.props.title = _('Download in progress')
548            alert.props.msg = _('Stopping now will cancel your download')
549            cancel_icon = Icon(icon_name='dialog-cancel')
550            alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), cancel_icon)
551            stop_icon = Icon(icon_name='dialog-ok')
552            alert.add_button(gtk.RESPONSE_OK, _('Stop'), stop_icon)
553            stop_icon.show()
554            self.add_alert(alert)
555            alert.connect('response', self.__inprogress_response_cb)
556            alert.show()           
557            self.present()
558
559    def __inprogress_response_cb(self, alert, response_id):
560        self.remove_alert(alert)
561        if response_id is gtk.RESPONSE_CANCEL:
562            logging.debug('Keep on')
563        elif response_id == gtk.RESPONSE_OK:
564            logging.debug('Stop downloads and quit')
565            downloadmanager.remove_all_downloads()
566            self.close(force=True)
567
568    def get_document_path(self, async_cb, async_err_cb):
569        self._browser.get_source(async_cb, async_err_cb)