1 /*******************************************************************************
2  * Copyright (c) 2000, 2008 IBM Corporation and others.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Eclipse Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/epl-v10.html
7  *
8  * Contributors:
9  *     IBM Corporation - initial API and implementation
10  * Port to the D programming language:
11  *     Frank Benoit <benoit@tionex.de>
12  *******************************************************************************/
13 module org.eclipse.swt.widgets.Tracker;
14 
15 import java.lang.all;
16 
17 
18 
19 import org.eclipse.swt.SWT;
20 import org.eclipse.swt.SWTException;
21 import org.eclipse.swt.events.ControlListener;
22 import org.eclipse.swt.events.KeyListener;
23 import org.eclipse.swt.graphics.Cursor;
24 import org.eclipse.swt.graphics.Point;
25 import org.eclipse.swt.graphics.Rectangle;
26 import org.eclipse.swt.internal.gtk.OS;
27 import org.eclipse.swt.widgets.Widget;
28 import org.eclipse.swt.widgets.Composite;
29 import org.eclipse.swt.widgets.Display;
30 import org.eclipse.swt.widgets.TypedListener;
31 import org.eclipse.swt.widgets.Event;
32 
33 import java.lang.Thread;
34 
35 /**
36  *  Instances of this class implement rubber banding rectangles that are
37  *  drawn onto a parent <code>Composite</code> or <code>Display</code>.
38  *  These rectangles can be specified to respond to mouse and key events
39  *  by either moving or resizing themselves accordingly.  Trackers are
40  *  typically used to represent window geometries in a lightweight manner.
41  *
42  * <dl>
43  * <dt><b>Styles:</b></dt>
44  * <dd>LEFT, RIGHT, UP, DOWN, RESIZE</dd>
45  * <dt><b>Events:</b></dt>
46  * <dd>Move, Resize</dd>
47  * </dl>
48  * <p>
49  * Note: Rectangle move behavior is assumed unless RESIZE is specified.
50  * </p><p>
51  * IMPORTANT: This class is <em>not</em> intended to be subclassed.
52  * </p>
53  *
54  * @see <a href="http://www.eclipse.org/swt/snippets/#tracker">Tracker snippets</a>
55  * @see <a href="http://www.eclipse.org/swt/">Sample code and further information</a>
56  */
57 public class Tracker : Widget {
58     Composite parent;
59     Cursor cursor;
60     GdkCursor* lastCursor;
61     GdkDrawable* window;
62     bool tracking, cancelled, grabbed, stippled;
63     Rectangle [] rectangles, proportions;
64     Rectangle bounds;
65     int cursorOrientation = SWT.NONE;
66     int oldX, oldY;
67 
68     const static int STEPSIZE_SMALL = 1;
69     const static int STEPSIZE_LARGE = 9;
70 
71 /**
72  * Constructs a new instance of this class given its parent
73  * and a style value describing its behavior and appearance.
74  * <p>
75  * The style value is either one of the style constants defined in
76  * class <code>SWT</code> which is applicable to instances of this
77  * class, or must be built by <em>bitwise OR</em>'ing together
78  * (that is, using the <code>int</code> "|" operator) two or more
79  * of those <code>SWT</code> style constants. The class description
80  * lists the style constants that are applicable to the class.
81  * Style bits are also inherited from superclasses.
82  * </p>
83  *
84  * @param parent a widget which will be the parent of the new instance (cannot be null)
85  * @param style the style of widget to construct
86  *
87  * @exception IllegalArgumentException <ul>
88  *    <li>ERROR_NULL_ARGUMENT - if the parent is null</li>
89  * </ul>
90  * @exception SWTException <ul>
91  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
92  *    <li>ERROR_INVALID_SUBCLASS - if this class is not an allowed subclass</li>
93  * </ul>
94  *
95  * @see SWT#LEFT
96  * @see SWT#RIGHT
97  * @see SWT#UP
98  * @see SWT#DOWN
99  * @see SWT#RESIZE
100  * @see Widget#checkSubclass
101  * @see Widget#getStyle
102  */
103 public this (Composite parent, int style) {
104     super (parent, checkStyle(style));
105     this.parent = parent;
106 }
107 
108 /**
109  * Constructs a new instance of this class given the display
110  * to create it on and a style value describing its behavior
111  * and appearance.
112  * <p>
113  * The style value is either one of the style constants defined in
114  * class <code>SWT</code> which is applicable to instances of this
115  * class, or must be built by <em>bitwise OR</em>'ing together
116  * (that is, using the <code>int</code> "|" operator) two or more
117  * of those <code>SWT</code> style constants. The class description
118  * lists the style constants that are applicable to the class.
119  * Style bits are also inherited from superclasses.
120  * </p><p>
121  * Note: Currently, null can be passed in for the display argument.
122  * This has the effect of creating the tracker on the currently active
123  * display if there is one. If there is no current display, the
124  * tracker is created on a "default" display. <b>Passing in null as
125  * the display argument is not considered to be good coding style,
126  * and may not be supported in a future release of SWT.</b>
127  * </p>
128  *
129  * @param display the display to create the tracker on
130  * @param style the style of control to construct
131  *
132  * @exception SWTException <ul>
133  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
134  *    <li>ERROR_INVALID_SUBCLASS - if this class is not an allowed subclass</li>
135  * </ul>
136  *
137  * @see SWT#LEFT
138  * @see SWT#RIGHT
139  * @see SWT#UP
140  * @see SWT#DOWN
141  */
142 public this (Display display, int style) {
143     if (display is null) display = Display.getCurrent ();
144     if (display is null) display = Display.getDefault ();
145     if (!display.isValidThread ()) {
146         error (SWT.ERROR_THREAD_INVALID_ACCESS);
147     }
148     this.style = checkStyle (style);
149     this.display = display;
150 }
151 
152 /**
153  * Adds the listener to the collection of listeners who will
154  * be notified when the control is moved or resized, by sending
155  * it one of the messages defined in the <code>ControlListener</code>
156  * interface.
157  *
158  * @param listener the listener which should be notified
159  *
160  * @exception IllegalArgumentException <ul>
161  *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
162  * </ul>
163  * @exception SWTException <ul>
164  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
165  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
166  * </ul>
167  *
168  * @see ControlListener
169  * @see #removeControlListener
170  */
171 public void addControlListener(ControlListener listener) {
172     checkWidget();
173     if (listener is null) error (SWT.ERROR_NULL_ARGUMENT);
174     TypedListener typedListener = new TypedListener (listener);
175     addListener (SWT.Resize, typedListener);
176     addListener (SWT.Move, typedListener);
177 }
178 
179 /**
180  * Adds the listener to the collection of listeners who will
181  * be notified when keys are pressed and released on the system keyboard, by sending
182  * it one of the messages defined in the <code>KeyListener</code>
183  * interface.
184  *
185  * @param listener the listener which should be notified
186  *
187  * @exception IllegalArgumentException <ul>
188  *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
189  * </ul>
190  * @exception SWTException <ul>
191  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
192  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
193  * </ul>
194  *
195  * @see KeyListener
196  * @see #removeKeyListener
197  */
198 public void addKeyListener(KeyListener listener) {
199     checkWidget();
200     if (listener is null) error (SWT.ERROR_NULL_ARGUMENT);
201     TypedListener typedListener = new TypedListener (listener);
202     addListener(SWT.KeyUp,typedListener);
203     addListener(SWT.KeyDown,typedListener);
204 }
205 
206 Point adjustMoveCursor () {
207     if (bounds is null) return null;
208     int newX = bounds.x + bounds.width / 2;
209     int newY = bounds.y;
210 
211     Point point = display.map (parent, null, newX, newY);
212     display.setCursorLocation (point);
213 
214     /*
215      * The call to XWarpPointer does not always place the pointer on the
216      * exact location that is specified, so do a query (below) to get the
217      * actual location of the pointer after it has been moved.
218      */
219     int actualX, actualY, state;
220     OS.gdk_window_get_pointer (cast(GdkWindow*)window, &actualX, &actualY, &state);
221     return new Point (actualX, actualY);
222 }
223 
224 Point adjustResizeCursor () {
225     if (bounds is null) return null;
226     int newX, newY;
227 
228     if ((cursorOrientation & SWT.LEFT) !is 0) {
229         newX = bounds.x;
230     } else if ((cursorOrientation & SWT.RIGHT) !is 0) {
231         newX = bounds.x + bounds.width;
232     } else {
233         newX = bounds.x + bounds.width / 2;
234     }
235 
236     if ((cursorOrientation & SWT.UP) !is 0) {
237         newY = bounds.y;
238     } else if ((cursorOrientation & SWT.DOWN) !is 0) {
239         newY = bounds.y + bounds.height;
240     } else {
241         newY = bounds.y + bounds.height / 2;
242     }
243 
244     Point point = display.map (parent, null, newX, newY);
245     display.setCursorLocation (point);
246 
247     /*
248      * The call to XWarpPointer does not always place the pointer on the
249      * exact location that is specified, so do a query (below) to get the
250      * actual location of the pointer after it has been moved.
251      */
252     int  actualX, actualY, state;
253     OS.gdk_window_get_pointer (window, &actualX, &actualY, &state);
254     return new Point (actualX, actualY );
255 }
256 
257 
258 /**
259  * Stops displaying the tracker rectangles.  Note that this is not considered
260  * to be a cancelation by the user.
261  *
262  * @exception SWTException <ul>
263  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
264  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
265  * </ul>
266  */
267 public void close () {
268     checkWidget();
269     tracking = false;
270 }
271 
272 static int checkStyle (int style) {
273     if ((style & (SWT.LEFT | SWT.RIGHT | SWT.UP | SWT.DOWN)) is 0) {
274         style |= SWT.LEFT | SWT.RIGHT | SWT.UP | SWT.DOWN;
275     }
276     return style;
277 }
278 
279 Rectangle computeBounds () {
280     if (rectangles.length is 0) return null;
281     int xMin = rectangles [0].x;
282     int yMin = rectangles [0].y;
283     int xMax = rectangles [0].x + rectangles [0].width;
284     int yMax = rectangles [0].y + rectangles [0].height;
285 
286     for (int i = 1; i < rectangles.length; i++) {
287         if (rectangles [i].x < xMin) xMin = rectangles [i].x;
288         if (rectangles [i].y < yMin) yMin = rectangles [i].y;
289         int rectRight = rectangles [i].x + rectangles [i].width;
290         if (rectRight > xMax) xMax = rectRight;
291         int rectBottom = rectangles [i].y + rectangles [i].height;
292         if (rectBottom > yMax) yMax = rectBottom;
293     }
294 
295     return new Rectangle (xMin, yMin, xMax - xMin, yMax - yMin);
296 }
297 
298 Rectangle [] computeProportions (Rectangle [] rects) {
299     Rectangle [] result = new Rectangle [rects.length];
300     bounds = computeBounds ();
301     if (bounds !is null) {
302         for (int i = 0; i < rects.length; i++) {
303             int x = 0, y = 0, width = 0, height = 0;
304             if (bounds.width !is 0) {
305                 x = (rects [i].x - bounds.x) * 100 / bounds.width;
306                 width = rects [i].width * 100 / bounds.width;
307             } else {
308                 width = 100;
309             }
310             if (bounds.height !is 0) {
311                 y = (rects [i].y - bounds.y) * 100 / bounds.height;
312                 height = rects [i].height * 100 / bounds.height;
313             } else {
314                 height = 100;
315             }
316             result [i] = new Rectangle (x, y, width, height);
317         }
318     }
319     return result;
320 }
321 
322 void drawRectangles (Rectangle [] rects) {
323     auto window = OS.GDK_ROOT_PARENT ();
324     if (parent !is null) {
325         window = OS.GTK_WIDGET_WINDOW (parent.paintHandle());
326     }
327     if (window is null) return;
328     auto gc = OS.gdk_gc_new (window);
329     if (gc is null) return;
330     auto colormap = OS.gdk_colormap_get_system ();
331     GdkColor color;
332     OS.gdk_color_white (colormap, &color);
333     OS.gdk_gc_set_foreground (gc, &color);
334     OS.gdk_gc_set_subwindow (gc, OS.GDK_INCLUDE_INFERIORS);
335     OS.gdk_gc_set_function (gc, OS.GDK_XOR);
336     for (int i=0; i<rects.length; i++) {
337         Rectangle rect = rects [i];
338         int x = rect.x;
339         if (parent !is null && (parent.style & SWT.MIRRORED) !is 0) x = parent.getClientWidth () - rect.width - x;
340         OS.gdk_draw_rectangle (window, gc, 0, x, rect.y, rect.width, rect.height);
341     }
342     OS.g_object_unref (gc);
343 }
344 
345 /**
346  * Returns the bounds that are being drawn, expressed relative to the parent
347  * widget.  If the parent is a <code>Display</code> then these are screen
348  * coordinates.
349  *
350  * @return the bounds of the Rectangles being drawn
351  *
352  * @exception SWTException <ul>
353  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
354  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
355  * </ul>
356  */
357 public Rectangle [] getRectangles () {
358     checkWidget();
359     Rectangle [] result = new Rectangle [rectangles.length];
360     for (int i = 0; i < rectangles.length; i++) {
361         Rectangle current = rectangles [i];
362         result [i] = new Rectangle (current.x, current.y, current.width, current.height);
363     }
364     return result;
365 }
366 
367 /**
368  * Returns <code>true</code> if the rectangles are drawn with a stippled line, <code>false</code> otherwise.
369  *
370  * @return the stippled effect of the rectangles
371  *
372  * @exception SWTException <ul>
373  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
374  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
375  * </ul>
376  */
377 public bool getStippled () {
378     checkWidget();
379     return stippled;
380 }
381 
382 bool grab () {
383     auto cursor = this.cursor !is null ? this.cursor.handle : null;
384     int result = OS.gdk_pointer_grab (window, false, OS.GDK_POINTER_MOTION_MASK | OS.GDK_BUTTON_RELEASE_MASK, window, cursor, OS.GDK_CURRENT_TIME);
385     return result is OS.GDK_GRAB_SUCCESS;
386 }
387 
388 override int gtk_button_release_event (GtkWidget* widget, GdkEventButton* event) {
389     return gtk_mouse (OS.GDK_BUTTON_RELEASE, widget, event);
390 }
391 
392 override int gtk_key_press_event (GtkWidget* widget, GdkEventKey* keyEvent) {
393     auto result = super.gtk_key_press_event (widget, keyEvent);
394     if (result !is 0) return result;
395     int stepSize = ((keyEvent.state & OS.GDK_CONTROL_MASK) !is 0) ? STEPSIZE_SMALL : STEPSIZE_LARGE;
396     int xChange = 0, yChange = 0;
397     switch (keyEvent.keyval) {
398         case OS.GDK_Escape:
399             cancelled = true;
400             goto case OS.GDK_Return;
401         case OS.GDK_Return:
402             tracking = false;
403             break;
404         case OS.GDK_Left:
405             xChange = -stepSize;
406             break;
407         case OS.GDK_Right:
408             xChange = stepSize;
409             break;
410         case OS.GDK_Up:
411             yChange = -stepSize;
412             break;
413         case OS.GDK_Down:
414             yChange = stepSize;
415             break;
416         default:
417     }
418     if (xChange !is 0 || yChange !is 0) {
419         Rectangle [] oldRectangles = rectangles;
420         Rectangle [] rectsToErase = new Rectangle [rectangles.length];
421         for (int i = 0; i < rectangles.length; i++) {
422             Rectangle current = rectangles [i];
423             rectsToErase [i] = new Rectangle (current.x, current.y, current.width, current.height);
424         }
425         Event event = new Event ();
426         event.x = oldX + xChange;
427         event.y = oldY + yChange;
428         if (parent !is null && (parent.style & SWT.MIRRORED) !is 0) {
429             event.x = parent.getClientWidth () - event.width - event.x;
430         }
431         if ((style & SWT.RESIZE) !is 0) {
432             resizeRectangles (xChange, yChange);
433             sendEvent (SWT.Resize, event);
434             /*
435             * It is possible (but unlikely) that application
436             * code could have disposed the widget in the resize
437             * event.  If this happens return false to indicate
438             * that the tracking has failed.
439             */
440             if (isDisposed ()) {
441                 cancelled = true;
442                 return 1;
443             }
444             bool draw = false;
445             /*
446              * It is possible that application code could have
447              * changed the rectangles in the resize event.  If this
448              * happens then only redraw the tracker if the rectangle
449              * values have changed.
450              */
451             if (rectangles !is oldRectangles) {
452                 ptrdiff_t length = rectangles.length;
453                 if (length !is rectsToErase.length) {
454                     draw = true;
455                 } else {
456                     for (int i = 0; i < length; i++) {
457                         if (rectangles [i] !=/*eq*/ rectsToErase [i]) {
458                             draw = true;
459                             break;
460                         }
461                     }
462                 }
463             } else {
464                 draw = true;
465             }
466             if (draw) {
467                 drawRectangles (rectsToErase);
468                 update ();
469                 drawRectangles (rectangles);
470             }
471             Point cursorPos = adjustResizeCursor ();
472             if (cursorPos !is null) {
473                 oldX = cursorPos.x;
474                 oldY = cursorPos.y;
475             }
476         } else {
477             moveRectangles (xChange, yChange);
478             sendEvent (SWT.Move, event);
479             /*
480             * It is possible (but unlikely) that application
481             * code could have disposed the widget in the move
482             * event.  If this happens return false to indicate
483             * that the tracking has failed.
484             */
485             if (isDisposed ()) {
486                 cancelled = true;
487                 return 1;
488             }
489             bool draw = false;
490             /*
491              * It is possible that application code could have
492              * changed the rectangles in the move event.  If this
493              * happens then only redraw the tracker if the rectangle
494              * values have changed.
495              */
496             if (rectangles !is oldRectangles) {
497                 ptrdiff_t length = rectangles.length;
498                 if (length !is rectsToErase.length) {
499                     draw = true;
500                 } else {
501                     for (int i = 0; i < length; i++) {
502                         if (rectangles [i] !=/*eq*/ rectsToErase [i]) {
503                             draw = true;
504                             break;
505                         }
506                     }
507                 }
508             } else {
509                 draw = true;
510             }
511             if (draw) {
512                 drawRectangles (rectsToErase);
513                 update ();
514                 drawRectangles (rectangles);
515             }
516             Point cursorPos = adjustMoveCursor ();
517             if (cursorPos !is null) {
518                 oldX = cursorPos.x;
519                 oldY = cursorPos.y;
520             }
521         }
522     }
523     return result;
524 }
525 
526 override int gtk_motion_notify_event (GtkWidget* widget, GdkEventMotion* eventPtr) {
527     auto cursor = this.cursor !is null ? this.cursor.handle : null;
528     if (cursor !is lastCursor) {
529         ungrab ();
530         grabbed = grab ();
531         lastCursor = cursor;
532     }
533     return gtk_mouse (OS.GDK_MOTION_NOTIFY, widget, eventPtr);
534 }
535 
536 private int gtk_mouse (int eventType, GtkWidget* widget, void* eventPtr) {
537     int newX, newY;
538     OS.gdk_window_get_pointer (window, &newX, &newY, null);
539     if (oldX !is newX || oldY !is newY ) {
540         Rectangle [] oldRectangles = rectangles;
541         Rectangle [] rectsToErase = new Rectangle [rectangles.length];
542         for (int i = 0; i < rectangles.length; i++) {
543             Rectangle current = rectangles [i];
544             rectsToErase [i] = new Rectangle (current.x, current.y, current.width, current.height);
545         }
546         Event event = new Event ();
547         if (parent is null) {
548             event.x = newX ;
549             event.y = newY ;
550         } else {
551             Point screenCoord = display.map (parent, null, newX , newY );
552             event.x = screenCoord.x;
553             event.y = screenCoord.y;
554         }
555         if ((style & SWT.RESIZE) !is 0) {
556             resizeRectangles (newX  - oldX, newY  - oldY);
557             sendEvent (SWT.Resize, event);
558             /*
559             * It is possible (but unlikely), that application
560             * code could have disposed the widget in the resize
561             * event.  If this happens, return false to indicate
562             * that the tracking has failed.
563             */
564             if (isDisposed ()) {
565                 cancelled = true;
566                 return 1;
567             }
568             bool draw = false;
569             /*
570              * It is possible that application code could have
571              * changed the rectangles in the resize event.  If this
572              * happens then only redraw the tracker if the rectangle
573              * values have changed.
574              */
575             if (rectangles !is oldRectangles) {
576                 ptrdiff_t length = rectangles.length;
577                 if (length !is rectsToErase.length) {
578                     draw = true;
579                 } else {
580                     for (int i = 0; i < length; i++) {
581                         if (rectangles [i] !=/*eq*/ rectsToErase [i]) {
582                             draw = true;
583                             break;
584                         }
585                     }
586                 }
587             } else {
588                 draw = true;
589             }
590             if (draw) {
591                 drawRectangles (rectsToErase);
592                 update ();
593                 drawRectangles (rectangles);
594             }
595             Point cursorPos = adjustResizeCursor ();
596             if (cursorPos !is null) {
597                 newX  = cursorPos.x;
598                 newY  = cursorPos.y;
599             }
600         } else {
601             moveRectangles (newX  - oldX, newY  - oldY);
602             sendEvent (SWT.Move, event);
603             /*
604             * It is possible (but unlikely), that application
605             * code could have disposed the widget in the move
606             * event.  If this happens, return false to indicate
607             * that the tracking has failed.
608             */
609             if (isDisposed ()) {
610                 cancelled = true;
611                 return 1;
612             }
613             bool draw = false;
614             /*
615              * It is possible that application code could have
616              * changed the rectangles in the move event.  If this
617              * happens then only redraw the tracker if the rectangle
618              * values have changed.
619              */
620             if (rectangles !is oldRectangles) {
621                 ptrdiff_t length = rectangles.length;
622                 if (length !is rectsToErase.length) {
623                     draw = true;
624                 } else {
625                     for (int i = 0; i < length; i++) {
626                         if (rectangles [i] !=/*eq*/ rectsToErase [i]) {
627                             draw = true;
628                             break;
629                         }
630                     }
631                 }
632             } else {
633                 draw = true;
634             }
635             if (draw) {
636                 drawRectangles (rectsToErase);
637                 update ();
638                 drawRectangles (rectangles);
639             }
640         }
641         oldX = newX ;
642         oldY = newY ;
643     }
644     tracking = eventType !is OS.GDK_BUTTON_RELEASE;
645     return 0;
646 }
647 
648 void moveRectangles (int xChange, int yChange) {
649     if (bounds is null) return;
650     if (xChange < 0 && ((style & SWT.LEFT) is 0)) xChange = 0;
651     if (xChange > 0 && ((style & SWT.RIGHT) is 0)) xChange = 0;
652     if (yChange < 0 && ((style & SWT.UP) is 0)) yChange = 0;
653     if (yChange > 0 && ((style & SWT.DOWN) is 0)) yChange = 0;
654     if (xChange is 0 && yChange is 0) return;
655     if (parent !is null && (parent.style & SWT.MIRRORED) !is 0) xChange *= -1;
656     bounds.x += xChange; bounds.y += yChange;
657     for (int i = 0; i < rectangles.length; i++) {
658         rectangles [i].x += xChange;
659         rectangles [i].y += yChange;
660     }
661 }
662 
663 /**
664  * Displays the Tracker rectangles for manipulation by the user.  Returns when
665  * the user has either finished manipulating the rectangles or has cancelled the
666  * Tracker.
667  *
668  * @return <code>true</code> if the user did not cancel the Tracker, <code>false</code> otherwise
669  *
670  * @exception SWTException <ul>
671  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
672  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
673  * </ul>
674  */
675 public bool open () {
676     checkWidget();
677     window = OS.GDK_ROOT_PARENT ();
678     if (parent !is null) {
679         window = OS.GTK_WIDGET_WINDOW (parent.paintHandle());
680     }
681     if (window is null) return false;
682     cancelled = false;
683     tracking = true;
684     update ();
685     drawRectangles (rectangles);
686     int oldX, oldY, state;
687     OS.gdk_window_get_pointer (window, &oldX, &oldY, &state);
688 
689     /*
690     * if exactly one of UP/DOWN is specified as a style then set the cursor
691     * orientation accordingly (the same is done for LEFT/RIGHT styles below)
692     */
693     int vStyle = style & (SWT.UP | SWT.DOWN);
694     if (vStyle is SWT.UP || vStyle is SWT.DOWN) {
695         cursorOrientation |= vStyle;
696     }
697     int hStyle = style & (SWT.LEFT | SWT.RIGHT);
698     if (hStyle is SWT.LEFT || hStyle is SWT.RIGHT) {
699         cursorOrientation |= hStyle;
700     }
701 
702     int mask = OS.GDK_BUTTON1_MASK | OS.GDK_BUTTON2_MASK | OS.GDK_BUTTON3_MASK;
703     bool mouseDown = (state  & mask) !is 0;
704     if (!mouseDown) {
705         Point cursorPos = null;
706         if ((style & SWT.RESIZE) !is 0) {
707             cursorPos = adjustResizeCursor ();
708         } else {
709             cursorPos = adjustMoveCursor ();
710         }
711         if (cursorPos !is null) {
712             oldX  = cursorPos.x;
713             oldY  = cursorPos.y;
714         }
715     }
716     this.oldX = oldX ;
717     this.oldY = oldY ;
718 
719     grabbed = grab ();
720     lastCursor = this.cursor !is null ? this.cursor.handle : null;
721 
722     /* Tracker behaves like a Dialog with its own OS event loop. */
723     GdkEvent* gdkEvent;
724     while (tracking) {
725         if (parent !is null && parent.isDisposed ()) break;
726         GdkEvent* eventPtr;
727         while (true) {
728             eventPtr = OS.gdk_event_get ();
729             if (eventPtr !is null) {
730                 break;
731             } else {
732                 try { Thread.sleep(50); } catch (Exception ex) {}
733             }
734         }
735         gdkEvent = eventPtr;
736         auto widget = OS.gtk_get_event_widget (eventPtr);
737         switch (gdkEvent.type) {
738             case OS.GDK_MOTION_NOTIFY: gtk_motion_notify_event (widget, cast(GdkEventMotion*)eventPtr); break;
739             case OS.GDK_BUTTON_RELEASE: gtk_button_release_event (widget, cast(GdkEventButton*)eventPtr); break;
740             case OS.GDK_KEY_PRESS: gtk_key_press_event (widget, cast(GdkEventKey*)eventPtr); break;
741             case OS.GDK_KEY_RELEASE: gtk_key_release_event (widget, cast(GdkEventKey*)eventPtr); break;
742             case OS.GDK_BUTTON_PRESS:
743             case OS.GDK_2BUTTON_PRESS:
744             case OS.GDK_3BUTTON_PRESS:
745             case OS.GDK_ENTER_NOTIFY:
746             case OS.GDK_LEAVE_NOTIFY:
747                 /* Do not dispatch these */
748                 break;
749             case OS.GDK_EXPOSE:
750                 update ();
751                 drawRectangles (rectangles);
752                 OS.gtk_main_do_event (eventPtr);
753                 drawRectangles (rectangles);
754                 break;
755             default:
756                 OS.gtk_main_do_event (eventPtr);
757         }
758         OS.gdk_event_free (eventPtr);
759     }
760     if (!isDisposed ()) {
761         update ();
762         drawRectangles (rectangles);
763     }
764     ungrab ();
765     window = null;
766     return !cancelled;
767 }
768 
769 override void releaseWidget () {
770     super.releaseWidget ();
771     parent = null;
772     rectangles = proportions = null;
773     bounds = null;
774 }
775 
776 /**
777  * Removes the listener from the collection of listeners who will
778  * be notified when the control is moved or resized.
779  *
780  * @param listener the listener which should no longer be notified
781  *
782  * @exception IllegalArgumentException <ul>
783  *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
784  * </ul>
785  * @exception SWTException <ul>
786  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
787  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
788  * </ul>
789  *
790  * @see ControlListener
791  * @see #addControlListener
792  */
793 public void removeControlListener (ControlListener listener) {
794     checkWidget();
795     if (listener is null) error (SWT.ERROR_NULL_ARGUMENT);
796     if (eventTable is null) return;
797     eventTable.unhook (SWT.Resize, listener);
798     eventTable.unhook (SWT.Move, listener);
799 }
800 
801 /**
802  * Removes the listener from the collection of listeners who will
803  * be notified when keys are pressed and released on the system keyboard.
804  *
805  * @param listener the listener which should no longer be notified
806  *
807  * @exception IllegalArgumentException <ul>
808  *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
809  * </ul>
810  * @exception SWTException <ul>
811  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
812  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
813  * </ul>
814  *
815  * @see KeyListener
816  * @see #addKeyListener
817  */
818 public void removeKeyListener(KeyListener listener) {
819     checkWidget();
820     if (listener is null) error (SWT.ERROR_NULL_ARGUMENT);
821     if (eventTable is null) return;
822     eventTable.unhook (SWT.KeyUp, listener);
823     eventTable.unhook (SWT.KeyDown, listener);
824 }
825 
826 void resizeRectangles (int xChange, int yChange) {
827     if (bounds is null) return;
828     if (parent !is null && (parent.style & SWT.MIRRORED) !is 0) xChange *= -1;
829     /*
830     * If the cursor orientation has not been set in the orientation of
831     * this change then try to set it here.
832     */
833     if (xChange < 0 && ((style & SWT.LEFT) !is 0) && ((cursorOrientation & SWT.RIGHT) is 0)) {
834         cursorOrientation |= SWT.LEFT;
835     }
836     if (xChange > 0 && ((style & SWT.RIGHT) !is 0) && ((cursorOrientation & SWT.LEFT) is 0)) {
837         cursorOrientation |= SWT.RIGHT;
838     }
839     if (yChange < 0 && ((style & SWT.UP) !is 0) && ((cursorOrientation & SWT.DOWN) is 0)) {
840         cursorOrientation |= SWT.UP;
841     }
842     if (yChange > 0 && ((style & SWT.DOWN) !is 0) && ((cursorOrientation & SWT.UP) is 0)) {
843         cursorOrientation |= SWT.DOWN;
844     }
845 
846     /*
847      * If the bounds will flip about the x or y axis then apply the adjustment
848      * up to the axis (ie.- where bounds width/height becomes 0), change the
849      * cursor's orientation accordingly, and flip each Rectangle's origin (only
850      * necessary for > 1 Rectangles)
851      */
852     if ((cursorOrientation & SWT.LEFT) !is 0) {
853         if (xChange > bounds.width) {
854             if ((style & SWT.RIGHT) is 0) return;
855             cursorOrientation |= SWT.RIGHT;
856             cursorOrientation &= ~SWT.LEFT;
857             bounds.x += bounds.width;
858             xChange -= bounds.width;
859             bounds.width = 0;
860             if (proportions.length > 1) {
861                 for (int i = 0; i < proportions.length; i++) {
862                     Rectangle proportion = proportions [i];
863                     proportion.x = 100 - proportion.x - proportion.width;
864                 }
865             }
866         }
867     } else if ((cursorOrientation & SWT.RIGHT) !is 0) {
868         if (bounds.width < -xChange) {
869             if ((style & SWT.LEFT) is 0) return;
870             cursorOrientation |= SWT.LEFT;
871             cursorOrientation &= ~SWT.RIGHT;
872             xChange += bounds.width;
873             bounds.width = 0;
874             if (proportions.length > 1) {
875                 for (int i = 0; i < proportions.length; i++) {
876                     Rectangle proportion = proportions [i];
877                     proportion.x = 100 - proportion.x - proportion.width;
878                 }
879             }
880         }
881     }
882     if ((cursorOrientation & SWT.UP) !is 0) {
883         if (yChange > bounds.height) {
884             if ((style & SWT.DOWN) is 0) return;
885             cursorOrientation |= SWT.DOWN;
886             cursorOrientation &= ~SWT.UP;
887             bounds.y += bounds.height;
888             yChange -= bounds.height;
889             bounds.height = 0;
890             if (proportions.length > 1) {
891                 for (int i = 0; i < proportions.length; i++) {
892                     Rectangle proportion = proportions [i];
893                     proportion.y = 100 - proportion.y - proportion.height;
894                 }
895             }
896         }
897     } else if ((cursorOrientation & SWT.DOWN) !is 0) {
898         if (bounds.height < -yChange) {
899             if ((style & SWT.UP) is 0) return;
900             cursorOrientation |= SWT.UP;
901             cursorOrientation &= ~SWT.DOWN;
902             yChange += bounds.height;
903             bounds.height = 0;
904             if (proportions.length > 1) {
905                 for (int i = 0; i < proportions.length; i++) {
906                     Rectangle proportion = proportions [i];
907                     proportion.y = 100 - proportion.y - proportion.height;
908                 }
909             }
910         }
911     }
912 
913     // apply the bounds adjustment
914     if ((cursorOrientation & SWT.LEFT) !is 0) {
915         bounds.x += xChange;
916         bounds.width -= xChange;
917     } else if ((cursorOrientation & SWT.RIGHT) !is 0) {
918         bounds.width += xChange;
919     }
920     if ((cursorOrientation & SWT.UP) !is 0) {
921         bounds.y += yChange;
922         bounds.height -= yChange;
923     } else if ((cursorOrientation & SWT.DOWN) !is 0) {
924         bounds.height += yChange;
925     }
926 
927     Rectangle [] newRects = new Rectangle [rectangles.length];
928     for (int i = 0; i < rectangles.length; i++) {
929         Rectangle proportion = proportions[i];
930         newRects[i] = new Rectangle (
931             proportion.x * bounds.width / 100 + bounds.x,
932             proportion.y * bounds.height / 100 + bounds.y,
933             proportion.width * bounds.width / 100,
934             proportion.height * bounds.height / 100);
935     }
936     rectangles = newRects;
937 }
938 
939 /**
940  * Sets the <code>Cursor</code> of the Tracker.  If this cursor is <code>null</code>
941  * then the cursor reverts to the default.
942  *
943  * @param newCursor the new <code>Cursor</code> to display
944  *
945  * @exception SWTException <ul>
946  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
947  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
948  * </ul>
949  */
950 public void setCursor (Cursor value) {
951     checkWidget ();
952     cursor = value;
953 }
954 
955 /**
956  * Specifies the rectangles that should be drawn, expressed relative to the parent
957  * widget.  If the parent is a Display then these are screen coordinates.
958  *
959  * @param rectangles the bounds of the rectangles to be drawn
960  *
961  * @exception IllegalArgumentException <ul>
962  *    <li>ERROR_NULL_ARGUMENT - if the set of rectangles contains a null rectangle</li>
963  * </ul>
964  * @exception SWTException <ul>
965  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
966  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
967  * </ul>
968  */
969 public void setRectangles (Rectangle [] rectangles) {
970     checkWidget();
971     // SWT extension: allow null for zero length string
972     //if (rectangles is null) error (SWT.ERROR_NULL_ARGUMENT);
973     ptrdiff_t length = rectangles.length;
974     this.rectangles = new Rectangle [length];
975     for (int i = 0; i < length; i++) {
976         Rectangle current = rectangles [i];
977         if (current is null) error (SWT.ERROR_NULL_ARGUMENT);
978         this.rectangles [i] = new Rectangle (current.x, current.y, current.width, current.height);
979     }
980     proportions = computeProportions (rectangles);
981 }
982 
983 /**
984  * Changes the appearance of the line used to draw the rectangles.
985  *
986  * @param stippled <code>true</code> if rectangle should appear stippled
987  *
988  * @exception SWTException <ul>
989  *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
990  *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
991  * </ul>
992  */
993 public void setStippled (bool stippled) {
994     checkWidget();
995     this.stippled = stippled;
996 }
997 
998 void ungrab () {
999     if (grabbed) OS.gdk_pointer_ungrab (OS.GDK_CURRENT_TIME);
1000 }
1001 
1002 void update () {
1003     if (parent !is null) {
1004         if (parent.isDisposed ()) return;
1005         parent.getShell ().update ();
1006     } else {
1007         display.update ();
1008     }
1009 }
1010 
1011 }