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