1 #!/usr/bin/env python 2 3 """ 4 Simple desktop dialogue box support for Python. 5 6 Copyright (C) 2007, 2009, 2014 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU Lesser General Public License as published by the Free 10 Software Foundation; either version 3 of the License, or (at your option) any 11 later version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 16 details. 17 18 You should have received a copy of the GNU Lesser General Public License along 19 with this program. If not, see <http://www.gnu.org/licenses/>. 20 21 -------- 22 23 Opening Dialogue Boxes (Dialogs) 24 -------------------------------- 25 26 To open a dialogue box (dialog) in the current desktop environment, relying on 27 the automatic detection of that environment, use the appropriate dialogue box 28 class: 29 30 question = desktop.dialog.Question("Are you sure?") 31 result = question.open() 32 33 To override the detected desktop, specify the desktop parameter to the open 34 function as follows: 35 36 question.open("KDE") # Insists on KDE 37 question.open("GNOME") # Insists on GNOME 38 39 The dialogue box options are documented in each class's docstring. 40 41 Available dialogue box classes are listed in the desktop.dialog.available 42 attribute. 43 44 Supported desktop environments are listed in the desktop.dialog.supported 45 attribute. 46 """ 47 48 from desktop import use_desktop, _run, _readfrom, _status 49 from time import strptime 50 51 class _wrapper: 52 def __init__(self, handler): 53 self.handler = handler 54 55 class _readvalue(_wrapper): 56 def __call__(self, cmd, shell): 57 return self.handler(cmd, shell).strip() 58 59 class _readinput(_wrapper): 60 def __call__(self, cmd, shell): 61 return self.handler(cmd, shell)[:-1] 62 63 class _readvalues_kdialog(_wrapper): 64 def __call__(self, cmd, shell): 65 result = self.handler(cmd, shell).strip().strip('"') 66 if result: 67 return result.split('" "') 68 else: 69 return [] 70 71 class _readvalues_zenity(_wrapper): 72 def __call__(self, cmd, shell): 73 result = self.handler(cmd, shell).strip() 74 if result: 75 return result.split("|") 76 else: 77 return [] 78 79 class _readvalues_Xdialog(_wrapper): 80 def __call__(self, cmd, shell): 81 result = self.handler(cmd, shell).strip() 82 if result: 83 return result.split("/") 84 else: 85 return [] 86 87 class _readdate_kdialog(_wrapper): 88 def __call__(self, cmd, shell): 89 result = self.handler(cmd, shell).strip() 90 if result: 91 return strptime(result, "%a %b %d %Y") 92 else: 93 return None 94 95 class _readdate_zenity(_wrapper): 96 def __call__(self, cmd, shell): 97 result = self.handler(cmd, shell).strip() 98 if result: 99 return strptime(result, "%Y %m %d") 100 else: 101 return None 102 103 # Dialogue parameter classes. 104 105 class String: 106 107 "A generic parameter." 108 109 def __init__(self, name): 110 self.name = name 111 112 def convert(self, value, program): 113 return [value or ""] 114 115 class Strings(String): 116 117 "Multiple string parameters." 118 119 def convert(self, value, program): 120 return value or [] 121 122 class StringPairs(String): 123 124 "Multiple string parameters duplicated to make identifiers." 125 126 def convert(self, value, program): 127 l = [] 128 for v in value: 129 l.append(v) 130 l.append(v) 131 return l 132 133 class StringKeyword: 134 135 "A keyword parameter." 136 137 def __init__(self, keyword, name): 138 self.keyword = keyword 139 self.name = name 140 141 def convert(self, value, program): 142 return [self.keyword + "=" + (value or "")] 143 144 class StringKeywords: 145 146 "Multiple keyword parameters." 147 148 def __init__(self, keyword, name): 149 self.keyword = keyword 150 self.name = name 151 152 def convert(self, value, program): 153 l = [] 154 for v in value or []: 155 l.append(self.keyword + "=" + v) 156 return l 157 158 class Integer(String): 159 160 "An integer parameter." 161 162 defaults = { 163 "width" : 40, 164 "height" : 15, 165 "list_height" : 10 166 } 167 scale = 8 168 169 def __init__(self, name, pixels=0): 170 String.__init__(self, name) 171 if pixels: 172 self.factor = self.scale 173 else: 174 self.factor = 1 175 176 def convert(self, value, program): 177 if value is None: 178 value = self.defaults[self.name] 179 return [str(int(value) * self.factor)] 180 181 class IntegerKeyword(Integer): 182 183 "An integer keyword parameter." 184 185 def __init__(self, keyword, name, pixels=0): 186 Integer.__init__(self, name, pixels) 187 self.keyword = keyword 188 189 def convert(self, value, program): 190 if value is None: 191 value = self.defaults[self.name] 192 return [self.keyword + "=" + str(int(value) * self.factor)] 193 194 class Boolean(String): 195 196 "A boolean parameter." 197 198 values = { 199 "kdialog" : ["off", "on"], 200 "zenity" : ["FALSE", "TRUE"], 201 "Xdialog" : ["off", "on"] 202 } 203 204 def convert(self, value, program): 205 values = self.values[program] 206 if value: 207 return [values[1]] 208 else: 209 return [values[0]] 210 211 class MenuItemList(String): 212 213 "A menu item list parameter." 214 215 def convert(self, value, program): 216 l = [] 217 for v in value: 218 l.append(v.value) 219 l.append(v.text) 220 return l 221 222 class ListItemList(String): 223 224 "A radiolist/checklist item list parameter." 225 226 def __init__(self, name, status_first=0): 227 String.__init__(self, name) 228 self.status_first = status_first 229 230 def convert(self, value, program): 231 l = [] 232 for v in value: 233 boolean = Boolean(None) 234 status = boolean.convert(v.status, program) 235 if self.status_first: 236 l += status 237 l.append(v.value) 238 l.append(v.text) 239 if not self.status_first: 240 l += status 241 return l 242 243 # Dialogue argument values. 244 245 class MenuItem: 246 247 "A menu item which can also be used with radiolists and checklists." 248 249 def __init__(self, value, text, status=0): 250 self.value = value 251 self.text = text 252 self.status = status 253 254 # Dialogue classes. 255 256 class Dialogue: 257 258 commands = { 259 "KDE" : "kdialog", 260 "KDE4" : "kdialog", 261 "GNOME" : "zenity", 262 "XFCE" : "zenity", # NOTE: Based on observations with Xubuntu. 263 "X11" : "Xdialog" 264 } 265 266 def open(self, desktop=None): 267 268 """ 269 Open a dialogue box (dialog) using a program appropriate to the desktop 270 environment in use. 271 272 If the optional 'desktop' parameter is specified then attempt to use 273 that particular desktop environment's mechanisms to open the dialog 274 instead of guessing or detecting which environment is being used. 275 276 Suggested values for 'desktop' are "standard", "KDE", "KDE4", "GNOME", 277 "Mac OS X", "Windows". 278 279 The result of the dialogue interaction may be a string indicating user 280 input (for Input, Password, Menu, Pulldown), a list of strings 281 indicating selections of one or more items (for RadioList, CheckList), 282 or a value indicating true or false (for Question, Warning, Message, 283 Error). 284 285 Where a string value may be expected but no choice is made, an empty 286 string may be returned. Similarly, where a list of values is expected 287 but no choice is made, an empty list may be returned. 288 """ 289 290 # Decide on the desktop environment in use. 291 292 desktop_in_use = use_desktop(desktop) 293 294 # Get the program. 295 296 try: 297 program = self.commands[desktop_in_use] 298 except KeyError: 299 raise OSError, "Desktop '%s' not supported (no known dialogue box command could be suggested)" % desktop_in_use 300 301 # The handler is one of the functions communicating with the subprocess. 302 # Some handlers return boolean values, others strings. 303 304 handler, options = self.info[program] 305 306 cmd = [program] 307 for option in options: 308 if isinstance(option, str): 309 cmd.append(option) 310 else: 311 value = getattr(self, option.name, None) 312 cmd += option.convert(value, program) 313 314 return handler(cmd, 0) 315 316 class Simple(Dialogue): 317 def __init__(self, text, width=None, height=None): 318 self.text = text 319 self.width = width 320 self.height = height 321 322 class Question(Simple): 323 324 """ 325 A dialogue asking a question and showing response buttons. 326 Options: text, width (in characters), height (in characters) 327 Response: a boolean value indicating an affirmative response (true) or a 328 negative response 329 """ 330 331 name = "question" 332 info = { 333 "kdialog" : (_status, ["--yesno", String("text")]), 334 "zenity" : (_status, ["--question", StringKeyword("--text", "text")]), 335 "Xdialog" : (_status, ["--stdout", "--yesno", String("text"), Integer("height"), Integer("width")]), 336 } 337 338 class Warning(Simple): 339 340 """ 341 A dialogue asking a question and showing response buttons. 342 Options: text, width (in characters), height (in characters) 343 Response: a boolean value indicating an affirmative response (true) or a 344 negative response 345 """ 346 347 name = "warning" 348 info = { 349 "kdialog" : (_status, ["--warningyesno", String("text")]), 350 "zenity" : (_status, ["--warning", StringKeyword("--text", "text")]), 351 "Xdialog" : (_status, ["--stdout", "--yesno", String("text"), Integer("height"), Integer("width")]), 352 } 353 354 class Message(Simple): 355 356 """ 357 A message dialogue. 358 Options: text, width (in characters), height (in characters) 359 Response: a boolean value indicating an affirmative response (true) or a 360 negative response 361 """ 362 363 name = "message" 364 info = { 365 "kdialog" : (_status, ["--msgbox", String("text")]), 366 "zenity" : (_status, ["--info", StringKeyword("--text", "text")]), 367 "Xdialog" : (_status, ["--stdout", "--msgbox", String("text"), Integer("height"), Integer("width")]), 368 } 369 370 class Error(Simple): 371 372 """ 373 An error dialogue. 374 Options: text, width (in characters), height (in characters) 375 Response: a boolean value indicating an affirmative response (true) or a 376 negative response 377 """ 378 379 name = "error" 380 info = { 381 "kdialog" : (_status, ["--error", String("text")]), 382 "zenity" : (_status, ["--error", StringKeyword("--text", "text")]), 383 "Xdialog" : (_status, ["--stdout", "--msgbox", String("text"), Integer("height"), Integer("width")]), 384 } 385 386 class Menu(Simple): 387 388 """ 389 A menu of options, one of which being selectable. 390 Options: text, width (in characters), height (in characters), 391 list_height (in items), items (MenuItem objects) 392 Response: a value corresponding to the chosen item 393 """ 394 395 name = "menu" 396 info = { 397 "kdialog" : (_readvalue(_readfrom), ["--menu", String("text"), MenuItemList("items")]), 398 "zenity" : (_readvalue(_readfrom), ["--list", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 399 MenuItemList("items")] 400 ), 401 "Xdialog" : (_readvalue(_readfrom), ["--stdout", "--menubox", 402 String("text"), Integer("height"), Integer("width"), Integer("list_height"), MenuItemList("items")] 403 ), 404 } 405 item = MenuItem 406 number_of_titles = 2 407 408 def __init__(self, text, titles, items=None, width=None, height=None, list_height=None): 409 410 """ 411 Initialise a menu with the given heading 'text', column 'titles', and 412 optional 'items' (which may be added later), 'width' (in characters), 413 'height' (in characters) and 'list_height' (in items). 414 """ 415 416 Simple.__init__(self, text, width, height) 417 self.titles = ([""] * self.number_of_titles + titles)[-self.number_of_titles:] 418 self.items = items or [] 419 self.list_height = list_height 420 421 def add(self, *args, **kw): 422 423 """ 424 Add an item, passing the given arguments to the appropriate item class. 425 """ 426 427 self.items.append(self.item(*args, **kw)) 428 429 class RadioList(Menu): 430 431 """ 432 A list of radio buttons, one of which being selectable. 433 Options: text, width (in characters), height (in characters), 434 list_height (in items), items (MenuItem objects), titles 435 Response: a list of values corresponding to chosen items (since some 436 programs, eg. zenity, appear to support multiple default 437 selections) 438 """ 439 440 name = "radiolist" 441 info = { 442 "kdialog" : (_readvalues_kdialog(_readfrom), ["--radiolist", String("text"), ListItemList("items")]), 443 "zenity" : (_readvalues_zenity(_readfrom), 444 ["--list", "--radiolist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 445 ListItemList("items", 1)] 446 ), 447 "Xdialog" : (_readvalues_Xdialog(_readfrom), ["--stdout", "--radiolist", 448 String("text"), Integer("height"), Integer("width"), Integer("list_height"), ListItemList("items")] 449 ), 450 } 451 number_of_titles = 3 452 453 class CheckList(Menu): 454 455 """ 456 A list of checkboxes, many being selectable. 457 Options: text, width (in characters), height (in characters), 458 list_height (in items), items (MenuItem objects), titles 459 Response: a list of values corresponding to chosen items 460 """ 461 462 name = "checklist" 463 info = { 464 "kdialog" : (_readvalues_kdialog(_readfrom), ["--checklist", String("text"), ListItemList("items")]), 465 "zenity" : (_readvalues_zenity(_readfrom), 466 ["--list", "--checklist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 467 ListItemList("items", 1)] 468 ), 469 "Xdialog" : (_readvalues_Xdialog(_readfrom), ["--stdout", "--checklist", 470 String("text"), Integer("height"), Integer("width"), Integer("list_height"), ListItemList("items")] 471 ), 472 } 473 number_of_titles = 3 474 475 class Pulldown(Menu): 476 477 """ 478 A pull-down menu of options, one of which being selectable. 479 Options: text, width (in characters), height (in characters), 480 items (list of values) 481 Response: a value corresponding to the chosen item 482 """ 483 484 name = "pulldown" 485 info = { 486 "kdialog" : (_readvalue(_readfrom), ["--combobox", String("text"), Strings("items")]), 487 "zenity" : (_readvalue(_readfrom), 488 ["--list", "--radiolist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 489 StringPairs("items")] 490 ), 491 "Xdialog" : (_readvalue(_readfrom), 492 ["--stdout", "--combobox", String("text"), Integer("height"), Integer("width"), Strings("items")]), 493 } 494 item = unicode 495 number_of_titles = 2 496 497 class Input(Simple): 498 499 """ 500 An input dialogue, consisting of an input field. 501 Options: text, input, width (in characters), height (in characters) 502 Response: the text entered into the dialogue by the user 503 """ 504 505 name = "input" 506 info = { 507 "kdialog" : (_readinput(_readfrom), 508 ["--inputbox", String("text"), String("data")]), 509 "zenity" : (_readinput(_readfrom), 510 ["--entry", StringKeyword("--text", "text"), StringKeyword("--entry-text", "data")]), 511 "Xdialog" : (_readinput(_readfrom), 512 ["--stdout", "--inputbox", String("text"), Integer("height"), Integer("width"), String("data")]), 513 } 514 515 def __init__(self, text, data="", width=None, height=None): 516 Simple.__init__(self, text, width, height) 517 self.data = data 518 519 class Password(Input): 520 521 """ 522 A password dialogue, consisting of a password entry field. 523 Options: text, width (in characters), height (in characters) 524 Response: the text entered into the dialogue by the user 525 """ 526 527 name = "password" 528 info = { 529 "kdialog" : (_readinput(_readfrom), 530 ["--password", String("text")]), 531 "zenity" : (_readinput(_readfrom), 532 ["--entry", StringKeyword("--text", "text"), "--hide-text"]), 533 "Xdialog" : (_readinput(_readfrom), 534 ["--stdout", "--password", "--inputbox", String("text"), Integer("height"), Integer("width")]), 535 } 536 537 class TextFile(Simple): 538 539 """ 540 A text file input box. 541 Options: filename, text, width (in characters), height (in characters) 542 Response: any text returned by the dialogue program (typically an empty 543 string) 544 """ 545 546 name = "textfile" 547 info = { 548 "kdialog" : (_readfrom, ["--textbox", String("filename"), Integer("width", pixels=1), Integer("height", pixels=1)]), 549 "zenity" : (_readfrom, ["--text-info", StringKeyword("--filename", "filename"), IntegerKeyword("--width", "width", pixels=1), 550 IntegerKeyword("--height", "height", pixels=1)] 551 ), 552 "Xdialog" : (_readfrom, ["--stdout", "--textbox", String("filename"), Integer("height"), Integer("width")]), 553 } 554 555 def __init__(self, filename, text="", width=None, height=None): 556 Simple.__init__(self, text, width, height) 557 self.filename = filename 558 559 class FileSelector(Simple): 560 561 """ 562 A file selector dialogue. 563 Options: directory to start in 564 Response: a filename 565 """ 566 567 name = "fileselector" 568 info = { 569 "kdialog" : (_readvalue(_readfrom), ["--getopenfilename", String("directory")]), 570 "zenity" : (_readvalue(_readfrom), ["--file-selection", StringKeyword("--filename", "directory")]), 571 "Xdialog" : (_readvalue(_readfrom), ["--fselect", String("directory")]), 572 } 573 574 def __init__(self, directory, text="", width=None, height=None): 575 Simple.__init__(self, text, width, height) 576 self.directory = directory 577 578 class DirectorySelector(Simple): 579 580 """ 581 A directory selector dialogue. 582 Options: directory to start in 583 Response: a filename 584 """ 585 586 name = "directoryselector" 587 info = { 588 "kdialog" : (_readvalue(_readfrom), ["--getexistingdirectory", String("directory")]), 589 "zenity" : (_readvalue(_readfrom), ["--file-selection", "--directory", StringKeyword("--filename", "directory")]), 590 "Xdialog" : (_readvalue(_readfrom), ["--dselect", String("directory")]), 591 } 592 593 def __init__(self, directory, text="", width=None, height=None): 594 Simple.__init__(self, text, width, height) 595 self.directory = directory 596 597 class Calendar(Simple): 598 599 """ 600 A calendar dialogue. 601 Response: a tuple of the form (year, month, day number) 602 """ 603 604 name = "calendar" 605 info = { 606 "kdialog" : (_readdate_kdialog(_readfrom), ["--calendar", String("text")]), 607 "zenity" : (_readdate_zenity(_readfrom), ["--calendar", "--date-format", "%Y %m %d"]), 608 "Xdialog" : (_readdate_kdialog(_readfrom), ["--calendar", String("text")]), 609 } 610 611 # Available dialogues. 612 613 available = [Question, Warning, Message, Error, Menu, CheckList, RadioList, Input, Password, Pulldown, TextFile, Calendar, 614 FileSelector, DirectorySelector] 615 616 # Supported desktop environments. 617 618 supported = Dialogue.commands.keys() 619 620 # vim: tabstop=4 expandtab shiftwidth=4