001 
002 /*
003  *  JScripter Standard 1.0 - To Script In Java
004  *  Copyright (C) 2008-2011  J.J.Liu<jianjunliu@126.com> <http://www.jscripter.org>
005  *  
006  *  This program is free software: you can redistribute it and/or modify
007  *  it under the terms of the GNU Affero General Public License as published by
008  *  the Free Software Foundation, either version 3 of the License, or
009  *  (at your option) any later version.
010  *  
011  *  This program is distributed in the hope that it will be useful,
012  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
013  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014  *  GNU Affero General Public License for more details.
015  *  
016  *  You should have received a copy of the GNU Affero General Public License
017  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
018  */
019 
020 package jsx.ui.dd;
021 
022 import js.ArrayLike;
023 import js.Function;
024 import js.Id;
025 import js.Js;
026 import js.JsApplet;
027 import js.ObjectLike;
028 import js.Static;
029 import js.Var;
030 import js.Vars;
031 import js.core.JsFunction;
032 import js.core.JsObject;
033 import js.dom.EventTarget;
034 import js.user.JsEvent;
035 import js.user.JsNode;
036 import jsx.Configurable;
037 import jsx.TaskManager;
038 import jsx.Timeout;
039 import jsx.client.Browser;
040 import jsx.client.Win;
041 import jsx.core.ObjectLikes;
042 import jsx.core.Variables;
043 import jsx.event.Events;
044 import jsx.event.Handle;
045 import jsx.ui.Component;
046 import jsx.ui.Widget;
047 import jsx.ui.dd.event.DragMove;
048 import jsx.ui.dd.event.DragOut;
049 import jsx.ui.dd.event.DragOver;
050 import jsx.ui.dd.event.DragStart;
051 import jsx.ui.dd.event.DragStop;
052 import jsx.ui.event.MouseDown;
053 import jsx.ui.event.MouseMove;
054 import jsx.ui.event.MouseOut;
055 import jsx.ui.event.MouseOver;
056 import jsx.ui.event.MouseUp;
057 import jsx.ui.event.OnRender;
058 import jsx.ui.event.Popup;
059 import jsx.ui.event.Render;
060 
061 /**
062  * <p>A base class for mouse widgets that wrap components to handle mouse operations.</p>
063  * <p>A {@link Mouse} widget is a {@link Widget} that wraps a {@link Component} to provide 
064  * additional mouse handling functionalities based on that component.</p>
065  * <p>A {@link Mouse} widget is {@link Configurable} and is also an event source which 
066  * fires mouse events and acts just like a mouse handle. Note that, the mouse events are 
067  * fired from the mouse wrapper widget not the wrapped component so that the event 
068  * listeners registered with the wrapped component may also handle them.</p>
069  * <p>Meanwhile, it is interesting that a {@link Mouse} widget is also an event listener 
070  * that handles {@link Render} event fired from the wrapped component, which means an 
071  * extended class of this one can do more things in an event handler method that overrides 
072  * the handler method of this class, such as {@link Mouse#onEvent(Render)}, but calling the 
073  * overridden method would be the first thing for the overriding one to do not to lose the 
074  * mouse handling of this class.</p>
075  * <p>This class fires {@link DragMove} events by its local event dispatcher other than 
076  * the general one.</p>
077  * 
078  * @author <a href="mailto:jianjunliu@126.com">J.J.Liu (Jianjun Liu)</a> at <a href="http://www.jscripter.org" target="_blank">http://www.jscripter.org</a>
079  */
080 public class Mouse extends Widget implements OnRender
081 {
082     /**
083      * <p>A typical constructor forcing constructors of subclasses to pass a component.</p>
084      * <p>This constructor tries to initialize the newly constructed mouse widget. 
085      * If the wrapped component is not rendered, it adds this event listener to the 
086      * component to handle {@link Render} event and makes the event handling method 
087      * {@link #onEvent(Render)} do the initialization.</p>
088      * @param e The underlying component.
089      * @since 1.0
090      */
091     protected Mouse(Component e) {
092         super(e);
093         if (!e.isRendered()) {
094             e.addListener(Render.class, this);
095         }
096         init(this);
097     }
098 
099     /**
100      * <p>Performs an action on the dispatched event.</p>
101      * <p>This method initializes the mouse wrapper widget.</p>
102      * @param evt The event dispatched to this listener.
103      * @since 1.0
104      */
105     public void onEvent(Render evt) {
106         init(this);
107     }
108 
109     private final static Var<TaskManager> DISPATCHER = new Static<TaskManager>(new Var<TaskManager>() {
110         @Override
111         public TaskManager var() {
112             return new TaskManager();
113         }
114     });
115 
116     private final static Id<Boolean> CAPTURING = new Id<Boolean>();
117     private final static Id<Integer> DELAY = new Id<Integer>();
118     private final static Id<Handle> LOSECAPTURE = new Id<Handle>();
119     private final static Id<Handle> MOUSEDOWN = new Id<Handle>();
120     private final static Id<Handle> MOUSEMOVE = new Id<Handle>();
121     private final static Id<Handle> MOUSEOVER = new Id<Handle>();
122     private final static Id<Handle> MOUSEOUT = new Id<Handle>();
123     private final static Id<Handle> MOUSERELEASE = new Id<Handle>();
124     private final static Id<Handle> MOUSEUP = new Id<Handle>();
125     private final static Id<Mouse> MOUSE = new Id<Mouse>();
126 
127     /**
128      * <p>Gets the mouse widget for a component.</p>
129      * <p>The method simply returns the mouse widget that wraps the specified component if 
130      * there is one. Otherwise, it creates one to wrap the component and returns the newly 
131      * created widget.</p>
132      * @param e An component to handle mouse operations.
133      * @return A mouse widget that wraps the specified component.
134      * @since 1.0
135      */
136     public static final Mouse getMouse(Component e) {
137         Mouse h = ini(e).var(MOUSE);
138         if (Js.not(h)) {
139             h = new Mouse(e);
140             ini(e).var(MOUSE, h);
141         }
142         return h;
143     }
144 
145     /**
146      * <p>Gets the delay of a mouse in milliseconds.</p>
147      * <p>This method returns a number value that specifies the milliseconds the mouse 
148      * needs to wait before firing a {@link MouseOver} event and a {@link DragStart} 
149      * event for dragging mode when it has been pressed.</p>
150      * @param h The current mouse wrapper.
151      * @return The milliseconds to wait before firing a {@link MouseOver} or 
152      * {@link DragStart} event.
153      * @since 1.0
154      */
155     public static final int getDelay(Mouse h) {
156         Integer d = ini(h).var(DELAY);
157         return Variables.undefined(d) ? 0 : d;
158     }
159 
160     /**
161      * <p>Sets the delay of a mouse in milliseconds.</p>
162      * <p>This method sets the number of milliseconds that the mouse needs to wait before 
163      * firing a {@link MouseOver} event and a {@link DragStart} event for dragging mode 
164      * when it has been pressed.</p>
165      * <p>This method may resets the mouse wrapper and should not be called in an event 
166      * handler method that is handling mouse events fired from this mouse.</p>
167      * @param h The current mouse wrapper.
168      * @param delay The milliseconds to wait before firing a {@link MouseOver} or 
169      * {@link DragStart} events.
170      * @since 1.0
171      */
172     public static final void setDelay(Mouse h, int delay) {
173         if (Js.neq(delay, getDelay(h))) {
174             if (delay > 0) {
175                 ini(h).var(DELAY, delay);
176             } else {
177                 ObjectLikes.delete(ini(h), DELAY);
178             }
179             destroy(h);
180             init(h);
181         }
182     }
183 
184     /**
185      * <p>Sets the delay of a mouse in milliseconds.</p>
186      * <p>This method sets the number of milliseconds that the mouse needs to wait before 
187      * firing a {@link DragStart} event when it has been pressed.</p>
188      * <p>This method may resets the mouse wrapper and should not be called in an event 
189      * handler method that is handling mouse events fired from this mouse.</p>
190      * @param h The current mouse wrapper.
191      * @param delay The milliseconds to wait before firing a {@link DragStart} event.
192      * @since 1.0
193      */
194     public static final void delay(Mouse h, int delay) {
195         if (Mouse.getDelay(h) < delay) {
196             Mouse.setDelay(h, delay);
197         }
198     }
199 
200     private final static ArrayLike<Id<Handle>> HANDLES = new Vars<Id<Handle>>()
201             .add(LOSECAPTURE)
202             .add(MOUSEMOVE)
203             .add(MOUSEOVER)
204             .add(MOUSEOUT)
205             .add(MOUSERELEASE)
206             .add(MOUSEUP)
207             .var();
208 
209     /**
210      * <p>Destroys the specified mouse.</p>
211      * <p>It detaches the current mouse wrapper from its underlying component but not that 
212      * the component is to destroy.</p>
213      * <p>A destroyed mouse will not fire any events but you can get it back with a call to 
214      * the method {@link #getMouse(Component)} with the same component.</p>
215      * <p>This method may resets the mouse wrapper and should not be called in an event 
216      * handler method that is handling mouse events fired from this mouse.</p>
217      * @param h The current mouse wrapper.
218      * @since 1.0
219      */
220     public static final void destroy(Mouse h) {
221         ObjectLike ini = ini(h);
222         Handle down = ini.var(MOUSEDOWN);
223         if (Js.be(down)) {
224             Handle.detach(down);
225             ObjectLikes.delete(ini, MOUSEDOWN);
226             ObjectLikes.delete(ini, HANDLES);
227             ObjectLikes.delete(ini(h.unwrap()), MOUSE);
228         }
229     }
230 
231     private static final JsFunction<Void> mouseOver(final Mouse h) {
232         JsFunction<Void> f = new Function<Void>() {
233             @Override
234             protected Void function(Object jsthis, Call<Void> callee) {
235                 synchronized(JsApplet.class) {
236                     JsEvent be = new JsEvent((JsObject)callee.arguments.get(0));
237                     if (!Browser.isIE) {
238                         Events.stop(be);
239                     }
240                     Component t = Component.get(Events.srcElement(be));
241                     if (Js.be(ini(h).var(CAPTURING))) {
242                         h.fire(new DragOver(t));
243                     } else {
244                         h.fire(new MouseOver(t));
245                     }
246                 }
247                 return null;
248             }
249         }.var();
250         return Browser.isIE ? new JsFunction<Void>(
251                 Handle.IE.var().invoke(f)
252         ) : f;
253     }
254 
255     private static final JsFunction<Void> mouseOut(final Mouse h) {
256         JsFunction<Void> f = new Function<Void>() {
257             @Override
258             protected Void function(Object jsthis, Call<Void> callee) {
259                 synchronized(JsApplet.class) {
260                     JsEvent be = new JsEvent((JsObject)callee.arguments.get(0));
261                     if (!Browser.isIE) {
262                         Events.stop(be);
263                     }
264                     Component t = Component.get(Events.srcElement(be));
265                     if (Js.be(ini(h).var(CAPTURING))) {
266                         h.fire(new DragOut(t));
267                     } else {
268                         h.fire(new MouseOut(t));
269                     }
270                 }
271                 return null;
272             }
273         }.var();
274         return Browser.isIE ? new JsFunction<Void>(
275                 Handle.IE.var().invoke(f)
276         ) : f;
277     }
278 
279     private static final JsFunction<Void> mouseMove(final Mouse h) {
280         JsFunction<Void> f = new Function<Void>() {
281             @Override
282             protected Void function(Object jsthis, Call<Void> callee) {
283                 synchronized(JsApplet.class) {
284                     JsEvent be = new JsEvent((JsObject)callee.arguments.get(0));
285                     if (!Browser.isIE) {
286                         Events.stop(be);
287                     }
288                     Component t = Component.get(Events.srcElement(be));
289                     if (Js.be(ini(h).var(CAPTURING))) {
290                         h.fire(new DragMove(
291                                 t,
292                                 Events.pageX(be),
293                                 Events.pageY(be)
294                         ), DISPATCHER.var());
295                     } else {
296                         h.fire(new MouseMove(
297                                 t,
298                                 Events.pageX(be),
299                                 Events.pageY(be)
300                         ), DISPATCHER.var());
301                     }
302                 }
303                 return null;
304             }
305         }.var();
306         return Browser.isIE ? new JsFunction<Void>(
307                 Handle.IE.var().invoke(f)
308         ) : f;
309     }
310 
311     private static final JsFunction<Void> mouseUp(final Mouse h) {
312         JsFunction<Void> f = new Function<Void>() {
313             @Override
314             protected Void function(Object jsthis, Call<Void> callee) {
315                 synchronized(JsApplet.class) {
316                     ObjectLike ini = ini(h);
317                     JsEvent be = new JsEvent((JsObject)callee.arguments.get(0));
318                     if (!Browser.isIE) {
319                         Events.stop(be);
320                         h.unwrap().pseudo(ACTIVE, "active").getRemover().invoke();
321                     }
322                     if (Js.be(ini.var(CAPTURING))) {
323                         ObjectLikes.delete(ini, CAPTURING);
324                         h.fire(new DragStop(
325                                 Component.get(Events.srcElement(be)),
326                                 Events.pageX(be),
327                                 Events.pageY(be)
328                         ));
329                     } else {
330                         ini.var(CAPTURING, true);
331                         h.fire(new MouseUp(
332                                 Component.get(Events.srcElement(be))
333                         ));
334                     }
335                     Handle.detach(ini, HANDLES);
336                     Events.releaseCapture(
337                             (JsNode)getEventTarget(h)
338                     );
339                 }
340                 return null;
341             }
342         }.var();
343         return Browser.isIE ? new JsFunction<Void>(
344                 Handle.IE.var().invoke(f)
345         ) : f;
346     }
347 
348     private static final EventTarget setCapture(Mouse h) {
349         EventTarget et = Win.document.var();
350         if (Browser.isIE) {
351             et = getEventTarget(h);
352             handle(LOSECAPTURE, "losecapture" , mouseUp(h), et, h, true);
353             Events.setCapture((JsNode)et);
354         }
355         return et;
356     }
357 
358     private static final void captureUp(EventTarget et, Mouse h) {
359         handle(MOUSEUP, "mouseup" , mouseUp(h), et, h, true);
360     }
361 
362     private static final void capture(EventTarget et, Mouse h) {
363         ini(h).var(CAPTURING, true);
364         handle(MOUSERELEASE, "mouseup"  , mouseUp  (h), et, h, true);
365         handle(MOUSEOUT ,    "mouseout" , mouseOut (h), et, h, true);
366         handle(MOUSEOVER,    "mouseover", mouseOver(h), et, h, true);
367         handle(MOUSEMOVE,    "mousemove", mouseMove(h), et, h, true);
368     }
369 
370     private static final JsFunction<Void> mouseDown(final Mouse h) {
371         JsFunction<Void> f = new Function<Void>() {
372             @Override
373             protected Void function(Object jsthis, Call<Void> callee) {
374                 synchronized(JsApplet.class) {
375                     final JsEvent be = new JsEvent((JsObject)callee.arguments.get(0));
376                     Events.stop(be);
377                     final Component he = h.unwrap();
378                     final DragStart dragStart = new DragStart(
379                             he,
380                             Events.pageX(be),
381                             Events.pageY(be)
382                     );
383                     h.fire(new Popup());
384                     h.fire(new MouseDown(he));
385                     final EventTarget et = setCapture(h);
386                     int delay = getDelay(h);
387                     if (Js.not(delay)) {
388                         h.fire(dragStart);
389                         capture(et, h);
390                     } else {
391                         ObjectLikes.delete(ini(he), PREVENTCLICK);
392                         new Timeout() {
393                             @Override
394                             public void run() {
395                                 synchronized(JsApplet.class) {
396                                     ObjectLike ini = ini(h);
397                                     if (Js.not(ini.var(CAPTURING))) {
398                                         Handle.detach(ini.var(MOUSEUP));
399                                         ini(h.unwrap()).var(PREVENTCLICK, true);
400                                         h.fire(dragStart);
401                                         capture(et, h);
402                                     } else {
403                                         ObjectLikes.delete(ini, CAPTURING);
404                                     }
405                                 }
406                             }
407                         }.set(delay);
408                         captureUp(et, h);
409                     }
410                 }
411                 return null;
412             }
413         }.var();
414         return Browser.isIE ? new JsFunction<Void>(
415                 Handle.IE.var().invoke(f)
416         ) : f;
417     }
418 
419     private static final void init(final Mouse h) {
420         if (h.isRendered()) {
421             EventTarget et = getEventTarget(h);
422             handle(MOUSEDOWN, "mousedown", mouseDown(h), et, h, false);
423         }
424     }
425 
426     private static final void handle(Id<Handle> id, String evt, JsFunction<Void> f, EventTarget et, Mouse t, boolean cap) {
427         Handle h = ini(t).var(id);
428         if (Js.not(h)) {
429             h = new Handle(et, evt, f, cap);
430             ini(t).var(id, h);
431         }
432         h.attach();
433     }
434 
435     private static final EventTarget getEventTarget(Mouse h) {
436         return Component.getElement(h.unwrap());
437     }
438 }