Merge lp:~nataliabidart/ubuntuone-control-panel/volumes-reborn into lp:ubuntuone-control-panel
- volumes-reborn
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Natalia Bidart | ||||
Approved revision: | 58 | ||||
Merged at revision: | 51 | ||||
Proposed branch: | lp:~nataliabidart/ubuntuone-control-panel/volumes-reborn | ||||
Merge into: | lp:ubuntuone-control-panel | ||||
Diff against target: |
902 lines (+393/-169) 8 files modified
bin/ubuntuone-control-panel-gtk (+4/-4) data/management.ui (+2/-2) data/volumes.ui (+73/-17) ubuntuone/controlpanel/gtk/gui.py (+123/-55) ubuntuone/controlpanel/gtk/tests/__init__.py (+28/-3) ubuntuone/controlpanel/gtk/tests/test_gui.py (+158/-71) ubuntuone/controlpanel/gtk/tests/test_widgets.py (+1/-8) ubuntuone/controlpanel/gtk/widgets.py (+4/-9) |
||||
To merge this branch: | bzr merge lp:~nataliabidart/ubuntuone-control-panel/volumes-reborn | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Roberto Alsina (community) | Approve | ||
Martin Albisetti (community) | Approve | ||
Review via email: mp+47064@code.launchpad.net |
Commit message
Complete redesign of Folders tab, now internally called Volumes since it will cover Shares in a future (LP: #705989).
Description of the change
Folders tab was renamed and improved.
To test, open 2 terminals and run in each:
killall ubuntuone-
DEBUG=True PYTHONPATH=. ./bin/ubuntuone
And play with the 'Cloud Storage' panel. Be careful that folder subcription/
Roberto Alsina (ralsina) wrote : | # |
+1
LOVE it. I agree with Martin about the Mine wording being a bit strange and about moving "Ubuntu one" to the top (and marking it somehow? emblem? shading?)
But great job :-)
- 57. By Natalia Bidart
-
Root folder is on top of others.
Natalia Bidart (nataliabidart) wrote : | # |
> All in all, this is shaping up to be a huge leap forward from what people will
> see from our service. GREAT job.
Thanks!
> A few comments:
>
> - It confused me for a few minutes to have "X of Y used" on top, and "Z
> available" under "Mine". I had even taken a screenshot to report it as a bug,
> until I realised what it was. I wonder if we should unify? Screehshot:
> http://
Agreed. Filed bug #706021 to discuss this further.
> - How about moving the non-deletable Ubuntu One folder fixed to the top?
> It'll make it feel a bit more immutable!
Fixed!
> - The icon next to the section "Mine" is of multiple people, which is odd, as
> I'm only one person :) Maybe the multi-person icon could be used for the
> "From Others" section, and "Mine" gets a single person?
Agreed, filed bug #706034.
- 58. By Natalia Bidart
-
Lint fixes.
Preview Diff
1 | === modified file 'bin/ubuntuone-control-panel-gtk' |
2 | --- bin/ubuntuone-control-panel-gtk 2011-01-20 21:27:21 +0000 |
3 | +++ bin/ubuntuone-control-panel-gtk 2011-01-21 19:25:14 +0000 |
4 | @@ -34,13 +34,13 @@ |
5 | def parser_options(): |
6 | """Parse command line parameters.""" |
7 | usage = "Usage: %prog [option]" |
8 | - parser = OptionParser(usage=usage) |
9 | - parser.add_option("", "--switch-to", dest="switch_to", type="string", |
10 | + result = OptionParser(usage=usage) |
11 | + result.add_option("", "--switch-to", dest="switch_to", type="string", |
12 | metavar="PANEL_NAME", |
13 | help="Start the Ubuntu One Control Panel (GTK) in the " |
14 | "PANEL_NAME tab. Possible values are: " |
15 | - "dashboard, folders, devices, applications") |
16 | - return parser |
17 | + "dashboard, volumes, devices, applications") |
18 | + return result |
19 | |
20 | |
21 | if __name__ == "__main__": |
22 | |
23 | === modified file 'data/management.ui' |
24 | --- data/management.ui 2010-12-20 22:18:01 +0000 |
25 | +++ data/management.ui 2011-01-21 19:25:14 +0000 |
26 | @@ -79,8 +79,8 @@ |
27 | </packing> |
28 | </child> |
29 | <child> |
30 | - <object class="GtkRadioButton" id="folders_button"> |
31 | - <property name="label" translatable="yes">Folders</property> |
32 | + <object class="GtkRadioButton" id="volumes_button"> |
33 | + <property name="label" translatable="yes">Cloud Storage</property> |
34 | <property name="visible">True</property> |
35 | <property name="can_focus">True</property> |
36 | <property name="receives_default">False</property> |
37 | |
38 | === renamed file 'data/folders.ui' => 'data/volumes.ui' |
39 | --- data/folders.ui 2010-12-18 20:16:18 +0000 |
40 | +++ data/volumes.ui 2011-01-21 19:25:14 +0000 |
41 | @@ -1,11 +1,10 @@ |
42 | <?xml version="1.0" encoding="UTF-8"?> |
43 | <interface> |
44 | - <requires lib="gtk+" version="2.16"/> |
45 | + <requires lib="gtk+" version="2.22"/> |
46 | <!-- interface-naming-policy project-wide --> |
47 | - <object class="GtkVBox" id="itself"> |
48 | + <object class="GtkAlignment" id="itself"> |
49 | <property name="visible">True</property> |
50 | - <property name="border_width">10</property> |
51 | - <property name="spacing">10</property> |
52 | + <property name="can_focus">False</property> |
53 | <child> |
54 | <object class="GtkScrolledWindow" id="scrolledwindow1"> |
55 | <property name="visible">True</property> |
56 | @@ -13,26 +12,83 @@ |
57 | <property name="hscrollbar_policy">automatic</property> |
58 | <property name="vscrollbar_policy">automatic</property> |
59 | <child> |
60 | - <object class="GtkViewport" id="viewport1"> |
61 | + <object class="GtkTreeView" id="volumes_view"> |
62 | <property name="visible">True</property> |
63 | - <property name="resize_mode">queue</property> |
64 | - <property name="shadow_type">none</property> |
65 | - <child> |
66 | - <object class="GtkAlignment" id="folders"> |
67 | - <property name="visible">True</property> |
68 | - <property name="xscale">0</property> |
69 | - <property name="yscale">0</property> |
70 | - <child> |
71 | - <placeholder/> |
72 | + <property name="can_focus">True</property> |
73 | + <property name="model">volumes_store</property> |
74 | + <property name="rules_hint">True</property> |
75 | + <property name="tooltip_column">0</property> |
76 | + <child> |
77 | + <object class="GtkTreeViewColumn" id="treeviewcolumn2"> |
78 | + <property name="resizable">True</property> |
79 | + <property name="sizing">autosize</property> |
80 | + <property name="expand">True</property> |
81 | + <child> |
82 | + <object class="GtkCellRendererPixbuf" id="cellrendererpixbuf1"/> |
83 | + <attributes> |
84 | + <attribute name="sensitive">1</attribute> |
85 | + <attribute name="icon-name">2</attribute> |
86 | + <attribute name="stock-size">5</attribute> |
87 | + </attributes> |
88 | + </child> |
89 | + <child> |
90 | + <object class="GtkCellRendererText" id="text_renderer"> |
91 | + <property name="ellipsize">end</property> |
92 | + <property name="width_chars">80</property> |
93 | + </object> |
94 | + <attributes> |
95 | + <attribute name="markup">0</attribute> |
96 | + <attribute name="text">0</attribute> |
97 | + </attributes> |
98 | + </child> |
99 | + </object> |
100 | + </child> |
101 | + <child> |
102 | + <object class="GtkTreeViewColumn" id="treeviewcolumn3"> |
103 | + <property name="sizing">autosize</property> |
104 | + <property name="title">On this device?</property> |
105 | + <child> |
106 | + <object class="GtkCellRendererToggle" id="cellrenderertoggle1"> |
107 | + <property name="indicator_size">15</property> |
108 | + <signal name="toggled" handler="on_subscribed_toggled" swapped="no"/> |
109 | + </object> |
110 | + <attributes> |
111 | + <attribute name="sensitive">4</attribute> |
112 | + <attribute name="visible">3</attribute> |
113 | + <attribute name="active">1</attribute> |
114 | + </attributes> |
115 | + </child> |
116 | + <child> |
117 | + <object class="GtkCellRendererText" id="cellrenderertext1"> |
118 | + <property name="visible">False</property> |
119 | + </object> |
120 | + <attributes> |
121 | + <attribute name="text">6</attribute> |
122 | + </attributes> |
123 | </child> |
124 | </object> |
125 | </child> |
126 | </object> |
127 | </child> |
128 | </object> |
129 | - <packing> |
130 | - <property name="position">0</property> |
131 | - </packing> |
132 | </child> |
133 | </object> |
134 | + <object class="GtkTreeStore" id="volumes_store"> |
135 | + <columns> |
136 | + <!-- column-name description --> |
137 | + <column type="gchararray"/> |
138 | + <!-- column-name subscribed --> |
139 | + <column type="gboolean"/> |
140 | + <!-- column-name icon-name --> |
141 | + <column type="gchararray"/> |
142 | + <!-- column-name subscribed-visible --> |
143 | + <column type="gboolean"/> |
144 | + <!-- column-name subscribed-sensitive --> |
145 | + <column type="gboolean"/> |
146 | + <!-- column-name icon-size --> |
147 | + <column type="gint"/> |
148 | + <!-- column-name identifier --> |
149 | + <column type="gchararray"/> |
150 | + </columns> |
151 | + </object> |
152 | </interface> |
153 | |
154 | === modified file 'ubuntuone/controlpanel/gtk/gui.py' |
155 | --- ubuntuone/controlpanel/gtk/gui.py 2011-01-20 21:23:11 +0000 |
156 | +++ ubuntuone/controlpanel/gtk/gui.py 2011-01-21 19:25:14 +0000 |
157 | @@ -22,6 +22,7 @@ |
158 | |
159 | import gettext |
160 | import operator |
161 | +import os |
162 | |
163 | from functools import wraps |
164 | |
165 | @@ -101,6 +102,11 @@ |
166 | return filter_by_app_name_inner |
167 | |
168 | |
169 | +def on_size_allocate(widget, allocation, label): |
170 | + """Resize labels according to who 'widget' is being resized.""" |
171 | + label.set_size_request(allocation.width - 2, -1) |
172 | + |
173 | + |
174 | @log_call(logger.info) |
175 | def uri_hook(button, uri, *args, **kwargs): |
176 | """Open an URI or do nothing if URI is not an URL.""" |
177 | @@ -159,10 +165,6 @@ |
178 | label.set_markup(WARNING_MARKUP % message) |
179 | label.show() |
180 | |
181 | - def on_size_allocate(self, label, allocation): |
182 | - """The label can re rezised, embrase it!.""" |
183 | - label.set_size_request(allocation.width - 2, -1) |
184 | - |
185 | |
186 | class ControlPanelWindow(gtk.Window): |
187 | """The main window for the Ubuntu One control panel.""" |
188 | @@ -241,8 +243,8 @@ |
189 | if self.get_current_page() == 0: |
190 | self.management.load() |
191 | if credentials_are_new: |
192 | - # redirect user to Folders page to review folders subscription |
193 | - self.management.folders_button.clicked() |
194 | + # redirect user to volumes page to review subscription |
195 | + self.management.volumes_button.clicked() |
196 | |
197 | self.next_page() |
198 | |
199 | @@ -254,17 +256,38 @@ |
200 | |
201 | def __init__(self, title=None): |
202 | gtk.VBox.__init__(self) |
203 | + self._is_processing = False |
204 | + |
205 | if title is None: |
206 | title = self.TITLE |
207 | |
208 | - self.title = PanelTitle(markup='<b>' + title + '</b>') |
209 | + title = '<span font_size="large">%s</span>' % title |
210 | + self.title = PanelTitle(markup=title) |
211 | self.pack_start(self.title, expand=False) |
212 | |
213 | self.message = LabelLoading(LOADING) |
214 | self.pack_start(self.message, expand=False) |
215 | |
216 | + self.connect('size-allocate', on_size_allocate, self.title.label) |
217 | self.show_all() |
218 | |
219 | + def _get_is_processing(self): |
220 | + """Is this panel processing a request?""" |
221 | + return self._is_processing |
222 | + |
223 | + def _set_is_processing(self, new_value): |
224 | + """Set if this panel is processing a request.""" |
225 | + if new_value: |
226 | + self.message.start() |
227 | + self.set_sensitive(False) |
228 | + else: |
229 | + self.message.stop() |
230 | + self.set_sensitive(True) |
231 | + |
232 | + self._is_processing = new_value |
233 | + |
234 | + is_processing = property(fget=_get_is_processing, fset=_set_is_processing) |
235 | + |
236 | @log_call(logger.debug) |
237 | def on_success(self, message=''): |
238 | """Use this callback to stop the Loading and show 'message'.""" |
239 | @@ -302,7 +325,7 @@ |
240 | self.add(self.itself) |
241 | self.warning_label.set_text('') |
242 | self.warning_label.set_property('xalign', 0.5) |
243 | - self.warning_label.connect('size-allocate', self.on_size_allocate) |
244 | + self.connect('size-allocate', on_size_allocate, self.warning_label) |
245 | |
246 | self.connect_button.set_uri(self.CONNECT) |
247 | |
248 | @@ -348,7 +371,7 @@ |
249 | label.set_markup(self.BULLET + ' ' + msg) |
250 | label.set_property('xalign', 0) |
251 | label.set_property('wrap', True) |
252 | - label.connect('size-allocate', self.on_size_allocate) |
253 | + self.messages.connect('size-allocate', on_size_allocate, label) |
254 | |
255 | self.messages.pack_start(label) |
256 | self.messages.show_all() |
257 | @@ -478,16 +501,25 @@ |
258 | self.on_error() |
259 | |
260 | |
261 | -class FoldersPanel(UbuntuOneBin, ControlPanelMixin): |
262 | - """The folders panel.""" |
263 | +class VolumesPanel(UbuntuOneBin, ControlPanelMixin): |
264 | + """The volumes panel.""" |
265 | |
266 | - TITLE = _('Listed below are the folders available on this machine. ' |
267 | - 'Subscribed means the folder will receive and send updates.') |
268 | + TITLE = _('Select which folders from your cloud you want synchronized ' |
269 | + 'on this device.') |
270 | + MY_FOLDERS = _('Mine') |
271 | + ALWAYS_SUBSCRIBED = _('Always in your personal cloud storage!') |
272 | + FREE_SPACE = _('%(free_space)s available storage') |
273 | + CONTACT_ICON_NAME = 'system-users' |
274 | + FOLDER_ICON_NAME = 'folder' |
275 | + SHARE_ICON_NAME = 'folder-remote' |
276 | + ROW_HEADER = '<span font_size="large"><b>%s</b></span> ' \ |
277 | + '<span foreground="grey">%s</span>' |
278 | + ROOT = '%s - <span foreground="%s" font_size="small">%s</span>' |
279 | NO_VOLUMES = _('No folders to show.') |
280 | |
281 | def __init__(self): |
282 | UbuntuOneBin.__init__(self) |
283 | - ControlPanelMixin.__init__(self, filename='folders.ui') |
284 | + ControlPanelMixin.__init__(self, filename='volumes.ui') |
285 | self.add(self.itself) |
286 | self.show_all() |
287 | |
288 | @@ -495,66 +527,102 @@ |
289 | self.on_volumes_info_ready) |
290 | self.backend.connect_to_signal('VolumesInfoError', |
291 | self.on_volumes_info_error) |
292 | - self.volumes = None |
293 | - self._subscribed = [] |
294 | + self.backend.connect_to_signal('VolumeSettingsChanged', |
295 | + self.on_volume_settings_changed) |
296 | + self.backend.connect_to_signal('VolumeSettingsChangeError', |
297 | + self.on_volume_settings_change_error) |
298 | + |
299 | + def _process_path(self, path): |
300 | + """Trim 'path' so the '~' is removed.""" |
301 | + home = os.path.expanduser('~') |
302 | + return path.replace(os.path.join(home, ''), '') |
303 | |
304 | def on_volumes_info_ready(self, info): |
305 | """Backend notifies of volumes info.""" |
306 | |
307 | - if self.volumes is not None: |
308 | - self.folders.remove(self.volumes) |
309 | - self.volumes = None |
310 | - |
311 | + self.volumes_store.clear() |
312 | if not info: |
313 | self.on_success(self.NO_VOLUMES) |
314 | return |
315 | else: |
316 | self.on_success() |
317 | |
318 | - self.volumes = gtk.Table(rows=len(info) + 1, columns=2) |
319 | - |
320 | - header = (gtk.Label(), gtk.Label()) |
321 | - header[0].set_markup('<b>' + _('Local path') + '</b>') |
322 | - header[1].set_markup('<b>' + _('Subscribed') + '</b>') |
323 | - self.volumes.attach(header[0], 0, 1, 0, 1) |
324 | - self.volumes.attach(header[1], 1, 2, 0, 1, xoptions=0) |
325 | - self.volumes.show_all() |
326 | - |
327 | - info.sort(key=operator.itemgetter('suggested_path')) |
328 | - for i, volume in enumerate(info): |
329 | - path = gtk.Label(volume['suggested_path']) |
330 | - path.set_property('xalign', 0) |
331 | - path.show() |
332 | - self.volumes.attach(path, 0, 1, i + 1, i + 2) |
333 | - |
334 | - subscribed = gtk.CheckButton(volume['volume_id']) |
335 | - subscribed.set_active(bool(volume['subscribed'])) |
336 | - subscribed.show() |
337 | - subscribed.get_child().hide() |
338 | - subscribed.connect('clicked', self.on_subscribed_clicked) |
339 | - self._subscribed.append(subscribed) |
340 | - self.volumes.attach(subscribed, 1, 2, i + 1, i + 2, xoptions=0) |
341 | - |
342 | - self.folders.add(self.volumes) |
343 | + # pylint: disable=W0612 |
344 | + # name, subscribed, icon name, show toggle, sensitive, icon size, id |
345 | + empty_row = ('', False, '', False, False, gtk.ICON_SIZE_MENU, None) |
346 | + |
347 | + for name, free_bytes, volumes in info: |
348 | + if name: |
349 | + name = name + "'s" |
350 | + icon_name = self.SHARE_ICON_NAME |
351 | + else: |
352 | + name = self.MY_FOLDERS |
353 | + icon_name = self.FOLDER_ICON_NAME |
354 | + |
355 | + free_bytes_args = {'free_space': self.humanize(int(free_bytes))} |
356 | + row = (self.ROW_HEADER % (name, self.FREE_SPACE % free_bytes_args), |
357 | + True, self.CONTACT_ICON_NAME, False, False, |
358 | + gtk.ICON_SIZE_LARGE_TOOLBAR, None) |
359 | + treeiter = self.volumes_store.append(None, row) |
360 | + |
361 | + volumes.sort(key=operator.itemgetter('path')) |
362 | + for volume in volumes: |
363 | + sensitive = True |
364 | + path = self._process_path(volume['path']) |
365 | + if volume['type'] == u'ROOT': |
366 | + sensitive = False |
367 | + path = self.ROOT % (path, ORANGE, self.ALWAYS_SUBSCRIBED) |
368 | + |
369 | + row = (path, bool(volume['subscribed']), icon_name, True, |
370 | + sensitive, gtk.ICON_SIZE_MENU, volume['volume_id']) |
371 | + |
372 | + if volume['type'] == u'ROOT': # root should go first! |
373 | + self.volumes_store.prepend(treeiter, row) |
374 | + else: |
375 | + self.volumes_store.append(treeiter, row) |
376 | + |
377 | + # When we display shares info, we'll need to smartly add |
378 | + # an empty row to the tree view to separate volume groups |
379 | + #treeiter = self.volumes_store.append(None, empty_row) |
380 | + |
381 | + self.volumes_view.expand_row(0, True) |
382 | + self.volumes_view.show_all() |
383 | + self.is_processing = False |
384 | |
385 | @log_call(logger.error) |
386 | def on_volumes_info_error(self, error_dict=None): |
387 | """Backend notifies of an error when fetching volumes info.""" |
388 | self.on_error() |
389 | |
390 | - def on_subscribed_clicked(self, checkbutton): |
391 | - """The user toggled 'checkbutton'.""" |
392 | - volume_id = checkbutton.get_label() |
393 | - subscribed = bool_str(checkbutton.get_active()) |
394 | + @log_call(logger.info) |
395 | + def on_volume_settings_changed(self, volume_id): |
396 | + """The settings for 'volume_id' were changed.""" |
397 | + self.is_processing = False |
398 | + |
399 | + @log_call(logger.error) |
400 | + def on_volume_settings_change_error(self, volume_id, error_dict=None): |
401 | + """The settings for 'volume_id' were not changed.""" |
402 | + self.load() |
403 | + |
404 | + def on_subscribed_toggled(self, widget, path, *args, **kwargs): |
405 | + """The user toggled 'widget'.""" |
406 | + treeiter = self.volumes_store.get_iter(path) |
407 | + volume_id = self.volumes_store.get_value(treeiter, 6) |
408 | + subscribed = not self.volumes_store.get_value(treeiter, 1) |
409 | + |
410 | + self.volumes_store.set_value(treeiter, 1, subscribed) |
411 | + |
412 | self.backend.change_volume_settings(volume_id, |
413 | - {'subscribed': subscribed}, |
414 | + {'subscribed': bool_str(subscribed)}, |
415 | reply_handler=NO_OP, error_handler=error_handler) |
416 | |
417 | + self.is_processing = True |
418 | + |
419 | def load(self): |
420 | """Load the volume list.""" |
421 | self.backend.volumes_info(reply_handler=NO_OP, |
422 | error_handler=error_handler) |
423 | - self.message.start() |
424 | + self.is_processing = True |
425 | |
426 | |
427 | class Device(gtk.VBox, ControlPanelMixin): |
428 | @@ -1228,7 +1296,7 @@ |
429 | class ManagementPanel(gtk.VBox, ControlPanelMixin): |
430 | """The management panel. |
431 | |
432 | - The user can manage dashboard, folders, devices and services. |
433 | + The user can manage dashboard, volumes, devices and services. |
434 | |
435 | """ |
436 | |
437 | @@ -1255,12 +1323,12 @@ |
438 | self.status_box.pack_end(self.status_label, expand=False) |
439 | |
440 | self.dashboard = DashboardPanel() |
441 | - self.folders = FoldersPanel() |
442 | + self.volumes = VolumesPanel() |
443 | self.devices = DevicesPanel() |
444 | self.services = ServicesPanel() |
445 | |
446 | cb = lambda button, page_num: self.notebook.set_current_page(page_num) |
447 | - self.tabs = (u'dashboard', u'folders', u'devices', u'services') |
448 | + self.tabs = (u'dashboard', u'volumes', u'devices', u'services') |
449 | for page_num, tab in enumerate(self.tabs): |
450 | setattr(self, ('%s_page' % tab).upper(), page_num) |
451 | button = getattr(self, '%s_button' % tab) |
452 | @@ -1270,7 +1338,7 @@ |
453 | gtk.gdk.Color(DEFAULT_FG)) |
454 | self.notebook.insert_page(getattr(self, tab), position=page_num) |
455 | |
456 | - self.folders_button.connect('clicked', lambda b: self.folders.load()) |
457 | + self.volumes_button.connect('clicked', lambda b: self.volumes.load()) |
458 | self.devices_button.connect('clicked', lambda b: self.devices.load()) |
459 | self.services_button.connect('clicked', lambda b: self.services.load()) |
460 | self.devices.connect('local-device-removed', |
461 | |
462 | === modified file 'ubuntuone/controlpanel/gtk/tests/__init__.py' |
463 | --- ubuntuone/controlpanel/gtk/tests/__init__.py 2011-01-10 02:26:22 +0000 |
464 | +++ ubuntuone/controlpanel/gtk/tests/__init__.py 2011-01-21 19:25:14 +0000 |
465 | @@ -28,10 +28,35 @@ |
466 | FAKE_ACCOUNT_INFO = {'type': 'Payed', 'name': 'Test me', |
467 | 'email': 'test.com', 'quota_total': '12345', 'quota_used': '9999'} |
468 | |
469 | +USER_HOME = '/home/tester' |
470 | + |
471 | +ROOT = { |
472 | + u'volume_id': '', u'path': '/home/tester/My Ubuntu', |
473 | + u'subscribed': 'True', u'type': u'ROOT', |
474 | +} |
475 | + |
476 | +FAKE_FOLDERS_INFO = [ |
477 | + {u'volume_id': u'0', u'path': u'/home/tester/foo', |
478 | + u'suggested_path': u'~/foo', u'subscribed': u'', u'type': u'UDF'}, |
479 | + {u'volume_id': u'1', u'path': u'/home/tester/bar', |
480 | + u'suggested_path': u'~/bar', u'subscribed': u'True', u'type': u'UDF'}, |
481 | + {u'volume_id': u'2', u'path': u'/home/tester/baz', |
482 | + u'suggested_path': u'~/baz', u'subscribed': u'True', u'type': u'UDF'}, |
483 | +] |
484 | + |
485 | +FAKE_SHARES_INFO = [ |
486 | + {u'volume_id': u'1234', |
487 | + u'path': u'/home/tester/.local/share/ubuntuone/shares/do from Other User', |
488 | + u'subscribed': u'', u'type': u'SHARE'}, |
489 | + |
490 | + {u'volume_id': u'5678', |
491 | + u'path': u'/home/tester/.local/share/ubuntuone/shares/re from Other User', |
492 | + u'subscribed': u'True', u'type': u'SHARE'}, |
493 | +] |
494 | + |
495 | FAKE_VOLUMES_INFO = [ |
496 | - {'volume_id': '0', 'suggested_path': '~/foo', 'subscribed': ''}, |
497 | - {'volume_id': '1', 'suggested_path': '~/bar', 'subscribed': 'True'}, |
498 | - {'volume_id': '2', 'suggested_path': '~/baz', 'subscribed': 'True'}, |
499 | + (u'', u'147852369', [ROOT] + FAKE_FOLDERS_INFO), |
500 | + (u'Other User', u'985674', FAKE_SHARES_INFO), |
501 | ] |
502 | |
503 | FAKE_DEVICE_INFO = { |
504 | |
505 | === modified file 'ubuntuone/controlpanel/gtk/tests/test_gui.py' |
506 | --- ubuntuone/controlpanel/gtk/tests/test_gui.py 2011-01-20 21:23:11 +0000 |
507 | +++ ubuntuone/controlpanel/gtk/tests/test_gui.py 2011-01-21 19:25:14 +0000 |
508 | @@ -27,7 +27,7 @@ |
509 | from ubuntuone.controlpanel.gtk import gui |
510 | from ubuntuone.controlpanel.gtk.tests import (FAKE_ACCOUNT_INFO, |
511 | FAKE_DEVICE_INFO, FAKE_DEVICES_INFO, |
512 | - FAKE_VOLUMES_INFO, FAKE_REPLICATIONS_INFO, |
513 | + FAKE_VOLUMES_INFO, FAKE_REPLICATIONS_INFO, USER_HOME, |
514 | FakedNMState, FakedSSOBackend, FakedSessionBus, FakedInterface, |
515 | FakedPackageManager, |
516 | ) |
517 | @@ -52,6 +52,8 @@ |
518 | |
519 | def setUp(self): |
520 | super(BaseTestCase, self).setUp() |
521 | + self.patch(gui.os.path, 'expanduser', |
522 | + lambda path: path.replace('~', USER_HOME)) |
523 | self.patch(gui.gtk, 'main', lambda: None) |
524 | self.patch(gui.dbus, 'SessionBus', FakedSessionBus) |
525 | self.patch(gui.dbus, 'Interface', FakedInterface) |
526 | @@ -279,10 +281,10 @@ |
527 | self.ui.management.DASHBOARD_PAGE) |
528 | self.assertEqual(self._called, ((), {})) |
529 | |
530 | - def test_credentials_found_shows_folders_management_panel(self): |
531 | + def test_credentials_found_shows_volumes_management_panel(self): |
532 | """On 'credentials-found' signal, the management panel is shown. |
533 | |
534 | - If first signal parameter is True, visible tab should be folders. |
535 | + If first signal parameter is True, visible tab should be volumes. |
536 | |
537 | """ |
538 | a_token = object() |
539 | @@ -290,7 +292,7 @@ |
540 | |
541 | self.assert_current_tab_correct(self.ui.management) |
542 | self.assertEqual(self.ui.management.notebook.get_current_page(), |
543 | - self.ui.management.FOLDERS_PAGE) |
544 | + self.ui.management.VOLUMES_PAGE) |
545 | |
546 | def test_local_device_removed_shows_overview_panel(self): |
547 | """On 'local-device-removed' signal, the overview panel is shown.""" |
548 | @@ -362,6 +364,29 @@ |
549 | self.assert_warning_correct(self.ui.message, msg) |
550 | self.assertFalse(self.ui.message.active) |
551 | |
552 | + def test_is_processing(self): |
553 | + """The flag 'is_processing' is False on start.""" |
554 | + self.assertFalse(self.ui.is_processing) |
555 | + self.assertTrue(self.ui.is_sensitive()) |
556 | + |
557 | + def test_set_is_processing(self): |
558 | + """When setting 'is_processing', the spinner is shown.""" |
559 | + self.ui.is_processing = False |
560 | + self.ui.is_processing = True |
561 | + |
562 | + self.assertTrue(self.ui.message.get_visible()) |
563 | + self.assertTrue(self.ui.message.active) |
564 | + self.assertFalse(self.ui.is_sensitive()) |
565 | + |
566 | + def test_unset_is_processing(self): |
567 | + """When unsetting 'is_processing', the spinner is not shown.""" |
568 | + self.ui.is_processing = True |
569 | + self.ui.is_processing = False |
570 | + |
571 | + self.assertTrue(self.ui.message.get_visible()) |
572 | + self.assertFalse(self.ui.message.active) |
573 | + self.assertTrue(self.ui.is_sensitive()) |
574 | + |
575 | |
576 | class OverwiewPanelTestCase(ControlPanelMixinTestCase): |
577 | """The test suite for the overview panel.""" |
578 | @@ -716,14 +741,14 @@ |
579 | self.assert_warning_correct(self.ui.message, gui.VALUE_ERROR) |
580 | |
581 | |
582 | -class FoldersTestCase(ControlPanelMixinTestCase): |
583 | - """The test suite for the folders panel.""" |
584 | +class VolumesTestCase(ControlPanelMixinTestCase): |
585 | + """The test suite for the volumes panel.""" |
586 | |
587 | - klass = gui.FoldersPanel |
588 | - ui_filename = 'folders.ui' |
589 | + klass = gui.VolumesPanel |
590 | + ui_filename = 'volumes.ui' |
591 | |
592 | def setUp(self): |
593 | - super(FoldersTestCase, self).setUp() |
594 | + super(VolumesTestCase, self).setUp() |
595 | self.ui.load() |
596 | |
597 | def test_is_an_ubuntuone_bin(self): |
598 | @@ -744,6 +769,10 @@ |
599 | [self.ui.on_volumes_info_ready]) |
600 | self.assertEqual(self.ui.backend._signals['VolumesInfoError'], |
601 | [self.ui.on_volumes_info_error]) |
602 | + self.assertEqual(self.ui.backend._signals['VolumeSettingsChanged'], |
603 | + [self.ui.on_volume_settings_changed]) |
604 | + self.assertEqual(self.ui.backend._signals['VolumeSettingsChangeError'], |
605 | + [self.ui.on_volume_settings_change_error]) |
606 | |
607 | def test_volumes_info_is_requested_on_load(self): |
608 | """The volumes info is requested to the backend.""" |
609 | @@ -753,12 +782,18 @@ |
610 | |
611 | self.assert_backend_called('volumes_info', ()) |
612 | |
613 | - def test_message_after_load(self): |
614 | - """The volumes label is active when contents are load.""" |
615 | + def test_is_processing_after_load(self): |
616 | + """The ui is processing when contents are load.""" |
617 | self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO) |
618 | self.ui.load() |
619 | |
620 | - self.assertTrue(self.ui.message.active) |
621 | + self.assertTrue(self.ui.is_processing) |
622 | + |
623 | + def test_is_not_processing_after_volumes_info_ready(self): |
624 | + """The ui is processing when contents are load.""" |
625 | + self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO) |
626 | + |
627 | + self.assertFalse(self.ui.is_processing) |
628 | |
629 | def test_message_after_non_empty_volumes_info_ready(self): |
630 | """The volumes label is a LabelLoading.""" |
631 | @@ -777,66 +812,67 @@ |
632 | """The volumes info is processed when ready.""" |
633 | self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO) |
634 | |
635 | - self.assertEqual(self.ui.folders.get_children(), [self.ui.volumes]) |
636 | - |
637 | - volumes = self.ui.volumes.get_children() |
638 | - volumes.reverse() |
639 | - |
640 | - header = volumes[:2] # grab header |
641 | - self.assertEqual(header[0].get_text(), 'Local path') |
642 | - self.assertEqual(header[1].get_text(), 'Subscribed') |
643 | - |
644 | - volumes = volumes[2:] # drop header |
645 | - labels = filter(lambda w: isinstance(w, gui.gtk.Label), volumes) |
646 | - checks = filter(lambda w: isinstance(w, gui.gtk.CheckButton), volumes) |
647 | - |
648 | - self.assertEqual(len(checks), len(FAKE_VOLUMES_INFO)) |
649 | - |
650 | - for label, check, volume in zip(labels, checks, FAKE_VOLUMES_INFO): |
651 | - self.assertEqual(volume['suggested_path'], label.get_text()) |
652 | - self.assertEqual(bool(volume['subscribed']), check.get_active()) |
653 | - self.assertEqual(volume['volume_id'], check.get_label()) |
654 | - self.assertFalse(check.get_child().get_visible()) |
655 | + self.assertEqual(len(FAKE_VOLUMES_INFO), len(self.ui.volumes_store)) |
656 | + treeiter = self.ui.volumes_store.get_iter_root() |
657 | + for name, free_bytes, volumes in FAKE_VOLUMES_INFO: |
658 | + name = "%s's" % name if name else self.ui.MY_FOLDERS |
659 | + free_bytes = self.ui.humanize(int(free_bytes)) |
660 | + header = (name, self.ui.FREE_SPACE % {'free_space': free_bytes}) |
661 | + |
662 | + # check parent row |
663 | + row = self.ui.volumes_store.get(treeiter, *xrange(7)) |
664 | + |
665 | + self.assertEqual(row[0], self.ui.ROW_HEADER % header) |
666 | + self.assertTrue(row[1], 'parent will always be subscribed') |
667 | + self.assertEqual(row[2], self.ui.CONTACT_ICON_NAME) |
668 | + self.assertFalse(row[3], 'no toggle should be shown on parent!') |
669 | + self.assertFalse(row[4], 'toggle should be non sensitive.') |
670 | + self.assertEqual(row[5], gui.gtk.ICON_SIZE_LARGE_TOOLBAR) |
671 | + self.assertEqual(row[6], None) |
672 | + |
673 | + # check children |
674 | + self.assertEqual(len(volumes), |
675 | + self.ui.volumes_store.iter_n_children(treeiter)) |
676 | + childiter = self.ui.volumes_store.iter_children(treeiter) |
677 | + |
678 | + sorted_vols = sorted(volumes, key=gui.operator.itemgetter('path')) |
679 | + for volume in sorted_vols: |
680 | + row = self.ui.volumes_store.get(childiter, *xrange(7)) |
681 | + |
682 | + sensitive = True |
683 | + path = volume['path'].replace(USER_HOME + '/', '') |
684 | + if volume['type'] == 'ROOT': |
685 | + sensitive = False |
686 | + path = self.ui.ROOT % (path, gui.ORANGE, |
687 | + self.ui.ALWAYS_SUBSCRIBED) |
688 | + |
689 | + self.assertEqual(row[0], path) |
690 | + self.assertEqual(row[1], bool(volume['subscribed'])) |
691 | + if volume['type'] != 'SHARE': |
692 | + self.assertEqual(row[2], self.ui.FOLDER_ICON_NAME) |
693 | + else: |
694 | + self.assertEqual(row[2], self.ui.SHARE_ICON_NAME) |
695 | + self.assertTrue(row[3], 'toggle should be shown on child!') |
696 | + self.assertEqual(row[4], sensitive) |
697 | + self.assertEqual(row[5], gui.gtk.ICON_SIZE_MENU) |
698 | + self.assertEqual(row[6], volume['volume_id']) |
699 | + |
700 | + childiter = self.ui.volumes_store.iter_next(childiter) |
701 | + |
702 | + treeiter = self.ui.volumes_store.iter_next(treeiter) |
703 | |
704 | def test_on_volumes_info_ready_clears_the_list(self): |
705 | """The old volumes info is cleared before updated.""" |
706 | self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO) |
707 | self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO) |
708 | |
709 | - self.assertEqual(len(self.ui.folders.get_children()), 1) |
710 | - child = self.ui.folders.get_children()[0] |
711 | - self.assertEqual(child, self.ui.volumes) |
712 | - |
713 | - volumes = filter(lambda w: isinstance(w, gui.gtk.CheckButton), |
714 | - self.ui.volumes.get_children()) |
715 | - self.assertEqual(len(volumes), len(FAKE_VOLUMES_INFO)) |
716 | + self.assertEqual(len(self.ui.volumes_store), len(FAKE_VOLUMES_INFO)) |
717 | |
718 | def test_on_volumes_info_ready_with_no_volumes(self): |
719 | """When there are no volumes, a notification is shown.""" |
720 | self.ui.on_volumes_info_ready([]) |
721 | - # no volumes table |
722 | - self.assertEqual(len(self.ui.folders.get_children()), 0) |
723 | - self.assertTrue(self.ui.volumes is None) |
724 | - |
725 | - def test_on_subscribed_clicked(self): |
726 | - """Clicking on 'subscribed' updates the folder subscription.""" |
727 | - self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO) |
728 | - |
729 | - method = 'change_volume_settings' |
730 | - for checkbutton in self.ui._subscribed: |
731 | - checkbutton.clicked() |
732 | - fid = checkbutton.get_label() |
733 | - |
734 | - subscribed = gui.bool_str(checkbutton.get_active()) |
735 | - self.assert_backend_called(method, |
736 | - (fid, {'subscribed': subscribed})) |
737 | - # clean backend calls |
738 | - self.ui.backend._called.pop(method) |
739 | - |
740 | - checkbutton.clicked() |
741 | - subscribed = gui.bool_str(checkbutton.get_active()) |
742 | - self.assert_backend_called('change_volume_settings', |
743 | - (fid, {'subscribed': subscribed})) |
744 | + |
745 | + self.assertEqual(len(self.ui.volumes_store), 0) |
746 | |
747 | def test_on_volumes_info_error(self): |
748 | """The volumes info couldn't be retrieved.""" |
749 | @@ -854,6 +890,57 @@ |
750 | self.test_on_volumes_info_error() |
751 | self.test_on_volumes_info_ready_with_no_volumes() |
752 | |
753 | + def test_on_subscribed_toggled(self): |
754 | + """Clicking on 'subscribed' updates the folder subscription.""" |
755 | + self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO) |
756 | + |
757 | + for parent, (_, _, volumes) in enumerate(FAKE_VOLUMES_INFO): |
758 | + |
759 | + sorted_vols = sorted(volumes, key=gui.operator.itemgetter('path')) |
760 | + for child, volume in enumerate(sorted_vols): |
761 | + if volume['type'] == 'ROOT': |
762 | + continue # not editable |
763 | + |
764 | + path = '%s:%s' % (parent, child) |
765 | + self.ui.on_subscribed_toggled(widget=None, path=path) |
766 | + |
767 | + fid = volume['volume_id'] |
768 | + subscribed = gui.bool_str(not bool(volume['subscribed'])) |
769 | + # backend was called |
770 | + self.assert_backend_called('change_volume_settings', |
771 | + (fid, {'subscribed': subscribed})) |
772 | + # store was updated |
773 | + it = self.ui.volumes_store.get_iter(path) |
774 | + value = self.ui.volumes_store.get_value(it, 1) |
775 | + self.assertEqual(value, bool(subscribed)) |
776 | + |
777 | + # the ui is processing |
778 | + self.assertTrue(self.ui.is_processing, 'ui must be processing') |
779 | + |
780 | + # simulate success for setting change |
781 | + self.ui.on_volume_settings_changed(volume_id=fid) |
782 | + |
783 | + def test_on_volume_setting_changed(self): |
784 | + """The setting for a volume was successfully changed.""" |
785 | + self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO) |
786 | + self.ui.on_subscribed_toggled(None, "0:0") |
787 | + |
788 | + self.ui.on_volume_settings_changed(volume_id=None) # id not used |
789 | + |
790 | + # the ui is no longer processing |
791 | + self.assertFalse(self.ui.is_processing, 'ui must not be processing') |
792 | + |
793 | + def test_on_volume_setting_change_error(self): |
794 | + """The setting for a volume was not successfully changed.""" |
795 | + self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO) |
796 | + self.ui.on_subscribed_toggled(None, "0:0") |
797 | + |
798 | + self.patch(self.ui, 'load', self._set_called) |
799 | + self.ui.on_volume_settings_change_error(volume_id=None, |
800 | + error_dict=None) # id not used |
801 | + # reload folders list to sanitize the info in volumes_store |
802 | + self.assertTrue(self._called, ((), {})) |
803 | + |
804 | |
805 | class DeviceTestCase(ControlPanelMixinTestCase): |
806 | """The test suite for the device widget.""" |
807 | @@ -2086,11 +2173,11 @@ |
808 | actual = self.ui.notebook.get_nth_page(self.ui.DASHBOARD_PAGE) |
809 | self.assertTrue(self.ui.dashboard is actual) |
810 | |
811 | - def test_folders_panel_is_packed(self): |
812 | - """The folders panel is packed.""" |
813 | - self.assertIsInstance(self.ui.folders, gui.FoldersPanel) |
814 | - actual = self.ui.notebook.get_nth_page(self.ui.FOLDERS_PAGE) |
815 | - self.assertTrue(self.ui.folders is actual) |
816 | + def test_volumes_panel_is_packed(self): |
817 | + """The volumes panel is packed.""" |
818 | + self.assertIsInstance(self.ui.volumes, gui.VolumesPanel) |
819 | + actual = self.ui.notebook.get_nth_page(self.ui.VOLUMES_PAGE) |
820 | + self.assertTrue(self.ui.volumes is actual) |
821 | |
822 | def test_devices_panel_is_packed(self): |
823 | """The devices panel is packed.""" |
824 | @@ -2104,11 +2191,11 @@ |
825 | actual = self.ui.notebook.get_nth_page(self.ui.SERVICES_PAGE) |
826 | self.assertTrue(self.ui.services is actual) |
827 | |
828 | - def test_entering_folders_tab_loads_content(self): |
829 | - """The volumes info is loaded when entering the Folders tab.""" |
830 | - self.patch(self.ui.folders, 'load', self._set_called) |
831 | + def test_entering_volumes_tab_loads_content(self): |
832 | + """The volumes info is loaded when entering the Volumes tab.""" |
833 | + self.patch(self.ui.volumes, 'load', self._set_called) |
834 | # clean backend calls |
835 | - self.ui.folders_button.clicked() |
836 | + self.ui.volumes_button.clicked() |
837 | |
838 | self.assertEqual(self._called, ((), {})) |
839 | |
840 | |
841 | === modified file 'ubuntuone/controlpanel/gtk/tests/test_widgets.py' |
842 | --- ubuntuone/controlpanel/gtk/tests/test_widgets.py 2010-12-18 20:16:18 +0000 |
843 | +++ ubuntuone/controlpanel/gtk/tests/test_widgets.py 2011-01-21 19:25:14 +0000 |
844 | @@ -156,7 +156,7 @@ |
845 | |
846 | def setUp(self): |
847 | super(PanelTitleTestCase, self).setUp() |
848 | - self.widget = widgets.PanelTitle(markup=self.TITLE) |
849 | + self.widget = widgets.PanelTitle(markup=self.TITLE, change_bg=True) |
850 | |
851 | win = widgets.gtk.Window() |
852 | win.add(self.widget) |
853 | @@ -196,13 +196,6 @@ |
854 | self.assertEqual(self.widget.label.get_line_wrap_mode(), |
855 | widgets.pango.WRAP_WORD) |
856 | |
857 | - def test_label_size_allocated_is_connected(self): |
858 | - """Label have the size-allocate signal connected.""" |
859 | - self.widget.label.emit('size-allocate', |
860 | - widgets.gtk.gdk.Rectangle(1, 2, 3, 4)) |
861 | - self.assertEqual(self.widget.label.get_size_request(), (3 - 2, -1), |
862 | - 'Label must have size-allocate connected.') |
863 | - |
864 | def test_label_padding(self): |
865 | """The label padding is correct.""" |
866 | self.assertEqual(self.widget.label.get_padding(), |
867 | |
868 | === modified file 'ubuntuone/controlpanel/gtk/widgets.py' |
869 | --- ubuntuone/controlpanel/gtk/widgets.py 2010-12-18 20:16:18 +0000 |
870 | +++ ubuntuone/controlpanel/gtk/widgets.py 2011-01-21 19:25:14 +0000 |
871 | @@ -108,27 +108,22 @@ |
872 | class PanelTitle(gtk.EventBox): |
873 | """A box with a given color and text.""" |
874 | |
875 | - def __init__(self, markup='', *args, **kwargs): |
876 | + def __init__(self, markup='', change_bg=False, *args, **kwargs): |
877 | super(PanelTitle, self).__init__(*args, **kwargs) |
878 | - self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(DEFAULT_BG)) |
879 | - |
880 | self.label = gtk.Label() |
881 | self.label.set_markup(markup) |
882 | self.label.set_padding(*DEFAULT_PADDING) |
883 | - self.label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.Color(DEFAULT_FG)) |
884 | self.label.set_property('xalign', 0.0) |
885 | self.label.set_line_wrap(True) |
886 | self.label.set_line_wrap_mode(pango.WRAP_WORD) |
887 | - self.label.connect('size-allocate', self.on_size_allocate) |
888 | + if change_bg: |
889 | + self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(DEFAULT_BG)) |
890 | + self.label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.Color(DEFAULT_FG)) |
891 | |
892 | self.add(self.label) |
893 | |
894 | self.show_all() |
895 | |
896 | - def on_size_allocate(self, widget, allocation): |
897 | - """The widget can re rezised, embrase it!.""" |
898 | - widget.set_size_request(allocation.width - 2, -1) |
899 | - |
900 | |
901 | # Modified from John Stowers' client-side-windows demo. |
902 | class GreyableBin(gtk.Bin): |
All in all, this is shaping up to be a huge leap forward from what people will see from our service. GREAT job.
A few comments:
- It confused me for a few minutes to have "X of Y used" on top, and "Z available" under "Mine". I had even taken a screenshot to report it as a bug, until I realised what it was. I wonder if we should unify? Screehshot: http:// ubuntuone. com/p/ZQL/
- How about moving the non-deletable Ubuntu One folder fixed to the top? It'll make it feel a bit more immutable!
- The icon next to the section "Mine" is of multiple people, which is odd, as I'm only one person :) Maybe the multi-person icon could be used for the "From Others" section, and "Mine" gets a single person?
I don't feel any of the above blocks this branch, they are open questions and suggestions that could be incorporated in future branches.