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
1=== modified file 'ubuntuone/controlpanel/gui/qt/controlpanel.py'
2--- ubuntuone/controlpanel/gui/qt/controlpanel.py 2012-10-18 21:45:05 +0000
3+++ ubuntuone/controlpanel/gui/qt/controlpanel.py 2013-03-26 20:55:24 +0000
4@@ -79,6 +79,11 @@
5 self.ui.tab_widget.setTabText(
6 self.ui.tab_widget.indexOf(self.ui.share_links_tab),
7 MAIN_SHARE_LINKS_TAB)
8+
9+ # Workaround for bug LP: 1152388
10+ handler = self.ui.share_links_tab.handle_current_tab_changed
11+ self.ui.tab_widget.currentChanged.connect(handler)
12+
13 self.ui.tab_widget.setTabText(
14 self.ui.tab_widget.indexOf(self.ui.devices_tab), MAIN_DEVICES_TAB)
15 self.ui.tab_widget.setTabText(
16
17=== modified file 'ubuntuone/controlpanel/gui/qt/share_links.py'
18--- ubuntuone/controlpanel/gui/qt/share_links.py 2012-11-02 17:15:15 +0000
19+++ ubuntuone/controlpanel/gui/qt/share_links.py 2013-03-26 20:55:24 +0000
20@@ -1,6 +1,6 @@
21 # -*- coding: utf-8 *-*
22
23-# Copyright 2012 Canonical Ltd.
24+# Copyright 2012-2013 Canonical Ltd.
25 #
26 # This program is free software: you can redistribute it and/or modify it
27 # under the terms of the GNU General Public License version 3, as published
28@@ -107,6 +107,11 @@
29 self.get_public_files()
30 self._enhanced_line.btn_operation.hide()
31
32+ def handle_current_tab_changed(self, index):
33+ """Workaround for bug LP: 1152388"""
34+ self.ui.line_search.clearFocus()
35+ self.ui.line_search.popup.hide()
36+
37 @inlineCallbacks
38 def share_file(self, file_path):
39 """Clean the previous file share details and publish file_path."""
40
41=== modified file 'ubuntuone/controlpanel/gui/qt/share_links_search.py'
42--- ubuntuone/controlpanel/gui/qt/share_links_search.py 2013-01-24 21:35:59 +0000
43+++ ubuntuone/controlpanel/gui/qt/share_links_search.py 2013-03-26 20:55:24 +0000
44@@ -36,6 +36,7 @@
45 HOME_DIR = ''
46 AVOID_SECOND_ITEM = 2
47 NORMAL_INCREMENT = 1
48+SEARCH_TYPING_DELAY = 100 # msec
49
50
51 def get_system_icon_for_filename(file_path):
52@@ -63,7 +64,6 @@
53 self.current_results = []
54 self.page_index = 0
55 self.page_size = 20
56- self.pending_call = None
57 self._post_key_event = {
58 QtCore.Qt.Key_Escape: lambda *args: self.popup.hide(),
59 QtCore.Qt.Key_Down: self._key_down_pressed,
60@@ -76,6 +76,10 @@
61 self.popup.list_widget.verticalScrollBar().valueChanged.connect(
62 self._scroll_fetch_more)
63 self.textChanged.connect(self.handle_text_changed)
64+ self._do_search_timer = QtCore.QTimer()
65+ self._do_search_timer.timeout.connect(self._do_search)
66+ self._do_search_timer.setInterval(SEARCH_TYPING_DELAY)
67+ self._do_search_timer.setSingleShot(True)
68
69 self._get_home_path()
70
71@@ -94,30 +98,19 @@
72 def handle_text_changed(self, text):
73 """Use delayed IPC to search for filenames after user stops typing."""
74
75- # Import here to avoid getting the wrong reactor due to import
76- # order. Save in class var to ease patching for tests:
77- if not self.qtreactor:
78- self.qtreactor = __import__('twisted').internet.reactor
79 text = unicode(text)
80 self.page_index = 0
81
82 if text == '':
83 self.popup.hide()
84- if self.pending_call and self.pending_call.active():
85- self.pending_call.cancel()
86- return
87-
88- if self.pending_call and self.pending_call.active():
89- self.pending_call.reset(0.1)
90- return
91-
92- self.pending_call = self.qtreactor.callLater(0.1, self._do_search)
93+ self._do_search_timer.stop()
94+ return
95+
96+ self._do_search_timer.start(SEARCH_TYPING_DELAY)
97
98 @inlineCallbacks
99 def _do_search(self):
100
101- self.pending_call = None
102-
103 search_text = unicode(self.text())
104 results = yield self.backend.search_files(search_text)
105
106
107=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py'
108--- ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py 2012-10-17 07:21:28 +0000
109+++ ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py 2013-03-26 20:55:24 +0000
110@@ -1,6 +1,6 @@
111 # -*- coding: utf-8 -*-
112 #
113-# Copyright 2011-2012 Canonical Ltd.
114+# Copyright 2011-2013 Canonical Ltd.
115 #
116 # This program is free software: you can redistribute it and/or modify it
117 # under the terms of the GNU General Public License version 3, as published
118@@ -19,10 +19,17 @@
119 from __future__ import division
120
121 from twisted.internet import defer
122+from PyQt4 import QtCore
123+
124+from ubuntuone.controlpanel import backend, cache
125+from ubuntuone.controlpanel.tests import TestCase
126+from mock import call, Mock
127+
128+from ubuntuone.controlpanel.gui.qt import share_links
129
130 from ubuntuone.controlpanel.gui.qt import controlpanel as gui
131 from ubuntuone.controlpanel.gui.qt.tests import (
132- SAMPLE_ACCOUNT_INFO, SAMPLE_NAME,
133+ FakedControlPanelBackend, SAMPLE_ACCOUNT_INFO, SAMPLE_NAME,
134 )
135 from ubuntuone.controlpanel.gui.qt.tests.test_ubuntuonebin import (
136 UbuntuOneBinTestCase,
137@@ -204,6 +211,39 @@
138 self.ui.ui.wizard.pages[self.ui.ui.wizard.license_page])
139
140
141+class ControlPanelConnectionTestCase(TestCase):
142+ """Test qt signal connections from controlpanel."""
143+
144+ @defer.inlineCallbacks
145+ def setUp(self):
146+ cache.Cache._shared_objects = {}
147+ yield super(ControlPanelConnectionTestCase, self).setUp()
148+ self.patch(backend, 'ControlBackend', FakedControlPanelBackend)
149+
150+ self.mock_handler = Mock(name='handle_current_tab_changed')
151+ self.patch(share_links.ShareLinksPanel, 'handle_current_tab_changed',
152+ self.mock_handler)
153+
154+ self.ui = gui.ControlPanel()
155+ self.ui.show()
156+ self.addCleanup(self.ui.hide)
157+ #self.addCleanup(self.ui.deleteLater)
158+ self.addCleanup(QtCore.QCoreApplication.instance().processEvents)
159+
160+ if getattr(self.ui, 'backend', None) is not None:
161+ self.addCleanup(self.ui.backend._called.clear)
162+
163+ def test_popup_hides_when_switching_tab(self):
164+ """Test that the share_links_tab gets the signal for changed tabs"""
165+ folders_index = self.ui.ui.tab_widget.indexOf(self.ui.ui.folders_tab)
166+ share_index = self.ui.ui.tab_widget.indexOf(self.ui.ui.share_links_tab)
167+ self.ui.ui.tab_widget.setCurrentIndex(share_index)
168+ self.ui.ui.tab_widget.setCurrentIndex(folders_index)
169+
170+ self.assertEqual(self.mock_handler.mock_calls,
171+ [call(share_index), call(folders_index)])
172+
173+
174 class ExternalLinkButtonsTestCase(ControlPanelTestCase):
175 """The link in the go-to-web buttons are correct."""
176
177
178=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_links.py'
179--- ubuntuone/controlpanel/gui/qt/tests/test_share_links.py 2012-11-02 17:15:15 +0000
180+++ ubuntuone/controlpanel/gui/qt/tests/test_share_links.py 2013-03-26 20:55:24 +0000
181@@ -254,6 +254,14 @@
182 os.path.basename(file_path))
183 self.assertEqual(widget.ui.lbl_path.text(), file_path)
184
185+ def test_hide_popup_on_tab_changed(self):
186+ """Test that the popup is hidden by the tab changed signal."""
187+
188+ self.ui.ui.line_search.popup.show()
189+ self.ui.handle_current_tab_changed(0)
190+ self.assertFalse(self.ui.ui.line_search.popup.isVisible())
191+ self.assertFalse(self.ui.ui.line_search.hasFocus())
192+
193
194 class ActionsButtonsTestCase(BaseTestCase):
195 """Test the Actions Buttons."""
196
197=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py'
198--- ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py 2013-01-24 21:35:59 +0000
199+++ ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py 2013-03-26 20:55:24 +0000
200@@ -24,7 +24,7 @@
201 from ubuntuone.controlpanel.gui.qt import share_links_search as gui
202 from ubuntuone.controlpanel.gui.qt.tests import BaseTestCase
203
204-from mock import call, patch
205+from mock import patch
206
207 # pylint: disable=W0212
208
209@@ -175,7 +175,7 @@
210
211
212 class SearchingTestCase(BaseTestCase):
213- """test _do_search by itself and with multiple calls to handle_text."""
214+ """Set up patches used by subclasses."""
215 class_ui = gui.SearchBox
216
217 @defer.inlineCallbacks
218@@ -208,6 +208,9 @@
219 self.mock_load_items = self.load_items_patch.start()
220 self.addCleanup(self.load_items_patch.stop)
221
222+
223+class DoSearchTestCase(SearchingTestCase):
224+ """A subclass so that MultipleSearchingTestCase doesn't also call these."""
225 @defer.inlineCallbacks
226 def test_do_search_text_same(self):
227 """If searchbox text same after search_files call, call load_items."""
228@@ -231,11 +234,37 @@
229 yield d
230 self.assertFalse(self.mock_load_items.called)
231
232+
233+class MultipleSearchingTestCase(SearchingTestCase):
234+ """Test multiple fast calls to handle_text."""
235+
236+ @defer.inlineCallbacks
237+ def setUp(self):
238+ # do all this before calling super, because ui._do_search is
239+ # connected to a Qt signal in the __init__ of the ui class,
240+ # and we need to connect our patched version:
241+ self.do_search_ended = defer.DeferredQueue()
242+ # save unbound function because self.ui won't exist yet:
243+ self.orig_do_search = gui.SearchBox._do_search
244+
245+ @defer.inlineCallbacks
246+ def do_search_later():
247+ # call unbound function with now-existing self.ui:
248+ yield self.orig_do_search(self.ui)
249+ self.do_search_ended.put("done")
250+
251+ self.do_search_patch = patch.object(gui.SearchBox, '_do_search')
252+ self.do_search_mock = self.do_search_patch.start()
253+ self.do_search_mock.side_effect = do_search_later
254+ self.addCleanup(self.do_search_patch.stop)
255+
256+ yield super(MultipleSearchingTestCase, self).setUp()
257+
258 @defer.inlineCallbacks
259 def test_multiple_searches_while_waiting(self):
260 """Only call load_items once despite multiple quick text changes."""
261
262- # This test checks the case not covered in other tests, where
263+ # This test checks a case not covered earlier, where
264 # text changes once, then the text changes again, after
265 # _do_search is called but before search_files has returned.
266
267@@ -243,48 +272,37 @@
268 # be called twice, but _load_items should only be called once,
269 # by the last call to _do_search.
270
271- self.do_search_ended = defer.DeferredQueue()
272- self.orig_do_search = self.ui._do_search
273-
274- @defer.inlineCallbacks
275- def do_search_later():
276- yield self.orig_do_search()
277- self.do_search_ended.put("done")
278-
279- with patch.object(self.ui, '_do_search') as mock_do_search:
280- mock_do_search.side_effect = do_search_later
281-
282- # call once with original query
283- self.mock_text.return_value = 'query'
284- self.ui.handle_text_changed('query')
285-
286- # wait for the first delayed do_search call to call
287- # search_files and check its arg for good measure
288- search_files_query = yield self.search_files_started_q.get()
289- self.assertEqual('query', search_files_query)
290-
291- # first call to search_files is paused waiting for
292- # search_files_done_q, simulating a long IPC call.
293- # the delayed call to do_search is no longer active.
294-
295- # while we wait for search_files, the user changes the
296- # query, scheduling a new delayed call to do_search:
297- self.mock_text.return_value = 'query2'
298- self.ui.handle_text_changed('query2')
299-
300- # release both calls to search_files:
301- self.search_files_done_q.put(['result1'])
302- self.search_files_done_q.put(['result2'])
303-
304- # wait for first delayed call to do_search to finish:
305- yield self.do_search_ended.get()
306-
307- # check that the second call to search_files got the right
308- # text:
309- search_files_query = yield self.search_files_started_q.get()
310- self.assertEqual('query2', search_files_query)
311- # wait for second do_search to end
312- yield self.do_search_ended.get()
313+ # call once with original query
314+ self.mock_text.return_value = 'query'
315+ self.ui.handle_text_changed('query')
316+
317+ # wait for the first delayed do_search call to call
318+ # search_files and check its arg for good measure
319+ search_files_query = yield self.search_files_started_q.get()
320+ self.assertEqual('query', search_files_query)
321+
322+ # first call to search_files is paused waiting for
323+ # search_files_done_q, simulating a long IPC call.
324+ # the delayed call to do_search is no longer active.
325+
326+ # while we wait for search_files, the user changes the
327+ # query, scheduling a new delayed call to do_search:
328+ self.mock_text.return_value = 'query2'
329+ self.ui.handle_text_changed('query2')
330+
331+ # release both calls to search_files:
332+ self.search_files_done_q.put(['result1'])
333+ self.search_files_done_q.put(['result2'])
334+
335+ # wait for first delayed call to do_search to finish:
336+ yield self.do_search_ended.get()
337+
338+ # check that the second call to search_files got the right
339+ # text:
340+ search_files_query = yield self.search_files_started_q.get()
341+ self.assertEqual('query2', search_files_query)
342+ # wait for second do_search to end
343+ yield self.do_search_ended.get()
344
345 # check that _load_items is only called once, and that it's
346 # using only the results from the last call to search_files:
347@@ -292,83 +310,6 @@
348 self.assertEqual(['result2'], self.ui.current_results)
349
350
351-class TextChangedTestCase(BaseTestCase):
352- """Test handle_text_changed scheduling pending calls."""
353-
354- class_ui = gui.SearchBox
355-
356- @defer.inlineCallbacks
357- def setUp(self):
358- yield super(TextChangedTestCase, self).setUp()
359-
360- # patch this way instead of using decorators because we need
361- # to patch self.ui.popup, which isn't defined yet when the
362- # decorators run.
363- self.qtreactor_patch = patch.object(gui.SearchBox, 'qtreactor')
364- self.mock_qtreactor = self.qtreactor_patch.start()
365- self.addCleanup(self.qtreactor_patch.stop)
366-
367- self.popup_patch = patch.object(self.ui, 'popup')
368- self.mock_popup = self.popup_patch.start()
369- self.addCleanup(self.popup_patch.stop)
370-
371- def test_empty_text_no_pending(self):
372- """arg='' with no pending call only hides the popup."""
373- self.ui.handle_text_changed('')
374- self.mock_popup.hide.assert_called_once_with()
375- self.assertFalse(self.mock_qtreactor.callLater.called)
376-
377- def test_empty_text_with_pending(self):
378- """arg='' with a pending call hides the popup and cancels pending."""
379- with patch.object(self.ui, 'pending_call') as mock_pending_call:
380- mock_pending_call.active.return_value = True
381- self.ui.handle_text_changed('')
382- mock_pending_call.active.assert_called_once_with()
383- mock_pending_call.cancel.assert_called_once_with()
384- self.mock_popup.hide.assert_called_once_with()
385- self.assertFalse(self.mock_qtreactor.callLater.called)
386-
387- def test_empty_text_with_inactive_pending(self):
388- """arg='' with a pending call hides the popup and cancels pending."""
389- with patch.object(self.ui, 'pending_call') as mock_pending_call:
390- mock_pending_call.active.return_value = False
391- self.ui.handle_text_changed('')
392- mock_pending_call.active.assert_called_once_with()
393- self.assertFalse(mock_pending_call.cancel.called)
394- self.mock_popup.hide.assert_called_once_with()
395- self.assertFalse(self.mock_qtreactor.callLater.called)
396-
397- def test_nonempty_text_with_no_pending(self):
398- """arg='b' with no pending call schedules a call."""
399- self.ui.handle_text_changed('b')
400- self.assertEqual([call(0.1, self.ui._do_search)],
401- self.mock_qtreactor.callLater.mock_calls)
402-
403- def test_nonempty_text_with_inactive_pending(self):
404- """call an inactive (called already) call schedules another call."""
405- with patch.object(self.ui, 'pending_call') as mock_pending_call:
406- mock_pending_call.active.return_value = False
407- self.ui.handle_text_changed('b')
408- mock_pending_call.active.assert_called_once_with()
409- self.assertFalse(mock_pending_call.reset.called)
410-
411- self.assertFalse(self.mock_popup.hide.called)
412-
413- self.assertEqual([call(0.1, self.ui._do_search)],
414- self.mock_qtreactor.callLater.mock_calls)
415-
416- def test_nonempty_text_with_pending(self):
417- """arg='' with a pending call hides the popup and resets pending."""
418- with patch.object(self.ui, 'pending_call') as mock_pending_call:
419- mock_pending_call.active.return_value = True
420- self.ui.handle_text_changed('b')
421- mock_pending_call.active.assert_called_once_with()
422- mock_pending_call.reset.assert_called_once_with(0.1)
423-
424- self.assertFalse(self.mock_popup.hide.called)
425- self.assertFalse(self.mock_qtreactor.callLater.called)
426-
427-
428 class FileItemTestCase(BaseTestCase):
429 """Test the File Item."""
430

Subscribers

People subscribed via source and target branches

to all changes: