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 269 # Decide on the desktop environment in use. 270 271 desktop_in_use = use_desktop(desktop) 272 273 # Get the program. 274 275 try: 276 program = self.commands[desktop_in_use] 277 except KeyError: 278 raise OSError, "Desktop '%s' not supported (no known dialogue box command could be suggested)" % desktop_in_use 279 280 # The handler is one of the functions communicating with the subprocess. 281 # Some handlers return boolean values, others strings. 282 283 handler, options = self.info[program] 284 285 cmd = [program] 286 for option in options: 287 if isinstance(option, str): 288 cmd.append(option) 289 else: 290 value = getattr(self, option.name, None) 291 cmd += option.convert(value, program) 292 293 return handler(cmd, 0) 294 295 class Simple(Dialogue): 296 def __init__(self, text, width=None, height=None): 297 self.text = text 298 self.width = width 299 self.height = height 300 301 class Question(Simple): 302 303 """ 304 A dialogue asking a question and showing response buttons. 305 Options: text, width (in characters), height (in characters) 306 Response: a boolean value indicating an affirmative response (true) or a 307 negative response 308 """ 309 310 name = "question" 311 info = { 312 "kdialog" : (_status, ["--yesno", String("text")]), 313 "zenity" : (_status, ["--question", StringKeyword("--text", "text")]), 314 "Xdialog" : (_status, ["--stdout", "--yesno", String("text"), Integer("height"), Integer("width")]), 315 } 316 317 class Warning(Simple): 318 319 """ 320 A dialogue asking a question and showing response buttons. 321 Options: text, width (in characters), height (in characters) 322 Response: a boolean value indicating an affirmative response (true) or a 323 negative response 324 """ 325 326 name = "warning" 327 info = { 328 "kdialog" : (_status, ["--warningyesno", String("text")]), 329 "zenity" : (_status, ["--warning", StringKeyword("--text", "text")]), 330 "Xdialog" : (_status, ["--stdout", "--yesno", String("text"), Integer("height"), Integer("width")]), 331 } 332 333 class Message(Simple): 334 335 """ 336 A message dialogue. 337 Options: text, width (in characters), height (in characters) 338 Response: a boolean value indicating an affirmative response (true) or a 339 negative response 340 """ 341 342 name = "message" 343 info = { 344 "kdialog" : (_status, ["--msgbox", String("text")]), 345 "zenity" : (_status, ["--info", StringKeyword("--text", "text")]), 346 "Xdialog" : (_status, ["--stdout", "--msgbox", String("text"), Integer("height"), Integer("width")]), 347 } 348 349 class Error(Simple): 350 351 """ 352 An error dialogue. 353 Options: text, width (in characters), height (in characters) 354 Response: a boolean value indicating an affirmative response (true) or a 355 negative response 356 """ 357 358 name = "error" 359 info = { 360 "kdialog" : (_status, ["--error", String("text")]), 361 "zenity" : (_status, ["--error", StringKeyword("--text", "text")]), 362 "Xdialog" : (_status, ["--stdout", "--msgbox", String("text"), Integer("height"), Integer("width")]), 363 } 364 365 class Menu(Simple): 366 367 """ 368 A menu of options, one of which being selectable. 369 Options: text, width (in characters), height (in characters), 370 list_height (in items), items (MenuItem objects) 371 Response: a value corresponding to the chosen item 372 """ 373 374 name = "menu" 375 info = { 376 "kdialog" : (_readvalue(_readfrom), ["--menu", String("text"), MenuItemList("items")]), 377 "zenity" : (_readvalue(_readfrom), ["--list", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 378 MenuItemList("items")] 379 ), 380 "Xdialog" : (_readvalue(_readfrom), ["--stdout", "--menubox", 381 String("text"), Integer("height"), Integer("width"), Integer("list_height"), MenuItemList("items")] 382 ), 383 } 384 item = MenuItem 385 number_of_titles = 2 386 387 def __init__(self, text, titles, items=None, width=None, height=None, list_height=None): 388 389 """ 390 Initialise a menu with the given heading 'text', column 'titles', and 391 optional 'items' (which may be added later), 'width' (in characters), 392 'height' (in characters) and 'list_height' (in items). 393 """ 394 395 Simple.__init__(self, text, width, height) 396 self.titles = ([""] * self.number_of_titles + titles)[-self.number_of_titles:] 397 self.items = items or [] 398 self.list_height = list_height 399 400 def add(self, *args, **kw): 401 402 """ 403 Add an item, passing the given arguments to the appropriate item class. 404 """ 405 406 self.items.append(self.item(*args, **kw)) 407 408 class RadioList(Menu): 409 410 """ 411 A list of radio buttons, one of which being selectable. 412 Options: text, width (in characters), height (in characters), 413 list_height (in items), items (MenuItem objects), titles 414 Response: a list of values corresponding to chosen items (since some 415 programs, eg. zenity, appear to support multiple default 416 selections) 417 """ 418 419 name = "radiolist" 420 info = { 421 "kdialog" : (_readvalues_kdialog(_readfrom), ["--radiolist", String("text"), ListItemList("items")]), 422 "zenity" : (_readvalues_zenity(_readfrom), 423 ["--list", "--radiolist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 424 ListItemList("items", 1)] 425 ), 426 "Xdialog" : (_readvalues_Xdialog(_readfrom), ["--stdout", "--radiolist", 427 String("text"), Integer("height"), Integer("width"), Integer("list_height"), ListItemList("items")] 428 ), 429 } 430 number_of_titles = 3 431 432 class CheckList(Menu): 433 434 """ 435 A list of checkboxes, many being selectable. 436 Options: text, width (in characters), height (in characters), 437 list_height (in items), items (MenuItem objects), titles 438 Response: a list of values corresponding to chosen items 439 """ 440 441 name = "checklist" 442 info = { 443 "kdialog" : (_readvalues_kdialog(_readfrom), ["--checklist", String("text"), ListItemList("items")]), 444 "zenity" : (_readvalues_zenity(_readfrom), 445 ["--list", "--checklist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 446 ListItemList("items", 1)] 447 ), 448 "Xdialog" : (_readvalues_Xdialog(_readfrom), ["--stdout", "--checklist", 449 String("text"), Integer("height"), Integer("width"), Integer("list_height"), ListItemList("items")] 450 ), 451 } 452 number_of_titles = 3 453 454 class Pulldown(Menu): 455 456 """ 457 A pull-down menu of options, one of which being selectable. 458 Options: text, width (in characters), height (in characters), 459 items (list of values) 460 Response: a value corresponding to the chosen item 461 """ 462 463 name = "pulldown" 464 info = { 465 "kdialog" : (_readvalue(_readfrom), ["--combobox", String("text"), Strings("items")]), 466 "zenity" : (_readvalue(_readfrom), 467 ["--list", "--radiolist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), 468 StringPairs("items")] 469 ), 470 "Xdialog" : (_readvalue(_readfrom), 471 ["--stdout", "--combobox", String("text"), Integer("height"), Integer("width"), Strings("items")]), 472 } 473 item = unicode 474 number_of_titles = 2 475 476 class Input(Simple): 477 478 """ 479 An input dialogue, consisting of an input field. 480 Options: text, input, width (in characters), height (in characters) 481 Response: the text entered into the dialogue by the user 482 """ 483 484 name = "input" 485 info = { 486 "kdialog" : (_readinput(_readfrom), 487 ["--inputbox", String("text"), String("data")]), 488 "zenity" : (_readinput(_readfrom), 489 ["--entry", StringKeyword("--text", "text"), StringKeyword("--entry-text", "data")]), 490 "Xdialog" : (_readinput(_readfrom), 491 ["--stdout", "--inputbox", String("text"), Integer("height"), Integer("width"), String("data")]), 492 } 493 494 def __init__(self, text, data="", width=None, height=None): 495 Simple.__init__(self, text, width, height) 496 self.data = data 497 498 class Password(Input): 499 500 """ 501 A password dialogue, consisting of a password entry field. 502 Options: text, width (in characters), height (in characters) 503 Response: the text entered into the dialogue by the user 504 """ 505 506 name = "password" 507 info = { 508 "kdialog" : (_readinput(_readfrom), 509 ["--password", String("text")]), 510 "zenity" : (_readinput(_readfrom), 511 ["--entry", StringKeyword("--text", "text"), "--hide-text"]), 512 "Xdialog" : (_readinput(_readfrom), 513 ["--stdout", "--password", "--inputbox", String("text"), Integer("height"), Integer("width")]), 514 } 515 516 class TextFile(Simple): 517 518 """ 519 A text file input box. 520 Options: filename, text, width (in characters), height (in characters) 521 Response: any text returned by the dialogue program 522 """ 523 524 name = "textfile" 525 info = { 526 "kdialog" : (_readfrom, ["--textbox", String("filename"), Integer("width", pixels=1), Integer("height", pixels=1)]), 527 "zenity" : (_readfrom, ["--text-info", StringKeyword("--filename", "filename"), IntegerKeyword("--width", "width", pixels=1), 528 IntegerKeyword("--height", "height", pixels=1)] 529 ), 530 "Xdialog" : (_readfrom, ["--stdout", "--textbox", String("filename"), Integer("height"), Integer("width")]), 531 } 532 533 def __init__(self, filename, text="", width=None, height=None): 534 Simple.__init__(self, text, width, height) 535 self.filename = filename 536 537 # Available dialogues. 538 539 available = [Question, Warning, Message, Error, Menu, CheckList, RadioList, Input, Password, Pulldown, TextFile] 540 541 # Supported desktop environments. 542 543 supported = Dialogue.commands.keys() 544 545 # vim: tabstop=4 expandtab shiftwidth=4