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 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 "KDE4" : "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", "KDE4", "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