1
# -*- coding: utf-8 -*-
3
# Copyright 2010-2012 Chicharreros
5
# This program is free software: you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License version 3, as published
7
# by the Free Software Foundation.
9
# This program is distributed in the hope that it will be useful, but
10
# WITHOUT ANY WARRANTY; without even the implied warranties of
11
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12
# PURPOSE. See the GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License along
15
# with this program. If not, see <http://www.gnu.org/licenses/>.
17
"""Magicicada Status widget."""
23
from gettext import gettext as _
24
from twisted.internet import defer
28
from magicicada import syncdaemon
29
from magicicada.dbusiface import NOT_SYNCHED_PATH
30
from magicicada.helpers import log, humanize_bytes
31
from magicicada.gui.gtk.helpers import Buildable, build_icon_dict
32
from magicicada.gui.gtk.listings import (
40
UBUNTU_ONE_ROOT = os.path.expanduser(u'~/Ubuntu One')
42
CONNECT = _(u'Connect')
43
CONNECTED = _(u'Connected')
44
CONNECTING = _(u'Connecting')
45
DISCONNECT = _(u'Disconnect')
46
DISCONNECTED = _(u'Disconnected')
47
DISCONNECTING = _(u'Disconnecting')
49
IDLE = _(u'All done!')
51
STARTED = _(u'Started')
52
STARTING = _(u'Starting')
54
STOPPED = _(u'Not running')
55
STOPPING = _(u'Stopping')
56
WORKING = _(u'Working')
58
logger = logging.getLogger('magicicada.gui.gtk.status')
61
syncdaemon.STATE_CONNECTING: (CONNECTING, DISCONNECT),
62
syncdaemon.STATE_DISCONNECTED: (DISCONNECTED, CONNECT),
63
syncdaemon.STATE_IDLE: (IDLE, DISCONNECT),
64
syncdaemon.STATE_STARTING: (STARTING, STOP),
65
syncdaemon.STATE_STOPPED: (STOPPED, START),
66
syncdaemon.STATE_WORKING: (WORKING, DISCONNECT),
70
class MetadataDialog(Buildable):
71
"""The metadata dialog."""
73
filename = 'metadata.ui'
76
syncdaemon.CHANGED_NONE: (u'Synchronized', gtk.STOCK_APPLY),
77
syncdaemon.CHANGED_SERVER: (u'With server changes, downloading',
79
syncdaemon.CHANGED_LOCAL: (u'With local changes, uploading',
87
def got_metadata(self, path, data):
88
"""Activate the information elements and hide the spinner."""
89
# stop and hide the spinner
93
# title with crude path
94
self.dialog.set_title("Metadata for %s" % (path,))
96
# the icon is taken from crude path, no matter if synched or not
97
if os.path.isdir(path):
98
icon = gtk.STOCK_DIRECTORY
100
icon = gtk.STOCK_FILE
101
self.filetype_image.set_from_stock(icon, gtk.ICON_SIZE_MENU)
103
# set the data in the elements
104
if data == NOT_SYNCHED_PATH:
105
# metadata path doesn't exist for syncdaemon, show crude path,
106
# error message, and quit
107
self.path_label.set_text(path)
108
self.filepath_hbox.show()
109
self.basic_info_label.set_text(NOT_SYNCHED_PATH)
110
self.basic_info_label.show()
114
self.path_label.set_text(data['path'])
115
self.filepath_hbox.show()
117
# prepare simple text
121
size = stat['st_size']
123
size = humanize_bytes(size)
124
except (ValueError, TypeError):
125
logger.exception('Error while humanizing bytes')
126
simple.append("Size: %s" % (size,))
127
tstamp = stat['st_mtime']
128
simple.append("Modified on %s" % (time.ctime(tstamp),))
130
# set state message and image
131
state, stock = self.changed_message.get(data['changed'],
132
('Unknown state', None))
134
simple_text = "\n".join(simple)
136
self.state_image.hide()
138
self.state_image.set_from_stock(stock, gtk.ICON_SIZE_LARGE_TOOLBAR)
139
self.state_hbox.show()
141
# prepare detailed text
142
raw = data['raw_result']
143
detailed_text = '\n'.join('%s: %s' % i for i in raw.iteritems())
146
self.basic_info_label.set_text(simple_text)
147
self.basic_info_label.show()
148
self.detailed_info_textview.get_buffer().set_text(detailed_text)
149
self.details_expander.show()
151
def on_dialog_close(self, widget, data=None):
152
"""Close the dialog."""
156
"""Destroy this widget's dialog."""
157
self.dialog.destroy()
160
class Status(Buildable, gtk.Alignment):
161
"""The toolbar with info."""
163
filename = 'status.ui'
165
_u1_root = UBUNTU_ONE_ROOT
167
def __init__(self, syncdaemon_instance=None, **kwargs):
169
Buildable.__init__(self)
170
gtk.Alignment.__init__(self, **kwargs)
172
if syncdaemon_instance is not None:
173
self.sd = syncdaemon_instance
175
self.sd = syncdaemon.SyncDaemon()
176
self.sd.on_metadata_ready_callback = self.on_metadata_ready
179
CONNECT: (self.sd.connect, DISCONNECT),
180
DISCONNECT: (self.sd.disconnect, CONNECT),
181
START: (self.sd.start, STOP),
182
STOP: (self.sd.quit, START),
184
self._metadata_dialogs = {}
185
self._status_images = build_icon_dict(48)
187
folders = FoldersButton(syncdaemon_instance=self.sd)
188
shares_to_me = SharesToMeButton(syncdaemon_instance=self.sd)
189
shares_to_others = SharesToOthersButton(syncdaemon_instance=self.sd)
190
self.public_files = PublicFilesButton(syncdaemon_instance=self.sd)
192
buttons = (folders, shares_to_me, shares_to_others, self.public_files)
193
for button in buttons:
194
self.toolbar.insert(button, -1)
195
self.toolbar.set_sensitive(False)
197
self.action_button.set_use_stock(True)
206
def _update_action_button(self, action):
207
"""Update the action button according to the SD state."""
208
self.action_button.set_label(action)
209
self.action_button.set_sensitive(True)
213
@defer.inlineCallbacks
214
def on_action_button_clicked(self, button):
215
"""An action was clicked by the user."""
216
self.action_button.set_sensitive(False)
218
sd_action, next_action = self._sd_actions[button.get_label()]
221
self._update_action_button(next_action)
223
def on_metadata_clicked(self, widget, data=None):
224
"""Show metadata for a path choosen by the user."""
225
res = self.file_chooser.run()
226
self.file_chooser.hide()
227
if res != gtk.FILE_CHOOSER_ACTION_OPEN:
230
path = self.file_chooser.get_filename()
231
assert path is not None
233
dialog = MetadataDialog()
234
self._metadata_dialogs[path] = dialog
235
self.sd.get_metadata(path)
238
def on_file_chooser_open_clicked(self, widget, data=None):
239
"""Close the file_chooser dialog."""
240
self.file_chooser.response(gtk.FILE_CHOOSER_ACTION_OPEN)
242
def on_file_chooser_show(self, widget, data=None):
243
"""Close the file_chooser dialog."""
244
self.file_chooser.set_current_folder(self._u1_root)
246
# SyncDaemon callbacks
248
@log(logger, level=logging.INFO)
249
def on_initial_data_ready(self):
250
"""Initial data is now available in syncdaemon."""
251
self.toolbar.set_sensitive(True)
252
self.public_files.set_sensitive(False)
254
@log(logger, level=logging.INFO)
255
def on_initial_online_data_ready(self):
256
"""Online initial data is now available in syncdaemon."""
257
self.public_files.set_sensitive(True)
260
def on_metadata_ready(self, path, metadata):
261
"""Lower layer has the requested metadata for 'path'."""
262
if path not in self._metadata_dialogs:
263
logger.info("on_metadata_ready: path %r not in stored paths!",
267
dialog = self._metadata_dialogs[path]
268
dialog.got_metadata(path, metadata)
270
def update(self, *args, **kwargs):
271
"""Update UI based on SD current state."""
272
current_state = self.sd.current_state
273
logger.debug('updating UI with state %r', current_state)
275
state = current_state.state
276
status, next_action = ACTION_MAP[state]
277
if state == syncdaemon.STATE_IDLE:
278
self.status_image.set_from_pixbuf(self._status_images['idle'])
279
elif state in (syncdaemon.STATE_CONNECTING, syncdaemon.STATE_STARTING,
280
syncdaemon.STATE_WORKING):
281
self.status_image.set_from_pixbuf(self._status_images['working'])
283
self.status_image.set_from_pixbuf(self._status_images['alert'])
285
self._update_action_button(next_action)
286
self.status_label.set_text(status)