~dobey/ubuntu/oneiric/ubuntuone-control-panel/release-113

« back to all changes in this revision

Viewing changes to ubuntuone/controlpanel/gtk/gui.py

  • Committer: Sebastien Bacher
  • Date: 2011-07-25 13:17:38 UTC
  • mfrom: (25.1.2 ubuntuone-control-panel)
  • Revision ID: seb128@ubuntu.com-20110725131738-yuevatnd859d1phs
Tags: 1.1.1-0ubuntu1
releasing version 1.1.1-0ubuntu1

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
 
3
 
# Authors: Natalia B Bidart <natalia.bidart@canonical.com>
4
 
#          Eric Casteleijn <eric.casteleijn@canonical.com>
5
 
#
6
 
# Copyright 2010 Canonical Ltd.
7
 
#
8
 
# This program is free software: you can redistribute it and/or modify it
9
 
# under the terms of the GNU General Public License version 3, as published
10
 
# by the Free Software Foundation.
11
 
#
12
 
# This program is distributed in the hope that it will be useful, but
13
 
# WITHOUT ANY WARRANTY; without even the implied warranties of
14
 
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15
 
# PURPOSE.  See the GNU General Public License for more details.
16
 
#
17
 
# You should have received a copy of the GNU General Public License along
18
 
# with this program.  If not, see <http://www.gnu.org/licenses/>.
19
 
 
20
 
"""The user interface for the control panel for Ubuntu One."""
21
 
 
22
 
from __future__ import division
23
 
 
24
 
import gettext
25
 
import operator
26
 
import os
27
 
 
28
 
from functools import wraps
29
 
 
30
 
import dbus
31
 
import gtk
32
 
import gobject
33
 
import ubuntu_sso
34
 
 
35
 
from dbus.mainloop.glib import DBusGMainLoop
36
 
from ubuntu_sso import networkstate
37
 
from ubuntu_sso.credentials import (TC_URL_KEY, HELP_TEXT_KEY, WINDOW_ID_KEY,
38
 
    PING_URL_KEY)
39
 
# No name 'clientdefs' in module 'ubuntuone'
40
 
# pylint: disable=E0611,F0401
41
 
from gi.repository import GLib
42
 
from ubuntuone.clientdefs import (APP_NAME as U1_APP_NAME, TC_URL as U1_TC_URL,
43
 
    PING_URL as U1_PING_URL, DESCRIPTION as U1_DESCRIPTION)
44
 
# pylint: enable=E0611,F0401
45
 
 
46
 
from ubuntuone.controlpanel.gtk import (
47
 
    DBUS_IFACE_GUI, DBUS_BUS_NAME as DBUS_BUS_NAME_GUI,
48
 
    DBUS_PATH as DBUS_PATH_GUI)
49
 
from ubuntuone.controlpanel.gtk.widgets import LabelLoading, PanelTitle
50
 
# Use ubiquity package when ready (LP: #673665)
51
 
from ubuntuone.controlpanel.gtk.widgets import GreyableBin
52
 
 
53
 
from ubuntuone.controlpanel import (DBUS_BUS_NAME, DBUS_PREFERENCES_PATH,
54
 
    DBUS_PREFERENCES_IFACE, backend)
55
 
from ubuntuone.controlpanel.backend import (DEVICE_TYPE_PHONE,
56
 
    DEVICE_TYPE_COMPUTER, bool_str)
57
 
from ubuntuone.controlpanel.logger import setup_logging, log_call
58
 
from ubuntuone.controlpanel.utils import (get_data_file,
59
 
    ERROR_TYPE, ERROR_MESSAGE)
60
 
 
61
 
from ubuntuone.controlpanel.gtk import package_manager, TRANSLATION_DOMAIN
62
 
 
63
 
try:
64
 
    from gi.repository import Unity     # pylint: disable=E0611
65
 
    USE_LIBUNITY = True
66
 
    U1_DOTDESKTOP = "ubuntuone-control-panel-gtk.desktop"
67
 
except ImportError:
68
 
    USE_LIBUNITY = False
69
 
 
70
 
logger = setup_logging('gtk.gui')
71
 
_ = gettext.gettext
72
 
 
73
 
 
74
 
# To be replaced by values from the theme or Ubuntu One' specific (LP: #673663)
75
 
ORANGE = '#c95724'
76
 
ERROR_COLOR = 'red'
77
 
LOADING = _('Loading...')
78
 
VALUE_ERROR = _('Value could not be retrieved.')
79
 
UNKNOWN_ERROR = _('Unknown error')
80
 
WARNING_MARKUP = '<span foreground="%s"><b>%%s</b></span>' % ERROR_COLOR
81
 
KILOBYTES = 1024
82
 
NO_OP = lambda *a, **kw: None
83
 
FILE_URI_PREFIX = 'file://'
84
 
 
85
 
 
86
 
def error_handler(*args, **kwargs):
87
 
    """Log errors when calling D-Bus methods in a async way."""
88
 
    logger.error('Error handler received: %r, %r', args, kwargs)
89
 
 
90
 
 
91
 
def register_service(bus):
92
 
    """Try to register DBus service for making sure we run only one instance.
93
 
 
94
 
    Return True if succesfully registered, False if already running.
95
 
    """
