culebron.py 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644
  1. # -*- coding: utf-8 -*-
  2. '''
  3. Copyright (C) 2012-2015 Diego Torres Milano
  4. Created on oct 6, 2014
  5. Licensed under the Apache License, Version 2.0 (the "License");
  6. you may not use this file except in compliance with the License.
  7. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. @author: Diego Torres Milano
  15. '''
  16. __version__ = '10.3.0'
  17. import sys
  18. import threading
  19. import warnings
  20. import copy
  21. import string
  22. import os
  23. import platform
  24. from __builtin__ import False
  25. from pkg_resources import Requirement, resource_filename
  26. try:
  27. from PIL import Image, ImageTk
  28. PIL_AVAILABLE = True
  29. except:
  30. PIL_AVAILABLE = False
  31. try:
  32. import Tkinter
  33. import tkSimpleDialog
  34. import tkFileDialog
  35. import tkFont
  36. import ScrolledText
  37. import ttk
  38. from Tkconstants import DISABLED, NORMAL
  39. TKINTER_AVAILABLE = True
  40. except:
  41. TKINTER_AVAILABLE = False
  42. from ast import literal_eval as make_tuple
  43. DEBUG = False
  44. DEBUG_MOVE = DEBUG and False
  45. DEBUG_TOUCH = DEBUG and False
  46. DEBUG_POINT = DEBUG and False
  47. DEBUG_KEY = DEBUG and False
  48. DEBUG_ISCCOF = DEBUG and False
  49. DEBUG_FIND_VIEW = DEBUG and False
  50. DEBUG_CONTEXT_MENU = DEBUG and False
  51. class Color:
  52. GOLD = '#d19615'
  53. GREEN = '#15d137'
  54. BLUE = '#1551d1'
  55. MAGENTA = '#d115af'
  56. DARK_GRAY = '#222222'
  57. LIGHT_GRAY = '#dddddd'
  58. class Unit:
  59. PX = 'PX'
  60. DIP = 'DIP'
  61. class Operation:
  62. ASSIGN = 'assign'
  63. CHANGE_LANGUAGE = 'change_language'
  64. DEFAULT = 'default'
  65. DRAG = 'drag'
  66. DUMP = 'dump'
  67. FLING_BACKWARD = 'fling_backward'
  68. FLING_FORWARD = 'fling_forward'
  69. FLING_TO_BEGINNING = 'fling_to_beginning'
  70. FLING_TO_END = 'fling_to_end'
  71. TEST = 'test'
  72. TEST_TEXT = 'test_text'
  73. TOUCH_VIEW = 'touch_view'
  74. TOUCH_POINT = 'touch_point'
  75. LONG_TOUCH_POINT = 'long_touch_point'
  76. OPEN_NOTIFICATION = 'open_notification'
  77. OPEN_QUICK_SETTINGS = 'open_quick_settings'
  78. TYPE = 'type'
  79. PRESS = 'press'
  80. SNAPSHOT = 'snapshot'
  81. SLEEP = 'sleep'
  82. TRAVERSE = 'traverse'
  83. VIEW_SNAPSHOT = 'view_snapshot'
  84. @staticmethod
  85. def fromCommandName(commandName):
  86. MAP = {'flingBackward': Operation.FLING_BACKWARD, 'flingForward': Operation.FLING_FORWARD,
  87. 'flingToBeginning': Operation.FLING_TO_BEGINNING, 'flingToEnd': Operation.FLING_TO_END,
  88. 'openNotification': Operation.OPEN_NOTIFICATION, 'openQuickSettings': Operation.OPEN_QUICK_SETTINGS,
  89. }
  90. return MAP[commandName]
  91. class Culebron:
  92. APPLICATION_NAME = "Culebra"
  93. UPPERCASE_CHARS = string.uppercase[:26]
  94. KEYSYM_TO_KEYCODE_MAP = {
  95. 'Home': 'HOME',
  96. 'BackSpace': 'BACK',
  97. 'Left': 'DPAD_LEFT',
  98. 'Right': 'DPAD_RIGHT',
  99. 'Up': 'DPAD_UP',
  100. 'Down': 'DPAD_DOWN',
  101. }
  102. KEYSYM_CULEBRON_COMMANDS = {
  103. 'F1': None,
  104. 'F5': None
  105. }
  106. canvas = None
  107. imageId = None
  108. vignetteId = None
  109. areTargetsMarked = False
  110. isDragDialogShowed = False
  111. isGrabbingTouch = False
  112. isGeneratingTestCondition = False
  113. isTouchingPoint = False
  114. isLongTouchingPoint = False
  115. onTouchListener = None
  116. snapshotDir = '/tmp'
  117. snapshotFormat = 'PNG'
  118. deviceArt = None
  119. dropShadow = False
  120. screenGlare = False
  121. @staticmethod
  122. def checkSupportedSdkVersion(sdkVersion):
  123. if sdkVersion <= 10:
  124. raise Exception('''culebra GUI requires Android API > 10 to work''')
  125. @staticmethod
  126. def checkDependencies():
  127. if not PIL_AVAILABLE:
  128. raise Exception('''PIL or Pillow is needed for GUI mode
  129. On Ubuntu install
  130. $ sudo apt-get install python-imaging python-imaging-tk
  131. On OSX install
  132. $ brew install homebrew/python/pillow
  133. ''')
  134. if not TKINTER_AVAILABLE:
  135. raise Exception('''Tkinter is needed for GUI mode
  136. This is usually installed by python package. Check your distribution details.
  137. ''')
  138. def __init__(self, vc, printOperation, scale=1):
  139. '''
  140. Culebron constructor.
  141. @param vc: The ViewClient used by this Culebron instance
  142. @type vc: ViewClient
  143. @param printOperation: the method invoked to print operations to the script
  144. @type printOperation: method
  145. @param scale: the scale of the device screen used to show it on the window
  146. @type scale: float
  147. '''
  148. self.vc = vc
  149. self.printOperation = printOperation
  150. self.device = vc.device
  151. self.serialno = vc.serialno
  152. self.scale = scale
  153. self.window = Tkinter.Tk()
  154. icon = resource_filename(Requirement.parse("androidviewclient"),
  155. "share/pixmaps/culebra.png")
  156. self.window.tk.call('wm', 'iconphoto', self.window._w,
  157. ImageTk.PhotoImage(file=icon))
  158. self.mainMenu = MainMenu(self)
  159. self.window.config(menu=self.mainMenu)
  160. self.mainFrame = Tkinter.Frame(self.window)
  161. self.placeholder = Tkinter.Frame(self.mainFrame, width=400, height=400, background=Color.LIGHT_GRAY)
  162. self.placeholder.grid(row=1, column=1, rowspan=4)
  163. self.sideFrame = Tkinter.Frame(self.window)
  164. self.viewTree = ViewTree(self.sideFrame)
  165. self.viewDetails = ViewDetails(self.sideFrame)
  166. self.mainFrame.grid(row=1, column=1, columnspan=1, rowspan=4, sticky=Tkinter.N+Tkinter.S)
  167. self.isSideFrameShown = False
  168. self.isViewTreeShown = False
  169. self.isViewDetailsShown = False
  170. self.statusBar = StatusBar(self.window)
  171. self.statusBar.grid(row=5, column=1, columnspan=2)
  172. self.statusBar.set("Always press F1 for help")
  173. self.window.update_idletasks()
  174. self.targetIds = []
  175. if DEBUG:
  176. self.printGridInfo()
  177. def printGridInfo(self):
  178. print >> sys.stderr, "window:", repr(self.window)
  179. print >> sys.stderr, "main:", repr(self.mainFrame)
  180. print >> sys.stderr, "main:", self.mainFrame.grid_info()
  181. print >> sys.stderr, "side:", repr(self.sideFrame)
  182. print >> sys.stderr, "side:", self.sideFrame.grid_info()
  183. print >> sys.stderr, "tree:", repr(self.viewTree)
  184. print >> sys.stderr, "tree:", self.viewTree.grid_info()
  185. print >> sys.stderr, "details:", repr(self.viewDetails)
  186. print >> sys.stderr, "details:", self.viewDetails.grid_info()
  187. def takeScreenshotAndShowItOnWindow(self):
  188. '''
  189. Takes the current screenshot and shows it on the main window.
  190. It also:
  191. - sizes the window
  192. - create the canvas
  193. - set the focus
  194. - enable the events
  195. - create widgets
  196. - finds the targets (as explained in L{findTargets})
  197. - hides the vignette (that could have been showed before)
  198. '''
  199. if DEBUG:
  200. print >> sys.stderr, "takeScreenshotAndShowItOnWindow()"
  201. self.unscaledScreenshot = self.device.takeSnapshot(reconnect=True)
  202. self.image = self.unscaledScreenshot
  203. (width, height) = self.image.size
  204. if self.scale != 1:
  205. self.image = self.image.resize((int(width*self.scale), int(height*self.scale)), Image.ANTIALIAS)
  206. (width, height) = self.image.size
  207. if self.canvas is None:
  208. if DEBUG:
  209. print >> sys.stderr, "Creating canvas", width, 'x', height
  210. self.placeholder.grid_forget()
  211. self.canvas = Tkinter.Canvas(self.mainFrame, width=width, height=height)
  212. self.canvas.focus_set()
  213. self.enableEvents()
  214. self.createMessageArea(width, height)
  215. self.createVignette(width, height)
  216. self.screenshot = ImageTk.PhotoImage(self.image)
  217. if self.imageId is not None:
  218. self.canvas.delete(self.imageId)
  219. self.imageId = self.canvas.create_image(0, 0, anchor=Tkinter.NW, image=self.screenshot)
  220. if DEBUG:
  221. try:
  222. print >> sys.stderr, "Grid info", self.canvas.grid_info()
  223. except:
  224. print >> sys.stderr, "Exception getting grid info"
  225. gridInfo = None
  226. try:
  227. gridInfo = self.canvas.grid_info()
  228. except:
  229. if DEBUG:
  230. print >> sys.stderr, "Adding canvas to grid (1,1)"
  231. self.canvas.grid(row=1, column=1, rowspan=4)
  232. if not gridInfo:
  233. self.canvas.grid(row=1, column=1, rowspan=4)
  234. self.findTargets()
  235. self.hideVignette()
  236. if DEBUG:
  237. self.printGridInfo()
  238. def createMessageArea(self, width, height):
  239. self.__message = Tkinter.Label(self.window, text='', background=Color.GOLD, font=('Helvetica', 16), anchor=Tkinter.W)
  240. self.__message.configure(width=width)
  241. self.__messageAreaId = self.canvas.create_window(0, 0, anchor=Tkinter.NW, window=self.__message)
  242. self.canvas.itemconfig(self.__messageAreaId, state='hidden')
  243. self.isMessageAreaVisible = False
  244. def showMessageArea(self):
  245. if self.__messageAreaId:
  246. self.canvas.itemconfig(self.__messageAreaId, state='normal')
  247. self.isMessageAreaVisible = True
  248. self.canvas.update_idletasks()
  249. def hideMessageArea(self):
  250. if self.__messageAreaId and self.isMessageAreaVisible:
  251. self.canvas.itemconfig(self.__messageAreaId, state='hidden')
  252. self.isMessageAreaVisible = False
  253. self.canvas.update_idletasks()
  254. def toggleMessageArea(self):
  255. if self.isMessageAreaVisible:
  256. self.hideMessageArea()
  257. else:
  258. self.showMessageArea()
  259. def message(self, text, background=None):
  260. self.__message.config(text=text)
  261. if background:
  262. self.__message.config(background=background)
  263. self.showMessageArea()
  264. def toast(self, text, background=None, timeout=5):
  265. if DEBUG:
  266. print >> sys.stderr, "toast(", text, ",", background, ")"
  267. self.message(text, background)
  268. if text:
  269. t = threading.Timer(timeout, self.hideMessageArea)
  270. t.start()
  271. else:
  272. self.hideMessageArea()
  273. def createVignette(self, width, height):
  274. if DEBUG:
  275. print >> sys.stderr, "createVignette(%d, %d)" % (width, height)
  276. self.vignetteId = self.canvas.create_rectangle(0, 0, width, height, fill=Color.MAGENTA,
  277. stipple='gray50')
  278. font = tkFont.Font(family='Helvetica',size=int(144*self.scale))
  279. msg = "Please\nwait..."
  280. self.waitMessageShadowId = self.canvas.create_text(width/2+2, height/2+2, text=msg,
  281. fill=Color.DARK_GRAY, font=font)
  282. self.waitMessageId = self.canvas.create_text(width/2, height/2, text=msg,
  283. fill=Color.LIGHT_GRAY, font=font)
  284. self.canvas.update_idletasks()
  285. def showVignette(self):
  286. if DEBUG:
  287. print >> sys.stderr, "showVignette()"
  288. if self.canvas is None:
  289. return
  290. if self.vignetteId:
  291. if DEBUG:
  292. print >> sys.stderr, " showing vignette"
  293. # disable events while we are processing one
  294. self.disableEvents()
  295. self.canvas.lift(self.vignetteId)
  296. self.canvas.lift(self.waitMessageShadowId)
  297. self.canvas.lift(self.waitMessageId)
  298. self.canvas.update_idletasks()
  299. def hideVignette(self):
  300. if DEBUG:
  301. print >> sys.stderr, "hideVignette()"
  302. if self.canvas is None:
  303. return
  304. if self.vignetteId:
  305. if DEBUG:
  306. print >> sys.stderr, " hiding vignette"
  307. self.canvas.lift(self.imageId)
  308. self.canvas.update_idletasks()
  309. self.enableEvents()
  310. def deleteVignette(self):
  311. if self.canvas is not None:
  312. self.canvas.delete(self.vignetteId)
  313. self.vignetteId = None
  314. self.canvas.delete(self.waitMessageShadowId)
  315. self.waitMessageShadowId = None
  316. self.canvas.delete(self.waitMessageId)
  317. self.waitMessageId = None
  318. def showPopupMenu(self, event):
  319. (scaledX, scaledY) = (event.x/self.scale, event.y/self.scale)
  320. v = self.findViewContainingPointInTargets(scaledX, scaledY)
  321. ContextMenu(self, view=v).showPopupMenu(event)
  322. def showHelp(self):
  323. d = HelpDialog(self)
  324. self.window.wait_window(d)
  325. def showSideFrame(self):
  326. if not self.isSideFrameShown:
  327. self.sideFrame.grid(row=1, column=2, rowspan=4, sticky=Tkinter.N+Tkinter.S)
  328. self.isSideFrameSown = True
  329. if DEBUG:
  330. self.printGridInfo()
  331. def hideSideFrame(self):
  332. self.sideFrame.grid_forget()
  333. self.isSideFrameShown = False
  334. if DEBUG:
  335. self.printGridInfo()
  336. def showViewTree(self):
  337. self.showSideFrame()
  338. self.viewTree.grid(row=1, column=1, rowspan=3, sticky=Tkinter.N+Tkinter.S)
  339. self.isViewTreeShown = True
  340. if DEBUG:
  341. self.printGridInfo()
  342. def hideViewTree(self):
  343. self.unmarkTargets()
  344. self.viewTree.grid_forget()
  345. self.isViewTreeShown = False
  346. if not self.isViewDetailsShown:
  347. self.hideSideFrame()
  348. if DEBUG:
  349. self.printGridInfo()
  350. def showViewDetails(self):
  351. self.showSideFrame()
  352. row = 4
  353. #if self.viewTree.grid_info() != {}:
  354. # row += 1
  355. self.viewDetails.grid(row=row, column=1, rowspan=1, sticky=Tkinter.S)
  356. self.isViewDetailsShown = True
  357. if DEBUG:
  358. self.printGridInfo()
  359. def hideViewDetails(self):
  360. self.viewDetails.grid_forget()
  361. self.isViewDetailsShown = False
  362. if not self.isViewTreeShown:
  363. self.hideSideFrame()
  364. if DEBUG:
  365. self.printGridInfo()
  366. def viewTreeItemClicked(self, event):
  367. if DEBUG:
  368. print >> sys.stderr, "viewTreeitemClicked:", event.__dict__
  369. self.unmarkTargets()
  370. vuid = self.viewTree.viewTree.identify_row(event.y)
  371. if vuid:
  372. view = self.vc.viewsById[vuid]
  373. if view:
  374. coords = view.getCoords()
  375. if view.isTarget():
  376. self.markTarget(coords[0][0], coords[0][1], coords[1][0], coords[1][1])
  377. self.viewDetails.set(view)
  378. def populateViewTree(self, view):
  379. '''
  380. Populates the View tree.
  381. '''
  382. vuid = view.getUniqueId()
  383. text = view.__smallStr__()
  384. if view.getParent() is None:
  385. self.viewTree.insert('', Tkinter.END, vuid, text=text)
  386. else:
  387. self.viewTree.insert(view.getParent().getUniqueId(), Tkinter.END, vuid, text=text, tags=('ttk'))
  388. self.viewTree.set(vuid, 'T', '*' if view.isTarget() else ' ')
  389. self.viewTree.tag_bind('ttk', '<1>', self.viewTreeItemClicked)
  390. def findTargets(self):
  391. '''
  392. Finds the target Views (i.e. for touches).
  393. '''
  394. if DEBUG:
  395. print >> sys.stderr, "findTargets()"
  396. LISTVIEW_CLASS = 'android.widget.ListView'
  397. ''' The ListView class name '''
  398. self.targets = []
  399. ''' The list of target coordinates (x1, y1, x2, y2) '''
  400. self.targetViews = []
  401. ''' The list of target Views '''
  402. if self.device.isKeyboardShown():
  403. print >> sys.stderr, "#### keyboard is show but handling it is not implemented yet ####"
  404. # FIXME: still no windows in uiautomator
  405. window = -1
  406. else:
  407. window = -1
  408. dump = self.vc.dump(window=window)
  409. self.printOperation(None, Operation.DUMP, window, dump)
  410. # the root element cannot be deleted from Treeview once added.
  411. # We have no option but to recreate it
  412. self.viewTree = ViewTree(self.sideFrame)
  413. for v in dump:
  414. if DEBUG:
  415. print >> sys.stderr, " findTargets: analyzing", v.getClass(), v.getId()
  416. if v.getClass() == LISTVIEW_CLASS:
  417. # We may want to touch ListView elements, not just the ListView
  418. continue
  419. parent = v.getParent()
  420. if (parent and parent.getClass() == LISTVIEW_CLASS and self.isClickableCheckableOrFocusable(parent)) \
  421. or self.isClickableCheckableOrFocusable(v):
  422. # If this is a touchable ListView, let's add its children instead
  423. # or add it if it's touchable, focusable, whatever
  424. ((x1, y1), (x2, y2)) = v.getCoords()
  425. if DEBUG:
  426. print >> sys.stderr, "appending target", ((x1, y1, x2, y2))
  427. v.setTarget(True)
  428. self.targets.append((x1, y1, x2, y2))
  429. self.targetViews.append(v)
  430. target = True
  431. else:
  432. target = False
  433. self.vc.traverse(transform=self.populateViewTree)
  434. def getViewContainingPointAndGenerateTestCondition(self, x, y):
  435. if DEBUG:
  436. print >> sys.stderr, 'getViewContainingPointAndGenerateTestCondition(%d, %d)' % (x, y)
  437. self.finishGeneratingTestCondition()
  438. vlist = self.vc.findViewsContainingPoint((x, y))
  439. vlist.reverse()
  440. for v in vlist:
  441. text = v.getText()
  442. if text:
  443. self.toast(u'Asserting view with text=%s' % text, timeout=2)
  444. # FIXME: only getText() is invoked by the generated assert(), a parameter
  445. # should be used to provide different alternatives to printOperation()
  446. self.printOperation(v, Operation.TEST, text)
  447. break
  448. def findViewContainingPointInTargets(self, x, y):
  449. vlist = self.vc.findViewsContainingPoint((x, y))
  450. if DEBUG_FIND_VIEW:
  451. print >> sys.stderr, "Views found:"
  452. for v in vlist:
  453. print >> sys.stderr, " ", v.__smallStr__()
  454. vlist.reverse()
  455. for v in vlist:
  456. if DEBUG:
  457. print >> sys.stderr, "checking if", v, "is in", self.targetViews
  458. if v in self.targetViews:
  459. if DEBUG_TOUCH:
  460. print >> sys.stderr
  461. print >> sys.stderr, "I guess you are trying to touch:", v
  462. print >> sys.stderr
  463. return v
  464. return None
  465. def getViewContainingPointAndTouch(self, x, y):
  466. if DEBUG:
  467. print >> sys.stderr, 'getViewContainingPointAndTouch(%d, %d)' % (x, y)
  468. if self.areEventsDisabled:
  469. if DEBUG:
  470. print >> sys.stderr, "Ignoring event"
  471. self.canvas.update_idletasks()
  472. return
  473. self.showVignette()
  474. if DEBUG_POINT:
  475. print >> sys.stderr, "getViewsContainingPointAndTouch(x=%s, y=%s)" % (x, y)
  476. print >> sys.stderr, "self.vc=", self.vc
  477. v = self.findViewContainingPointInTargets(x, y)
  478. if v is None:
  479. # FIXME: We can touch by DIP by default if no Views were found
  480. self.hideVignette()
  481. msg = "There are no touchable or clickable views here!"
  482. self.toast(msg)
  483. return
  484. clazz = v.getClass()
  485. if clazz == 'android.widget.EditText':
  486. title = "EditText"
  487. kwargs = {}
  488. if DEBUG:
  489. print >>sys.stderr, v
  490. if v.isPassword():
  491. title = "Password"
  492. kwargs = {'show': '*'}
  493. text = tkSimpleDialog.askstring(title, "Enter text to type into this field", **kwargs)
  494. self.canvas.focus_set()
  495. if text:
  496. v.type(text)
  497. self.printOperation(v, Operation.TYPE, text)
  498. else:
  499. self.hideVignette()
  500. return
  501. else:
  502. candidates = [v]
  503. def findBestCandidate(view):
  504. isccf = Culebron.isClickableCheckableOrFocusable(view)
  505. cd = view.getContentDescription()
  506. text = view.getText()
  507. if (cd or text) and not isccf:
  508. # because isccf==False this view was not added to the list of targets
  509. # (i.e. Settings)
  510. candidates.insert(0, view)
  511. return None
  512. if not (v.getText() or v.getContentDescription()) and v.getChildren():
  513. self.vc.traverse(root=v, transform=findBestCandidate, stream=None)
  514. if len(candidates) > 2:
  515. warnings.warn("We are in trouble, we have more than one candidate to touch", stacklevel=0)
  516. candidate = candidates[0]
  517. candidate.touch()
  518. # we pass root=v as an argument so the corresponding findView*() searches in this
  519. # subtree instead of the full tree
  520. self.printOperation(candidate, Operation.TOUCH_VIEW, v if candidate != v else None)
  521. self.printOperation(None, Operation.SLEEP, Operation.DEFAULT)
  522. self.vc.sleep(5)
  523. self.takeScreenshotAndShowItOnWindow()
  524. def touchPoint(self, x, y):
  525. '''
  526. Touches a point in the device screen.
  527. The generated operation will use the units specified in L{coordinatesUnit} and the
  528. orientation in L{vc.display['orientation']}.
  529. '''
  530. if DEBUG:
  531. print >> sys.stderr, 'touchPoint(%d, %d)' % (x, y)
  532. print >> sys.stderr, 'touchPoint:', type(x), type(y)
  533. if self.areEventsDisabled:
  534. if DEBUG:
  535. print >> sys.stderr, "Ignoring event"
  536. self.canvas.update_idletasks()
  537. return
  538. if DEBUG:
  539. print >> sys.stderr, "Is touching point:", self.isTouchingPoint
  540. if self.isTouchingPoint:
  541. self.showVignette()
  542. self.device.touch(x, y)
  543. if self.coordinatesUnit == Unit.DIP:
  544. x = round(x / self.vc.display['density'], 2)
  545. y = round(y / self.vc.display['density'], 2)
  546. self.printOperation(None, Operation.TOUCH_POINT, x, y, self.coordinatesUnit, self.vc.display['orientation'])
  547. self.printOperation(None, Operation.SLEEP, Operation.DEFAULT)
  548. self.vc.sleep(5)
  549. self.isTouchingPoint = False
  550. self.takeScreenshotAndShowItOnWindow()
  551. self.hideVignette()
  552. self.statusBar.clear()
  553. return
  554. def longTouchPoint(self, x, y):
  555. '''
  556. Long-touches a point in the device screen.
  557. The generated operation will use the units specified in L{coordinatesUnit} and the
  558. orientation in L{vc.display['orientation']}.
  559. '''
  560. if DEBUG:
  561. print >> sys.stderr, 'longTouchPoint(%d, %d)' % (x, y)
  562. if self.areEventsDisabled:
  563. if DEBUG:
  564. print >> sys.stderr, "Ignoring event"
  565. self.canvas.update_idletasks()
  566. return
  567. if DEBUG:
  568. print >> sys.stderr, "Is long touching point:", self.isLongTouchingPoint
  569. if self.isLongTouchingPoint:
  570. self.showVignette()
  571. self.device.longTouch(x, y)
  572. if self.coordinatesUnit == Unit.DIP:
  573. x = round(x / self.vc.display['density'], 2)
  574. y = round(y / self.vc.display['density'], 2)
  575. self.printOperation(None, Operation.LONG_TOUCH_POINT, x, y, 2000, self.coordinatesUnit, self.vc.display['orientation'])
  576. self.printOperation(None, Operation.SLEEP, 5)
  577. self.vc.sleep(5)
  578. self.isLongTouchingPoint = False
  579. self.takeScreenshotAndShowItOnWindow()
  580. self.hideVignette()
  581. self.statusBar.clear()
  582. return
  583. def onButton1Pressed(self, event):
  584. if DEBUG:
  585. print >> sys.stderr, "onButton1Pressed((", event.x, ", ", event.y, "))"
  586. (scaledX, scaledY) = (event.x/self.scale, event.y/self.scale)
  587. if DEBUG:
  588. print >> sys.stderr, " onButton1Pressed: scaled: (", scaledX, ", ", scaledY, ")"
  589. print >> sys.stderr, " onButton1Pressed: is grabbing:", self.isGrabbingTouch
  590. if self.isGrabbingTouch:
  591. self.onTouchListener((scaledX, scaledY))
  592. self.isGrabbingTouch = False
  593. elif self.isDragDialogShowed:
  594. self.toast("No touch events allowed while setting drag parameters", background=Color.GOLD)
  595. return
  596. elif self.isTouchingPoint:
  597. self.touchPoint(scaledX, scaledY)
  598. elif self.isLongTouchingPoint:
  599. self.longTouchPoint(scaledX, scaledY)
  600. elif self.isGeneratingTestCondition:
  601. self.getViewContainingPointAndGenerateTestCondition(scaledX, scaledY)
  602. else:
  603. self.getViewContainingPointAndTouch(scaledX, scaledY)
  604. def onCtrlButton1Pressed(self, event):
  605. if DEBUG:
  606. print >> sys.stderr, "onCtrlButton1Pressed((", event.x, ", ", event.y, "))"
  607. (scaledX, scaledY) = (event.x/self.scale, event.y/self.scale)
  608. l = self.vc.findViewsContainingPoint((scaledX, scaledY))
  609. if l and len(l) > 0:
  610. self.saveViewSnapshot(l[-1])
  611. else:
  612. msg = "There are no views here!"
  613. self.toast(msg)
  614. return
  615. def onButton2Pressed(self, event):
  616. if DEBUG:
  617. print >> sys.stderr, "onButton2Pressed((", event.x, ", ", event.y, "))"
  618. osName = platform.system()
  619. if osName == 'Darwin':
  620. self.showPopupMenu(event)
  621. def onButton3Pressed(self, event):
  622. if DEBUG:
  623. print >> sys.stderr, "onButton3Pressed((", event.x, ", ", event.y, "))"
  624. self.showPopupMenu(event)
  625. def command(self, keycode):
  626. '''
  627. Presses a key.
  628. Generates the actual key press on the device and prints the line in the script.
  629. '''
  630. self.device.press(keycode)
  631. self.printOperation(None, Operation.PRESS, keycode)
  632. def onKeyPressed(self, event):
  633. if DEBUG_KEY:
  634. print >> sys.stderr, "onKeyPressed(", repr(event), ")"
  635. print >> sys.stderr, " event", type(event.char), len(event.char), repr(event.char), event.keysym, event.keycode, event.type
  636. print >> sys.stderr, " events disabled:", self.areEventsDisabled
  637. if self.areEventsDisabled:
  638. if DEBUG_KEY:
  639. print >> sys.stderr, "ignoring event"
  640. self.canvas.update_idletasks()
  641. return
  642. char = event.char
  643. keysym = event.keysym
  644. if len(char) == 0 and not (keysym in Culebron.KEYSYM_TO_KEYCODE_MAP or keysym in Culebron.KEYSYM_CULEBRON_COMMANDS):
  645. if DEBUG_KEY:
  646. print >> sys.stderr, "returning because len(char) == 0"
  647. return
  648. ###
  649. ### internal commands: no output to generated script
  650. ###
  651. try:
  652. handler = getattr(self, 'onCtrl%s' % self.UPPERCASE_CHARS[ord(char)-1])
  653. except:
  654. handler = None
  655. if handler:
  656. return handler(event)
  657. elif keysym == 'F1':
  658. self.showHelp()
  659. return
  660. elif keysym == 'F5':
  661. self.refresh()
  662. return
  663. elif keysym == 'F8':
  664. self.printGridInfo()
  665. return
  666. elif keysym == 'Alt_L':
  667. return
  668. elif keysym == 'Control_L':
  669. return
  670. elif keysym == 'Escape':
  671. # we cannot send Escape to the device, but I think it's fine
  672. self.cancelOperation()
  673. return
  674. ### empty char (modifier) ###
  675. # here does not process events like Home where char is ''
  676. #if char == '':
  677. # return
  678. ###
  679. ### target actions
  680. ###
  681. self.showVignette()
  682. if keysym in Culebron.KEYSYM_TO_KEYCODE_MAP:
  683. if DEBUG_KEY:
  684. print >> sys.stderr, "Pressing", Culebron.KEYSYM_TO_KEYCODE_MAP[keysym]
  685. self.command(Culebron.KEYSYM_TO_KEYCODE_MAP[keysym])
  686. elif char == '\r':
  687. self.command('ENTER')
  688. elif char == '':
  689. # do nothing
  690. pass
  691. else:
  692. self.command(char.decode('ascii', errors='replace'))
  693. self.vc.sleep(1)
  694. self.takeScreenshotAndShowItOnWindow()
  695. def refresh(self):
  696. self.showVignette()
  697. self.device.wake()
  698. display = copy.copy(self.device.display)
  699. self.device.initDisplayProperties()
  700. changed = False
  701. for prop in display:
  702. if display[prop] != self.device.display[prop]:
  703. changed = True
  704. break
  705. if changed:
  706. self.window.geometry('%dx%d' % (self.device.display['width']*self.scale, self.device.display['height']*self.scale+int(self.statusBar.winfo_height())))
  707. self.deleteVignette()
  708. self.canvas.destroy()
  709. self.canvas = None
  710. self.window.update_idletasks()
  711. self.takeScreenshotAndShowItOnWindow()
  712. def cancelOperation(self):
  713. '''
  714. Cancels the ongoing operation if any.
  715. '''
  716. if self.isLongTouchingPoint:
  717. self.toggleLongTouchPoint()
  718. elif self.isTouchingPoint:
  719. self.toggleTouchPoint()
  720. elif self.isGeneratingTestCondition:
  721. self.toggleGenerateTestCondition()
  722. def onCtrlA(self, event):
  723. if DEBUG:
  724. self.toggleMessageArea()
  725. def showDragDialog(self):
  726. d = DragDialog(self)
  727. self.window.wait_window(d)
  728. self.setDragDialogShowed(False)
  729. def onCtrlD(self, event):
  730. self.showDragDialog()
  731. def onCtrlF(self, event):
  732. self.saveSnapshot()
  733. def saveSnapshot(self):
  734. '''
  735. Saves the current shanpshot to the specified file.
  736. Current snapshot is the image being displayed on the main window.
  737. '''
  738. filename = self.snapshotDir + os.sep + '${serialno}-${focusedwindowname}-${timestamp}' + '.' + self.snapshotFormat.lower()
  739. # We have the snapshot already taken, no need to retake
  740. d = FileDialog(self, self.device.substituteDeviceTemplate(filename))
  741. saveAsFilename = d.askSaveAsFilename()
  742. if saveAsFilename:
  743. _format = os.path.splitext(saveAsFilename)[1][1:].upper()
  744. self.printOperation(None, Operation.SNAPSHOT, filename, _format, self.deviceArt, self.dropShadow, self.screenGlare)
  745. #FIXME: we should add deviceArt, dropShadow and screenGlare to the saved image
  746. #self.unscaledScreenshot.save(saveAsFilename, _format, self.deviceArt, self.dropShadow, self.screenGlare)
  747. self.unscaledScreenshot.save(saveAsFilename, _format)
  748. def saveViewSnapshot(self, view):
  749. '''
  750. Saves the View snapshot.
  751. '''
  752. if not view:
  753. raise ValueError("view must be provided to take snapshot")
  754. filename = self.snapshotDir + os.sep + '${serialno}-' + view.variableNameFromId() + '-${timestamp}' + '.' + self.snapshotFormat.lower()
  755. d = FileDialog(self, self.device.substituteDeviceTemplate(filename))
  756. saveAsFilename = d.askSaveAsFilename()
  757. if saveAsFilename:
  758. _format = os.path.splitext(saveAsFilename)[1][1:].upper()
  759. self.printOperation(view, Operation.VIEW_SNAPSHOT, filename, _format)
  760. view.writeImageToFile(saveAsFilename, _format)
  761. def toggleTouchPointDip(self):
  762. '''
  763. Toggles the touch point operation using L{Unit.DIP}.
  764. This invokes L{toggleTouchPoint}.
  765. '''
  766. self.coordinatesUnit = Unit.DIP
  767. self.toggleTouchPoint()
  768. def onCtrlI(self, event):
  769. self.toggleTouchPointDip()
  770. def toggleLongTouchPoint(self):
  771. '''
  772. Toggles the long touch point operation.
  773. '''
  774. if not self.isLongTouchingPoint:
  775. msg = 'Long touching point'
  776. self.toast(msg, background=Color.GREEN)
  777. self.statusBar.set(msg)
  778. self.isLongTouchingPoint = True
  779. # FIXME: There should be 2 methods DIP & PX
  780. self.coordinatesUnit = Unit.PX
  781. else:
  782. self.toast(None)
  783. self.statusBar.clear()
  784. self.isLongTouchingPoint = False
  785. def onCtrlL(self, event):
  786. self.toggleLongTouchPoint()
  787. def toggleTouchPoint(self):
  788. '''
  789. Toggles the touch point operation using the units specified in L{coordinatesUnit}.
  790. '''
  791. if not self.isTouchingPoint:
  792. msg = 'Touching point (units=%s)' % self.coordinatesUnit
  793. self.toast(msg, background=Color.GREEN)
  794. self.statusBar.set(msg)
  795. self.isTouchingPoint = True
  796. else:
  797. self.toast(None)
  798. self.statusBar.clear()
  799. self.isTouchingPoint = False
  800. def toggleTouchPointPx(self):
  801. self.coordinatesUnit = Unit.PX
  802. self.toggleTouchPoint()
  803. def onCtrlP(self, event):
  804. self.toggleTouchPointPx()
  805. def onCtrlQ(self, event):
  806. if DEBUG:
  807. print >> sys.stderr, "onCtrlQ(%s)" % event
  808. self.quit()
  809. def quit(self):
  810. self.window.destroy()
  811. def showSleepDialog(self):
  812. seconds = tkSimpleDialog.askfloat('Sleep Interval', 'Value in seconds:', initialvalue=1, minvalue=0, parent=self.window)
  813. if seconds is not None:
  814. self.printOperation(None, Operation.SLEEP, seconds)
  815. self.canvas.focus_set()
  816. def onCtrlS(self, event):
  817. self.showSleepDialog()
  818. def startGeneratingTestCondition(self):
  819. self.message('Generating test condition...', background=Color.GREEN)
  820. self.isGeneratingTestCondition = True
  821. def finishGeneratingTestCondition(self):
  822. self.isGeneratingTestCondition = False
  823. self.hideMessageArea()
  824. def toggleGenerateTestCondition(self):
  825. '''
  826. Toggles generating test condition
  827. '''
  828. if self.isGeneratingTestCondition:
  829. self.finishGeneratingTestCondition()
  830. else:
  831. self.startGeneratingTestCondition()
  832. def onCtrlT(self, event):
  833. if DEBUG:
  834. print >>sys.stderr, "onCtrlT()"
  835. # FIXME: This is only valid if we are generating a test case
  836. self.toggleGenerateTestCondition()
  837. def onCtrlU(self, event):
  838. if DEBUG:
  839. print >>sys.stderr, "onCtrlU()"
  840. def onCtrlV(self, event):
  841. if DEBUG:
  842. print >>sys.stderr, "onCtrlV()"
  843. self.printOperation(None, Operation.TRAVERSE)
  844. def toggleTargetZones(self):
  845. self.toggleTargets()
  846. self.canvas.update_idletasks()
  847. def onCtrlZ(self, event):
  848. if DEBUG:
  849. print >> sys.stderr, "onCtrlZ()"
  850. self.toggleTargetZones()
  851. def showControlPanel(self):
  852. from com.dtmilano.android.controlpanel import ControlPanel
  853. self.controlPanel = ControlPanel(self, self.vc, self.printOperation)
  854. def onCtrlK(self, event):
  855. self.showControlPanel()
  856. def drag(self, start, end, duration, steps, units=Unit.DIP):
  857. self.showVignette()
  858. # the operation on this device is always done in PX
  859. self.device.drag(start, end, duration, steps)
  860. if units == Unit.DIP:
  861. x0 = round(start[0] / self.vc.display['density'], 2)
  862. y0 = round(start[1] / self.vc.display['density'], 2)
  863. x1 = round(end[0] / self.vc.display['density'], 2)
  864. y1 = round(end[1] / self.vc.display['density'], 2)
  865. start = (x0, y0)
  866. end = (x1, y1)
  867. self.printOperation(None, Operation.DRAG, start, end, duration, steps, units, self.vc.display['orientation'])
  868. self.printOperation(None, Operation.SLEEP, 1)
  869. self.vc.sleep(1)
  870. self.takeScreenshotAndShowItOnWindow()
  871. def enableEvents(self):
  872. self.canvas.update_idletasks()
  873. self.canvas.bind("<Button-1>", self.onButton1Pressed)
  874. self.canvas.bind("<Control-Button-1>", self.onCtrlButton1Pressed)
  875. self.canvas.bind("<Button-2>", self.onButton2Pressed)
  876. self.canvas.bind("<Button-3>", self.onButton3Pressed)
  877. self.canvas.bind("<BackSpace>", self.onKeyPressed)
  878. #self.canvas.bind("<Control-Key-S>", self.onCtrlS)
  879. self.canvas.bind("<Key>", self.onKeyPressed)
  880. self.areEventsDisabled = False
  881. def disableEvents(self):
  882. if self.canvas is not None:
  883. self.canvas.update_idletasks()
  884. self.areEventsDisabled = True
  885. self.canvas.unbind("<Button-1>")
  886. self.canvas.unbind("<Control-Button-1>")
  887. self.canvas.unbind("<Button-2>")
  888. self.canvas.unbind("<Button-3>")
  889. self.canvas.unbind("<BackSpace>")
  890. #self.canvas.unbind("<Control-Key-S>")
  891. self.canvas.unbind("<Key>")
  892. def toggleTargets(self):
  893. if DEBUG:
  894. print >> sys.stderr, "toggletargets: aretargetsmarked=", self.areTargetsMarked
  895. if not self.areTargetsMarked:
  896. self.markTargets()
  897. else:
  898. self.unmarkTargets()
  899. def markTargets(self):
  900. if DEBUG:
  901. print >> sys.stderr, "marktargets: aretargetsmarked=", self.areTargetsMarked
  902. print >> sys.stderr, " marktargets: targets=", self.targets
  903. colors = ["#ff00ff", "#ffff00", "#00ffff"]
  904. self.targetIds = []
  905. c = 0
  906. for (x1, y1, x2, y2) in self.targets:
  907. if DEBUG:
  908. print "adding rectangle:", x1, y1, x2, y2
  909. self.markTarget(x1, y1, x2, y2, colors[c%len(colors)])
  910. c += 1
  911. self.areTargetsMarked = True
  912. def markTarget(self, x1, y1, x2, y2, color='#ff00ff'):
  913. '''
  914. @return the id of the rectangle added
  915. '''
  916. self.areTargetsMarked = True
  917. return self.targetIds.append(self.canvas.create_rectangle(x1*self.scale, y1*self.scale, x2*self.scale, y2*self.scale, fill=color, stipple="gray25"))
  918. def unmarkTargets(self):
  919. if not self.areTargetsMarked:
  920. return
  921. for t in self.targetIds:
  922. self.canvas.delete(t)
  923. self.targetIds = []
  924. self.areTargetsMarked = False
  925. def setDragDialogShowed(self, showed):
  926. self.isDragDialogShowed = showed
  927. if showed:
  928. pass
  929. else:
  930. self.isGrabbingTouch = False
  931. def drawTouchedPoint(self, x, y):
  932. size = 50
  933. return self.canvas.create_oval((x-size)*self.scale, (y-size)*self.scale, (x+size)*self.scale, (y+size)*self.scale, fill=Color.MAGENTA)
  934. def drawDragLine(self, x0, y0, x1, y1):
  935. width = 15
  936. 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))
  937. def executeCommandAndRefresh(self, command):
  938. self.showVignette()
  939. if DEBUG:
  940. print >> sys.stderr, 'DEBUG: command=', command, command.__name__
  941. print >> sys.stderr, 'DEBUG: command=', command.__self__, command.__self__.view
  942. try:
  943. view = command.__self__.view
  944. except AttributeError:
  945. view = None
  946. self.printOperation(view, Operation.fromCommandName(command.__name__))
  947. command()
  948. self.printOperation(None, Operation.SLEEP, Operation.DEFAULT)
  949. self.vc.sleep(5)
  950. # FIXME: perhaps refresh() should be invoked here just in case size or orientation changed
  951. self.takeScreenshotAndShowItOnWindow()
  952. def changeLanguage(self):
  953. code = tkSimpleDialog.askstring("Change language", "Enter the language code")
  954. self.vc.uiDevice.changeLanguage(code)
  955. self.printOperation(None, Operation.CHANGE_LANGUAGE, code)
  956. self.refresh()
  957. def setOnTouchListener(self, listener):
  958. self.onTouchListener = listener
  959. def setGrab(self, state):
  960. if DEBUG:
  961. print >> sys.stderr, "Culebron.setGrab(%s)" % state
  962. if state and not self.onTouchListener:
  963. warnings.warn('Starting to grab but no onTouchListener')
  964. self.isGrabbingTouch = state
  965. if state:
  966. self.toast('Grabbing drag points...', background=Color.GREEN)
  967. else:
  968. self.hideMessageArea()
  969. @staticmethod
  970. def isClickableCheckableOrFocusable(v):
  971. if DEBUG_ISCCOF:
  972. print >> sys.stderr, "isClickableCheckableOrFocusable(", v.__tinyStr__(), ")"
  973. try:
  974. return v.isClickable()
  975. except AttributeError:
  976. pass
  977. try:
  978. return v.isCheckable()
  979. except AttributeError:
  980. pass
  981. try:
  982. return v.isFocusable()
  983. except AttributeError:
  984. pass
  985. return False
  986. def mainloop(self):
  987. self.window.title("%s v%s" % (Culebron.APPLICATION_NAME, __version__))
  988. self.window.resizable(width=Tkinter.FALSE, height=Tkinter.FALSE)
  989. self.window.lift()
  990. self.window.mainloop()
  991. if TKINTER_AVAILABLE:
  992. class MainMenu(Tkinter.Menu):
  993. def __init__(self, culebron):
  994. Tkinter.Menu.__init__(self, culebron.window)
  995. self.culebron = culebron
  996. self.fileMenu = Tkinter.Menu(self, tearoff=False)
  997. self.fileMenu.add_command(label="Quit", underline=0, accelerator='Command-Q', command=self.culebron.quit)
  998. self.add_cascade(label="File", underline=0, menu=self.fileMenu)
  999. self.viewMenu = Tkinter.Menu(self, tearoff=False)
  1000. self.showViewTree = Tkinter.BooleanVar()
  1001. self.showViewTree.set(False)
  1002. self.viewMenu.add_checkbutton(label="Tree", underline=0, accelerator='Command-T', onvalue=True, offvalue=False, variable=self.showViewTree, state=NORMAL, command=self.onshowViewTreeChanged)
  1003. self.showViewDetails = Tkinter.BooleanVar()
  1004. self.viewMenu.add_checkbutton(label="View details", underline=0, accelerator='Command-V', onvalue=True, offvalue=False, variable=self.showViewDetails, state=NORMAL, command=self.onShowViewDetailsChanged)
  1005. self.add_cascade(label="View", underline=0, menu=self.viewMenu)
  1006. self.uiDeviceMenu = Tkinter.Menu(self, tearoff=False)
  1007. self.uiDeviceMenu.add_command(label="Open Notification", underline=6, command=lambda: culebron.executeCommandAndRefresh(self.culebron.vc.uiDevice.openNotification))
  1008. self.uiDeviceMenu.add_command(label="Open Quick settings", underline=6, command=lambda: culebron.executeCommandAndRefresh(command=self.culebron.vc.uiDevice.openQuickSettings))
  1009. self.uiDeviceMenu.add_command(label="Change Language", underline=7, command=self.culebron.changeLanguage)
  1010. self.add_cascade(label="UiDevice", menu=self.uiDeviceMenu)
  1011. self.helpMenu = Tkinter.Menu(self, tearoff=False)
  1012. self.helpMenu.add_command(label="Keyboard shortcuts", underline=0, accelerator='Command-K', command=self.culebron.showHelp)
  1013. self.add_cascade(label="Help", underline=0, menu=self.helpMenu)
  1014. def callback(self):
  1015. pass
  1016. def onshowViewTreeChanged(self):
  1017. if self.showViewTree.get() == 1:
  1018. self.culebron.showViewTree()
  1019. else:
  1020. self.culebron.hideViewTree()
  1021. def onShowViewDetailsChanged(self):
  1022. if self.showViewDetails.get() == 1:
  1023. self.culebron.showViewDetails()
  1024. else:
  1025. self.culebron.hideViewDetails()
  1026. class ViewTree(Tkinter.Frame):
  1027. def __init__(self, parent):
  1028. Tkinter.Frame.__init__(self, parent)
  1029. self.viewTree = ttk.Treeview(self, columns=['T'], height=35)
  1030. self.viewTree.column(0, width=20)
  1031. self.viewTree.heading('#0', None, text='View', anchor=Tkinter.W)
  1032. self.viewTree.heading(0, None, text='T', anchor=Tkinter.W)
  1033. self.scrollbar = ttk.Scrollbar(self, orient=Tkinter.HORIZONTAL, command=self.__xscroll)
  1034. self.viewTree.grid(row=1, rowspan=1, column=1, sticky=Tkinter.N+Tkinter.S)
  1035. self.scrollbar.grid(row=2, rowspan=1, column=1, sticky=Tkinter.E+Tkinter.W)
  1036. self.viewTree.configure(xscrollcommand=self.scrollbar.set)
  1037. def __xscroll(self, *args):
  1038. if DEBUG:
  1039. print >> sys.stderr, "__xscroll:", args
  1040. self.viewTree.xview(*args)
  1041. def insert(self, parent, index, iid=None, **kw):
  1042. """Creates a new item and return the item identifier of the newly
  1043. created item.
  1044. parent is the item ID of the parent item, or the empty string
  1045. to create a new top-level item. index is an integer, or the value
  1046. end, specifying where in the list of parent's children to insert
  1047. the new item. If index is less than or equal to zero, the new node
  1048. is inserted at the beginning, if index is greater than or equal to
  1049. the current number of children, it is inserted at the end. If iid
  1050. is specified, it is used as the item identifier, iid must not
  1051. already exist in the tree. Otherwise, a new unique identifier
  1052. is generated."""
  1053. return self.viewTree.insert(parent, index, iid, **kw)
  1054. def set(self, item, column=None, value=None):
  1055. """With one argument, returns a dictionary of column/value pairs
  1056. for the specified item. With two arguments, returns the current
  1057. value of the specified column. With three arguments, sets the
  1058. value of given column in given item to the specified value."""
  1059. return self.viewTree.set(item, column, value)
  1060. def tag_bind(self, tagname, sequence=None, callback=None):
  1061. if DEBUG:
  1062. print >> sys.stderr, 'ViewTree.tag_bind(', tagname, ',', sequence, ',', callback, ')'
  1063. return self.viewTree.tag_bind(tagname, sequence, callback)
  1064. class ViewDetails(Tkinter.Frame):
  1065. VIEW_DETAILS = "View Details:\n"
  1066. def __init__(self, parent):
  1067. Tkinter.Frame.__init__(self, parent)
  1068. self.label = Tkinter.Label(self, bd=1, width=30, wraplength=200, justify=Tkinter.LEFT, anchor=Tkinter.NW)
  1069. self.label.configure(text=self.VIEW_DETAILS)
  1070. self.label.configure(bg="white")
  1071. self.label.grid(row=3, column=1, rowspan=1)
  1072. def set(self, view):
  1073. self.label.configure(text=self.VIEW_DETAILS + view.__str__())
  1074. class StatusBar(Tkinter.Frame):
  1075. def __init__(self, parent):
  1076. Tkinter.Frame.__init__(self, parent)
  1077. self.label = Tkinter.Label(self, bd=1, relief=Tkinter.SUNKEN, anchor=Tkinter.W)
  1078. self.label.grid(row=1, column=1, columnspan=2, sticky=Tkinter.E+Tkinter.W)
  1079. def set(self, fmt, *args):
  1080. self.label.config(text=fmt % args)
  1081. self.label.update_idletasks()
  1082. def clear(self):
  1083. self.label.config(text="")
  1084. self.label.update_idletasks()
  1085. class LabeledEntry():
  1086. def __init__(self, parent, text, validate, validatecmd):
  1087. self.f = Tkinter.Frame(parent)
  1088. Tkinter.Label(self.f, text=text, anchor="w", padx=8).grid(row=1, column=1, sticky=Tkinter.E)
  1089. self.entry = Tkinter.Entry(self.f, validate=validate, validatecommand=validatecmd)
  1090. self.entry.grid(row=1, column=2, padx=5, sticky=Tkinter.E)
  1091. def grid(self, **kwargs):
  1092. self.f.grid(kwargs)
  1093. def get(self):
  1094. return self.entry.get()
  1095. def set(self, text):
  1096. self.entry.delete(0, Tkinter.END)
  1097. self.entry.insert(0, text)
  1098. class LabeledEntryWithButton(LabeledEntry):
  1099. def __init__(self, parent, text, buttonText, command, validate, validatecmd):
  1100. LabeledEntry.__init__(self, parent, text, validate, validatecmd)
  1101. self.button = Tkinter.Button(self.f, text=buttonText, command=command)
  1102. self.button.grid(row=1, column=3)
  1103. class DragDialog(Tkinter.Toplevel):
  1104. DEFAULT_DURATION = 1000
  1105. DEFAULT_STEPS = 20
  1106. spX = None
  1107. spY = None
  1108. epX = None
  1109. epY = None
  1110. spId = None
  1111. epId = None
  1112. def __init__(self, culebron):
  1113. self.culebron = culebron
  1114. self.parent = culebron.window
  1115. Tkinter.Toplevel.__init__(self, self.parent)
  1116. self.transient(self.parent)
  1117. self.culebron.setDragDialogShowed(True)
  1118. self.title("Drag: selecting parameters")
  1119. # valid percent substitutions (from the Tk entry man page)
  1120. # %d = Type of action (1=insert, 0=delete, -1 for others)
  1121. # %i = index of char string to be inserted/deleted, or -1
  1122. # %P = value of the entry if the edit is allowed
  1123. # %s = value of entry prior to editing
  1124. # %S = the text string being inserted or deleted, if any
  1125. # %v = the type of validation that is currently set
  1126. # %V = the type of validation that triggered the callback
  1127. # (key, focusin, focusout, forced)
  1128. # %W = the tk name of the widget
  1129. self.validate = (self.parent.register(self.onValidate), '%P')
  1130. self.sp = LabeledEntryWithButton(self, "Start point", "Grab", command=self.onGrabSp, validate="focusout", validatecmd=self.validate)
  1131. self.sp.grid(row=1, column=1, columnspan=3, pady=5)
  1132. self.ep = LabeledEntryWithButton(self, "End point", "Grab", command=self.onGrabEp, validate="focusout", validatecmd=self.validate)
  1133. self.ep.grid(row=2, column=1, columnspan=3, pady=5)
  1134. l = Tkinter.Label(self, text="Units")
  1135. l.grid(row=3, column=1, sticky=Tkinter.E)
  1136. self.units = Tkinter.StringVar()
  1137. self.units.set(Unit.DIP)
  1138. col = 2
  1139. for u in dir(Unit):
  1140. if u.startswith('_'):
  1141. continue
  1142. rb = Tkinter.Radiobutton(self, text=u, variable=self.units, value=u)
  1143. rb.grid(row=3, column=col, padx=20, sticky=Tkinter.E)
  1144. col += 1
  1145. self.d = LabeledEntry(self, "Duration", validate="focusout", validatecmd=self.validate)
  1146. self.d.set(DragDialog.DEFAULT_DURATION)
  1147. self.d.grid(row=4, column=1, columnspan=3, pady=5)
  1148. self.s = LabeledEntry(self, "Steps", validate="focusout", validatecmd=self.validate)
  1149. self.s.set(DragDialog.DEFAULT_STEPS)
  1150. self.s.grid(row=5, column=1, columnspan=2, pady=5)
  1151. self.buttonBox()
  1152. def buttonBox(self):
  1153. # add standard button box. override if you don't want the
  1154. # standard buttons
  1155. box = Tkinter.Frame(self)
  1156. self.ok = Tkinter.Button(box, text="OK", width=10, command=self.onOk, default=Tkinter.ACTIVE, state=Tkinter.DISABLED)
  1157. self.ok.grid(row=6, column=1, sticky=Tkinter.E, padx=5, pady=5)
  1158. w = Tkinter.Button(box, text="Cancel", width=10, command=self.onCancel)
  1159. w.grid(row=6, column=2, sticky=Tkinter.E, padx=5, pady=5)
  1160. self.bind("<Return>", self.onOk)
  1161. self.bind("<Escape>", self.onCancel)
  1162. box.grid(row=6, column=1, columnspan=3)
  1163. def onValidate(self, value):
  1164. if self.sp.get() and self.ep.get() and self.d.get() and self.s.get():
  1165. self.ok.configure(state=Tkinter.NORMAL)
  1166. else:
  1167. self.ok.configure(state=Tkinter.DISABLED)
  1168. def onOk(self, event=None):
  1169. if DEBUG:
  1170. print >> sys.stderr, "onOK()"
  1171. print >> sys.stderr, "values are:",
  1172. print >> sys.stderr, self.sp.get(),
  1173. print >> sys.stderr, self.ep.get(),
  1174. print >> sys.stderr, self.d.get(),
  1175. print >> sys.stderr, self.s.get(),
  1176. print >> sys.stderr, self.units.get()
  1177. sp = make_tuple(self.sp.get())
  1178. ep = make_tuple(self.ep.get())
  1179. d = int(self.d.get())
  1180. s = int(self.s.get())
  1181. self.cleanUp()
  1182. # put focus back to the parent window's canvas
  1183. self.culebron.canvas.focus_set()
  1184. self.destroy()
  1185. self.culebron.drag(sp, ep, d, s, self.units.get())
  1186. def onCancel(self, event=None):
  1187. self.culebron.setGrab(False)
  1188. self.cleanUp()
  1189. # put focus back to the parent window's canvas
  1190. self.culebron.canvas.focus_set()
  1191. self.destroy()
  1192. def onGrabSp(self):
  1193. '''
  1194. Grab starting point
  1195. '''
  1196. self.sp.entry.focus_get()
  1197. self.onGrab(self.sp)
  1198. def onGrabEp(self):
  1199. '''
  1200. Grab ending point
  1201. '''
  1202. self.ep.entry.focus_get()
  1203. self.onGrab(self.ep)
  1204. def onGrab(self, entry):
  1205. '''
  1206. Generic grab method.
  1207. @param entry: the entry being grabbed
  1208. @type entry: Tkinter.Entry
  1209. '''
  1210. self.culebron.setOnTouchListener(self.onTouchListener)
  1211. self.__grabbing = entry
  1212. self.culebron.setGrab(True)
  1213. def onTouchListener(self, point):
  1214. '''
  1215. Listens for touch events and draws the corresponding shapes on the Culebron canvas.
  1216. If the starting point is being grabbed it draws the touching point via
  1217. C{Culebron.drawTouchedPoint()} and if the end point is being grabbed it draws
  1218. using C{Culebron.drawDragLine()}.
  1219. @param point: the point touched
  1220. @type point: tuple
  1221. '''
  1222. x = point[0]
  1223. y = point[1]
  1224. value = "(%d,%d)" % (x, y)
  1225. self.__grabbing.set(value)
  1226. self.onValidate(value)
  1227. self.culebron.setGrab(False)
  1228. if self.__grabbing == self.sp:
  1229. self.__cleanUpSpId()
  1230. self.__cleanUpEpId()
  1231. self.spX = x
  1232. self.spY = y
  1233. elif self.__grabbing == self.ep:
  1234. self.__cleanUpEpId()
  1235. self.epX = x
  1236. self.epY = y
  1237. if self.spX and self.spY and not self.spId:
  1238. self.spId = self.culebron.drawTouchedPoint(self.spX, self.spY)
  1239. if self.spX and self.spY and self.epX and self.epY and not self.epId:
  1240. self.epId = self.culebron.drawDragLine(self.spX, self.spY, self.epX, self.epY)
  1241. self.__grabbing = None
  1242. self.culebron.setOnTouchListener(None)
  1243. def __cleanUpSpId(self):
  1244. if self.spId:
  1245. self.culebron.canvas.delete(self.spId)
  1246. self.spId = None
  1247. def __cleanUpEpId(self):
  1248. if self.epId:
  1249. self.culebron.canvas.delete(self.epId)
  1250. self.epId = None
  1251. def cleanUp(self):
  1252. self.__cleanUpSpId()
  1253. self.__cleanUpEpId()
  1254. class ContextMenu(Tkinter.Menu):
  1255. # FIXME: should get rid of the nested classes, otherwise it's not possible to create a parent class
  1256. # SubMenu for UiScrollableSubMenu
  1257. '''
  1258. The context menu (popup).
  1259. '''
  1260. PADDING = ' '
  1261. ''' Padding used to separate menu entries from border '''
  1262. class Separator():
  1263. SEPARATOR = 'SEPARATOR'
  1264. def __init__(self):
  1265. self.description = self.SEPARATOR
  1266. class Command():
  1267. def __init__(self, description, underline, shortcut, event, command):
  1268. self.description = description
  1269. self.underline = underline
  1270. self.shortcut = shortcut
  1271. self.event = event
  1272. self.command = command
  1273. class UiScrollableSubMenu(Tkinter.Menu):
  1274. def __init__(self, menu, description, view, culebron):
  1275. # Tkninter.Menu is not extending object, so we can't do this:
  1276. #super(ContextMenu, self).__init__(culebron.window, tearoff=False)
  1277. Tkinter.Menu.__init__(self, menu, tearoff=False)
  1278. self.description = description
  1279. self.add_command(label='Fling backward', command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingBackward))
  1280. self.add_command(label='Fling forward', command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingForward))
  1281. self.add_command(label='Fling to beginning', command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingToBeginning))
  1282. self.add_command(label='Fling to end', command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingToEnd))
  1283. def __init__(self, culebron, view):
  1284. # Tkninter.Menu is not extending object, so we can't do this:
  1285. #super(ContextMenu, self).__init__(culebron.window, tearoff=False)
  1286. Tkinter.Menu.__init__(self, culebron.window, tearoff=False)
  1287. if DEBUG_CONTEXT_MENU:
  1288. print >> sys.stderr, "Creating ContextMenu for", view.__smallStr__() if view else "No View"
  1289. self.view = view
  1290. items = []
  1291. if self.view:
  1292. _saveViewSnapshotForSelectedView = lambda: culebron.saveViewSnapshot(self.view)
  1293. items.append(ContextMenu.Command('Take view snapshot and save to file', 5, 'Ctrl+W', '<Control-W>', _saveViewSnapshotForSelectedView))
  1294. if self.view.uiScrollable:
  1295. items.append(ContextMenu.UiScrollableSubMenu(self, 'UiScrollable', view, culebron))
  1296. else:
  1297. parent = self.view.parent
  1298. while parent:
  1299. if parent.uiScrollable:
  1300. # WARNING:
  1301. # A bit dangerous, but may work
  1302. # If we click ona ListView then the View pased to this ContextMenu is the child,
  1303. # perhaps we want to scroll the parent
  1304. items.append(ContextMenu.UiScrollableSubMenu(self, 'UiScrollable', parent, culebron))
  1305. break
  1306. parent = parent.parent
  1307. items.append(ContextMenu.Separator())
  1308. items.append(ContextMenu.Command('Drag dialog', 0, 'Ctrl+D', '<Control-D>', culebron.showDragDialog))
  1309. items.append(ContextMenu.Command('Take snapshot and save to file', 26, 'Ctrl+F', '<Control-F>', culebron.saveSnapshot))
  1310. items.append(ContextMenu.Command('Control Panel', 0, 'Ctrl+K', '<Control-K>', culebron.showControlPanel))
  1311. items.append(ContextMenu.Command('Long touch point using PX', 0, 'Ctrl+L', '<Control-L>', culebron.toggleLongTouchPoint))
  1312. items.append(ContextMenu.Command('Touch using DIP', 13, 'Ctrl+I', '<Control-I>', culebron.toggleTouchPointDip))
  1313. items.append(ContextMenu.Command('Touch using PX', 12, 'Ctrl+P', '<Control-P>', culebron.toggleTouchPointPx))
  1314. items.append(ContextMenu.Command('Generates a Sleep() on output script', 12, 'Ctrl+S', '<Control-S>', culebron.showSleepDialog))
  1315. items.append(ContextMenu.Command('Toggle generating Test Condition', 18, 'Ctrl+T', '<Control-T>', culebron.toggleGenerateTestCondition))
  1316. items.append(ContextMenu.Command('Touch Zones', 6, 'Ctrl+Z', '<Control-Z>', culebron.toggleTargetZones))
  1317. items.append(ContextMenu.Command('Refresh', 0, 'F5', '<F5>', culebron.refresh))
  1318. items.append(ContextMenu.Separator())
  1319. items.append(ContextMenu.Command('Quit', 0, 'Ctrl+Q', '<Control-Q>', culebron.quit))
  1320. for item in items:
  1321. self.addItem(item)
  1322. def addItem(self, item):
  1323. if isinstance(item, ContextMenu.Separator):
  1324. self.addSeparator()
  1325. elif isinstance(item, ContextMenu.Command):
  1326. self.addCommand(item)
  1327. elif isinstance(item, ContextMenu.UiScrollableSubMenu):
  1328. self.addSubMenu(item)
  1329. else:
  1330. raise RuntimeError("Unsupported item=" + str(item))
  1331. def addSeparator(self):
  1332. self.add_separator()
  1333. def addCommand(self, item):
  1334. self.add_command(label=self.PADDING + item.description, underline=item.underline + len(self.PADDING), command=item.command, accelerator=item.shortcut)
  1335. #if item.event:
  1336. # # These bindings remain even after the menu has been dismissed, so it seems not a good idea
  1337. # #self.bind_all(item.event, item.command)
  1338. # pass
  1339. def addSubMenu(self, item):
  1340. self.add_cascade(label=self.PADDING + item.description, menu=item)
  1341. def showPopupMenu(self, event):
  1342. try:
  1343. self.tk_popup(event.x_root, event.y_root)
  1344. finally:
  1345. # make sure to release the grab (Tk 8.0a1 only)
  1346. #self.grab_release()
  1347. pass
  1348. class HelpDialog(Tkinter.Toplevel):
  1349. def __init__(self, culebron):
  1350. self.culebron = culebron
  1351. self.parent = culebron.window
  1352. Tkinter.Toplevel.__init__(self, self.parent)
  1353. #self.transient(self.parent)
  1354. self.title("%s: help" % Culebron.APPLICATION_NAME)
  1355. self.text = ScrolledText.ScrolledText(self, width=60, height=40)
  1356. self.text.insert(Tkinter.INSERT, '''
  1357. Special keys
  1358. ------------
  1359. F1: Help
  1360. F5: Refresh
  1361. Mouse Buttons
  1362. -------------
  1363. <1>: Touch the underlying View
  1364. Commands
  1365. --------
  1366. Ctrl-A: Toggle message area
  1367. Ctrl-D: Drag dialog
  1368. Ctrl-F: Take snapshot and save to file
  1369. Ctrl-K: Control Panel
  1370. Ctrl-L: Long touch point using PX
  1371. Ctrl-I: Touch using DIP
  1372. Ctrl-P: Touch using PX
  1373. Ctrl-Q: Quit
  1374. Ctrl-S: Generates a sleep() on output script
  1375. Ctrl-T: Toggle generating test condition
  1376. Ctrl-V: Verifies the content of the screen dump
  1377. Ctrl-Z: Touch zones
  1378. ''')
  1379. self.text.grid(row=1, column=1)
  1380. self.buttonBox()
  1381. def buttonBox(self):
  1382. # add standard button box. override if you don't want the
  1383. # standard buttons
  1384. box = Tkinter.Frame(self)
  1385. w = Tkinter.Button(box, text="Dismiss", width=10, command=self.onDismiss, default=Tkinter.ACTIVE)
  1386. w.grid(row=1, column=1, padx=5, pady=5)
  1387. self.bind("<Return>", self.onDismiss)
  1388. self.bind("<Escape>", self.onDismiss)
  1389. box.grid(row=1, column=1)
  1390. def onDismiss(self, event=None):
  1391. # put focus back to the parent window's canvas
  1392. self.culebron.canvas.focus_set()
  1393. self.destroy()
  1394. class FileDialog():
  1395. def __init__(self, culebron, filename):
  1396. self.parent = culebron.window
  1397. self.filename = filename
  1398. self.basename = os.path.basename(self.filename)
  1399. self.dirname = os.path.dirname(self.filename)
  1400. self.ext = os.path.splitext(self.filename)[1]
  1401. self.fileTypes = [('images', self.ext)]
  1402. def askSaveAsFilename(self):
  1403. return tkFileDialog.asksaveasfilename(parent=self.parent, filetypes=self.fileTypes, defaultextension=self.ext, initialdir=self.dirname, initialfile=self.basename)