1
# -*- coding: utf-8 -*-
3
# Authors: Natalia B Bidart <natalia.bidart@canonical.com>
4
# Eric Casteleijn <eric.casteleijn@canonical.com>
6
# Copyright 2010 Canonical Ltd.
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.
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.
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/>.
20
"""The user interface for the control panel for Ubuntu One."""
22
from __future__ import division
28
from functools import wraps
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,
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
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
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)
61
from ubuntuone.controlpanel.gtk import package_manager, TRANSLATION_DOMAIN
64
from gi.repository import Unity # pylint: disable=E0611
66
U1_DOTDESKTOP = "ubuntuone-control-panel-gtk.desktop"
70
logger = setup_logging('gtk.gui')
74
# To be replaced by values from the theme or Ubuntu One' specific (LP: #673663)
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
82
NO_OP = lambda *a, **kw: None
83
FILE_URI_PREFIX = 'file://'
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)
91
def register_service(bus):
92
"""Try to register DBus service for making sure we run only one instance.
94
Return True if succesfully registered, False if already running.
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
101
def publish_service(window=None, switch_to='', alert=False):
102
"""Publish the service on DBus."""
104
window = ControlPanelWindow(switch_to=switch_to, alert=alert)
105
return ControlPanelService(window)
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)
115
obj = bus.get_object(DBUS_BUS_NAME_GUI, DBUS_PATH_GUI)
116
service = dbus.Interface(obj, dbus_interface=DBUS_IFACE_GUI)
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)
123
def gui_reply_handler(*args, **kwargs):
124
"""Exit when done."""
127
service.switch_to_alert(
128
switch_to, alert, reply_handler=gui_reply_handler,
129
error_handler=gui_error_handler)
134
def filter_by_app_name(f):
135
"""Excecute 'f' filtering by app_name."""
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)
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
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)
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)
161
class ControlPanelMixin(object):
162
"""The main interface for the Ubuntu One control panel."""
164
def __init__(self, filename=None, backend_instance=None):
165
if backend_instance is not None:
166
self.backend = backend_instance
168
bus = dbus.SessionBus()
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))
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)
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)
193
logger.warning("%s has no name (??)", obj)
195
setattr(self, name, obj)
197
logger.debug('%s: started.', self.__class__.__name__)
199
def humanize(self, int_bytes):
200
"""Return a human readble string of 'int_bytes'."""
201
return GLib.format_size_for_display(int_bytes)
203
def _set_warning(self, message, label):
204
"""Set 'message' as warning in 'label'."""
205
label.set_markup(WARNING_MARKUP % message)
209
class UbuntuOneBin(gtk.VBox):
210
"""A Ubuntu One bin."""
214
def __init__(self, title=None):
215
gtk.VBox.__init__(self)
216
self._is_processing = False
221
title = '<span font_size="large">%s</span>' % title
222
self.title = PanelTitle(markup=title)
223
self.pack_start(self.title, expand=False)
225
self.message = LabelLoading(LOADING)
226
self.pack_start(self.message, expand=False)
228
self.connect('size-allocate', on_size_allocate, self.title)
231
def _get_is_processing(self):
232
"""Is this panel processing a request?"""
233
return self._is_processing
235
def _set_is_processing(self, new_value):
236
"""Set if this panel is processing a request."""
239
self.set_sensitive(False)
242
self.set_sensitive(True)
244
self._is_processing = new_value
246
is_processing = property(fget=_get_is_processing, fset=_set_is_processing)
248
@log_call(logger.debug)
249
def on_success(self, message=''):
250
"""Use this callback to stop the Loading and show 'message'."""
252
self.message.set_markup(message)
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)
263
message = "%s (%s: %s)" % (VALUE_ERROR, error_type, error_msg)
265
message = "%s (%s)" % (VALUE_ERROR, error_type)
267
assert message is not None
270
self.message.set_markup(WARNING_MARKUP % message)
273
class OverviewPanel(GreyableBin, ControlPanelMixin):
274
"""The overview panel. Introduces Ubuntu One to the not logged user."""
277
'credentials-found': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
278
(gobject.TYPE_BOOLEAN, gobject.TYPE_PYOBJECT)),
281
CREDENTIALS_ERROR = _('There was a problem while retrieving the '
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/'
288
def __init__(self, main_window):
289
GreyableBin.__init__(self)
292
bus = dbus.SessionBus()
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))
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)
311
self.connect_button.set_uri(self.CONNECT)
313
self.main_window = main_window
314
self._credentials_are_new = False
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)
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()
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)
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)
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)
346
def get_sensitive(self):
347
"""Return the sensitiveness."""
348
result = self.join_now_button.get_sensitive() and \
349
self.connect_button.get_sensitive()
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('')
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('')
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)
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)
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)
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)
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)
403
@log_call(logger.info)
404
def on_network_state_changed(self, state):
405
"""Network state is reported."""
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)
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)
418
class DashboardPanel(UbuntuOneBin, ControlPanelMixin):
419
"""The dashboard panel. The user can manage the subscription."""
421
TITLE = _('Welcome to Ubuntu One!')
422
VALUE_ERROR = _('The information could not be retrieved. '
423
'Maybe your internet connection is down?')
425
def __init__(self, main_window=None):
426
UbuntuOneBin.__init__(self)
427
ControlPanelMixin.__init__(self, filename='dashboard.ui')
428
self.add(self.itself)
431
self.is_processing = True
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)
439
@log_call(logger.debug)
440
def on_account_info_ready(self, info):
441
"""Backend notifies of account info."""
444
for i in (u'name', u'type', u'email'):
445
label = getattr(self, '%s_label' % i)
446
label.set_markup('%s' % (info[i]))
449
self.is_processing = False
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
458
class VolumesPanel(UbuntuOneBin, ControlPanelMixin):
459
"""The volumes panel."""
461
TITLE = _('Select which folders from your cloud you want to sync with '
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'
475
MIN_SIZE_FULL = 1048576
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>'
486
def __init__(self, main_window=None):
487
UbuntuOneBin.__init__(self)
488
ControlPanelMixin.__init__(self, filename='volumes.ui')
489
self.add(self.itself)
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)
498
# name, subscribed, icon name, show toggle, sensitive, icon size,
500
self._empty_row = ('', False, '', False, False, gtk.ICON_SIZE_MENU,
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)
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)
517
if path == music_path:
518
result = self.MUSIC_DISPLAY_NAME
520
result = path.replace(os.path.join(home, ''), '')
524
def on_volumes_info_ready(self, info):
525
"""Backend notifies of volumes info."""
527
self.volumes_store.clear()
529
self.on_success(self.NO_VOLUMES)
534
for name, free_bytes, volumes in info:
535
if backend.ControlBackend.NAME_NOT_SET in name:
536
name = self.NAME_NOT_SET
540
# we already added user folders, let's add an empty row
541
treeiter = self.volumes_store.append(None, self._empty_row)
543
name = self.MY_FOLDERS
545
scroll_to_cell = False
546
if free_bytes == backend.ControlBackend.FREE_BYTES_NOT_AVAILABLE:
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
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
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)
564
path = self.volumes_store.get_string_from_iter(treeiter)
565
self.volumes_view.scroll_to_cell(path)
567
volumes.sort(key=operator.itemgetter('path'))
568
for volume in volumes:
570
name = self._process_path(volume[u'path'])
571
icon_name = self.FOLDER_ICON_NAME
573
is_root = volume[u'type'] == backend.ControlBackend.ROOT_TYPE
574
is_share = volume[u'type'] == backend.ControlBackend.SHARE_TYPE
578
name = self.ROOT % (name, ORANGE, self.ALWAYS_SUBSCRIBED)
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
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)
589
row = (name, bool(volume[u'subscribed']), icon_name, True,
590
sensitive, gtk.ICON_SIZE_MENU, volume['volume_id'],
593
if is_root: # root should go first!
594
self.volumes_store.prepend(treeiter, row)
596
self.volumes_store.append(treeiter, row)
598
self.volumes_view.expand_all()
599
self.volumes_view.show_all()
601
self.is_processing = False
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)
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
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."""
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)
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()
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)
639
self.is_processing = True
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)
652
uri_hook(None, FILE_URI_PREFIX + volume_path)
655
"""Load the volume list."""
656
self.backend.volumes_info(reply_handler=NO_OP,
657
error_handler=error_handler)
658
self.is_processing = True
661
class SharesPanel(UbuntuOneBin, ControlPanelMixin):
662
"""The shares panel - NOT IMPLEMENTED YET."""
664
TITLE = _('Manage permissions for shares made to other users.')
666
def __init__(self, main_window=None):
667
UbuntuOneBin.__init__(self)
668
ControlPanelMixin.__init__(self)
670
self.on_success('Not implemented yet.')
673
class Device(gtk.EventBox, ControlPanelMixin):
674
"""The device widget."""
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 @ '
681
def __init__(self, confirm_remove_dialog=None):
682
gtk.EventBox.__init__(self)
683
ControlPanelMixin.__init__(self, filename='device.ui')
685
self.confirm_dialog = confirm_remove_dialog
686
self._updating = False
687
self._last_settings = {}
689
self.is_local = False
690
self.configurable = False
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)
697
self.add(self.itself)
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)
709
def _change_device_settings(self, *args):
710
"""Update backend settings for this device."""
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)
720
def _block_signals(f):
721
"""Execute 'f' while having the _updating flag set."""
723
# pylint: disable=E0213,W0212,E1102
726
def inner(self, *args, **kwargs):
727
"""Execute 'f' while having the _updating flag set."""
729
self._updating = True
731
result = f(self, *args, **kwargs)
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
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()
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()
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)
760
def update(self, **kwargs):
761
"""Update according to named parameters.
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)
776
if 'device_id' in kwargs:
777
self.id = kwargs['device_id']
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)
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)
790
if 'is_local' in kwargs:
791
self.is_local = bool(kwargs['is_local'])
793
if 'configurable' in kwargs:
794
self.configurable = bool(kwargs['configurable'])
795
self.config_settings.set_visible(self.configurable)
797
if 'show_all_notifications' in kwargs:
798
value = bool(kwargs['show_all_notifications'])
799
self.show_all_notifications.set_active(value)
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)
806
for speed in ('max_upload_speed', 'max_download_speed'):
808
value = int(kwargs[speed]) // KILOBYTES
809
getattr(self, speed).set_value(value)
811
self._last_settings = self.__dict__
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),
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:
836
self.set_sensitive(True)
837
self.warning_label.set_text('')
838
self._last_settings = self.__dict__
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:
845
self.update(**self._last_settings)
846
self._set_warning(self.DEVICE_CHANGE_ERROR, self.warning_label)
847
self.set_sensitive(True)
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:
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:
862
self._set_warning(self.DEVICE_REMOVAL_ERROR, self.warning_label)
863
self.set_sensitive(True)
866
class DevicesPanel(UbuntuOneBin, ControlPanelMixin):
867
"""The devices panel."""
870
'local-device-removed': (gobject.SIGNAL_RUN_FIRST,
871
gobject.TYPE_NONE, ()),
874
TITLE = _('The devices connected with your personal cloud are listed '
876
NO_DEVICES = _('No devices to show.')
877
CONFIRM_REMOVE = _('Are you sure you want to remove this device '
880
def __init__(self, main_window=None):
881
UbuntuOneBin.__init__(self)
882
ControlPanelMixin.__init__(self, filename='devices.ui')
883
self.add(self.itself)
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)
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)
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)
908
self.on_success(self.NO_DEVICES)
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)
921
device.modify_bg(gtk.STATE_NORMAL, odd_row_color)
923
self.devices.pack_start(device)
924
self._devices[device.id] = device
926
self.is_processing = False
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
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)
942
self.emit('local-device-removed')
945
"""Load the device list."""
946
self.backend.devices_info(reply_handler=NO_OP,
947
error_handler=error_handler)
948
self.is_processing = True
951
class InstallPackage(gtk.VBox, ControlPanelMixin):
952
"""A widget to process the install of a package."""
955
'finished': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
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')
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)
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
974
self.progress_bar = None
976
self.message = message
977
if self.message is None:
978
self.message = self.INSTALL_PACKAGE % self.args
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)
992
@package_manager.inline_callbacks
993
def on_install_button_clicked(self, button):
994
"""The install button was clicked."""
996
# create the install transaction
997
self.transaction = yield self.package_manager.install(
1000
logger.debug('on_install_button_clicked: transaction is %r',
1002
success = package_manager.aptdaemon.enums.EXIT_SUCCESS
1003
if self.transaction == success:
1004
self.on_install_finished(None, self.transaction)
1007
# create the progress bar and pack it to the box
1008
self.progress_bar = package_manager.PackageManagerProgressBar(
1010
self.progress_bar.show()
1012
self.itself.remove(self.install_button_box)
1013
self.itself.pack_start(self.progress_bar)
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:
1020
except: # pylint: disable=W0702
1021
logger.exception('on_install_button_clicked')
1022
self._set_warning(self.FAILED_INSTALL % self.args,
1024
if self.progress_bar is not None:
1025
self.progress_bar.hide()
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)
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,
1041
self.install_label.set_markup(self.SUCCESS_INSTALL % self.args)
1042
self.emit('finished')
1045
class Service(gtk.VBox, ControlPanelMixin):
1048
CHANGE_ERROR = _('The settings could not be changed,\n'
1049
'previous values were restored.')
1051
def __init__(self, service_id, name,
1052
container=None, check_button=None, action_button=None,
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
1061
self.warning_label = gtk.Label()
1062
self.pack_start(self.warning_label, expand=False)
1064
self.button = gtk.CheckButton(label=name)
1065
self.pack_start(self.button, expand=False)
1070
class FileSyncService(Service):
1071
"""The file sync service."""
1073
FILES_SERVICE_NAME = _('File Sync')
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)
1082
self.container.set_sensitive(False)
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)
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)
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)
1104
def on_files_enabled(self):
1105
"""Files service was enabled."""
1106
self.on_file_sync_status_changed('enabled!')
1108
def on_files_disabled(self):
1109
"""Files service was disabled."""
1110
self.on_file_sync_status_changed(backend.FILE_SYNC_DISABLED)
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)
1120
self.backend.disable_files(reply_handler=NO_OP,
1121
error_handler=error_handler)
1124
"""Load the information."""
1125
self.backend.file_sync_status(reply_handler=NO_OP,
1126
error_handler=error_handler)
1129
class DesktopcouchService(Service):
1130
"""A desktopcouch service."""
1132
INSTALL_PACKAGE = _('Install the %(plugin_name)s for the sync service: '
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)
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)
1146
self.check_button.set_active(enabled)
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)
1157
self.container.pack_end(self.dependency, expand=False)
1158
self.check_button.set_sensitive(False)
1160
self.check_button.connect('toggled', self.on_button_toggled)
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
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())
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)
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:
1183
self.warning_label.set_text('')
1185
@log_call(logger.error)
1186
def on_replication_settings_change_error(self, replication_id,
1188
"""The change of settings for this replication failed."""
1189
if replication_id != self.id:
1191
self.check_button.set_active(not self.check_button.get_active())
1192
self._set_warning(self.CHANGE_ERROR, self.warning_label)
1195
class ServicesPanel(UbuntuOneBin, ControlPanelMixin):
1196
"""The services panel."""
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/'
1205
def __init__(self, main_window=None):
1206
UbuntuOneBin.__init__(self)
1207
ControlPanelMixin.__init__(self, filename='services.ui')
1208
self.add(self.itself)
1210
self.plugin_names = {'contacts': self.CONTACTS,
1211
'bookmarks': self.BOOKMARKS}
1213
self.package_manager = package_manager.PackageManager()
1214
self.install_box = None
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)
1222
self.file_sync_service = FileSyncService(container=self.files,
1223
check_button=self.file_sync_check,
1224
action_button=self.file_sync_button)
1229
def has_desktopcouch(self):
1230
"""Is desktopcouch installed?"""
1231
return self.package_manager.is_installed(self.DESKTOPCOUCH_PKG)
1233
def on_file_sync_button_clicked(self, *args, **kwargs):
1234
"""The "Show me my U1 folder" button was clicked.
1236
XXX: this should be part of the FileSyncService widget.
1237
XXX: the Ubuntu One folder should be the user's root.
1240
uri_hook(None, FILE_URI_PREFIX + os.path.expanduser('~/Ubuntu One'))
1242
def on_contacts_button_clicked(self, *args, **kwargs):
1243
"""The "Take me to the Ubuntu One website" button was clicked.
1245
XXX: this should be part of the DesktopcouchService widget.
1248
uri_hook(None, self.CONTACTS)
1250
def on_bookmarks_button_clicked(self, *args, **kwargs):
1251
"""The bookmarks button was clicked.
1253
XXX: this should be part of the DesktopcouchService widget.
1257
@log_call(logger.debug)
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
1266
logger.info('load: has_desktopcouch? %r', self.has_desktopcouch)
1267
if not self.has_desktopcouch:
1268
self.message.set_text('')
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)
1275
self.load_replications()
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)
1288
@log_call(logger.debug)
1289
def on_replications_info_ready(self, info):
1290
"""The replication info is ready."""
1293
self.replications.show()
1295
if self.install_box is not None:
1296
self.itself.remove(self.install_box)
1297
self.install_box = None
1300
pkg = item['dependency']
1301
if not pkg or self.package_manager.is_installed(pkg):
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
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)
1322
self.on_error(error_dict=error_dict)
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()
1331
class FileSyncStatus(gtk.HBox, ControlPanelMixin):
1332
"""A file sync status widget."""
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.')
1342
CONNECT = _('Connect')
1343
DISCONNECT = _('Disconnect')
1344
ENABLE = _('Enable')
1345
RESTART = _('Restart')
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')
1359
gtk.HBox.__init__(self)
1360
ControlPanelMixin.__init__(self)
1362
self.label = LabelLoading(LOADING)
1363
self.pack_start(self.label, expand=True)
1365
self.button = gtk.LinkButton(uri='')
1366
self.button.connect('clicked', self._on_button_clicked)
1367
self.pack_start(self.button, expand=False)
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)
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)
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)
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)
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)
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)
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)
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,)
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)
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)
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 ''
1462
msg += ' (' + reason + ')'
1463
self._update_status(WARNING_MARKUP % msg,
1464
self.RESTART, self.on_restart_clicked,
1465
tooltip=self.RESTART_TOOLTIP)
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)
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)
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)
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)
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)
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)
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)
1505
"""Load the information."""
1506
self.backend.file_sync_status(reply_handler=NO_OP,
1507
error_handler=error_handler)
1510
class ManagementPanel(gtk.VBox, ControlPanelMixin):
1511
"""The management panel.
1513
The user can manage dashboard, volumes, devices and services.
1518
'local-device-removed': (gobject.SIGNAL_RUN_FIRST,
1519
gobject.TYPE_NONE, ()),
1520
'unauthorized': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
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'
1528
DASHBOARD_BUTTON_TOOLTIP = _('View your personal details and service '
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 '
1534
SERVICES_BUTTON_TOOLTIP = _('Manage the sync services')
1536
def __init__(self, main_window=None):
1537
gtk.VBox.__init__(self)
1538
ControlPanelMixin.__init__(self, filename='management.ui')
1539
self.add(self.itself)
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)
1549
self.quota_progressbar.set_sensitive(False)
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)
1555
self.status_label = FileSyncStatus()
1556
self.status_box.pack_end(self.status_label, expand=True)
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)
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)
1573
self.dashboard_button.set_name(self.DASHBOARD_BUTTON_NAME)
1574
self.dashboard_button.set_tooltip_text(self.DASHBOARD_BUTTON_TOOLTIP)
1576
self.volumes_button.set_tooltip_text(self.VOLUMES_BUTTON_TOOLTIP)
1577
self.volumes_button.connect('clicked', lambda b: self.volumes.load())
1579
self.shares_button.set_tooltip_text(self.SHARES_BUTTON_TOOLTIP)
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'))
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())
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)
1596
def _update_quota(self, msg, data=None):
1597
"""Update the quota info."""
1599
if data is not None:
1600
fraction = data.get('percentage', 0.0) / 100
1601
if fraction > 0 and fraction < 0.05:
1604
fraction = round(fraction, 2)
1606
logger.debug('ManagementPanel: updating quota to %r.', fraction)
1607
if fraction >= self.QUOTA_THRESHOLD:
1608
self.quota_label.set_markup(WARNING_MARKUP % msg)
1610
self.quota_label.set_markup(msg)
1611
self.quota_label.stop()
1614
self.quota_progressbar.set_sensitive(False)
1616
self.quota_progressbar.set_sensitive(True)
1618
self.quota_progressbar.set_fraction(min(fraction, 1))
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()
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)
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='')
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')
1647
class ControlPanel(gtk.Notebook, ControlPanelMixin):
1648
"""The control panel per se, can be added into any other widget."""
1650
# should not be any larger than 736x525
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)
1658
self.main_window = main_window
1660
self.set_show_tabs(False)
1661
self.set_show_border(False)
1663
self.overview = OverviewPanel(main_window=main_window)
1664
self.insert_page(self.overview, position=0)
1666
self.management = ManagementPanel(main_window=main_window)
1667
self.insert_page(self.management, position=1)
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)
1677
self.on_show_overview_panel()
1679
logger.debug('%s: started (window size %r).',
1680
self.__class__.__name__, self.get_size_request())
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)
1688
def on_show_overview_panel(self, widget=None):
1689
"""Show the overview panel."""
1690
self.set_current_page(0)
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)
1707
class ControlPanelService(dbus.service.Object):
1708
"""DBUS service that exposes some of the window's methods."""
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)
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."""
1722
self.window.switch_to(panel)
1724
self.window.draw_attention()
1727
class ControlPanelWindow(gtk.Window):
1728
"""The main window for the Ubuntu One control panel."""
1730
TITLE = _('%(app_name)s Control Panel')
1732
def __init__(self, switch_to='', alert=False):
1733
super(ControlPanelWindow, self).__init__()
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
1741
self.connect('delete-event', lambda w, e: gtk.main_quit())
1743
self.draw_attention()
1747
self.control_panel = ControlPanel(main_window=self)
1748
self.add(self.control_panel)
1750
logger.info('Starting %s pointing at panel: %r.',
1751
self.__class__.__name__, switch_to)
1753
self.switch_to(switch_to)
1755
logger.debug('%s: started (window size %r).',
1756
self.__class__.__name__, self.get_size_request())
1758
def remove_urgency(self, *args, **kwargs):
1759
"""Remove urgency from the launcher entry."""
1760
if not USE_LIBUNITY:
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
1767
def draw_attention(self):
1768
"""Draw attention to the control panel."""
1769
self.present_with_time(1)
1770
self.set_urgency_hint(True)
1772
def switch_to(self, panel):
1773
"""Switch to named panel."""
1775
self.control_panel.management, '%s_button' % panel, None)
1776
if button is not None:
1779
logger.warning('Could not start at panel: %r.', panel)
1782
"""Run the main loop of the widget toolkit."""
1783
logger.debug('Starting GTK main loop.')