| # -*- coding: utf-8 -*- |
| ''' |
| Copyright (C) 2012-2015 Diego Torres Milano |
| Created on oct 6, 2014 |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
| @author: Diego Torres Milano |
| |
| ''' |
| import StringIO |
| import random |
| import time |
| import re |
| from com.dtmilano.android.common import profileStart |
| from com.dtmilano.android.common import profileEnd |
| from com.dtmilano.android.concertina import Concertina |
| |
| __version__ = '11.5.9' |
| |
| import sys |
| import threading |
| import warnings |
| import copy |
| import string |
| import os |
| import platform |
| from pkg_resources import Requirement, resource_filename |
| |
| try: |
| import PIL |
| from PIL import Image, ImageTk |
| |
| PIL_AVAILABLE = True |
| except: |
| PIL_AVAILABLE = False |
| |
| try: |
| import Tkinter |
| import tkSimpleDialog |
| import tkFileDialog |
| import tkFont |
| import ScrolledText |
| import ttk |
| from Tkconstants import DISABLED, NORMAL |
| |
| TKINTER_AVAILABLE = True |
| except: |
| TKINTER_AVAILABLE = False |
| |
| from ast import literal_eval as make_tuple |
| |
| CHECK_KEYBOARD_SHOWN = False |
| PROFILE = False |
| |
| DEBUG = False |
| DEBUG_MOVE = DEBUG and False |
| DEBUG_TOUCH = DEBUG and False |
| DEBUG_POINT = DEBUG and False |
| DEBUG_KEY = DEBUG and False |
| DEBUG_ISCCOF = DEBUG and False |
| DEBUG_FIND_VIEW = DEBUG and False |
| DEBUG_CONTEXT_MENU = DEBUG and False |
| DEBUG_CONCERTINA = DEBUG and False |
| DEBUG_UI_AUTOMATOR_HELPER = DEBUG and False |
| |
| |
| class Color: |
| GOLD = '#d19615' |
| GREEN = '#15d137' |
| BLUE = '#1551d1' |
| MAGENTA = '#d115af' |
| DARK_GRAY = '#222222' |
| LIGHT_GRAY = '#dddddd' |
| |
| |
| class Unit: |
| PX = 'PX' |
| DIP = 'DIP' |
| |
| |
| class Operation: |
| ASSIGN = 'assign' |
| CHANGE_LANGUAGE = 'change_language' |
| DEFAULT = 'default' |
| DRAG = 'drag' |
| DUMP = 'dump' |
| FLING_BACKWARD = 'fling_backward' |
| FLING_FORWARD = 'fling_forward' |
| FLING_TO_BEGINNING = 'fling_to_beginning' |
| FLING_TO_END = 'fling_to_end' |
| TEST = 'test' |
| TEST_TEXT = 'test_text' |
| TOUCH_VIEW = 'touch_view' |
| TOUCH_VIEW_UI_AUTOMATOR_HELPER = 'touch_view_ui_automator_helper' |
| TOUCH_POINT = 'touch_point' |
| LONG_TOUCH_POINT = 'long_touch_point' |
| LONG_TOUCH_VIEW = 'long_touch_view' |
| LONG_TOUCH_VIEW_UI_AUTOMATOR_HELPER = 'long_touch_view_ui_automator_helper' |
| OPEN_NOTIFICATION = 'open_notification' |
| OPEN_QUICK_SETTINGS = 'open_quick_settings' |
| TYPE = 'type' |
| PRESS = 'press' |
| PRESS_BACK = 'press_back' |
| PRESS_BACK_UI_AUTOMATOR_HELPER = 'press_back_ui_automator_helper' |
| PRESS_HOME = 'press_home' |
| PRESS_HOME_UI_AUTOMATOR_HELPER = 'press_home_ui_automator_helper' |
| PRESS_RECENT_APPS = 'press_recent_apps' |
| PRESS_RECENT_APPS_UI_AUTOMATOR_HELPER = 'press_recent_apps_ui_automator_helper' |
| SET_TEXT = 'set_text' |
| SNAPSHOT = 'snapshot' |
| START_ACTIVITY = 'start_activity' |
| SLEEP = 'sleep' |
| SWIPE_UI_AUTOMATOR_HELPER = 'swipe_ui_automator_helper' |
| TRAVERSE = 'traverse' |
| VIEW_SNAPSHOT = 'view_snapshot' |
| WAKE = 'wake' |
| |
| COMMAND_NAME_OPERATION_MAP = {'flingBackward': FLING_BACKWARD, 'flingForward': FLING_FORWARD, |
| 'flingToBeginning': FLING_TO_BEGINNING, 'flingToEnd': FLING_TO_END, |
| 'openNotification': OPEN_NOTIFICATION, 'openQuickSettings': OPEN_QUICK_SETTINGS, |
| } |
| |
| @staticmethod |
| def fromCommandName(commandName): |
| return Operation.COMMAND_NAME_OPERATION_MAP[commandName] |
| |
| @staticmethod |
| def toCommandName(operation): |
| return next((cmd for cmd, op in Operation.COMMAND_NAME_OPERATION_MAP.items() if op == operation), None) |
| |
| |
| class Culebron: |
| APPLICATION_NAME = "Culebra" |
| |
| UPPERCASE_CHARS = string.uppercase[:26] |
| |
| KEYSYM_TO_KEYCODE_MAP = { |
| 'Home': 'HOME', |
| 'BackSpace': 'BACK', |
| 'Left': 'DPAD_LEFT', |
| 'Right': 'DPAD_RIGHT', |
| 'Up': 'DPAD_UP', |
| 'Down': 'DPAD_DOWN', |
| } |
| |
| KEYSYM_CULEBRON_COMMANDS = { |
| 'F1': None, |
| 'F5': None |
| } |
| |
| canvas = None |
| imageId = None |
| vignetteId = None |
| areTargetsMarked = False |
| isDragDialogShowed = False |
| isGrabbingTouch = False |
| isGeneratingTestCondition = False |
| isTouchingPoint = False |
| isLongTouchingPoint = False |
| isLongTouchingView = False |
| onTouchListener = None |
| snapshotDir = '/tmp' |
| snapshotFormat = 'PNG' |
| deviceArt = None |
| dropShadow = False |
| screenGlare = False |
| osName = platform.system() |
| ''' The OS name. We sometimes need specific behavior. ''' |
| isDarwin = (osName == 'Darwin') |
| ''' Is it Mac OSX? ''' |
| |
| @staticmethod |
| def checkSupportedSdkVersion(sdkVersion): |
| if sdkVersion <= 10: |
| raise Exception('''culebra GUI requires Android API > 10 to work''') |
| |
| @staticmethod |
| def checkDependencies(): |
| if not PIL_AVAILABLE: |
| raise Exception('''PIL or Pillow is needed for GUI mode |
| |
| On Ubuntu install |
| |
| $ sudo apt-get install python-imaging python-imaging-tk |
| |
| On OSX install |
| |
| $ brew install homebrew/python/pillow |
| |
| or, preferred since El Capitan |
| |
| $ sudo easy_install pip |
| $ sudo pip install pillow |
| |
| ''') |
| if not TKINTER_AVAILABLE: |
| raise Exception('''Tkinter is needed for GUI mode |
| |
| This is usually installed by python package. Check your distribution details. |
| ''') |
| |
| def __init__(self, vc, device, serialno, printOperation, scale=1, concertina=False): |
| ''' |
| Culebron constructor. |
| |
| @param vc: The ViewClient used by this Culebron instance. Can be C{None} if no back-end is used. |
| @type vc: ViewClient |
| @param device: The device |
| @type device: L{AdbClient} |
| @param serialno: The device's serial number |
| @type serialno: str |
| @param printOperation: the method invoked to print operations to the script |
| @type printOperation: method |
| @param scale: the scale of the device screen used to show it on the window |
| @type scale: float |
| @:param concertina: bool |
| @:type concertina: enable concertina mode (see documentation) |
| ''' |
| |
| self.vc = vc |
| self.printOperation = printOperation |
| self.device = device |
| self.sdkVersion = device.getSdkVersion() |
| self.serialno = serialno |
| self.scale = scale |
| self.concertina = concertina |
| self.window = Tkinter.Tk() |
| try: |
| f = resource_filename(Requirement.parse("androidviewclient"), |
| "share/pixmaps/culebra.png") |
| icon = ImageTk.PhotoImage(file=f) |
| except: |
| icon = None |
| if icon: |
| self.window.tk.call('wm', 'iconphoto', self.window._w, icon) |
| self.mainMenu = MainMenu(self) |
| self.window.config(menu=self.mainMenu) |
| self.mainFrame = Tkinter.Frame(self.window) |
| self.placeholder = Tkinter.Frame(self.mainFrame, width=400, height=400, background=Color.LIGHT_GRAY) |
| self.placeholder.grid(row=1, column=1, rowspan=4) |
| self.sideFrame = Tkinter.Frame(self.window) |
| self.viewTree = ViewTree(self.sideFrame) |
| self.viewDetails = ViewDetails(self.sideFrame) |
| self.mainFrame.grid(row=1, column=1, columnspan=1, rowspan=4, sticky=Tkinter.N + Tkinter.S) |
| self.isSideFrameShown = False |
| self.isViewTreeShown = False |
| self.isViewDetailsShown = False |
| self.statusBar = StatusBar(self.window) |
| self.statusBar.grid(row=5, column=1, columnspan=2) |
| self.statusBar.set("Always press F1 for help") |
| self.window.update_idletasks() |
| self.markedTargetIds = {} |
| self.isTouchingPoint = self.vc is None |
| self.coordinatesUnit = Unit.DIP |
| self.isLongTouchingPoint = False |
| self.isLongTouchingView = False |
| self.permanentlyDisableEvents = False |
| self.unscaledScreenshot = None |
| self.image = None |
| self.screenshot = None |
| if DEBUG: |
| try: |
| self.printGridInfo() |
| except: |
| pass |
| |
| def printGridInfo(self): |
| print >> sys.stderr, "window:", repr(self.window) |
| print >> sys.stderr, "main:", repr(self.mainFrame) |
| print >> sys.stderr, "main:", self.mainFrame.grid_info() |
| print >> sys.stderr, "side:", repr(self.sideFrame) |
| print >> sys.stderr, "side:", self.sideFrame.grid_info() |
| print >> sys.stderr, "tree:", repr(self.viewTree) |
| print >> sys.stderr, "tree:", self.viewTree.grid_info() |
| print >> sys.stderr, "details:", repr(self.viewDetails) |
| print >> sys.stderr, "details:", self.viewDetails.grid_info() |
| |
| def takeScreenshotAndShowItOnWindow(self): |
| ''' |
| Takes the current screenshot and shows it on the main window. |
| It also: |
| - sizes the window |
| - create the canvas |
| - set the focus |
| - enable the events |
| - create widgets |
| - finds the targets (as explained in L{findTargets}) |
| - hides the vignette (that could have been showed before) |
| ''' |
| |
| if PROFILE: |
| print >> sys.stderr, "PROFILING: takeScreenshotAndShowItOnWindow()" |
| profileStart() |
| |
| if DEBUG: |
| print >> sys.stderr, "takeScreenshotAndShowItOnWindow()" |
| if self.vc and self.vc.uiAutomatorHelper: |
| received = self.vc.uiAutomatorHelper.takeScreenshot() |
| stream = StringIO.StringIO(received) |
| self.unscaledScreenshot = Image.open(stream) |
| else: |
| self.unscaledScreenshot = self.device.takeSnapshot(reconnect=True) |
| self.image = self.unscaledScreenshot |
| (width, height) = self.image.size |
| if self.scale != 1: |
| scaledWidth = int(width * self.scale) |
| scaledHeight = int(height * self.scale) |
| self.image = self.image.resize((scaledWidth, scaledHeight), PIL.Image.ANTIALIAS) |
| (width, height) = self.image.size |
| if self.isDarwin and 14 < self.sdkVersion < 23: |
| stream = StringIO.StringIO() |
| self.image.save(stream, 'GIF') |
| import base64 |
| gif = base64.b64encode(stream.getvalue()) |
| stream.close() |
| if self.canvas is None: |
| if DEBUG: |
| print >> sys.stderr, "Creating canvas", width, 'x', height |
| self.placeholder.grid_forget() |
| self.canvas = Tkinter.Canvas(self.mainFrame, width=width, height=height) |
| self.canvas.focus_set() |
| self.enableEvents() |
| self.createMessageArea(width, height) |
| self.createVignette(width, height) |
| if self.isDarwin and self.scale != 1 and 14 < self.sdkVersion < 23: |
| # Extremely weird Tkinter bug, I guess |
| # If the image was rotated and then resized if ImageTk.PhotoImage(self.image) |
| # is used as usual then the result is a completely transparent image and only |
| # the "Please wait..." is seen. |
| # Converting it to GIF seems to solve the problem |
| self.screenshot = Tkinter.PhotoImage(data=gif) |
| else: |
| self.screenshot = ImageTk.PhotoImage(self.image) |
| if self.imageId is not None: |
| self.canvas.delete(self.imageId) |
| self.imageId = self.canvas.create_image(0, 0, anchor=Tkinter.NW, image=self.screenshot) |
| if DEBUG: |
| try: |
| print >> sys.stderr, "Grid info", self.canvas.grid_info() |
| except: |
| print >> sys.stderr, "Exception getting grid info" |
| gridInfo = None |
| try: |
| gridInfo = self.canvas.grid_info() |
| except: |
| if DEBUG: |
| print >> sys.stderr, "Adding canvas to grid (1,1)" |
| self.canvas.grid(row=1, column=1, rowspan=4) |
| if not gridInfo: |
| self.canvas.grid(row=1, column=1, rowspan=4) |
| self.findTargets() |
| self.hideVignette() |
| if DEBUG: |
| try: |
| self.printGridInfo() |
| except: |
| pass |
| if PROFILE: |
| profileEnd() |
| |
| def createMessageArea(self, width, height): |
| self.__message = Tkinter.Label(self.window, text='', background=Color.GOLD, font=('Helvetica', 16), |
| anchor=Tkinter.W) |
| self.__message.configure(width=width) |
| self.__messageAreaId = self.canvas.create_window(0, 0, anchor=Tkinter.NW, window=self.__message) |
| self.canvas.itemconfig(self.__messageAreaId, state='hidden') |
| self.isMessageAreaVisible = False |
| |
| def showMessageArea(self): |
| if self.__messageAreaId: |
| self.canvas.itemconfig(self.__messageAreaId, state='normal') |
| self.isMessageAreaVisible = True |
| self.canvas.update_idletasks() |
| |
| def hideMessageArea(self): |
| if self.__messageAreaId and self.isMessageAreaVisible: |
| self.canvas.itemconfig(self.__messageAreaId, state='hidden') |
| self.isMessageAreaVisible = False |
| self.canvas.update_idletasks() |
| |
| def toggleMessageArea(self): |
| if self.isMessageAreaVisible: |
| self.hideMessageArea() |
| else: |
| self.showMessageArea() |
| |
| def message(self, text, background=None): |
| self.__message.config(text=text) |
| if background: |
| self.__message.config(background=background) |
| self.showMessageArea() |
| |
| def toast(self, text, background=None, timeout=5): |
| if DEBUG: |
| print >> sys.stderr, "toast(", text, ",", background, ")" |
| self.message(text, background) |
| if text: |
| t = threading.Timer(timeout, self.hideMessageArea) |
| t.start() |
| else: |
| self.hideMessageArea() |
| |
| def createVignette(self, width, height): |
| if DEBUG: |
| print >> sys.stderr, "createVignette(%d, %d)" % (width, height) |
| self.vignetteId = self.canvas.create_rectangle(0, 0, width, height, fill=Color.MAGENTA, |
| stipple='gray50') |
| font = tkFont.Font(family='Helvetica', size=int(144 * self.scale)) |
| msg = "Please\nwait..." |
| self.waitMessageShadowId = self.canvas.create_text(width / 2 + 2, height / 2 + 2, text=msg, |
| fill=Color.DARK_GRAY, font=font) |
| self.waitMessageId = self.canvas.create_text(width / 2, height / 2, text=msg, |
| fill=Color.LIGHT_GRAY, font=font) |
| self.canvas.update_idletasks() |
| |
| def showVignette(self): |
| if DEBUG: |
| print >> sys.stderr, "showVignette()" |
| if self.canvas is None: |
| return |
| if self.vignetteId: |
| if DEBUG: |
| print >> sys.stderr, " showing vignette" |
| # disable events while we are processing one |
| self.disableEvents() |
| self.canvas.lift(self.vignetteId) |
| self.canvas.lift(self.waitMessageShadowId) |
| self.canvas.lift(self.waitMessageId) |
| self.canvas.update_idletasks() |
| |
| def hideVignette(self): |
| if DEBUG: |
| print >> sys.stderr, "hideVignette()" |
| if self.canvas is None: |
| return |
| if self.vignetteId: |
| if DEBUG: |
| print >> sys.stderr, " hiding vignette" |
| self.canvas.lift(self.imageId) |
| self.canvas.update_idletasks() |
| self.enableEvents() |
| |
| def deleteVignette(self): |
| if self.canvas is not None: |
| self.canvas.delete(self.vignetteId) |
| self.vignetteId = None |
| self.canvas.delete(self.waitMessageShadowId) |
| self.waitMessageShadowId = None |
| self.canvas.delete(self.waitMessageId) |
| self.waitMessageId = None |
| |
| def showPopupMenu(self, event): |
| (scaledX, scaledY) = (event.x / self.scale, event.y / self.scale) |
| v = self.findViewContainingPointInTargets(scaledX, scaledY) |
| ContextMenu(self, view=v).showPopupMenu(event) |
| |
| def showHelp(self): |
| d = HelpDialog(self) |
| self.window.wait_window(d) |
| |
| def showSideFrame(self): |
| if not self.isSideFrameShown: |
| self.sideFrame.grid(row=1, column=2, rowspan=4, sticky=Tkinter.N + Tkinter.S) |
| self.isSideFrameSown = True |
| if DEBUG: |
| self.printGridInfo() |
| |
| def hideSideFrame(self): |
| self.sideFrame.grid_forget() |
| self.isSideFrameShown = False |
| if DEBUG: |
| self.printGridInfo() |
| |
| def showViewTree(self): |
| self.showSideFrame() |
| self.viewTree.grid(row=1, column=1, rowspan=3, sticky=Tkinter.N + Tkinter.S) |
| self.isViewTreeShown = True |
| if DEBUG: |
| self.printGridInfo() |
| |
| def hideViewTree(self): |
| self.unmarkTargets() |
| self.viewTree.grid_forget() |
| self.isViewTreeShown = False |
| if not self.isViewDetailsShown: |
| self.hideSideFrame() |
| if DEBUG: |
| self.printGridInfo() |
| |
| def showViewDetails(self): |
| self.showSideFrame() |
| row = 4 |
| # if self.viewTree.grid_info() != {}: |
| # row += 1 |
| self.viewDetails.grid(row=row, column=1, rowspan=1, sticky=Tkinter.S) |
| self.isViewDetailsShown = True |
| if DEBUG: |
| self.printGridInfo() |
| |
| def hideViewDetails(self): |
| self.viewDetails.grid_forget() |
| self.isViewDetailsShown = False |
| if not self.isViewTreeShown: |
| self.hideSideFrame() |
| if DEBUG: |
| self.printGridInfo() |
| |
| def viewTreeItemClicked(self, event): |
| if DEBUG: |
| print >> sys.stderr, "viewTreeitemClicked:", event.__dict__ |
| self.unmarkTargets() |
| vuid = self.viewTree.viewTree.identify_row(event.y) |
| if vuid: |
| view = self.vc.viewsById[vuid] |
| if view: |
| coords = view.getCoords() |
| if view.isTarget(): |
| self.markTarget(coords[0][0], coords[0][1], coords[1][0], coords[1][1]) |
| self.viewDetails.set(view) |
| |
| def populateViewTree(self, view): |
| ''' |
| Populates the View tree. |
| ''' |
| |
| vuid = view.getUniqueId() |
| text = view.__smallStr__() |
| if view.getParent() is None: |
| self.viewTree.insert('', Tkinter.END, vuid, text=text) |
| else: |
| self.viewTree.insert(view.getParent().getUniqueId(), Tkinter.END, vuid, text=text, tags=('ttk')) |
| self.viewTree.set(vuid, 'T', '*' if view.isTarget() else ' ') |
| self.viewTree.tag_bind('ttk', '<1>', self.viewTreeItemClicked) |
| |
| def findTargets(self): |
| ''' |
| Finds the target Views (i.e. for touches). |
| ''' |
| |
| if DEBUG: |
| print >> sys.stderr, "findTargets()" |
| LISTVIEW_CLASS = 'android.widget.ListView' |
| ''' The ListView class name ''' |
| self.targets = [] |
| ''' The list of target coordinates (x1, y1, x2, y2) ''' |
| self.targetViews = [] |
| ''' The list of target Views ''' |
| if CHECK_KEYBOARD_SHOWN: |
| if self.device.isKeyboardShown(): |
| print >> sys.stderr, "#### keyboard is show but handling it is not implemented yet ####" |
| # FIXME: still no windows in uiautomator |
| window = -1 |
| else: |
| window = -1 |
| else: |
| window = -1 |
| if self.vc: |
| dump = self.vc.dump(window=window, sleep=0.1) |
| self.printOperation(None, Operation.DUMP, window, dump) |
| else: |
| dump = [] |
| # the root element cannot be deleted from Treeview once added. |
| # We have no option but to recreate it |
| self.viewTree = ViewTree(self.sideFrame) |
| for v in dump: |
| if DEBUG: |
| print >> sys.stderr, " findTargets: analyzing", v.getClass(), v.getId() |
| if v.getClass() == LISTVIEW_CLASS: |
| # We may want to touch ListView elements, not just the ListView |
| continue |
| parent = v.getParent() |
| if (parent and parent.getClass() == LISTVIEW_CLASS and self.isClickableCheckableOrFocusable(parent)) \ |
| or self.isClickableCheckableOrFocusable(v): |
| # If this is a touchable ListView, let's add its children instead |
| # or add it if it's touchable, focusable, whatever |
| ((x1, y1), (x2, y2)) = v.getCoords() |
| if DEBUG: |
| print >> sys.stderr, "appending target", ((x1, y1, x2, y2)) |
| v.setTarget(True) |
| self.targets.append((x1, y1, x2, y2)) |
| self.targetViews.append(v) |
| target = True |
| else: |
| target = False |
| |
| if self.vc: |
| self.vc.traverse(transform=self.populateViewTree) |
| |
| def getViewContainingPointAndGenerateTestCondition(self, x, y): |
| if DEBUG: |
| print >> sys.stderr, 'getViewContainingPointAndGenerateTestCondition(%d, %d)' % (x, y) |
| self.finishGeneratingTestCondition() |
| vlist = self.vc.findViewsContainingPoint((x, y)) |
| vlist.reverse() |
| for v in vlist: |
| text = v.getText() |
| if text: |
| self.toast(u'Asserting view with text=%s' % text, timeout=5) |
| # FIXME: only getText() is invoked by the generated assert(), a parameter |
| # should be used to provide different alternatives to printOperation() |
| self.printOperation(v, Operation.TEST, text) |
| break |
| |
| def findViewContainingPointInTargets(self, x, y): |
| if self.vc: |
| vlist = self.vc.findViewsContainingPoint((x, y)) |
| if DEBUG_FIND_VIEW: |
| print >> sys.stderr, "Views found:" |
| for v in vlist: |
| print >> sys.stderr, " ", v.__smallStr__() |
| vlist.reverse() |
| for v in vlist: |
| if DEBUG: |
| print >> sys.stderr, "checking if", v, "is in", self.targetViews |
| if v in self.targetViews: |
| if DEBUG_TOUCH: |
| print >> sys.stderr |
| print >> sys.stderr, "I guess you are trying to touch:", v |
| print >> sys.stderr |
| return v |
| |
| return None |
| |
| def getViewContainingPointAndTouch(self, x, y): |
| if DEBUG: |
| print >> sys.stderr, 'getViewContainingPointAndTouch(%d, %d)' % (x, y) |
| if self.areEventsDisabled: |
| if DEBUG: |
| print >> sys.stderr, "Ignoring event" |
| self.canvas.update_idletasks() |
| return |
| |
| self.showVignette() |
| if DEBUG_POINT: |
| print >> sys.stderr, "getViewsContainingPointAndTouch(x=%s, y=%s)" % (x, y) |
| print >> sys.stderr, "self.vc=", self.vc |
| v = self.findViewContainingPointInTargets(x, y) |
| |
| if v is None: |
| self.hideVignette() |
| msg = "There are no explicitly touchable or clickable views here! Touching with [x,y]" |
| self.toast(msg) |
| # A partial hack which temporarily toggles touch point |
| self.toggleTouchPoint() |
| self.touchPoint(x, y) |
| self.toggleTouchPoint() |
| else: |
| if self.vc.uiAutomatorHelper: |
| # These operations are only available through uiAutomatorHelper |
| if v == self.vc.navBack: |
| self.pressBack() |
| return |
| elif v == self.vc.navHome: |
| self.pressHome() |
| return |
| elif v == self.vc.navRecentApps: |
| self.pressRecentApps() |
| return |
| |
| clazz = v.getClass() |
| if clazz == 'android.widget.EditText': |
| title = "EditText" |
| kwargs = {} |
| if DEBUG: |
| print >> sys.stderr, v |
| if v.isPassword(): |
| title = "Password" |
| kwargs = {'show': '*'} |
| text = tkSimpleDialog.askstring(title, "Enter text to type into this field", **kwargs) |
| self.canvas.focus_set() |
| if text: |
| self.vc.setText(v, text) |
| self.printOperation(v, Operation.SET_TEXT, text) |
| else: |
| self.hideVignette() |
| return |
| else: |
| candidates = [v] |
| |
| def findBestCandidate(view): |
| isccf = Culebron.isClickableCheckableOrFocusable(view) |
| cd = view.getContentDescription() |
| text = view.getText() |
| if (cd or text) and not isccf: |
| # because isccf==False this view was not added to the list of targets |
| # (i.e. Settings) |
| candidates.insert(0, view) |
| return None |
| |
| if not (v.getText() or v.getContentDescription()) and v.getChildren(): |
| self.vc.traverse(root=v, transform=findBestCandidate, stream=None) |
| if len(candidates) > 2: |
| warnings.warn("We are in trouble, we have more than one candidate to touch", stacklevel=0) |
| candidate = candidates[0] |
| self.touchView(candidate, v if candidate != v else None) |
| |
| self.printOperation(None, Operation.SLEEP, Operation.DEFAULT) |
| self.vc.sleep(5) |
| self.takeScreenshotAndShowItOnWindow() |
| |
| def pressBack(self): |
| self.showVignette() |
| self.vc.pressBack() |
| if self.vc.uiAutomatorHelper: |
| self.printOperation(None, Operation.PRESS_BACK_UI_AUTOMATOR_HELPER) |
| else: |
| self.printOperation(None, Operation.PRESS_BACK) |
| self.takeScreenshotAndShowItOnWindow() |
| |
| def pressHome(self): |
| self.showVignette() |
| self.vc.pressHome() |
| if self.vc.uiAutomatorHelper: |
| self.printOperation(None, Operation.PRESS_HOME_UI_AUTOMATOR_HELPER) |
| else: |
| self.printOperation(None, Operation.PRESS_HOME) |
| self.takeScreenshotAndShowItOnWindow() |
| |
| def pressRecentApps(self): |
| self.showVignette() |
| self.vc.pressRecentApps() |
| if self.vc.uiAutomatorHelper: |
| self.printOperation(None, Operation.PRESS_RECENT_APPS_UI_AUTOMATOR_HELPER) |
| else: |
| self.printOperation(None, Operation.PRESS_RECENT_APPS) |
| self.vc.sleep(1) |
| self.takeScreenshotAndShowItOnWindow() |
| |
| def setText(self, v, text): |
| if DEBUG: |
| print >> sys.stderr, "setText(%s, '%s')" % (v.__tinyStr__(), text) |
| # This is deleting the existing text, which should be asked in the dialog, but I would have to implement |
| # the dialog myself |
| v.setText(text) |
| # This is not deleting the text, so appending if there's something |
| # v.type(text) |
| self.printOperation(v, Operation.TYPE, text) |
| |
| def touchView(self, v, root=None): |
| v.touch() |
| if v.uiAutomatorHelper: |
| self.printOperation(v, Operation.TOUCH_VIEW_UI_AUTOMATOR_HELPER, v.obtainSelectorForView()) |
| else: |
| # we pass root=v as an argument so the corresponding findView*() searches in this |
| # subtree instead of the full tree |
| self.printOperation(v, Operation.TOUCH_VIEW, root) |
| |
| def touchPoint(self, x, y): |
| ''' |
| Touches a point in the device screen. |
| The generated operation will use the units specified in L{coordinatesUnit} and the |
| orientation in L{vc.display['orientation']}. |
| ''' |
| |
| if DEBUG: |
| print >> sys.stderr, 'touchPoint(%d, %d)' % (x, y) |
| print >> sys.stderr, 'touchPoint:', type(x), type(y) |
| if self.areEventsDisabled: |
| if DEBUG: |
| print >> sys.stderr, "Ignoring event" |
| self.canvas.update_idletasks() |
| return |
| if DEBUG: |
| print >> sys.stderr, "Is touching point:", self.isTouchingPoint |
| if self.isTouchingPoint: |
| self.showVignette() |
| self.vc.touch(x, y) |
| if self.coordinatesUnit == Unit.DIP: |
| x = round(x / self.device.display['density'], 2) |
| y = round(y / self.device.display['density'], 2) |
| self.printOperation(None, Operation.TOUCH_POINT, x, y, self.coordinatesUnit, |
| self.device.display['orientation']) |
| self.printOperation(None, Operation.SLEEP, Operation.DEFAULT) |
| # FIXME: can we reduce this sleep? (was 5) |
| time.sleep(1) |
| self.isTouchingPoint = self.vc is None |
| self.takeScreenshotAndShowItOnWindow() |
| # self.hideVignette() |
| self.statusBar.clear() |
| return |
| |
| def longTouchPoint(self, x, y): |
| ''' |
| Long-touches a point in the device screen. |
| The generated operation will use the units specified in L{coordinatesUnit} and the |
| orientation in L{vc.display['orientation']}. |
| ''' |
| |
| if DEBUG: |
| print >> sys.stderr, 'longTouchPoint(%d, %d)' % (x, y) |
| if self.areEventsDisabled: |
| if DEBUG: |
| print >> sys.stderr, "Ignoring event" |
| self.canvas.update_idletasks() |
| return |
| if DEBUG: |
| print >> sys.stderr, "Is long touching point:", self.isLongTouchingPoint |
| if self.isLongTouchingPoint: |
| self.showVignette() |
| self.vc.longTouch(x, y) |
| if self.coordinatesUnit == Unit.DIP: |
| x = round(x / self.device.display['density'], 2) |
| y = round(y / self.device.display['density'], 2) |
| self.printOperation(None, Operation.LONG_TOUCH_POINT, x, y, 2000, self.coordinatesUnit, |
| self.device.display['orientation']) |
| self.printOperation(None, Operation.SLEEP, 5) |
| time.sleep(5) |
| self.isLongTouchingPoint = False |
| self.takeScreenshotAndShowItOnWindow() |
| # self.hideVignette() |
| self.statusBar.clear() |
| return |
| |
| def longTouchView(self, v, root=None): |
| v.longTouch() |
| if v.uiAutomatorHelper: |
| self.printOperation(v, Operation.LONG_TOUCH_VIEW_UI_AUTOMATOR_HELPER, v.obtainSelectorForView()) |
| else: |
| # we pass root=v as an argument so the corresponding findView*() searches in this |
| # subtree instead of the full tree |
| self.printOperation(v, Operation.LONG_TOUCH_VIEW, root) |
| self.isLongTouchingView = False |
| |
| def onButton1Pressed(self, event): |
| if DEBUG: |
| print >> sys.stderr, "onButton1Pressed((", event.x, ", ", event.y, "))" |
| (scaledX, scaledY) = (event.x / self.scale, event.y / self.scale) |
| if DEBUG: |
| print >> sys.stderr, " onButton1Pressed: scaled: (", scaledX, ", ", scaledY, ")" |
| print >> sys.stderr, " onButton1Pressed: is grabbing:", self.isGrabbingTouch |
| |
| if self.isGrabbingTouch: |
| self.onTouchListener((scaledX, scaledY)) |
| self.isGrabbingTouch = False |
| elif self.isDragDialogShowed: |
| self.toast("No touch events allowed while setting drag parameters", background=Color.GOLD) |
| return |
| elif self.isTouchingPoint: |
| self.touchPoint(scaledX, scaledY) |
| elif self.isLongTouchingPoint: |
| self.longTouchPoint(scaledX, scaledY) |
| elif self.isLongTouchingView: |
| self.getViewContainingPointAndLongTouch(scaledX, scaledY) |
| elif self.isGeneratingTestCondition: |
| self.getViewContainingPointAndGenerateTestCondition(scaledX, scaledY) |
| else: |
| if self.vc: |
| self.getViewContainingPointAndTouch(scaledX, scaledY) |
| else: |
| # If we don't have Views, there no other option than touching points |
| self.touchPoint(scaledX, scaledY) |
| |
| def onCtrlButton1Pressed(self, event): |
| if DEBUG: |
| print >> sys.stderr, "onCtrlButton1Pressed((", event.x, ", ", event.y, "))" |
| (scaledX, scaledY) = (event.x / self.scale, event.y / self.scale) |
| l = self.vc.findViewsContainingPoint((scaledX, scaledY)) |
| if l and len(l) > 0: |
| self.saveViewSnapshot(l[-1]) |
| else: |
| msg = "There are no views here!" |
| self.toast(msg) |
| return |
| |
| def onButton2Pressed(self, event): |
| if DEBUG: |
| print >> sys.stderr, "onButton2Pressed((", event.x, ", ", event.y, "))" |
| osName = platform.system() |
| if osName == 'Darwin': |
| self.showPopupMenu(event) |
| |
| def onButton3Pressed(self, event): |
| if DEBUG: |
| print >> sys.stderr, "onButton3Pressed((", event.x, ", ", event.y, "))" |
| self.showPopupMenu(event) |
| |
| def command(self, keycode): |
| ''' |
| Presses a key. |
| Generates the actual key press on the device and prints the line in the script. |
| ''' |
| |
| self.device.press(keycode) |
| self.printOperation(None, Operation.PRESS, keycode) |
| |
| def onKeyPressed(self, event): |
| if DEBUG_KEY: |
| print >> sys.stderr, "onKeyPressed(", repr(event), ")" |
| print >> sys.stderr, " event", type(event.char), len(event.char), repr( |
| event.char), event.keysym, event.keycode, event.type |
| print >> sys.stderr, " events disabled:", self.areEventsDisabled |
| if self.areEventsDisabled: |
| if DEBUG_KEY: |
| print >> sys.stderr, "ignoring event" |
| self.canvas.update_idletasks() |
| return |
| |
| char = event.char |
| keysym = event.keysym |
| |
| if len(char) == 0 and not ( |
| keysym in Culebron.KEYSYM_TO_KEYCODE_MAP or keysym in Culebron.KEYSYM_CULEBRON_COMMANDS): |
| if DEBUG_KEY: |
| print >> sys.stderr, "returning because len(char) == 0" |
| return |
| |
| ### |
| ### internal commands: no output to generated script |
| ### |
| try: |
| handler = getattr(self, 'onCtrl%s' % self.UPPERCASE_CHARS[ord(char) - 1]) |
| except: |
| handler = None |
| if handler: |
| return handler(event) |
| elif keysym == 'F1': |
| self.showHelp() |
| return |
| elif keysym == 'F5': |
| self.refresh() |
| return |
| elif keysym == 'F8': |
| self.printGridInfo() |
| return |
| elif keysym == 'Alt_L': |
| return |
| elif keysym == 'Control_L': |
| return |
| elif keysym == 'Escape': |
| # we cannot send Escape to the device, but I think it's fine |
| self.cancelOperation() |
| return |
| |
| ### empty char (modifier) ### |
| # here does not process events like Home where char is '' |
| # if char == '': |
| # return |
| |
| ### |
| ### target actions |
| ### |
| self.showVignette() |
| |
| if keysym in Culebron.KEYSYM_TO_KEYCODE_MAP: |
| if DEBUG_KEY: |
| print >> sys.stderr, "Pressing", Culebron.KEYSYM_TO_KEYCODE_MAP[keysym] |
| self.command(Culebron.KEYSYM_TO_KEYCODE_MAP[keysym]) |
| elif char == '\r': |
| self.command('ENTER') |
| elif char == '': |
| # do nothing |
| pass |
| else: |
| self.command(char.decode('ascii', errors='replace')) |
| # commented out (profile) |
| # time.sleep(1) |
| self.takeScreenshotAndShowItOnWindow() |
| |
| def wake(self): |
| self.refresh() |
| self.printOperation(None, Operation.WAKE) |
| |
| def refresh(self): |
| self.showVignette() |
| self.device.wake() |
| display = copy.copy(self.device.display) |
| self.device.initDisplayProperties() |
| changed = False |
| for prop in display: |
| if display[prop] != self.device.display[prop]: |
| changed = True |
| break |
| if changed: |
| self.window.geometry('%dx%d' % (self.device.display['width'] * self.scale, |
| self.device.display['height'] * self.scale + int( |
| self.statusBar.winfo_height()))) |
| self.deleteVignette() |
| self.canvas.destroy() |
| self.canvas = None |
| self.window.update_idletasks() |
| self.takeScreenshotAndShowItOnWindow() |
| |
| def cancelOperation(self): |
| ''' |
| Cancels the ongoing operation if any. |
| ''' |
| if self.isLongTouchingPoint: |
| self.toggleLongTouchPoint() |
| elif self.isTouchingPoint: |
| self.toggleTouchPoint() |
| elif self.isGeneratingTestCondition: |
| self.toggleGenerateTestCondition() |
| |
| def printStartActivityAtTop(self): |
| self.printOperation(None, Operation.START_ACTIVITY, self.device.getTopActivityName()) |
| |
| def onCtrlA(self, event): |
| if DEBUG: |
| print >> sys.stderr, "onCtrlA(", event, ")" |
| self.printStartActivityAtTop() |
| |
| def showDragDialog(self): |
| d = DragDialog(self) |
| self.window.wait_window(d) |
| self.setDragDialogShowed(False) |
| |
| def onCtrlD(self, event): |
| self.showDragDialog() |
| |
| def onCtrlF(self, event): |
| self.saveSnapshot() |
| |
| def saveSnapshot(self): |
| ''' |
| Saves the current shanpshot to the specified file. |
| Current snapshot is the image being displayed on the main window. |
| ''' |
| |
| filename = self.snapshotDir + os.sep + '${serialno}-${focusedwindowname}-${timestamp}' + '.' + self.snapshotFormat.lower() |
| # We have the snapshot already taken, no need to retake |
| d = FileDialog(self, self.device.substituteDeviceTemplate(filename)) |
| saveAsFilename = d.askSaveAsFilename() |
| if saveAsFilename: |
| _format = os.path.splitext(saveAsFilename)[1][1:].upper() |
| self.printOperation(None, Operation.SNAPSHOT, filename, _format, self.deviceArt, self.dropShadow, |
| self.screenGlare) |
| # FIXME: we should add deviceArt, dropShadow and screenGlare to the saved image |
| # self.unscaledScreenshot.save(saveAsFilename, _format, self.deviceArt, self.dropShadow, self.screenGlare) |
| self.unscaledScreenshot.save(saveAsFilename, _format) |
| |
| def saveViewSnapshot(self, view): |
| ''' |
| Saves the View snapshot. |
| ''' |
| |
| if not view: |
| raise ValueError("view must be provided to take snapshot") |
| filename = self.snapshotDir + os.sep + '${serialno}-' + view.variableNameFromId() + '-${timestamp}' + '.' + self.snapshotFormat.lower() |
| d = FileDialog(self, self.device.substituteDeviceTemplate(filename)) |
| saveAsFilename = d.askSaveAsFilename() |
| if saveAsFilename: |
| _format = os.path.splitext(saveAsFilename)[1][1:].upper() |
| self.printOperation(view, Operation.VIEW_SNAPSHOT, filename, _format) |
| view.writeImageToFile(saveAsFilename, _format) |
| |
| def toggleTouchPointDip(self): |
| ''' |
| Toggles the touch point operation using L{Unit.DIP}. |
| This invokes L{toggleTouchPoint}. |
| ''' |
| |
| self.coordinatesUnit = Unit.DIP |
| self.toggleTouchPoint() |
| |
| def onCtrlI(self, event): |
| self.toggleTouchPointDip() |
| |
| def toggleLongTouchPoint(self): |
| ''' |
| Toggles the long touch point operation. |
| ''' |
| if not self.isLongTouchingPoint: |
| msg = 'Long touching point' |
| self.toast(msg, background=Color.GREEN) |
| self.statusBar.set(msg) |
| self.isLongTouchingPoint = True |
| # FIXME: There should be 2 methods DIP & PX |
| self.coordinatesUnit = Unit.PX |
| else: |
| self.toast(None) |
| self.statusBar.clear() |
| self.isLongTouchingPoint = False |
| |
| def toggleLongTouchView(self): |
| ''' |
| Toggles the long touch View operation. |
| :return: |
| ''' |
| if not self.isLongTouchingView: |
| msg = 'Long touching View' |
| self.toast(msg, background=Color.GREEN) |
| self.statusBar.set(msg) |
| self.isLongTouchingView = True |
| else: |
| self.toast(None) |
| self.statusBar.clear() |
| self.isLongTouchingView = False |
| |
| def onCtrlL(self, event): |
| self.toggleLongTouchPoint() |
| |
| def toggleTouchPoint(self): |
| ''' |
| Toggles the touch point operation using the units specified in L{coordinatesUnit}. |
| |
| When there are L{View}s (obtained from the back-end) we have to determine if the |
| intention when something is touched on the window if we want to touch the L{View} |
| or the point. |
| |
| If there's no back-end, we don't allow L{self.isTouchingPoint} to be disabled so we will |
| never be attempting to touch L{View}s. |
| ''' |
| |
| if not self.isTouchingPoint: |
| msg = 'Touching point (units=%s)' % self.coordinatesUnit |
| self.toast(msg, background=Color.GREEN) |
| self.statusBar.set(msg) |
| self.isTouchingPoint = True |
| else: |
| self.toast(None) |
| self.statusBar.clear() |
| self.isTouchingPoint = self.vc is None |
| |
| def toggleTouchPointPx(self): |
| self.coordinatesUnit = Unit.PX |
| self.toggleTouchPoint() |
| |
| def onCtrlP(self, event): |
| self.toggleTouchPointPx() |
| |
| def onCtrlQ(self, event): |
| if DEBUG: |
| print >> sys.stderr, "onCtrlQ(%s)" % event |
| self.quit() |
| |
| def quit(self): |
| if self.vc.uiAutomatorHelper: |
| if DEBUG or True: |
| print >> sys.stderr, "Quitting UiAutomatorHelper..." |
| self.vc.uiAutomatorHelper.quit() |
| self.window.destroy() |
| |
| def showSleepDialog(self): |
| seconds = tkSimpleDialog.askfloat('Sleep Interval', 'Value in seconds:', initialvalue=1, minvalue=0, |
| parent=self.window) |
| if seconds is not None: |
| self.printOperation(None, Operation.SLEEP, seconds) |
| self.canvas.focus_set() |
| |
| def onCtrlS(self, event): |
| self.showSleepDialog() |
| |
| def startGeneratingTestCondition(self): |
| self.message('Generating test condition...', background=Color.GREEN) |
| self.isGeneratingTestCondition = True |
| |
| def finishGeneratingTestCondition(self): |
| self.isGeneratingTestCondition = False |
| self.hideMessageArea() |
| |
| def toggleGenerateTestCondition(self): |
| ''' |
| Toggles generating test condition |
| ''' |
| |
| if self.vc is None: |
| self.toast('Test conditions can be generated when a back-end is defined') |
| return |
| if self.isGeneratingTestCondition: |
| self.finishGeneratingTestCondition() |
| else: |
| self.startGeneratingTestCondition() |
| |
| def onCtrlT(self, event): |
| if DEBUG: |
| print >> sys.stderr, "onCtrlT()" |
| if self.vc is None: |
| self.toast('Test conditions can be generated when a back-end is defined') |
| return |
| # FIXME: This is only valid if we are generating a test case |
| self.toggleGenerateTestCondition() |
| |
| def onCtrlU(self, event): |
| if DEBUG: |
| print >> sys.stderr, "onCtrlU()" |
| |
| def onCtrlV(self, event): |
| if DEBUG: |
| print >> sys.stderr, "onCtrlV()" |
| self.printOperation(None, Operation.TRAVERSE) |
| |
| def toggleTargetZones(self): |
| self.toggleTargets() |
| self.canvas.update_idletasks() |
| |
| def onCtrlZ(self, event): |
| if DEBUG: |
| print >> sys.stderr, "onCtrlZ()" |
| self.toggleTargetZones() |
| |
| def showControlPanel(self): |
| from com.dtmilano.android.controlpanel import ControlPanel |
| |
| self.controlPanel = ControlPanel(self, self.printOperation) |
| |
| def onCtrlK(self, event): |
| self.showControlPanel() |
| |
| def drag(self, start, end, duration, steps, units=Unit.DIP): |
| self.showVignette() |
| # the operation on this current device is always done in PX |
| # so let's do it before any conversion takes place |
| self.device.drag(start, end, duration, steps) |
| if units == Unit.DIP: |
| x0 = round(start[0] / self.device.display['density'], 2) |
| y0 = round(start[1] / self.device.display['density'], 2) |
| x1 = round(end[0] / self.device.display['density'], 2) |
| y1 = round(end[1] / self.device.display['density'], 2) |
| start = (x0, y0) |
| end = (x1, y1) |
| if self.vc.uiAutomatorHelper: |
| self.printOperation(None, Operation.SWIPE_UI_AUTOMATOR_HELPER, x0, y0, x1, y1, steps, units, |
| self.device.display['orientation']) |
| else: |
| self.printOperation(None, Operation.DRAG, start, end, duration, steps, units, |
| self.device.display['orientation']) |
| self.printOperation(None, Operation.SLEEP, 1) |
| time.sleep(1) |
| self.takeScreenshotAndShowItOnWindow() |
| |
| |
| def enableEvents(self): |
| if self.permanentlyDisableEvents: |
| return |
| self.canvas.update_idletasks() |
| self.canvas.bind("<Button-1>", self.onButton1Pressed) |
| self.canvas.bind("<Control-Button-1>", self.onCtrlButton1Pressed) |
| self.canvas.bind("<Button-2>", self.onButton2Pressed) |
| self.canvas.bind("<Button-3>", self.onButton3Pressed) |
| self.canvas.bind("<BackSpace>", self.onKeyPressed) |
| # self.canvas.bind("<Control-Key-S>", self.onCtrlS) |
| self.canvas.bind("<Key>", self.onKeyPressed) |
| self.areEventsDisabled = False |
| |
| |
| def disableEvents(self, permanently=False): |
| self.permanentlyDisableEvents = permanently |
| if self.canvas is not None: |
| self.canvas.update_idletasks() |
| self.areEventsDisabled = True |
| self.canvas.unbind("<Button-1>") |
| self.canvas.unbind("<Control-Button-1>") |
| self.canvas.unbind("<Button-2>") |
| self.canvas.unbind("<Button-3>") |
| self.canvas.unbind("<BackSpace>") |
| # self.canvas.unbind("<Control-Key-S>") |
| self.canvas.unbind("<Key>") |
| |
| |
| def toggleTargets(self): |
| if DEBUG: |
| print >> sys.stderr, "toggletargets: aretargetsmarked=", self.areTargetsMarked |
| if not self.areTargetsMarked: |
| self.markTargets() |
| else: |
| self.unmarkTargets() |
| |
| |
| def markTargets(self): |
| if DEBUG: |
| print >> sys.stderr, "marktargets: aretargetsmarked=", self.areTargetsMarked |
| print >> sys.stderr, " marktargets: targets=", self.targets |
| colors = ["#ff00ff", "#ffff00", "#00ffff"] |
| |
| self.markedTargetIds = {} |
| c = 0 |
| for (x1, y1, x2, y2) in self.targets: |
| if DEBUG: |
| print "adding rectangle:", x1, y1, x2, y2 |
| self.markTarget(x1, y1, x2, y2, colors[c % len(colors)]) |
| c += 1 |
| self.areTargetsMarked = True |
| |
| |
| def markTarget(self, x1, y1, x2, y2, color='#ff00ff'): |
| ''' |
| @return the id of the rectangle added |
| ''' |
| |
| # self.areTargetsMarked = True |
| _id = self.canvas.create_rectangle(x1 * self.scale, y1 * self.scale, x2 * self.scale, y2 * self.scale, |
| fill=color, |
| stipple="gray25") |
| self.markedTargetIds[_id] = (x1, y1, x2, y2) |
| return _id |
| |
| |
| def unmarkTarget(self, _id): |
| self.canvas.delete(_id) |
| |
| |
| def unmarkTargets(self): |
| if not self.areTargetsMarked: |
| return |
| for _id in self.markedTargetIds: |
| self.unmarkTarget(_id) |
| self.markedTargetIds = {} |
| self.areTargetsMarked = False |
| |
| |
| def setDragDialogShowed(self, showed): |
| self.isDragDialogShowed = showed |
| if showed: |
| pass |
| else: |
| self.isGrabbingTouch = False |
| |
| |
| def drawTouchedPoint(self, x, y): |
| if DEBUG: |
| print >> sys.stderr, "drawTouchedPoint(", x, ",", y, ")" |
| size = 50 |
| return self.canvas.create_oval((x - size) * self.scale, (y - size) * self.scale, (x + size) * self.scale, |
| (y + size) * self.scale, fill=Color.MAGENTA) |
| |
| |
| def drawDragLine(self, x0, y0, x1, y1): |
| if DEBUG: |
| print >> sys.stderr, "drawDragLine(", x0, ",", y0, ",", x1, ",", y1, ")" |
| width = 15 |
| return self.canvas.create_line(x0 * self.scale, y0 * self.scale, x1 * self.scale, y1 * self.scale, width=width, |
| fill=Color.MAGENTA, arrow="last", arrowshape=(50, 50, 30), dash=(50, 25)) |
| |
| |
| def executeCommandAndRefresh(self, command): |
| self.showVignette() |
| if DEBUG: |
| print >> sys.stderr, 'DEBUG: command=', command, command.__name__ |
| print >> sys.stderr, 'DEBUG: command=', command.__self__, command.__self__.view |
| try: |
| view = command.__self__.view |
| except AttributeError: |
| view = None |
| # FIXME: If we are not dumping the Views and assigning to variables (i.e -u was used on command line) then |
| # when we try to do an operation on the View via its variable name it's going to fail when the saved script |
| # is executed |
| self.printOperation(view, Operation.fromCommandName(command.__name__)) |
| command() |
| self.printOperation(None, Operation.SLEEP, Operation.DEFAULT) |
| self.vc.sleep(5) |
| # FIXME: perhaps refresh() should be invoked here just in case size or orientation changed |
| self.takeScreenshotAndShowItOnWindow() |
| |
| |
| def changeLanguage(self): |
| code = tkSimpleDialog.askstring("Change language", "Enter the language code") |
| self.vc.uiDevice.changeLanguage(code) |
| self.printOperation(None, Operation.CHANGE_LANGUAGE, code) |
| self.refresh() |
| |
| |
| def setOnTouchListener(self, listener): |
| self.onTouchListener = listener |
| |
| |
| def setGrab(self, state): |
| if DEBUG: |
| print >> sys.stderr, "Culebron.setGrab(%s)" % state |
| if state and not self.onTouchListener: |
| warnings.warn('Starting to grab but no onTouchListener') |
| self.isGrabbingTouch = state |
| if state: |
| self.toast('Grabbing drag points...', background=Color.GREEN) |
| else: |
| self.hideMessageArea() |
| |
| |
| @staticmethod |
| def isClickableCheckableOrFocusable(v): |
| if DEBUG_ISCCOF: |
| print >> sys.stderr, "isClickableCheckableOrFocusable(", v.__tinyStr__(), ")" |
| try: |
| if not v.isEnabled(): |
| # if not enabled, then it cannot be a target |
| return False |
| except AttributeError: |
| pass |
| try: |
| return v.isClickable() |
| except AttributeError: |
| pass |
| try: |
| return v.isCheckable() |
| except AttributeError: |
| pass |
| try: |
| return v.isFocusable() |
| except AttributeError: |
| pass |
| return False |
| |
| |
| def mainloop(self): |
| self.window.title("%s v%s" % (Culebron.APPLICATION_NAME, __version__)) |
| self.window.resizable(width=Tkinter.FALSE, height=Tkinter.FALSE) |
| self.window.lift() |
| if self.concertina: |
| self.concertinaLoop() |
| else: |
| self.window.mainloop() |
| |
| |
| def concertinaLoop(self): |
| random.seed() |
| self.disableEvents(permanently=True) |
| self.concertinaLoopCallback(dontinteract=True) |
| self.window.mainloop() |
| |
| |
| def concertinaLoopCallback(self, dontinteract=False): |
| if not dontinteract: |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "CONCERTINA: should select one of these targets:" |
| for v in self.targetViews: |
| print >> sys.stderr, " ", unicode(v.__tinyStr__()) |
| rand = random.random() |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "CONCERTINA: random=%f" % rand |
| if rand > 0.85: |
| # Send key events |
| k = random.choice(['ENTER', 'BACK', 'HOME', 'MENU']) |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "CONCERTINA: key=" + k |
| # DEBUG ONLY! |
| # print >> sys.stderr, "Not sending key event" |
| self.command(k) |
| else: |
| # Act on views |
| _len = len(self.targetViews) |
| if _len > 0: |
| i = random.randrange(len(self.targetViews)) |
| target = self.targetViews[i] |
| z = self.targets[i] |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "CONCERTINA: selected", unicode(target.__smallStr__()) |
| print >> sys.stderr, "CONCERTINA: selected", z |
| _id = self.markTarget(*z) |
| self.window.update_idletasks() |
| time.sleep(1) |
| self.unmarkTarget(_id) |
| self.window.update_idletasks() |
| clazz = target.getClass() |
| parent = target.getParent() |
| if parent: |
| parentClass = parent.getClass() |
| else: |
| parentClass = None |
| isScrollable = target.isScrollable() |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "CONCERTINA: is scrollable: ", isScrollable |
| if parent: |
| print >> sys.stderr, "CONCERTINA: is scrollable parent: ", parent.isScrollable() |
| # cond = (isScrollable or parent.isScrollable() or parentClass == 'android.widget.ScrollView') |
| # DEBUG ONLY! |
| # print >> sys.stderr, "CONCERTINA: check:", cond |
| # if not cond: |
| # self.window.after(500, self.concertinaLoopCallback) |
| # return |
| if clazz == 'android.widget.EditText': |
| id = target.getId() |
| txt = target.getText() |
| if target.isPassword() or re.search('password', id, re.IGNORECASE) or re.search('password', txt, |
| re.IGNORECASE): |
| text = Concertina.getRandomPassword() |
| elif re.search('email', id, re.IGNORECASE) or re.search('email', txt, re.IGNORECASE): |
| text = Concertina.getRandomEmail() |
| else: |
| text = Concertina.getRandomText() |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "Entering text: ", text |
| if not text: |
| raise RuntimeError('text is None') |
| self.setText(target, text) |
| elif target.getContentDescription() in ['Voice Search', 'Tap to speak']: |
| Concertina.sayRandomText() |
| time.sleep(5) |
| elif random.choice(['SCROLL', 'TOUCH']) == 'SCROLL' and ( |
| isScrollable or parent.isScrollable() or parentClass == 'android.widget.ScrollView'): |
| # NOTE: The order here is important because some EditText are inside ScrollView's and we want to |
| # capture the case of other ScrollViews |
| if isScrollable: |
| ((l, t), (r, b)) = target.getBounds() |
| else: |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "CONCERTINA: using parent bounds because it's scrollable" |
| ((l, t), (r, b)) = parent.getBounds() |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "CONCERTINA: bounds=", ((l, t), (r, b)) |
| if random.choice(['VERTICAL', 'HORIZONTAL']) == 'VERTICAL': |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, 'CONCERTINA: VERTICAL' |
| sp = (l + (r - l) / 2, t + 50) |
| ep = (l + (r - l) / 2, b - 50) |
| else: |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, 'CONCERTINA: HORIZONTAL' |
| sp = (l + 50, t + (b - t) / 2) |
| ep = (r - 50, t + (b - t) / 2) |
| if random.choice(['FORWARD', 'REVERSE']) == 'REVERSE': |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, 'CONCERTINA: REVERSE' |
| temp = sp |
| sp = ep |
| ep = temp |
| else: |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, 'CONCERTINA: FORWARD' |
| d = 500 |
| s = 20 |
| _id = self.canvas.create_rectangle(l * self.scale, t * self.scale, r * self.scale, |
| b * self.scale, |
| fill="#00ffff", stipple="gray12") |
| self.window.update_idletasks() |
| units = Unit.PX |
| self.drawTouchedPoint(sp[0], sp[1]) |
| self.window.update_idletasks() |
| self.drawDragLine(sp[0], sp[1], ep[0], ep[1]) |
| self.window.update_idletasks() |
| time.sleep(5) |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "CONCERTINA: dragging %s %s %s %s %s" % (sp, ep, d, s, units) |
| self.drag(sp, ep, d, s, units) |
| else: |
| self.touchView(target) |
| self.printOperation(None, Operation.SLEEP, Operation.DEFAULT) |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "CONCERTINA: waiting 5 secs" |
| time.sleep(5) |
| if DEBUG_CONCERTINA: |
| print >> sys.stderr, "CONCERTINA: taking screenshot" |
| self.takeScreenshotAndShowItOnWindow() |
| else: |
| print >> sys.stderr, "CONCERTINA: No target views" |
| self.window.after(5000, self.concertinaLoopCallback) |
| |
| |
| def getViewContainingPointAndLongTouch(self, x, y): |
| # FIXME: this method is almost exactly as getViewContainingPointAndTouch() |
| if DEBUG: |
| print >> sys.stderr, 'getViewContainingPointAndLongTouch(%d, %d)' % (x, y) |
| if self.areEventsDisabled: |
| if DEBUG: |
| print >> sys.stderr, "Ignoring event" |
| self.canvas.update_idletasks() |
| return |
| |
| self.showVignette() |
| if DEBUG_POINT: |
| print >> sys.stderr, "getViewsContainingPointAndLongTouch(x=%s, y=%s)" % (x, y) |
| print >> sys.stderr, "self.vc=", self.vc |
| v = self.findViewContainingPointInTargets(x, y) |
| |
| if v is None: |
| # FIXME: We can touch by DIP by default if no Views were found |
| self.hideVignette() |
| msg = "There are no touchable or clickable views here!" |
| self.toast(msg) |
| return |
| |
| clazz = v.getClass() |
| candidates = [v] |
| |
| def findBestCandidate(view): |
| isccf = Culebron.isClickableCheckableOrFocusable(view) |
| cd = view.getContentDescription() |
| text = view.getText() |
| if (cd or text) and not isccf: |
| # because isccf==False this view was not added to the list of targets |
| # (i.e. Settings) |
| candidates.insert(0, view) |
| return None |
| |
| if not (v.getText() or v.getContentDescription()) and v.getChildren(): |
| self.vc.traverse(root=v, transform=findBestCandidate, stream=None) |
| if len(candidates) > 2: |
| warnings.warn("We are in trouble, we have more than one candidate to touch", stacklevel=0) |
| candidate = candidates[0] |
| self.longTouchView(candidate, v if candidate != v else None) |
| |
| self.printOperation(None, Operation.SLEEP, Operation.DEFAULT) |
| self.vc.sleep(5) |
| self.takeScreenshotAndShowItOnWindow() |
| |
| |
| if TKINTER_AVAILABLE: |
| class MainMenu(Tkinter.Menu): |
| def __init__(self, culebron): |
| Tkinter.Menu.__init__(self, culebron.window) |
| self.culebron = culebron |
| |
| self.fileMenu = Tkinter.Menu(self, tearoff=False) |
| self.fileMenu.add_command(label="Quit", underline=0, accelerator='Command-Q', command=self.culebron.quit) |
| self.add_cascade(label="File", underline=0, menu=self.fileMenu) |
| |
| self.viewMenu = Tkinter.Menu(self, tearoff=False) |
| self.showViewTree = Tkinter.BooleanVar() |
| self.showViewTree.set(False) |
| state = NORMAL if culebron.vc else DISABLED |
| self.viewMenu.add_checkbutton(label="Tree", underline=0, accelerator='Command-T', onvalue=True, |
| offvalue=False, variable=self.showViewTree, state=state, |
| command=self.onshowViewTreeChanged) |
| self.showViewDetails = Tkinter.BooleanVar() |
| self.showViewDetails.set(False) |
| state = NORMAL if culebron.vc else DISABLED |
| self.viewMenu.add_checkbutton(label="View details", underline=0, accelerator='Command-V', onvalue=True, |
| offvalue=False, variable=self.showViewDetails, state=state, |
| command=self.onShowViewDetailsChanged) |
| self.add_cascade(label="View", underline=0, menu=self.viewMenu) |
| |
| self.uiDeviceMenu = Tkinter.Menu(self, tearoff=False) |
| state = NORMAL if culebron.vc else DISABLED |
| self.uiDeviceMenu.add_command(label="Open Notification", underline=6, state=state, |
| command=lambda: culebron.executeCommandAndRefresh( |
| self.culebron.vc.uiDevice.openNotification)) |
| state = NORMAL if culebron.vc else DISABLED |
| self.uiDeviceMenu.add_command(label="Open Quick settings", underline=6, state=state, |
| command=lambda: culebron.executeCommandAndRefresh( |
| command=self.culebron.vc.uiDevice.openQuickSettings)) |
| state = NORMAL if culebron.vc else DISABLED |
| self.uiDeviceMenu.add_command(label="Change Language", underline=7, state=state, |
| command=self.culebron.changeLanguage) |
| self.add_cascade(label="UiDevice", menu=self.uiDeviceMenu) |
| |
| self.helpMenu = Tkinter.Menu(self, tearoff=False) |
| self.helpMenu.add_command(label="Keyboard shortcuts", underline=0, accelerator='Command-K', |
| command=self.culebron.showHelp) |
| self.add_cascade(label="Help", underline=0, menu=self.helpMenu) |
| |
| def callback(self): |
| pass |
| |
| def onshowViewTreeChanged(self): |
| if self.showViewTree.get() == 1: |
| self.culebron.showViewTree() |
| else: |
| self.culebron.hideViewTree() |
| |
| def onShowViewDetailsChanged(self): |
| if self.showViewDetails.get() == 1: |
| self.culebron.showViewDetails() |
| else: |
| self.culebron.hideViewDetails() |
| |
| |
| class ViewTree(Tkinter.Frame): |
| def __init__(self, parent): |
| Tkinter.Frame.__init__(self, parent) |
| self.viewTree = ttk.Treeview(self, columns=['T'], height=35) |
| self.viewTree.column(0, width=20) |
| self.viewTree.heading('#0', None, text='View', anchor=Tkinter.W) |
| self.viewTree.heading(0, None, text='T', anchor=Tkinter.W) |
| self.scrollbar = ttk.Scrollbar(self, orient=Tkinter.HORIZONTAL, command=self.__xscroll) |
| self.viewTree.grid(row=1, rowspan=1, column=1, sticky=Tkinter.N + Tkinter.S) |
| self.scrollbar.grid(row=2, rowspan=1, column=1, sticky=Tkinter.E + Tkinter.W) |
| self.viewTree.configure(xscrollcommand=self.scrollbar.set) |
| |
| def __xscroll(self, *args): |
| if DEBUG: |
| print >> sys.stderr, "__xscroll:", args |
| self.viewTree.xview(*args) |
| |
| def insert(self, parent, index, iid=None, **kw): |
| """Creates a new item and return the item identifier of the newly |
| created item. |
| |
| parent is the item ID of the parent item, or the empty string |
| to create a new top-level item. index is an integer, or the value |
| end, specifying where in the list of parent's children to insert |
| the new item. If index is less than or equal to zero, the new node |
| is inserted at the beginning, if index is greater than or equal to |
| the current number of children, it is inserted at the end. If iid |
| is specified, it is used as the item identifier, iid must not |
| already exist in the tree. Otherwise, a new unique identifier |
| is generated.""" |
| |
| return self.viewTree.insert(parent, index, iid, **kw) |
| |
| def set(self, item, column=None, value=None): |
| """With one argument, returns a dictionary of column/value pairs |
| for the specified item. With two arguments, returns the current |
| value of the specified column. With three arguments, sets the |
| value of given column in given item to the specified value.""" |
| |
| return self.viewTree.set(item, column, value) |
| |
| def tag_bind(self, tagname, sequence=None, callback=None): |
| if DEBUG: |
| print >> sys.stderr, 'ViewTree.tag_bind(', tagname, ',', sequence, ',', callback, ')' |
| return self.viewTree.tag_bind(tagname, sequence, callback) |
| |
| |
| class ViewDetails(Tkinter.Frame): |
| VIEW_DETAILS = "View Details:\n" |
| |
| def __init__(self, parent): |
| Tkinter.Frame.__init__(self, parent) |
| self.label = Tkinter.Label(self, bd=1, width=30, wraplength=200, justify=Tkinter.LEFT, anchor=Tkinter.NW) |
| self.label.configure(text=self.VIEW_DETAILS) |
| self.label.configure(bg="white") |
| self.label.grid(row=3, column=1, rowspan=1) |
| |
| def set(self, view): |
| self.label.configure(text=self.VIEW_DETAILS + view.__str__()) |
| |
| |
| class StatusBar(Tkinter.Frame): |
| |
| def __init__(self, parent): |
| Tkinter.Frame.__init__(self, parent) |
| self.label = Tkinter.Label(self, bd=1, relief=Tkinter.SUNKEN, anchor=Tkinter.W) |
| self.label.grid(row=1, column=1, columnspan=2, sticky=Tkinter.E + Tkinter.W) |
| |
| def set(self, fmt, *args): |
| self.label.config(text=fmt % args) |
| self.label.update_idletasks() |
| |
| def clear(self): |
| self.label.config(text="") |
| self.label.update_idletasks() |
| |
| |
| class LabeledEntry(): |
| def __init__(self, parent, text, validate, validatecmd): |
| self.f = Tkinter.Frame(parent) |
| Tkinter.Label(self.f, text=text, anchor="w", padx=8).grid(row=1, column=1, sticky=Tkinter.E) |
| self.entry = Tkinter.Entry(self.f, validate=validate, validatecommand=validatecmd) |
| self.entry.grid(row=1, column=2, padx=5, sticky=Tkinter.E) |
| |
| def grid(self, **kwargs): |
| self.f.grid(kwargs) |
| |
| def get(self): |
| return self.entry.get() |
| |
| def set(self, text): |
| self.entry.delete(0, Tkinter.END) |
| self.entry.insert(0, text) |
| |
| |
| class LabeledEntryWithButton(LabeledEntry): |
| def __init__(self, parent, text, buttonText, command, validate, validatecmd): |
| LabeledEntry.__init__(self, parent, text, validate, validatecmd) |
| self.button = Tkinter.Button(self.f, text=buttonText, command=command) |
| self.button.grid(row=1, column=3) |
| |
| |
| class DragDialog(Tkinter.Toplevel): |
| |
| DEFAULT_DURATION = 1000 |
| DEFAULT_STEPS = 20 |
| |
| spX = None |
| spY = None |
| epX = None |
| epY = None |
| spId = None |
| epId = None |
| |
| def __init__(self, culebron): |
| self.culebron = culebron |
| self.parent = culebron.window |
| Tkinter.Toplevel.__init__(self, self.parent) |
| self.transient(self.parent) |
| self.culebron.setDragDialogShowed(True) |
| self.title("Drag: selecting parameters") |
| |
| # valid percent substitutions (from the Tk entry man page) |
| # %d = Type of action (1=insert, 0=delete, -1 for others) |
| # %i = index of char string to be inserted/deleted, or -1 |
| # %P = value of the entry if the edit is allowed |
| # %s = value of entry prior to editing |
| # %S = the text string being inserted or deleted, if any |
| # %v = the type of validation that is currently set |
| # %V = the type of validation that triggered the callback |
| # (key, focusin, focusout, forced) |
| # %W = the tk name of the widget |
| self.validate = (self.parent.register(self.onValidate), '%P') |
| self.sp = LabeledEntryWithButton(self, "Start point", "Grab", command=self.onGrabSp, validate="focusout", |
| validatecmd=self.validate) |
| self.sp.grid(row=1, column=1, columnspan=3, pady=5) |
| |
| self.ep = LabeledEntryWithButton(self, "End point", "Grab", command=self.onGrabEp, validate="focusout", |
| validatecmd=self.validate) |
| self.ep.grid(row=2, column=1, columnspan=3, pady=5) |
| |
| l = Tkinter.Label(self, text="Units") |
| l.grid(row=3, column=1, sticky=Tkinter.E) |
| |
| self.units = Tkinter.StringVar() |
| self.units.set(Unit.DIP) |
| col = 2 |
| for u in dir(Unit): |
| if u.startswith('_'): |
| continue |
| rb = Tkinter.Radiobutton(self, text=u, variable=self.units, value=u) |
| rb.grid(row=3, column=col, padx=20, sticky=Tkinter.E) |
| col += 1 |
| |
| self.d = LabeledEntry(self, "Duration", validate="focusout", validatecmd=self.validate) |
| self.d.set(DragDialog.DEFAULT_DURATION) |
| self.d.grid(row=4, column=1, columnspan=3, pady=5) |
| |
| self.s = LabeledEntry(self, "Steps", validate="focusout", validatecmd=self.validate) |
| self.s.set(DragDialog.DEFAULT_STEPS) |
| self.s.grid(row=5, column=1, columnspan=2, pady=5) |
| |
| self.buttonBox() |
| |
| def buttonBox(self): |
| # add standard button box. override if you don't want the |
| # standard buttons |
| |
| box = Tkinter.Frame(self) |
| |
| self.ok = Tkinter.Button(box, text="OK", width=10, command=self.onOk, default=Tkinter.ACTIVE, |
| state=Tkinter.DISABLED) |
| self.ok.grid(row=6, column=1, sticky=Tkinter.E, padx=5, pady=5) |
| w = Tkinter.Button(box, text="Cancel", width=10, command=self.onCancel) |
| w.grid(row=6, column=2, sticky=Tkinter.E, padx=5, pady=5) |
| |
| self.bind("<Return>", self.onOk) |
| self.bind("<Escape>", self.onCancel) |
| |
| box.grid(row=6, column=1, columnspan=3) |
| |
| def onValidate(self, value): |
| if self.sp.get() and self.ep.get() and self.d.get() and self.s.get(): |
| self.ok.configure(state=Tkinter.NORMAL) |
| else: |
| self.ok.configure(state=Tkinter.DISABLED) |
| |
| def onOk(self, event=None): |
| if DEBUG: |
| print >> sys.stderr, "onOK()" |
| print >> sys.stderr, "values are:", |
| print >> sys.stderr, self.sp.get(), |
| print >> sys.stderr, self.ep.get(), |
| print >> sys.stderr, self.d.get(), |
| print >> sys.stderr, self.s.get(), |
| print >> sys.stderr, self.units.get() |
| |
| sp = make_tuple(self.sp.get()) |
| ep = make_tuple(self.ep.get()) |
| d = int(self.d.get()) |
| s = int(self.s.get()) |
| self.cleanUp() |
| # put focus back to the parent window's canvas |
| self.culebron.canvas.focus_set() |
| self.destroy() |
| self.culebron.drag(sp, ep, d, s, self.units.get()) |
| |
| def onCancel(self, event=None): |
| self.culebron.setGrab(False) |
| self.cleanUp() |
| # put focus back to the parent window's canvas |
| self.culebron.canvas.focus_set() |
| self.destroy() |
| |
| def onGrabSp(self): |
| ''' |
| Grab starting point |
| ''' |
| |
| self.sp.entry.focus_get() |
| self.onGrab(self.sp) |
| |
| def onGrabEp(self): |
| ''' |
| Grab ending point |
| ''' |
| |
| self.ep.entry.focus_get() |
| self.onGrab(self.ep) |
| |
| def onGrab(self, entry): |
| ''' |
| Generic grab method. |
| |
| @param entry: the entry being grabbed |
| @type entry: Tkinter.Entry |
| ''' |
| |
| self.culebron.setOnTouchListener(self.onTouchListener) |
| self.__grabbing = entry |
| self.culebron.setGrab(True) |
| |
| def onTouchListener(self, point): |
| ''' |
| Listens for touch events and draws the corresponding shapes on the Culebron canvas. |
| If the starting point is being grabbed it draws the touching point via |
| C{Culebron.drawTouchedPoint()} and if the end point is being grabbed it draws |
| using C{Culebron.drawDragLine()}. |
| |
| @param point: the point touched |
| @type point: tuple |
| ''' |
| |
| x = point[0] |
| y = point[1] |
| value = "(%d,%d)" % (x, y) |
| self.__grabbing.set(value) |
| self.onValidate(value) |
| self.culebron.setGrab(False) |
| if self.__grabbing == self.sp: |
| self.__cleanUpSpId() |
| self.__cleanUpEpId() |
| self.spX = x |
| self.spY = y |
| elif self.__grabbing == self.ep: |
| self.__cleanUpEpId() |
| self.epX = x |
| self.epY = y |
| if self.spX and self.spY and not self.spId: |
| self.spId = self.culebron.drawTouchedPoint(self.spX, self.spY) |
| if self.spX and self.spY and self.epX and self.epY and not self.epId: |
| self.epId = self.culebron.drawDragLine(self.spX, self.spY, self.epX, self.epY) |
| self.__grabbing = None |
| self.culebron.setOnTouchListener(None) |
| |
| def __cleanUpSpId(self): |
| if self.spId: |
| self.culebron.canvas.delete(self.spId) |
| self.spId = None |
| |
| def __cleanUpEpId(self): |
| if self.epId: |
| self.culebron.canvas.delete(self.epId) |
| self.epId = None |
| |
| def cleanUp(self): |
| self.__cleanUpSpId() |
| self.__cleanUpEpId() |
| |
| |
| class ContextMenu(Tkinter.Menu): |
| # FIXME: should get rid of the nested classes, otherwise it's not possible to create a parent class |
| # SubMenu for UiScrollableSubMenu |
| ''' |
| The context menu (popup). |
| ''' |
| |
| PADDING = ' ' |
| ''' Padding used to separate menu entries from border ''' |
| |
| class Separator(): |
| SEPARATOR = 'SEPARATOR' |
| |
| def __init__(self): |
| self.description = self.SEPARATOR |
| |
| class Command(): |
| def __init__(self, description, underline, shortcut, event, command): |
| self.description = description |
| self.underline = underline |
| self.shortcut = shortcut |
| self.event = event |
| self.command = command |
| |
| class UiScrollableSubMenu(Tkinter.Menu): |
| def __init__(self, menu, description, view, culebron): |
| # Tkninter.Menu is not extending object, so we can't do this: |
| # super(ContextMenu, self).__init__(culebron.window, tearoff=False) |
| Tkinter.Menu.__init__(self, menu, tearoff=False) |
| self.description = description |
| self.add_command(label='Fling backward', |
| command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingBackward)) |
| self.add_command(label='Fling forward', |
| command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingForward)) |
| self.add_command(label='Fling to beginning', |
| command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingToBeginning)) |
| self.add_command(label='Fling to end', |
| command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingToEnd)) |
| |
| def __init__(self, culebron, view): |
| # Tkninter.Menu is not extending object, so we can't do this: |
| # super(ContextMenu, self).__init__(culebron.window, tearoff=False) |
| Tkinter.Menu.__init__(self, culebron.window, tearoff=False) |
| if DEBUG_CONTEXT_MENU: |
| print >> sys.stderr, "Creating ContextMenu for", view.__smallStr__() if view else "No View" |
| self.view = view |
| items = [] |
| |
| if self.view: |
| _saveViewSnapshotForSelectedView = lambda: culebron.saveViewSnapshot(self.view) |
| items.append(ContextMenu.Command('Take view snapshot and save to file', 5, 'Ctrl+W', '<Control-W>', |
| _saveViewSnapshotForSelectedView)) |
| if self.view.uiScrollable: |
| items.append(ContextMenu.UiScrollableSubMenu(self, 'UiScrollable', view, culebron)) |
| else: |
| parent = self.view.parent |
| while parent: |
| if parent.uiScrollable: |
| # WARNING: |
| # A bit dangerous, but may work |
| # If we click ona ListView then the View pased to this ContextMenu is the child, |
| # perhaps we want to scroll the parent |
| items.append(ContextMenu.UiScrollableSubMenu(self, 'UiScrollable', parent, culebron)) |
| break |
| parent = parent.parent |
| items.append(ContextMenu.Separator()) |
| |
| items.append(ContextMenu.Command('Drag dialog', 0, 'Ctrl+D', '<Control-D>', culebron.showDragDialog)) |
| items.append(ContextMenu.Command('Take snapshot and save to file', 26, 'Ctrl+F', '<Control-F>', |
| culebron.saveSnapshot)) |
| items.append(ContextMenu.Command('Control Panel', 0, 'Ctrl+K', '<Control-K>', culebron.showControlPanel)) |
| items.append(ContextMenu.Command('Long touch point using PX', 0, 'Ctrl+L', '<Control-L>', |
| culebron.toggleLongTouchPoint)) |
| items.append(ContextMenu.Command('Long touch View', 0, None, None, |
| culebron.toggleLongTouchView)) |
| items.append( |
| ContextMenu.Command('Touch using DIP', 13, 'Ctrl+I', '<Control-I>', culebron.toggleTouchPointDip)) |
| items.append( |
| ContextMenu.Command('Touch using PX', 12, 'Ctrl+P', '<Control-P>', culebron.toggleTouchPointPx)) |
| items.append(ContextMenu.Command('Generates a Sleep() on output script', 12, 'Ctrl+S', '<Control-S>', |
| culebron.showSleepDialog)) |
| if culebron.vc is not None: |
| items.append(ContextMenu.Command('Toggle generating Test Condition', 18, 'Ctrl+T', '<Control-T>', |
| culebron.toggleGenerateTestCondition)) |
| items.append(ContextMenu.Command('Touch Zones', 6, 'Ctrl+Z', '<Control-Z>', culebron.toggleTargetZones)) |
| items.append(ContextMenu.Command('Generates a startActivity()', 17, 'Ctrl+A', '<Control-A>', |
| culebron.printStartActivityAtTop)) |
| items.append(ContextMenu.Command('Refresh', 0, 'F5', '<F5>', culebron.refresh)) |
| items.append(ContextMenu.Command('Wake up', 0, None, None, culebron.wake)) |
| items.append(ContextMenu.Separator()) |
| items.append(ContextMenu.Command('Quit', 0, 'Ctrl+Q', '<Control-Q>', culebron.quit)) |
| |
| for item in items: |
| self.addItem(item) |
| |
| def addItem(self, item): |
| if isinstance(item, ContextMenu.Separator): |
| self.addSeparator() |
| elif isinstance(item, ContextMenu.Command): |
| self.addCommand(item) |
| elif isinstance(item, ContextMenu.UiScrollableSubMenu): |
| self.addSubMenu(item) |
| else: |
| raise RuntimeError("Unsupported item=" + str(item)) |
| |
| def addSeparator(self): |
| self.add_separator() |
| |
| def addCommand(self, item): |
| self.add_command(label=self.PADDING + item.description, underline=item.underline + len(self.PADDING), |
| command=item.command, accelerator=item.shortcut) |
| # if item.event: |
| # # These bindings remain even after the menu has been dismissed, so it seems not a good idea |
| # #self.bind_all(item.event, item.command) |
| # pass |
| |
| def addSubMenu(self, item): |
| self.add_cascade(label=self.PADDING + item.description, menu=item) |
| |
| def showPopupMenu(self, event): |
| try: |
| self.tk_popup(event.x_root, event.y_root) |
| finally: |
| # make sure to release the grab (Tk 8.0a1 only) |
| # self.grab_release() |
| pass |
| |
| |
| class HelpDialog(Tkinter.Toplevel): |
| |
| def __init__(self, culebron): |
| self.culebron = culebron |
| self.parent = culebron.window |
| Tkinter.Toplevel.__init__(self, self.parent) |
| # self.transient(self.parent) |
| self.title("%s: help" % Culebron.APPLICATION_NAME) |
| |
| self.text = ScrolledText.ScrolledText(self, width=60, height=40) |
| self.text.insert(Tkinter.INSERT, ''' |
| Special keys |
| ------------ |
| |
| F1: Help |
| F5: Refresh |
| |
| Mouse Buttons |
| ------------- |
| <1>: Touch the underlying View |
| |
| Commands |
| -------- |
| Ctrl-A: Generates startActivity() call on output script |
| Ctrl-D: Drag dialog |
| Ctrl-F: Take snapshot and save to file |
| Ctrl-K: Control Panel |
| Ctrl-L: Long touch point using PX |
| Ctrl-I: Touch using DIP |
| Ctrl-P: Touch using PX |
| Ctrl-Q: Quit |
| Ctrl-S: Generates a sleep() on output script |
| Ctrl-T: Toggle generating test condition |
| Ctrl-V: Verifies the content of the screen dump |
| Ctrl-Z: Touch zones |
| ''') |
| self.text.grid(row=1, column=1) |
| |
| self.buttonBox() |
| |
| def buttonBox(self): |
| # add standard button box. override if you don't want the |
| # standard buttons |
| |
| box = Tkinter.Frame(self) |
| |
| w = Tkinter.Button(box, text="Dismiss", width=10, command=self.onDismiss, default=Tkinter.ACTIVE) |
| w.grid(row=1, column=1, padx=5, pady=5) |
| |
| self.bind("<Return>", self.onDismiss) |
| self.bind("<Escape>", self.onDismiss) |
| |
| box.grid(row=1, column=1) |
| |
| def onDismiss(self, event=None): |
| # put focus back to the parent window's canvas |
| self.culebron.canvas.focus_set() |
| self.destroy() |
| |
| |
| class FileDialog(): |
| def __init__(self, culebron, filename): |
| self.parent = culebron.window |
| self.filename = filename |
| self.basename = os.path.basename(self.filename) |
| self.dirname = os.path.dirname(self.filename) |
| self.ext = os.path.splitext(self.filename)[1] |
| self.fileTypes = [('images', self.ext)] |
| |
| def askSaveAsFilename(self): |
| return tkFileDialog.asksaveasfilename(parent=self.parent, filetypes=self.fileTypes, |
| defaultextension=self.ext, initialdir=self.dirname, |
| initialfile=self.basename) |