/******************************************************************************* * Copyright (c) 2012 Google, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Google, Inc. - initial API and implementation *******************************************************************************/ package com.windowtester.swt.event.model; import org.eclipse.jface.action.ActionContributionItem; import org.eclipse.jface.action.IContributionManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.DropTargetEvent; import org.eclipse.swt.events.TypedEvent; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Widget; import org.eclipse.ui.IViewPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.PlatformUI; import com.windowtester.internal.runtime.IWidgetIdentifier; import com.windowtester.internal.runtime.event.StyleBits; import com.windowtester.recorder.event.IUISemanticEvent; import com.windowtester.recorder.event.user.ISemanticSelectionEvent; import com.windowtester.recorder.event.user.SemanticFocusEvent; import com.windowtester.recorder.event.user.SemanticKeyDownEvent; import com.windowtester.recorder.event.user.SemanticListSelectionEvent; import com.windowtester.recorder.event.user.SemanticMenuSelectionEvent; import com.windowtester.recorder.event.user.SemanticTreeItemSelectionEvent; import com.windowtester.recorder.event.user.SemanticWidgetClosedEvent; import com.windowtester.recorder.event.user.SemanticWidgetInspectionEvent; import com.windowtester.recorder.event.user.SemanticWidgetSelectionEvent; import com.windowtester.recorder.event.user.TreeEventType; import com.windowtester.runtime.swt.internal.display.DisplayExec; import com.windowtester.runtime.swt.internal.display.RunnableWithResult; import com.windowtester.runtime.swt.internal.finder.eclipse.views.ViewFinder; import com.windowtester.runtime.swt.locator.SWTWidgetLocator; import com.windowtester.runtime.swt.locator.eclipse.ViewLocator; import com.windowtester.swt.event.model.factory.SWTSemanticEventFactoryImplV2; /** * * While the <code>SWTSemanticEventInterpreter</code> translates one event, this stateful parser * handles streams of events. * * <br><br> * The grammar of SWT events has an effective look-ahead of one token. To accommodate this, * we implement a buffer where we store the last event seen. This event only gets dispatched * from the parser <em>after</em> the next event is processed. Clients of the parser must * call <code>flush()</code> on event stream termination to ensure that the buffer is flushed * (otherwise a token may remain cached and undispatched). * * @see com.windowtester.swt.event.model.SWTSemanticEventInterpreter */ public class SWTSemanticEventParser { /** The interpreter used for single event translations.*/ private final SWTSemanticEventInterpreter _interpreter; /** The buffered last seen event */ private IUISemanticEvent _buffered; /** Temporary event references */ private IUISemanticEvent _fresh; private IUISemanticEvent _result; /** A buffered last seen typed event for dnd handling */ private TypedEvent _lastTypedEvent; private Event _lastEvent; /** * Create an instance. * @param interpreter */ public SWTSemanticEventParser(SWTSemanticEventInterpreter interpreter) { _interpreter = interpreter; } /** * Translates the given SWT widget event into an SWT semantic event. * @return the semantic event -- or <code>null</code> if there is none (and it should be ignored) */ public IUISemanticEvent parse(final Event event) { /* * invalidate our typed event back pointer */ _lastTypedEvent = null; //System.out.println("parsing: " + event); _fresh = _interpreter.interpret(event); if (_fresh == null) return _fresh; //fast fail if (_fresh instanceof SemanticWidgetInspectionEvent) { _buffered = _fresh; } //check for double-click case if (_fresh.getClicks() == 2) { _result = handleDoubleClick(_buffered); _buffered = null; } else { /* * (Arrow) traversal events may be followed by a selection. * This selection is bogus and should be ignored. */ if (isArrowTraversalCase(_buffered, _fresh)) { _result = _buffered; _buffered = null; /* * If a shell shows that is not the shell associated with the next * event, assume it is transient and can be discarded */ } else if (isSuperfluousShellChangeEvent(_buffered, _fresh)){ //dispose of superfluous Shell event _result = _buffered; _buffered = null; } else if (isWidgetDisposalCase(_buffered, _fresh)) { handleWidgetCloseCase(); } else if (isCRCase(_buffered, _fresh)) { //dispose of superfluous CR _result = _buffered; _buffered = null; /* * Focus events that succeed tab traversals can be ignored. */ } else if (isTabTraversalEvent(_buffered) && _fresh instanceof SemanticFocusEvent) { //System.out.println("focus ignored: " + _fresh); _result = _buffered; _buffered = null; /** * Workaround for the demo to ensure that all changes to focus on a text * box are guaranteed to create a setFocus event. * NOTE: this is a TEMPORARY FIX: it will become unnecessary when we have * proper traversal event handling. */ } else if (targetIsText(_buffered)) { //System.out.println("target is text!"); //just advance to the next token advanceToNextToken(); } else if (_buffered instanceof SemanticFocusEvent && (!(_fresh instanceof SemanticKeyDownEvent) /*&& !(_fresh instanceof SemanticFocusEvent)*/ )) { /** * Focus events that precede anything BUT text entry and other focus events can be discarded. * Setting focus is redundant... */ //uncommenting focus events causes lots of superfluous focus changes to be codegened _buffered = _fresh; _result = null; } else { /** * Check for two focus events for the same element in a row; in which case, * we can discard one */ if (_buffered instanceof SemanticFocusEvent && _fresh instanceof SemanticFocusEvent && _buffered.getHierarchyInfo().equals(_fresh.getHierarchyInfo())) { _buffered = _fresh; _result = null; //this check may or may not help (they could be an intervening ALT-TAB) keystroke that gets ignored later //a second fix occurs in code-gen } else if (isPullDownMenuCase(_fresh)) { /* * Strategy: parcel pull-down target in parent info of menu select */ final SemanticMenuSelectionEvent menuSelect = (SemanticMenuSelectionEvent)_fresh; IWidgetIdentifier info = menuSelect.getHierarchyInfo(); if (_buffered != null) { // note: only addressing NEW API case if (info instanceof SWTWidgetLocator) { IUISemanticEvent parent = _buffered; // advance the fresh menu select to the buffer _buffered = _fresh; // and update its hierarchy info with a synthesized // pull-down: _buffered.setHierarchyInfo(new SWTSemanticEventFactoryImplV2() .createPullDownMenuSelection((SWTWidgetLocator) parent.getHierarchyInfo(), menuSelect)); // set current to null _result = null; } else { if (_lastEvent != null) { SWTWidgetLocator pullDownHost = getPullDownHostLocator(event); if (pullDownHost == null) { advanceToNextToken(); } else { _buffered = _fresh; _buffered.setHierarchyInfo(new SWTSemanticEventFactoryImplV2().createPullDownMenuSelection(pullDownHost, menuSelect)); // set current to null _result = null; } } else { advanceToNextToken(); } } } else { //view case? String viewID = getActiveViewPartId(); if (viewID == null) advanceToNextToken(); else { _fresh.setHierarchyInfo(new SWTSemanticEventFactoryImplV2().createPullDownMenuSelection(new ViewLocator(viewID), menuSelect)); _buffered = _fresh; _result = null; } } } else { advanceToNextToken(); } } } //buffer for view menu source _lastEvent = event; return sanityCheck(_result); } //last chance to filter results private IUISemanticEvent sanityCheck(IUISemanticEvent result) { if (isSpuriousPulldownMenuSelection(result)) return null; return result; } /** * @param result * @return * @since 3.9.1 */ private boolean isSpuriousPulldownMenuSelection(IUISemanticEvent result) { if (result == null) return false; IWidgetIdentifier info = result.getHierarchyInfo(); return result instanceof SemanticMenuSelectionEvent && info == null; } private SWTWidgetLocator getPullDownHostLocator(Event currentEvent) { if (isViewPullDown(currentEvent)) { Widget lastWidget = _lastEvent.widget; return ViewFinder.findInViewStack(lastWidget); } return (SWTWidgetLocator) _buffered.getHierarchyInfo(); } private boolean isViewPullDown(Event event) { Widget widget = event.widget; if (!(widget instanceof MenuItem)) return false; MenuItem item = (MenuItem)widget; Object data = item.getData(); if (!(data instanceof ActionContributionItem)) return false; ActionContributionItem action = (ActionContributionItem)data; IContributionManager parent = action.getParent(); return isViewManager(parent); } private boolean isViewManager(IContributionManager parent) { if (parent == null) return false; String name = parent.getClass().getName(); if (name.startsWith("org.eclipse.ui.internal.ViewPane")) return true; if (parent instanceof MenuManager) return isViewManager( ((MenuManager)parent).getParent()); return false; } private void handleWidgetCloseCase() { //update close target _fresh.setHierarchyInfo(_buffered.getHierarchyInfo()); //advance _buffered =_fresh; /* * NOTE: since we are eagerly flushing, the codegenerator will have to ignore the selection before the close */ } private boolean isWidgetDisposalCase(IUISemanticEvent buffered, IUISemanticEvent fresh) { return (buffered instanceof SemanticWidgetSelectionEvent && fresh instanceof SemanticWidgetClosedEvent); } private void advanceToNextToken() { //just advance to the next token _result = _buffered; _buffered = _fresh; } private String getActiveViewPartId() { try { return (String) DisplayExec.sync(new RunnableWithResult() { public Object runWithResult() { IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); IWorkbenchPart part = page.getActivePart(); if (!(part instanceof IViewPart)) return null; IViewPart view = (IViewPart) part; return view.getSite().getId(); } }); } catch (Throwable e) { return null; } } private boolean isPullDownMenuCase(IUISemanticEvent event) { if (!(event instanceof SemanticMenuSelectionEvent)) return false; SemanticMenuSelectionEvent menuSelect = (SemanticMenuSelectionEvent)event; if (StyleBits.isPullDown(menuSelect.getStyle())) return true; return false; } /** * Translates the given SWT typed event into an SWT semantic event. * @return the semantic event -- or <code>null</code> if there is none (and it should be ignored) */ public IUISemanticEvent parse(TypedEvent event) { /* * N.B. Typed events bypass our buffering scheme. * flush() should have been called previous to this parse. */ IUISemanticEvent current = _interpreter.interpret(event); /* * for some reason drops are producing two events... * in this case we want to nullify one of them */ if (_lastTypedEvent != null && _lastTypedEvent instanceof DropTargetEvent) { DropTargetEvent lastDrop = (DropTargetEvent)_lastTypedEvent; if (event instanceof DropTargetEvent) { DropTargetEvent currentDrop = (DropTargetEvent)event; if (lastDrop.data == currentDrop.data) current = null; } } //update pointer _lastTypedEvent = event; return current; } private boolean isSuperfluousShellChangeEvent(IUISemanticEvent last, IUISemanticEvent current) { //TODO: reimplement this? (parent shell info has been refactored away...) return false; // if (!(last instanceof SemanticShellShowingEvent)) // return false; // return last.getParentShellTitle().equals(current.getParentShellTitle()); } //widgetSelect(w=x) keyDown('\r' w=x) private boolean isCRCase(IUISemanticEvent last, IUISemanticEvent current) { //System.out.println("&&&&comparing: " + last + " to " + current); if (!(last instanceof SemanticWidgetSelectionEvent)) return false; if (!(current instanceof SemanticKeyDownEvent)) return false; SemanticKeyDownEvent kde = (SemanticKeyDownEvent)current; //System.out.println("&&&&checking char"); //if it's not a carriage return if (!"\r".equals(kde.getKey())) return false; //System.out.println("&&&&checking hierarchy info: " + last + " " + current); return sameHierarchyInfo(last, current); } private boolean isArrowTraversalCase(IUISemanticEvent last, IUISemanticEvent current) { if (!(last instanceof SemanticKeyDownEvent)) return false; SemanticKeyDownEvent kde = (SemanticKeyDownEvent)last; if (!isArrowTraversalEvent(kde)) return false; if (!(current instanceof ISemanticSelectionEvent)) return false; /* * Finally, we want to make sure that the events are associated with the same widget. */ return sameHierarchyInfo(last, current); } private boolean sameHierarchyInfo(IUISemanticEvent last, IUISemanticEvent current) { IWidgetIdentifier info1 = last.getHierarchyInfo(); IWidgetIdentifier info2 = current.getHierarchyInfo(); if (info1 == null) return info2 == null; return info1.equals(info2); } private boolean isArrowTraversalEvent(SemanticKeyDownEvent kde) { switch(kde.getKeyCode()) { case SWT.ARROW_RIGHT : return true; case SWT.ARROW_LEFT : return true; case SWT.ARROW_UP : return true; case SWT.ARROW_DOWN : return true; default : return false; } } private boolean isTabTraversalEvent(IUISemanticEvent event) { if (!(event instanceof SemanticKeyDownEvent)) return false; SemanticKeyDownEvent keyDown = (SemanticKeyDownEvent)event; return "\t".equals(keyDown.getKey()); } /** * Check to see if the target of this event is a Text widget. * !pq: note: this is a temporary workaround, when traverse events * are properly handled, this should go away. */ private boolean targetIsText(IUISemanticEvent event) { /* * day before release: treading lightly (i.e., lots of guards) * */ if (event == null) return false; IWidgetIdentifier info = event.getHierarchyInfo(); if (info == null) return false; String clsName = info.getTargetClassName(); if (clsName == null) return false; //System.out.println("target is: " + clsName); return clsName.equals("org.eclipse.swt.widgets.Text"); } /** * Flush the buffer, returning the buffered event. * @return the buffered event (or null if there is none) */ public IUISemanticEvent flush() { _result = _buffered; _buffered = null; return sanityCheck(_result); } /** * Handle the double-click case. */ private IUISemanticEvent handleDoubleClick(IUISemanticEvent event) { if (event == null) { //no buffered event return _fresh; } else if (event instanceof SemanticTreeItemSelectionEvent) { SemanticTreeItemSelectionEvent treeSelect = (SemanticTreeItemSelectionEvent)event; //TreeEventType type = treeSelect.getType(); //assert type == single-click here... treeSelect.setType(TreeEventType.DOUBLE_CLICK); } else if (event instanceof SemanticListSelectionEvent) { SemanticListSelectionEvent listSelect = (SemanticListSelectionEvent)event; listSelect.setClicks(2); } else if (event instanceof SemanticWidgetSelectionEvent) { IWidgetIdentifier current = event.getHierarchyInfo(); //tables require special treatment //TODO[pq]: this isn't right -- investigate! if (current.equals(Table.class) && TableItem.class.equals((_buffered.getHierarchyInfo()).getTargetClass())) { //discard Table selection event = _buffered; } SemanticWidgetSelectionEvent wSelect = (SemanticWidgetSelectionEvent)event; wSelect.setClicks(2); } //TODO: combos? return event; } }