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
26
from functools import wraps
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,
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
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
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,
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)
66
from gi.repository import Unity # pylint: disable=E0611
68
U1_DOTDESKTOP = "ubuntuone-control-panel-gtk.desktop"
72
logger = setup_logging('gtk.gui')
75
WARNING_MARKUP = '<span foreground="%s"><b>%%s</b></span>' % ERROR_COLOR
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)
83
def register_service(bus):
84
"""Try to register DBus service for making sure we run only one instance.
86
Return True if succesfully registered, False if already running.
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
93
def publish_service(window=None, switch_to='', alert=False):
94
"""Publish the service on DBus."""
96
window = ControlPanelWindow(switch_to=switch_to, alert=alert)
97
return ControlPanelService(window)
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)
107
obj = bus.get_object(DBUS_BUS_NAME_GUI, DBUS_PATH_GUI)
108
service = dbus.Interface(obj, dbus_interface=DBUS_IFACE_GUI)
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)
115
def gui_reply_handler(*args, **kwargs):
116
"""Exit when done."""
119
service.switch_to_alert(
120
switch_to, alert, reply_handler=gui_reply_handler,
121
error_handler=gui_error_handler)
126
def filter_by_app_name(f):
127
"""Excecute 'f' filtering by app_name."""
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)
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
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)
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)
153
class ControlPanelMixin(object):
154
"""A basic mixin class to provide common functionality to widgets."""
156
def __init__(self, filename=None, backend_instance=None):
157
if backend_instance is not None:
158
self.backend = backend_instance
160
bus = dbus.SessionBus()
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))
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)
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)
185
logger.warning("%s has no name (??)", obj)
187
setattr(self, name, obj)
189
logger.debug('%s: started.', self.__class__.__name__)
191
def humanize(self, int_bytes):
192
"""Return a human readble string of 'int_bytes'."""
193
return GLib.format_size_for_display(int_bytes)
195
def _set_warning(self, message, label):
196
"""Set 'message' as warning in 'label'."""
197
label.set_markup(WARNING_MARKUP % message)
201
class UbuntuOneBin(gtk.VBox):
202
"""A Ubuntu One bin."""
206
def __init__(self, title=None):
207
gtk.VBox.__init__(self)
208
self._is_processing = False
213
title = '<span font_size="large">%s</span>' % title
214
self.title = PanelTitle(markup=title)
215
self.pack_start(self.title, expand=False)
217
self.message = LabelLoading(LOADING)
218
self.pack_start(self.message, expand=False)
220
self.connect('size-allocate', on_size_allocate, self.title)
223
def _get_is_processing(self):
224
"""Is this panel processing a request?"""
225
return self._is_processing
227
def _set_is_processing(self, new_value):
228
"""Set if this panel is processing a request."""
231
self.set_sensitive(False)
234
self.set_sensitive(True)
236
self._is_processing = new_value
238
is_processing = property(fget=_get_is_processing, fset=_set_is_processing)
240
@log_call(logger.debug)
241
def on_success(self, message=''):
242
"""Use this callback to stop the Loading and show 'message'."""
244
self.message.set_markup(message)
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)
255
message = "%s (%s: %s)" % (VALUE_ERROR, error_type, error_msg)
257
message = "%s (%s)" % (VALUE_ERROR, error_type)
259
assert message is not None
262
self.message.set_markup(WARNING_MARKUP % message)
265
class OverviewPanel(GreyableBin, ControlPanelMixin):
266
"""The overview panel. Introduces Ubuntu One to the not logged user."""
269
'credentials-found': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
270
(gobject.TYPE_BOOLEAN, gobject.TYPE_PYOBJECT)),
273
def __init__(self, main_window):
274
GreyableBin.__init__(self)
277
bus = dbus.SessionBus()
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))
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))
299
self.warning_label.set_text('')
300
self.warning_label.set_property('xalign', 0.5)
302
self.connect_button.set_uri(CONNECT_BUTTON_LABEL)
304
self.main_window = main_window
305
self._credentials_are_new = False
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)
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()
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)
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)
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)
337
def get_sensitive(self):
338
"""Return the sensitiveness."""
339
result = self.join_now_button.get_sensitive() and \
340
self.connect_button.get_sensitive()
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('')
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('')
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)
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)
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)
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)
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)
394
@log_call(logger.info)
395
def on_network_state_changed(self, state):
396
"""Network state is reported."""
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)
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)
409
class DashboardPanel(UbuntuOneBin, ControlPanelMixin):
410
"""The dashboard panel. The user can manage the subscription."""
412
TITLE = DASHBOARD_TITLE
413
VALUE_ERROR = DASHBOARD_VALUE_ERROR
415
def __init__(self, main_window=None):
416
UbuntuOneBin.__init__(self)
417
ControlPanelMixin.__init__(self, filename='dashboard.ui')
418
self.add(self.itself)
421
self.is_processing = True
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)
429
@log_call(logger.debug)
430
def on_account_info_ready(self, info):
431
"""Backend notifies of account info."""
434
for i in (u'name', u'type', u'email'):
435
label = getattr(self, '%s_label' % i)
436
label.set_markup('%s' % (info[i]))
439
self.is_processing = False
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
448
class VolumesPanel(UbuntuOneBin, ControlPanelMixin):
449
"""The volumes panel."""
451
TITLE = FOLDERS_TITLE
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>'
458
def __init__(self, main_window=None):
459
UbuntuOneBin.__init__(self)
460
ControlPanelMixin.__init__(self, filename='volumes.ui')
461
self.add(self.itself)
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)
470
# name, subscribed, icon name, show toggle, sensitive, icon size,
472
self._empty_row = ('', False, '', False, False, gtk.ICON_SIZE_MENU,
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)
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
492
def on_volumes_info_ready(self, info):
493
"""Backend notifies of volumes info."""
495
self.volumes_store.clear()
497
self.on_success(NO_FOLDERS)
502
for name, free_bytes, volumes in info:
503
if backend.ControlBackend.NAME_NOT_SET in name:
508
# we already added user folders, let's add an empty row
509
treeiter = self.volumes_store.append(None, self._empty_row)
513
scroll_to_cell = False
514
if free_bytes == backend.ControlBackend.FREE_BYTES_NOT_AVAILABLE:
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
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
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)
532
path = self.volumes_store.get_string_from_iter(treeiter)
533
self.volumes_view.scroll_to_cell(path)
535
for volume in volumes:
537
name = self._process_name(volume[u'display_name'])
538
icon_name = FOLDER_ICON_NAME
540
is_root = volume[u'type'] == backend.ControlBackend.ROOT_TYPE
541
is_share = volume[u'type'] == backend.ControlBackend.SHARE_TYPE
545
name = self.ROOT % (name, ORANGE, ALWAYS_SUBSCRIBED)
547
icon_name = SHARE_ICON_NAME
548
elif name == MUSIC_DISPLAY_NAME:
549
icon_name = MUSIC_ICON_NAME
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)
555
row = (name, bool(volume[u'subscribed']), icon_name, True,
556
sensitive, gtk.ICON_SIZE_MENU, volume['volume_id'],
559
if is_root: # root should go first!
560
self.volumes_store.prepend(treeiter, row)
562
self.volumes_store.append(treeiter, row)
564
self.volumes_view.expand_all()
565
self.volumes_view.show_all()
567
self.is_processing = False
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)
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
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."""
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)
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()
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)
605
self.is_processing = True
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)
618
uri_hook(None, FILE_URI_PREFIX + volume_path)
621
"""Load the volume list."""
622
self.backend.volumes_info(reply_handler=NO_OP,
623
error_handler=error_handler)
624
self.is_processing = True
627
class SharesPanel(UbuntuOneBin, ControlPanelMixin):
628
"""The shares panel - NOT IMPLEMENTED YET."""
632
def __init__(self, main_window=None):
633
UbuntuOneBin.__init__(self)
634
ControlPanelMixin.__init__(self)
636
self.on_success('Not implemented yet.')
639
class Device(gtk.EventBox, ControlPanelMixin):
640
"""The device widget."""
642
def __init__(self, confirm_remove_dialog=None):
643
gtk.EventBox.__init__(self)
644
ControlPanelMixin.__init__(self, filename='device.ui')
646
self.confirm_dialog = confirm_remove_dialog
647
self._updating = False
648
self._last_settings = {}
650
self.is_local = False
651
self.configurable = False
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)
658
self.add(self.itself)
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)
670
def _change_device_settings(self, *args):
671
"""Update backend settings for this device."""
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)
681
def _block_signals(f):
682
"""Execute 'f' while having the _updating flag set."""
684
# pylint: disable=E0213,W0212,E1102
687
def inner(self, *args, **kwargs):
688
"""Execute 'f' while having the _updating flag set."""
690
self._updating = True
692
result = f(self, *args, **kwargs)
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
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()
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()
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)
721
def update(self, **kwargs):
722
"""Update according to named parameters.
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)
737
if 'device_id' in kwargs:
738
self.id = kwargs['device_id']
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)
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)
751
if 'is_local' in kwargs:
752
self.is_local = bool(kwargs['is_local'])
754
if 'configurable' in kwargs:
755
self.configurable = bool(kwargs['configurable'])
756
self.config_settings.set_visible(self.configurable)
758
if 'show_all_notifications' in kwargs:
759
value = bool(kwargs['show_all_notifications'])
760
self.show_all_notifications.set_active(value)
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)
767
for speed in ('max_upload_speed', 'max_download_speed'):
769
value = int(kwargs[speed]) // KILOBYTES
770
getattr(self, speed).set_value(value)
772
self._last_settings = self.__dict__
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),
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:
797
self.set_sensitive(True)
798
self.warning_label.set_text('')
799
self._last_settings = self.__dict__
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:
806
self.update(**self._last_settings)
807
self._set_warning(DEVICE_CHANGE_ERROR, self.warning_label)
808
self.set_sensitive(True)
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:
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:
823
self._set_warning(DEVICE_REMOVAL_ERROR, self.warning_label)
824
self.set_sensitive(True)
827
class DevicesPanel(UbuntuOneBin, ControlPanelMixin):
828
"""The devices panel."""
831
'local-device-removed': (gobject.SIGNAL_RUN_FIRST,
832
gobject.TYPE_NONE, ()),
835
TITLE = DEVICES_TITLE
837
def __init__(self, main_window=None):
838
UbuntuOneBin.__init__(self)
839
ControlPanelMixin.__init__(self, filename='devices.ui')
840
self.add(self.itself)
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)
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)
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)
865
self.on_success(NO_DEVICES)
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)
878
device.modify_bg(gtk.STATE_NORMAL, odd_row_color)
880
self.devices.pack_start(device)
881
self._devices[device.id] = device
883
self.is_processing = False
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
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)
899
self.emit('local-device-removed')
902
"""Load the device list."""
903
self.backend.devices_info(reply_handler=NO_OP,
904
error_handler=error_handler)
905
self.is_processing = True
908
class InstallPackage(gtk.VBox, ControlPanelMixin):
909
"""A widget to process the install of a package."""
912
'finished': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
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)
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
925
self.progress_bar = None
927
self.message = message
928
if self.message is None:
929
self.message = INSTALL_PACKAGE % self.args
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)
943
@package_manager.inline_callbacks
944
def on_install_button_clicked(self, button):
945
"""The install button was clicked."""
947
# create the install transaction
948
self.transaction = yield self.package_manager.install(
951
logger.debug('on_install_button_clicked: transaction is %r',
953
success = package_manager.aptdaemon.enums.EXIT_SUCCESS
954
if self.transaction == success:
955
self.on_install_finished(None, self.transaction)
958
# create the progress bar and pack it to the box
959
self.progress_bar = package_manager.PackageManagerProgressBar(
961
self.progress_bar.show()
963
self.itself.remove(self.install_button_box)
964
self.itself.pack_start(self.progress_bar)
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:
971
except: # pylint: disable=W0702
972
logger.exception('on_install_button_clicked')
973
self._set_warning(FAILED_INSTALL % self.args,
975
if self.progress_bar is not None:
976
self.progress_bar.hide()
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)
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,
992
self.install_label.set_markup(SUCCESS_INSTALL % self.args)
993
self.emit('finished')
996
class Service(gtk.VBox, ControlPanelMixin):
999
def __init__(self, service_id, name,
1000
container=None, check_button=None, action_button=None,
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
1009
self.warning_label = gtk.Label()
1010
self.pack_start(self.warning_label, expand=False)
1012
self.button = gtk.CheckButton(label=name)
1013
self.pack_start(self.button, expand=False)
1018
class FileSyncService(Service):
1019
"""The file sync service."""
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)
1028
self.container.set_sensitive(False)
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)
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)
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)
1050
def on_files_enabled(self):
1051
"""Files service was enabled."""
1052
self.on_file_sync_status_changed('enabled!')
1054
def on_files_disabled(self):
1055
"""Files service was disabled."""
1056
self.on_file_sync_status_changed(backend.FILE_SYNC_DISABLED)
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)
1066
self.backend.disable_files(reply_handler=NO_OP,
1067
error_handler=error_handler)
1070
"""Load the information."""
1071
self.backend.file_sync_status(reply_handler=NO_OP,
1072
error_handler=error_handler)
1075
class DesktopcouchService(Service):
1076
"""A desktopcouch service."""
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)
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)
1089
self.check_button.set_active(enabled)
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)
1100
self.container.pack_end(self.dependency, expand=False)
1101
self.check_button.set_sensitive(False)
1103
self.check_button.connect('toggled', self.on_button_toggled)
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
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())
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)
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:
1126
self.warning_label.set_text('')
1128
@log_call(logger.error)
1129
def on_replication_settings_change_error(self, replication_id,
1131
"""The change of settings for this replication failed."""
1132
if replication_id != self.id:
1134
self.check_button.set_active(not self.check_button.get_active())
1135
self._set_warning(SETTINGS_CHANGE_ERROR, self.warning_label)
1138
class ServicesPanel(UbuntuOneBin, ControlPanelMixin):
1139
"""The services panel."""
1141
TITLE = SERVICES_TITLE
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)
1153
self.plugin_names = {'contacts': CONTACTS,
1154
'bookmarks': BOOKMARKS}
1156
self.package_manager = package_manager.PackageManager()
1157
self.install_box = None
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)
1165
self.file_sync_service = FileSyncService(container=self.files,
1166
check_button=self.file_sync_check,
1167
action_button=self.file_sync_button)
1172
def has_desktopcouch(self):
1173
"""Is desktopcouch installed?"""
1174
return self.package_manager.is_installed(DESKTOPCOUCH_PKG)
1176
def on_file_sync_button_clicked(self, *args, **kwargs):
1177
"""The "Show me my U1 folder" button was clicked.
1179
XXX: this should be part of the FileSyncService widget.
1180
XXX: the Ubuntu One folder should be the user's root.
1183
uri_hook(None, FILE_URI_PREFIX + os.path.expanduser('~/Ubuntu One'))
1185
def on_contacts_button_clicked(self, *args, **kwargs):
1186
"""The "Take me to the Ubuntu One website" button was clicked.
1188
XXX: this should be part of the DesktopcouchService widget.
1191
uri_hook(None, CONTACTS)
1193
def on_bookmarks_button_clicked(self, *args, **kwargs):
1194
"""The bookmarks button was clicked.
1196
XXX: this should be part of the DesktopcouchService widget.
1200
@log_call(logger.debug)
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
1209
logger.info('load: has_desktopcouch? %r', self.has_desktopcouch)
1210
if not self.has_desktopcouch:
1211
self.message.set_text('')
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)
1218
self.load_replications()
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)
1231
@log_call(logger.debug)
1232
def on_replications_info_ready(self, info):
1233
"""The replication info is ready."""
1236
self.replications.show()
1238
if self.install_box is not None:
1239
self.itself.remove(self.install_box)
1240
self.install_box = None
1243
pkg = item['dependency']
1244
if not pkg or self.package_manager.is_installed(pkg):
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
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)
1265
self.on_error(error_dict=error_dict)
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()
1274
class FileSyncStatus(gtk.HBox, ControlPanelMixin):
1275
"""A file sync status widget."""
1278
gtk.HBox.__init__(self)
1279
ControlPanelMixin.__init__(self)
1281
self.label = LabelLoading(LOADING)
1282
self.pack_start(self.label, expand=True)
1284
self.button = gtk.LinkButton(uri='')
1285
self.button.connect('clicked', self._on_button_clicked)
1286
self.pack_start(self.button, expand=False)
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)
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)
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)
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)
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)
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)
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)
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,)
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)
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)
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 ''
1383
msg += ' (' + reason + ')'
1384
self._update_status(WARNING_MARKUP % msg,
1385
FILE_SYNC_RESTART, self.on_restart_clicked,
1386
tooltip=FILE_SYNC_RESTART_TOOLTIP)
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)
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)
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)
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)
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)
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)
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)
1426
"""Load the information."""
1427
self.backend.file_sync_status(reply_handler=NO_OP,
1428
error_handler=error_handler)
1431
class ManagementPanel(gtk.VBox, ControlPanelMixin):
1432
"""The management panel.
1434
The user can manage dashboard, volumes, devices and services.
1439
'local-device-removed': (gobject.SIGNAL_RUN_FIRST,
1440
gobject.TYPE_NONE, ()),
1441
'unauthorized': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
1444
DASHBOARD_BUTTON_NAME = 'ModeLeft'
1445
SERVICES_BUTTON_NAME = 'ModeRight'
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))
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)
1462
self.quota_progressbar.set_sensitive(False)
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)
1468
self.status_label = FileSyncStatus()
1469
self.status_box.pack_end(self.status_label, expand=True)
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)
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)
1486
self.dashboard_button.set_name(self.DASHBOARD_BUTTON_NAME)
1487
self.dashboard_button.set_tooltip_text(DASHBOARD_BUTTON_TOOLTIP)
1489
self.volumes_button.set_tooltip_text(FOLDERS_BUTTON_TOOLTIP)
1490
self.volumes_button.connect('clicked', lambda b: self.volumes.load())
1492
self.shares_button.set_tooltip_text(SHARES_BUTTON_TOOLTIP)
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'))
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())
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)
1509
def _update_quota(self, msg, data=None):
1510
"""Update the quota info."""
1512
if data is not None:
1513
fraction = data.get('percentage', 0.0) / 100
1514
if fraction > 0 and fraction < 0.05:
1517
fraction = round(fraction, 2)
1519
logger.debug('ManagementPanel: updating quota to %r.', fraction)
1520
if fraction >= QUOTA_THRESHOLD:
1521
self.quota_label.set_markup(WARNING_MARKUP % msg)
1523
self.quota_label.set_markup(msg)
1524
self.quota_label.stop()
1527
self.quota_progressbar.set_sensitive(False)
1529
self.quota_progressbar.set_sensitive(True)
1531
self.quota_progressbar.set_fraction(min(fraction, 1))
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()
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)
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='')
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')
1560
class ControlPanel(gtk.Notebook, ControlPanelMixin):
1561
"""The control panel per se, can be added into any other widget."""
1563
# should not be any larger than 736x525
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)
1571
self.main_window = main_window
1573
self.set_show_tabs(False)
1574
self.set_show_border(False)
1576
self.overview = OverviewPanel(main_window=main_window)
1577
self.insert_page(self.overview, position=0)
1579
self.management = ManagementPanel(main_window=main_window)
1580
self.insert_page(self.management, position=1)
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)
1590
self.on_show_overview_panel()
1592
logger.debug('%s: started (window size %r).',
1593
self.__class__.__name__, self.get_size_request())
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)
1601
def on_show_overview_panel(self, widget=None):
1602
"""Show the overview panel."""
1603
self.set_current_page(0)
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)
1620
class ControlPanelService(dbus.service.Object):
1621
"""DBUS service that exposes some of the window's methods."""
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)
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."""
1635
self.window.switch_to(panel)
1637
self.window.draw_attention()
1640
class ControlPanelWindow(gtk.Window):
1641
"""The main window for the Ubuntu One control panel."""
1643
def __init__(self, switch_to='', alert=False):
1644
super(ControlPanelWindow, self).__init__()
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
1652
self.connect('delete-event', lambda w, e: gtk.main_quit())
1654
self.draw_attention()
1658
self.control_panel = ControlPanel(main_window=self)
1659
self.add(self.control_panel)
1661
logger.info('Starting %s pointing at panel: %r.',
1662
self.__class__.__name__, switch_to)
1664
self.switch_to(switch_to)
1666
logger.debug('%s: started (window size %r).',
1667
self.__class__.__name__, self.get_size_request())
1669
def remove_urgency(self, *args, **kwargs):
1670
"""Remove urgency from the launcher entry."""
1671
if not USE_LIBUNITY:
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
1678
def draw_attention(self):
1679
"""Draw attention to the control panel."""
1680
self.present_with_time(1)
1681
self.set_urgency_hint(True)
1683
def switch_to(self, panel):
1684
"""Switch to named panel."""
1686
self.control_panel.management, '%s_button' % panel, None)
1687
if button is not None:
1690
logger.warning('Could not start at panel: %r.', panel)
1693
"""Run the main loop of the widget toolkit."""
1694
logger.debug('Starting GTK main loop.')