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

« back to all changes in this revision

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