96
 
    name = bus.request_name(DBUS_BUS_NAME_GUI,
97
 
                                    dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
98
 
    return name != dbus.bus.REQUEST_NAME_REPLY_EXISTS
99
 
 
100
 
 
101
 
def publish_service(window=None, switch_to='', alert=False):
102
 
    """Publish the service on DBus."""
103
 
    if window is None:
104
 
        window = ControlPanelWindow(switch_to=switch_to, alert=alert)
105
 
    return ControlPanelService(window)
106
 
 
107
 
 
108
 
def main(switch_to='', alert=False):
109
 
    """Hook the DBus listeners and start the main loop."""
110
 
    DBusGMainLoop(set_as_default=True)
111
 
    bus = dbus.SessionBus()
112
 
    if register_service(bus):
113
 
        publish_service(switch_to=switch_to, alert=alert)
114
 
    else:
115
 
        obj = bus.get_object(DBUS_BUS_NAME_GUI, DBUS_PATH_GUI)
116
 
        service = dbus.Interface(obj, dbus_interface=DBUS_IFACE_GUI)
117
 
 
118
 
        def gui_error_handler(*args, **kwargs):
119
 
            """Log errors when calling D-Bus methods in a async way."""
120
 
            logger.error('Error handler received: %r, %r', args, kwargs)
121
 
            gtk.main_quit()
122
 
 
123
 
        def gui_reply_handler(*args, **kwargs):
124
 
            """Exit when done."""
125
 
            gtk.main_quit()
126
 
 
127
 
        service.switch_to_alert(
128
 
            switch_to, alert, reply_handler=gui_reply_handler,
129
 
            error_handler=gui_error_handler)
130
 
 
131
 
    gtk.main()
132
 
 
133
 
 
134
 
def filter_by_app_name(f):
135
 
    """Excecute 'f' filtering by app_name."""
136
 
 
137
 
    @wraps(f)
138
 
    def filter_by_app_name_inner(instance, app_name, *args, **kwargs):
139
 
        """Execute 'f' only if 'app_name' matches 'U1_APP_NAME'."""
140
 
        if app_name == U1_APP_NAME:
141
 
            return f(instance, app_name, *args, **kwargs)
142
 
        else:
143
 
            logger.info('%s: ignoring call since received app_name '\
144
 
                        '"%s" (expected "%s")',
145
 
                        f.__name__, app_name, U1_APP_NAME)
146
 
    return filter_by_app_name_inner
147
 
 
148
 
 
149
 
def on_size_allocate(widget, allocation, label):
150
 
    """Resize labels according to who 'widget' is being resized."""
151
 
    label.set_size_request(allocation.width - 2, -1)
152
 
 
153
 
 
154
 
@log_call(logger.debug)
155
 
def uri_hook(button, uri, *args, **kwargs):
156
 
    """Open an URI or do nothing if URI is not an URL."""
157
 
    if uri.startswith('http') or uri.startswith(FILE_URI_PREFIX):
158
 
        gtk.show_uri(None, uri, gtk.gdk.CURRENT_TIME)
159
 
 
160
 
 
161
 
class ControlPanelMixin(object):
162
 
    """The main interface for the Ubuntu One control panel."""
163
 
 
164
 
    def __init__(self, filename=None, backend_instance=None):
165
 
        if backend_instance is not None:
166
 
            self.backend = backend_instance
167
 
        else:
168
 
            bus = dbus.SessionBus()
169
 
            try:
170
 
                obj = bus.get_object(DBUS_BUS_NAME,
171
 
                                     DBUS_PREFERENCES_PATH,
172
 
                                     follow_name_owner_changes=True)
173
 
                iface = DBUS_PREFERENCES_IFACE
174
 
                self.backend = dbus.Interface(obj, dbus_interface=iface)
175
 
            except dbus.exceptions.DBusException:
176
 
                logger.exception('Can not connect to DBus at %r',
177
 
                                 (DBUS_BUS_NAME, DBUS_PREFERENCES_PATH))
178
 
                raise
179
 
 
180
 
        if filename is not None:
181
 
            builder = gtk.Builder()
182
 
            builder.set_translation_domain(TRANSLATION_DOMAIN)
183
 
            builder.add_from_file(get_data_file(filename))
184
 
            builder.connect_signals(self)
185
 
 
186
 
            # untested directly
187
 
            for obj in builder.get_objects():
188
 
                name = getattr(obj, 'name', None)
189
 
                if name is None and isinstance(obj, gtk.Buildable):
190
 
                    # work around bug lp:507739
191
 
                    name = gtk.Buildable.get_name(obj)
192
 
                if name is None:
193
 
                    logger.warning("%s has no name (??)", obj)
194
 
                else:
195
 
                    setattr(self, name, obj)
196
 
 
197
 
        logger.debug('%s: started.', self.__class__.__name__)
198
 
 
199
 
    def humanize(self, int_bytes):
200
 
        """Return a human readble string of 'int_bytes'."""
201
 
        return GLib.format_size_for_display(int_bytes)
202
 
 
203
 
    def _set_warning(self, message, label):
204
 
        """Set 'message' as warning in 'label'."""
205
 
        label.set_markup(WARNING_MARKUP % message)
206
 
        label.show()
207
 
 
208
 
 
209
 
class UbuntuOneBin(gtk.VBox):
210
 
    """A Ubuntu One bin."""
211
 
 
212
 
    TITLE = ''
213
 
 
214
 
    def __init__(self, title=None):
215
 
        gtk.VBox.__init__(self)
216
 
        self._is_processing = False
217
 
 
218
 
        if title is None:
219
 
            title = self.TITLE
220
 
 
221
 
        title = '<span font_size="large">%s</span>' % title
222
 
        self.title = PanelTitle(markup=title)
223
 
        self.pack_start(self.title, expand=False)
224
 
 
225
 
        self.message = LabelLoading(LOADING)
226
 
        self.pack_start(self.message, expand=False)
227
 
 
228
 
        self.connect('size-allocate', on_size_allocate, self.title)
229
 
        self.show_all()
230
 
 
231
 
    def _get_is_processing(self):
232
 
        """Is this panel processing a request?"""
233
 
        return self._is_processing
234
 
 
235
 
    def _set_is_processing(self, new_value):
236
 
        """Set if this panel is processing a request."""
237
 
        if new_value:
238
 
            self.message.start()
239
 
            self.set_sensitive(False)
240
 
        else:
241
 
            self.message.stop()
242
 
            self.set_sensitive(True)
243
 
 
244
 
        self._is_processing = new_value
245
 
 
246
 
    is_processing = property(fget=_get_is_processing, fset=_set_is_processing)
247
 
 
248
 
    @log_call(logger.debug)
249
 
    def on_success(self, message=''):
250
 
        """Use this callback to stop the Loading and show 'message'."""
251
 
        self.message.stop()
252
 
        self.message.set_markup(message)
253
 
 
254
 
    @log_call(logger.error)
255
 
    def on_error(self, message=None, error_dict=None):
256
 
        """Use this callback to stop the Loading and set a warning message."""
257
 
        if message is None and error_dict is None:
258
 
            message = VALUE_ERROR
259
 
        elif message is None and error_dict is not None:
260
 
            error_type = error_dict.get(ERROR_TYPE, UNKNOWN_ERROR)
261
 
            error_msg = error_dict.get(ERROR_MESSAGE)
262
 
            if error_msg:
263
 
                message = "%s (%s: %s)" % (VALUE_ERROR, error_type, error_msg)
264
 
            else:
265
 
                message = "%s (%s)" % (VALUE_ERROR, error_type)
266
 
 
267
 
        assert message is not None
268
 
 
269
 
        self.message.stop()
270
 
        self.message.set_markup(WARNING_MARKUP % message)
271
 
 
272
 
 
273
 
class OverviewPanel(GreyableBin, ControlPanelMixin):
274
 
    """The overview panel. Introduces Ubuntu One to the not logged user."""
275
 
 
276
 
    __gsignals__ = {
277
 
        'credentials-found': (gobject.SIGNAL_RUN_FIRST,  gobject.TYPE_NONE,
278
 
                              (gobject.TYPE_BOOLEAN, gobject.TYPE_PYOBJECT)),
279
 
    }
280
 
 
281
 
    CREDENTIALS_ERROR = _('There was a problem while retrieving the '
282
 
        'credentials.')
283
 
    NETWORK_OFFLINE = _('An internet connection is required to join or sign '
284
 
        'in to %(app_name)s.')
285
 
    CONNECT = _('Connect to Ubuntu One')
286
 
    LEARN_MORE_LINK = 'https://one.ubuntu.com/'
287
 
 
288
 
    def __init__(self, main_window):
289
 
        GreyableBin.__init__(self)
290
 
 
291
 
        sso_backend = None
292
 
        bus = dbus.SessionBus()
293
 
        try:
294
 
            obj = bus.get_object(ubuntu_sso.DBUS_BUS_NAME,
295
 
                                 ubuntu_sso.DBUS_CREDENTIALS_PATH,
296
 
                                 follow_name_owner_changes=True)
297
 
            iface = ubuntu_sso.DBUS_CREDENTIALS_IFACE
298
 
            sso_backend = dbus.Interface(obj, dbus_interface=iface)
299
 
        except dbus.exceptions.DBusException:
300
 
            logger.exception('Can not connect to DBus at %r',
301
 
                             (ubuntu_sso.DBUS_BUS_NAME,
302
 
                              ubuntu_sso.DBUS_CREDENTIALS_PATH))
303
 
            raise
304
 
 
305
 
        ControlPanelMixin.__init__(self, filename='overview.ui',
306
 
                                   backend_instance=sso_backend)
307
 
        self.add(self.itself)
308
 
        self.warning_label.set_text('')
309
 
        self.warning_label.set_property('xalign', 0.5)
310
 
 
311
 
        self.connect_button.set_uri(self.CONNECT)
312
 
 
313
 
        self.main_window = main_window
314
 
        self._credentials_are_new = False
315
 
        self.show()
316
 
 
317
 
        self.backend.connect_to_signal('CredentialsFound',
318
 
                                       self.on_credentials_found)
319
 
        self.backend.connect_to_signal('CredentialsNotFound',
320
 
                                       self.on_credentials_not_found)
321
 
        self.backend.connect_to_signal('CredentialsError',
322
 
                                       self.on_credentials_error)
323
 
        self.backend.connect_to_signal('AuthorizationDenied',
324
 
                                       self.on_authorization_denied)
325
 
 
326
 
        kw = dict(result_cb=self.on_network_state_changed)
327
 
        self.network_manager_state = networkstate.NetworkManagerState(**kw)
328
 
        self.network_manager_state.find_online_state()
329
 
 
330
 
    def _set_warning(self, message, label=None):
331
 
        """Set 'message' as global warning."""
332
 
        ControlPanelMixin._set_warning(self, message,
333
 
                                       label=self.warning_label)
334
 
 
335
 
    def set_property(self, prop_name, new_value):
336
 
        """Override 'set_property' to disable buttons if prop is 'greyed'."""
337
 
        if prop_name == 'greyed':
338
 
            self.set_sensitive(not new_value)
339
 
        GreyableBin.set_property(self, prop_name, new_value)
340
 
 
341
 
    def set_sensitive(self, value):
342
 
        """Set the sensitiveness as per 'value'."""
343
 
        self.join_now_button.set_sensitive(value)
344
 
        self.connect_button.set_sensitive(value)
345
 
 
346
 
    def get_sensitive(self):
347
 
        """Return the sensitiveness."""
348
 
        result = self.join_now_button.get_sensitive() and \
349
 
                 self.connect_button.get_sensitive()
350
 
        return result
351
 
 
352
 
    def on_join_now_button_clicked(self, *a, **kw):
353
 
        """User wants to join now."""
354
 
        settings = {TC_URL_KEY: U1_TC_URL, HELP_TEXT_KEY: U1_DESCRIPTION,
355
 
                    WINDOW_ID_KEY: str(self.main_window.window.xid),
356
 
                    PING_URL_KEY: U1_PING_URL}
357
 
        self.backend.register(U1_APP_NAME, settings,
358
 
            reply_handler=NO_OP, error_handler=error_handler)
359
 
        self.set_property('greyed', True)
360
 
        self.warning_label.set_text('')
361
 
 
362
 
    def on_connect_button_clicked(self, *a, **kw):
363
 
        """User wants to connect now."""
364
 
        settings = {TC_URL_KEY: U1_TC_URL, HELP_TEXT_KEY: U1_DESCRIPTION,
365
 
                    WINDOW_ID_KEY: str(self.main_window.window.xid),
366
 
                    PING_URL_KEY: U1_PING_URL}
367
 
        self.backend.login(U1_APP_NAME, settings,
368
 
            reply_handler=NO_OP, error_handler=error_handler)
369
 
        self.set_property('greyed', True)
370
 
        self.warning_label.set_text('')
371
 
 
372
 
    def on_learn_more_button_clicked(self, *a, **kw):
373
 
        """User wants to learn more."""
374
 
        uri_hook(self.learn_more_button, self.LEARN_MORE_LINK)
375
 
 
376
 
    @filter_by_app_name
377
 
    @log_call(logger.info, with_args=False)
378
 
    def on_credentials_found(self, app_name, credentials):
379
 
        """SSO backend notifies of credentials found."""
380
 
        self.set_property('greyed', False)
381
 
        self.emit('credentials-found', self._credentials_are_new, credentials)
382
 
 
383
 
    @filter_by_app_name
384
 
    @log_call(logger.info)
385
 
    def on_credentials_not_found(self, app_name):
386
 
        """SSO backend notifies of credentials not found."""
387
 
        self._credentials_are_new = True
388
 
        self.set_property('greyed', False)
389
 
 
390
 
    @filter_by_app_name
391
 
    @log_call(logger.error)
392
 
    def on_credentials_error(self, app_name, error_dict):
393
 
        """SSO backend notifies of an error when fetching credentials."""
394
 
        self.set_property('greyed', False)
395
 
        self._set_warning(self.CREDENTIALS_ERROR)
396
 
 
397
 
    @filter_by_app_name
398
 
    @log_call(logger.info)
399
 
    def on_authorization_denied(self, app_name):
400
 
        """SSO backend notifies that user refused auth for 'app_name'."""
401
 
        self.set_property('greyed', False)
402
 
 
403
 
    @log_call(logger.info)
404
 
    def on_network_state_changed(self, state):
405
 
        """Network state is reported."""
406
 
        msg = ''
407
 
        if state is networkstate.OFFLINE:
408
 
            msg = self.NETWORK_OFFLINE % {'app_name': U1_APP_NAME}
409
 
            self.set_sensitive(False)
410
 
            self._set_warning(msg)
411
 
        else:
412
 
            self.set_sensitive(True)
413
 
            self.warning_label.set_text(msg)
414
 
            self.backend.find_credentials(U1_APP_NAME, {},
415
 
                reply_handler=NO_OP, error_handler=error_handler)
416
 
 
417
 
 
418
 
class DashboardPanel(UbuntuOneBin, ControlPanelMixin):
419
 
    """The dashboard panel. The user can manage the subscription."""
420
 
 
421
 
    TITLE = _('Welcome to Ubuntu One!')
422
 
    VALUE_ERROR = _('The information could not be retrieved. '
423
 
        'Maybe your internet connection is down?')
424
 
 
425
 
    def __init__(self, main_window=None):
426
 
        UbuntuOneBin.__init__(self)
427
 
        ControlPanelMixin.__init__(self, filename='dashboard.ui')
428
 
        self.add(self.itself)
429
 
        self.show()
430
 
 
431
 
        self.is_processing = True
432
 
 
433
 
        self.backend.connect_to_signal('AccountInfoReady',
434
 
                                       self.on_account_info_ready)
435
 
        self.backend.connect_to_signal('AccountInfoError',
436
 
                                       self.on_account_info_error)
437
 
        self.account.hide()
438
 
 
439
 
    @log_call(logger.debug)
440
 
    def on_account_info_ready(self, info):
441
 
        """Backend notifies of account info."""
442
 
        self.on_success()
443
 
 
444
 
        for i in (u'name', u'type', u'email'):
445
 
            label = getattr(self, '%s_label' % i)
446
 
            label.set_markup('%s' % (info[i]))
447
 
        self.account.show()
448
 
 
449
 
        self.is_processing = False
450
 
 
451
 
    @log_call(logger.error)
452
 
    def on_account_info_error(self, error_dict=None):
453
 
        """Backend notifies of an error when fetching account info."""
454
 
        self.on_error(message=self.VALUE_ERROR)
455
 
        self.is_processing = False
456
 
 
457
 
 
458
 
class VolumesPanel(UbuntuOneBin, ControlPanelMixin):
459
 
    """The volumes panel."""
460
 
 
461
 
    TITLE = _('Select which folders from your cloud you want to sync with '
462
 
              'this computer')
463
 
    MY_FOLDERS = _('My folders')
464
 
    ALWAYS_SUBSCRIBED = _('Always in sync!')
465
 
    FREE_SPACE_TEXT = _('%(free_space)s available storage')
466
 
    NO_VOLUMES = _('No folders to show.')
467
 
    NAME_NOT_SET = _('[unknown user name]')
468
 
    CONFIRM_MERGE = _('The contents of your cloud folder will be merged with '
469
 
                      'your local folder "%(folder_path)s" when subscribing.\n'
470
 
                      'Do you want to subscribe to this cloud folder?')
471
 
    MUSIC_DISPLAY_NAME = _('Purchased Music')
472
 
    MUSIC_REAL_PATH = '~/.ubuntuone/Purchased from Ubuntu One'
473
 
 
474
 
    MAX_COLS = 8
475
 
    MIN_SIZE_FULL = 1048576
476
 
 
477
 
    CONTACT_ICON_NAME = 'avatar-default'
478
 
    FOLDER_ICON_NAME = 'folder'
479
 
    SHARE_ICON_NAME = 'folder-remote'
480
 
    MUSIC_ICON_NAME = 'audio-x-generic'
481
 
    FREE_SPACE = '<span foreground="grey">%s</span>' % FREE_SPACE_TEXT
482
 
    NO_FREE_SPACE = '<span foreground="red"><b>%s</b></span>' % FREE_SPACE_TEXT
483
 
    ROW_HEADER = '<span font_size="large"><b>%s</b></span> %s'
484
 
    ROOT = '%s - <span foreground="%s" font_size="small">%s</span>'
485
 
 
486
 
    def __init__(self, main_window=None):
487
 
        UbuntuOneBin.__init__(self)
488
 
        ControlPanelMixin.__init__(self, filename='volumes.ui')
489
 
        self.add(self.itself)
490
 
        self.show_all()
491
 
 
492
 
        kw = dict(parent=main_window,
493
 
                  flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
494
 
                  type=gtk.MESSAGE_WARNING,
495
 
                  buttons=gtk.BUTTONS_YES_NO)
496
 
        self.confirm_dialog = gtk.MessageDialog(**kw)
497
 
 
498
 
        # name, subscribed, icon name, show toggle, sensitive, icon size,
499
 
        # id, path
500
 
        self._empty_row = ('', False, '', False, False, gtk.ICON_SIZE_MENU,
501
 
                           None, None)
502
 
 
503
 
        self.backend.connect_to_signal('VolumesInfoReady',
504
 
                                       self.on_volumes_info_ready)
505
 
        self.backend.connect_to_signal('VolumesInfoError',
506
 
                                       self.on_volumes_info_error)
507
 
        self.backend.connect_to_signal('VolumeSettingsChanged',
508
 
                                       self.on_volume_settings_changed)
509
 
        self.backend.connect_to_signal('VolumeSettingsChangeError',
510
 
                                       self.on_volume_settings_change_error)
511
 
 
512
 
    def _process_path(self, path):
513
 
        """Trim 'path' so the '~' is removed."""
514
 
        home = os.path.expanduser('~')
515
 
        music_path = os.path.expanduser(self.MUSIC_REAL_PATH)
516
 
 
517
 
        if path == music_path:
518
 
            result = self.MUSIC_DISPLAY_NAME
519
 
        else:
520
 
            result = path.replace(os.path.join(home, ''), '')
521
 
 
522
 
        return result
523
 
 
524
 
    def on_volumes_info_ready(self, info):
525
 
        """Backend notifies of volumes info."""
526
 
 
527
 
        self.volumes_store.clear()
528
 
        if not info:
529
 
            self.on_success(self.NO_VOLUMES)
530
 
            return
531
 
        else:
532
 
            self.on_success()
533
 
 
534
 
        for name, free_bytes, volumes in info:
535
 
            if backend.ControlBackend.NAME_NOT_SET in name:
536
 
                name = self.NAME_NOT_SET
537
 
 
538
 
            if name:
539
 
                name = name + "'s"
540
 
                # we already added user folders, let's add an empty row
541
 
                treeiter = self.volumes_store.append(None, self._empty_row)
542
 
            else:
543
 
                name = self.MY_FOLDERS
544
 
 
545
 
            scroll_to_cell = False
546
 
            if free_bytes == backend.ControlBackend.FREE_BYTES_NOT_AVAILABLE:
547
 
                free_bytes = ''
548
 
            else:
549
 
                free_bytes = int(free_bytes)
550
 
                if free_bytes < self.MIN_SIZE_FULL:
551
 
                    free_bytes_str = self.NO_FREE_SPACE
552
 
                    scroll_to_cell = True
553
 
                else:
554
 
                    free_bytes_str = self.FREE_SPACE
555
 
                free_bytes_args = {'free_space': self.humanize(free_bytes)}
556
 
                free_bytes = free_bytes_str % free_bytes_args
557
 
 
558
 
            row = (self.ROW_HEADER % (name, free_bytes),
559
 
                   True, self.CONTACT_ICON_NAME, False, False,
560
 
                   gtk.ICON_SIZE_LARGE_TOOLBAR, None, None)
561
 
            treeiter = self.volumes_store.append(None, row)
562
 
 
563
 
            if scroll_to_cell:
564
 
                path = self.volumes_store.get_string_from_iter(treeiter)
565
 
                self.volumes_view.scroll_to_cell(path)
566
 
 
567
 
            volumes.sort(key=operator.itemgetter('path'))
568
 
            for volume in volumes:
569
 
                sensitive = True
570
 
                name = self._process_path(volume[u'path'])
571
 
                icon_name = self.FOLDER_ICON_NAME
572
 
 
573
 
                is_root = volume[u'type'] == backend.ControlBackend.ROOT_TYPE
574
 
                is_share = volume[u'type'] == backend.ControlBackend.SHARE_TYPE
575
 
 
576
 
                if is_root:
577
 
                    sensitive = False
578
 
                    name = self.ROOT % (name, ORANGE, self.ALWAYS_SUBSCRIBED)
579
 
                elif is_share:
580
 
                    name = volume[u'name']
581
 
                    icon_name = self.SHARE_ICON_NAME
582
 
                elif name == self.MUSIC_DISPLAY_NAME:
583
 
                    icon_name = self.MUSIC_ICON_NAME
584
 
 
585
 
                if volume[u'path'] is None:
586
 
                    logger.warning('on_volumes_info_ready: about to store a '
587
 
                                   'volume with None path: %r', volume)
588
 
 
589
 
                row = (name, bool(volume[u'subscribed']), icon_name, True,
590
 
                       sensitive, gtk.ICON_SIZE_MENU, volume['volume_id'],
591
 
                       volume[u'path'])
592
 
 
593
 
                if is_root:  # root should go first!
594
 
                    self.volumes_store.prepend(treeiter, row)
595
 
                else:
596
 
                    self.volumes_store.append(treeiter, row)
597
 
 
598
 
        self.volumes_view.expand_all()
599
 
        self.volumes_view.show_all()
600
 
 
601
 
        self.is_processing = False
602
 
 
603
 
    @log_call(logger.error)
604
 
    def on_volumes_info_error(self, error_dict=None):
605
 
        """Backend notifies of an error when fetching volumes info."""
606
 
        self.on_error(error_dict=error_dict)
607
 
 
608
 
    @log_call(logger.info)
609
 
    def on_volume_settings_changed(self, volume_id):
610
 
        """The settings for 'volume_id' were changed."""
611
 
        self.is_processing = False
612
 
 
613
 
    @log_call(logger.error)
614
 
    def on_volume_settings_change_error(self, volume_id, error_dict=None):
615
 
        """The settings for 'volume_id' were not changed."""
616
 
        self.load()
617
 
 
618
 
    def on_subscribed_toggled(self, widget, path, *args, **kwargs):
619
 
        """The user toggled 'widget'."""
620
 
        treeiter = self.volumes_store.get_iter(path)
621
 
        volume_id = self.volumes_store.get_value(treeiter, 6)
622
 
        volume_path = self.volumes_store.get_value(treeiter, 7)
623
 
        subscribed = self.volumes_store.get_value(treeiter, 1)
624
 
 
625
 
        response = gtk.RESPONSE_YES
626
 
        if not subscribed and os.path.exists(volume_path):
627
 
            self.confirm_dialog.set_markup(self.CONFIRM_MERGE %
628
 
                                           {'folder_path': volume_path})
629
 
            response = self.confirm_dialog.run()
630
 
            self.confirm_dialog.hide()
631
 
 
632
 
        if response == gtk.RESPONSE_YES:
633
 
            subscribed = not subscribed
634
 
            self.volumes_store.set_value(treeiter, 1, subscribed)
635
 
            self.backend.change_volume_settings(volume_id,
636
 
                {'subscribed': bool_str(subscribed)},
637
 
                reply_handler=NO_OP, error_handler=error_handler)
638
 
 
639
 
            self.is_processing = True
640
 
 
641
 
    def on_volumes_view_row_activated(self, widget, path, *args, **kwargs):
642
 
        """The user double clicked on a row."""
643
 
        treeiter = self.volumes_store.get_iter(path)
644
 
        volume_path = self.volumes_store.get_value(treeiter, 7)
645
 
        if volume_path is None:
646
 
            logger.warning('on_volumes_view_row_activated: volume_path for '
647
 
                           'tree_path %r is None', path)
648
 
        elif not os.path.exists(volume_path):
649
 
            logger.warning('on_volumes_view_row_activated: path %r '
650
 
                           'does not exist', volume_path)
651
 
        else:
652
 
            uri_hook(None, FILE_URI_PREFIX + volume_path)
653
 
 
654
 
    def load(self):
655
 
        """Load the volume list."""
656
 
        self.backend.volumes_info(reply_handler=NO_OP,
657
 
                                  error_handler=error_handler)
658
 
        self.is_processing = True
659
 
 
660
 
 
661
 
class SharesPanel(UbuntuOneBin, ControlPanelMixin):
662
 
    """The shares panel - NOT IMPLEMENTED YET."""
663
 
 
664
 
    TITLE = _('Manage permissions for shares made to other users.')
665
 
 
666
 
    def __init__(self, main_window=None):
667
 
        UbuntuOneBin.__init__(self)
668
 
        ControlPanelMixin.__init__(self)
669
 
        self.show_all()
670
 
        self.on_success('Not implemented yet.')
671
 
 
672
 
 
673
 
class Device(gtk.EventBox, ControlPanelMixin):
674
 
    """The device widget."""
675
 
 
676
 
    DEVICE_CHANGE_ERROR = _('The settings could not be changed,\n'
677
 
                            'previous values were restored.')
678
 
    DEVICE_REMOVAL_ERROR = _('The device could not be removed.')
679
 
    REMOVABLE_PREFIX = 'Ubuntu One @ '
680
 
 
681
 
    def __init__(self, confirm_remove_dialog=None):
682
 
        gtk.EventBox.__init__(self)
683
 
        ControlPanelMixin.__init__(self, filename='device.ui')
684
 
 
685
 
        self.confirm_dialog = confirm_remove_dialog
686
 
        self._updating = False
687
 
        self._last_settings = {}
688
 
        self.id = None
689
 
        self.is_local = False
690
 
        self.configurable = False
691
 
 
692
 
        self.update(device_id=None, device_name='',
693
 
                    is_local=False, configurable=False, limit_bandwidth=False,
694
 
                    max_upload_speed=0, max_download_speed=0,
695
 
                    show_all_notifications=True)
696
 
 
697
 
        self.add(self.itself)
698
 
        self.show()
699
 
 
700
 
        self.backend.connect_to_signal('DeviceSettingsChanged',
701
 
                                       self.on_device_settings_changed)
702
 
        self.backend.connect_to_signal('DeviceSettingsChangeError',
703
 
                                       self.on_device_settings_change_error)
704
 
        self.backend.connect_to_signal('DeviceRemoved',
705
 
                                       self.on_device_removed)
706
 
        self.backend.connect_to_signal('DeviceRemovalError',
707
 
                                       self.on_device_removal_error)
708
 
 
709
 
    def _change_device_settings(self, *args):
710
 
        """Update backend settings for this device."""
711
 
        if self._updating:
712
 
            return
713
 
 
714
 
        # Not disabling the GUI to avoid annyong twitchings
715
 
        #self.set_sensitive(False)
716
 
        self.warning_label.set_text('')
717
 
        self.backend.change_device_settings(self.id, self.__dict__,
718
 
            reply_handler=NO_OP, error_handler=error_handler)
719
 
 
720
 
    def _block_signals(f):
721
 
        """Execute 'f' while having the _updating flag set."""
722
 
 
723
 
        # pylint: disable=E0213,W0212,E1102
724
 
 
725
 
        @wraps(f)
726
 
        def inner(self, *args, **kwargs):
727
 
            """Execute 'f' while having the _updating flag set."""
728
 
            old = self._updating
729
 
            self._updating = True
730
 
 
731
 
            result = f(self, *args, **kwargs)
732
 
 
733
 
            self._updating = old
734
 
            return result
735
 
 
736
 
        return inner
737
 
 
738
 
    on_show_all_notifications_toggled = _change_device_settings
739
 
    on_max_upload_speed_value_changed = _change_device_settings
740
 
    on_max_download_speed_value_changed = _change_device_settings
741
 
 
742
 
    def on_limit_bandwidth_toggled(self, *args, **kwargs):
743
 
        """The limit bandwidth checkbox was toggled."""
744
 
        self.throttling_limits.set_sensitive(self.limit_bandwidth.get_active())
745
 
        self._change_device_settings()
746
 
 
747
 
    def on_remove_clicked(self, widget):
748
 
        """Remove button was clicked or activated."""
749
 
        response = gtk.RESPONSE_YES
750
 
        if self.confirm_dialog is not None:
751
 
            response = self.confirm_dialog.run()
752
 
            self.confirm_dialog.hide()
753
 
 
754
 
        if response == gtk.RESPONSE_YES:
755
 
            self.backend.remove_device(self.id,
756
 
                reply_handler=NO_OP, error_handler=error_handler)
757
 
            self.set_sensitive(False)
758
 
 
759
 
    @_block_signals
760
 
    def update(self, **kwargs):
761
 
        """Update according to named parameters.
762
 
 
763
 
        Possible settings are:
764
 
            * device_id (string, not shown to the user)
765
 
            * device_name (string)
766
 
            * type (either DEVICE_TYPE_PHONE or DEVICE_TYPE_COMPUTER)
767
 
            * is_local (True/False)
768
 
            * configurable (True/False)
769
 
            * if configurable, the following can be set:
770
 
                * show_all_notifications (True/False)
771
 
                * limit_bandwidth (True/False)
772
 
                * max_upload_speed (bytes)
773
 
                * max_download_speed (bytes)
774
 
 
775
 
        """
776
 
        if 'device_id' in kwargs:
777
 
            self.id = kwargs['device_id']
778
 
 
779
 
        if 'device_name' in kwargs:
780
 
            name = kwargs['device_name'].replace(self.REMOVABLE_PREFIX, '')
781
 
            name = '<span font_size="large"><b>%s</b></span>' % name
782
 
            self.device_name.set_markup(name)
783
 
 
784
 
        if 'device_type' in kwargs:
785
 
            dtype = kwargs['device_type']
786
 
            if dtype in (DEVICE_TYPE_COMPUTER, DEVICE_TYPE_PHONE):
787
 
                self.device_type.set_from_icon_name(dtype.lower(),
788
 
                    gtk.ICON_SIZE_LARGE_TOOLBAR)
789
 
 
790
 
        if 'is_local' in kwargs:
791
 
            self.is_local = bool(kwargs['is_local'])
792
 
 
793
 
        if 'configurable' in kwargs:
794
 
            self.configurable = bool(kwargs['configurable'])
795
 
            self.config_settings.set_visible(self.configurable)
796
 
 
797
 
        if 'show_all_notifications' in kwargs:
798
 
            value = bool(kwargs['show_all_notifications'])
799
 
            self.show_all_notifications.set_active(value)
800
 
 
801
 
        if 'limit_bandwidth' in kwargs:
802
 
            enabled = bool(kwargs['limit_bandwidth'])
803
 
            self.limit_bandwidth.set_active(enabled)
804
 
            self.throttling_limits.set_sensitive(enabled)
805
 
 
806
 
        for speed in ('max_upload_speed', 'max_download_speed'):
807
 
            if speed in kwargs:
808
 
                value = int(kwargs[speed]) // KILOBYTES
809
 
                getattr(self, speed).set_value(value)
810
 
 
811
 
        self._last_settings = self.__dict__
812
 
 
813
 
    @property
814
 
    def __dict__(self):
815
 
        result = {
816
 
            'device_id': self.id,
817
 
            'device_name': self.device_name.get_text(),
818
 
            'device_type': self.device_type.get_icon_name()[0].capitalize(),
819
 
            'is_local': bool_str(self.is_local),
820
 
            'configurable': bool_str(self.configurable),
821
 
            'show_all_notifications': \
822
 
                bool_str(self.show_all_notifications.get_active()),
823
 
            'limit_bandwidth': bool_str(self.limit_bandwidth.get_active()),
824
 
            'max_upload_speed': \
825
 
                str(self.max_upload_speed.get_value_as_int() * KILOBYTES),
826
 
            'max_download_speed': \
827
 
                str(self.max_download_speed.get_value_as_int() * KILOBYTES),
828
 
        }
829
 
        return result
830
 
 
831
 
    @log_call(logger.info, with_args=False)
832
 
    def on_device_settings_changed(self, device_id):
833
 
        """The change of this device settings succeded."""
834
 
        if device_id != self.id:
835
 
            return
836
 
        self.set_sensitive(True)
837
 
        self.warning_label.set_text('')
838
 
        self._last_settings = self.__dict__
839
 
 
840
 
    @log_call(logger.error)
841
 
    def on_device_settings_change_error(self, device_id, error_dict=None):
842
 
        """The change of this device settings failed."""
843
 
        if device_id != self.id:
844
 
            return
845
 
        self.update(**self._last_settings)
846
 
        self._set_warning(self.DEVICE_CHANGE_ERROR, self.warning_label)
847
 
        self.set_sensitive(True)
848
 
 
849
 
    # is safe to log the device_id since it was already removed
850
 
    @log_call(logger.warning)
851
 
    def on_device_removed(self, device_id):
852
 
        """The removal of this device succeded."""
853
 
        if device_id != self.id:
854
 
            return
855
 
        self.hide()
856
 
 
857
 
    @log_call(logger.error)
858
 
    def on_device_removal_error(self, device_id, error_dict=None):
859
 
        """The removal of this device failed."""
860
 
        if device_id != self.id:
861
 
            return
862
 
        self._set_warning(self.DEVICE_REMOVAL_ERROR, self.warning_label)
863
 
        self.set_sensitive(True)
864
 
 
865
 
 
866
 
class DevicesPanel(UbuntuOneBin, ControlPanelMixin):
867
 
    """The devices panel."""
868
 
 
869
 
    __gsignals__ = {
870
 
        'local-device-removed': (gobject.SIGNAL_RUN_FIRST,
871
 
                                 gobject.TYPE_NONE, ()),
872
 
    }
873
 
 
874
 
    TITLE = _('The devices connected with your personal cloud are listed '
875
 
              'below.')
876
 
    NO_DEVICES = _('No devices to show.')
877
 
    CONFIRM_REMOVE = _('Are you sure you want to remove this device '
878
 
                       'from Ubuntu One?')
879
 
 
880
 
    def __init__(self, main_window=None):
881
 
        UbuntuOneBin.__init__(self)
882
 
        ControlPanelMixin.__init__(self, filename='devices.ui')
883
 
        self.add(self.itself)
884
 
        self.show()
885
 
 
886
 
        self._devices = {}
887
 
        kw = dict(parent=main_window,
888
 
                  flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
889
 
                  type=gtk.MESSAGE_WARNING,
890
 
                  buttons=gtk.BUTTONS_YES_NO,
891
 
                  message_format=self.CONFIRM_REMOVE)
892
 
        self.confirm_remove_dialog = gtk.MessageDialog(**kw)
893
 
 
894
 
        self.backend.connect_to_signal('DevicesInfoReady',
895
 
                                       self.on_devices_info_ready)
896
 
        self.backend.connect_to_signal('DevicesInfoError',
897
 
                                       self.on_devices_info_error)
898
 
        self.backend.connect_to_signal('DeviceRemoved',
899
 
                                       self.on_device_removed)
900
 
 
901
 
    @log_call(logger.info, with_args=False)
902
 
    def on_devices_info_ready(self, info):
903
 
        """Backend notifies of devices info."""
904
 
        for child in self.devices.get_children():
905
 
            self.devices.remove(child)
906
 
 
907
 
        if not info:
908
 
            self.on_success(self.NO_DEVICES)
909
 
        else:
910
 
            self.on_success()
911
 
 
912
 
        odd_row_color = self.message.style.bg[gtk.STATE_NORMAL]
913
 
        for i, device_info in enumerate(info):
914
 
            device = Device(confirm_remove_dialog=self.confirm_remove_dialog)
915
 
            device_info['device_name'] = device_info.pop('name', '')
916
 
            device_info['device_type'] = device_info.pop('type',
917
 
                                                         DEVICE_TYPE_COMPUTER)
918
 
            device.update(**device_info)
919
 
 
920
 
            if i % 2 == 1:
921
 
                device.modify_bg(gtk.STATE_NORMAL, odd_row_color)
922
 
 
923
 
            self.devices.pack_start(device)
924
 
            self._devices[device.id] = device
925
 
 
926
 
        self.is_processing = False
927
 
 
928
 
    @log_call(logger.error)
929
 
    def on_devices_info_error(self, error_dict=None):
930
 
        """Backend notifies of an error when fetching volumes info."""
931
 
        self.on_error(error_dict=error_dict)
932
 
        self.is_processing = False
933
 
 
934
 
    @log_call(logger.warning)
935
 
    def on_device_removed(self, device_id):
936
 
        """The removal of a device succeded."""
937
 
        if device_id in self._devices:
938
 
            child = self._devices.pop(device_id)
939
 
            self.devices.remove(child)
940
 
 
941
 
            if child.is_local:
942
 
                self.emit('local-device-removed')
943
 
 
944
 
    def load(self):
945
 
        """Load the device list."""
946
 
        self.backend.devices_info(reply_handler=NO_OP,
947
 
                                  error_handler=error_handler)
948
 
        self.is_processing = True
949
 
 
950
 
 
951
 
class InstallPackage(gtk.VBox, ControlPanelMixin):
952
 
    """A widget to process the install of a package."""
953
 
 
954
 
    __gsignals__ = {
955
 
        'finished': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
956
 
    }
957
 
 
958
 
    INSTALL_PACKAGE = _('You need to install the package <i>%(package_name)s'
959
 
                        '</i> in order to enable more sync services.')
960
 
    INSTALLING = _('Installation of <i>%(package_name)s</i> in progress')
961
 
    FAILED_INSTALL = _('<i>%(package_name)s</i> could not be installed')
962
 
    SUCCESS_INSTALL = _('<i>%(package_name)s</i> was successfully installed')
963
 
 
964
 
    def __init__(self, package_name, message=None):
965
 
        gtk.VBox.__init__(self)
966
 
        ControlPanelMixin.__init__(self, filename='install.ui')
967
 
        self.add(self.itself)
968
 
 
969
 
        self.package_name = package_name
970
 
        self.package_manager = package_manager.PackageManager()
971
 
        self.args = {'package_name': self.package_name}
972
 
        self.transaction = None
973
 
 
974
 
        self.progress_bar = None
975
 
 
976
 
        self.message = message
977
 
        if self.message is None:
978
 
            self.message = self.INSTALL_PACKAGE % self.args
979
 
        self.reset()
980
 
 
981
 
        self.show()
982
 
 
983
 
    def reset(self):
984
 
        """Reset this interface."""
985
 
        children = self.itself.get_children()
986
 
        if self.progress_bar in children:
987
 
            self.itself.remove(self.progress_bar)
988
 
        if self.install_button_box not in children:
989
 
            self.itself.pack_start(self.install_button_box)
990
 
        self.install_label.set_markup(self.message)
991
 
 
992
 
    @package_manager.inline_callbacks
993
 
    def on_install_button_clicked(self, button):
994
 
        """The install button was clicked."""
995
 
        try:
996
 
            # create the install transaction
997
 
            self.transaction = yield self.package_manager.install(
998
 
                                    self.package_name)
999
 
 
1000
 
            logger.debug('on_install_button_clicked: transaction is %r',
1001
 
                         self.transaction)
1002
 
            success = package_manager.aptdaemon.enums.EXIT_SUCCESS
1003
 
            if self.transaction == success:
1004
 
                self.on_install_finished(None, self.transaction)
1005
 
                return
1006
 
 
1007
 
            # create the progress bar and pack it to the box
1008
 
            self.progress_bar = package_manager.PackageManagerProgressBar(
1009
 
                                    self.transaction)
1010
 
            self.progress_bar.show()
1011
 
 
1012
 
            self.itself.remove(self.install_button_box)
1013
 
            self.itself.pack_start(self.progress_bar)
1014
 
 
1015
 
            self.transaction.connect('finished', self.on_install_finished)
1016
 
            self.install_label.set_markup(self.INSTALLING % self.args)
1017
 
            yield self.transaction.run()
1018
 
        except package_manager.aptdaemon.errors.NotAuthorizedError:
1019
 
            self.reset()
1020
 
        except:  # pylint: disable=W0702
1021
 
            logger.exception('on_install_button_clicked')
1022
 
            self._set_warning(self.FAILED_INSTALL % self.args,
1023
 
                              self.install_label)
1024
 
            if self.progress_bar is not None:
1025
 
                self.progress_bar.hide()
1026
 
 
1027
 
    @log_call(logger.info)
1028
 
    def on_install_finished(self, transaction, exit_code):
1029
 
        """The installation finished."""
1030
 
        if self.progress_bar is not None:
1031
 
            self.progress_bar.set_sensitive(False)
1032
 
 
1033
 
        logger.info('on_install_finished: installation of %r was %r',
1034
 
                    self.package_name, exit_code)
1035
 
        if exit_code != package_manager.aptdaemon.enums.EXIT_SUCCESS:
1036
 
            if hasattr(transaction, 'error'):
1037
 
                logger.error('transaction failed: %r', transaction.error)
1038
 
            self._set_warning(self.FAILED_INSTALL % self.args,
1039
 
                              self.install_label)
1040
 
        else:
1041
 
            self.install_label.set_markup(self.SUCCESS_INSTALL % self.args)
1042
 
            self.emit('finished')
1043
 
 
1044
 
 
1045
 
class Service(gtk.VBox, ControlPanelMixin):
1046
 
    """A service."""
1047
 
 
1048
 
    CHANGE_ERROR = _('The settings could not be changed,\n'
1049
 
                     'previous values were restored.')
1050
 
 
1051
 
    def __init__(self, service_id, name,
1052
 
                 container=None, check_button=None, action_button=None,
1053
 
                 *args, **kwargs):
1054
 
        gtk.VBox.__init__(self)
1055
 
        ControlPanelMixin.__init__(self)
1056
 
        self.id = service_id
1057
 
        self.container = container
1058
 
        self.check_button = check_button
1059
 
        self.action_button = action_button
1060
 
 
1061
 
        self.warning_label = gtk.Label()
1062
 
        self.pack_start(self.warning_label, expand=False)
1063
 
 
1064
 
        self.button = gtk.CheckButton(label=name)
1065
 
        self.pack_start(self.button, expand=False)
1066
 
 
1067
 
        self.show_all()
1068
 
 
1069
 
 
1070
 
class FileSyncService(Service):
1071
 
    """The file sync service."""
1072
 
 
1073
 
    FILES_SERVICE_NAME = _('File Sync')
1074
 
 
1075
 
    def __init__(self, container, check_button, action_button):
1076
 
        Service.__init__(self, service_id='file-sync',
1077
 
                         name=self.FILES_SERVICE_NAME,
1078
 
                         container=container,
1079
 
                         check_button=check_button,
1080
 
                         action_button=action_button)
1081
 
 
1082
 
        self.container.set_sensitive(False)
1083
 
 
1084
 
        self.backend.connect_to_signal('FileSyncStatusChanged',
1085
 
                                       self.on_file_sync_status_changed)
1086
 
        self.backend.connect_to_signal('FilesEnabled', self.on_files_enabled)
1087
 
        self.backend.connect_to_signal('FilesDisabled', self.on_files_disabled)
1088
 
 
1089
 
    @log_call(logger.debug)
1090
 
    def on_file_sync_status_changed(self, status):
1091
 
        """File Sync status changed."""
1092
 
        enabled = status != backend.FILE_SYNC_DISABLED
1093
 
        logger.info('FileSyncService: on_file_sync_status_changed: '
1094
 
                    'status %r, enabled? %r', status, enabled)
1095
 
        self.check_button.set_active(enabled)
1096
 
        # if service is disabled, disable the action_button
1097
 
        self.action_button.set_sensitive(enabled)
1098
 
 
1099
 
        if not self.container.is_sensitive():
1100
 
            # first time we're getting this event
1101
 
            self.check_button.connect('toggled', self.on_button_toggled)
1102
 
            self.container.set_sensitive(True)
1103
 
 
1104
 
    def on_files_enabled(self):
1105
 
        """Files service was enabled."""
1106
 
        self.on_file_sync_status_changed('enabled!')
1107
 
 
1108
 
    def on_files_disabled(self):
1109
 
        """Files service was disabled."""
1110
 
        self.on_file_sync_status_changed(backend.FILE_SYNC_DISABLED)
1111
 
 
1112
 
    @log_call(logger.debug)
1113
 
    def on_button_toggled(self, button):
1114
 
        """Button was toggled, exclude/replicate the service properly."""
1115
 
        logger.info('File Sync enabled? %r', self.check_button.get_active())
1116
 
        if self.check_button.get_active():
1117
 
            self.backend.enable_files(reply_handler=NO_OP,
1118
 
                                      error_handler=error_handler)
1119
 
        else:
1120
 
            self.backend.disable_files(reply_handler=NO_OP,
1121
 
                                       error_handler=error_handler)
1122
 
 
1123
 
    def load(self):
1124
 
        """Load the information."""
1125
 
        self.backend.file_sync_status(reply_handler=NO_OP,
1126
 
                                      error_handler=error_handler)
1127
 
 
1128
 
 
1129
 
class DesktopcouchService(Service):
1130
 
    """A desktopcouch service."""
1131
 
 
1132
 
    INSTALL_PACKAGE = _('Install the %(plugin_name)s for the sync service: '
1133
 
                        '%(service_name)s')
1134
 
 
1135
 
    def __init__(self, service_id, name, enabled,
1136
 
                 container, check_button,
1137
 
                 dependency=None, dependency_name=None):
1138
 
        Service.__init__(self, service_id, name,
1139
 
                         container, check_button, action_button=None)
1140
 
 
1141
 
        self.backend.connect_to_signal('ReplicationSettingsChanged',
1142
 
            self.on_replication_settings_changed)
1143
 
        self.backend.connect_to_signal('ReplicationSettingsChangeError',
1144
 
            self.on_replication_settings_change_error)
1145
 
 
1146
 
        self.check_button.set_active(enabled)
1147
 
 
1148
 
        self.dependency = None
1149
 
        if dependency is not None:
1150
 
            if dependency_name is None:
1151
 
                dependency_name = dependency
1152
 
            args = {'plugin_name': dependency_name, 'service_name': service_id}
1153
 
            message = self.INSTALL_PACKAGE % args
1154
 
            self.dependency = InstallPackage(dependency, message)
1155
 
            self.dependency.connect('finished', self.on_depedency_finished)
1156
 
 
1157
 
            self.container.pack_end(self.dependency, expand=False)
1158
 
            self.check_button.set_sensitive(False)
1159
 
 
1160
 
        self.check_button.connect('toggled', self.on_button_toggled)
1161
 
 
1162
 
    def on_depedency_finished(self, widget):
1163
 
        """The dependency was installed."""
1164
 
        self.check_button.set_sensitive(True)
1165
 
        self.container.remove(self.dependency)
1166
 
        self.dependency = None
1167
 
 
1168
 
    @log_call(logger.debug)
1169
 
    def on_button_toggled(self, button):
1170
 
        """Button was toggled, exclude/replicate the service properly."""
1171
 
        logger.info('Starting replication for %r? %r',
1172
 
                    self.id, self.check_button.get_active())
1173
 
 
1174
 
        args = {'enabled': bool_str(self.check_button.get_active())}
1175
 
        self.backend.change_replication_settings(self.id, args,
1176
 
            reply_handler=NO_OP, error_handler=error_handler)
1177
 
 
1178
 
    @log_call(logger.info)
1179
 
    def on_replication_settings_changed(self, replication_id):
1180
 
        """The change of settings for this replication succeded."""
1181
 
        if replication_id != self.id:
1182
 
            return
1183
 
        self.warning_label.set_text('')
1184
 
 
1185
 
    @log_call(logger.error)
1186
 
    def on_replication_settings_change_error(self, replication_id,
1187
 
                                             error_dict=None):
1188
 
        """The change of settings for this replication failed."""
1189
 
        if replication_id != self.id:
1190
 
            return
1191
 
        self.check_button.set_active(not self.check_button.get_active())
1192
 
        self._set_warning(self.CHANGE_ERROR, self.warning_label)
1193
 
 
1194
 
 
1195
 
class ServicesPanel(UbuntuOneBin, ControlPanelMixin):
1196
 
    """The services panel."""
1197
 
 
1198
 
    TITLE = _('Enable the sync services for this computer.')
1199
 
    DESKTOPCOUCH_PKG = 'desktopcouch-ubuntuone'
1200
 
    BOOKMARKS = _('Firefox extension')
1201
 
    CONTACTS = _('Evolution plug-in')
1202
 
    NO_PAIRING_RECORD = _('There is no Ubuntu One pairing record.')
1203
 
    CONTACTS_LINK = 'https://one.ubuntu.com/'
1204
 
 
1205
 
    def __init__(self, main_window=None):
1206
 
        UbuntuOneBin.__init__(self)
1207
 
        ControlPanelMixin.__init__(self, filename='services.ui')
1208
 
        self.add(self.itself)
1209
 
 
1210
 
        self.plugin_names = {'contacts': self.CONTACTS,
1211
 
                              'bookmarks': self.BOOKMARKS}
1212
 
 
1213
 
        self.package_manager = package_manager.PackageManager()
1214
 
        self.install_box = None
1215
 
 
1216
 
        self._replications_ready = False  # hack to solve LP: #750309
1217
 
        self.backend.connect_to_signal('ReplicationsInfoReady',
1218
 
                                       self.on_replications_info_ready)
1219
 
        self.backend.connect_to_signal('ReplicationsInfoError',
1220
 
                                       self.on_replications_info_error)
1221
 
 
1222
 
        self.file_sync_service = FileSyncService(container=self.files,
1223
 
            check_button=self.file_sync_check,
1224
 
            action_button=self.file_sync_button)
1225
 
 
1226
 
        self.show()
1227
 
 
1228
 
    @property
1229
 
    def has_desktopcouch(self):
1230
 
        """Is desktopcouch installed?"""
1231
 
        return self.package_manager.is_installed(self.DESKTOPCOUCH_PKG)
1232
 
 
1233
 
    def on_file_sync_button_clicked(self, *args, **kwargs):
1234
 
        """The "Show me my U1 folder" button was clicked.
1235
 
 
1236
 
        XXX: this should be part of the FileSyncService widget.
1237
 
        XXX: the Ubuntu One folder should be the user's root.
1238
 
 
1239
 
        """
1240
 
        uri_hook(None, FILE_URI_PREFIX + os.path.expanduser('~/Ubuntu One'))
1241
 
 
1242
 
    def on_contacts_button_clicked(self, *args, **kwargs):
1243
 
        """The "Take me to the Ubuntu One website" button was clicked.
1244
 
 
1245
 
        XXX: this should be part of the DesktopcouchService widget.
1246
 
 
1247
 
        """
1248
 
        uri_hook(None, self.CONTACTS)
1249
 
 
1250
 
    def on_bookmarks_button_clicked(self, *args, **kwargs):
1251
 
        """The bookmarks button was clicked.
1252
 
 
1253
 
        XXX: this should be part of the DesktopcouchService widget.
1254
 
 
1255
 
        """
1256
 
 
1257
 
    @log_call(logger.debug)
1258
 
    def load(self):
1259
 
        """Load info."""
1260
 
        self.file_sync_service.load()
1261
 
        self.replications.hide()
1262
 
        if self.install_box is not None:
1263
 
            self.itself.remove(self.install_box)
1264
 
            self.install_box = None
1265
 
 
1266
 
        logger.info('load: has_desktopcouch? %r', self.has_desktopcouch)
1267
 
        if not self.has_desktopcouch:
1268
 
            self.message.set_text('')
1269
 
 
1270
 
            self.install_box = InstallPackage(self.DESKTOPCOUCH_PKG)
1271
 
            self.install_box.connect('finished', self.load_replications)
1272
 
            self.itself.pack_end(self.install_box, expand=False)
1273
 
            self.itself.reorder_child(self.install_box, 0)
1274
 
        else:
1275
 
            self.load_replications()
1276
 
 
1277
 
        self.message.stop()
1278
 
 
1279
 
    @log_call(logger.debug)
1280
 
    def load_replications(self, *args):
1281
 
        """Load replications info."""
1282
 
        self._replications_ready = False  # hack to solve LP: #750309
1283
 
        # ask replications to the backend
1284
 
        self.message.start()
1285
 
        self.backend.replications_info(reply_handler=NO_OP,
1286
 
                                       error_handler=error_handler)
1287
 
 
1288
 
    @log_call(logger.debug)
1289
 
    def on_replications_info_ready(self, info):
1290
 
        """The replication info is ready."""
1291
 
        self.on_success()
1292
 
 
1293
 
        self.replications.show()
1294
 
 
1295
 
        if self.install_box is not None:
1296
 
            self.itself.remove(self.install_box)
1297
 
            self.install_box = None
1298
 
 
1299
 
        for item in info:
1300
 
            pkg = item['dependency']
1301
 
            if not pkg or self.package_manager.is_installed(pkg):
1302
 
                pkg = None
1303
 
 
1304
 
            sid = item['replication_id']
1305
 
            container = getattr(self, sid, None)
1306
 
            check_button = getattr(self, '%s_check' % sid, None)
1307
 
            name = self.plugin_names.get(sid, None)
1308
 
            child = DesktopcouchService(service_id=sid, name=item['name'],
1309
 
                enabled=bool(item['enabled']), container=container,
1310
 
                check_button=check_button,
1311
 
                dependency=pkg, dependency_name=name)
1312
 
            setattr(self, '%s_service' % sid, child)
1313
 
            self._replications_ready = True  # hack to solve LP: #750309
1314
 
 
1315
 
    @log_call(logger.error)
1316
 
    def on_replications_info_error(self, error_dict=None):
1317
 
        """The replication info can not be retrieved."""
1318
 
        if error_dict is not None and \
1319
 
           error_dict.get('error_type', None) == 'NoPairingRecord':
1320
 
            self.on_error(self.NO_PAIRING_RECORD)
1321
 
        else:
1322
 
            self.on_error(error_dict=error_dict)
1323
 
 
1324
 
    def refresh(self):
1325
 
        """If replication list has been loaded, hide and show them."""
1326
 
        if self._replications_ready:  # hack to solve LP: #750309
1327
 
            self.replications.hide()
1328
 
            self.replications.show()
1329
 
 
1330
 
 
1331
 
class FileSyncStatus(gtk.HBox, ControlPanelMixin):
1332
 
    """A file sync status widget."""
1333
 
 
1334
 
    FILE_SYNC_DISABLED = _('File Sync is disabled.')
1335
 
    FILE_SYNC_STARTING = _('File Sync starting...')
1336
 
    FILE_SYNC_STOPPED = _('File Sync is stopped.')
1337
 
    FILE_SYNC_DISCONNECTED = _('File Sync is disconnected.')
1338
 
    FILE_SYNC_SYNCING = _('File Sync in progress...')
1339
 
    FILE_SYNC_IDLE = _('File Sync is up-to-date.')
1340
 
    FILE_SYNC_ERROR = _('File Sync error.')
1341
 
 
1342
 
    CONNECT = _('Connect')
1343
 
    DISCONNECT = _('Disconnect')
1344
 
    ENABLE = _('Enable')
1345
 
    RESTART = _('Restart')
1346
 
    START = _('Start')
1347
 
    STOP = _('Stop')
1348
 
 
1349
 
    CONNECT_TOOLTIP = _('Connect the file sync service '
1350
 
                        'with your personal cloud')
1351
 
    DISCONNECT_TOOLTIP = _('Disconnect the file sync service '
1352
 
                           'from your personal cloud')
1353
 
    ENABLE_TOOLTIP = _('Enable the file sync service')
1354
 
    RESTART_TOOLTIP = _('Restart the file sync service')
1355
 
    START_TOOLTIP = _('Start the file sync service')
1356
 
    STOP_TOOLTIP = _('Stop the file sync service')
1357
 
 
1358
 
    def __init__(self):
1359
 
        gtk.HBox.__init__(self)
1360
 
        ControlPanelMixin.__init__(self)
1361
 
 
1362
 
        self.label = LabelLoading(LOADING)
1363
 
        self.pack_start(self.label, expand=True)
1364
 
 
1365
 
        self.button = gtk.LinkButton(uri='')
1366
 
        self.button.connect('clicked', self._on_button_clicked)
1367
 
        self.pack_start(self.button, expand=False)
1368
 
 
1369
 
        self.show_all()
1370
 
 
1371
 
        self.backend.connect_to_signal('FileSyncStatusDisabled',
1372
 
                                       self.on_file_sync_status_disabled)
1373
 
        self.backend.connect_to_signal('FileSyncStatusStarting',
1374
 
                                       self.on_file_sync_status_starting)
1375
 
        self.backend.connect_to_signal('FileSyncStatusStopped',
1376
 
                                       self.on_file_sync_status_stopped)
1377
 
        self.backend.connect_to_signal('FileSyncStatusDisconnected',
1378
 
                                       self.on_file_sync_status_disconnected)
1379
 
        self.backend.connect_to_signal('FileSyncStatusSyncing',
1380
 
                                       self.on_file_sync_status_syncing)
1381
 
        self.backend.connect_to_signal('FileSyncStatusIdle',
1382
 
                                       self.on_file_sync_status_idle)
1383
 
        self.backend.connect_to_signal('FileSyncStatusError',
1384
 
                                       self.on_file_sync_status_error)
1385
 
        self.backend.connect_to_signal('FilesStartError',
1386
 
                                       self.on_files_start_error)
1387
 
        self.backend.connect_to_signal('FilesEnabled',
1388
 
                                       self.on_file_sync_status_starting)
1389
 
        self.backend.connect_to_signal('FilesDisabled',
1390
 
                                       self.on_file_sync_status_disabled)
1391
 
 
1392
 
    def _update_status(self, msg, action, callback,
1393
 
                       icon=None, color=None, tooltip=None):
1394
 
        """Update the status info."""
1395
 
        if icon is not None:
1396
 
            foreground = '' if color is None else 'foreground="%s"' % color
1397
 
            msg = '<span %s>%s</span> %s' % (foreground, icon, msg)
1398
 
        self.label.set_markup(msg)
1399
 
        self.label.stop()
1400
 
 
1401
 
        self.button.set_label(action)
1402
 
        self.button.set_uri(action)
1403
 
        self.button.set_sensitive(True)
1404
 
        self.button.set_data('callback', callback)
1405
 
        if tooltip is not None:
1406
 
            self.button.set_tooltip_text(tooltip)
1407
 
 
1408
 
    def _on_button_clicked(self, button):
1409
 
        """Button was clicked, act accordingly the label."""
1410
 
        button.set_visited(False)
1411
 
        button.set_sensitive(False)
1412
 
        button.get_data('callback')(button)
1413
 
 
1414
 
    @log_call(logger.info)
1415
 
    def on_file_sync_status_disabled(self, msg=None):
1416
 
        """Backend notifies of file sync status being disabled."""
1417
 
        self._update_status(self.FILE_SYNC_DISABLED,
1418
 
                            self.ENABLE, self.on_enable_clicked,
1419
 
                            '✘', 'red', self.ENABLE_TOOLTIP)
1420
 
 
1421
 
    @log_call(logger.info)
1422
 
    def on_file_sync_status_starting(self, msg=None):
1423
 
        """Backend notifies of file sync status being starting."""
1424
 
        self._update_status(self.FILE_SYNC_STARTING,
1425
 
                            self.STOP, self.on_stop_clicked,
1426
 
                            '⇅', ORANGE, self.STOP_TOOLTIP)
1427
 
 
1428
 
    @log_call(logger.info)
1429
 
    def on_file_sync_status_stopped(self, msg=None):
1430
 
        """Backend notifies of file sync being stopped."""
1431
 
        self._update_status(self.FILE_SYNC_STOPPED,
1432
 
                            self.START, self.on_start_clicked,
1433
 
                            '✘', 'red', self.START_TOOLTIP)
1434
 
 
1435
 
    @log_call(logger.info)
1436
 
    def on_file_sync_status_disconnected(self, msg=None):
1437
 
        """Backend notifies of file sync status being ready."""
1438
 
        self._update_status(self.FILE_SYNC_DISCONNECTED,
1439
 
                            self.CONNECT, self.on_connect_clicked,
1440
 
                            '✘', 'red', self.CONNECT_TOOLTIP,)
1441
 
 
1442
 
    @log_call(logger.info)
1443
 
    def on_file_sync_status_syncing(self, msg=None):
1444
 
        """Backend notifies of file sync status being syncing."""
1445
 
        self._update_status(self.FILE_SYNC_SYNCING,
1446
 
                            self.DISCONNECT, self.on_disconnect_clicked,
1447
 
                            '⇅', ORANGE, self.DISCONNECT_TOOLTIP)
1448
 
 
1449
 
    @log_call(logger.info)
1450
 
    def on_file_sync_status_idle(self, msg=None):
1451
 
        """Backend notifies of file sync status being idle."""
1452
 
        self._update_status(self.FILE_SYNC_IDLE,
1453
 
                            self.DISCONNECT, self.on_disconnect_clicked,
1454
 
                            '✔', 'green', self.DISCONNECT_TOOLTIP)
1455
 
 
1456
 
    @log_call(logger.error)
1457
 
    def on_file_sync_status_error(self, error_dict=None):
1458
 
        """Backend notifies of an error when fetching file sync status."""
1459
 
        msg = self.FILE_SYNC_ERROR
1460
 
        reason = error_dict.get('error_msg', '') if error_dict else ''
1461
 
        if reason:
1462
 
            msg += ' (' + reason + ')'
1463
 
        self._update_status(WARNING_MARKUP % msg,
1464
 
                            self.RESTART, self.on_restart_clicked,
1465
 
                            tooltip=self.RESTART_TOOLTIP)
1466
 
 
1467
 
    @log_call(logger.error)
1468
 
    def on_files_start_error(self, error_dict=None):
1469
 
        """Backend notifies of an error when starting the files service."""
1470
 
        # service is probably disabled, ask for status to backend
1471
 
        self.backend.file_sync_status(reply_handler=NO_OP,
1472
 
                                      error_handler=error_handler)
1473
 
 
1474
 
    def on_connect_clicked(self, button=None):
1475
 
        """User requested connection."""
1476
 
        self.backend.connect_files(reply_handler=NO_OP,
1477
 
                                   error_handler=error_handler)
1478
 
 
1479
 
    def on_disconnect_clicked(self, button=None):
1480
 
        """User requested disconnection."""
1481
 
        self.backend.disconnect_files(reply_handler=NO_OP,
1482
 
                                      error_handler=error_handler)
1483
 
 
1484
 
    def on_enable_clicked(self, button=None):
1485
 
        """User requested enable the service."""
1486
 
        self.backend.enable_files(reply_handler=NO_OP,
1487
 
                                  error_handler=error_handler)
1488
 
 
1489
 
    def on_restart_clicked(self, button=None):
1490
 
        """User requested restart the service."""
1491
 
        self.backend.restart_files(reply_handler=NO_OP,
1492
 
                                   error_handler=error_handler)
1493
 
 
1494
 
    def on_start_clicked(self, button=None):
1495
 
        """User requested start the service."""
1496
 
        self.backend.start_files(reply_handler=NO_OP,
1497
 
                                 error_handler=error_handler)
1498
 
 
1499
 
    def on_stop_clicked(self, button=None):
1500
 
        """User requested stop the service."""
1501
 
        self.backend.stop_files(reply_handler=NO_OP,
1502
 
                                error_handler=error_handler)
1503
 
 
1504
 
    def load(self):
1505
 
        """Load the information."""
1506
 
        self.backend.file_sync_status(reply_handler=NO_OP,
1507
 
                                      error_handler=error_handler)
1508
 
 
1509
 
 
1510
 
class ManagementPanel(gtk.VBox, ControlPanelMixin):
1511
 
    """The management panel.
1512
 
 
1513
 
    The user can manage dashboard, volumes, devices and services.
1514
 
 
1515
 
    """
1516
 
 
1517
 
    __gsignals__ = {
1518
 
        'local-device-removed': (gobject.SIGNAL_RUN_FIRST,
1519
 
                                 gobject.TYPE_NONE, ()),
1520
 
        'unauthorized': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
1521
 
    }
1522
 
 
1523
 
    QUOTA_LABEL = _('Using %(used)s of %(total)s (%(percentage).0f%%)')
1524
 
    QUOTA_THRESHOLD = 0.95
1525
 
    DASHBOARD_BUTTON_NAME = 'ModeLeft'
1526
 
    SERVICES_BUTTON_NAME = 'ModeRight'
1527
 
 
1528
 
    DASHBOARD_BUTTON_TOOLTIP = _('View your personal details and service '
1529
 
                                 'summary')
1530
 
    VOLUMES_BUTTON_TOOLTIP = _('Manage your cloud folders')
1531
 
    SHARES_BUTTON_TOOLTIP = _('Manage the shares offered to others')
1532
 
    DEVICES_BUTTON_TOOLTIP = _('Manage devices registered with your personal '
1533
 
                               'cloud')
1534
 
    SERVICES_BUTTON_TOOLTIP = _('Manage the sync services')
1535
 
 
1536
 
    def __init__(self, main_window=None):
1537
 
        gtk.VBox.__init__(self)
1538
 
        ControlPanelMixin.__init__(self, filename='management.ui')
1539
 
        self.add(self.itself)
1540
 
        self.show()
1541
 
 
1542
 
        self.backend.connect_to_signal('AccountInfoReady',
1543
 
                                       self.on_account_info_ready)
1544
 
        self.backend.connect_to_signal('AccountInfoError',
1545
 
                                       self.on_account_info_error)
1546
 
        self.backend.connect_to_signal('UnauthorizedError',
1547
 
                                       self.on_unauthorized_error)
1548
 
 
1549
 
        self.quota_progressbar.set_sensitive(False)
1550
 
 
1551
 
        self.quota_label = LabelLoading(LOADING)
1552
 
        self.quota_box.pack_start(self.quota_label, expand=False)
1553
 
        self.quota_box.reorder_child(self.quota_label, 0)
1554
 
 
1555
 
        self.status_label = FileSyncStatus()
1556
 
        self.status_box.pack_end(self.status_label, expand=True)
1557
 
 
1558
 
        self.dashboard = DashboardPanel(main_window=main_window)
1559
 
        self.volumes = VolumesPanel(main_window=main_window)
1560
 
        self.shares = SharesPanel(main_window=main_window)
1561
 
        self.devices = DevicesPanel(main_window=main_window)
1562
 
        self.services = ServicesPanel(main_window=main_window)
1563
 
 
1564
 
        cb = lambda button, page_num: self.notebook.set_current_page(page_num)
1565
 
        self.tabs = (u'dashboard', u'volumes', u'shares',
1566
 
                     u'devices', u'services')
1567
 
        for page_num, tab in enumerate(self.tabs):
1568
 
            setattr(self, ('%s_page' % tab).upper(), page_num)
1569
 
            button = getattr(self, '%s_button' % tab)
1570
 
            button.connect('clicked', cb, page_num)
1571
 
            self.notebook.insert_page(getattr(self, tab), position=page_num)
1572
 
 
1573
 
        self.dashboard_button.set_name(self.DASHBOARD_BUTTON_NAME)
1574
 
        self.dashboard_button.set_tooltip_text(self.DASHBOARD_BUTTON_TOOLTIP)
1575
 
 
1576
 
        self.volumes_button.set_tooltip_text(self.VOLUMES_BUTTON_TOOLTIP)
1577
 
        self.volumes_button.connect('clicked', lambda b: self.volumes.load())
1578
 
 
1579
 
        self.shares_button.set_tooltip_text(self.SHARES_BUTTON_TOOLTIP)
1580
 
 
1581
 
        self.devices_button.set_tooltip_text(self.DEVICES_BUTTON_TOOLTIP)
1582
 
        self.devices_button.connect('clicked', lambda b: self.devices.load())
1583
 
        self.devices.connect('local-device-removed',
1584
 
                             lambda widget: self.emit('local-device-removed'))
1585
 
 
1586
 
        self.services_button.set_name(self.SERVICES_BUTTON_NAME)
1587
 
        self.services_button.set_tooltip_text(self.SERVICES_BUTTON_TOOLTIP)
1588
 
        self.services_button.connect('clicked',
1589
 
                                     lambda b: self.services.refresh())
1590
 
 
1591
 
        self.enable_volumes = lambda: self.volumes_button.set_sensitive(True)
1592
 
        self.disable_volumes = lambda: self.volumes_button.set_sensitive(False)
1593
 
        self.backend.connect_to_signal('FilesEnabled', self.enable_volumes)
1594
 
        self.backend.connect_to_signal('FilesDisabled', self.disable_volumes)
1595
 
 
1596
 
    def _update_quota(self, msg, data=None):
1597
 
        """Update the quota info."""
1598
 
        fraction = 0.0
1599
 
        if data is not None:
1600
 
            fraction = data.get('percentage', 0.0) / 100
1601
 
            if fraction > 0 and fraction < 0.05:
1602
 
                fraction = 0.05
1603
 
            else:
1604
 
                fraction = round(fraction, 2)
1605
 
 
1606
 
        logger.debug('ManagementPanel: updating quota to %r.', fraction)
1607
 
        if fraction >= self.QUOTA_THRESHOLD:
1608
 
            self.quota_label.set_markup(WARNING_MARKUP % msg)
1609
 
        else:
1610
 
            self.quota_label.set_markup(msg)
1611
 
        self.quota_label.stop()
1612
 
 
1613
 
        if fraction == 0.0:
1614
 
            self.quota_progressbar.set_sensitive(False)
1615
 
        else:
1616
 
            self.quota_progressbar.set_sensitive(True)
1617
 
 
1618
 
        self.quota_progressbar.set_fraction(min(fraction, 1))
1619
 
 
1620
 
    def load(self):
1621
 
        """Load the account info and file sync status list."""
1622
 
        self.backend.account_info(reply_handler=NO_OP,
1623
 
                                  error_handler=error_handler)
1624
 
        self.status_label.load()
1625
 
        self.services.load()
1626
 
 
1627
 
    @log_call(logger.debug)
1628
 
    def on_account_info_ready(self, info):
1629
 
        """Backend notifies of account info."""
1630
 
        used = int(info['quota_used'])
1631
 
        total = int(info['quota_total'])
1632
 
        data = {'used': self.humanize(used), 'total': self.humanize(total),
1633
 
                'percentage': (used / total) * 100}
1634
 
        self._update_quota(self.QUOTA_LABEL % data, data)
1635
 
 
1636
 
    @log_call(logger.error)
1637
 
    def on_account_info_error(self, error_dict=None):
1638
 
        """Backend notifies of an error when fetching account info."""
1639
 
        self._update_quota(msg='')
1640
 
 
1641
 
    @log_call(logger.error)
1642
 
    def on_unauthorized_error(self, error_dict=None):
1643
 
        """Backend notifies that credentials are not valid."""
1644
 
        self.emit('unauthorized')
1645
 
 
1646
 
 
1647
 
class ControlPanel(gtk.Notebook, ControlPanelMixin):
1648
 
    """The control panel per se, can be added into any other widget."""
1649
 
 
1650
 
    # should not be any larger than 736x525
1651
 
 
1652
 
    def __init__(self, main_window):
1653
 
        gtk.Notebook.__init__(self)
1654
 
        ControlPanelMixin.__init__(self)
1655
 
        gtk.link_button_set_uri_hook(uri_hook)
1656
 
        self.connect('destroy', self.shutdown)
1657
 
 
1658
 
        self.main_window = main_window
1659
 
 
1660
 
        self.set_show_tabs(False)
1661
 
        self.set_show_border(False)
1662
 
 
1663
 
        self.overview = OverviewPanel(main_window=main_window)
1664
 
        self.insert_page(self.overview, position=0)
1665
 
 
1666
 
        self.management = ManagementPanel(main_window=main_window)
1667
 
        self.insert_page(self.management, position=1)
1668
 
 
1669
 
        self.overview.connect('credentials-found',
1670
 
                              self.on_show_management_panel)
1671
 
        self.management.connect('local-device-removed',
1672
 
                                self.on_show_overview_panel)
1673
 
        self.management.connect('unauthorized',
1674
 
                                self.on_show_overview_panel)
1675
 
 
1676
 
        self.show()
1677
 
        self.on_show_overview_panel()
1678
 
 
1679
 
        logger.debug('%s: started (window size %r).',
1680
 
                     self.__class__.__name__, self.get_size_request())
1681
 
 
1682
 
    def shutdown(self, *args, **kwargs):
1683
 
        """Shutdown backend."""
1684
 
        logger.info('Shutting down...')
1685
 
        self.backend.shutdown(reply_handler=NO_OP,
1686
 
                              error_handler=error_handler)
1687
 
 
1688
 
    def on_show_overview_panel(self, widget=None):
1689
 
        """Show the overview panel."""
1690
 
        self.set_current_page(0)
1691
 
 
1692
 
    def on_show_management_panel(self, widget=None,
1693
 
                                 credentials_are_new=False, token=None):
1694
 
        """Show the notebook (main panel)."""
1695
 
        if self.get_current_page() == 0:
1696
 
            self.management.load()
1697
 
            if credentials_are_new:
1698
 
                # redirect user to services page to start using Ubuntu One
1699
 
                self.management.services_button.clicked()
1700
 
                # instruct syncdaemon to connect
1701
 
                self.backend.connect_files(reply_handler=NO_OP,
1702
 
                                           error_handler=error_handler)
1703
 
 
1704
 
            self.next_page()
1705
 
 
1706
 
 
1707
 
class ControlPanelService(dbus.service.Object):
1708
 
    """DBUS service that exposes some of the window's methods."""
1709
 
 
1710
 
    def __init__(self, window):
1711
 
        self.window = window
1712
 
        bus_name = dbus.service.BusName(
1713
 
            DBUS_BUS_NAME_GUI, bus=dbus.SessionBus())
1714
 
        dbus.service.Object.__init__(
1715
 
            self, bus_name=bus_name, object_path=DBUS_PATH_GUI)
1716
 
 
1717
 
    @log_call(logger.debug)
1718
 
    @dbus.service.method(dbus_interface=DBUS_IFACE_GUI, in_signature='sb')
1719
 
    def switch_to_alert(self, panel='', alert=False):
1720
 
        """Switch to named panel."""
1721
 
        if panel:
1722
 
            self.window.switch_to(panel)
1723
 
        if alert:
1724
 
            self.window.draw_attention()
1725
 
 
1726
 
 
1727
 
class ControlPanelWindow(gtk.Window):
1728
 
    """The main window for the Ubuntu One control panel."""
1729
 
 
1730
 
    TITLE = _('%(app_name)s Control Panel')
1731
 
 
1732
 
    def __init__(self, switch_to='', alert=False):
1733
 
        super(ControlPanelWindow, self).__init__()
1734
 
 
1735
 
        self.connect('focus-in-event', self.remove_urgency)
1736
 
        self.set_title(self.TITLE % {'app_name': U1_APP_NAME})
1737
 
        self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
1738
 
        self.set_icon_name('ubuntuone')
1739
 
        self.set_size_request(736, 525)  # bug #683164
1740
 
 
1741
 
        self.connect('delete-event', lambda w, e: gtk.main_quit())
1742
 
        if alert:
1743
 
            self.draw_attention()
1744
 
        else:
1745
 
            self.present()
1746
 
 
1747
 
        self.control_panel = ControlPanel(main_window=self)
1748
 
        self.add(self.control_panel)
1749
 
 
1750
 
        logger.info('Starting %s pointing at panel: %r.',
1751
 
                     self.__class__.__name__, switch_to)
1752
 
        if switch_to:
1753
 
            self.switch_to(switch_to)
1754
 
 
1755
 
        logger.debug('%s: started (window size %r).',
1756
 
                     self.__class__.__name__, self.get_size_request())
1757
 
 
1758
 
    def remove_urgency(self, *args, **kwargs):
1759
 
        """Remove urgency from the launcher entry."""
1760
 
        if not USE_LIBUNITY:
1761
 
            return
1762
 
        entry = Unity.LauncherEntry.get_for_desktop_id(U1_DOTDESKTOP)
1763
 
        if getattr(entry.props, 'urgent', False):
1764
 
            self.switch_to('volumes')
1765
 
            entry.props.urgent = False
1766
 
 
1767
 
    def draw_attention(self):
1768
 
        """Draw attention to the control panel."""
1769
 
        self.present_with_time(1)
1770
 
        self.set_urgency_hint(True)
1771
 
 
1772
 
    def switch_to(self, panel):
1773
 
        """Switch to named panel."""
1774
 
        button = getattr(
1775
 
            self.control_panel.management, '%s_button' % panel, None)
1776
 
        if button is not None:
1777
 
            button.clicked()
1778
 
        else:
1779
 
            logger.warning('Could not start at panel: %r.', panel)
1780
 
 
1781
 
    def main(self):
1782
 
        """Run the main loop of the widget toolkit."""
1783
 
        logger.debug('Starting GTK main loop.')
1784
 
        gtk.main()