Merge lp:~nataliabidart/ubuntuone-control-panel/volumes-reborn into lp:ubuntuone-control-panel

Proposed by Natalia Bidart
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
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-control-panel-backend; DEBUG=True PYTHONPATH=. ./bin/ubuntuone-control-panel-backend

DEBUG=True PYTHONPATH=. ./bin/ubuntuone-control-panel-gtk

And play with the 'Cloud Storage' panel. Be careful that folder subcription/unsubscription is "real" :-)

To post a comment you must log in.
Revision history for this message
Martin Albisetti (beuno) wrote :

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.

review: Approve
Revision history for this message
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 :-)

review: Approve
57. By Natalia Bidart

Root folder is on top of others.

Revision history for this message
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://ubuntuone.com/p/ZQL/

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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):

Subscribers

People subscribed via source and target branches