Package lib :: Module scripting
[hide private]
[frames] | no frames]

Source Code for Module lib.scripting

  1  # -*- coding: utf-8 -*- 
  2   
  3  # Copyright (C) 2009 Chris Dekter 
  4   
  5  # This program is free software; you can redistribute it and/or modify 
  6  # it under the terms of the GNU General Public License as published by 
  7  # the Free Software Foundation; either version 2 of the License, or 
  8  # (at your option) any later version. 
  9  # 
 10  # This program is distributed in the hope that it will be useful, but 
 11  # WITHOUT ANY WARRANTY; without even the implied warranty of 
 12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 13  # General Public License for more details. 
 14  # 
 15  # You should have received a copy of the GNU General Public License 
 16  # along with this program; if not, write to the Free Software 
 17  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
 18   
 19  import subprocess, threading, time, re 
 20  #from PyQt4.QtGui import QClipboard, QApplication 
 21  #import model 
 22   
23 -class Keyboard:
24 """ 25 Provides access to the keyboard for event generation. 26 """ 27
28 - def __init__(self, mediator):
29 self.mediator = mediator
30
31 - def send_keys(self, keyString):
32 """ 33 Send a sequence of keys via keyboard events 34 35 Usage: C{keyboard.send_keys(keyString)} 36 37 @param keyString: string of keys (including special keys) to send 38 """ 39 self.mediator.send_string(keyString.decode("utf-8")) 40 self.mediator.flush()
41
42 - def send_key(self, key, repeat=1):
43 """ 44 Send a keyboard event 45 46 Usage: C{keyboard.send_key(key, repeat=1)} 47 48 @param key: they key to be sent (e.g. "s" or "<enter>") 49 @param repeat: number of times to repeat the key event 50 """ 51 for x in range(repeat): 52 self.mediator.send_key(key.decode("utf-8")) 53 self.mediator.flush()
54 55
56 -class Mouse:
57 """ 58 Provides access to send mouse clicks 59 """
60 - def __init__(self, mediator):
61 self.mediator = mediator
62
63 - def click_relative(self, x, y, button):
64 """ 65 Send a mouse click relative to the active window 66 67 Usage: C{mouse.click_relative(x, y, button)} 68 69 @param x: x-coordinate in pixels, relative to upper left corner of window 70 @param y: y-coordinate in pixels, relative to upper left corner of window 71 @param button: mouse button to simulate (left=1, middle=2, right=3) 72 """ 73 self.mediator.send_mouse_click(x, y, button, True)
74
75 - def click_absolute(self, x, y, button):
76 """ 77 Send a mouse click relative to the screen (absolute) 78 79 Usage: C{mouse.click_absolute(x, y, button)} 80 81 @param x: x-coordinate in pixels, relative to upper left corner of window 82 @param y: y-coordinate in pixels, relative to upper left corner of window 83 @param button: mouse button to simulate (left=1, middle=2, right=3) 84 """ 85 self.mediator.send_mouse_click(x, y, button, False)
86 87
88 -class Store(dict):
89 """ 90 Allows persistent storage of values between invocations of the script. 91 """ 92
93 - def set_value(self, key, value):
94 """ 95 Store a value 96 97 Usage: C{store.set_value(key, value)} 98 """ 99 self[key] = value
100
101 - def get_value(self, key):
102 """ 103 Get a value 104 105 Usage: C{store.get_value(key)} 106 """ 107 return self[key]
108
109 - def remove_value(self, key):
110 """ 111 Remove a value 112 113 Usage: C{store.remove_value(key)} 114 """ 115 del self[key]
116
117 -class Dialog:
118 """ 119 Provides a simple interface for the display of some basic dialogs to collect information from the user. 120 """ 121
122 - def __runKdialog(self, title, args):
123 p = subprocess.Popen(["kdialog", "--title", title] + args, stdout=subprocess.PIPE) 124 retCode = p.wait() 125 output = p.stdout.read()[:-1] # Drop trailing newline 126 127 return (retCode, output)
128
129 - def input_dialog(self, title="Enter a value", message="Enter a value", default=""):
130 """ 131 Show an input dialog 132 133 Usage: C{dialog.input_dialog(title="Enter a value", message="Enter a value", default="")} 134 135 @param title: window title for the dialog 136 @param message: message displayed above the input box 137 @param default: default value for the input box 138 """ 139 return self.__runKdialog(title, ["--inputbox", message, default])
140
141 - def password_dialog(self, title="Enter password", message="Enter password"):
142 """ 143 Show a password input dialog 144 145 Usage: C{dialog.password_dialog(title="Enter password", message="Enter password")} 146 147 @param title: window title for the dialog 148 @param message: message displayed above the password input box 149 """ 150 return self.__runKdialog(title, ["--password", message])
151
152 - def combo_menu(self, options, title="Choose an option", message="Choose an option"):
153 """ 154 Show a combobox menu 155 156 Usage: C{dialog.combo_menu(options, title="Choose an option", message="Choose an option")} 157 158 @param options: list of options (strings) for the dialog 159 @param title: window title for the dialog 160 @param message: message displayed above the combobox 161 """ 162 return self.__runKdialog(title, ["--combobox", message] + options)
163
164 - def list_menu(self, options, title="Choose a value", message="Choose a value", default=None):
165 """ 166 Show a single-selection list menu 167 168 Usage: C{dialog.list_menu(options, title="Choose a value", message="Choose a value", default=None)} 169 170 @param options: list of options (strings) for the dialog 171 @param title: window title for the dialog 172 @param message: message displayed above the list 173 @param default: default value to be selected 174 """ 175 176 choices = [] 177 optionNum = 0 178 for option in options: 179 choices.append(str(optionNum)) 180 choices.append(option) 181 if option == default: 182 choices.append("on") 183 else: 184 choices.append("off") 185 optionNum += 1 186 187 retCode, result = self.__runKdialog(title, ["--radiolist", message] + choices) 188 choice = options[int(result)] 189 190 return retCode, choice
191
192 - def list_menu_multi(self, options, title="Choose one or more values", message="Choose one or more values", defaults=[]):
193 """ 194 Show a multiple-selection list menu 195 196 Usage: C{dialog.list_menu_multi(options, title="Choose one or more values", message="Choose one or more values", defaults=[])} 197 198 @param options: list of options (strings) for the dialog 199 @param title: window title for the dialog 200 @param message: message displayed above the list 201 @param defaults: list of default values to be selected 202 """ 203 204 choices = [] 205 optionNum = 0 206 for option in options: 207 choices.append(str(optionNum)) 208 choices.append(option) 209 if option in defaults: 210 choices.append("on") 211 else: 212 choices.append("off") 213 optionNum += 1 214 215 retCode, output = self.__runKdialog(title, ["--separate-output", "--checklist", message] + choices) 216 results = output.split() 217 218 choices = [] 219 for index in results: 220 choices.append(options[int(index)]) 221 222 return retCode, choices
223
224 - def open_file(self, title="Open File", initialDir="~", fileTypes="*|All Files", rememberAs=None):
225 """ 226 Show an Open File dialog 227 228 Usage: C{dialog.open_file(title="Open File", initialDir="~", fileTypes="*|All Files", rememberAs=None)} 229 230 @param title: window title for the dialog 231 @param initialDir: starting directory for the file dialog 232 @param fileTypes: file type filter expression 233 @param rememberAs: gives an ID to this file dialog, allowing it to open at the last used path next time 234 """ 235 if rememberAs is not None: 236 return self.__runKdialog(title, ["--getopenfilename", initialDir, fileTypes, ":" + rememberAs]) 237 else: 238 return self.__runKdialog(title, ["--getopenfilename", initialDir, fileTypes])
239
240 - def save_file(self, title="Save As", initialDir="~", fileTypes="*|All Files", rememberAs=None):
241 """ 242 Show a Save As dialog 243 244 Usage: C{dialog.save_file(title="Save As", initialDir="~", fileTypes="*|All Files", rememberAs=None)} 245 246 @param title: window title for the dialog 247 @param initialDir: starting directory for the file dialog 248 @param fileTypes: file type filter expression 249 @param rememberAs: gives an ID to this file dialog, allowing it to open at the last used path next time 250 """ 251 if rememberAs is not None: 252 return self.__runKdialog(title, ["--getsavefilename", initialDir, fileTypes, ":" + rememberAs]) 253 else: 254 return self.__runKdialog(title, ["--getsavefilename", initialDir, fileTypes])
255
256 - def choose_directory(self, title="Select Directory", initialDir="~", rememberAs=None):
257 """ 258 Show a Directory Chooser dialog 259 260 Usage: C{dialog.choose_directory(title="Select Directory", initialDir="~", rememberAs=None)} 261 262 @param title: window title for the dialog 263 @param initialDir: starting directory for the directory chooser dialog 264 @param rememberAs: gives an ID to this file dialog, allowing it to open at the last used path next time 265 """ 266 if rememberAs is not None: 267 return self.__runKdialog(title, ["--getexistingdirectory", initialDir, ":" + rememberAs]) 268 else: 269 return self.__runKdialog(title, ["--getexistingdirectory", initialDir])
270
271 - def choose_colour(self, title="Select Colour"):
272 """ 273 Show a Colour Chooser dialog 274 275 Usage: C{dialog.choose_colour(title="Select Colour")} 276 277 @param title: window title for the dialog 278 """ 279 return self.__runKdialog(title, ["--getcolor"])
280 281
282 -class System:
283 """ 284 Simplified access to some system commands. 285 """ 286
287 - def exec_command(self, command):
288 """ 289 Execute a shell command 290 291 Usage: C{system.exec_command(command)} 292 293 @param command: command to be executed (including any arguments) - e.g. "ls -l" 294 @raises subprocess.CalledProcessError: if the command returns a non-zero exit code 295 """ 296 p = subprocess.Popen(command, shell=True, bufsize=-1, stdout=subprocess.PIPE) 297 retCode = p.wait() 298 output = p.stdout.read()[:-1] 299 if retCode != 0: 300 raise subprocess.CalledProcessError(retCode, output) 301 else: 302 return output
303
304 - def create_file(self, fileName, contents=""):
305 """ 306 Create a file with contents 307 308 Usage: C{system.create_file(fileName, contents="")} 309 310 @param fileName: full path to the file to be created 311 @param contents: contents to insert into the file 312 """ 313 f = open(fileName, "w") 314 f.write(contents) 315 f.close()
316 317
318 -class Clipboard:
319 """ 320 Read/write access to the X selection and clipboard 321 """ 322
323 - def __init__(self, app):
324 self.clipBoard = QApplication.clipboard() 325 self.app = app
326
327 - def fill_selection(self, contents):
328 """ 329 Copy text into the X selection 330 331 Usage: C{clipboard.fill_selection(contents)} 332 333 @param contents: string to be placed in the selection 334 """ 335 self.__execAsync(self.__fillSelection, contents)
336
337 - def __fillSelection(self, string):
338 self.clipBoard.setText(string, QClipboard.Selection) 339 self.sem.release()
340
341 - def get_selection(self):
342 """ 343 Read text from the X selection 344 345 Usage: C{clipboard.get_selection()} 346 """ 347 self.__execAsync(self.__getSelection) 348 return str(self.text)
349
350 - def __getSelection(self):
351 self.text = self.clipBoard.text(QClipboard.Selection) 352 self.sem.release()
353
354 - def fill_clipboard(self, contents):
355 """ 356 Copy text into the clipboard 357 358 Usage: C{clipboard.fill_clipboard(contents)} 359 360 @param contents: string to be placed in the selection 361 """ 362 self.__execAsync(self.__fillClipboard, contents)
363
364 - def __fillClipboard(self, string):
365 self.clipBoard.setText(string, QClipboard.Clipboard) 366 self.sem.release()
367
368 - def get_clipboard(self):
369 """ 370 Read text from the clipboard 371 372 Usage: C{clipboard.get_clipboard()} 373 """ 374 self.__execAsync(self.__getClipboard) 375 return str(self.text)
376
377 - def __getClipboard(self):
378 self.text = self.clipBoard.text(QClipboard.Clipboard) 379 self.sem.release()
380
381 - def __execAsync(self, callback, *args):
382 self.sem = threading.Semaphore(0) 383 self.app.exec_in_main(callback, *args) 384 self.sem.acquire()
385 386
387 -class Window:
388 """ 389 Basic window management using wmctrl 390 391 Note: in all cases where a window title is required (with the exception of wait_for_focus()), 392 two special values of window title are permitted: 393 394 :ACTIVE: - select the currently active window 395 :SELECT: - select the desired window by clicking on it 396 """ 397
398 - def __init__(self, mediator):
399 self.mediator = mediator
400
401 - def wait_for_focus(self, title, timeOut=5):
402 """ 403 Wait for window with the given title to have focus 404 405 Usage: C{window.wait_for_focus(title, timeOut=5)} 406 407 If the window becomes active, returns True. Otherwise, returns False if 408 the window has not become active by the time the timeout has elapsed. 409 410 @param title: title to match against (as a regular expression) 411 @param timeOut: period (seconds) to wait before giving up 412 """ 413 regex = re.compile(title) 414 waited = 0 415 while waited < timeOut: 416 if regex.match(self.mediator.interface.get_window_title()): 417 return True 418 time.sleep(0.3) 419 waited += 0.3 420 421 return False
422
423 - def wait_for_exist(self, title, timeOut=5):
424 """ 425 Wait for window with the given title to be created 426 427 Usage: C{window.wait_for_exist(title, timeOut=5)} 428 429 If the window is in existence, returns True. Otherwise, returns False if 430 the window has not been created by the time the timeout has elapsed. 431 432 @param title: title to match against (as a regular expression) 433 @param timeOut: period (seconds) to wait before giving up 434 """ 435 regex = re.compile(title) 436 waited = 0 437 while waited < timeOut: 438 retCode, output = self.__runWmctrl(["-l"]) 439 for line in output.split('\n'): 440 if regex.match(line[14:].split(' ', 1)[-1]): 441 return True 442 443 time.sleep(0.3) 444 waited += 0.3 445 446 return False
447
448 - def activate(self, title, switchDesktop=False):
449 """ 450 Activate the specified window, giving it input focus 451 452 Usage: C{window.activate(title, switchDesktop=False)} 453 454 If switchDesktop is False (default), the window will be moved to the current desktop 455 and activated. Otherwise, switch to the window's current desktop and activate it there. 456 457 @param title: window title to match against (as case-insensitive substring match) 458 @param switchDesktop: whether or not to switch to the window's current desktop 459 """ 460 if switchDesktop: 461 args = ["-a", title] 462 else: 463 args = ["-R", title] 464 self.__runWmctrl(args)
465
466 - def close(self, title):
467 """ 468 Close the specified window gracefully 469 470 Usage: C{window.close(title)} 471 472 @param title: window title to match against (as case-insensitive substring match) 473 """ 474 self.__runWmctrl(["-c", title])
475
476 - def resize_move(self, title, xOrigin=-1, yOrigin=-1, width=-1, height=-1):
477 """ 478 Resize and/or move the specified window 479 480 Usage: C{window.close(title, xOrigin=-1, yOrigin=-1, width=-1, height=-1)} 481 482 Leaving and of the position/dimension values as the default (-1) will cause that 483 value to be left unmodified. 484 485 @param title: window title to match against (as case-insensitive substring match) 486 @param xOrigin: new x origin of the window (upper left corner) 487 @param yOrigin: new y origin of the window (upper left corner) 488 @param width: new width of the window 489 @param height: new height of the window 490 """ 491 mvArgs = ["0", str(xOrigin), str(yOrigin), str(width), str(height)] 492 self.__runWmctrl(["-r", title, "-e", ','.join(mvArgs)])
493 494
495 - def move_to_desktop(self, title, deskNum):
496 """ 497 Move the specified window to the given desktop 498 499 Usage: C{window.move_to_desktop(title, deskNum)} 500 501 @param title: window title to match against (as case-insensitive substring match) 502 @param deskNum: desktop to move the window to (note: zero based) 503 """ 504 self.__runWmctrl(["-r", title, "-t", str(deskNum)])
505 506
507 - def switch_desktop(self, deskNum):
508 """ 509 Switch to the specified desktop 510 511 Usage: C{window.switch_desktop(deskNum)} 512 513 @param deskNum: desktop to switch to (note: zero based) 514 """ 515 self.__runWmctrl(["-s", str(deskNum)])
516
517 - def set_property(self, title, action, prop):
518 """ 519 Set a property on the given window using the specified action 520 521 Usage: C{window.set_property(title, title, action, prop)} 522 523 Allowable actions: C{add, remove, toggle} 524 Allowable properties: C{modal, sticky, maximized_vert, maximized_horz, shaded, skip_taskbar, 525 skip_pager, hidden, fullscreen, above} 526 527 @param title: window title to match against (as case-insensitive substring match) 528 @param action: one of the actions listed above 529 @param prop: one of the properties listed above 530 """ 531 self.__runWmctrl(["-r", title, "-b" + action + ',' + prop])
532
533 - def get_active_geometry(self):
534 """ 535 Get the geometry of the currently active window 536 537 Usage: C{window.get_active_geometry()} 538 539 Returns a 4-tuple containing the x-origin, y-origin, width and height of the window (in pixels) 540 """ 541 active = self.mediator.interface.get_window_title() 542 result, output = self.__runWmctrl(["-l", "-G"]) 543 matchingLine = None 544 for line in output.split('\n'): 545 if active in line[34:].split(' ', 1)[-1]: 546 matchingLine = line 547 548 if matchingLine is not None: 549 output = matchingLine[14:].split(' ')[0:3] 550 return map(int, output) 551 else: 552 return None
553
554 - def __runWmctrl(self, args):
555 p = subprocess.Popen(["wmctrl"] + args, stdout=subprocess.PIPE) 556 retCode = p.wait() 557 output = p.stdout.read()[:-1] # Drop trailing newline 558 559 return (retCode, output)
560 561
562 -class Engine:
563 """ 564 Provides access to the internals of AutoKey. 565 566 Note that any configuration changes made using this API while the configuration window 567 is open will not appear until it is closed and re-opened. 568 """ 569
570 - def __init__(self, configManager, runner):
571 self.configManager = configManager 572 self.runner = runner
573
574 - def get_folder(self, title):
575 """ 576 Retrieve a folder by its title 577 578 Usage: C{engine.get_folder(title)} 579 580 Note that if more than one folder has the same title, only the first match will be 581 returned. 582 """ 583 for folder in self.configManager.allFolders: 584 if folder.title == title: 585 return folder 586 return None
587
588 - def create_phrase(self, folder, description, contents):
589 """ 590 Create a text phrase 591 592 Usage: C{engine.create_phrase(folder, description, contents)} 593 594 A new phrase with no abbreviation or hotkey is created in the specified folder 595 596 @param folder: folder to place the abbreviation in, retrieved using C{engine.get_folder()} 597 @param description: description for the phrase 598 @param contents: the expansion text 599 """ 600 p = model.Phrase(description, contents) 601 folder.add_item(p) 602 self.configManager.config_altered()
603
604 - def create_abbreviation(self, folder, description, abbr, contents):
605 """ 606 Create a text abbreviation 607 608 Usage: C{engine.create_abbreviation(folder, description, abbr, contents)} 609 610 When the given abbreviation is typed, it will be replaced with the given 611 text. 612 613 @param folder: folder to place the abbreviation in, retrieved using C{engine.get_folder()} 614 @param description: description for the phrase 615 @param abbr: the abbreviation that will trigger the expansion 616 @param contents: the expansion text 617 @raises Exception: if the specified abbreviation is not unique 618 """ 619 if not self.configManager.check_abbreviation_unique(abbr, None): 620 raise Exception("The specified abbreviation is already in use") 621 622 p = model.Phrase(description, contents) 623 p.modes.append(model.TriggerMode.ABBREVIATION) 624 p.abbreviation = abbr 625 folder.add_item(p) 626 self.configManager.config_altered()
627
628 - def create_hotkey(self, folder, description, modifiers, key, contents):
629 """ 630 Create a text hotkey. 631 632 Usage: C{engine.create_hotkey(folder, description, modifiers, key, contents)} 633 634 When the given hotkey is pressed, it will be replaced with the given 635 text. Modifiers must be given as a list of strings, with the following 636 values permitted: 637 638 <control> 639 <alt> 640 <super> 641 <shift> 642 643 The key must be an unshifted character (i.e. lowercase) 644 645 @param folder: folder to place the abbreviation in, retrieved using C{engine.get_folder()} 646 @param description: description for the phrase 647 @param modifiers: modifiers to use with the hotkey (as a list) 648 @param key: the hotkey 649 @param contents: the expansion text 650 @raises Exception: if the specified hotkey is not unique 651 """ 652 modifiers.sort() 653 if not self.configManager.check_hotkey_unique(modifiers, key, None): 654 raise Exception("The specified hotkey and modifier combination is already in use") 655 656 p = model.Phrase(description, contents) 657 p.modes.append(model.TriggerMode.HOTKEY) 658 p.set_hotkey(modifiers, key) 659 folder.add_item(p) 660 self.configManager.config_altered()
661
662 - def run_script(self, description):
663 """ 664 Run an existing script using its description to look it up 665 666 Usage: C{engine.run_script(description)} 667 668 @param description: description of the script to run 669 @raises Exception: if the specified script does not exist 670 """ 671 targetScript = None 672 for item in self.configManager.allItems: 673 if item.description == description and isinstance(item, Script): 674 targetScript = item 675 676 if targetScript is not None: 677 self.runner.execute(targetScript, "") 678 else: 679 raise Exception("No script with description '%s' found" % description)
680