3
# ubuntuone-client-applet - Tray icon applet for managing Ubuntu One
5
# Author: Rodney Dawes <rodney.dawes@canonical.com>
7
# Copyright 2009 Canonical Ltd.
9
# This program is free software: you can redistribute it and/or modify it
10
# under the terms of the GNU General Public License version 3, as published
11
# by the Free Software Foundation.
13
# This program is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranties of
15
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
16
# PURPOSE. See the GNU General Public License for more details.
18
# You should have received a copy of the GNU General Public License along
19
# with this program. If not, see <http://www.gnu.org/licenses/>.
21
from __future__ import with_statement
33
# pylint: disable-msg=F0401
36
from dbus.exceptions import DBusException
37
from dbus.mainloop.glib import DBusGMainLoop
38
from canonical.ubuntuone.oauthdesktop.main import Login
39
from gobject.option import OptionGroup, OptionParser, make_option
40
from xdg.BaseDirectory import xdg_data_home, xdg_config_home
41
from urllib import quote
43
from canonical.ubuntuone.oauthdesktop.logger import setupLogging
46
logger = logging.getLogger("UbuntuOne.Client.Applet")
48
DBusGMainLoop(set_as_default=True)
50
# Wait for 30 seconds to get a proper login request
53
# Prepend tooltip labels with this label
54
TOOLTIP_TITLE = "Ubuntu One: "
56
DBUS_IFACE_NAME = "com.ubuntuone.SyncDaemon"
57
DBUS_IFACE_SYNC_NAME = "com.ubuntuone.SyncDaemon.SyncDaemon"
58
DBUS_IFACE_STATUS_NAME = "com.ubuntuone.SyncDaemon.Status"
60
class AppletMain(object):
61
"""Main applet process class."""
63
def __init__(self, signup=False, *args, **kw):
64
"""Initializes the child threads and dbus monitor."""
65
from twisted.internet import gtk2reactor
67
login = Login(dbus.service.BusName("com.ubuntuone.Authentication",
68
bus=dbus.SessionBus()))
70
self.__signup = signup
74
self.__bus = dbus.SessionBus()
75
self.__bus.add_signal_receiver(
76
handler_function=self.__new_credentials,
77
signal_name="NewCredentials",
78
dbus_interface="com.ubuntuone.Authentication")
79
self.__bus.add_signal_receiver(
80
handler_function=self.__auth_denied,
81
signal_name="AuthorizationDenied",
82
dbus_interface="com.ubuntuone.Authentication")
84
def __new_credentials(self, realm=None, consumer_key=None, sender=None):
85
"""Signal callback for when we get new credentials."""
86
self.__start_storage_daemon()
89
self.__icon = AppletIcon()
90
if self.__check_id > 0:
91
gobject.source_remove(self.__check_id)
93
self.add_to_autostart()
95
def __auth_denied(self):
96
"""Signal callback for when auth was denied by user."""
98
"""Do the quit on a timeout"""
99
from twisted.internet import reactor
103
self.remove_from_autostart()
104
if not self.__signup:
105
self.__check_id = gobject.timeout_add_seconds(LOGIN_TIMEOUT,
108
def __check_for_token(self):
109
"""Method to check for an existing token."""
111
client = self.__bus.get_object("com.ubuntuone.Authentication",
113
except DBusException:
116
iface = dbus.Interface(client, "com.ubuntuone.Authentication")
118
"""Simple handler to make dbus do stuff async."""
121
iface.maybe_login("https://ubuntuone.com", "ubuntuone",
123
reply_handler=handler, error_handler=handler)
126
def __start_storage_daemon_maybe(self):
127
"""Start the storage daemon."""
129
client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
130
except DBusException:
133
iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
135
"""Simple handler to make dbus to stuff async."""
138
iface.get_rootdir(reply_handler=handler, error_handler=handler)
141
def __start_storage_daemon(self):
142
"""Need to call dbus from a idle callback"""
143
gobject.idle_add(self.__start_storage_daemon_maybe)
145
def add_to_autostart(self):
146
"""Add ourself to the autostart config."""
147
autostart_entry = """[Desktop Entry]
149
Comment=Control applet for Ubuntu One
150
Exec=ubuntuone-client-applet
151
Icon=ubuntuone-client
154
X-Ubuntu-Gettext-Domain=ubuntuone-client
155
X-KDE-autostart-after=panel
156
X-GNOME-Autostart-enabled=true
158
with open(os.path.join(xdg_config_home, "autostart",
159
"ubuntuone-client-applet.desktop"),
161
f.write(autostart_entry)
163
def remove_from_autostart(self):
164
"""Remove ourself from the autostart config."""
165
path = os.path.join(xdg_config_home, "autostart",
166
"ubuntuone-client-applet.desktop")
173
"""Starts the gtk main loop."""
174
from twisted.internet import reactor
175
gobject.idle_add(self.__check_for_token)
180
def do_xdg_open(path_or_url):
181
"""Utility method to run xdg-open with path_or_url."""
182
ret = subprocess.call(["xdg-open", path_or_url])
184
logger.debug("Failed to run 'xdg-open %s'" % path_or_url)
186
class AppletIcon(gtk.StatusIcon):
188
Custom StatusIcon derived from gtk.StatusIcon which supports
189
animated icons and a few other nice things.
192
def __init__(self, *args, **kw):
193
"""Initializes our custom StatusIcon based widget."""
194
super(AppletIcon, self).__init__(*args, **kw)
196
self.__managed_dir = None
198
self.__theme = gtk.icon_theme_get_default()
199
iconpath = os.path.abspath(os.path.join(
200
os.path.split(os.path.dirname(__file__))[0],
202
self.__theme.append_search_path(iconpath)
204
self.__theme.append_search_path(os.path.sep + os.path.join(
205
"usr", "share", "ubuntuone-client", "icons"))
206
self.__theme.append_search_path(os.path.sep + os.path.join(
207
"usr", "local", "share", "ubuntuone-client", "icons"))
209
self.set_from_icon_name('ubuntuone-client')
210
self.connect("popup-menu", self.__popup_menu)
211
self.connect("activate", self.__open_folder)
224
self.__size_changed(self, self.__size)
227
self.__menu = self.__build_menus()
229
self.__connected = False
230
self.__need_update = False
232
pynotify.init("Ubuntu One")
234
self.__bus = dbus.SessionBus()
236
self.__bus.add_signal_receiver(
237
handler_function=self.__status_changed,
238
signal_name="StatusChanged",
239
dbus_interface=DBUS_IFACE_STATUS_NAME)
241
gobject.idle_add(self.__get_root)
243
def __status_changed(self, status):
244
"""The sync daemon status changed."""
245
if self.__managed_dir is None:
246
gobject.idle_add(self.__get_root)
247
if str(status) == "OFFLINE" or str(status).startswith("INIT") or \
248
str(status).startswith("READY"):
250
self.set_from_icon_name("ubuntuone-client-offline")
251
self.set_tooltip(TOOLTIP_TITLE + "Disconnected")
252
self.__connected = False
253
elif str(status).startswith("IDLE"):
255
self.set_from_icon_name("ubuntuone-client")
256
self.set_tooltip(TOOLTIP_TITLE + "Idle")
257
self.__connected = True
258
elif str(status) == "BAD_VERSION":
260
self.set_from_icon_name("ubuntuone-client-offline")
261
self.set_tooltip(TOOLTIP_TITLE + "Update Required")
262
self.__connected = False
263
self.__need_update = True
264
self.__items["connect"].set_sensitive(False)
265
n = pynotify.Notification(
267
"A new client version is required to continue " +
268
"using the service. Please click the Ubuntu One" +
269
"applet icon to upgrade.",
270
"ubuntuone-client-offline")
271
n.set_urgency(pynotify.URGENCY_CRITICAL)
273
logger.debug("Invalid protocol version. Update needed.")
274
elif str(status) == "AUTH_FAILED":
276
self.set_from_icon_name("ubuntuone-client-offline")
277
self.set_tooltip(TOOLTIP_TITLE + "Authentication failed")
278
self.__connected = False
280
client = self.__bus.get_object(
281
"com.ubuntuone.Authentication",
283
iface = dbus.Interface(client,
284
"com.ubuntuone.Authentication")
286
"""Simple handler to make dbus do stuff async."""
289
iface.maybe_login("https://ubuntuone.com",
292
reply_handler=handler,
293
error_handler=handler)
295
except DBusException, e:
300
self.__connected = True
301
if str(status).startswith("CONNECTING") or \
302
str(status).startswith("AUTHENTICATING") or \
303
str(status).startswith("CONNECTED"):
304
self.set_tooltip(TOOLTIP_TITLE + "Connecting")
305
elif str(status).startswith("SCANNING") or \
306
str(status).startswith("READING"):
307
self.set_tooltip(TOOLTIP_TITLE + "Scanning")
308
elif str(status).startswith("WORKING"):
309
self.set_tooltip(TOOLTIP_TITLE + "Synchronizing")
310
elif str(status).startswith("UNKNOWN_ERROR"):
313
self.set_tooltip(TOOLTIP_TITLE + "Working")
316
self.__items["connect"].hide()
317
self.__items["disconnect"].show()
319
self.__items["connect"].show()
320
self.__items["disconnect"].hide()
322
def __get_root(self):
323
"""Method to get the rootdir from the sync daemon."""
324
# Get the managed root directory
326
client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
327
except DBusException:
330
iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
332
"""We got the root dir."""
333
if self.__managed_dir:
334
self.__remove_from_places(self.__managed_dir)
335
self.__managed_dir = root
336
if os.path.isdir(self.__managed_dir) and \
337
os.access(self.__managed_dir,
338
os.F_OK | os.R_OK | os.W_OK):
339
self.__items["open"].set_sensitive(True)
340
self.__add_to_places()
342
self.__items["open"].set_sensitive(False)
345
"""Handle error from the dbus callback."""
346
if self.__managed_dir:
347
self.__remove_from_places(self.__managed_dir)
348
self.__managed_dir = None
349
self.__items["open"].set_sensitive(False)
351
iface.get_rootdir(reply_handler=got_root, error_handler=got_err)
353
# Now get the current status
355
client = self.__bus.get_object(DBUS_IFACE_NAME, "/status")
356
except DBusException:
359
iface = dbus.Interface(client, DBUS_IFACE_STATUS_NAME)
360
def status_error(error):
361
"""Handle status error."""
364
iface.current_status(reply_handler=self.__status_changed,
365
error_handler=status_error)
369
def __build_menus(self):
370
"""Create the pop-up menu items."""
373
self.__items["connect"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_CONNECT)
374
menu.append(self.__items["connect"])
375
self.__items["connect"].connect("activate", self.__toggle_state)
376
self.__items["connect"].show()
378
self.__items["disconnect"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_DISCONNECT)
379
menu.append(self.__items["disconnect"])
380
self.__items["disconnect"].connect("activate", self.__toggle_state)
382
sep = gtk.SeparatorMenuItem()
386
self.__items["bug"] = gtk.MenuItem(label="_Report a Problem")
387
menu.append(self.__items["bug"])
388
self.__items["bug"].connect("activate", self.__open_website,
389
"https://bugs.launchpad.net/ubuntuone-client/+filebug")
390
self.__items["bug"].show()
392
self.__items["open"] = gtk.MenuItem(label="_Open Folder")
393
menu.append(self.__items["open"])
394
self.__items["open"].connect("activate", self.__open_folder)
395
self.__items["open"].set_sensitive(False)
396
self.__items["open"].show()
398
self.__items["web"] = gtk.MenuItem(label="_Go to Web")
399
menu.append(self.__items["web"])
400
self.__items["web"].connect("activate", self.__open_website)
401
self.__items["web"].show()
403
sep = gtk.SeparatorMenuItem()
407
self.__items["quit"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_QUIT)
408
menu.append(self.__items["quit"])
409
self.__items["quit"].connect("activate", self.__quit_applet)
410
self.__items["quit"].show()
416
def __size_changed(self, icon, size, data=None):
417
"""Callback for when the size changes."""
420
elif size >= 24 and size < 32:
422
elif size >= 32 and size < 48:
424
elif size >= 48 and size < 64:
429
self.__pixbuf = self.__theme.load_icon(
430
'ubuntuone-client-working',
432
gtk.ICON_LOOKUP_GENERIC_FALLBACK)
434
self.__hframes = self.__pixbuf.get_width() / self.__size
435
self.__vframes = self.__pixbuf.get_height() / self.__size
440
def __spinner_timeout(self):
441
"""Timeout callback that spins the spinner."""
446
# Skip the first (resting) frame to avoid flicker
447
if self.__hpos == 0 and self.__vpos == 0:
450
x = self.__size * self.__hpos
451
y = self.__size * self.__vpos
452
pixbuf = self.__pixbuf.subpixbuf(x, y, self.__size, self.__size)
453
self.set_from_pixbuf(pixbuf)
456
if self.__hpos == self.__hframes:
459
if self.__vpos >= self.__vframes:
464
def set_busy(self, busy=True):
465
if self.__busy and busy:
468
if not busy and self.__busy_id:
469
gobject.source_remove(self.__busy_id)
472
self.__busy_id = gobject.timeout_add(100,
473
self.__spinner_timeout)
475
def __popup_menu(self, icon, button, timestamp, data=None):
476
"""Pops up the context menu for the tray icon."""
477
self.__menu.popup(None, None, gtk.status_icon_position_menu,
478
button, timestamp, icon)
480
def __quit_applet(self, menuitem, data=None):
481
"""Disconnects the daemon and closes the applet."""
483
"""Simple handler to make dbus do stuff async."""
487
client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
488
iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
489
iface.disconnect(reply_handler=handler, error_handler=handler)
490
except DBusException:
493
from twisted.internet import reactor
496
def __toggle_state(self, menuitem, data=None):
497
"""Connects or disconnects the storage sync process."""
499
"""Simple handler to make dbus do stuff async."""
503
client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
504
except DBusException:
507
iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
509
iface.disconnect(reply_handler=handler, error_handler=handler)
511
iface.connect(reply_handler=handler, error_handler=handler)
513
def __open_folder(self, data=None):
514
"""Opens the storage folder in the file manager."""
515
if self.__need_update:
516
do_xdg_open("apt:ubuntuone-storage-protocol?refresh=yes")
519
if not self.__managed_dir or not os.path.isdir(self.__managed_dir):
522
folder = "file://%s" % quote(self.__managed_dir)
525
def __open_website(self, data=None, url=None):
526
"""Opens the ubuntuone.com web site."""
530
do_xdg_open("https://ubuntuone.com/")
532
def __add_to_places(self):
533
"""Add the managed directory to the .gtk-bookmarks file."""
534
path = os.path.join(os.path.expanduser("~"), ".gtk-bookmarks")
535
with open(path, "a+") as f:
536
bookmarks_entry = "file://%s %s\n" % (
537
quote(self.__managed_dir),
538
os.path.basename(self.__managed_dir))
541
if line == bookmarks_entry:
544
f.write(bookmarks_entry)
546
def __remove_from_places(self, dir):
547
"""Remove the old path from the .gtk-bookmarks file."""
548
path = os.path.join(os.path.expanduser("~"), ".gtk-bookmarks")
549
with open(path, "a+") as f:
550
entry = "file://%s %s\n" % (quote(dir), os.path.basename(dir))
557
output = "".join(lines)
561
if __name__ == "__main__":
562
parser = OptionParser("",
563
description="Ubuntu One status applet",
565
make_option("--signup", "-s",
568
help="Log in or Sign Up for Ubuntu One"),
570
options, args = parser.parse_args()
572
# Register DBus service for making sure we run only one instance
573
bus = dbus.SessionBus()
574
if bus.request_name("com.ubuntuone.ClientApplet", dbus.bus.NAME_FLAG_DO_NOT_QUEUE) == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
575
print "Ubuntu One client applet already running, quitting"
578
icon = AppletMain(signup=options.signup)