/*******************************************************************************
* 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 java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.events.TreeListener;
import org.eclipse.swt.events.TypedEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.List;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Widget;
import abbot.Platform;
import abbot.tester.swt.ButtonTester;
import abbot.tester.swt.TreeTester;
import com.windowtester.internal.debug.Logger;
import com.windowtester.internal.debug.Tracer;
import com.windowtester.internal.runtime.MouseConfig;
import com.windowtester.recorder.event.IUISemanticEvent;
import com.windowtester.recorder.event.user.SemanticDropEvent;
import com.windowtester.recorder.event.user.SemanticEventAdapter;
import com.windowtester.recorder.event.user.SemanticKeyDownEvent;
import com.windowtester.recorder.event.user.SemanticTreeItemSelectionEvent;
import com.windowtester.recorder.event.user.TreeEventType;
import com.windowtester.recorder.event.user.UISemanticEvent;
import com.windowtester.runtime.swt.internal.debug.LogHandler;
import com.windowtester.swt.event.model.dnd.DropTargetHelper;
import com.windowtester.swt.event.recorder.IEventRecorderPluginTraceOptions;
import com.windowtester.swt.event.spy.SpyEventHandler;
import com.windowtester.swt.util.SelectionDeltaParser;
/**
* A simple stateful interpreter for semantic events.
*/
public class SWTSemanticEventInterpreter extends SemanticEventAdapter {
//enable/disable interpreter tracing to the console
private static final boolean CONSOLE_TRACE = false;
/** Flags to indicate if this selection is associated with a mouse event */
private boolean _singleClick, _doubleClick;
/** A helper for interpreting mouse events */
//private MouseEventInterpreter _mouseHandler = new MouseEventInterpreter();
/** A helper for interpreting tree mouse events */
private TreeEventInterpreter _treeEventHandler = new TreeEventInterpreter();
/** A helper for interpreting list selection events */
private ListSelectionHandler _listSelectionHandler;
/** A helper for interpreting browser selection events */
private BrowserHandler _browserHandler;
/** A helper for interpreting table item selection events */
//private TableSelectionHandler _tableSelectionHandler;
/** A helper for interpreting text traversal events */
private ControlTraversalHandler _controlTraversalHandler = new ControlTraversalHandler();
/** A helper for interpreting text traversal events */
//private KeyEntryHandler _keyEntryHandler = new KeyEntryHandler();
/** A callback to update raw event listening filters */
private IEventRecorderCallBack _recorderCallBack;
/** A stack for caching key strokes */
private Stack _keyStack = new Stack();
/** A reference to the currently clicked button (useful for determining context click cases) */
private int _button;
/** A reference to the target of a context click */
private Widget _contextTarget;
/** A back pointer to a _cCombo for use in handling cCombo selections */
private CCombo _cComboTarget;
/** A reference to the control that has current display focus
* This is required to address a bug in detecting double-clicks on items in controls that
* have initial focus. The rub is that we use the FocusIn event to add handlers that detect the
* double click. Controls with initial focus don't have such an event so handlers are not added.
* Our workaround is to check on mouse entry whether this current focus pointer is set. If not,
* it is updated to the Display's control in current focus.
*
* TODO: this work-around can probably be made to go away in light of the new traversal
* handler logic.
*/
private Control _controlInFocus;
private boolean _isDoubleClickEventCase;
private UISemanticEvent _contextTableSelection;
/** A pointer to the last mouse event for use in click statemask discovery */
private Event _lastMouseEvent;
/** a pointer to a detail widget in mouse events --- used to detect user-driven disposals in things like ctabfolders */
private Widget _lastMouseEventDetail;
private SemanticTreeItemSelectionEvent _contextMenuTreeItemTarget;
/** A map of drop targets to controls --- used for dnd event generation */
private final Map _dropTargetMappings = new HashMap();
/** Check state for use in table item selection creation */
private boolean _isCheckSelection;
private final DNDHelper _dndHelper = new DNDHelper();
private Event lastEvent; //cached to avoid duplicate handlings of the same event
private final SpyEventHandler spyHandler = new SpyEventHandler();
private UISemanticEvent cachedDragStart;
/**
* 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 interpret(TypedEvent event) {
IUISemanticEvent semantic = null;
if (event instanceof DropTargetEvent) {
semantic = handleDrop((DropTargetEvent)event);
} else {
LogHandler.log("typed event of type: " + event.getClass() + " ignored");
}
return semantic;
}
/**
* 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 UISemanticEvent interpret(Event event) {
//1.16/2008: oddly: text entry events are getting doubled...
//this guard protects against this --- TODO: investigate, fix and remove guard!
if (isDuplicateEvent(event))
return null;
traceInterpretingEvent(event);
UISemanticEvent semantic = null;
Widget widget = event.widget;
switch (event.type) {
case SWT.Activate :
handleActivate(widget);
break;
case SWT.Deactivate :
handleDeActivate(widget);
break;
case SWT.MouseDown :
handleMouseDown(event);
break;
case SWT.MouseUp :
semantic = handleMouseUp(event, semantic, widget);
break;
case SWT.MouseDoubleClick :
semantic = handleDoubleClick(event, widget);
break;
case SWT.MouseEnter :
handleMouseEnter(widget);
break;
case SWT.FocusIn :
semantic = handleFocusIn(event, widget);
break;
case SWT.FocusOut :
semantic = handleFocusOut(widget);
break;
case SWT.Arm :
semantic = handleArm(event, semantic, widget);
break;
case SWT.Selection :
semantic = handleSelection(semantic, widget, event);
break;
case SWT.KeyDown :
semantic = handleKeyDown(event, semantic);
break;
case SWT.KeyUp :
handleKeyUp();
break;
case SWT.Show :
semantic = handleShow(event, widget);
break;
case SWT.Close :
semantic = handleClose(event, widget);
break;
case SWT.Dispose :
semantic = handleDispose(event, widget);
break;
case SWT.Move :
semantic = handleMove(event, widget);
break;
case SWT.Resize :
semantic = handleResize(event, widget);
break;
case SWT.Expand :
semantic = handleExpand(event, widget);
break;
case SWT.Collapse :
semantic = handleCollapse(event, widget);
break;
case SWT.DragDetect :
semantic = handleDragDetect(event, widget); //TODO: is this ever seen?
break;
case SWT.Hide :
//ignored for now
break;
case SWT.MouseHover :
semantic = handleHover(event);
break;
default : /* raw event */
semantic = handleDefault(event);
break;
}
return semantic;
}
private boolean isDuplicateEvent(Event event) {
Event cached = lastEvent;
lastEvent = event; //advance
return event == cached;
}
////////////////////////////////////////////////////////////////////////////////////////
//
// Specific event type handlers
//
////////////////////////////////////////////////////////////////////////////////////////
private UISemanticEvent handleDragDetect(Event event, Widget widget) {
/*
* sigh -- another special case:
*
* In case an explicit selection event is not generated for the drag start, this cached
* locator will be used later to recreate it.
*/
cachedDragStart = createDragStartSelectionEvent(event);
return _dndHelper.getDragSource();
}
private UISemanticEvent createDragStartSelectionEvent(Event event) {
if (event.widget instanceof Tree) {
UISemanticEvent treeSelection = _treeEventHandler.getEvent();
if (treeSelection != null)
return treeSelection;
}
return SWTSemanticEventFactory.createWidgetSelectionEvent(event);
}
/**
* Handle activation.
* (We need to track activation events in case a focus event was not sent.)
*/
private void handleActivate(Widget widget) {
// addHandlers(w);
// just add traversal handler
if (widget instanceof Control) {
Control control = (Control) widget;
//only add handler if control has focus
if (control.isFocusControl())
addControlFocusHandler(control);
}
if (widget instanceof Browser) {
_browserHandler = new BrowserHandler((Browser)widget);
}
/*
* N.B. there is no need to handle de-activations as listeners will be
* removed on focus out (TODO: and pause?)
*/
}
private void handleDeActivate(Widget widget) {
if (widget instanceof Browser) {
if(_browserHandler!=null){
_browserHandler.stopListening();
_browserHandler = null;
}
}
}
private void addControlFocusHandler(Control control) {
// _keyEntryHandler.startListeningTo(control);
_controlTraversalHandler.startListeningTo(control);
_dndHelper.startListeningTo(control);
}
private UISemanticEvent handleDoubleClick(Event event, Widget widget) {
UISemanticEvent semantic = createDragStartSelectionEvent(event);
semantic.setClicks(2);
if (widget instanceof Canvas) //canvases use (x,y) info for playback
semantic.setRequiresLocationInfo(true);
_isDoubleClickEventCase = true;
return semantic;
}
private void handleMouseEnter(Widget widget) {
/**
* Workaround to ensure that widget's with initial focus have handlers added.
*/
if (_controlInFocus == null) {
_controlInFocus = widget.getDisplay().getFocusControl();
//add the appropriate handlers...
addHandlers(_controlInFocus);
}
}
/**
* Handle a hover event.
*/
private UISemanticEvent handleHover(Event event) {
return spyHandler.interepretHover(event);
}
/////////////////////////////////////////////////////////////////////////////////////
//
// Specific event type handlers
//
/////////////////////////////////////////////////////////////////////////////////////
/**
* Handle drop event.
*/
private IUISemanticEvent handleDrop(DropTargetEvent event) {
Control target = (Control) _dropTargetMappings.get(event.widget);
SemanticDropEvent dropEvent = SWTSemanticEventFactory.createDropEvent(event, target);
UISemanticEvent dragSource = _dndHelper.getDragSource();
if (dragSource != null) {
dropEvent.withSource(dragSource);
} else {
if (cachedDragStart != null)
dropEvent.withSource(cachedDragStart);
}
return dropEvent;
}
/**
* Handle close event.
*/
private UISemanticEvent handleClose(Event event, Widget widget) {
if (widget instanceof Shell)
return SWTSemanticEventFactory.createShellClosingEvent(event);
return null;
}
/**
* Handle dispose event.
*/
private UISemanticEvent handleDispose(Event event, Widget widget) {
if (widget instanceof Shell)
return SWTSemanticEventFactory.createShellDisposedEvent(event);
if (widget instanceof CTabItem) {
if (lastEventWasAClickOnThisWidget(widget))
return SWTSemanticEventFactory.createWidgetClosedEvent(widget);
}
return null;
}
private boolean lastEventWasAClickOnThisWidget(Widget widget) {
if (_lastMouseEvent == null)
return false;
boolean isMouseEvent = (_lastMouseEvent.type == SWT.MouseDown || _lastMouseEvent.type == SWT.MouseUp);
boolean isWidget = (widget == _lastMouseEvent.widget || widget == _lastMouseEvent.item || widget == _lastMouseEventDetail);
return isMouseEvent && isWidget;
}
/**
* Handle resize event.
*/
private UISemanticEvent handleResize(Event event, Widget widget) {
//TODO: need to identify resize events that are strictly USER-generated
// if (widget instanceof Control)
// return SWTSemanticEventFactory.createResizeEvent(event, widget);
return null;
}
/**
* Handle move event.
*/
private UISemanticEvent handleMove(Event event, Widget widget) {
// TODO: need to identify resize events that are strictly USER-generated
// if (widget instanceof Control)
// return SWTSemanticEventFactory.createMoveEvent(event, widget);
return null;
}
/**
* Handle show event.
*/
private UISemanticEvent handleShow(Event event, Widget widget) {
//notice we have to ignore Shells associated with cCombos
if (widget instanceof Shell && _cComboTarget == null) {
//an approximation here --- we're assuming that shells with no children (contents?)
//are transient and uninteresting
//this issue can be reproduced via selection events on GEF palette items which trigger
//an empty shell to show
if (shellHasTitleOrChildren((Shell) widget))
return SWTSemanticEventFactory.createShellShowingEvent(event);
}
return null;
}
private boolean shellHasTitleOrChildren(Shell shell) {
try {
String text = shell.getText();
if (text != null && text.trim().length() != 0)
return true;
if (shell.getChildren().length != 0)
return true;
} catch (Exception e) { // $codepro.audit.disable emptyCatchClause
/*
* assuming that an exception is an indication of a really transient shell which can be ignored
*/
}
return false;
}
/**
* Handle collapse event.
*/
private UISemanticEvent handleCollapse(Event event, Widget widget) {
//collapses are not supported fully, this is just a place holder
//return SWTSemanticEventFactory.createTreeItemSelectionEvent(widget, TreeEventType.COLLAPSE, SWT.BUTTON1);
return null;
}
/**
* Handle expand event.
*/
private UISemanticEvent handleExpand(Event event, Widget widget) {
return null;
//expands are not supported fully, this is just a place holder
// return SWTSemanticEventFactory.createTreeItemSelectionEvent(widget, TreeEventType.EXPAND, SWT.BUTTON1);
}
/**
* Handle focus in change.
* @param event
*/
private UISemanticEvent handleFocusIn(Event event, Widget widget) {
//add the appropriate handlers...
addHandlers(widget);
traceEvent("focus in: " + widget);
/**
* Focus events should only be associated with Controls so a cast should be safe... still,
* we'll be careful.
*/
_controlInFocus = widget instanceof Control ? (Control)widget : null;
//again, be safe and ignore the (hopefully impossible) null case
// if (_controlInFocus != null)
// return SWTSemanticEventFactory.createFocusEvent(event, widget);
return null;
}
/**
* Add widget-specific handlers/listener.
*/
private void addHandlers(Widget widget) {
/**
* All controls get a traversal handler and a drop target listener
*/
if (widget instanceof Control) {
//System.out.println("adding traversal listener to: " + widget);
Control control = (Control)widget;
addControlFocusHandler(control);
addDropTargetListener(control);
}
//when trees get focus we want to setup a listener to inform us of selection events
if (widget instanceof Tree) {
Tree tree = (Tree)widget;
//tree.addListener(SWT.Selection, _mouseHandler);
//tree.addTreeListener(_mouseHandler);
//tree.addMouseListener(_mouseHandler);
tree.addListener(SWT.Selection, _treeEventHandler);
tree.addMouseListener(_treeEventHandler);
tree.addTreeListener(_treeEventHandler);
//similarly with CTabFolders
} else if (widget instanceof CTabFolder) {
//TODO: add CTabFolder support here...
} else if (widget instanceof List) {
List list = (List)widget;
_listSelectionHandler = new ListSelectionHandler(list);
} else if (widget instanceof Table) {
//_tableSelectionHandler = new TableSelectionHandler((Table)widget);
}
}
/**
* Add a callback to the event recorder whcih is used to update
* filters on primitive events (e.g., when adding a drop target listener)
* @param recorder
*/
public void addEventRecorderCallBack(IEventRecorderCallBack recorder) {
_recorderCallBack = recorder;
}
private void addDropTargetListener(final Control control) {
if (_recorderCallBack == null) {
LogHandler.log("recorder callback is null; drop target listener not added");
return;
}
DropTarget dropTarget = DropTargetHelper.findDropTarget(control);
//if a target was found, update the recorder and register a mapping
if (dropTarget != null) {
//tell the recorder to list to this target
_recorderCallBack.listenForDropEvents(dropTarget);
//remember the owner control; for use in semantic event generation
_dropTargetMappings.put(dropTarget, control);
}
// IDropHandler dropHandler = DropHandlerFactory.createHandler(control);
// //tell the recorder to listen to drops on this target
// _recorderCallBack.addDropHandler(dropHandler);
// try {
//
//// DropTarget dropTarget = (DropTarget) control.getData("DropTarget"); // note
//// Control parent = null;
////
//// if (dropTarget == null) {
//// ControlTester tester = new ControlTester();
////
//// parent = tester.getParent(control);
//// while (parent != null && dropTarget == null) {
//// dropTarget = (DropTarget) parent.getData("DropTarget");
//// parent = tester.getParent(parent);
//// }
////
//// if (dropTarget == null)
//// return; //ignored
//// }
////
////
//// dropTarget.addDropListener(new DropTargetAdapter() {
////
//// public void drop(DropTargetEvent event) {
//// System.out.println("dropped: " + event.widget + " - " + event.item + " "
//// + event.x + ", " + event.y + " (display relative)");
////
//// Display d = event.widget.getDisplay();
//// System.out.println("relative to control: " + control + ":");
//// System.out.println(d.map(null, control, event.x, event.y));
////
//// }
//// });
//
//// _recorderCallBack.listenForDropEvents(dropTarget);
//
//
//
//
// } catch (Throwable t) {
// LogHandler.log(t);
// }
}
/**
* Handle focus out change.
*/
private UISemanticEvent handleFocusOut(Widget widget) {
//fetch the associated traversal event (if there is one)
UISemanticEvent e = _controlTraversalHandler.getEvent();
//UISemanticEvent e = _keyEntryHandler.getEvent();
//System.out.println("..removing handlers for: " + widget);
removeHandlers(widget);
return e;
}
/**
* Remove widget-specific handlers/listener.
*/
private void removeHandlers(Widget widget) {
/**
* All controls get a traversal handler
*/
if (widget instanceof Control) {
Control control = (Control)widget;
//_controlTraversalHandler.stopListeningTo(control);
removeControlFocusHandler(control);
}
//when trees go out of focus we want to remove the associated tree event listener
if (widget instanceof Tree) {
Tree tree = (Tree)widget;
// tree.removeListener(SWT.Selection, _mouseHandler);
// tree.removeTreeListener(_mouseHandler);
// tree.removeMouseListener(_mouseHandler);
//
tree.addMouseListener(_treeEventHandler); //TODO: shouldn't this be a REMOVE?
tree.addTreeListener(_treeEventHandler);
//same with CTabFolders
} else if (widget instanceof CTabFolder) {
//TODO: add CTabFolder support here...
} else if (widget instanceof List) {
//TODO: setting this to null here is not safe since the focus out event gets called before the handler is queried
//_listSelectionHandler = null;
} else if (widget instanceof Table) {
//_tableSelectionHandler = null;
}
}
/**
* Remove control focus handler.
*/
private void removeControlFocusHandler(Control control) {
_controlTraversalHandler.stopListeningTo(control);
_dndHelper.stopListeningTo(control);
}
/**
* Handle key down events.
*/
private UISemanticEvent handleKeyDown(Event event, UISemanticEvent semantic) {
traceEvent("Key-down event: " + event.keyCode);
/*
* Here we cache key strokes (for use in shift-click and ctrl-key entry events
*/
//TODO: what happened here? do we no longer need to do this?
if (!isControlOrShift(event)) {
//create the keyDown event
semantic = SWTSemanticEventFactory.createKeyDownEvent(event);
//System.out.print((int)event.character);
//if the previous key was a ctrl char, set control flag
Integer topKey = peekAtKeyStack();
if (topKey != null && topKey.intValue() == SWT.CTRL) {
((SemanticKeyDownEvent)semantic).setIsControlSequence(true);
((SemanticKeyDownEvent)semantic).setKey((char)event.keyCode);
//N.B. no need to pop previous, it will be popped on key Up since this keystroke
//will not be on the stack...
} else {
//otherwise a simple push will do
_keyStack.push(new Integer(event.keyCode));
}
} else {
_keyStack.push(new Integer(event.keyCode)); //store it for later inspection
}
return semantic;
}
/**
* Handle key up events.
*/
private void handleKeyUp() {
//remove the key from the cache
popKeyStack();
resetButtonCache();
}
private void resetButtonCache() {
_button = MouseConfig.PRIMARY_BUTTON;
}
/**
* Handle arm events.
*/
private UISemanticEvent handleArm(Event event, UISemanticEvent semantic, Widget widget) {
/**
* Once, this is where we managed menu selections...
*/
return semantic;
}
/**
* Handle mouse down events.
*/
private void handleMouseDown(Event event) {
//cache the current button down type:
_button = event.button;
_lastMouseEvent = event;
_lastMouseEventDetail = getDetail(event);
}
/**
* Handle mouse up events.
*/
private UISemanticEvent handleMouseUp(Event event, UISemanticEvent semantic, Widget widget) {
_lastMouseEvent = event;
_lastMouseEventDetail = getDetail(event);
if (_isDoubleClickEventCase) {
/**
* Some widgets fire double-click events. In these cases, ignore the mouseUp.
*/
_isDoubleClickEventCase = false; //reset
} else if (widget instanceof Tree) {
semantic = _treeEventHandler.getEvent(); //on the way out, fetch the selection event
/*
* N.B. context selections create a (bogus) event preceding the menu selection which is
* handled on the associated MenuItemSelection; here we discard the event but cache the
* selected item for use in context menu event creation
*/
if (semantic != null && semantic.isContext()) {
_contextMenuTreeItemTarget = (SemanticTreeItemSelectionEvent) semantic;
semantic = null;
}
} else if ((widget instanceof TabFolder || widget instanceof CTabFolder) && !Platform.isOSX()) {
semantic = SWTSemanticEventFactory.createTabItemSelectionEvent(event);
//ctabitems MAY require xy
if (semantic != null && requiresLocationInfo(widget)) {
semantic.setRequiresLocationInfo(true);
}
} else if (widget instanceof List) {
//moved to selection....
} else if (widget instanceof Table) {
// //moved to selection....
// //here we grab the table context selection
// _contextTableSelection = _tableSelectionHandler.getEvent();
// //and cache the clicked item
// _clickedTableItem = _tableSelectionHandler.getSelection();
Point point = new Point(event.x, event.y);
Table table = (Table) widget;
TableItem item = table.getItem(point);
traceEvent("Mouse up on table item: " + item);
int index = 0;
if (item == null) {
index = getColumnIndex(table, point);
item = getItem(table, point);
}
//checks the state mask of the event
String mask = null;
if ((event.stateMask & (SWT.SHIFT | SWT.CTRL | SWT.ALT)) != 0 || _isCheckSelection) {
mask = "SWT.BUTTON1";
if ((event.stateMask & SWT.SHIFT) != 0)
mask = mask + " | SWT.SHIFT";
if ((event.stateMask & SWT.CTRL) != 0)
mask = mask + " | SWT.CTRL";
if ((event.stateMask & SWT.ALT) != 0)
mask = mask + " | SWT.ALT";
if (_isCheckSelection)
mask = mask + " | SWT.CHECK";
}
/**
* Sanity: in case the item cannot be found, we skip the event.
* TODO: characterize this corner case.
*/
if (item != null) {
UISemanticEvent semanticEvent = SWTSemanticEventFactory
.createTableItemSelectionEvent(table, item, index, mask);
// context case is handled in the associated menu selection
if (!isContextCase()) {
semantic = semanticEvent;
} else {
_contextTableSelection = semanticEvent;
}
}
} else if (widget instanceof Combo) {
//moved to selection....
} else if (widget instanceof ToolBar || widget instanceof ToolItem /* shouldn't see this but in case...*/) {
//moved to selection....
} else if (widget instanceof Button) {
//moved _from_ selection:
semantic = createDragStartSelectionEvent(event);
} else {
//context menus require us to cache the target of the menu click
if (isContextCase()) {
_contextTarget = event.widget;
} else {
UISemanticEvent dragSource = _dndHelper.getDragSource();
if (dragSource != null) {
/*
* Note we cache source in case we need to synthesize a click (happens if the click was not
* explicitly performed before the drag)
*/
semantic = SWTSemanticEventFactory.createDragToEvent(event).withSource(dragSource);
_dndHelper.processed();
} else
semantic = createDragStartSelectionEvent(event);
if (requiresLocationInfo(widget))
semantic.setRequiresLocationInfo(true);
}
//finally, ccombos require us to cache the ccombo since the selection happens in a
//forthcomming list selection
// if (widget instanceof Button) {
// Widget parent = new ButtonTester().getParent((Button)widget);
// if (parent instanceof CCombo) {
// _cComboTarget = (CCombo)parent; //cache the combo
// semantic = null; //nullify the selection event
// }
// }
//<--- moved to selection
}
return semantic;
}
private Widget getDetail(Event event) {
if (event.widget instanceof CTabFolder)
return ((CTabFolder)event.widget).getItem(new Point(event.x, event.y));
return null;
}
private TableItem getItem(Table table, Point point) {
int columnCount = table.getColumnCount();
TableItem[] items = table.getItems();
for (int i = 0; i < items.length; i++) {
TableItem item = (TableItem)items[i];
for (int j = 0; j < columnCount; ++j) {
if (item.getBounds(j).contains(point))
return item;
}
}
// TODO Auto-generated method stub
return null;
}
private int getColumnIndex(Table table, Point point) {
TableItem[] items = table.getItems();
int columnCount = table.getColumnCount();
if (columnCount == 0)
return 0;
Rectangle bounds = null;
for (int i = 0; i < items.length; i++) {
TableItem item = (TableItem)items[i];
for (int j=0; j < columnCount; ++j) {
bounds = item.getBounds(j);
if (bounds.contains(point))
return j;
}
}
return 0; //fail but default
}
/**
* Check to see if the given widget requires x,y info for playback.
*/
private boolean requiresLocationInfo(Object widget) {
if (widget == null)
return false; //being safe...
/**
* If a text widget has text in it, we want the click to be location conscious
* (in case there are text entries forthcoming)
*/
if (widget instanceof Text) {
Text text = (Text)widget;
String txt = text.getText();
return txt != null && txt.length()>0;
}
/** canvases use (x,y) info for playback by default*/
if (widget instanceof Canvas) {
//but like text widgets, styled text is location aware if it has contents
if (widget instanceof StyledText) {
StyledText stext = (StyledText)widget;
String txt = stext.getText();
return txt != null && txt.length()>0;
} else
return true;
}
//Moving to new close API
// /**
// * A kludge to add x,y info to ctabitems to attempt to reproduce close events
// */
// if (widget instanceof CTabItem) {
// return true; //if we're lucky, location will help reproduce close events
// }
if (widget instanceof CTabFolder)
return requiresLocationInfo(((CTabFolder)widget).getSelection());
//otherwise, unknown widgets fall back to use x,y coordinates
Package pkg = widget.getClass().getPackage();
String name = pkg.getName();
if ("org.eclipse.swt.widgets".equals(name))
return false;
if ("org.eclipse.swt.custom".equals(name))
return false;
return true;
}
/**
* Handle selection events.
*/
private UISemanticEvent handleSelection(UISemanticEvent semantic, Widget widget, Event event) {
traceEvent("selecting: " + widget);
//cache check state for use in table event creation
_isCheckSelection = isCheckSelection(event);
if (_isCheckSelection)
traceEvent("[check event]");
/**
* Check to see if the selection is associated with a traversal event first
* if it is, short-circuit regular selection handling.
*/
SemanticKeyDownEvent traversalEvent = _controlTraversalHandler.getEvent();
/*
* N.B. : for now we let the following logic continue even if it is a traversal
* (to clear the event cached by the selection handlers.
*/
if (widget instanceof List) {
//ignore lists that belong to a cCombo
if (_cComboTarget == null)
semantic = _listSelectionHandler.getEvent();
} else if (widget instanceof Table) {
//context case is handled in the menu item selection
// if (!isContextCase())
// semantic = _tableSelectionHandler.getEvent();
//^--- now handled in mouse up
} else if (widget instanceof MenuItem) {
if (isContextCase()) { //the context case requires a pointer back to the source of the menu
if (_contextTableSelection != null) {
semantic = SWTSemanticEventFactory.createContextMenuSelectionEvent(_contextTableSelection, event);
} else {
/*
* If we have a cached context target we use it:
*/
if (_contextTarget != null) {
semantic = SWTSemanticEventFactory.createContextMenuSelectionEvent(_contextTarget, event);
} else {
// /*
// * if we don't we try and infer it:
// */
// if (_controlInFocus != null) {
// semantic = SWTSemanticEventFactory.createContextMenuSelectionEventFromFocusControl(_controlInFocus, event);
// }
if (_contextMenuTreeItemTarget != null) {
semantic = SWTSemanticEventFactory.createTreeItemContextMenuSelectionEvent(_contextMenuTreeItemTarget, event);
} else {
if (_controlInFocus instanceof Tree) {
/*
* We assume this is a case where a tree item
* has initial focus and so never gets
* selected...
*/
TreeItem[] selection = new TreeTester().getSelection((Tree)_controlInFocus);
if (selection != null && selection.length > 0) {
/*
* We're taking the first selection (because we have no choice!)
*/
if (selection.length > 1)
LogHandler.log("selection count for tree with initial focus > 1; defaulting to first selection for context menu event recording");
SemanticTreeItemSelectionEvent targetSelect = (SemanticTreeItemSelectionEvent) SWTSemanticEventFactory.createTreeItemSelectionEvent(selection[0], TreeEventType.SINGLE_CLICK, 3);
semantic = SWTSemanticEventFactory.createTreeItemContextMenuSelectionEvent(targetSelect, event);
}
}
//fall-through: click on the control:
if (semantic == null)
semantic = SWTSemanticEventFactory.createContextMenuSelectionEvent(_controlInFocus, event);
}
}
}
_contextMenuTreeItemTarget = null; //reset
_contextTarget = null; //reset the context pointer for sanity
} else
semantic = SWTSemanticEventFactory.createMenuSelectionEvent(event);
} else if (widget instanceof ToolItem) {
semantic = createDragStartSelectionEvent(event);
} else if (widget instanceof Tree) {
//semantic = _treeEventHandler.getEvent();
} else if (widget instanceof Combo) {
semantic = SWTSemanticEventFactory.createComboSelectionEvent(event);
} else if (widget instanceof CCombo) {
semantic = SWTSemanticEventFactory.createCComboSelectionEvent(event);
_cComboTarget = null; //set target to null to clean up
} else if (widget instanceof Button) {
//moved to mouseup to avoid programmatic (vs. user) selections
//semantic = SWTSemanticEventFactory.createWidgetSelectionEvent(event);
//finally, ccombos require us to cache the ccombo since the selection happens in a
Widget parent = new ButtonTester().getParent((Button)widget);
if (parent instanceof CCombo) {
_cComboTarget = (CCombo)parent; //cache the combo
semantic = null; //nullify the selection event
}
} else if ((widget instanceof TabFolder || widget instanceof CTabFolder) && Platform.isOSX()) {
boolean skip = false;
if (widget instanceof TabFolder) {
TabFolder tab = (TabFolder) widget;
TabItem[] items = tab.getItems();
if (items.length == 1 && items[0] == event.item && items[0].getControl() == null) {
// see TabFolder.createItem()
skip = true;
}
}
if (!skip) {
semantic = SWTSemanticEventFactory.createTabItemSelectionEvent(event);
//ctabitems MAY require xy
if (semantic != null && requiresLocationInfo(widget)) {
semantic.setRequiresLocationInfo(true);
}
}
}
/*
* Now, having advanced the relevant handlers, check to see if we're
* actually in a traversal
*/
if (traversalEvent != null)
semantic = traversalEvent;
if ((semantic != null) && requiresLocationInfo(widget))
semantic.setRequiresLocationInfo(true);
return semantic;
}
/**
* Handle fall-through/default events.
*/
private UISemanticEvent handleDefault(Event event) {
UISemanticEvent semantic = null;
// if (event.type == SWT.DefaultSelection)
// // semantic = SWTSemanticEventFactory.createDefaultSelectionEvent(event);
// ; //now ignored
if (event.type == SWT.DragDetect) {
//check handlers for cached events
semantic = checkHandlersForCachedEvents();
//if there's none cached, create a raw event
if (semantic == null)
semantic = SWTSemanticEventFactory.createRawEvent(event);
//finally: turn the event into a drag event
semantic = SWTSemanticEventFactory.createDragEvent(semantic);
} else {
semantic = SWTSemanticEventFactory.createRawEvent(event);
}
return semantic;
}
/**
* Check handlers for for cached events.
*/
private UISemanticEvent checkHandlersForCachedEvents() {
UISemanticEvent cached = null;
cached = _treeEventHandler.getEvent();
if (cached == null && _listSelectionHandler != null)
cached = _listSelectionHandler.getEvent();
return cached;
}
/**
* Respond to start recorder events.
* @see com.windowtester.recorder.event.ISemanticEventListener#notifyStart()
*/
public void notifyStart() {
/*
* At start, discover initial focus and ensure the traversal listener is
* properly registered to listen.
*/
establishInitialFocus();
}
/**
* Respond to pause recorder events.
* @see com.windowtester.recorder.event.ISemanticEventListener#notifyPause()
*/
public void notifyPause() {
//remove traversal handler from all its listener queues
_controlTraversalHandler.clearInterests();
// _keyEntryHandler.clearInterests();
}
/**
* Respond to stop recorder events.
* @see com.windowtester.recorder.event.ISemanticEventListener#notifyStop()
*/
public void notifyStop() {
//remove traversal handler from all its listener queues
_controlTraversalHandler.clearInterests();
}
/* (non-Javadoc)
* @see com.windowtester.recorder.event.user.SemanticEventAdapter#notifySpyModeToggle()
*/
public void notifySpyModeToggle() {
spyHandler.spyModeToggled();
}
private void establishInitialFocus() {
//get the current active display
Display display = Display.getDefault();
if (display == null) {
Logger.log("Error getting current display");
return;
}
//get the current focus control
Control focusControl = display.getFocusControl();
if (focusControl == null) {
//changed log entry to trace:
Tracer.trace(IEventRecorderPluginTraceOptions.RECORDER_EVENTS,"Error getting current focus control");
return;
}
//add appropriate handlers
addHandlers(focusControl);
}
/////////////////////////////////////////////////////////////////////////////////////
//
// Helper predicates
//
/////////////////////////////////////////////////////////////////////////////////////
/**
* Is this a control or shift key event?
*/
private boolean isControlOrShift(Event event) {
return (event.keyCode&SWT.SHIFT)==SWT.SHIFT || (event.keyCode&SWT.CTRL)==SWT.CTRL;
}
/**
* Are we in a context selection?
*/
private boolean isContextCase() {
return _button == 3;
}
/**
* Peek at the key stack. If it is empty return null.
* @return the Integer at the top of the stack, or null if it's empty
*/
private Integer peekAtKeyStack() {
return _keyStack.isEmpty() ? null : (Integer)_keyStack.peek();
}
/**
* Pop the top key off the stack.
*/
private void popKeyStack() {
if (!_keyStack.isEmpty())
_keyStack.pop();
}
/**
* Check for an SWT.CHECK.
*/
private boolean isCheckSelection(Event event) {
return (event.detail & SWT.CHECK) != 0;
}
/**
* Handles interpretation of mouse and tree selection events.
*/
class TreeEventInterpreter implements MouseListener, TreeListener, Listener {
/** The cache constructured Semantic event */
private UISemanticEvent _cached;
/** The cached button */
private int _button;
/* (non-Javadoc)
* @see org.eclipse.swt.events.MouseListener#mouseDoubleClick(org.eclipse.swt.events.MouseEvent)
*/
public void mouseDoubleClick(MouseEvent e) {
traceEvent("Double clicked: " + e);
_doubleClick = true;
_button = e.button;
//System.out.println("double-click mouse selection event: " + item);
//_cached = SWTSemanticEventFactory.createTreeItemSelectionEvent(e.widget, TreeEventType.DOUBLE_CLICK, _button);
}
/* (non-Javadoc)
* @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events.MouseEvent)
*/
public void mouseDown(MouseEvent e) {
traceEvent("Mouse down: " + e);
_singleClick = true;
_button = e.button;
//_cached = SWTSemanticEventFactory.createTreeItemSelectionEvent(e.widget, TreeEventType.SINGLE_CLICK, _button);
}
/* (non-Javadoc)
* @see org.eclipse.swt.events.MouseListener#mouseUp(org.eclipse.swt.events.MouseEvent)
*/
public void mouseUp(MouseEvent e) {
//System.out.println("mouse up: " + e);
_singleClick = _doubleClick = false;
}
/**
* get the cached event, and set it to null
* @return the cached event
*/
UISemanticEvent getEvent() {
UISemanticEvent event = _cached;
_cached = null;
return event;
}
/* (non-Javadoc)
* @see org.eclipse.swt.events.TreeListener#treeCollapsed(org.eclipse.swt.events.TreeEvent)
*/
public void treeCollapsed(TreeEvent e) {
//FIXME[author=pq] SWTEvents are built around Events and NOT TreeEvents... must unify
//_cached = new SemanticTreeItemSelectionEvent(e, TreeEventType.COLLAPSE);
traceEvent("Collapse events unsupported...");
}
/* (non-Javadoc)
* @see org.eclipse.swt.events.TreeListener#treeExpanded(org.eclipse.swt.events.TreeEvent)
*/
public void treeExpanded(TreeEvent e) {
//_cached = new SemanticTreeItemSelectionEvent(e, TreeEventType.EXPAND);
traceEvent("Expand events unsupported...");
}
public void handleEvent(Event e) {
TreeItem item = (TreeItem)e.item;
boolean check = (e.detail & SWT.CHECK) != 0;
Integer code = peekAtKeyStack();
String mask = null;
if (code != null) {
if (code.intValue() == SWT.CTRL)
mask = "SWT.BUTTON1 | SWT.CTRL";
if (code.intValue() == SWT.SHIFT)
mask = "SWT.BUTTON1 | SWT.SHIFT";
}
/**
* Select events don't carry the button info so we need to pass it along too.
*/
if (_singleClick) {
traceEvent("Single-click mouse selection event: " + item + (check ? " [check]" : ""));
_cached = SWTSemanticEventFactory.createTreeItemSelectionEvent(e, TreeEventType.SINGLE_CLICK, _button);
} else if (_doubleClick) {
traceEvent("Double-click mouse selection event: " + item + (check ? " [check]" : ""));
_cached = SWTSemanticEventFactory.createTreeItemSelectionEvent(e, TreeEventType.DOUBLE_CLICK, _button);
}
if (check)
_cached.setChecked(true);
if (mask != null && _cached != null)
((SemanticTreeItemSelectionEvent)_cached).setMask(mask);
}
}
/**
* Base class for handling deltas in selections (e.g., tables and lists).
*/
abstract class SelectionHandler /*<T,I>*/ {
/** The cached constructed Semantic event */
private UISemanticEvent _cached;
/** The cached indices */
private int[] _cachedIndices;
/** The owner of this selection */
protected final Object /*<T>*/ _owner;
/**
* Create an instance.
*/
public SelectionHandler(Object /*<T>*/ owner) {
_owner = owner;
updateIndices();
}
/**
* Update cached indices.
*/
private void updateIndices() {
_cachedIndices = getSelectionIndices();
}
/**
* Get the cached event, and set it to null
* @return the cached event
*/
UISemanticEvent getEvent() {
String mask = null;
int selectType = 0;
if (_lastMouseEvent != null) {
if ((_lastMouseEvent.stateMask & (SWT.SHIFT | SWT.CTRL | SWT.ALT)) != 0) {
mask = "SWT.BUTTON1";
if ((_lastMouseEvent.stateMask & SWT.SHIFT) != 0)
mask = mask + " | SWT.SHIFT";
if ((_lastMouseEvent.stateMask & SWT.CTRL) != 0)
mask = mask + " | SWT.CTRL";
if ((_lastMouseEvent.stateMask & SWT.ALT) != 0)
mask = mask + " | SWT.ALT";
}
}
int index = 0;
try {
index = SelectionDeltaParser.indexToSelect(_cachedIndices, getSelectionIndices(), selectType);
} catch(Throwable t) {
/* A pesky and (very intermittent) bug causes an array index out of bounds exception
* in index parsing.
*/
Logger.log("Error in selection delta parsing", t);
}
Object /*<I>*/ selection = getItem(index);
_cached = createSelectionEvent(_owner, selection, mask);
updateIndices();
UISemanticEvent event = _cached;
_cached = null;
return event;
}
/**
* Get the selection indices for this parent.
*/
protected abstract int[] getSelectionIndices();
/**
* Create the appropriate selection event.
*/
protected abstract UISemanticEvent createSelectionEvent(Object /*<T>*/ owner, Object /*<I>*/ selection, String mask);
/**
* Get the item at the given index.
*/
protected abstract Object /*<I>*/ getItem(int index);
}
/**
* A selection handler tailored for lists.
*/
class ListSelectionHandler extends SelectionHandler {
/**
* Create an instance.
* @param list
*/
public ListSelectionHandler(List list) {
super(list);
}
/**
* @see com.windowtester.swt.event.model.SWTSemanticEventInterpreter.SelectionHandler#getSelectionIndices()
*/
protected int[] getSelectionIndices() {
return getList().getSelectionIndices();
}
/**
* @return the underlying list
*/
public List getList() {
return (List)_owner;
}
/**
* @see com.windowtester.swt.event.model.SWTSemanticEventInterpreter.SelectionHandler#createSelectionEvent(java.lang.Object, java.lang.Object, java.lang.String)
*/
protected UISemanticEvent createSelectionEvent(Object owner, Object selection, String mask) {
return SWTSemanticEventFactory.createListItemSelectionEvent(getList(), (String)selection, mask);
}
/**
* @see com.windowtester.swt.event.model.SWTSemanticEventInterpreter.SelectionHandler#getItem(int)
*/
protected Object getItem(int index) {
return getList().getItem(index);
}
}
class ControlTraversalHandler implements TraverseListener {
/** The current traversal event */
TraverseEvent _event;
/** Collection of controls to which this handler is listening */
java.util.List _interests = new ArrayList();
/**
* Handle a key traversal event.
* @see org.eclipse.swt.events.TraverseListener#keyTraversed(org.eclipse.swt.events.TraverseEvent)
*/
public void keyTraversed(TraverseEvent e) {
_event = e;
//System.out.println("-->key traversed: " + e);
}
/**
* @return the Semantic event associated with the last recorded traversal event
*/
SemanticKeyDownEvent getEvent() {
if (_event == null) // || _event.character != '\t')
return null;
//reset for next call
TraverseEvent current = _event;
_event = null;
//System.out.println("traversal event gotten" + current);
return SWTSemanticEventFactory.createKeyDownEvent(current);
}
void startListeningTo(Control c) {
if (!_interests.contains(c)) {
c.addTraverseListener(this);
_interests.add(c);
}
}
synchronized void stopListeningTo(Control c) {
if (!c.isDisposed()) //check may be unnecessary but better to be safe...
c.removeTraverseListener(this);
_interests.remove(c);
}
//remove all registered interests
synchronized void clearInterests() {
/*
* We'd like to do something obvious like:
* for (Iterator iter = _interests.iterator(); iter.hasNext(); )
* stopListeningTo((Control)iter.next());
* but the removals are causing ConcurrentModification Exceptions to be thrown...
*/
int size = _interests.size();
for (int i= 0; i < size; ++i)
stopListeningTo((Control)_interests.get(0));
}
}
class KeyEntryHandler implements KeyListener {
/** The current key event */
KeyEvent _event;
/** Collection of controls to which this handler is listening */
Collection _interests = new ArrayList();
public void keyPressed(KeyEvent e) {
//no-op
//key cached on release
}
public void keyReleased(KeyEvent e) {
_event = e;
}
/**
* @return the Semantic event associated with the last recorded traversal event
*/
SemanticKeyDownEvent getEvent() {
if (_event == null || _event.character != '\t')
return null;
//reset for next call
KeyEvent current = _event;
_event = null;
//System.out.println("traversal event gotten" + current);
return SWTSemanticEventFactory.createKeyDownEvent(current);
}
void startListeningTo(Control c) {
if (!_interests.contains(c)) {
c.addKeyListener(this);
_interests.add(c);
// Control parent = c.getParent();
// if (parent != null) {
// startListeningTo(parent); //TODO: mirror this in stop
// System.out.println("got control parent: " + parent);
// }
}
}
void stopListeningTo(Control c) {
if (!c.isDisposed()) //check may be unnecessary but better to be safe...
c.removeKeyListener(this);
_interests.remove(c);
}
//remove all registered interests
void clearInterests() {
//TODO: throws ConcurrentModificationErrors...
for (Iterator iter = _interests.iterator(); iter.hasNext(); )
stopListeningTo((Control)iter.next());
}
}
class BrowserHandler implements MouseListener, LocationListener, Listener {
private final Browser _browser;
public BrowserHandler(Browser browser) {
_browser = browser;
startListening();
}
public void mouseDoubleClick(MouseEvent e) {
System.out.println("browser doubleClick: " + e);
}
public void mouseDown(MouseEvent e) {
System.out.println("browser mouseDown: " + e);
}
public void mouseUp(MouseEvent e) {
System.out.println("browser mouseUp: " + e);
}
public void startListening() {
_browser.addMouseListener(this);
_browser.addLocationListener(this);
Control[] children = _browser.getChildren();
for (int i=0 ; i < children.length; ++i) {
Control child = children[i];
child.addMouseListener(this);
}
}
public void stopListening() {
_browser.removeMouseListener(this);
_browser.removeLocationListener(this);
}
public void changing(LocationEvent event) {
System.out.println("changing: " + event);
}
public void changed(LocationEvent event) {
System.out.println("changed: " + event);
}
public void handleEvent(Event event) {
System.out.println("browser got: " + event);
}
}
/**
* Send this event-related tracing message to the tracer.
* @param event - the event to trace
*/
private void traceInterpretingEvent(Event event) {
String eventDescription = "Interpreting ";
try {
if (event.widget instanceof Menu && event.type == SWT.Dispose) {
eventDescription += "{Menu Disposed}";
} else {
eventDescription += event.toString();
}
} catch(RuntimeException e) {
eventDescription += "<unable to get event description>";
}
traceEvent(eventDescription);
}
/**
* Send this event-related tracing message to the tracer.
* @param msg - the message to trace
*/
private void traceEvent(String eventDescription) {
if (CONSOLE_TRACE)
System.out.println(eventDescription);
Tracer.trace(IEventRecorderPluginTraceOptions.SWT_EVENTS, eventDescription);
}
}