Merge lp:~dobey/ubuntuone-control-panel/update-4-2 into lp:ubuntuone-control-panel/stable-4-2

Proposed by dobey
Status: Merged
Approved by: dobey
Approved revision: 383
Merged at revision: 383
Proposed branch: lp:~dobey/ubuntuone-control-panel/update-4-2
Merge into: lp:ubuntuone-control-panel/stable-4-2
Diff against target: 429 lines (+133/-141)
6 files modified
ubuntuone/controlpanel/gui/qt/controlpanel.py (+5/-0)
ubuntuone/controlpanel/gui/qt/share_links.py (+6/-1)
ubuntuone/controlpanel/gui/qt/share_links_search.py (+9/-16)
ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py (+42/-2)
ubuntuone/controlpanel/gui/qt/tests/test_share_links.py (+8/-0)
ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py (+63/-122)
To merge this branch: bzr merge lp:~dobey/ubuntuone-control-panel/update-4-2
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
Review via email: mp+155611@code.launchpad.net

Commit message

[Mike McCracken]

    - Work around Qt issue where search files popup frame was not hidden after switching to another tab. (LP: #1152388)
    - Use Qt timers to delay and coalesce IPC for files search. (LP: #1150316)

To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'ubuntuone/controlpanel/gui/qt/controlpanel.py'
--- ubuntuone/controlpanel/gui/qt/controlpanel.py 2012-10-18 21:45:05 +0000
+++ ubuntuone/controlpanel/gui/qt/controlpanel.py 2013-03-26 20:55:24 +0000
@@ -79,6 +79,11 @@
79 self.ui.tab_widget.setTabText(79 self.ui.tab_widget.setTabText(
80 self.ui.tab_widget.indexOf(self.ui.share_links_tab),80 self.ui.tab_widget.indexOf(self.ui.share_links_tab),
81 MAIN_SHARE_LINKS_TAB)81 MAIN_SHARE_LINKS_TAB)
82
83 # Workaround for bug LP: 1152388
84 handler = self.ui.share_links_tab.handle_current_tab_changed
85 self.ui.tab_widget.currentChanged.connect(handler)
86
82 self.ui.tab_widget.setTabText(87 self.ui.tab_widget.setTabText(
83 self.ui.tab_widget.indexOf(self.ui.devices_tab), MAIN_DEVICES_TAB)88 self.ui.tab_widget.indexOf(self.ui.devices_tab), MAIN_DEVICES_TAB)
84 self.ui.tab_widget.setTabText(89 self.ui.tab_widget.setTabText(
8590
=== modified file 'ubuntuone/controlpanel/gui/qt/share_links.py'
--- ubuntuone/controlpanel/gui/qt/share_links.py 2012-11-02 17:15:15 +0000
+++ ubuntuone/controlpanel/gui/qt/share_links.py 2013-03-26 20:55:24 +0000
@@ -1,6 +1,6 @@
1# -*- coding: utf-8 *-*1# -*- coding: utf-8 *-*
22
3# Copyright 2012 Canonical Ltd.3# Copyright 2012-2013 Canonical Ltd.
4#4#
5# This program is free software: you can redistribute it and/or modify it5# 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 published6# under the terms of the GNU General Public License version 3, as published
@@ -107,6 +107,11 @@
107 self.get_public_files()107 self.get_public_files()
108 self._enhanced_line.btn_operation.hide()108 self._enhanced_line.btn_operation.hide()
109109
110 def handle_current_tab_changed(self, index):
111 """Workaround for bug LP: 1152388"""
112 self.ui.line_search.clearFocus()
113 self.ui.line_search.popup.hide()
114
110 @inlineCallbacks115 @inlineCallbacks
111 def share_file(self, file_path):116 def share_file(self, file_path):
112 """Clean the previous file share details and publish file_path."""117 """Clean the previous file share details and publish file_path."""
113118
=== modified file 'ubuntuone/controlpanel/gui/qt/share_links_search.py'
--- ubuntuone/controlpanel/gui/qt/share_links_search.py 2013-01-24 21:35:59 +0000
+++ ubuntuone/controlpanel/gui/qt/share_links_search.py 2013-03-26 20:55:24 +0000
@@ -36,6 +36,7 @@
36HOME_DIR = ''36HOME_DIR = ''
37AVOID_SECOND_ITEM = 237AVOID_SECOND_ITEM = 2
38NORMAL_INCREMENT = 138NORMAL_INCREMENT = 1
39SEARCH_TYPING_DELAY = 100 # msec
3940
4041
41def get_system_icon_for_filename(file_path):42def get_system_icon_for_filename(file_path):
@@ -63,7 +64,6 @@
63 self.current_results = []64 self.current_results = []
64 self.page_index = 065 self.page_index = 0
65 self.page_size = 2066 self.page_size = 20
66 self.pending_call = None
67 self._post_key_event = {67 self._post_key_event = {
68 QtCore.Qt.Key_Escape: lambda *args: self.popup.hide(),68 QtCore.Qt.Key_Escape: lambda *args: self.popup.hide(),
69 QtCore.Qt.Key_Down: self._key_down_pressed,69 QtCore.Qt.Key_Down: self._key_down_pressed,
@@ -76,6 +76,10 @@
76 self.popup.list_widget.verticalScrollBar().valueChanged.connect(76 self.popup.list_widget.verticalScrollBar().valueChanged.connect(
77 self._scroll_fetch_more)77 self._scroll_fetch_more)
78 self.textChanged.connect(self.handle_text_changed)78 self.textChanged.connect(self.handle_text_changed)
79 self._do_search_timer = QtCore.QTimer()
80 self._do_search_timer.timeout.connect(self._do_search)
81 self._do_search_timer.setInterval(SEARCH_TYPING_DELAY)
82 self._do_search_timer.setSingleShot(True)
7983
80 self._get_home_path()84 self._get_home_path()
8185
@@ -94,30 +98,19 @@
94 def handle_text_changed(self, text):98 def handle_text_changed(self, text):
95 """Use delayed IPC to search for filenames after user stops typing."""99 """Use delayed IPC to search for filenames after user stops typing."""
96100
97 # Import here to avoid getting the wrong reactor due to import
98 # order. Save in class var to ease patching for tests:
99 if not self.qtreactor:
100 self.qtreactor = __import__('twisted').internet.reactor
101 text = unicode(text)101 text = unicode(text)
102 self.page_index = 0102 self.page_index = 0
103103
104 if text == '':104 if text == '':
105 self.popup.hide()105 self.popup.hide()
106 if self.pending_call and self.pending_call.active():106 self._do_search_timer.stop()
107 self.pending_call.cancel()107 return
108 return108
109109 self._do_search_timer.start(SEARCH_TYPING_DELAY)
110 if self.pending_call and self.pending_call.active():
111 self.pending_call.reset(0.1)
112 return
113
114 self.pending_call = self.qtreactor.callLater(0.1, self._do_search)
115110
116 @inlineCallbacks111 @inlineCallbacks
117 def _do_search(self):112 def _do_search(self):
118113
119 self.pending_call = None
120
121 search_text = unicode(self.text())114 search_text = unicode(self.text())
122 results = yield self.backend.search_files(search_text)115 results = yield self.backend.search_files(search_text)
123116
124117
=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py'
--- ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py 2012-10-17 07:21:28 +0000
+++ ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py 2013-03-26 20:55:24 +0000
@@ -1,6 +1,6 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2#2#
3# Copyright 2011-2012 Canonical Ltd.3# Copyright 2011-2013 Canonical Ltd.
4#4#
5# This program is free software: you can redistribute it and/or modify it5# 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 published6# under the terms of the GNU General Public License version 3, as published
@@ -19,10 +19,17 @@
19from __future__ import division19from __future__ import division
2020
21from twisted.internet import defer21from twisted.internet import defer
22from PyQt4 import QtCore
23
24from ubuntuone.controlpanel import backend, cache
25from ubuntuone.controlpanel.tests import TestCase
26from mock import call, Mock
27
28from ubuntuone.controlpanel.gui.qt import share_links
2229
23from ubuntuone.controlpanel.gui.qt import controlpanel as gui30from ubuntuone.controlpanel.gui.qt import controlpanel as gui
24from ubuntuone.controlpanel.gui.qt.tests import (31from ubuntuone.controlpanel.gui.qt.tests import (
25 SAMPLE_ACCOUNT_INFO, SAMPLE_NAME,32 FakedControlPanelBackend, SAMPLE_ACCOUNT_INFO, SAMPLE_NAME,
26)33)
27from ubuntuone.controlpanel.gui.qt.tests.test_ubuntuonebin import (34from ubuntuone.controlpanel.gui.qt.tests.test_ubuntuonebin import (
28 UbuntuOneBinTestCase,35 UbuntuOneBinTestCase,
@@ -204,6 +211,39 @@
204 self.ui.ui.wizard.pages[self.ui.ui.wizard.license_page])211 self.ui.ui.wizard.pages[self.ui.ui.wizard.license_page])
205212
206213
214class ControlPanelConnectionTestCase(TestCase):
215 """Test qt signal connections from controlpanel."""
216
217 @defer.inlineCallbacks
218 def setUp(self):
219 cache.Cache._shared_objects = {}
220 yield super(ControlPanelConnectionTestCase, self).setUp()
221 self.patch(backend, 'ControlBackend', FakedControlPanelBackend)
222
223 self.mock_handler = Mock(name='handle_current_tab_changed')
224 self.patch(share_links.ShareLinksPanel, 'handle_current_tab_changed',
225 self.mock_handler)
226
227 self.ui = gui.ControlPanel()
228 self.ui.show()
229 self.addCleanup(self.ui.hide)
230 #self.addCleanup(self.ui.deleteLater)
231 self.addCleanup(QtCore.QCoreApplication.instance().processEvents)
232
233 if getattr(self.ui, 'backend', None) is not None:
234 self.addCleanup(self.ui.backend._called.clear)
235
236 def test_popup_hides_when_switching_tab(self):
237 """Test that the share_links_tab gets the signal for changed tabs"""
238 folders_index = self.ui.ui.tab_widget.indexOf(self.ui.ui.folders_tab)
239 share_index = self.ui.ui.tab_widget.indexOf(self.ui.ui.share_links_tab)
240 self.ui.ui.tab_widget.setCurrentIndex(share_index)
241 self.ui.ui.tab_widget.setCurrentIndex(folders_index)
242
243 self.assertEqual(self.mock_handler.mock_calls,
244 [call(share_index), call(folders_index)])
245
246
207class ExternalLinkButtonsTestCase(ControlPanelTestCase):247class ExternalLinkButtonsTestCase(ControlPanelTestCase):
208 """The link in the go-to-web buttons are correct."""248 """The link in the go-to-web buttons are correct."""
209249
210250
=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_links.py'
--- ubuntuone/controlpanel/gui/qt/tests/test_share_links.py 2012-11-02 17:15:15 +0000
+++ ubuntuone/controlpanel/gui/qt/tests/test_share_links.py 2013-03-26 20:55:24 +0000
@@ -254,6 +254,14 @@
254 os.path.basename(file_path))254 os.path.basename(file_path))
255 self.assertEqual(widget.ui.lbl_path.text(), file_path)255 self.assertEqual(widget.ui.lbl_path.text(), file_path)
256256
257 def test_hide_popup_on_tab_changed(self):
258 """Test that the popup is hidden by the tab changed signal."""
259
260 self.ui.ui.line_search.popup.show()
261 self.ui.handle_current_tab_changed(0)
262 self.assertFalse(self.ui.ui.line_search.popup.isVisible())
263 self.assertFalse(self.ui.ui.line_search.hasFocus())
264
257265
258class ActionsButtonsTestCase(BaseTestCase):266class ActionsButtonsTestCase(BaseTestCase):
259 """Test the Actions Buttons."""267 """Test the Actions Buttons."""
260268
=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py'
--- ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py 2013-01-24 21:35:59 +0000
+++ ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py 2013-03-26 20:55:24 +0000
@@ -24,7 +24,7 @@
24from ubuntuone.controlpanel.gui.qt import share_links_search as gui24from ubuntuone.controlpanel.gui.qt import share_links_search as gui
25from ubuntuone.controlpanel.gui.qt.tests import BaseTestCase25from ubuntuone.controlpanel.gui.qt.tests import BaseTestCase
2626
27from mock import call, patch27from mock import patch
2828
29# pylint: disable=W021229# pylint: disable=W0212
3030
@@ -175,7 +175,7 @@
175175
176176
177class SearchingTestCase(BaseTestCase):177class SearchingTestCase(BaseTestCase):
178 """test _do_search by itself and with multiple calls to handle_text."""178 """Set up patches used by subclasses."""
179 class_ui = gui.SearchBox179 class_ui = gui.SearchBox
180180
181 @defer.inlineCallbacks181 @defer.inlineCallbacks
@@ -208,6 +208,9 @@
208 self.mock_load_items = self.load_items_patch.start()208 self.mock_load_items = self.load_items_patch.start()
209 self.addCleanup(self.load_items_patch.stop)209 self.addCleanup(self.load_items_patch.stop)
210210
211
212class DoSearchTestCase(SearchingTestCase):
213 """A subclass so that MultipleSearchingTestCase doesn't also call these."""
211 @defer.inlineCallbacks214 @defer.inlineCallbacks
212 def test_do_search_text_same(self):215 def test_do_search_text_same(self):
213 """If searchbox text same after search_files call, call load_items."""216 """If searchbox text same after search_files call, call load_items."""
@@ -231,11 +234,37 @@
231 yield d234 yield d
232 self.assertFalse(self.mock_load_items.called)235 self.assertFalse(self.mock_load_items.called)
233236
237
238class MultipleSearchingTestCase(SearchingTestCase):
239 """Test multiple fast calls to handle_text."""
240
241 @defer.inlineCallbacks
242 def setUp(self):
243 # do all this before calling super, because ui._do_search is
244 # connected to a Qt signal in the __init__ of the ui class,
245 # and we need to connect our patched version:
246 self.do_search_ended = defer.DeferredQueue()
247 # save unbound function because self.ui won't exist yet:
248 self.orig_do_search = gui.SearchBox._do_search
249
250 @defer.inlineCallbacks
251 def do_search_later():
252 # call unbound function with now-existing self.ui:
253 yield self.orig_do_search(self.ui)
254 self.do_search_ended.put("done")
255
256 self.do_search_patch = patch.object(gui.SearchBox, '_do_search')
257 self.do_search_mock = self.do_search_patch.start()
258 self.do_search_mock.side_effect = do_search_later
259 self.addCleanup(self.do_search_patch.stop)
260
261 yield super(MultipleSearchingTestCase, self).setUp()
262
234 @defer.inlineCallbacks263 @defer.inlineCallbacks
235 def test_multiple_searches_while_waiting(self):264 def test_multiple_searches_while_waiting(self):
236 """Only call load_items once despite multiple quick text changes."""265 """Only call load_items once despite multiple quick text changes."""
237266
238 # This test checks the case not covered in other tests, where267 # This test checks a case not covered earlier, where
239 # text changes once, then the text changes again, after268 # text changes once, then the text changes again, after
240 # _do_search is called but before search_files has returned.269 # _do_search is called but before search_files has returned.
241270
@@ -243,48 +272,37 @@
243 # be called twice, but _load_items should only be called once,272 # be called twice, but _load_items should only be called once,
244 # by the last call to _do_search.273 # by the last call to _do_search.
245274
246 self.do_search_ended = defer.DeferredQueue()275 # call once with original query
247 self.orig_do_search = self.ui._do_search276 self.mock_text.return_value = 'query'
248277 self.ui.handle_text_changed('query')
249 @defer.inlineCallbacks278
250 def do_search_later():279 # wait for the first delayed do_search call to call
251 yield self.orig_do_search()280 # search_files and check its arg for good measure
252 self.do_search_ended.put("done")281 search_files_query = yield self.search_files_started_q.get()
253282 self.assertEqual('query', search_files_query)
254 with patch.object(self.ui, '_do_search') as mock_do_search:283
255 mock_do_search.side_effect = do_search_later284 # first call to search_files is paused waiting for
256285 # search_files_done_q, simulating a long IPC call.
257 # call once with original query286 # the delayed call to do_search is no longer active.
258 self.mock_text.return_value = 'query'287
259 self.ui.handle_text_changed('query')288 # while we wait for search_files, the user changes the
260289 # query, scheduling a new delayed call to do_search:
261 # wait for the first delayed do_search call to call290 self.mock_text.return_value = 'query2'
262 # search_files and check its arg for good measure291 self.ui.handle_text_changed('query2')
263 search_files_query = yield self.search_files_started_q.get()292
264 self.assertEqual('query', search_files_query)293 # release both calls to search_files:
265294 self.search_files_done_q.put(['result1'])
266 # first call to search_files is paused waiting for295 self.search_files_done_q.put(['result2'])
267 # search_files_done_q, simulating a long IPC call.296
268 # the delayed call to do_search is no longer active.297 # wait for first delayed call to do_search to finish:
269298 yield self.do_search_ended.get()
270 # while we wait for search_files, the user changes the299
271 # query, scheduling a new delayed call to do_search:300 # check that the second call to search_files got the right
272 self.mock_text.return_value = 'query2'301 # text:
273 self.ui.handle_text_changed('query2')302 search_files_query = yield self.search_files_started_q.get()
274303 self.assertEqual('query2', search_files_query)
275 # release both calls to search_files:304 # wait for second do_search to end
276 self.search_files_done_q.put(['result1'])305 yield self.do_search_ended.get()
277 self.search_files_done_q.put(['result2'])
278
279 # wait for first delayed call to do_search to finish:
280 yield self.do_search_ended.get()
281
282 # check that the second call to search_files got the right
283 # text:
284 search_files_query = yield self.search_files_started_q.get()
285 self.assertEqual('query2', search_files_query)
286 # wait for second do_search to end
287 yield self.do_search_ended.get()
288306
289 # check that _load_items is only called once, and that it's307 # check that _load_items is only called once, and that it's
290 # using only the results from the last call to search_files:308 # using only the results from the last call to search_files:
@@ -292,83 +310,6 @@
292 self.assertEqual(['result2'], self.ui.current_results)310 self.assertEqual(['result2'], self.ui.current_results)
293311
294312
295class TextChangedTestCase(BaseTestCase):
296 """Test handle_text_changed scheduling pending calls."""
297
298 class_ui = gui.SearchBox
299
300 @defer.inlineCallbacks
301 def setUp(self):
302 yield super(TextChangedTestCase, self).setUp()
303
304 # patch this way instead of using decorators because we need
305 # to patch self.ui.popup, which isn't defined yet when the
306 # decorators run.
307 self.qtreactor_patch = patch.object(gui.SearchBox, 'qtreactor')
308 self.mock_qtreactor = self.qtreactor_patch.start()
309 self.addCleanup(self.qtreactor_patch.stop)
310
311 self.popup_patch = patch.object(self.ui, 'popup')
312 self.mock_popup = self.popup_patch.start()
313 self.addCleanup(self.popup_patch.stop)
314
315 def test_empty_text_no_pending(self):
316 """arg='' with no pending call only hides the popup."""
317 self.ui.handle_text_changed('')
318 self.mock_popup.hide.assert_called_once_with()
319 self.assertFalse(self.mock_qtreactor.callLater.called)
320
321 def test_empty_text_with_pending(self):
322 """arg='' with a pending call hides the popup and cancels pending."""
323 with patch.object(self.ui, 'pending_call') as mock_pending_call:
324 mock_pending_call.active.return_value = True
325 self.ui.handle_text_changed('')
326 mock_pending_call.active.assert_called_once_with()
327 mock_pending_call.cancel.assert_called_once_with()
328 self.mock_popup.hide.assert_called_once_with()
329 self.assertFalse(self.mock_qtreactor.callLater.called)
330
331 def test_empty_text_with_inactive_pending(self):
332 """arg='' with a pending call hides the popup and cancels pending."""
333 with patch.object(self.ui, 'pending_call') as mock_pending_call:
334 mock_pending_call.active.return_value = False
335 self.ui.handle_text_changed('')
336 mock_pending_call.active.assert_called_once_with()
337 self.assertFalse(mock_pending_call.cancel.called)
338 self.mock_popup.hide.assert_called_once_with()
339 self.assertFalse(self.mock_qtreactor.callLater.called)
340
341 def test_nonempty_text_with_no_pending(self):
342 """arg='b' with no pending call schedules a call."""
343 self.ui.handle_text_changed('b')
344 self.assertEqual([call(0.1, self.ui._do_search)],
345 self.mock_qtreactor.callLater.mock_calls)
346
347 def test_nonempty_text_with_inactive_pending(self):
348 """call an inactive (called already) call schedules another call."""
349 with patch.object(self.ui, 'pending_call') as mock_pending_call:
350 mock_pending_call.active.return_value = False
351 self.ui.handle_text_changed('b')
352 mock_pending_call.active.assert_called_once_with()
353 self.assertFalse(mock_pending_call.reset.called)
354
355 self.assertFalse(self.mock_popup.hide.called)
356
357 self.assertEqual([call(0.1, self.ui._do_search)],
358 self.mock_qtreactor.callLater.mock_calls)
359
360 def test_nonempty_text_with_pending(self):
361 """arg='' with a pending call hides the popup and resets pending."""
362 with patch.object(self.ui, 'pending_call') as mock_pending_call:
363 mock_pending_call.active.return_value = True
364 self.ui.handle_text_changed('b')
365 mock_pending_call.active.assert_called_once_with()
366 mock_pending_call.reset.assert_called_once_with(0.1)
367
368 self.assertFalse(self.mock_popup.hide.called)
369 self.assertFalse(self.mock_qtreactor.callLater.called)
370
371
372class FileItemTestCase(BaseTestCase):313class FileItemTestCase(BaseTestCase):
373 """Test the File Item."""314 """Test the File Item."""
374315

Subscribers

People subscribed via source and target branches

to all changes: