blob: 1a17e7e576cb1987b5749054ca7c2ad8ad3889e4 [file] [log] [blame]
package org.eclipse.swt.widgets;
/*
* (c) Copyright IBM Corp. 2000, 2001.
* All Rights Reserved
*/
import org.eclipse.swt.internal.*;
import org.eclipse.swt.internal.win32.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.*;
import org.eclipse.swt.events.*;
/**
* Instances of this class implement rubber banding rectangles.
*
* <dl>
* <dt><b>Styles:</b></dt>
* <dd>(none)</dd>
* <dt><b>Events:</b></dt>
* <dd>Move</dd>
* </dl>
* <p>
* IMPORTANT: This class is <em>not</em> intended to be subclassed.
* </p>
*/
public class Tracker extends Widget {
Control parent;
Display display;
boolean tracking, stippled;
Rectangle [] rectangles, proportions;
int resizeCursor, clientCursor, cursorOrientation = SWT.NONE;
boolean inEvent = false;
/*
* The following values mirror step sizes on Windows
*/
final static int STEPSIZE_SMALL = 1;
final static int STEPSIZE_LARGE = 9;
/**
* Constructs a new instance of this class given its parent
* and a style value describing its behavior and appearance.
* <p>
* The style value is either one of the style constants defined in
* class <code>SWT</code> which is applicable to instances of this
* class, or must be built by <em>bitwise OR</em>'ing together
* (that is, using the <code>int</code> "|" operator) two or more
* of those <code>SWT</code> style constants. The class description
* for all SWT widget classes should include a comment which
* describes the style constants which are applicable to the class.
* </p>
*
* @param parent a widget which will be the parent of the new instance (cannot be null)
* @param style the style of widget to construct
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the parent is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
* <li>ERROR_INVALID_SUBCLASS - if this class is not an allowed subclass</li>
* </ul>
*
* @see SWT
* @see Widget#checkSubclass
* @see Widget#getStyle
*/
public Tracker (Composite parent, int style) {
super (parent, checkStyle (style));
this.parent = parent;
display = parent.getDisplay ();
}
/**
* Constructs a new instance of this class given the display
* to create it on and a style value describing its behavior
* and appearance.
* <p>
* The style value is either one of the style constants defined in
* class <code>SWT</code> which is applicable to instances of this
* class, or must be built by <em>bitwise OR</em>'ing together
* (that is, using the <code>int</code> "|" operator) two or more
* of those <code>SWT</code> style constants. The class description
* for all SWT widget classes should include a comment which
* describes the style constants which are applicable to the class.
* </p><p>
* Note: Currently, null can be passed in for the display argument.
* This has the effect of creating the tracker on the currently active
* display if there is one. If there is no current display, the
* tracker is created on a "default" display. <b>Passing in null as
* the display argument is not considered to be good coding style,
* and may not be supported in a future release of SWT.</b>
* </p>
*
* @param display the display to create the tracker on
* @param style the style of control to construct
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
* <li>ERROR_INVALID_SUBCLASS - if this class is not an allowed subclass</li>
* </ul>
*/
public Tracker (Display display, int style) {
if (display == null) display = Display.getCurrent ();
if (display == null) display = Display.getDefault ();
if (!display.isValidThread ()) {
error (SWT.ERROR_THREAD_INVALID_ACCESS);
}
this.style = checkStyle (style);
this.display = display;
}
/**
* Adds the listener to the collection of listeners who will
* be notified when the control is moved or resized, by sending
* it one of the messages defined in the <code>ControlListener</code>
* interface.
*
* @param listener the listener which should be notified
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @see ControlListener
* @see #removeControlListener
*/
public void addControlListener (ControlListener listener) {
checkWidget ();
if (listener == null) error (SWT.ERROR_NULL_ARGUMENT);
TypedListener typedListener = new TypedListener (listener);
addListener (SWT.Move,typedListener);
}
Point adjustMoveCursor () {
Rectangle bounds = computeBounds ();
int newX = bounds.x + bounds.width / 2;
int newY = bounds.y;
POINT pt = new POINT ();
pt.x = newX; pt.y = newY;
/*
* Convert to screen coordinates iff needed
*/
if (parent != null) {
OS.ClientToScreen (parent.handle, pt);
}
OS.SetCursorPos (pt.x, pt.y);
return new Point (pt.x, pt.y);
}
Point adjustResizeCursor () {
int newX, newY;
Rectangle bounds = computeBounds ();
if ((cursorOrientation & SWT.LEFT) != 0) {
newX = bounds.x;
} else if ((cursorOrientation & SWT.RIGHT) != 0) {
newX = bounds.x + bounds.width;
} else {
newX = bounds.x + bounds.width / 2;
}
if ((cursorOrientation & SWT.UP) != 0) {
newY = bounds.y;
} else if ((cursorOrientation & SWT.DOWN) != 0) {
newY = bounds.y + bounds.height;
} else {
newY = bounds.y + bounds.height / 2;
}
POINT pt = new POINT ();
pt.x = newX; pt.y = newY;
/*
* Convert to screen coordinates iff needed
*/
if (parent != null) {
OS.ClientToScreen (parent.handle, pt);
}
OS.SetCursorPos (pt.x, pt.y);
/*
* If the client has not provided a custom cursor then determine
* the appropriate resize cursor.
*/
if (clientCursor == 0) {
int newCursor = 0;
switch (cursorOrientation) {
case SWT.UP:
newCursor = OS.LoadCursor (0, OS.IDC_SIZENS);
break;
case SWT.DOWN:
newCursor = OS.LoadCursor (0, OS.IDC_SIZENS);
break;
case SWT.LEFT:
newCursor = OS.LoadCursor (0, OS.IDC_SIZEWE);
break;
case SWT.RIGHT:
newCursor = OS.LoadCursor (0, OS.IDC_SIZEWE);
break;
case SWT.LEFT | SWT.UP:
newCursor = OS.LoadCursor (0, OS.IDC_SIZENWSE);
break;
case SWT.RIGHT | SWT.DOWN:
newCursor = OS.LoadCursor (0, OS.IDC_SIZENWSE);
break;
case SWT.LEFT | SWT.DOWN:
newCursor = OS.LoadCursor (0, OS.IDC_SIZENESW);
break;
case SWT.RIGHT | SWT.UP:
newCursor = OS.LoadCursor (0, OS.IDC_SIZENESW);
break;
default:
newCursor = OS.LoadCursor (0, OS.IDC_SIZEALL);
break;
}
OS.SetCursor (newCursor);
if (resizeCursor != 0) {
OS.DestroyCursor (resizeCursor);
}
resizeCursor = newCursor;
}
return new Point (pt.x, pt.y);
}
static int checkStyle (int style) {
if ((style & (SWT.LEFT | SWT.RIGHT | SWT.UP | SWT.DOWN)) == 0) {
style |= SWT.LEFT | SWT.RIGHT | SWT.UP | SWT.DOWN;
}
return style;
}
/**
* Stop displaying the tracker rectangles.
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public void close () {
checkWidget ();
tracking = false;
}
Rectangle computeBounds () {
int xMin = rectangles [0].x;
int yMin = rectangles [0].y;
int xMax = rectangles [0].x + rectangles [0].width;
int yMax = rectangles [0].y + rectangles [0].height;
for (int i = 1; i < rectangles.length; i++) {
if (rectangles [i].x < xMin) xMin = rectangles [i].x;
if (rectangles [i].y < yMin) yMin = rectangles [i].y;
int rectRight = rectangles [i].x + rectangles [i].width;
if (rectRight > xMax) xMax = rectRight;
int rectBottom = rectangles [i].y + rectangles [i].height;
if (rectBottom > yMax) yMax = rectBottom;
}
return new Rectangle (xMin, yMin, xMax - xMin, yMax - yMin);
}
Rectangle [] computeProportions (Rectangle [] rects) {
Rectangle [] result = new Rectangle [rects.length];
Rectangle bounds = computeBounds ();
for (int i = 0; i < rects.length; i++) {
result[i] = new Rectangle (
(rects[i].x - bounds.x) * 100 / bounds.width,
(rects[i].y - bounds.y) * 100 / bounds.height,
rects[i].width * 100 / bounds.width,
rects[i].height * 100 / bounds.height);
}
return result;
}
/**
* Draw the rectangles displayed by the tracker.
*/
void drawRectangles (Rectangle [] rects) {
if (parent != null) {
if (parent.isDisposed ()) return;
parent.getShell ().update ();
} else {
display.update ();
}
int bandWidth = 1;
int hwndTrack = OS.GetDesktopWindow ();
if (parent != null) hwndTrack = parent.handle;
int hDC = OS.GetDCEx (hwndTrack, 0, OS.DCX_CACHE);
int hBitmap = 0, hBrush = 0, oldBrush = 0;
if (stippled) {
bandWidth = 3;
byte [] bits = {-86, 0, 85, 0, -86, 0, 85, 0, -86, 0, 85, 0, -86, 0, 85, 0};
hBitmap = OS.CreateBitmap (8, 8, 1, 1, bits);
hBrush = OS.CreatePatternBrush (hBitmap);
oldBrush = OS.SelectObject (hDC, hBrush);
}
for (int i=0; i<rects.length; i++) {
Rectangle rect = rects [i];
OS.PatBlt (hDC, rect.x, rect.y, rect.width, bandWidth, OS.PATINVERT);
OS.PatBlt (hDC, rect.x, rect.y + bandWidth, bandWidth, rect.height - (bandWidth * 2), OS.PATINVERT);
OS.PatBlt (hDC, rect.x + rect.width - bandWidth, rect.y + bandWidth, bandWidth, rect.height - (bandWidth * 2), OS.PATINVERT);
OS.PatBlt (hDC, rect.x, rect.y + rect.height - bandWidth, rect.width, bandWidth, OS.PATINVERT);
}
if (stippled) {
OS.SelectObject (hDC, oldBrush);
OS.DeleteObject (hBrush);
OS.DeleteObject (hBitmap);
}
OS.ReleaseDC (hwndTrack, hDC);
}
public Display getDisplay () {
return display;
}
/**
* Returns the bounds of the Rectangles being drawn.
*
* @return the bounds of the Rectangles being drawn
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public Rectangle [] getRectangles () {
checkWidget ();
return rectangles;
}
/**
* Returns <code>true</code> if the rectangles are drawn with a stippled line, <code>false</code> otherwise.
*
* @return the stippled effect of the rectangles
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public boolean getStippled () {
checkWidget ();
return stippled;
}
void moveRectangles (int xChange, int yChange) {
if (xChange < 0 && ((style & SWT.LEFT) == 0)) return;
if (xChange > 0 && ((style & SWT.RIGHT) == 0)) return;
if (yChange < 0 && ((style & SWT.UP) == 0)) return;
if (yChange > 0 && ((style & SWT.DOWN) == 0)) return;
Rectangle bounds = computeBounds ();
bounds.x += xChange; bounds.y += yChange;
for (int i = 0; i < rectangles.length; i++) {
rectangles [i].x += xChange;
rectangles [i].y += yChange;
}
}
/**
* Start displaying the Tracker rectangles.
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public boolean open () {
checkWidget ();
if (rectangles == null) return false;
boolean cancelled = false;
tracking = true;
Event event = new Event ();
MSG msg = new MSG ();
/*
* If this tracker is being created without a mouse drag then
* we need to create a transparent window that fills the screen
* in order to get all mouse/keyboard events that occur
* outside of our visible windows (ie.- over the desktop).
*/
int hwndTransparent = 0;
Callback newProc = null;
boolean mouseDown = OS.GetKeyState(OS.VK_LBUTTON) < 0;
if (!mouseDown) {
int width = OS.GetSystemMetrics (OS.SM_CXSCREEN);
int height = OS.GetSystemMetrics (OS.SM_CYSCREEN);
hwndTransparent = OS.CreateWindowEx (
OS.WS_EX_TRANSPARENT,
display.windowClass,
null,
OS.WS_POPUP | OS.WS_VISIBLE,
0, 0,
width, height,
0,
0,
OS.GetModuleHandle (null),
null);
final int oldProc = OS.GetWindowLong (hwndTransparent, OS.GWL_WNDPROC);
Object windowProc = new Object () {
public int windowProc (int hwnd, int msg, int wParam, int lParam) {
switch (msg) {
/*
* We typically do not want to answer that the transparent window is
* transparent to hits since doing so negates the effect of having it
* to grab events. However, clients of the tracker should not be aware
* of this transparent window. Therefore if there is a hit query
* performed as a result of client code then answer that the transparent
* window is transparent to hits so that its existence will not impact
* the client.
*/
case OS.WM_NCHITTEST:
if (inEvent) return OS.HTTRANSPARENT;
break;
case OS.WM_SETCURSOR:
if (clientCursor != 0) {
OS.SetCursor(clientCursor);
return 1;
}
if (resizeCursor != 0) {
OS.SetCursor(resizeCursor);
return 1;
}
}
return OS.CallWindowProc (oldProc, hwnd, msg, wParam, lParam);
}
};
newProc = new Callback (windowProc, "windowProc", 4);
OS.SetWindowLong (hwndTransparent, OS.GWL_WNDPROC, newProc.getAddress ());
}
drawRectangles (rectangles);
Point cursorPos;
if (mouseDown) {
POINT pt = new POINT ();
OS.GetCursorPos (pt);
cursorPos = new Point (pt.x, pt.y);
} else {
if ((style & SWT.RESIZE) != 0) {
cursorPos = adjustResizeCursor ();
} else {
cursorPos = adjustMoveCursor ();
}
}
int oldX = cursorPos.x, oldY = cursorPos.y;
/*
* Tracker behaves like a Dialog with its own OS event loop.
*/
while (tracking && !cancelled) {
if (parent != null && parent.isDisposed ()) break;
OS.GetMessage (msg, 0, 0, 0);
int message = msg.message;
switch (message) {
case OS.WM_LBUTTONUP:
case OS.WM_MOUSEMOVE:
int newPos = OS.GetMessagePos ();
int newX = (short) (newPos & 0xFFFF);
int newY = (short) (newPos >> 16);
if (newX != oldX || newY != oldY) {
drawRectangles (rectangles);
event.x = newX;
event.y = newY;
if ((style & SWT.RESIZE) != 0) {
resizeRectangles (newX - oldX, newY - oldY);
cursorPos = adjustResizeCursor ();
newX = cursorPos.x; newY = cursorPos.y;
inEvent = true;
sendEvent (SWT.Resize, event);
} else {
moveRectangles (newX - oldX, newY - oldY);
inEvent = true;
sendEvent (SWT.Move, event);
}
inEvent = false;
/*
* It is possible (but unlikely), that application
* code could have disposed the widget in the move
* event. If this happens, return false to indicate
* that the tracking has failed.
*/
if (isDisposed ()) return false;
drawRectangles (rectangles);
oldX = newX; oldY = newY;
}
tracking = msg.message != OS.WM_LBUTTONUP;
break;
case OS.WM_KEYDOWN:
int stepSize = OS.GetKeyState (OS.VK_CONTROL) < 0 ? STEPSIZE_SMALL : STEPSIZE_LARGE;
int xChange = 0, yChange = 0;
switch (msg.wParam) {
case OS.VK_ESCAPE:
cancelled = true;
tracking = false;
break;
case OS.VK_RETURN:
tracking = false;
break;
case OS.VK_LEFT:
xChange = -stepSize;
break;
case OS.VK_RIGHT:
xChange = stepSize;
break;
case OS.VK_UP:
yChange = -stepSize;
break;
case OS.VK_DOWN:
yChange = stepSize;
break;
}
if (xChange != 0 || yChange != 0) {
drawRectangles (rectangles);
newX = oldX + xChange;
newY = oldY + yChange;
event.x = newX;
event.y = newY;
if ((style & SWT.RESIZE) != 0) {
resizeRectangles (xChange, yChange);
cursorPos = adjustResizeCursor ();
inEvent = true;
sendEvent (SWT.Resize, event);
} else {
moveRectangles (xChange, yChange);
cursorPos = adjustMoveCursor ();
inEvent = true;
sendEvent (SWT.Move, event);
}
inEvent = false;
/*
* It is possible (but unlikely) that application
* code could have disposed the widget in the move
* event. If this happens return false to indicate
* that the tracking has failed.
*/
if (isDisposed ()) return false;
drawRectangles (rectangles);
oldX = cursorPos.x; oldY = cursorPos.y;
}
break;
}
/*
* Don't dispatch mouse and key events in general, EXCEPT once this
* tracker has finished its work.
*/
if (tracking && !cancelled) {
if (OS.WM_KEYFIRST <= message && message <= OS.WM_KEYLAST) continue;
if (OS.WM_MOUSEFIRST <= message && message <= OS.WM_MOUSELAST) continue;
}
OS.DispatchMessage (msg);
}
drawRectangles (rectangles);
/*
* Cleanup: If a transparent window was created in order to capture events then
* destroy it and its callback object now.
*/
if (hwndTransparent != 0) {
OS.DestroyWindow (hwndTransparent);
}
if (newProc != null) {
newProc.dispose();
}
/*
* Cleanup: If this tracker was resizing then the last cursor that it created
* needs to be destroyed.
*/
if (resizeCursor != 0) {
OS.DestroyCursor (resizeCursor);
}
tracking = false;
return !cancelled;
}
/**
* Removes the listener from the collection of listeners who will
* be notified when the control is moved or resized.
*
* @param listener the listener which should be notified
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @see ControlListener
* @see #addControlListener
*/
public void removeControlListener (ControlListener listener) {
checkWidget ();
if (listener == null) error (SWT.ERROR_NULL_ARGUMENT);
if (eventTable == null) return;
eventTable.unhook (SWT.Move, listener);
}
void resizeRectangles (int xChange, int yChange) {
/*
* If the cursor orientation has not been set in the orientation of
* this change then try to set it here.
*/
if (xChange < 0 && ((style & SWT.LEFT) != 0) && ((cursorOrientation & SWT.RIGHT) == 0)) {
cursorOrientation |= SWT.LEFT;
} else if (xChange > 0 && ((style & SWT.RIGHT) != 0) && ((cursorOrientation & SWT.LEFT) == 0)) {
cursorOrientation |= SWT.RIGHT;
} else if (yChange < 0 && ((style & SWT.UP) != 0) && ((cursorOrientation & SWT.DOWN) == 0)) {
cursorOrientation |= SWT.UP;
} else if (yChange > 0 && ((style & SWT.DOWN) != 0) && ((cursorOrientation & SWT.UP) == 0)) {
cursorOrientation |= SWT.DOWN;
}
Rectangle bounds = computeBounds ();
if ((cursorOrientation & SWT.LEFT) != 0) {
bounds.x += xChange;
bounds.width -= xChange;
} else if ((cursorOrientation & SWT.RIGHT) != 0) {
bounds.width += xChange;
}
if ((cursorOrientation & SWT.UP) != 0) {
bounds.y += yChange;
bounds.height -= yChange;
} else if ((cursorOrientation & SWT.DOWN) != 0) {
bounds.height += yChange;
}
/*
* The following are conditions under which the resize should not be applied
*/
if (bounds.width < 0 || bounds.height < 0) return;
Rectangle [] newRects = new Rectangle [rectangles.length];
for (int i = 0; i < rectangles.length; i++) {
Rectangle proportion = proportions[i];
newRects[i] = new Rectangle (
proportion.x * bounds.width / 100 + bounds.x,
proportion.y * bounds.height / 100 + bounds.y,
proportion.width * bounds.width / 100,
proportion.height * bounds.height / 100);
}
rectangles = newRects;
}
public void setCursor(Cursor newCursor) {
checkWidget();
clientCursor = 0;
if (newCursor != null) {
clientCursor = newCursor.handle;
if (inEvent) OS.SetCursor(clientCursor);
}
}
/**
* Specify the rectangles that should be drawn.
*
* @param rectangles the bounds of the rectangles to be drawn
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public void setRectangles (Rectangle [] rectangles) {
checkWidget ();
if (rectangles == null) error (SWT.ERROR_NULL_ARGUMENT);
this.rectangles = rectangles;
proportions = computeProportions (rectangles);
}
/**
* Change the appearance of the line used to draw the rectangles.
*
* @param stippled <code>true</code> if rectangle should appear stippled
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public void setStippled (boolean stippled) {
checkWidget ();
this.stippled = stippled;
}
}