001    /*
002     * Copyright 2006 Mat Gessel <mat.gessel@gmail.com>
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     * 
008     * http://www.apache.org/licenses/LICENSE-2.0
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package asquare.gwt.tk.client.ui.behavior;
017    
018    import java.util.Vector;
019    
020    import asquare.gwt.tk.client.util.GwtUtil;
021    
022    import com.google.gwt.user.client.ui.FocusWidget;
023    import com.google.gwt.user.client.ui.HasFocus;
024    
025    /**
026     * A primitive focus model. Tracks the widgets in a focus cycle and which widget
027     * <em>should</em> be focused. 
028     */
029    public class FocusModel
030    {
031            private final Vector m_listeners = new Vector();
032            private final Vector m_widgets = new Vector();
033            
034            private int m_focusIndex = -1;
035            
036            public void addListener(FocusModelListener listener)
037            {
038                    m_listeners.add(listener);
039            }
040            
041            public void removeListener(FocusModelListener listener)
042            {
043                    m_listeners.remove(listener);
044            }
045            
046            /**
047             * Get the number of widgets in the model. 
048             */
049            public int getSize()
050            {
051                    return m_widgets.size();
052            }
053            
054            /**
055             * Remove all widgets from the model. 
056             */
057            public void clear()
058            {
059                    HasFocus[] removed = new HasFocus[m_widgets.size()];
060                    for (int i = removed.length - 1; i >= 0; i--)
061                    {
062                            removed[i] = (HasFocus) m_widgets.get(i);
063                    }
064                    m_widgets.clear();
065                    m_focusIndex = -1;
066                    fireRemoved(removed);
067            }
068            
069            /**
070             * Add widgets to the model in bulk.
071             * 
072             * @param widgets an array of 0 or more widgets
073             * @throws IllegalArgumentException if <code>widgets</code> is <code>null</code>
074             * @throws IllegalArgumentException if an element is <code>null</code>
075             * @throws IllegalArgumentException if an element is already present in the model
076             * @see #shouldAdd(HasFocus)
077             */
078            public void add(HasFocus[] widgets)
079            {
080                    if (widgets == null)
081                            throw new IllegalArgumentException();
082                    
083                    if (widgets.length == 0)
084                            return;
085                    
086                    Vector added = new Vector();
087                    for (int i = 0; i < widgets.length; i++)
088                    {
089                            if (insertImpl(widgets[i], m_widgets.size()))
090                            {
091                                    added.add(widgets[i]);
092                            }
093                    }
094                    if (added.size() > 0)
095                    {
096                            HasFocus[] result = new HasFocus[added.size()];
097                            GwtUtil.toArray(added, result);
098                            fireAdded(result);
099                    }
100            }
101            
102            /**
103             * Add a widget to the model. 
104             * 
105             * @param widget a widget
106             * @throws IllegalArgumentException if <code>widget</code> is <code>null</code>
107             * @throws IllegalArgumentException if <code>widget</code> is already present in the model
108             * @see #shouldAdd(HasFocus)
109             */
110            public void add(HasFocus widget)
111            {
112                    insert(widget, m_widgets.size());
113            }
114            
115            /**
116             * Inserts a widget into to the model at the specified index.
117             * 
118             * @param widget a widget
119             * @param index an integer greater than 0 and less than or equal to number of
120             *            widgets in the model
121             * @throws IllegalArgumentException if <code>widget</code> is <code>null</code>
122             * @throws IllegalArgumentException if <code>widget</code> is already present in the model
123             * @throws IndexOutOfBoundsException if <code>index < 0 || index > {@link #getSize()}</code>
124             * @see #shouldAdd(HasFocus)
125             */
126            public void insert(HasFocus widget, int index)
127            {
128                    if (insertImpl(widget, index))
129                    {
130                            fireAdded(new HasFocus[] {widget});
131                    }
132            }
133            
134            /**
135             * @return true if <code>widget</code> is added to the model
136             */
137            private boolean insertImpl(HasFocus widget, int index)
138            {
139                    if (widget == null)
140                            throw new IllegalArgumentException("widget cannot be null");
141                    
142                    GwtUtil.rangeCheck(0, m_widgets.size(), index, true);
143                    
144                    if (m_widgets.indexOf(widget) != -1)
145                            throw new IllegalArgumentException("cannot add widget twice");
146                    
147                    boolean result = false;
148                    
149                    if(shouldAdd(widget))
150                    {
151                            m_widgets.insertElementAt(widget, index);
152                            if (index <= m_focusIndex)
153                            {
154                                    m_focusIndex++;
155                            }
156                            result = true;
157                    }
158                    return result;
159            }
160            
161            /**
162             * Determines whether the specified Widget can be added to the model. 
163             * 
164             * @param widget a widget which is candidate to be added to the model
165             * @return <code>true</code> if <code>widget.getTabIndex() >= 0</code>
166             */
167            protected boolean shouldAdd(HasFocus widget)
168            {
169                    return widget.getTabIndex() >= 0;
170            }
171            
172            /**
173             * Get the index corresponding to the currently focused widget.
174             * 
175             * @return a value between <code>0</code> and
176             *         <code>{@link #getSize()}</code>, or <code>-1</code> if no
177             *         widget is focused
178             */
179            public int getCurrentIndex()
180            {
181                    return m_focusIndex;
182            }
183            
184            /**
185             * Get the index of the specified widget in the model.
186             * 
187             * @param widget a widget
188             * @return a valid index, or <code>-1</code> if <code>widget</code> is
189             *         not present in the model
190             * @throws IllegalArgumentException if <code>widget</code> is
191             *             <code>null</code>
192             */
193            public int getIndexOf(HasFocus widget)
194            {
195                    if (widget == null)
196                            throw new IllegalArgumentException();
197                    
198                    return m_widgets.indexOf(widget);
199            }
200            
201            /**
202             * Get the widget at the specified index. 
203             * 
204             * @param index an integer greater than 0 and less than or equal to number of
205             *            widgets in the model
206             * @return the widget
207             * @throws IndexOutOfBoundsException if <code>index</code> is out of range
208             */
209            public HasFocus getWidgetAt(int index)
210            {
211                    GwtUtil.rangeCheck(0, m_widgets.size(), index, false);
212                    
213                    return (HasFocus) m_widgets.get(index);
214            }
215            
216            /**
217             * Removes a widget from the model. 
218             * 
219             * @param widget
220             */
221            public void remove(HasFocus widget)
222            {
223                    int index = m_widgets.indexOf(widget);
224                    if (index >= 0)
225                    {
226                            remove(index);
227                    }
228            }
229            
230            private void remove(int index)
231            {
232                    HasFocus widget = (HasFocus) m_widgets.remove(index);
233                    if (m_focusIndex > index)
234                    {
235                            m_focusIndex--;
236                    }
237                    else if (m_focusIndex == index)
238                    {
239                            m_focusIndex = -1;
240                    }
241                    fireRemoved(new HasFocus[] {widget});
242            }
243            
244            /**
245             * Get the widget which is focused.  
246             * 
247             * @return a widget, or <code>null</code> if no widget is focused
248             */
249            public HasFocus getFocusWidget()
250            {
251                    return (m_focusIndex != -1) ? (HasFocus) m_widgets.get(m_focusIndex) : null;
252            }
253            
254            /**
255             * Set the widget which is focused.
256             * 
257             * @param widget a widget in the model, or <code>null</code>
258             * @throws IllegalArgumentException if <code>widget</code> is not
259             *             present in the model and not <code>null</code>
260             */
261            public void setFocusWidget(HasFocus widget)
262            {
263                    int index;
264                    if (widget == null)
265                    {
266                            index = -1;
267                    }
268                    else
269                    {
270                            index = m_widgets.indexOf(widget);
271                            if (index == -1)
272                                    throw new IllegalArgumentException();
273                    }
274                    m_focusIndex = index;
275    //              m_focusIndex = (widget == null) ? -1 : m_widgets.indexOf(widget);
276            }
277            
278            /**
279             * Get the next widget in the cycle.  
280             * The widget is skipped if <code>(widget.getTabIndex() < 0 || ! widget.isEnabled())</code>.
281             * 
282             * @param forward <code>true</code> to cycle forward, <code>false</code> to cycle backward
283             * @return the next focusable widget, or <br/>
284             * the currently focused widget if no other focusable widget is available, or<br/>
285             * <code>null</code> no if focusable widget is available
286             * @throws IllegalStateException if the model is empty
287             */
288            public HasFocus getNextWidget(boolean forward)
289            {
290                    int nextIndex = getNextIndex(m_focusIndex, forward);
291                    if (nextIndex != -1)
292                    {
293                            return (HasFocus) getWidgetAt(nextIndex); 
294                    }
295                    return null;
296            }
297            
298            /**
299             * Get the widget after the currently focused widget.  
300             * 
301             * @return the next widget
302             * @throws IllegalStateException if the model is empty
303             */
304            public HasFocus getNextWidget()
305            {
306                    return getNextWidget(true);
307            }
308            
309            /**
310             * Get the widget previous to the currently focused widget.  
311             * 
312             * @return the previous widget
313             * @throws IllegalStateException if the model is empty
314             */
315            public HasFocus getPreviousWidget()
316            {
317                    return getNextWidget(false);
318            }
319            
320            private int getNextIndex(int index, boolean forward)
321            {
322                    int size = m_widgets.size();
323                    GwtUtil.rangeCheck(-1, size + 1, index, false);
324                    if (size == 0)
325                            throw new IllegalStateException();
326                    
327                    return getNextIndex(index, index, m_widgets.size(), forward);
328            }
329            
330            private int getNextIndex(int initialIndex, int current, int size, boolean forward)
331            {
332                    // avoid infinite loop if initialIndex = -1
333                    if (initialIndex == -1)
334                    {
335                            if (forward && current == size - 1 || ! forward && current == 0) 
336                                    return initialIndex;
337                    }
338                    
339                    if (forward)
340                    {
341                            current = (current + 1) % size;
342                    }
343                    else
344                    {
345                            current = (current > 0) ? current - 1 : size - 1;
346                    }
347                    
348                    // give up after looping once
349                    if (current == initialIndex)
350                            return initialIndex;
351                    
352                    // return if widget is not a disabled FocusWidget
353                    if (shouldFocus((HasFocus) m_widgets.get(current)))
354                            return current;
355                    
356                    return getNextIndex(initialIndex, current, size, forward);
357            }
358            
359            /**
360             * Determines whether the specified Widget can receive focus.
361             * 
362             * @param w a widget in this model which is candidate for focus
363             * @return <code>true</code> unless <code>w</code> is a disabled
364             *         {@link FocusWidget}
365             */
366            protected boolean shouldFocus(HasFocus w)
367            {
368                    if (w instanceof FocusWidget)
369                    {
370                            return ((FocusWidget) w).isEnabled();
371                    }
372                    return true;
373            }
374            
375            private void fireAdded(HasFocus[] added)
376            {
377                    Object[] listeners = m_listeners.toArray();
378                    for (int i = 0; i < listeners.length; i++)
379                    {
380                            ((FocusModelListener) listeners[i]).widgetsAdded(this, added);
381                    }
382            }
383            
384            private void fireRemoved(HasFocus[] removed)
385            {
386                    Object[] listeners = m_listeners.toArray();
387                    for (int i = 0; i < listeners.length; i++)
388                    {
389                            ((FocusModelListener) listeners[i]).widgetsRemoved(this, removed);
390                    }
391            }
392    }