Merge lp:~sinzui/bzr-gtk/ui-factory into lp:bzr-gtk
- ui-factory
- Merge into gtk3
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Jelmer Vernooij | ||||
Approved revision: | 781 | ||||
Merged at revision: | 780 | ||||
Proposed branch: | lp:~sinzui/bzr-gtk/ui-factory | ||||
Merge into: | lp:bzr-gtk | ||||
Diff against target: |
873 lines (+582/-110) 4 files modified
setup.py (+41/-24) tests/__init__.py (+51/-19) tests/test_ui.py (+375/-0) ui.py (+115/-67) |
||||
To merge this branch: | bzr merge lp:~sinzui/bzr-gtk/ui-factory | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jelmer Vernooij (community) | Approve | ||
Review via email: mp+94866@code.launchpad.net |
Commit message
Description of the change
Add GtkUIFactory implementations and missing tests.
This branch was extracted from my effort to guarantee that the progress bar
shows when I use gpush.
-------
RULES
* Add tests for all the implemented classes and specifically GtkUIFactory.
* Add ._progress_
needed to update the progress widget.
* Add the message, warning, and error dialogs.
* Maybe update the Confirmation dialog to extend MessageDialog?
* ADDENDUM: Let me run just one test module!
LINT
added:
tests/
modified:
setup.py
tests/
ui.py
TEST
./setup.py check
or
./setup.py check -m test_ui
IMPLEMENTATION
I was frustrated running the whole suite which takes 18 seconds on my
computer. I updated the test runner code to use existing 'discover'
option to run the whole suite, or accept a single module as an arg. The
ui tests complete in 0.107s on my computer.
setup.py
tests/
I First added tests for the existing classes. I have never used this
many mocks in a test suite before, they definitely make the tests fast,
though I thought I might have used too many. I tried removing a few but
the tests could become may times slower or just never complete without
forcing the Gtk.main_loop to run.
tests/
I then added test for new methods and refactored some of the code.
* I extracted the main_iteration code in one method to a decorator
function that I used on several progress bar methods to ensure
the UI updates immediately.
* I added the Info, Warning, and Error dialogs and refactored PromptDialog
to extend Gtk.MessageDialog. This provides an icon with the text
message, as well a guarantees that the spacing and layout conforms
to Gnome HIG.
* After I extracted the main_loop iteration rules from update(), changed
self.fraction to fraction because nothing used it. I changed the error
to a ValueError because it can only be caused by insane inputs.
* I extracted the common progress window and panel methods to
ProgressConta
tests to that used a similar mixin to verify their function.
* I updated ProgressPanel to GtkBox since GtkHBox is deprecated.
* I added show_user_warning, which is almost identical to the TextUI
implementation.
* I added ._progress_
the gpush progress bar show sooner and update more often.
ui.py
tests/
- 779. By Curtis Hovey
-
Merged GtkUIFactory additions and tweaks.
Jelmer Vernooij (jelmer) wrote : | # |
This seems to break 'bzr selftest -s bp.gtk'.
Curtis Hovey (sinzui) wrote : | # |
I suck. I will fix this right away.
- 780. By Curtis Hovey
-
Support selftest, check, and check -m
- 781. By Curtis Hovey
-
DRY.
Curtis Hovey (sinzui) wrote : | # |
I merged this branch into trunk a few hours before you discovered my badness. I have a branch that fixes this issue: https:/
Curtis Hovey (sinzui) wrote : | # |
Oh never mind. either I never pushed my branch or you reverted. I have pushed my changes to this branch and these are my changes I reported in the other MP that I am deleting.
RULES
* Consider reverting the changes if the fix cannot be made in a few
hours
* Setup.py can set the module to None, self tests passes a module
object, and the user -m arg can be a string
* Only filter the discovered tests if the module is a basestring.
* ADDENDUM: It is trick using the native discover feature because
it builds a module name in a different name space; not
rules as discover might work to construct a module name that
always works.
TEST
./setup.py check
./setup.py check -m ui
BZR_
IMPLEMENTATION
I changed the default module in setup.py to None so that it was easy
to detect a basestring to select a test. I wrote my own function to
find tests module names because discover was not construction the module
name of bzrlib.plugins.gtk when run under selftest. The function uses
the same file matching rules as discover, but always uses the current
module name to construct the test module name. There are still few lines
of code to load tests then there were before.
Preview Diff
1 | === modified file 'setup.py' |
2 | --- setup.py 2011-11-06 00:49:02 +0000 |
3 | +++ setup.py 2012-02-28 18:15:21 +0000 |
4 | @@ -1,33 +1,47 @@ |
5 | #!/usr/bin/python |
6 | """GTK+ Frontends for various Bazaar commands.""" |
7 | |
8 | -from info import * |
9 | +import os |
10 | +import sys |
11 | + |
12 | +from info import bzr_plugin_version |
13 | |
14 | from distutils.core import setup, Command |
15 | from distutils.command.install_data import install_data |
16 | from distutils.command.build import build |
17 | from distutils.command.sdist import sdist |
18 | try: |
19 | - from DistUtilsExtra.command import * |
20 | + from DistUtilsExtra.command import build_i18n |
21 | except ImportError: |
22 | - # Python distutils extra is not available. |
23 | - class cmd_build_i18n(Command): |
24 | + # Python distutils extra is not available. |
25 | + class cmd_build_i18n(build): |
26 | + user_options = [] |
27 | + |
28 | + def initialize_options(self): |
29 | + self.domain = None |
30 | + self.desktop_files = None |
31 | + |
32 | + def finalize_options(self): |
33 | + pass |
34 | + |
35 | def run(self): |
36 | - print >> sys.stderr, "For internationalization support you'll need to install https://launchpad.net/python-distutils-extra" |
37 | + print >> sys.stderr, ( |
38 | + "For internationalization support you'll need to install " |
39 | + "https://launchpad.net/python-distutils-extra") |
40 | else: |
41 | # Use build_i18n from DistUtilsExtra |
42 | cmd_build_i18n = build_i18n.build_i18n |
43 | |
44 | -import os |
45 | -import sys |
46 | |
47 | class Check(Command): |
48 | description = "Run unit tests" |
49 | |
50 | - user_options = [] |
51 | + user_options = [ |
52 | + ('module=', 'm', 'The test module to run'), |
53 | + ] |
54 | |
55 | def initialize_options(self): |
56 | - pass |
57 | + self.module = None |
58 | |
59 | def finalize_options(self): |
60 | pass |
61 | @@ -38,11 +52,12 @@ |
62 | def run(self): |
63 | from bzrlib.tests import TestLoader, TestSuite, TextTestRunner |
64 | from bzrlib.plugin import PluginImporter |
65 | - PluginImporter.specific_paths["bzrlib.plugins.gtk"] = os.path.dirname(__file__) |
66 | + PluginImporter.specific_paths["bzrlib.plugins.gtk"] = os.path.dirname( |
67 | + __file__) |
68 | from bzrlib.plugins.gtk.tests import load_tests |
69 | suite = TestSuite() |
70 | loader = TestLoader() |
71 | - load_tests(suite, None, loader) |
72 | + load_tests(suite, self.module, loader) |
73 | runner = TextTestRunner() |
74 | result = runner.run(suite) |
75 | return result.wasSuccessful() |
76 | @@ -63,7 +78,8 @@ |
77 | return 'build_credits' |
78 | |
79 | def run(self): |
80 | - from bzrlib.plugin import load_plugins; load_plugins() |
81 | + from bzrlib.plugin import load_plugins |
82 | + load_plugins() |
83 | from bzrlib.branch import Branch |
84 | from bzrlib.plugins.stats.cmds import find_credits |
85 | |
86 | @@ -126,15 +142,15 @@ |
87 | version = bzr_plugin_version[:3] |
88 | version_string = ".".join([str(x) for x in version]) |
89 | setup( |
90 | - name = "bzr-gtk", |
91 | - version = version_string, |
92 | - maintainer = "Jelmer Vernooij", |
93 | - maintainer_email = "jelmer@samba.org", |
94 | - description = "GTK+ Frontends for various Bazaar commands", |
95 | - license = "GNU GPL v2 or later", |
96 | - scripts = ['bzr-handle-patch', 'bzr-notify'], |
97 | - url = "http://bazaar-vcs.org/BzrGtk", |
98 | - package_dir = { |
99 | + name="bzr-gtk", |
100 | + version=version_string, |
101 | + maintainer="Jelmer Vernooij", |
102 | + maintainer_email="jelmer@samba.org", |
103 | + description="GTK+ Frontends for various Bazaar commands", |
104 | + license="GNU GPL v2 or later", |
105 | + scripts=['bzr-handle-patch', 'bzr-notify'], |
106 | + url="http://bazaar-vcs.org/BzrGtk", |
107 | + package_dir={ |
108 | "bzrlib.plugins.gtk": ".", |
109 | "bzrlib.plugins.gtk.viz": "viz", |
110 | "bzrlib.plugins.gtk.annotate": "annotate", |
111 | @@ -142,7 +158,7 @@ |
112 | "bzrlib.plugins.gtk.branchview": "branchview", |
113 | "bzrlib.plugins.gtk.preferences": "preferences", |
114 | }, |
115 | - packages = [ |
116 | + packages=[ |
117 | "bzrlib.plugins.gtk", |
118 | "bzrlib.plugins.gtk.viz", |
119 | "bzrlib.plugins.gtk.annotate", |
120 | @@ -150,7 +166,7 @@ |
121 | "bzrlib.plugins.gtk.branchview", |
122 | "bzrlib.plugins.gtk.preferences", |
123 | ], |
124 | - data_files=[ ('share/bzr-gtk', ['credits.pickle']), |
125 | + data_files=[('share/bzr-gtk', ['credits.pickle']), |
126 | ('share/bzr-gtk/icons', ['icons/commit.png', |
127 | 'icons/commit16.png', |
128 | 'icons/diff.png', |
129 | @@ -176,7 +192,8 @@ |
130 | 'bzr-notify.desktop']), |
131 | ('share/application-registry', ['bzr-gtk.applications']), |
132 | ('share/pixmaps', ['icons/bzr-icon-64.png']), |
133 | - ('share/icons/hicolor/scalable/apps', ['icons/bzr-panel.svg']), |
134 | + ('share/icons/hicolor/scalable/apps', |
135 | + ['icons/bzr-panel.svg']), |
136 | ('share/icons/hicolor/scalable/emblems', |
137 | ['icons/emblem-bzr-added.svg', |
138 | 'icons/emblem-bzr-conflict.svg', |
139 | |
140 | === modified file 'tests/__init__.py' |
141 | --- tests/__init__.py 2012-02-03 18:59:38 +0000 |
142 | +++ tests/__init__.py 2012-02-28 18:15:21 +0000 |
143 | @@ -14,40 +14,72 @@ |
144 | # along with this program; if not, write to the Free Software |
145 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
146 | |
147 | +__all__ = [ |
148 | + 'load_tests', |
149 | + 'MockMethod', |
150 | + 'MockProperty', |
151 | + ] |
152 | + |
153 | +import os |
154 | + |
155 | + |
156 | +def discover_test_names(module_or_name): |
157 | + if isinstance(module_or_name, basestring): |
158 | + match = module_or_name |
159 | + else: |
160 | + match = '' |
161 | + file_names = os.listdir(os.path.dirname(__file__)) |
162 | + test_names = set() |
163 | + for file_name in file_names: |
164 | + name, ext = os.path.splitext(file_name) |
165 | + if name.startswith('test_') and ext == '.py' and match in name: |
166 | + test_names.add("%s.%s" % (__name__, name)) |
167 | + return test_names |
168 | + |
169 | |
170 | def load_tests(basic_tests, module, loader): |
171 | - testmod_names = [ |
172 | - 'test_annotate_config', |
173 | - 'test_avatarsbox', |
174 | - 'test_commit', |
175 | - 'test_diff', |
176 | - 'test_history', |
177 | - 'test_graphcell', |
178 | - 'test_linegraph', |
179 | - 'test_notify', |
180 | - 'test_revisionview', |
181 | - 'test_treemodel', |
182 | - ] |
183 | - |
184 | - basic_tests.addTest(loader.loadTestsFromModuleNames( |
185 | - ["%s.%s" % (__name__, tmn) for tmn in testmod_names])) |
186 | + test_names = discover_test_names(module) |
187 | + basic_tests.addTest(loader.loadTestsFromModuleNames(test_names)) |
188 | return basic_tests |
189 | |
190 | |
191 | -class MockMethod(): |
192 | +class MockMethod(object): |
193 | |
194 | @classmethod |
195 | - def bind(klass, test_instance, obj, method_name): |
196 | + def bind(klass, test_instance, obj, method_name, return_value=None): |
197 | original_method = getattr(obj, method_name) |
198 | test_instance.addCleanup(setattr, obj, method_name, original_method) |
199 | - setattr(obj, method_name, klass()) |
200 | + setattr(obj, method_name, klass(return_value)) |
201 | |
202 | - def __init__(self): |
203 | + def __init__(self, return_value=None): |
204 | self.called = False |
205 | + self.call_count = 0 |
206 | self.args = None |
207 | self.kwargs = None |
208 | + self.return_value = return_value |
209 | |
210 | def __call__(self, *args, **kwargs): |
211 | self.called = True |
212 | + self.call_count += 1 |
213 | self.args = args |
214 | self.kwargs = kwargs |
215 | + return self.return_value |
216 | + |
217 | + |
218 | +class MockProperty(MockMethod): |
219 | + |
220 | + @classmethod |
221 | + def bind(klass, test_instance, obj, method_name, return_value=None): |
222 | + original_method = getattr(obj, method_name) |
223 | + test_instance.addCleanup(setattr, obj, method_name, original_method) |
224 | + mock = klass(return_value) |
225 | + setattr(obj, method_name, property(mock.get_value, mock.set_value)) |
226 | + return mock |
227 | + |
228 | + def get_value(self, other): |
229 | + self.called = True |
230 | + return self.return_value |
231 | + |
232 | + def set_value(self, other, value): |
233 | + self.called = True |
234 | + self.return_value = value |
235 | |
236 | === added file 'tests/test_ui.py' |
237 | --- tests/test_ui.py 1970-01-01 00:00:00 +0000 |
238 | +++ tests/test_ui.py 2012-02-28 18:15:21 +0000 |
239 | @@ -0,0 +1,375 @@ |
240 | +# Copyright (C) 2012 Curtis Hovey <sinzui.is@verizon.net> |
241 | +# |
242 | +# This program is free software; you can redistribute it and/or modify |
243 | +# it under the terms of the GNU General Public License as published by |
244 | +# the Free Software Foundation; either version 2 of the License, or |
245 | +# (at your option) any later version. |
246 | +# |
247 | +# This program is distributed in the hope that it will be useful, |
248 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
249 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
250 | +# GNU General Public License for more details. |
251 | +# |
252 | +# You should have received a copy of the GNU General Public License |
253 | +# along with this program; if not, write to the Free Software |
254 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
255 | + |
256 | +"""Test the ui functionality.""" |
257 | + |
258 | +from gi.repository import Gtk |
259 | + |
260 | +from bzrlib import ( |
261 | + tests, |
262 | + ) |
263 | + |
264 | +from bzrlib.plugins.gtk import ui |
265 | +from bzrlib.plugins.gtk.tests import ( |
266 | + MockMethod, |
267 | + MockProperty, |
268 | + ) |
269 | +from bzrlib.progress import ProgressTask |
270 | + |
271 | + |
272 | +class MainIterationTestCase(tests.TestCase): |
273 | + |
274 | + def test_main_iteration(self): |
275 | + # The main_iteration decorator iterates over the pending Gtk events |
276 | + # after calling its function so that the UI is updated too. |
277 | + button = Gtk.ToggleButton(label='before') |
278 | + |
279 | + def event_listener(button): |
280 | + button.props.label = 'after' |
281 | + |
282 | + button.connect('clicked', event_listener) |
283 | + |
284 | + def test_func(self): |
285 | + button.emit('clicked') |
286 | + return True |
287 | + |
288 | + decorated_func = ui.main_iteration(test_func) |
289 | + result = decorated_func(object()) |
290 | + self.assertIs(True, result) |
291 | + self.assertIs(False, Gtk.events_pending()) |
292 | + self.assertEqual('after', button.props.label) |
293 | + |
294 | + |
295 | +class PromptDialogTestCase(tests.TestCase): |
296 | + |
297 | + def test_init(self): |
298 | + # The text and buttons are created. |
299 | + dialog = ui.PromptDialog('test 123') |
300 | + self.assertEqual('test 123', dialog.props.text) |
301 | + self.assertEqual(Gtk.MessageType.QUESTION, dialog.props.message_type) |
302 | + buttons = dialog.get_action_area().get_children() |
303 | + self.assertEqual('gtk-yes', buttons[0].props.label) |
304 | + self.assertEqual('gtk-no', buttons[1].props.label) |
305 | + |
306 | + |
307 | +class InfoDialogTestCase(tests.TestCase): |
308 | + |
309 | + def test_init(self): |
310 | + # The text and buttons are created. |
311 | + dialog = ui.InfoDialog('test 123') |
312 | + self.assertEqual('test 123', dialog.props.text) |
313 | + self.assertEqual(Gtk.MessageType.INFO, dialog.props.message_type) |
314 | + buttons = dialog.get_action_area().get_children() |
315 | + self.assertEqual('gtk-close', buttons[0].props.label) |
316 | + |
317 | + |
318 | +class WarningDialogTestCase(tests.TestCase): |
319 | + |
320 | + def test_init(self): |
321 | + # The text and buttons are created. |
322 | + dialog = ui.WarningDialog('test 123') |
323 | + self.assertEqual('test 123', dialog.props.text) |
324 | + self.assertEqual(Gtk.MessageType.WARNING, dialog.props.message_type) |
325 | + buttons = dialog.get_action_area().get_children() |
326 | + self.assertEqual('gtk-close', buttons[0].props.label) |
327 | + |
328 | + |
329 | +class ErrorDialogTestCase(tests.TestCase): |
330 | + |
331 | + def test_init(self): |
332 | + # The text and buttons are created, then shown. |
333 | + dialog = ui.ErrorDialog('test 123') |
334 | + self.assertEqual('test 123', dialog.props.text) |
335 | + self.assertEqual(Gtk.MessageType.ERROR, dialog.props.message_type) |
336 | + buttons = dialog.get_action_area().get_children() |
337 | + self.assertEqual('gtk-close', buttons[0].props.label) |
338 | + |
339 | + |
340 | +class PasswordDialogTestCase(tests.TestCase): |
341 | + |
342 | + def test_init(self): |
343 | + # The label, password entry, and buttons are created, then shown. |
344 | + MockMethod.bind(self, Gtk.Box, 'show_all') |
345 | + dialog = ui.PasswordDialog('test password') |
346 | + content_area = dialog.get_content_area() |
347 | + self.assertIs(True, dialog.get_content_area().show_all.called) |
348 | + widgets = content_area.get_children() |
349 | + self.assertEqual('test password', widgets[0].props.label) |
350 | + self.assertEqual(False, widgets[1].props.visibility) |
351 | + buttons = dialog.get_action_area().get_children() |
352 | + self.assertEqual('gtk-cancel', buttons[0].props.label) |
353 | + self.assertEqual( |
354 | + Gtk.ResponseType.CANCEL, |
355 | + dialog.get_response_for_widget(buttons[0])) |
356 | + self.assertEqual('gtk-ok', buttons[1].props.label) |
357 | + self.assertEqual( |
358 | + Gtk.ResponseType.OK, |
359 | + dialog.get_response_for_widget(buttons[1])) |
360 | + |
361 | + |
362 | +class GtkProgressBarTestCase(tests.TestCase): |
363 | + |
364 | + def test_init(self): |
365 | + progress_bar = ui.GtkProgressBar() |
366 | + self.assertEqual(0.0, progress_bar.props.fraction) |
367 | + self.assertIs(None, progress_bar.total) |
368 | + self.assertIs(None, progress_bar.current) |
369 | + |
370 | + def test_tick(self): |
371 | + # tick() shows the widget, does one pulse, then handles the pending |
372 | + # events in the main loop. |
373 | + MockMethod.bind(self, ui.GtkProgressBar, 'show') |
374 | + MockMethod.bind(self, ui.GtkProgressBar, 'pulse') |
375 | + progress_bar = ui.GtkProgressBar() |
376 | + progress_bar.tick() |
377 | + self.assertIs(True, progress_bar.show.called) |
378 | + self.assertEqual('with_main_iteration', progress_bar.tick.__name__) |
379 | + |
380 | + def test_update_with_data(self): |
381 | + # update() shows the widget, sets the fraction, then handles the |
382 | + # pending events in the main loop. |
383 | + MockMethod.bind(self, ui.GtkProgressBar, 'show') |
384 | + progress_bar = ui.GtkProgressBar() |
385 | + progress_bar.update(msg='test', current_cnt=5, total_cnt=10) |
386 | + self.assertIs(True, progress_bar.show.called) |
387 | + self.assertEqual(0.5, progress_bar.props.fraction) |
388 | + self.assertEqual(10, progress_bar.total) |
389 | + self.assertEqual(5, progress_bar.current) |
390 | + self.assertEqual('with_main_iteration', progress_bar.update.__name__) |
391 | + |
392 | + def test_update_without_data(self): |
393 | + progress_bar = ui.GtkProgressBar() |
394 | + progress_bar.update(current_cnt=5, total_cnt=None) |
395 | + self.assertEqual(0.0, progress_bar.props.fraction) |
396 | + self.assertIs(None, progress_bar.total) |
397 | + self.assertEqual(5, progress_bar.current) |
398 | + |
399 | + def test_update_with_insane_data(self): |
400 | + # The fraction must be between 0.0 and 1.0. |
401 | + progress_bar = ui.GtkProgressBar() |
402 | + self.assertRaises( |
403 | + ValueError, progress_bar.update, None, 20, 2) |
404 | + |
405 | + def test_finished(self): |
406 | + # finished() hides the widget, resets the state, then handles the |
407 | + # pending events in the main loop. |
408 | + MockMethod.bind(self, ui.GtkProgressBar, 'hide') |
409 | + progress_bar = ui.GtkProgressBar() |
410 | + progress_bar.finished() |
411 | + self.assertIs(True, progress_bar.hide.called) |
412 | + self.assertEqual(0.0, progress_bar.props.fraction) |
413 | + self.assertIs(None, progress_bar.total) |
414 | + self.assertIs(None, progress_bar.current) |
415 | + self.assertEqual('with_main_iteration', progress_bar.finished.__name__) |
416 | + |
417 | + def test_clear(self): |
418 | + # clear() is synonymous with finished. |
419 | + MockMethod.bind(self, ui.GtkProgressBar, 'finished') |
420 | + progress_bar = ui.GtkProgressBar() |
421 | + progress_bar.finished() |
422 | + self.assertIs(True, progress_bar.finished.called) |
423 | + |
424 | + |
425 | +class ProgressContainerMixin: |
426 | + |
427 | + def test_tick(self): |
428 | + progress_widget = self.progress_container() |
429 | + MockMethod.bind(self, progress_widget, 'show_all') |
430 | + MockMethod.bind(self, progress_widget.pb, 'tick') |
431 | + progress_widget.tick() |
432 | + self.assertIs(True, progress_widget.show_all.called) |
433 | + self.assertIs(True, progress_widget.pb.tick.called) |
434 | + |
435 | + def test_update(self): |
436 | + progress_widget = self.progress_container() |
437 | + MockMethod.bind(self, progress_widget, 'show_all') |
438 | + MockMethod.bind(self, progress_widget.pb, 'update') |
439 | + progress_widget.update('test', 5, 10) |
440 | + self.assertIs(True, progress_widget.show_all.called) |
441 | + self.assertIs(True, progress_widget.pb.update.called) |
442 | + self.assertEqual( |
443 | + ('test', 5, 10), progress_widget.pb.update.args) |
444 | + |
445 | + def test_finished(self): |
446 | + progress_widget = self.progress_container() |
447 | + MockMethod.bind(self, progress_widget, 'hide') |
448 | + MockMethod.bind(self, progress_widget.pb, 'finished') |
449 | + progress_widget.finished() |
450 | + self.assertIs(True, progress_widget.hide.called) |
451 | + self.assertIs(True, progress_widget.pb.finished.called) |
452 | + |
453 | + def test_clear(self): |
454 | + progress_widget = self.progress_container() |
455 | + MockMethod.bind(self, progress_widget, 'hide') |
456 | + MockMethod.bind(self, progress_widget.pb, 'clear') |
457 | + progress_widget.clear() |
458 | + self.assertIs(True, progress_widget.hide.called) |
459 | + self.assertIs(True, progress_widget.pb.clear.called) |
460 | + |
461 | + |
462 | +class ProgressBarWindowTestCase(ProgressContainerMixin, tests.TestCase): |
463 | + |
464 | + progress_container = ui.ProgressBarWindow |
465 | + |
466 | + def test_init(self): |
467 | + pb_window = ui.ProgressBarWindow() |
468 | + self.assertEqual('Progress', pb_window.props.title) |
469 | + self.assertEqual( |
470 | + Gtk.WindowPosition.CENTER_ALWAYS, pb_window.props.window_position) |
471 | + self.assertIsInstance(pb_window.pb, ui.GtkProgressBar) |
472 | + |
473 | + |
474 | +class ProgressPanelTestCase(ProgressContainerMixin, tests.TestCase): |
475 | + |
476 | + progress_container = ui.ProgressPanel |
477 | + |
478 | + def test_init(self): |
479 | + pb_window = ui.ProgressPanel() |
480 | + self.assertEqual( |
481 | + Gtk.Orientation.HORIZONTAL, pb_window.props.orientation) |
482 | + self.assertEqual(5, pb_window.props.spacing) |
483 | + self.assertIsInstance(pb_window.pb, ui.GtkProgressBar) |
484 | + widgets = pb_window.get_children() |
485 | + # The image's stock and icon_name properties are always None? |
486 | + self.assertIsInstance(widgets[0], Gtk.Image) |
487 | + |
488 | + |
489 | +class GtkUIFactoryTestCase(tests.TestCase): |
490 | + |
491 | + def test__init(self): |
492 | + ui_factory = ui.GtkUIFactory() |
493 | + self.assertIs(None, ui_factory._progress_bar_widget) |
494 | + |
495 | + def test_set_progress_bar_widget(self): |
496 | + ui_factory = ui.GtkUIFactory() |
497 | + progress_widget = ui.ProgressPanel() |
498 | + ui_factory.set_progress_bar_widget(progress_widget) |
499 | + self.assertIs(progress_widget, ui_factory._progress_bar_widget) |
500 | + |
501 | + def test_get_boolean_true(self): |
502 | + ui_factory = ui.GtkUIFactory() |
503 | + MockMethod.bind(self, ui.PromptDialog, 'run', Gtk.ResponseType.YES) |
504 | + boolean_value = ui_factory.get_boolean('test') |
505 | + self.assertIs(True, ui.PromptDialog.run.called) |
506 | + self.assertIs(True, boolean_value) |
507 | + |
508 | + def test_get_boolean_false(self): |
509 | + ui_factory = ui.GtkUIFactory() |
510 | + MockMethod.bind(self, ui.PromptDialog, 'run', Gtk.ResponseType.NO) |
511 | + boolean_value = ui_factory.get_boolean('test') |
512 | + self.assertIs(True, ui.PromptDialog.run.called) |
513 | + self.assertIs(False, boolean_value) |
514 | + |
515 | + def test_show_message(self): |
516 | + ui_factory = ui.GtkUIFactory() |
517 | + MockMethod.bind(self, ui.InfoDialog, 'run', Gtk.ResponseType.CLOSE) |
518 | + ui_factory.show_message('test') |
519 | + self.assertIs(True, ui.InfoDialog.run.called) |
520 | + |
521 | + def test_show_warning(self): |
522 | + ui_factory = ui.GtkUIFactory() |
523 | + MockMethod.bind(self, ui.WarningDialog, 'run', Gtk.ResponseType.CLOSE) |
524 | + ui_factory.show_warning('test') |
525 | + self.assertIs(True, ui.WarningDialog.run.called) |
526 | + |
527 | + def test_show_Error(self): |
528 | + ui_factory = ui.GtkUIFactory() |
529 | + MockMethod.bind(self, ui.ErrorDialog, 'run', Gtk.ResponseType.CLOSE) |
530 | + ui_factory.show_error('test') |
531 | + self.assertIs(True, ui.ErrorDialog.run.called) |
532 | + |
533 | + def test_show_user_warning(self): |
534 | + ui_factory = ui.GtkUIFactory() |
535 | + MockMethod.bind(self, ui.WarningDialog, 'run', Gtk.ResponseType.CLOSE) |
536 | + ui_factory.show_user_warning( |
537 | + 'recommend_upgrade', current_format_name='1.0', basedir='./test') |
538 | + self.assertIs(True, ui.WarningDialog.run.called) |
539 | + |
540 | + def test_show_user_warning_supressed(self): |
541 | + ui_factory = ui.GtkUIFactory() |
542 | + ui_factory.suppressed_warnings.add('recommend_upgrade') |
543 | + MockMethod.bind(self, ui.WarningDialog, 'run', Gtk.ResponseType.CLOSE) |
544 | + ui_factory.show_user_warning( |
545 | + 'recommend_upgrade', current_format_name='1.0', basedir='./test') |
546 | + self.assertIs(False, ui.WarningDialog.run.called) |
547 | + |
548 | + def test_get_password(self): |
549 | + ui_factory = ui.GtkUIFactory() |
550 | + MockMethod.bind(self, ui.PasswordDialog, 'run', Gtk.ResponseType.OK) |
551 | + mock_property = MockProperty.bind( |
552 | + self, ui.PasswordDialog, 'passwd', 'secret') |
553 | + password = ui_factory.get_password('test') |
554 | + self.assertIs(True, ui.PasswordDialog.run.called) |
555 | + self.assertIs(True, mock_property.called) |
556 | + self.assertEqual('secret', password) |
557 | + |
558 | + def test_progress_all_finished_with_widget(self): |
559 | + ui_factory = ui.GtkUIFactory() |
560 | + progress_widget = ui.ProgressPanel() |
561 | + MockMethod.bind(self, progress_widget, 'finished') |
562 | + ui_factory.set_progress_bar_widget(progress_widget) |
563 | + self.assertIs(None, ui_factory._progress_all_finished()) |
564 | + self.assertIs(True, progress_widget.finished.called) |
565 | + |
566 | + def test_progress_all_finished_without_widget(self): |
567 | + ui_factory = ui.GtkUIFactory() |
568 | + self.assertIs(None, ui_factory._progress_all_finished()) |
569 | + |
570 | + def test_progress_updated_with_widget(self): |
571 | + ui_factory = ui.GtkUIFactory() |
572 | + progress_widget = ui.ProgressPanel() |
573 | + MockMethod.bind(self, progress_widget, 'update') |
574 | + ui_factory.set_progress_bar_widget(progress_widget) |
575 | + task = ProgressTask() |
576 | + task.msg = 'test' |
577 | + task.current_cnt = 1 |
578 | + task.total_cnt = 2 |
579 | + self.assertIs(None, ui_factory._progress_updated(task)) |
580 | + self.assertIs(True, progress_widget.update.called) |
581 | + self.assertEqual( |
582 | + ('test', 1, 2), progress_widget.update.args) |
583 | + |
584 | + def test_progress_updated_without_widget(self): |
585 | + ui_factory = ui.GtkUIFactory() |
586 | + MockMethod.bind(self, ui.ProgressBarWindow, 'update') |
587 | + task = ProgressTask() |
588 | + task.msg = 'test' |
589 | + task.current_cnt = 1 |
590 | + task.total_cnt = 2 |
591 | + self.assertIs(None, ui_factory._progress_updated(task)) |
592 | + self.assertIsInstance( |
593 | + ui_factory._progress_bar_widget, ui.ProgressBarWindow) |
594 | + self.assertIs(True, ui_factory._progress_bar_widget.update.called) |
595 | + self.assertEqual( |
596 | + ('test', 1, 2), ui_factory._progress_bar_widget.update.args) |
597 | + |
598 | + def test_report_transport_activity_with_widget(self): |
599 | + ui_factory = ui.GtkUIFactory() |
600 | + progress_widget = ui.ProgressPanel() |
601 | + MockMethod.bind(self, progress_widget, 'tick') |
602 | + ui_factory.set_progress_bar_widget(progress_widget) |
603 | + self.assertIs( |
604 | + None, ui_factory.report_transport_activity(None, None, None)) |
605 | + self.assertIs(True, progress_widget.tick.called) |
606 | + |
607 | + def test_report_transport_activity_without_widget(self): |
608 | + ui_factory = ui.GtkUIFactory() |
609 | + MockMethod.bind(self, ui.ProgressBarWindow, 'tick') |
610 | + self.assertIs( |
611 | + None, ui_factory.report_transport_activity(None, None, None)) |
612 | + self.assertIsInstance( |
613 | + ui_factory._progress_bar_widget, ui.ProgressBarWindow) |
614 | + self.assertIs(True, ui.ProgressBarWindow.tick.called) |
615 | |
616 | === modified file 'ui.py' |
617 | --- ui.py 2011-09-08 03:11:06 +0000 |
618 | +++ ui.py 2012-02-28 18:15:21 +0000 |
619 | @@ -24,18 +24,45 @@ |
620 | from bzrlib.ui import UIFactory |
621 | |
622 | |
623 | -class PromptDialog(Gtk.Dialog): |
624 | +def main_iteration(function): |
625 | + def with_main_iteration(self, *args, **kwargs): |
626 | + result = function(self, *args, **kwargs) |
627 | + while Gtk.events_pending(): |
628 | + Gtk.main_iteration_do(False) |
629 | + return result |
630 | + return with_main_iteration |
631 | + |
632 | + |
633 | +class PromptDialog(Gtk.MessageDialog): |
634 | """Prompt the user for a yes/no answer.""" |
635 | |
636 | - def __init__(self, prompt): |
637 | - super(PromptDialog, self).__init__() |
638 | - |
639 | - label = Gtk.Label(label=prompt) |
640 | - self.get_content_area().pack_start(label, True, True, 10) |
641 | - self.get_content_area().show_all() |
642 | - |
643 | - self.add_buttons(Gtk.STOCK_YES, Gtk.ResponseType.YES, Gtk.STOCK_NO, |
644 | - Gtk.ResponseType.NO) |
645 | + def __init__(self, prompt, parent=None): |
646 | + super(PromptDialog, self).__init__( |
647 | + parent, Gtk.DialogFlags.DESTROY_WITH_PARENT, |
648 | + Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, prompt) |
649 | + |
650 | + |
651 | +class InfoDialog(Gtk.MessageDialog): |
652 | + """Show the user an informational message.""" |
653 | + |
654 | + MESSAGE_TYPE = Gtk.MessageType.INFO |
655 | + |
656 | + def __init__(self, prompt, parent=None): |
657 | + super(InfoDialog, self).__init__( |
658 | + parent, Gtk.DialogFlags.DESTROY_WITH_PARENT, |
659 | + self.MESSAGE_TYPE, Gtk.ButtonsType.CLOSE, prompt) |
660 | + |
661 | + |
662 | +class WarningDialog(InfoDialog): |
663 | + """Show the user a warning message.""" |
664 | + |
665 | + MESSAGE_TYPE = Gtk.MessageType.WARNING |
666 | + |
667 | + |
668 | +class ErrorDialog(InfoDialog): |
669 | + """Show the user a warning message.""" |
670 | + |
671 | + MESSAGE_TYPE = Gtk.MessageType.ERROR |
672 | |
673 | |
674 | class GtkProgressBar(Gtk.ProgressBar): |
675 | @@ -46,10 +73,12 @@ |
676 | self.current = None |
677 | self.total = None |
678 | |
679 | + @main_iteration |
680 | def tick(self): |
681 | self.show() |
682 | self.pulse() |
683 | |
684 | + @main_iteration |
685 | def update(self, msg=None, current_cnt=None, total_cnt=None): |
686 | self.show() |
687 | if current_cnt is not None: |
688 | @@ -59,21 +88,43 @@ |
689 | if msg is not None: |
690 | self.set_text(msg) |
691 | if None not in (self.current, self.total): |
692 | - self.fraction = float(self.current) / self.total |
693 | - if self.fraction < 0.0 or self.fraction > 1.0: |
694 | - raise AssertionError |
695 | - self.set_fraction(self.fraction) |
696 | - while Gtk.events_pending(): |
697 | - Gtk.main_iteration() |
698 | - |
699 | - def finished(self): |
700 | - self.hide() |
701 | - |
702 | - def clear(self): |
703 | - self.hide() |
704 | - |
705 | - |
706 | -class ProgressBarWindow(Gtk.Window): |
707 | + fraction = float(self.current) / self.total |
708 | + if fraction < 0.0 or fraction > 1.0: |
709 | + raise ValueError |
710 | + self.set_fraction(fraction) |
711 | + |
712 | + @main_iteration |
713 | + def finished(self): |
714 | + self.set_fraction(0.0) |
715 | + self.current = None |
716 | + self.total = None |
717 | + self.hide() |
718 | + |
719 | + def clear(self): |
720 | + self.finished() |
721 | + |
722 | + |
723 | +class ProgressContainerMixin: |
724 | + """Expose GtkProgressBar methods to a container class.""" |
725 | + |
726 | + def tick(self, *args, **kwargs): |
727 | + self.show_all() |
728 | + self.pb.tick(*args, **kwargs) |
729 | + |
730 | + def update(self, *args, **kwargs): |
731 | + self.show_all() |
732 | + self.pb.update(*args, **kwargs) |
733 | + |
734 | + def finished(self): |
735 | + self.hide() |
736 | + self.pb.finished() |
737 | + |
738 | + def clear(self): |
739 | + self.hide() |
740 | + self.pb.clear() |
741 | + |
742 | + |
743 | +class ProgressBarWindow(ProgressContainerMixin, Gtk.Window): |
744 | |
745 | def __init__(self): |
746 | super(ProgressBarWindow, self).__init__(type=Gtk.WindowType.TOPLEVEL) |
747 | @@ -85,54 +136,20 @@ |
748 | self.resize(250, 15) |
749 | self.set_resizable(False) |
750 | |
751 | - def tick(self, *args, **kwargs): |
752 | - self.show_all() |
753 | - self.pb.tick(*args, **kwargs) |
754 | - |
755 | - def update(self, *args, **kwargs): |
756 | - self.show_all() |
757 | - self.pb.update(*args, **kwargs) |
758 | - |
759 | - def finished(self): |
760 | - self.pb.finished() |
761 | - self.hide() |
762 | - self.destroy() |
763 | - |
764 | - def clear(self): |
765 | - self.pb.clear() |
766 | - self.hide() |
767 | - |
768 | - |
769 | -class ProgressPanel(Gtk.HBox): |
770 | + |
771 | +class ProgressPanel(ProgressContainerMixin, Gtk.Box): |
772 | |
773 | def __init__(self): |
774 | - super(ProgressPanel, self).__init__() |
775 | + super(ProgressPanel, self).__init__(Gtk.Orientation.HORIZONTAL, 5) |
776 | image_loading = Gtk.Image.new_from_stock(Gtk.STOCK_REFRESH, |
777 | Gtk.IconSize.BUTTON) |
778 | image_loading.show() |
779 | |
780 | self.pb = GtkProgressBar() |
781 | - self.set_spacing(5) |
782 | self.set_border_width(5) |
783 | self.pack_start(image_loading, False, False, 0) |
784 | self.pack_start(self.pb, True, True, 0) |
785 | |
786 | - def tick(self, *args, **kwargs): |
787 | - self.show_all() |
788 | - self.pb.tick(*args, **kwargs) |
789 | - |
790 | - def update(self, *args, **kwargs): |
791 | - self.show_all() |
792 | - self.pb.update(*args, **kwargs) |
793 | - |
794 | - def finished(self): |
795 | - self.pb.finished() |
796 | - self.hide() |
797 | - |
798 | - def clear(self): |
799 | - self.pb.clear() |
800 | - self.hide() |
801 | - |
802 | |
803 | class PasswordDialog(Gtk.Dialog): |
804 | """ Prompt the user for a password. """ |
805 | @@ -176,6 +193,30 @@ |
806 | dialog.destroy() |
807 | return (response == Gtk.ResponseType.YES) |
808 | |
809 | + def show_message(self, msg): |
810 | + """See UIFactory.show_message.""" |
811 | + dialog = InfoDialog(msg) |
812 | + dialog.run() |
813 | + dialog.destroy() |
814 | + |
815 | + def show_warning(self, msg): |
816 | + """See UIFactory.show_warning.""" |
817 | + dialog = WarningDialog(msg) |
818 | + dialog.run() |
819 | + dialog.destroy() |
820 | + |
821 | + def show_error(self, msg): |
822 | + """See UIFactory.show_error.""" |
823 | + dialog = ErrorDialog(msg) |
824 | + dialog.run() |
825 | + dialog.destroy() |
826 | + |
827 | + def show_user_warning(self, warning_id, **message_args): |
828 | + """See UIFactory.show_user_warning.""" |
829 | + if warning_id not in self.suppressed_warnings: |
830 | + message = self.format_user_warning(warning_id, message_args) |
831 | + self.show_warning(message) |
832 | + |
833 | def get_password(self, prompt='', **kwargs): |
834 | """Prompt the user for a password. |
835 | |
836 | @@ -183,7 +224,7 @@ |
837 | :param kwargs: Arguments which will be expanded into the prompt. |
838 | This lets front ends display different things if |
839 | they so choose. |
840 | - :return: The password string, return None if the user |
841 | + :return: The password string, return None if the user |
842 | canceled the request. |
843 | """ |
844 | dialog = PasswordDialog(prompt % kwargs) |
845 | @@ -196,17 +237,24 @@ |
846 | return None |
847 | |
848 | def _progress_all_finished(self): |
849 | - """See UIFactory._progress_all_finished""" |
850 | + """See UIFactory._progress_all_finished.""" |
851 | pbw = self._progress_bar_widget |
852 | if pbw: |
853 | pbw.finished() |
854 | |
855 | - def _progress_updated(self, task): |
856 | - """See UIFactory._progress_updated""" |
857 | + def _ensure_progress_widget(self): |
858 | if self._progress_bar_widget is None: |
859 | - # Default to a window since nobody gave us a better mean to report |
860 | + # Default to a window since nobody gave us a better means to report |
861 | # progress. |
862 | self.set_progress_bar_widget(ProgressBarWindow()) |
863 | + |
864 | + def _progress_updated(self, task): |
865 | + """See UIFactory._progress_updated.""" |
866 | + self._ensure_progress_widget() |
867 | self._progress_bar_widget.update(task.msg, |
868 | task.current_cnt, task.total_cnt) |
869 | |
870 | + def report_transport_activity(self, transport, byte_count, direction): |
871 | + """See UIFactory.report_transport_activity.""" |
872 | + self._ensure_progress_widget() |
873 | + self._progress_bar_widget.tick() |
This is *really* nice. Thanks, Curtis!