1 #!/usr/bin/env python 2 3 """ 4 Simple desktop dialogue box support for Python. 5 6 Copyright (C) 2007, 2009 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 50 class _wrapper: 51 def __init__(self, handler): 52 self.handler = handler 53 54 class _readvalue(_wrapper): 55 def __call__(self, cmd, shell): 56 return self.handler(cmd, shell).strip() 57 58 class _readinput(_wrapper): 59 def __call__(self, cmd, shell): 60 return self.handler(cmd, shell)[:-1] 61 62 class _readvalues_kdialog(_wrapper): 63 def __call__(self, cmd, shell): 64 result = self.handler(cmd, shell).strip().strip('"') 65 if result: 66 return result.split('" "') 67 else: 68 return [] 69 70 class _readvalues_zenity(_wrapper): 71 def __call__(self, cmd, shell): 72 result = self.handler(cmd, shell).strip() 73 if result: 74 return result.split("|") 75 else: 76 return [] 77 78 class _readvalues_Xdialog(_wrapper): 79 def __call__(self, cmd, shell): 80 result = self.handler(cmd, shell).strip() 81 if result: 82 return result.split("/") 83 else: 84 return [] 85 86 # Dialogue parameter classes. 87 88 class String: 89 90 "A generic parameter." 91 92 def __init__(self, name): 93 self.name = name 94 95 def convert(self, value, program): 96 return [value or ""] 97 98 class Strings(String): 99 100 "Multiple string parameters." 101 102 def convert(self, value, program): 103 return value or [] 104 105 class StringPairs(String): 106 107 "Multiple string parameters duplicated to make identifiers." 108 109 def convert(self, value, program): 110 l = [] 111 for v in value: 112 l.append(v) 113 l.append(v) 114 return l 115 116 class StringKeyword: 117 118 "A keyword parameter." 119 120 def __init__(self, keyword, name): 121 self.keyword = keyword 122 self.name = name 123 124 def convert(self, value, program): 125 return [self.keyword + "=" + (value or "")] 126 127 class StringKeywords: 128 129 "Multiple keyword parameters." 130 131 def __init__(self, keyword, name): 132 self.keyword = keyword 133 self.name = name 134 135 def convert(self, value, program): 136 l = [] 137 for v in value or []: 138 l.append(self.keyword + "=" + v) 139 return l 140 141 class Integer(String): 142 143 "An integer parameter." 144 145 defaults = { 146 "width" : 40, 147 "height" : 15, 148 "list_height" : 10 149 } 150 scale = 8 151 152 def __init__(self, name, pixels=0): 153 String.__init__(self, name) 154 if pixels: 155 self.factor = self.scale 156 else: 157 self.factor = 1 158 159 def convert(self, value, program): 160 if value is None: 161 value = self.defaults[self.name] 162 return [str(int(value) * self.factor)] 163 164 class IntegerKeyword(Integer): 165 166 "An integer keyword parameter." 167 168 def __init__(self, keyword, name, pixels=0): 169 Integer.__init__(self, name, pixels) 170 self.keyword = keyword 171 172 def convert(self, value, program): 173 if value is None: 174 value = self.defaults[self.name] 175 return [self.keyword + "=" + str(int(value) * self.factor)] 176 177 class Boolean(String): 178 179 "A boolean parameter." 180 181 values = { 182 "kdialog" : ["off", "on"], 183 "zenity" : ["FALSE", "TRUE"], 184 "Xdialog" : ["off", "on"] 185 } 186 187 def convert(self, value, program): 188 values = self.values[program] 189 if value: 190 return [values[1]] 191 else: 192 return [values[0]] 193 194 class MenuItemList(String): 195 196 "A menu item list parameter." 197 198 def convert(self, value, program): 199 l = [] 200 for v in value: 201 l.append(v.value) 202 l.append(v.text) 203 return l 204 205 class ListItemList(String): 206 207 "A radiolist/checklist item list parameter." 208 209 def __init__(self, name, status_first=0): 210 String.__init__(self, name) 211 self.status_first = status_first 212 213 def convert(self, value, program): 214 l = [] 215 for v in value: 216 boolean = Boolean(None) 217 status = boolean.convert(v.status, program) 218 if self.status_first: 219 l += status 220 l.append(v.value) 221 l.append(v.text) 222 if not self.status_first: 223 l += status 224 return l 225 226 # Dialogue argument values. 227 228 class MenuItem: 229 230 "A menu item which can also be used with radiolists and checklists." 231 232 def __init__(self, value, text, status=0): 233 self.value = value 234 self.text = text 235 self.status = status 236 237 # Dialogue classes. 238 239 class Dialogue: 240 241 commands = { 242 "KDE" : "kdialog", 243 "GNOME" : "zenity", 244 "XFCE" : "zenity", # NOTE: Based on observations with Xubuntu. 245 "X11" : "Xdialog" 246 } 247 248 def open(self, desktop=None): 249 250 """ 251 Open a dialogue box (dialog) using a program appropriate to the desktop 252 environment in use. 253 254 If the optional 'desktop' parameter is specified then attempt to use 255 that particular desktop environment's mechanisms to open the dialog 256 instead of guessing or detecting which environment is being used. 257 258 Suggested values for 'desktop' are "standard", "KDE", "GNOME", 259 "Mac OS X", "Windows". 260 261 The result of the dialogue interaction may be a string indicating user 262 input (for Input, Password, Menu, Pulldown), a list of strings 263 indicating selections of one or more items (for RadioList, CheckList), 264 or a value indicating true or false (for Question, Warning, Message, 265 Error). 266 267 Where a string value may be expected but no choice is made, an empty 268 string may be returned. Similarly, where a list of values is expected 269 but no choice is made, an empty list may be returned. 270 """ 271 272 # Decide on the desktop environment in use. 273 274 desktop_in_use = use_desktop(desktop) 275 276 # Get the program. 277 278 try: 279 program = self.commands[desktop_in_use] 280 except KeyError: 281 raise OSError, "Desktop '%s' not supported (no known dialogue box command could be suggested)" % desktop_in_use 282 283 # The handler is one of the functions communicating with the subprocess. 284 # Some handlers return boolean values, others strings. 285 286 handler, options = self.info[program] 287 288 cmd = [program] 289 for option in options: 290 if isinstance(option, str): 291 cmd.append(option) 292 else: 293 value = getattr(self, option.name, None) 294 cmd += option.convert(value, program) 295 296 return handler(cmd, 0) 297 298 class Simple(Dialogue): 299 def __init__(self, text, width=None, height=None): 300 self.text = text 301 self.width = width 302 self.height = height 303 304 class Question(Simple): 305 306 """ 307 A dialogue asking a question and showing response buttons. 308 Options: text, width (in characters), height (in characters) 309 Response: a boolean value indicating an affirmative response (true) or a 310 negative response 311 """ 312 313 name = "question" 314 info = { 315 "kdialog" : (_status, ["--yesno", String("text")]), 316 "zenity" : (_status, ["--question", StringKeyword("--text", "text")]), 317 "Xdialog" : (_status, ["--stdout", "--yesno", String("text"), Integer("height"), Integer("width")]), 318 } 319 320 class Warning(Simple): 321 322 """ 323 A dialogue asking a question and showing response buttons. 324 Options: text, width (in characters), height (in characters) 325 Response: a boolean value indicating an affirmative response (true) or a 326 negative response 327 """ 328 329 name = "warning" 330 info = { 331 "kdialog" : (_status, ["--warningyesno", String("text")]), 332 "zenity" : (_status, ["--warning", StringKeyword("--text", "text")]), 333 "Xdialog" : (_status, ["--stdout", "--yesno", String("text"), Integer("height"), Integer("width")]), 334 } 335 336 class Message(Simple): 337 338 """ 339 A message dialogue. 340 Options: text, width (in characters), height (in characters) 341 Response: a boolean value indicating an affirmative response (true) or a 342 negative response 343 """ 344 345 name = "message" 346 info = { 347 "kdialog" : (_status, ["--msgbox", String("text")]), 348 "zenity" : (_status, ["--info", StringKeyword("--text", "text")]), 349 "Xdialog" : (_status, ["--stdout", "--msgbox", String("text"), Integer("height"), Integer("width")]), 350 } 351 352 class Error(Simple): 353 354 """ 355 An error dialogue. 356 Options: text, width (in characters), height (in characters) 357 Response: a boolean value indicating an affirmative response (true) or a 358 negative response 359 """ 360 361 name = "error" 362 info = { 363 "kdialog" : (_status, ["--error", String("text")]), 364 "zenity" : (_status, ["--error", StringKeyword("--text", "text")]), 365 "Xdialog" : (_status, ["--stdout", "--msgbox", String("text"), Integer("height"), Integer("width")]), 366 } 367 368 class Menu(Simple): 369 370 """ 371 A menu of options, one of which being selectable. 372 Options: text, width (in characters), height (in characters), 373 list_height (in items), items (MenuItem objects) 374 Response: a value corresponding to the chosen item 375 """ 376 377 name = "menu" 378 info = { 379 "kdialog" : (_readvalue(_readfrom), ["--menu", String("text"), MenuItemList("items")]), 380 "zenity" : (_readvalue(_readfrom), ["--list", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 381 MenuItemList("items")] 382 ), 383 "Xdialog" : (_readvalue(_readfrom), ["--stdout", "--menubox", 384 String("text"), Integer("height"), Integer("width"), Integer("list_height"), MenuItemList("items")] 385 ), 386 } 387 item = MenuItem 388 number_of_titles = 2 389 390 def __init__(self, text, titles, items=None, width=None, height=None, list_height=None): 391 392 """ 393 Initialise a menu with the given heading 'text', column 'titles', and 394 optional 'items' (which may be added later), 'width' (in characters), 395 'height' (in characters) and 'list_height' (in items). 396 """ 397 398 Simple.__init__(self, text, width, height) 399 self.titles = ([""] * self.number_of_titles + titles)[-self.number_of_titles:] 400 self.items = items or [] 401 self.list_height = list_height 402 403 def add(self, *args, **kw): 404 405 """ 406 Add an item, passing the given arguments to the appropriate item class. 407 """ 408 409 self.items.append(self.item(*args, **kw)) 410 411 class RadioList(Menu): 412 413 """ 414 A list of radio buttons, one of which being selectable. 415 Options: text, width (in characters), height (in characters), 416 list_height (in items), items (MenuItem objects), titles 417 Response: a list of values corresponding to chosen items (since some 418 programs, eg. zenity, appear to support multiple default 419 selections) 420 """ 421 422 name = "radiolist" 423 info = { 424 "kdialog" : (_readvalues_kdialog(_readfrom), ["--radiolist", String("text"), ListItemList("items")]), 425 "zenity" : (_readvalues_zenity(_readfrom), 426 ["--list", "--radiolist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 427 ListItemList("items", 1)] 428 ), 429 "Xdialog" : (_readvalues_Xdialog(_readfrom), ["--stdout", "--radiolist", 430 String("text"), Integer("height"), Integer("width"), Integer("list_height"), ListItemList("items")] 431 ), 432 } 433 number_of_titles = 3 434 435 class CheckList(Menu): 436 437 """ 438 A list of checkboxes, many being selectable. 439 Options: text, width (in characters), height (in characters), 440 list_height (in items), items (MenuItem objects), titles 441 Response: a list of values corresponding to chosen items 442 """ 443 444 name = "checklist" 445 info = { 446 "kdialog" : (_readvalues_kdialog(_readfrom), ["--checklist", String("text"), ListItemList("items")]), 447 "zenity" : (_readvalues_zenity(_readfrom), 448 ["--list", "--checklist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 449 ListItemList("items", 1)] 450 ), 451 "Xdialog" : (_readvalues_Xdialog(_readfrom), ["--stdout", "--checklist", 452 String("text"), Integer("height"), Integer("width"), Integer("list_height"), ListItemList("items")] 453 ), 454 } 455 number_of_titles = 3 456 457 class Pulldown(Menu): 458 459 """ 460 A pull-down menu of options, one of which being selectable. 461 Options: text, width (in characters), height (in characters), 462 items (list of values) 463 Response: a value corresponding to the chosen item 464 """ 465 466 name = "pulldown" 467 info = { 468 "kdialog" : (_readvalue(_readfrom), ["--combobox", String("text"), Strings("items")]), 469 "zenity" : (_readvalue(_readfrom), 470 ["--list", "--radiolist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 471 StringPairs("items")] 472 ), 473 "Xdialog" : (_readvalue(_readfrom), 474 ["--stdout", "--combobox", String("text"), Integer("height"), Integer("width"), Strings("items")]), 475 } 476 item = unicode 477 number_of_titles = 2 478 479 class Input(Simple): 480 481 """ 482 An input dialogue, consisting of an input field. 483 Options: text, input, width (in characters), height (in characters) 484 Response: the text entered into the dialogue by the user 485 """ 486 487 name = "input" 488 info = { 489 "kdialog" : (_readinput(_readfrom), 490 ["--inputbox", String("text"), String("data")]), 491 "zenity" : (_readinput(_readfrom), 492 ["--entry", StringKeyword("--text", "text"), StringKeyword("--entry-text", "data")]), 493 "Xdialog" : (_readinput(_readfrom), 494 ["--stdout", "--inputbox", String("text"), Integer("height"), Integer("width"), String("data")]), 495 } 496 497 def __init__(self, text, data="", width=None, height=None): 498 Simple.__init__(self, text, width, height) 499 self.data = data 500 501 class Password(Input): 502 503 """ 504 A password dialogue, consisting of a password entry field. 505 Options: text, width (in characters), height (in characters) 506 Response: the text entered into the dialogue by the user 507 """ 508 509 name = "password" 510 info = { 511 "kdialog" : (_readinput(_readfrom), 512 ["--password", String("text")]), 513 "zenity" : (_readinput(_readfrom), 514 ["--entry", StringKeyword("--text", "text"), "--hide-text"]), 515 "Xdialog" : (_readinput(_readfrom), 516 ["--stdout", "--password", "--inputbox", String("text"), Integer("height"), Integer("width")]), 517 } 518 519 class TextFile(Simple): 520 521 """ 522 A text file input box. 523 Options: filename, text, width (in characters), height (in characters) 524 Response: any text returned by the dialogue program (typically an empty 525 string) 526 """ 527 528 name = "textfile" 529 info = { 530 "kdialog" : (_readfrom, ["--textbox", String("filename"), Integer("width", pixels=1), Integer("height", pixels=1)]), 531 "zenity" : (_readfrom, ["--text-info", StringKeyword("--filename", "filename"), IntegerKeyword("--width", "width", pixels=1), 532 IntegerKeyword("--height", "height", pixels=1)] 533 ), 534 "Xdialog" : (_readfrom, ["--stdout", "--textbox", String("filename"), Integer("height"), Integer("width")]), 535 } 536 537 def __init__(self, filename, text="", width=None, height=None): 538 Simple.__init__(self, text, width, height) 539 self.filename = filename 540 541 # Available dialogues. 542 543 available = [Question, Warning, Message, Error, Menu, CheckList, RadioList, Input, Password, Pulldown, TextFile] 544 545 # Supported desktop environments. 546 547 supported = Dialogue.commands.keys() 548 549 # vim: tabstop=4 expandtab shiftwidth=4