1#! /usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4# demo.py --- Demonstration program and cheap test suite for pythondialog 5# 6# Copyright (C) 2000 Robb Shecter, Sultanbek Tezadov 7# Copyright (C) 2002-2019 Florent Rougon 8# 9# This program is in the public domain. 10 11"""Demonstration program for pythondialog. 12 13This is a program demonstrating most of the possibilities offered by 14the pythondialog module (which is itself a Python interface to the 15well-known dialog utility, or any other program compatible with 16dialog). 17 18Executive summary 19----------------- 20 21If you are looking for a very simple example of pythondialog usage, 22short and straightforward, please refer to simple_example.py. The 23file you are now reading serves more as a demonstration of what can 24be done with pythondialog and as a cheap test suite than as a first 25time tutorial. However, it can also be used to learn how to invoke 26the various widgets. The following paragraphs explain what you should 27keep in mind if you read it for this purpose. 28 29 30Most of the code in the MyApp class (which defines the actual 31contents of the demo) relies on a class called MyDialog implemented 32here that: 33 34 1. wraps all widget-producing calls in a way that automatically 35 spawns a "confirm quit" dialog box if the user presses the 36 Escape key or chooses the Cancel button, and then redisplays the 37 original widget if the user doesn't actually want to quit; 38 39 2. provides a few additional dialog-related methods and convenience 40 wrappers. 41 42The handling in (1) is completely automatic, implemented with 43MyDialog.__getattr__() returning decorated versions of the 44widget-producing methods of dialog.Dialog. Therefore, most of the 45demo can be read as if the module-level 'd' attribute were a 46dialog.Dialog instance whereas it is actually a MyDialog instance. 47The only meaningful difference is that MyDialog.<widget>() will never 48return a CANCEL or ESC code (attributes of 'd', or more generally of 49dialog.Dialog). The reason is that these return codes are 50automatically handled by the MyDialog.__getattr__() machinery to 51display the "confirm quit" dialog box. 52 53In some cases (e.g., fselect_demo()), I wanted the "Cancel" button to 54perform a specific action instead of spawning the "confirm quit" 55dialog box. To achieve this, the widget is invoked using 56dialog.Dialog.<widget> instead of MyDialog.<widget>, and the return 57code is handled in a semi-manual way. A prominent feature that needs 58such special-casing is the yesno widget, because the "No" button 59corresponds to the CANCEL exit code, which in general must not be 60interpreted as an attempt to quit the program! 61 62To sum it up, you can read most of the code in the MyApp class (which 63defines the actual contents of the demo) as if 'd' were a 64dialog.Dialog instance. Just keep in mind that there is a little 65magic behind the scenes that automatically handles the CANCEL and ESC 66Dialog exit codes, which wouldn't be the case if 'd' were a 67dialog.Dialog instance. For a first introduction to pythondialog with 68simple stuff and absolutely no magic, please have a look at 69simple_example.py. 70 71""" 72 73 74import sys, os, locale, stat, time, getopt, subprocess, traceback, textwrap 75import pprint 76import dialog 77from dialog import DialogBackendVersion 78 79progname = os.path.basename(sys.argv[0]) 80progversion = "0.12-autowidgetsize" 81version_blurb = """Demonstration program and cheap test suite for pythondialog. 82 83Copyright (C) 2002-2010, 2013-2016 Florent Rougon 84Copyright (C) 2000 Robb Shecter, Sultanbek Tezadov 85 86This is free software; see the source for copying conditions. There is NO 87warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.""" 88 89default_debug_filename = "pythondialog.debug" 90 91usage = """Usage: {progname} [option ...] 92Demonstration program and cheap test suite for pythondialog. 93 94Options: 95 -t, --test-suite test all widgets; implies '--fast' 96 -f, --fast fast mode (e.g., makes the gauge demo run faster) 97 --debug enable logging of all dialog command lines 98 --debug-file=FILE where to write debug information (default: 99 {debug_file} in the current directory) 100 -E, --debug-expand-file-opt expand the '--file' options in the debug file 101 generated by '--debug' 102 --help display this message and exit 103 --version output version information and exit""".format( 104 progname=progname, debug_file=default_debug_filename) 105 106# Global parameters 107params = {} 108 109# We'll use a module-level attribute 'd' ("global") to store the MyDialog 110# instance that is used throughout the demo. This object could alternatively be 111# passed to the MyApp constructor and stored there as a class or instance 112# attribute. However, for the sake of readability, we'll simply use a global 113# (d.msgbox(...) versus self.d.msgbox(...), etc.). 114d = None 115 116tw = textwrap.TextWrapper(width=78, break_long_words=False, 117 break_on_hyphens=True) 118from textwrap import dedent 119 120try: 121 from textwrap import indent 122except ImportError: 123 try: 124 callable # Normally, should be __builtins__.callable 125 except NameError: 126 # Python 3.1 doesn't have the 'callable' builtin function. Let's 127 # provide ours. 128 def callable(f): 129 return hasattr(f, '__call__') 130 131 def indent(text, prefix, predicate=None): 132 l = [] 133 134 for line in text.splitlines(True): 135 if (callable(predicate) and predicate(line)) \ 136 or (not callable(predicate) and predicate) \ 137 or (predicate is None and line.strip()): 138 line = prefix + line 139 l.append(line) 140 141 return ''.join(l) 142 143 144class MyDialog: 145 """Wrapper class for dialog.Dialog. 146 147 This class behaves similarly to dialog.Dialog. The differences 148 are that: 149 150 1. MyDialog wraps all widget-producing methods in a way that 151 automatically spawns a "confirm quit" dialog box if the user 152 presses the Escape key or chooses the Cancel button, and 153 then redisplays the original widget if the user doesn't 154 actually want to quit. 155 156 2. MyDialog provides a few additional dialog-related methods 157 and convenience wrappers. 158 159 Please refer to the module docstring and to the particular 160 methods for more details. 161 162 """ 163 def __init__(self, Dialog_instance): 164 self.dlg = Dialog_instance 165 166 def check_exit_request(self, code, ignore_Cancel=False): 167 if code == self.CANCEL and ignore_Cancel: 168 # Ignore the Cancel button, i.e., don't interpret it as an exit 169 # request; instead, let the caller handle CANCEL himself. 170 return True 171 172 if code in (self.CANCEL, self.ESC): 173 button_name = { self.CANCEL: "Cancel", 174 self.ESC: "Escape" } 175 msg = "You pressed {0} in the last dialog box. Do you want " \ 176 "to exit this demo?".format(button_name[code]) 177 # 'self.dlg' instead of 'self' here, because we want to use the 178 # original yesno() method from the Dialog class instead of the 179 # decorated method returned by self.__getattr__(). 180 if self.dlg.yesno(msg) == self.OK: 181 sys.exit(0) 182 else: # "No" button chosen, or ESC pressed 183 return False # in the "confirm quit" dialog 184 else: 185 return True 186 187 def widget_loop(self, method): 188 """Decorator to handle eventual exit requests from a Dialog widget. 189 190 method -- a dialog.Dialog method that returns either a Dialog 191 exit code, or a sequence whose first element is a 192 Dialog exit code (cf. the docstring of the Dialog 193 class in dialog.py) 194 195 Return a wrapper function that behaves exactly like 'method', 196 except for the following point: 197 198 If the Dialog exit code obtained from 'method' is CANCEL or 199 ESC (attributes of dialog.Dialog), a "confirm quit" dialog 200 is spawned; depending on the user choice, either the 201 program exits or 'method' is called again, with the same 202 arguments and same handling of the exit status. In other 203 words, the wrapper function builds a loop around 'method'. 204 205 The above condition on 'method' is satisfied for all 206 dialog.Dialog widget-producing methods. More formally, these 207 are the methods defined with the @widget decorator in 208 dialog.py, i.e., that have an "is_widget" attribute set to 209 True. 210 211 """ 212 # One might want to use @functools.wraps here, but since the wrapper 213 # function is very likely to be used only once and then 214 # garbage-collected, this would uselessly add a little overhead inside 215 # __getattr__(), where widget_loop() is called. 216 def wrapper(*args, **kwargs): 217 while True: 218 res = method(*args, **kwargs) 219 220 if hasattr(method, "retval_is_code") \ 221 and getattr(method, "retval_is_code"): 222 code = res 223 else: 224 code = res[0] 225 226 if self.check_exit_request(code): 227 break 228 return res 229 230 return wrapper 231 232 def __getattr__(self, name): 233 # This is where the "magic" of this class originates from. 234 # Please refer to the module and self.widget_loop() 235 # docstrings if you want to understand the why and the how. 236 obj = getattr(self.dlg, name) 237 if hasattr(obj, "is_widget") and getattr(obj, "is_widget"): 238 return self.widget_loop(obj) 239 else: 240 return obj 241 242 def clear_screen(self): 243 # This program comes with ncurses 244 program = "clear" 245 246 try: 247 p = subprocess.Popen([program], shell=False, stdout=None, 248 stderr=None, close_fds=True) 249 retcode = p.wait() 250 except os.error as e: 251 self.msgbox("Unable to execute program '%s': %s." % (program, 252 e.strerror), 253 title="Error") 254 return False 255 256 if retcode > 0: 257 msg = "Program %s returned exit status %d." % (program, retcode) 258 elif retcode < 0: 259 msg = "Program %s was terminated by signal %d." % (program, -retcode) 260 else: 261 return True 262 263 self.msgbox(msg) 264 return False 265 266 def _Yesno(self, *args, **kwargs): 267 """Convenience wrapper around dialog.Dialog.yesno(). 268 269 Return the same exit code as would return 270 dialog.Dialog.yesno(), except for ESC which is handled as in 271 the rest of the demo, i.e. make it spawn the "confirm quit" 272 dialog. 273 274 """ 275 # self.yesno() automatically spawns the "confirm quit" dialog if ESC or 276 # the "No" button is pressed, because of self.__getattr__(). Therefore, 277 # we have to use self.dlg.yesno() here and call 278 # self.check_exit_request() manually. 279 while True: 280 code = self.dlg.yesno(*args, **kwargs) 281 # If code == self.CANCEL, it means the "No" button was chosen; 282 # don't interpret this as a wish to quit the program! 283 if self.check_exit_request(code, ignore_Cancel=True): 284 break 285 286 return code 287 288 def Yesno(self, *args, **kwargs): 289 """Convenience wrapper around dialog.Dialog.yesno(). 290 291 Return True if "Yes" was chosen, False if "No" was chosen, 292 and handle ESC as in the rest of the demo, i.e. make it spawn 293 the "confirm quit" dialog. 294 295 """ 296 return self._Yesno(*args, **kwargs) == self.dlg.OK 297 298 def Yesnohelp(self, *args, **kwargs): 299 """Convenience wrapper around dialog.Dialog.yesno(). 300 301 Return "yes", "no", "extra" or "help" depending on the button 302 that was pressed to close the dialog. ESC is handled as in 303 the rest of the demo, i.e. it spawns the "confirm quit" 304 dialog. 305 306 """ 307 kwargs["help_button"] = True 308 code = self._Yesno(*args, **kwargs) 309 d = { self.dlg.OK: "yes", 310 self.dlg.CANCEL: "no", 311 self.dlg.EXTRA: "extra", 312 self.dlg.HELP: "help" } 313 314 return d[code] 315 316 317# Dummy context manager to make sure the debug file is closed on exit, be it 318# normal or abnormal, and to avoid having two code paths, one for normal mode 319# and one for debug mode. 320class DummyContextManager: 321 def __enter__(self): 322 return self 323 324 def __exit__(self, *exc): 325 return False 326 327 328class MyApp: 329 def __init__(self): 330 # The MyDialog instance 'd' could be passed via the constructor and 331 # stored here as a class or instance attribute. However, for the sake 332 # of readability, we'll simply use a module-level attribute ("global") 333 # (d.msgbox(...) versus self.d.msgbox(...), etc.). 334 global d 335 # If you want to use Xdialog (pathnames are also OK for the 'dialog' 336 # argument), you can use: 337 # dialog.Dialog(dialog="Xdialog", compat="Xdialog", ...) 338 # 339 # With the 'autowidgetsize' option enabled, pythondialog's 340 # widget-producing methods behave as if width=0, height=0, etc. had 341 # been passed, except where these parameters are explicitely specified 342 # with different values. 343 self.Dialog_instance = dialog.Dialog(dialog="cdialog", 344 autowidgetsize=True) 345 # See the module docstring at the top of the file to understand the 346 # purpose of MyDialog. 347 d = MyDialog(self.Dialog_instance) 348 backtitle = "pythondialog demo" 349 d.set_background_title(backtitle) 350 # These variables take the background title into account 351 self.max_lines, self.max_cols = d.maxsize(backtitle=backtitle) 352 self.demo_context = self.setup_debug() 353 # Warn if the terminal is smaller than this size 354 self.min_rows, self.min_cols = 24, 80 355 self.term_rows, self.term_cols, self.backend_version = \ 356 self.get_term_size_and_backend_version() 357 358 # With Python strings, we don't need dialog to change '\n' to a newline 359 # char: tell the dialog program to use the strings as we prepared them. 360 # The same technique can be used to pass other options to dialog(1). 361 d.add_persistent_args(["--no-nl-expand"]) 362 363 def setup_debug(self): 364 if params["debug"]: 365 debug_file = open(params["debug_filename"], "w") 366 d.setup_debug(True, file=debug_file, 367 expand_file_opt=params["debug_expand_file_opt"]) 368 return debug_file 369 else: 370 return DummyContextManager() 371 372 def get_term_size_and_backend_version(self): 373 # Avoid running '<backend> --print-version' every time we need the 374 # version 375 backend_version = d.cached_backend_version 376 if not backend_version: 377 print(tw.fill( 378 "Unable to retrieve the version of the dialog-like backend. " 379 "Not running cdialog?") + "\nPress Enter to continue.", 380 file=sys.stderr) 381 input() 382 383 term_rows, term_cols = d.maxsize(use_persistent_args=False) 384 if term_rows < self.min_rows or term_cols < self.min_cols: 385 print(tw.fill(dedent("""\ 386 Your terminal has less than {0} rows or less than {1} columns; 387 you may experience problems with the demo. You have been warned.""" 388 .format(self.min_rows, self.min_cols))) 389 + "\nPress Enter to continue.") 390 input() 391 392 return (term_rows, term_cols, backend_version) 393 394 def run(self): 395 with self.demo_context: 396 if params["testsuite_mode"]: 397 # Show the additional widgets before the "normal demo", so that 398 # I can test new widgets quickly and simply hit Ctrl-C once 399 # they've been shown. 400 self.additional_widgets() 401 402 # "Normal" demo 403 self.demo() 404 405 def demo(self): 406 d.msgbox("""\ 407Hello, and welcome to the pythondialog {pydlg_version} demonstration program. 408 409You can scroll through this dialog box with the Page Up and Page Down keys. \ 410Please note that some of the dialogs will not work, and cause the demo to \ 411stop, if your terminal is too small. The recommended size is (at least) \ 412{min_rows} rows by {min_cols} columns. 413 414This script is being run by a Python interpreter identified as follows: 415 416{py_version} 417 418The dialog-like program displaying this message box reports version \ 419{backend_version} and a terminal size of {rows} rows by {cols} columns.""" 420 .format( 421 pydlg_version=dialog.__version__, 422 backend_version=self.backend_version, 423 py_version=indent(sys.version, " "), 424 rows=self.term_rows, cols=self.term_cols, 425 min_rows=self.min_rows, min_cols=self.min_cols), 426 width=60, height=17) 427 428 self.progressbox_demo_with_file_descriptor() 429 # First dialog version where the programbox widget works fine 430 if self.dialog_version_check("1.2-20140112"): 431 self.programbox_demo() 432 self.infobox_demo() 433 self.gauge_demo() 434 answer = self.yesno_demo(with_help=True) 435 self.msgbox_demo(answer) 436 self.textbox_demo() 437 self.timeout_demo() 438 name = self.inputbox_demo_with_help() 439 size, weight, city, state, country, last_will1, last_will2, \ 440 last_will3, last_will4, secret_code = self.mixedform_demo() 441 self.form_demo_with_help() 442 favorite_day = self.menu_demo(name, city, state, country, size, weight, 443 secret_code, last_will1, last_will2, 444 last_will3, last_will4) 445 446 if self.dialog_version_check("1.2-20130902", 447 "the menu demo with help facilities", 448 explain=True): 449 self.menu_demo_with_help() 450 451 toppings = self.checklist_demo() 452 if self.dialog_version_check("1.2-20130902", 453 "the checklist demo with help facilities", 454 explain=True): 455 self.checklist_demo_with_help() 456 457 sandwich = self.radiolist_demo() 458 459 if self.dialog_version_check("1.2-20121230", "the rangebox demo", explain=True): 460 nb_engineers = self.rangebox_demo() 461 else: 462 nb_engineers = None 463 464 if self.dialog_version_check("1.2-20121230", "the buildlist demo", explain=True): 465 desert_island_stuff = self.buildlist_demo() 466 else: 467 desert_island_stuff = None 468 469 if self.dialog_version_check("1.2-20130902", 470 "the buildlist demo with help facilities", 471 explain=True): 472 self.buildlist_demo_with_help() 473 474 date = self.calendar_demo_with_help() 475 time_ = self.timebox_demo() 476 477 password = self.passwordbox_demo() 478 self.scrollbox_demo(name, favorite_day, toppings, sandwich, 479 nb_engineers, desert_island_stuff, date, time_, 480 password) 481 482 if self.dialog_version_check("1.2-20121230", "the treeview demo", 483 explain=True): 484 if self.dialog_version_check("1.2-20130902"): 485 self.treeview_demo_with_help() 486 else: 487 self.treeview_demo() 488 489 self.mixedgauge_demo() 490 self.editbox_demo("/etc/passwd") 491 self.inputmenu_demo() 492 d.msgbox("""\ 493Haha. You thought it was over. Wrong. Even more fun is to come! 494 495Now, please select a file you would like to see growing (or not...).""", 496 width=75, height=8) 497 498 # Looks nicer if the screen is not completely filled by the widget, 499 # hence the -1. 500 self.tailbox_demo(height=self.max_lines-1, 501 width=self.max_cols) 502 503 directory = self.dselect_demo() 504 505 timeout = 2 if params["fast_mode"] else 20 506 self.pause_demo(timeout) 507 508 d.clear_screen() 509 if not params["fast_mode"]: 510 # Rest assured, this is not necessary in any way: it is only a 511 # psychological trick to try to give the impression of a reboot 512 # (cf. pause_demo(); would be even nicer with a "visual bell")... 513 time.sleep(1) 514 515 def additional_widgets(self): 516 # Requires a careful choice of the file to be of any interest 517 self.progressbox_demo_with_filepath() 518 # This can be confusing without any pause if the user specified a 519 # regular file. 520 time.sleep(1 if params["fast_mode"] else 2) 521 522 # programbox_demo is fine right after 523 # progressbox_demo_with_file_descriptor in demo(), but there was a 524 # little bug in dialog that made the first two lines disappear too 525 # early. This bug has been fixed in version 1.2-20140112, therefore 526 # we'll run the programbox_demo as part of the main demo if the dialog 527 # version is >= than this one, otherwise we'll keep it here. 528 if self.dialog_version_check("1.1-20110302", "the programbox demo", 529 explain=True): 530 # First dialog version where the programbox widget works fine 531 if not self.dialog_version_check("1.2-20140112"): 532 self.programbox_demo() 533 # Almost identical to mixedform (mixedform being more powerful). Also, 534 # there is now form_demo_with_help() which uses the form widget. 535 self.form_demo() 536 # Almost identical to passwordbox 537 self.passwordform_demo() 538 539 def dialog_version_check(self, version_string, feature="", *, start="", 540 explain=False): 541 if d.compat != "dialog": 542 # non-dialog implementations are not affected by 543 # 'dialog_version_check'. 544 return True 545 546 minimum_version = DialogBackendVersion.fromstring(version_string) 547 res = (d.cached_backend_version >= minimum_version) 548 549 if explain and not res: 550 self.too_old_dialog_version(feature=feature, start=start, 551 min=version_string) 552 553 return res 554 555 def too_old_dialog_version(self, feature="", *, start="", min=None): 556 assert (feature and not start) or (not feature and start), \ 557 (feature, start) 558 if not start: 559 start = "Skipping {0},".format(feature) 560 561 d.msgbox( 562 "{start} because it requires dialog {min} or later; " 563 "however, it appears that you are using version {used}.".format( 564 start=start, min=min, used=d.cached_backend_version), 565 width=60, height=9, title="Demo skipped") 566 567 def progressbox_demo_with_filepath(self): 568 widget = "progressbox" 569 570 # First, ask the user for a file (possibly FIFO) 571 d.msgbox(self.FIFO_HELP(widget), width=72, height=20) 572 path = self.fselect_demo(widget, allow_FIFOs=True, 573 title="Please choose a file to be shown as " 574 "with 'tail -f'") 575 if path is None: 576 # User chose to abort 577 return 578 else: 579 d.progressbox(file_path=path, width=78, height=20, 580 text="You can put some header text here", 581 title="Progressbox example with a file path") 582 583 def progressboxoid(self, widget, func_name, text, **kwargs): 584 # Since this is just a demo, I will not try to catch os.error exceptions 585 # in this function, for the sake of readability. 586 read_fd, write_fd = os.pipe() 587 588 child_pid = os.fork() 589 if child_pid == 0: 590 try: 591 # We are in the child process. We MUST NOT raise any exception. 592 # No need for this one in the child process 593 os.close(read_fd) 594 595 # Python file objects are easier to use than file descriptors. 596 # For a start, you don't have to check the number of bytes 597 # actually written every time... 598 # "buffering = 1" means wfile is going to be line-buffered 599 with os.fdopen(write_fd, mode="w", buffering=1) as wfile: 600 for line in text.split('\n'): 601 wfile.write(line + '\n') 602 time.sleep(0.02 if params["fast_mode"] else 1.2) 603 604 os._exit(0) 605 except: 606 os._exit(127) 607 608 # We are in the father process. No need for write_fd anymore. 609 os.close(write_fd) 610 # Call d.progressbox() if widget == "progressbox" 611 # d.programbox() if widget == "programbox" 612 # etc. 613 getattr(d, widget)( 614 fd=read_fd, 615 title="{0} example with a file descriptor".format(widget), 616 **kwargs) 617 618 # Now that the progressbox is over (second child process, running the 619 # dialog-like program), we can wait() for the first child process. 620 # Otherwise, we could have a deadlock in case the pipe gets full, since 621 # dialog wouldn't be reading it. 622 exit_info = os.waitpid(child_pid, 0)[1] 623 if os.WIFEXITED(exit_info): 624 exit_code = os.WEXITSTATUS(exit_info) 625 elif os.WIFSIGNALED(exit_info): 626 d.msgbox("%s(): first child process terminated by signal %d" % 627 (func_name, os.WTERMSIG(exit_info))) 628 else: 629 assert False, "How the hell did we manage to get here?" 630 631 if exit_code != 0: 632 d.msgbox("%s(): first child process ended with exit status %d" 633 % (func_name, exit_code)) 634 635 def progressbox_demo_with_file_descriptor(self): 636 func_name = "progressbox_demo_with_file_descriptor" 637 text = """\ 638A long time ago in a galaxy far, 639far away... 640 641 642 643 644 645A NEW HOPE 646 647It was a period of intense 648sucking. Graphical toolkits for 649Python were all nice and clean, 650but they were, well, graphical. 651And as every one knows, REAL 652PROGRAMMERS ALWAYS WORK ON VT-100 653TERMINALS. In text mode. 654 655Besides, those graphical toolkits 656were usually too complex for 657simple programs, so most FLOSS 658geeks ended up writing 659command-line tools except when 660they really needed the full power 661of mainstream graphical toolkits, 662such as Qt, GTK+ and wxWidgets. 663 664But... thanks to people like 665Thomas E. Dickey, there are now 666at our disposal several free 667software command-line programs, 668such as dialog, that allow easy 669building of graphically-oriented 670interfaces in text-mode 671terminals. These are good for 672tasks where line-oriented 673interfaces are not well suited, 674as well as for the increasingly 675common type who runs away as soon 676as he sees something remotely 677resembling a command line. 678 679But this is not for Python! I want 680my poney! 681 682Seeing this unacceptable 683situation, Robb Shecter had the 684idea, back in the olden days of 685Y2K (when the world was supposed 686to suddenly collapse, remember?), 687to wrap a dialog interface into a 688Python module called dialog.py. 689 690pythondialog was born. Florent 691Rougon, who was looking for 692something like that in 2002, 693found the idea rather cool and 694improved the module during the 695following years...""" + 15*'\n' 696 697 return self.progressboxoid("progressbox", func_name, text, 698 width=78, height=20) 699 700 def programbox_demo(self): 701 func_name = "programbox_demo" 702 text = """\ 703The 'progressbox' widget 704has a little brother 705called 'programbox' 706that displays text 707read from a pipe 708and only adds an OK button 709when the pipe indicates EOF 710(End Of File). 711 712This can be used 713to display the output 714of some external program. 715 716This will be done right away if you choose "Yes" in the next dialog. 717This choice will cause 'find /usr/bin' to be run with subprocess.Popen() 718and the output to be displayed, via a pipe, in a 'programbox' widget.""" 719 self.progressboxoid("programbox", func_name, text, width=78, height=20) 720 721 if d.Yesno("Do you want to run 'find /usr/bin' in a programbox widget?"): 722 try: 723 devnull = subprocess.DEVNULL 724 except AttributeError: # Python < 3.3 725 devnull_context = devnull = open(os.devnull, "wb") 726 else: 727 devnull_context = DummyContextManager() 728 729 args = ["find", "/usr/bin"] 730 with devnull_context: 731 p = subprocess.Popen(args, stdout=subprocess.PIPE, 732 stderr=devnull, close_fds=True) 733 # One could use title=... instead of text=... to put the text 734 # in the title bar. 735 d.programbox(fd=p.stdout.fileno(), 736 text="Example showing the output of a command " 737 "with programbox", width=78, height=20) 738 retcode = p.wait() 739 740 # Context manager support for subprocess.Popen objects requires 741 # Python 3.2 or later. 742 p.stdout.close() 743 return retcode 744 else: 745 return None 746 747 def infobox_demo(self): 748 d.infobox("One moment, please. Just wasting some time here to " 749 "show you the infobox...") 750 751 time.sleep(0.5 if params["fast_mode"] else 4.0) 752 753 def gauge_demo(self): 754 d.gauge_start("Progress: 0%", title="Still testing your patience...") 755 756 for i in range(1, 101): 757 if i < 50: 758 d.gauge_update(i, "Progress: {0}%".format(i), update_text=True) 759 elif i == 50: 760 d.gauge_update(i, "Over {0}%. Good.".format(i), 761 update_text=True) 762 elif i == 80: 763 d.gauge_update(i, "Yeah, this boring crap will be over Really " 764 "Soon Now.", update_text=True) 765 else: 766 d.gauge_update(i) 767 768 time.sleep(0.01 if params["fast_mode"] else 0.1) 769 770 d.gauge_stop() 771 772 def mixedgauge_demo(self): 773 for i in range(1, 101, 20): 774 d.mixedgauge("This is the 'text' part of the mixedgauge\n" 775 "and this is a forced new line.", 776 title="'mixedgauge' demo", 777 percent=int(round(72+28*i/100)), 778 elements=[("Task 1", "Foobar"), 779 ("Task 2", 0), 780 ("Task 3", 1), 781 ("Task 4", 2), 782 ("Task 5", 3), 783 ("", 8), 784 ("Task 6", 5), 785 ("Task 7", 6), 786 ("Task 8", 7), 787 ("", ""), 788 # 0 is the dialog special code for 789 # "Succeeded", so these must not be equal to 790 # zero! That is why I made the range() above 791 # start at 1. 792 ("Task 9", -max(1, 100-i)), 793 ("Task 10", -i)]) 794 time.sleep(0.5 if params["fast_mode"] else 2) 795 796 def yesno_demo(self, with_help=True): 797 if not with_help: 798 # Simple version, without the "Help" button (the return value is 799 # True or False): 800 return d.Yesno("\nDo you like this demo?", yes_label="Yes, I do", 801 no_label="No, I do not", height=10, width=40, 802 title="An Important Question") 803 804 # 'yesno' dialog box with custom Yes, No and Help buttons 805 while True: 806 reply = d.Yesnohelp("\nDo you like this demo?", 807 yes_label="Yes, I do", no_label="No, I do not", 808 help_label="Please help me!", height=10, 809 width=60, title="An Important Question") 810 if reply == "yes": 811 return True 812 elif reply == "no": 813 return False 814 elif reply == "help": 815 d.msgbox("""\ 816I can hear your cry for help, and would really like to help you. However, I \ 817am afraid there is not much I can do for you here; you will have to decide \ 818for yourself on this matter. 819 820Keep in mind that you can always rely on me. \ 821You have all my support, be brave!""", 822 height=15, width=60, 823 title="From Your Faithful Servant") 824 else: 825 assert False, "Unexpected reply from MyDialog.Yesnohelp(): " \ 826 + repr(reply) 827 828 def msgbox_demo(self, answer): 829 if answer: 830 msg = "Excellent! Press OK to see its source code (or another " \ 831 "file if not in the correct directory)." 832 else: 833 msg = "Well, feel free to send your complaints to /dev/null!\n\n" \ 834 "Sincerely yours, etc." 835 836 d.msgbox(msg, height=7, width=50) 837 838 def textbox_demo(self): 839 # Better use the absolute path for displaying in the dialog title 840 filepath = os.path.abspath(__file__) 841 code = d.textbox(filepath, width=76, 842 title="Contents of {0}".format(filepath), 843 extra_button=True, extra_label="Stop it now!") 844 845 if code == "extra": 846 d.msgbox("Your wish is my command, Master.", width=40, 847 title="Exiting") 848 sys.exit(0) 849 850 def timeout_demo(self): 851 msg = "If you take more than five seconds to validate this, I'll " \ 852 "know. :)" 853 code = d.msgbox(msg, title="Widget with a timeout", timeout=5) 854 if code == d.TIMEOUT: 855 d.msgbox("Timeout expired.") 856 else: 857 d.msgbox("You answered in less than five seconds.") 858 859 def inputbox_demo(self): 860 code, answer = d.inputbox("What's your name?", init="Snow White") 861 return answer 862 863 def inputbox_demo_with_help(self): 864 init_str = "Snow White" 865 while True: 866 code, answer = d.inputbox("What's your name?", init=init_str, 867 title="'inputbox' demo", help_button=True) 868 869 if code == "help": 870 d.msgbox("Help from the 'inputbox' demo. The string entered " 871 "so far is {0!r}.".format(answer), 872 title="'inputbox' demo") 873 init_str = answer 874 else: 875 break 876 877 return answer 878 879 def form_demo(self): 880 elements = [ 881 ("Size (cm)", 1, 1, "175", 1, 20, 4, 3), 882 ("Weight (kg)", 2, 1, "85", 2, 20, 4, 3), 883 ("City", 3, 1, "Groboule-les-Bains", 3, 20, 15, 25), 884 ("State", 4, 1, "Some Lost Place", 4, 20, 15, 25), 885 ("Country", 5, 1, "Nowhereland", 5, 20, 15, 20), 886 ("My", 6, 1, "I hereby declare that, upon leaving this " 887 "world, all", 6, 20, 0, 0), 888 ("Very", 7, 1, "my fortune shall be transferred to Florent " 889 "Rougon's", 7, 20, 0, 0), 890 ("Last", 8, 1, "bank account number 000 4237 4587 32454/78 at " 891 "Banque", 8, 20, 0, 0), 892 ("Will", 9, 1, "Cantonale Vaudoise, Lausanne, Switzerland.", 893 9, 20, 0, 0) ] 894 895 code, fields = d.form("Please fill in some personal information:", 896 elements, width=77) 897 return fields 898 899 def form_demo_with_help(self, item_help=True): 900 # This function is slightly complex because it provides help support 901 # with 'help_status=True', and optionally also with 'item_help=True' 902 # together with 'help_tags=True'. For a very simple version (without 903 # any help support), see form_demo() above. 904 minver_for_helptags = "1.2-20130902" 905 906 if item_help: 907 if self.dialog_version_check(minver_for_helptags): 908 complement = """'item_help=True' is also used in conjunction \ 909with 'help_tags=True' in order to display per-item help at the bottom of the \ 910widget.""" 911 else: 912 item_help = False 913 complement = """'item_help=True' is not used, because to make \ 914it consistent with the 'item_help=False' case, dialog {min} or later is \ 915required (for the --help-tags option); however, it appears that you are using \ 916version {used}.""".format(min=minver_for_helptags, 917 used=d.cached_backend_version) 918 else: 919 complement = """'item_help=True' is not used, because it has \ 920been disabled; therefore, there is no per-item help at the bottom of the \ 921widget.""" 922 923 text = """\ 924This is a demo for the 'form' widget, which is similar to 'mixedform' but \ 925a bit simpler in that it has no notion of field type (to hide contents such \ 926as passwords). 927 928This demo uses 'help_button=True' to provide a Help button \ 929and 'help_status=True' to allow redisplaying the widget in the same state \ 930when leaving the help dialog. {complement}""".format(complement=complement) 931 932 elements = [ ("Fruit", 1, 8, "mirabelle plum", 1, 20, 18, 30), 933 ("Color", 2, 8, "yellowish", 2, 20, 18, 30), 934 ("Flavor", 3, 8, "sweet when ripe", 3, 20, 18, 30), 935 ("Origin", 4, 8, "Lorraine", 4, 20, 18, 30) ] 936 937 more_kwargs = {} 938 939 if item_help: 940 more_kwargs.update({ "item_help": True, 941 "help_tags": True }) 942 elements = [ list(l) + [ "Help text for item {0}".format(i+1) ] 943 for i, l in enumerate(elements) ] 944 945 while True: 946 code, t = d.form(text, elements, height=20, width=65, 947 title="'form' demo with help facilities", 948 help_button=True, help_status=True, **more_kwargs) 949 950 if code == "help": 951 label, status, elements = t 952 d.msgbox("You asked for help concerning the field labelled " 953 "{0!r}.".format(label)) 954 else: 955 # 't' contains the list of items as filled by the user 956 break 957 958 answers = '\n'.join(t) 959 d.msgbox("Your answers:\n\n{0}".format(indent(answers, " ")), 960 title="'form' demo with help facilities", no_collapse=True) 961 return t 962 963 def mixedform_demo(self): 964 HIDDEN = 0x1 965 READ_ONLY = 0x2 966 967 elements = [ 968 ("Size (cm)", 1, 1, "175", 1, 20, 4, 3, 0x0), 969 ("Weight (kg)", 2, 1, "85", 2, 20, 4, 3, 0x0), 970 ("City", 3, 1, "Groboule-les-Bains", 3, 20, 15, 25, 0x0), 971 ("State", 4, 1, "Some Lost Place", 4, 20, 15, 25, 0x0), 972 ("Country", 5, 1, "Nowhereland", 5, 20, 15, 20, 0x0), 973 ("My", 6, 1, "I hereby declare that, upon leaving this " 974 "world, all", 6, 20, 54, 0, READ_ONLY), 975 ("Very", 7, 1, "my fortune shall be transferred to Florent " 976 "Rougon's", 7, 20, 54, 0, READ_ONLY), 977 ("Last", 8, 1, "bank account number 000 4237 4587 32454/78 at " 978 "Banque", 8, 20, 54, 0, READ_ONLY), 979 ("Will", 9, 1, "Cantonale Vaudoise, Lausanne, Switzerland.", 980 9, 20, 54, 0, READ_ONLY), 981 ("Read-only field...", 10, 1, "... that doesn't go into the " 982 "output list", 10, 20, 0, 0, 0x0), 983 (r"\/3r`/ 53kri7 (0d3", 11, 1, "", 11, 20, 15, 20, HIDDEN) ] 984 985 code, fields = d.mixedform( 986 "Please fill in some personal information:", elements, width=77) 987 988 return fields 989 990 def passwordform_demo(self): 991 elements = [ 992 ("Secret field 1", 1, 1, "", 1, 20, 12, 0), 993 ("Secret field 2", 2, 1, "", 2, 20, 12, 0), 994 ("Secret field 3", 3, 1, "Providing a non-empty initial content " 995 "(like this) for an invisible field can be very confusing!", 996 3, 20, 30, 160)] 997 998 code, fields = d.passwordform( 999 "Please enter all your secret passwords.\n\nOn purpose here, " 1000 "nothing is echoed when you type in the passwords. If you want " 1001 "asterisks, use the 'insecure' keyword argument as in the " 1002 "passwordbox demo.", 1003 elements, width=77, height=15, title="Passwordform demo") 1004 1005 d.msgbox("Secret password 1: '%s'\n" 1006 "Secret password 2: '%s'\n" 1007 "Secret password 3: '%s'" % tuple(fields), 1008 width=60, height=20, title="The Whole Truth Now Revealed") 1009 1010 return fields 1011 1012 def menu_demo(self, name, city, state, country, size, weight, secret_code, 1013 last_will1, last_will2, last_will3, last_will4): 1014 text = """\ 1015Hello, %s from %s, %s, %s, %s cm, %s kg. 1016Thank you for giving us your Very Secret Code '%s'. 1017 1018As expressly stated in the previous form, your Last Will reads: "%s" 1019 1020All that was very interesting, thank you. However, in order to know you \ 1021better and provide you with the best possible customer service, we would \ 1022still need to know your favorite day of the week. Please indicate your \ 1023preference below.""" \ 1024 % (name, city, state, country, size, weight, secret_code, 1025 ' '.join([last_will1, last_will2, last_will3, last_will4])) 1026 1027 code, tag = d.menu(text, height=23, width=76, menu_height=7, 1028 choices=[("Monday", "Being the first day of the week..."), 1029 ("Tuesday", "Comes after Monday"), 1030 ("Wednesday", "Before Thursday day"), 1031 ("Thursday", "Itself after Wednesday"), 1032 ("Friday", "The best day of all"), 1033 ("Saturday", "Well, I've had enough, thanks"), 1034 ("Sunday", "Let's rest a little bit")]) 1035 1036 return tag 1037 1038 def menu_demo_with_help(self): 1039 text = """Sample 'menu' dialog box with help_button=True and \ 1040item_help=True.""" 1041 1042 while True: 1043 code, tag = d.menu(text, height=16, width=60, menu_height=7, 1044 choices=[("Tag 1", "Item 1", "Help text for item 1"), 1045 ("Tag 2", "Item 2", "Help text for item 2"), 1046 ("Tag 3", "Item 3", "Help text for item 3"), 1047 ("Tag 4", "Item 4", "Help text for item 4"), 1048 ("Tag 5", "Item 5", "Help text for item 5"), 1049 ("Tag 6", "Item 6", "Help text for item 6"), 1050 ("Tag 7", "Item 7", "Help text for item 7"), 1051 ("Tag 8", "Item 8", "Help text for item 8")], 1052 title="A menu with help facilities", 1053 help_button=True, item_help=True, help_tags=True) 1054 1055 if code == "help": 1056 d.msgbox("You asked for help concerning the item identified by " 1057 "tag {0!r}.".format(tag), height=8, width=40) 1058 else: 1059 break 1060 1061 d.msgbox("You have chosen the item identified by tag " 1062 "{0!r}.".format(tag), height=8, width=40) 1063 1064 def checklist_demo(self): 1065 # We could put non-empty items here (not only the tag for each entry) 1066 code, tags = d.checklist(text="What sandwich toppings do you like?", 1067 height=15, width=54, list_height=7, 1068 choices=[("Catsup", "", False), 1069 ("Mustard", "", False), 1070 ("Pesto", "", False), 1071 ("Mayonnaise", "", True), 1072 ("Horse radish","", True), 1073 ("Sun-dried tomatoes", "", True)], 1074 title="Do you prefer ham or spam?", 1075 backtitle="And now, for something " 1076 "completely different...") 1077 return tags 1078 1079 SAMPLE_DATA_FOR_BUILDLIST_AND_CHECKLIST = [ 1080 ("Tag 1", "Item 1", True, "Help text for item 1"), 1081 ("Tag 2", "Item 2", False, "Help text for item 2"), 1082 ("Tag 3", "Item 3", False, "Help text for item 3"), 1083 ("Tag 4", "Item 4", True, "Help text for item 4"), 1084 ("Tag 5", "Item 5", True, "Help text for item 5"), 1085 ("Tag 6", "Item 6", False, "Help text for item 6"), 1086 ("Tag 7", "Item 7", True, "Help text for item 7"), 1087 ("Tag 8", "Item 8", False, "Help text for item 8") ] 1088 1089 def checklist_demo_with_help(self): 1090 text = """Sample 'checklist' dialog box with help_button=True, \ 1091item_help=True and help_status=True.""" 1092 choices = self.SAMPLE_DATA_FOR_BUILDLIST_AND_CHECKLIST 1093 1094 while True: 1095 code, t = d.checklist(text, choices=choices, 1096 title="A checklist with help facilities", 1097 help_button=True, item_help=True, 1098 help_tags=True, help_status=True) 1099 if code == "help": 1100 tag, selected_tags, choices = t 1101 d.msgbox("You asked for help concerning the item identified " 1102 "by tag {0!r}.".format(tag), height=7, width=60) 1103 else: 1104 # 't' contains the list of tags corresponding to checked items 1105 break 1106 1107 s = '\n'.join(t) 1108 d.msgbox("The tags corresponding to checked items are:\n\n" 1109 "{0}".format(indent(s, " ")), height=15, width=60, 1110 title="'checklist' demo with help facilities", 1111 no_collapse=True) 1112 1113 def radiolist_demo(self): 1114 choices = [ 1115 ("Hamburger", "2 slices of bread, a steak...", False), 1116 ("Hotdog", "doesn't bite any more", False), 1117 ("Burrito", "no se lo que es", False), 1118 ("Doener", "Huh?", False), 1119 ("Falafel", "Erm...", False), 1120 ("Bagel", "Of course!", False), 1121 ("Big Mac", "Ah, that's easy!", True), 1122 ("Whopper", "Erm, sorry", False), 1123 ("Quarter Pounder", 'called "le Big Mac" in France', False), 1124 ("Peanut Butter and Jelly", "Well, that's your own business...", 1125 False), 1126 ("Grilled cheese", "And nothing more?", False) ] 1127 1128 while True: 1129 code, t = d.radiolist( 1130 "What's your favorite kind of sandwich?", width=68, 1131 choices=choices, help_button=True, help_status=True) 1132 1133 if code == "help": 1134 # Prepare to redisplay the radiolist in the same state as it 1135 # was before the user pressed the Help button. 1136 tag, selected, choices = t 1137 d.msgbox("You asked for help about something called {0!r}. " 1138 "Sorry, but I am quite incompetent in this matter." 1139 .format(tag)) 1140 else: 1141 # 't' is the chosen tag 1142 break 1143 1144 return t 1145 1146 def rangebox_demo(self): 1147 nb = 10 # initial value 1148 1149 while True: 1150 code, nb = d.rangebox("""\ 1151How many Microsoft(TM) engineers are needed to prepare such a sandwich? 1152 1153You can use the Up and Down arrows, Page Up and Page Down, Home and End keys \ 1154to change the value; you may also use the Tab key, Left and Right arrows \ 1155and any of the 0-9 keys to change a digit of the value.""", 1156 min=1, max=20, init=nb, 1157 extra_button=True, extra_label="Joker") 1158 if code == "ok": 1159 break 1160 elif code == "extra": 1161 d.msgbox("Well, {0} may be enough. Or not, depending on the " 1162 "phase of the moon...".format(nb)) 1163 else: 1164 assert False, "Unexpected Dialog exit code: {0!r}".format(code) 1165 1166 return nb 1167 1168 def buildlist_demo(self): 1169 items0 = [("A Monty Python DVD", False), 1170 ("A Monty Python script", False), 1171 ('A DVD of "Barry Lyndon" by Stanley Kubrick', False), 1172 ('A DVD of "The Good, the Bad and the Ugly" by Sergio Leone', 1173 False), 1174 ('A DVD of "The Trial" by Orson Welles', False), 1175 ('The Trial, by Franz Kafka', False), 1176 ('Animal Farm, by George Orwell', False), 1177 ('Notre-Dame de Paris, by Victor Hugo', False), 1178 ('Les Misérables, by Victor Hugo', False), 1179 ('Le Lys dans la Vallée, by Honoré de Balzac', False), 1180 ('Les Rois Maudits, by Maurice Druon', False), 1181 ('A Georges Brassens CD', False), 1182 ("A book of Georges Brassens' songs", False), 1183 ('A Nina Simone CD', False), 1184 ('Javier Vazquez y su Salsa - La Verdad', False), 1185 ('The last Justin Bieber album', False), 1186 ('A printed copy of the Linux kernel source code', False), 1187 ('A CD player', False), 1188 ('A DVD player', False), 1189 ('An MP3 player', False)] 1190 1191 # Use the name as tag, item string and item-help string; the item-help 1192 # will be useful for long names because it is displayed in a place 1193 # that is large enough to avoid truncation. If not using 1194 # item_help=True, then the last element of eash tuple must be omitted. 1195 items = [ (tag, tag, status, tag) for (tag, status) in items0 ] 1196 1197 text = """If you were stranded on a desert island, what would you \ 1198take? 1199 1200Press the space bar to toggle the status of an item between selected (on \ 1201the left) and unselected (on the right). You can use the TAB key or \ 1202^ and $ to change the focus between the different parts of the widget. 1203 1204(this widget is called with item_help=True and visit_items=True)""" 1205 1206 code, l = d.buildlist(text, items=items, visit_items=True, 1207 item_help=True, 1208 title="A simple 'buildlist' demo") 1209 return l 1210 1211 def buildlist_demo_with_help(self): 1212 text = """Sample 'buildlist' dialog box with help_button=True, \ 1213item_help=True, help_status=True, and visit_items=False. 1214 1215Keys: SPACE select or deselect the highlighted item, i.e., 1216 move it between the left and right lists 1217 ^ move the focus to the left list 1218 $ move the focus to the right list 1219 TAB move focus 1220 ENTER press the focused button""" 1221 items = self.SAMPLE_DATA_FOR_BUILDLIST_AND_CHECKLIST 1222 1223 while True: 1224 code, t = d.buildlist(text, items=items, 1225 title="A 'buildlist' with help facilities", 1226 help_button=True, item_help=True, 1227 help_tags=True, help_status=True, 1228 no_collapse=True) 1229 if code == "help": 1230 tag, selected_tags, items = t 1231 d.msgbox("You asked for help concerning the item identified " 1232 "by tag {0!r}.".format(tag), height=7, width=60) 1233 else: 1234 # 't' contains the list of tags corresponding to selected items 1235 break 1236 1237 s = '\n'.join(t) 1238 d.msgbox("The tags corresponding to selected items are:\n\n" 1239 "{0}".format(indent(s, " ")), height=15, width=60, 1240 title="'buildlist' demo with help facilities", 1241 no_collapse=True) 1242 1243 def calendar_demo(self): 1244 code, date = d.calendar("When do you think Georg Cantor was born?") 1245 return date 1246 1247 def calendar_demo_with_help(self): 1248 # Start with the current date 1249 day, month, year = -1, -1, -1 1250 1251 while True: 1252 code, date = d.calendar("When do you think Georg Cantor was born?", 1253 day=day, month=month, year=year, 1254 title="'calendar' demo", 1255 help_button=True) 1256 if code == "help": 1257 day, month, year = date 1258 d.msgbox("Help dialog for date {0:04d}-{1:02d}-{2:02d}.".format( 1259 year, month, day), title="'calendar' demo") 1260 else: 1261 break 1262 1263 return date 1264 1265 def comment_on_Cantor_date_of_birth(self, day, month, year): 1266 complement = """\ 1267For your information, Georg Ferdinand Ludwig Philip Cantor, a great \ 1268mathematician, was born on March 3, 1845 in Saint Petersburg, and died on \ 1269January 6, 1918. Among other things, Georg Cantor laid the foundation for \ 1270the set theory (which is at the basis of most modern mathematics) \ 1271and was the first person to give a rigorous definition of real numbers.""" 1272 1273 if (year, month, day) == (1845, 3, 3): 1274 return "Spot-on! I'm impressed." 1275 elif year == 1845: 1276 return "You guessed the year right. {0}".format(complement) 1277 elif abs(year-1845) < 30: 1278 return "Not too far. {0}".format(complement) 1279 else: 1280 return "Well, not quite. {0}".format(complement) 1281 1282 def timebox_demo(self): 1283 # Get the current time (to display initially in the timebox) 1284 tm = time.localtime() 1285 init_hour, init_min, init_sec = tm.tm_hour, tm.tm_min, tm.tm_sec 1286 # tm.tm_sec can be 60 or even 61 according to the doc of the time module! 1287 init_sec = min(59, init_sec) 1288 1289 code, (hour, minute, second) = d.timebox( 1290 "And at what time, if I may ask?", 1291 hour=init_hour, minute=init_min, second=init_sec) 1292 1293 return (hour, minute, second) 1294 1295 def passwordbox_demo(self): 1296 # 'insecure' keyword argument only asks dialog to echo asterisks when 1297 # the user types characters. Not *that* bad. 1298 code, password = d.passwordbox("What is your root password, " 1299 "so that I can crack your system " 1300 "right now?", height=10, width=60, 1301 insecure=True) 1302 return password 1303 1304 def scrollbox_demo(self, name, favorite_day, toppings, sandwich, 1305 nb_engineers, desert_island_stuff, date, time_, 1306 password): 1307 tw71 = textwrap.TextWrapper(width=71, break_long_words=False, 1308 break_on_hyphens=True) 1309 1310 if nb_engineers is not None: 1311 sandwich_comment = " (the preparation of which requires, " \ 1312 "according to you, {nb_engineers} MS {engineers})".format( 1313 nb_engineers=nb_engineers, 1314 engineers="engineers" if nb_engineers != 1 else "engineer") 1315 else: 1316 sandwich_comment = "" 1317 1318 sandwich_report = "Favorite sandwich: {sandwich}{comment}".format( 1319 sandwich=sandwich, comment=sandwich_comment) 1320 1321 if desert_island_stuff is None: 1322 # The widget was not available, the user didn't see anything. 1323 desert_island_string = "" 1324 else: 1325 if len(desert_island_stuff) == 0: 1326 desert_things = " nothing!" 1327 else: 1328 desert_things = "\n\n " + "\n ".join(desert_island_stuff) 1329 1330 desert_island_string = \ 1331 "\nOn a desert island, you would take:{0}\n".format( 1332 desert_things) 1333 1334 day, month, year = date 1335 hour, minute, second = time_ 1336 msg = """\ 1337Here are some vital statistics about you: 1338 1339Name: {name} 1340Favorite day of the week: {favday} 1341Favorite sandwich toppings:{toppings} 1342{sandwich_report} 1343{desert_island_string} 1344Your answer about Georg Cantor's date of birth: \ 1345{year:04d}-{month:02d}-{day:02d} 1346(at precisely {hour:02d}:{min:02d}:{sec:02d}!) 1347 1348{comment} 1349 1350Your root password is: ************************** (looks good!)""".format( 1351 name=name, favday=favorite_day, 1352 toppings="\n ".join([''] + toppings), 1353 sandwich_report=tw71.fill(sandwich_report), 1354 desert_island_string=desert_island_string, 1355 year=year, month=month, day=day, 1356 hour=hour, min=minute, sec=second, 1357 comment=tw71.fill( 1358 self.comment_on_Cantor_date_of_birth(day, month, year))) 1359 d.scrollbox(msg, height=20, width=75, title="Great Report of the Year") 1360 1361 TREEVIEW_BASE_TEXT = """\ 1362This is an example of the 'treeview' widget{options}. Nodes are labelled in a \ 1363way that reflects their position in the tree, but this is not a requirement: \ 1364you are free to name them the way you like. 1365 1366Node 0 is the root node. It has 3 children tagged 0.1, 0.2 and 0.3. \ 1367You should now select a node with the space bar.""" 1368 1369 def treeview_demo(self): 1370 code, tag = d.treeview(self.TREEVIEW_BASE_TEXT.format(options=""), 1371 nodes=[ ("0", "node 0", False, 0), 1372 ("0.1", "node 0.1", False, 1), 1373 ("0.2", "node 0.2", False, 1), 1374 ("0.2.1", "node 0.2.1", False, 2), 1375 ("0.2.1.1", "node 0.2.1.1", True, 3), 1376 ("0.2.2", "node 0.2.2", False, 2), 1377 ("0.3", "node 0.3", False, 1), 1378 ("0.3.1", "node 0.3.1", False, 2), 1379 ("0.3.2", "node 0.3.2", False, 2) ], 1380 title="'treeview' demo") 1381 1382 d.msgbox("You selected the node tagged {0!r}.".format(tag), 1383 title="treeview demo") 1384 return tag 1385 1386 def treeview_demo_with_help(self): 1387 text = self.TREEVIEW_BASE_TEXT.format( 1388 options=" with help_button=True, item_help=True and " 1389 "help_status=True") 1390 1391 nodes = [ ("0", "node 0", False, 0, "Help text 1"), 1392 ("0.1", "node 0.1", False, 1, "Help text 2"), 1393 ("0.2", "node 0.2", False, 1, "Help text 3"), 1394 ("0.2.1", "node 0.2.1", False, 2, "Help text 4"), 1395 ("0.2.1.1", "node 0.2.1.1", True, 3, "Help text 5"), 1396 ("0.2.2", "node 0.2.2", False, 2, "Help text 6"), 1397 ("0.3", "node 0.3", False, 1, "Help text 7"), 1398 ("0.3.1", "node 0.3.1", False, 2, "Help text 8"), 1399 ("0.3.2", "node 0.3.2", False, 2, "Help text 9") ] 1400 1401 while True: 1402 code, t = d.treeview(text, nodes=nodes, 1403 title="'treeview' demo with help facilities", 1404 help_button=True, item_help=True, 1405 help_tags=True, help_status=True) 1406 1407 if code == "help": 1408 # Prepare to redisplay the treeview in the same state as it 1409 # was before the user pressed the Help button. 1410 tag, selected_tag, nodes = t 1411 d.msgbox("You asked for help about the node with tag {0!r}." 1412 .format(tag)) 1413 else: 1414 # 't' is the chosen tag 1415 break 1416 1417 d.msgbox("You selected the node tagged {0!r}.".format(t), 1418 title="'treeview' demo") 1419 return t 1420 1421 def editbox_demo(self, filepath): 1422 if os.path.isfile(filepath): 1423 code, text = d.editbox(filepath, 20, 60, 1424 title="A Cheap Text Editor") 1425 d.scrollbox(text, title="Resulting text") 1426 else: 1427 d.msgbox("Skipping the first part of the 'editbox' demo, " 1428 "as '{0}' can't be found.".format(filepath), 1429 title="'msgbox' demo") 1430 1431 l = ["In the previous dialog, the initial contents was", 1432 "explicitly written to a file. With Dialog.editbox_str(),", 1433 "you can provide it as a string and pythondialog will", 1434 "automatically create and delete a temporary file for you", 1435 "holding this text for dialog.\n"] + \ 1436 [ "This is line {0} of a boring sample text.".format(i+1) 1437 for i in range(100) ] 1438 code, text = d.editbox_str('\n'.join(l), 0, 0, 1439 title="A Cheap Text Editor") 1440 d.scrollbox(text, title="Resulting text") 1441 1442 def inputmenu_demo(self): 1443 choices = [ ("1st_tag", "Item 1 text"), 1444 ("2nd_tag", "Item 2 text"), 1445 ("3rd_tag", "Item 3 text") ] 1446 1447 for i in range(4, 21): 1448 choices.append(("%dth_tag" % i, "Item %d text" % i)) 1449 1450 while True: 1451 code, tag, new_item_text = d.inputmenu( 1452 "Demonstration of 'inputmenu'. Any single item can be either " 1453 "accepted as is, or renamed.", 1454 width=60, menu_height=10, choices=choices, 1455 help_button=True, title="'inputmenu' demo") 1456 1457 if code == "help": 1458 d.msgbox("You asked for help about the item with tag {0!r}." 1459 .format(tag)) 1460 continue 1461 elif code == "accepted": 1462 text = "The item corresponding to tag {0!r} was " \ 1463 "accepted.".format(tag) 1464 elif code == "renamed": 1465 text = "The item corresponding to tag {0!r} was renamed to " \ 1466 "{1!r}.".format(tag, new_item_text) 1467 else: 1468 text = "Unexpected exit code from 'inputmenu': {0!r}.\n\n" \ 1469 "It may be a bug. Please report.".format(code) 1470 1471 break 1472 1473 d.msgbox(text, height=10, width=60, 1474 title="Outcome of the 'inputmenu' demo") 1475 1476 # Help strings used in several places 1477 FSELECT_HELP = """\ 1478Hint: the complete file path must be entered in the bottom field. One \ 1479convenient way to achieve this is to use the SPACE bar when the desired file \ 1480is highlighted in the top-right list. 1481 1482As usual, you can use the TAB and arrow keys to move between controls. If in \ 1483the bottom field, the SPACE key provides auto-completion.""" 1484 1485 # The following help text was initially meant to be used for several 1486 # widgets (at least progressbox and tailbox). Currently (dialog version 1487 # 1.2-20130902), "dialog --tailbox" doesn't seem to work with FIFOs, so the 1488 # "flexibility" of the help text is unused (another text is used when 1489 # demonstrating --tailbox). However, this might change in the future... 1490 def FIFO_HELP(self, widget): 1491 return """\ 1492For demos based on the {widget} widget, you may use a FIFO, also called \ 1493"named pipe". This is a special kind of file, to which you will be able to \ 1494easily append data. With the {widget} widget, you can see the data stream \ 1495flow in real time. 1496 1497To create a FIFO, you can use the commmand mkfifo(1), like this: 1498 1499 % mkfifo /tmp/my_shiny_new_fifo 1500 1501Then, you can cat(1) data to the FIFO like this: 1502 1503 % cat >>/tmp/my_shiny_new_fifo 1504 First line of text 1505 Second line of text 1506 ... 1507 1508You can end the input to cat(1) by typing Ctrl-D at the beginning of a \ 1509line.""".format(widget=widget) 1510 1511 def fselect_demo(self, widget, init_path=None, allow_FIFOs=False, **kwargs): 1512 init_path = init_path or params["home_dir"] 1513 # Make sure the directory we chose ends with os.sep so that dialog 1514 # shows its contents right away 1515 if not init_path.endswith(os.sep): 1516 init_path += os.sep 1517 1518 while True: 1519 # We want to let the user quit this particular dialog with Cancel 1520 # without having to bother choosing a file, therefore we use the 1521 # original fselect() from dialog.Dialog and interpret the return 1522 # code manually. (By default, the MyDialog class defined in this 1523 # file intercepts the CANCEL and ESC exit codes and causes them to 1524 # spawn the "confirm quit" dialog.) 1525 code, path = self.Dialog_instance.fselect( 1526 init_path, height=10, width=60, help_button=True, **kwargs) 1527 1528 # Display the "confirm quit" dialog if the user pressed ESC. 1529 if not d.check_exit_request(code, ignore_Cancel=True): 1530 continue 1531 1532 # Provide an easy way out... 1533 if code == d.CANCEL: 1534 path = None 1535 break 1536 elif code == "help": 1537 d.msgbox("Help about {0!r} from the 'fselect' dialog.".format( 1538 path), title="'fselect' demo") 1539 init_path = path 1540 elif code == d.OK: 1541 # Of course, one can use os.path.isfile(path) here, but we want 1542 # to allow regular files *and* possibly FIFOs. Since there is 1543 # no os.path.is*** convenience function for FIFOs, let's go 1544 # with os.stat. 1545 try: 1546 mode = os.stat(path)[stat.ST_MODE] 1547 except os.error as e: 1548 d.msgbox("Error: {0}".format(e)) 1549 continue 1550 1551 # Accept FIFOs only if allow_FIFOs is True 1552 if stat.S_ISREG(mode) or (allow_FIFOs and stat.S_ISFIFO(mode)): 1553 break 1554 else: 1555 if allow_FIFOs: 1556 help_text = """\ 1557You are expected to select a *file* here (possibly a FIFO), or press the \ 1558Cancel button.\n\n%s 1559 1560For your convenience, I will reproduce the FIFO help text here:\n\n%s""" \ 1561 % (self.FSELECT_HELP, self.FIFO_HELP(widget)) 1562 else: 1563 help_text = """\ 1564You are expected to select a regular *file* here, or press the \ 1565Cancel button.\n\n%s""" % (self.FSELECT_HELP,) 1566 1567 d.msgbox(help_text, width=72, height=20) 1568 else: 1569 d.msgbox("Unexpected exit code from Dialog.fselect(): {0}.\n\n" 1570 "It may be a bug. Please report.".format(code)) 1571 return path 1572 1573 def dselect_demo(self, init_dir=None): 1574 init_dir = init_dir or params["home_dir"] 1575 # Make sure the directory we chose ends with os.sep so that dialog 1576 # shows its contents right away 1577 if not init_dir.endswith(os.sep): 1578 init_dir += os.sep 1579 1580 while True: 1581 code, path = d.dselect(init_dir, 10, 50, 1582 title="Please choose a directory", 1583 help_button=True) 1584 if code == "help": 1585 d.msgbox("Help about {0!r} from the 'dselect' dialog.".format( 1586 path), title="'dselect' demo") 1587 init_dir = path 1588 # When Python 3.2 is old enough, we'll be able to check if 1589 # path.endswith(os.sep) and remove the trailing os.sep if this 1590 # does not change the path according to os.path.samefile(). 1591 elif not os.path.isdir(path): 1592 d.msgbox("Hmm. It seems that {0!r} is not a directory".format( 1593 path), title="'dselect' demo") 1594 else: 1595 break 1596 1597 d.msgbox("Directory '%s' thanks you for choosing him." % path) 1598 return path 1599 1600 def tailbox_demo(self, height=22, width=78): 1601 widget = "tailbox" 1602 1603 # First, ask the user for a file. 1604 # Strangely (dialog version 1.2-20130902 bug?), "dialog --tailbox" 1605 # doesn't work with FIFOs: "Error moving file pointer in last_lines()" 1606 # and DIALOG_ERROR exit status. 1607 path = self.fselect_demo(widget, allow_FIFOs=False, 1608 title="Please choose a file to be shown as " 1609 "with 'tail -f'") 1610 # Now, the tailbox 1611 if path is None: 1612 # User chose to abort 1613 return 1614 else: 1615 d.tailbox(path, height, width, title="Tailbox example") 1616 1617 def pause_demo(self, seconds): 1618 d.pause("""\ 1619Ugh, sorry. pythondialog is still in development, and its advanced circuitry \ 1620detected internal error number 0x666. That's a pretty nasty one, you know. 1621 1622I am embarrassed. I don't know how to tell you, but we are going to have to \ 1623reboot. In %d seconds. 1624 1625Fasten your seatbelt...""" % seconds, height=18, seconds=seconds) 1626 1627 1628def process_command_line(): 1629 global params 1630 1631 try: 1632 opts, args = getopt.getopt(sys.argv[1:], "ftE", 1633 ["test-suite", 1634 "fast", 1635 "debug", 1636 "debug-file=", 1637 "debug-expand-file-opt", 1638 "help", 1639 "version"]) 1640 except getopt.GetoptError: 1641 print(usage, file=sys.stderr) 1642 return ("exit", 1) 1643 1644 # Let's start with the options that don't require any non-option argument 1645 # to be present 1646 for option, value in opts: 1647 if option == "--help": 1648 print(usage) 1649 return ("exit", 0) 1650 elif option == "--version": 1651 print("%s %s\n%s" % (progname, progversion, version_blurb)) 1652 return ("exit", 0) 1653 1654 # Now, require a correct invocation. 1655 if len(args) != 0: 1656 print(usage, file=sys.stderr) 1657 return ("exit", 1) 1658 1659 # Default values for parameters 1660 params = { "fast_mode": False, 1661 "testsuite_mode": False, 1662 "debug": False, 1663 "debug_filename": default_debug_filename, 1664 "debug_expand_file_opt": False } 1665 1666 # Get the home directory, if any, and store it in params (often useful). 1667 root_dir = os.sep # This is OK for Unix-like systems 1668 params["home_dir"] = os.getenv("HOME", root_dir) 1669 1670 # General option processing 1671 for option, value in opts: 1672 if option in ("-t", "--test-suite"): 1673 params["testsuite_mode"] = True 1674 # --test-suite implies --fast 1675 params["fast_mode"] = True 1676 elif option in ("-f", "--fast"): 1677 params["fast_mode"] = True 1678 elif option == "--debug": 1679 params["debug"] = True 1680 elif option == "--debug-file": 1681 params["debug_filename"] = value 1682 elif option in ("-E", "--debug-expand-file-opt"): 1683 params["debug_expand_file_opt"] = True 1684 else: 1685 # The options (such as --help) that cause immediate exit 1686 # were already checked, and caused the function to return. 1687 # Therefore, if we are here, it can't be due to any of these 1688 # options. 1689 assert False, "Unexpected option received from the " \ 1690 "getopt module: '%s'" % option 1691 1692 return ("continue", None) 1693 1694 1695def main(): 1696 """This demo shows the main features of pythondialog.""" 1697 locale.setlocale(locale.LC_ALL, '') 1698 1699 what_to_do, code = process_command_line() 1700 if what_to_do == "exit": 1701 sys.exit(code) 1702 1703 try: 1704 app = MyApp() 1705 app.run() 1706 except dialog.error as exc_instance: 1707 # The error that causes a PythonDialogErrorBeforeExecInChildProcess to 1708 # be raised happens in the child process used to run the dialog-like 1709 # program, and the corresponding traceback is printed right away from 1710 # that child process when the error is encountered. Therefore, don't 1711 # print a second, not very useful traceback for this kind of exception. 1712 if not isinstance(exc_instance, 1713 dialog.PythonDialogErrorBeforeExecInChildProcess): 1714 print(traceback.format_exc(), file=sys.stderr) 1715 1716 print("Error (see above for a traceback):\n\n{0}".format( 1717 exc_instance), file=sys.stderr) 1718 sys.exit(1) 1719 1720 sys.exit(0) 1721 1722 1723if __name__ == "__main__": main() 1724