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;
017    
018    import java.util.List;
019    
020    import asquare.gwt.tk.client.ui.behavior.ControllerAdaptor;
021    import asquare.gwt.tk.client.ui.behavior.FocusModel;
022    import asquare.gwt.tk.client.util.DomUtil;
023    import asquare.gwt.tk.client.util.KeyMap;
024    
025    import com.google.gwt.user.client.*;
026    import com.google.gwt.user.client.ui.*;
027    
028    /**
029     * A modal dialog tailored to conveniently displaying alerts. 
030     * <p>Features: 
031     * <dl>
032     * <dt>Icon</dt>
033     * <dd>An image indicating the type / severity of the condition. </dd>
034     * <dt>Caption Text</dt>
035     * <dd>A brief summary of the condition which triggered the dialog. Displayed in the "title". </dd>
036     * <dt>Message</dt>
037     * <dd>A detail of the consequences of the actions presented by the dialog.</dd>
038     * <dt>Buttons</dt>
039     * <dd>0 or more buttons. The button text should correspond to the alert message, as if the user
040     * is answering a question. </dd>
041     * <dt>Hot keys</dt>
042     * <dd>Hot keys can be assigned to trigger actions when pressed. </dd>
043     * <dt>Button roles</dt>
044     * <dd>The "Default" button is automatically focused when the dialog is shown. 
045     * The "Cancel" button maps to the "Esc" hotkey. </dd>
046     * </dl>
047     * <p>A callback is assigned to each button in the form of a {@link com.google.gwt.user.client.Command Command}. 
048     * When a button is pressed, the dialog will be hidden and the command will be executed. 
049     * <h3>CSS Style Rules</h3>
050     * <ul class='css'>
051     * <li>.tk-AlertDialog-defaultButton { the default button (if applicable) }</li>
052     * <li>.tk-AlertDialog-captionLeft { the left part of the caption (contains the icon)}</li>
053     * <li>.tk-AlertDialog-captionCenter { the center part of the caption (contains the caption text) }</li>
054     * <li>.tk-AlertDialog-captionRight { the right part of the caption (set width to center caption text) }</li>
055     * <li>.tk-AlertDialog-captionIcon { the caption icon itself }</li>
056     * <li>.tk-AlertDialog-message { the message between the caption and the buttons }</li>
057     * <li>.tk-AlertDialog-buttons { the panel containing the buttons }</li>
058     * <li>.tk-AlertDialog-hotKeyChar { the hotkey character in the button text (availible in factory generated dialogs) }</li>
059     * </ul>
060     * 
061     * @see <a
062     *      href="http://developer.apple.com/documentation/UserExperience/Conceptual/OSXHIGuidelines/XHIGWindows/chapter_17_section_6.html">Apple
063     *      Human Interface Guidlines</a>
064     * @see <a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwue/html/ch09f.asp">Windows User Interface Guidelines</a>
065     */
066    public class AlertDialog extends ModalDialog
067    {
068            /**
069             * Text for the "OK" button used in factory generated dialogs. Applies the
070             * <code>.tk-AlertDialog-hotKeyChar</code> style to "O".
071             */
072            public static final String TEXT_OK = "<span class='tk-AlertDialog-hotKeyChar'>O</span>K";
073            
074            /**
075             * Text for the "Cancel" button used in factory generated dialogs. Applies the
076             * <code>.tk-AlertDialog-hotKeyChar</code> style to "C".
077             */
078            public static final String TEXT_CANCEL = "<span class='tk-AlertDialog-hotKeyChar'>C</span>ancel";
079            
080            /**
081             * Indicates a button which has no special roles. 
082             */
083            public static final int BUTTON_PLAIN = 0;
084            
085            /**
086             * Indicates that a button has the <em>Default</em> role. The
087             * <em>Default</em> button will have initial focus. Never make an action 
088             * default if it could have severe consequences such as data loss. The
089             * dialog may only have one button of this type.
090             */
091            public static final int BUTTON_DEFAULT = 1 << 0;
092            
093            /**
094             * Indicates that a button has the <em>Cancel</em> role. Pressing
095             * <code>Esc</code> will execute the button's associated command. The
096             * dialog may only have one button of this type.
097             */
098            public static final int BUTTON_CANCEL = 1 << 1;
099            
100            /**
101             * Indicates that a button has both the <em>Default</em> and
102             * <em>Cancel</em> roles. It will have initial focus and the user can press
103             * <code>Esc</code> to execute this button's command. The dialog may
104             * have no other buttons of type <code>Default</code> or
105             * <code>Cancel</code>.
106             */
107            public static final int BUTTON_CANCEL_DEFAULT = BUTTON_DEFAULT | BUTTON_CANCEL;
108            
109            private final ColumnPanel m_buttonPanel = new ColumnPanel();
110            private final KeyMap m_keyMap = new KeyMap();
111            
112            private Image m_captionIcon = null;
113            private String m_captionText = null;
114            private boolean m_captionTextAsHtml = false;
115            private Widget m_message = null;
116            private Widget m_defaultButton = null;
117            
118            /**
119             * Creates an empty AlertDialog. 
120             */
121            public AlertDialog()
122            {
123                    addStyleName("tk-AlertDialog");
124            }
125            
126            /**
127             * Creates a low severity modal dialog with an OK button. 
128             * Use for hints, tips, welcome messages, etc.... 
129             * 
130             * @param okCommand a command to execute after the dialog is dismissed, or null
131             * @param captionText a String to display in the dialog title, or null
132             * @param message text a String display in the content area of the dialog, or null
133             */
134            public static AlertDialog createInfo(Command okCommand, String captionText, String message)
135            {
136                    AlertDialog dialog = new AlertDialog();
137                    dialog.setCaptionText(captionText, false);
138                    dialog.setMessage(message);
139                    dialog.setIcon(new Icon("InfoIcon16.gif", 16, 16));
140                    dialog.addButton(TEXT_OK, 'o', okCommand, BUTTON_DEFAULT | BUTTON_CANCEL);
141                    return dialog;
142            }
143            
144            /**
145             * Creates a medium severity modal dialog with a OK and Cancel buttons. 
146             * Use for "Do you want to continue" style dialogs. 
147             * 
148             * @param okCommand a command to execute if the user presses the OK button, or null
149             * @param captionText a String to display in the dialog title, or null
150             * @param message text a String display in the content area of the dialog, or null
151             */
152            public static AlertDialog createWarning(Command okCommand, String captionText, String message)
153            {
154                    AlertDialog dialog = new AlertDialog();
155                    dialog.setCaptionText(captionText, false);
156                    dialog.setMessage(message);
157                    dialog.setIcon(new Icon("AlertIcon16.gif", 16, 16));
158                    dialog.addButton(TEXT_OK, 'o', okCommand, BUTTON_DEFAULT);
159                    dialog.addButton(TEXT_CANCEL, 'c', null, BUTTON_CANCEL);
160                    return dialog;
161            }
162            
163            /**
164             * Creates a high severity "stop" modal dialog with an OK button. Use when
165             * an error condition prevents the normal execution of the application.
166             * 
167             * @param okCommand a command to execute after the dialog is dismissed, or null
168             * @param captionText a String to display in the dialog title, or null
169             * @param message text a String display in the content area of the dialog, or null
170             */
171            public static AlertDialog createError(Command okCommand, String captionText, String message)
172            {
173                    AlertDialog dialog = new AlertDialog();
174                    dialog.setCaptionText(captionText, false);
175                    dialog.setMessage(message);
176                    dialog.setIcon(new Icon("ErrorIcon16.gif", 16, 16));
177                    dialog.addButton(TEXT_OK, 'o', okCommand, BUTTON_DEFAULT | BUTTON_CANCEL);
178                    return dialog;
179            }
180            
181            protected List createControllers()
182            {
183                    List result = super.createControllers();
184                    result.add(new HotKeyController());
185                    result.add(new ArrowKeyFocusController());
186                    return result;
187            }
188            
189            /**
190             * Gets the map of hotkeys to commands. Alpha keycodes are represented in upper
191             * case. 
192             * 
193             * @return the keymap
194             */
195            public KeyMap getKeyMap()
196            {
197                    return m_keyMap;
198            }
199            
200            /**
201             * Get the image which will be displayed in the caption. 
202             */
203            public Image getIcon()
204            {
205                    return m_captionIcon;
206            }
207            
208            /**
209             * Set the image will be displayed in the caption. 
210             * 
211             * @param url a URL to an image or null
212             */
213            public void setIcon(String url)
214            {
215                    setIcon(url != null ? new Image(url) : null);
216            }
217            
218            /**
219             * Set the image will be displayed in the caption. You can use an
220             * {@link Icon} to ensure size information is available when the dialog
221             * layout is calculated.
222             * 
223             * @param icon an image or null
224             * @see Icon
225             */
226            public void setIcon(Image icon)
227            {
228                    if (m_captionIcon != null)
229                    {
230                            m_captionIcon = null;
231                    }
232                    if (icon != null)
233                    {
234                            m_captionIcon = icon;
235                            m_captionIcon.addStyleName("tk-AlertDialog-captionIcon");
236                            Image.prefetch(icon.getUrl());
237                    }
238            }
239            
240            /**
241             * Get the text which will be displayed in the caption. 
242             * 
243             * @return a String or null
244             */
245            public String getCaptionText()
246            {
247                    return m_captionText;
248            }
249            
250            /**
251             * Set the text which will be displayed in the caption.
252             * 
253             * @param captionText a String or null
254             * @param asHtml true to treat <code>captionText</code> as HTML, false to
255             *            treat <code>captionText</code> as plain text
256             */
257            public void setCaptionText(String captionText, boolean asHtml)
258            {
259                    m_captionText = captionText;
260                    m_captionTextAsHtml = asHtml;
261            }
262            
263            /**
264             * Factory method which creates the caption. Called just before the dialog is
265             * shown. 
266             * 
267             * @return the caption, or <code>null</code>
268             */
269            protected Widget buildCaption()
270            {
271                    ColumnPanel captionPanel = new ColumnPanel();
272                    captionPanel.setWidth("100%"); // necessary so that descendent TD can have 100% width in Opera
273                    captionPanel.addCell();
274                    captionPanel.setCellStyleName("tk-AlertDialog-captionLeft");
275                    captionPanel.addCell();
276                    captionPanel.setCellStyleName("tk-AlertDialog-captionCenter");
277                    captionPanel.addCell();
278                    captionPanel.setCellStyleName("tk-AlertDialog-captionRight");
279                    
280                    if (m_captionIcon != null)
281                    {
282                            captionPanel.addWidgetTo(m_captionIcon, 0);
283                    }
284                    
285                    if (m_captionText != null)
286                    {
287                            if (m_captionTextAsHtml)
288                            {
289                                    DOM.setInnerHTML(captionPanel.getCellElement(1), m_captionText);
290                            }
291                            else
292                            {
293                                    DOM.setInnerText(captionPanel.getCellElement(1), m_captionText);
294                            }
295                    }
296                    
297                    return captionPanel;
298            }
299            
300            private void buildContent()
301            {
302                    if (m_message != null)
303                    {
304                            m_message.addStyleName("tk-AlertDialog-message");
305                            add(m_message);
306                    }
307                    m_buttonPanel.setStyleName("tk-AlertDialog-buttons");
308                    DomUtil.setAttribute(m_buttonPanel, "cellSpacing", "");
309                    DomUtil.setAttribute(m_buttonPanel, "cellPadding", "");
310                    add(m_buttonPanel);
311            }
312            
313            /**
314             * Get the message which will be displayed in the dialog. 
315             * 
316             * @return a String, or <code>null</code>
317             */
318            public Widget getMessage()
319            {
320                    return m_message;
321            }
322            
323            /**
324             * Set the message which will be displayed in the dialog. 
325             * 
326             * @param text a String, or <code>null</code>
327             */
328            public void setMessage(String text)
329            {
330                    setMessage(text, false);
331            }
332            
333            /**
334             * Set the message which will be displayed in the dialog. 
335             * 
336             * @param text a String, or <code>null</code>
337             * @param asHtml true to treat <code>captionText</code> as HTML, false to
338             *            treat <code>captionText</code> as plain text
339             */
340            public void setMessage(String text, boolean asHtml)
341            {
342                    if (text == null)
343                    {
344                            setMessage((Widget) null);
345                    }
346                    else
347                    {
348                            if (asHtml)
349                            {
350                                    setMessage(new HTML(text));
351                            }
352                            else
353                            {
354                                    setMessage(new Label(text));
355                            }
356                    }
357            }
358            
359            /**
360             * Set a widget to be displayed in the message area of the dialog. 
361             * 
362             * @param widget a Widget, or <code>null</code>
363             */
364            public void setMessage(Widget widget)
365            {
366                    m_message = widget;
367            }
368            
369            /**
370             * Gets the widget in the button panel corresponding to <code>index</code>. 
371             * 
372             * @param index an integer >= 0
373             * @return the button widget
374             */
375            public Widget getButton(int index)
376            {
377                    return (Button) m_buttonPanel.getWidgetAt(index, 0);
378            }
379            
380            /**
381             * Gets the number of widgets in the button panel. 
382             */
383            public int getButtonCount()
384            {
385                    return m_buttonPanel.getWidgetCount();
386            }
387            
388            /**
389             * Adds a button to button panel.
390             * 
391             * @param text the text to display in the button
392             * @param hotKey the keycode of a key which will execute the widget's
393             *            associated command when pressed
394             * @param command a command to execute if the button is clicked, or
395             *            <code>null</code>
396             * @param type a constant representing special button behavior
397             */
398            public void addButton(String text, char hotKey, Command command, int type)
399            {
400                    addButton(new Button(text), hotKey, command, type);
401            }
402            
403            /**
404             * Adds a widget to button panel. The widget will be added to the focus
405             * cycle if it implements {@link HasFocus} and does not have a tabIndex < 0.
406             * The widget must implement {@link SourcesClickEvents}. When a button is
407             * clicked, the dialog will be closed and the specified command will be
408             * executed.
409             * 
410             * @param widget the widget to add
411             * @param hotKey the keycode of a key which will execute the widget's
412             *            associated command when pressed
413             * @param command a command to execute if the button is clicked, or
414             *            <code>null</code>
415             * @param type a constant representing special button behavior
416             * @throws ClassCastException if <code>widget</code> does not implement
417             *             {@link HasFocus}
418             */
419            public void addButton(Widget widget, char hotKey, final Command command, int type)
420            {
421                    SourcesClickEvents clickable = (SourcesClickEvents) widget;
422                    boolean focusable = widget instanceof HasFocus;
423                    
424                    clickable.addClickListener(new ClickListener()
425                    {
426                            public void onClick(Widget sender)
427                            {
428                                    new HideAndExecuteCommand(AlertDialog.this, command).execute();
429                            }
430                    });
431                    m_buttonPanel.add(widget);
432                    if (focusable)
433                    {
434                            getFocusModel().add((HasFocus) widget);
435                    }
436                    if ((type & BUTTON_DEFAULT) != 0)
437                    {
438                            widget.addStyleName("tk-AlertDialog-defaultButton");
439                            m_defaultButton = widget;
440                            if (focusable)
441                            {
442                                    getFocusModel().setFocusWidget((HasFocus) widget);
443                            }
444                    }
445                    if ((type & BUTTON_CANCEL) != 0)
446                    {
447                            m_keyMap.put((char) KeyboardListener.KEY_ESCAPE, command);
448                    }
449                    if (hotKey > 0)
450                    {
451                            m_keyMap.put(Character.toUpperCase(hotKey), command);
452                    }
453            }
454            
455            /**
456             * Removes the specified button from the button panel.
457             * <em>Note: this will not remove commands that were put in the keymap when the button was added. </em>
458             * 
459             * @param button
460             * @see #getKeyMap()
461             */
462            public void removeButton(Widget button)
463            {
464                    m_buttonPanel.remove(button);
465                    if (button == m_defaultButton)
466                    {
467                            m_defaultButton = null;
468                            m_defaultButton.removeStyleName("tk-AlertDialog-defaultButton");
469                    }
470                    if (button instanceof HasFocus)
471                    {
472                            if (getFocusModel().getFocusWidget() == button)
473                            {
474                                    getFocusModel().setFocusWidget(null);
475                            }
476                            getFocusModel().remove(((HasFocus) button));
477                    }
478            }
479            
480            /*
481             *  (non-Javadoc)
482             * @see com.google.gwt.user.client.ui.PopupPanel#show()
483             */
484            public void show()
485            {
486                    show(null);
487            }
488            
489            /*
490             *  (non-Javadoc)
491             * @see asquare.gwt.tk.client.ui.ModalDialog#show(com.google.gwt.user.client.ui.HasFocus)
492             */
493            public void show(HasFocus focusOnCloseWidget)
494            {
495                    setCaption(buildCaption());
496                    buildContent();
497                    super.show(focusOnCloseWidget);
498            }
499            
500            /**
501             * A command wrapper which hides the dialog then executes a wrapped command.
502             */
503            public static class HideAndExecuteCommand implements Command
504            {
505                    private final ModalDialog m_dialog;
506                    private final Command m_command;
507                    
508                    public HideAndExecuteCommand(ModalDialog dialog, Command command)
509                    {
510                            m_dialog = dialog;
511                            m_command = command;
512                    }
513                    
514                    public void execute()
515                    {
516                            m_dialog.hide();
517                            if (m_command != null)
518                            {
519                                    DeferredCommand.add(m_command);
520                            }
521                    }
522            }
523            
524            /**
525             * A controller which listens for the onkeydown event of a registered hotkey
526             * and executes the associated command.
527             */
528            public static class HotKeyController extends ControllerAdaptor
529            {
530                    public HotKeyController()
531                    {
532                            super(Event.ONKEYDOWN, HotKeyController.class);
533                    }
534                    
535                    protected boolean doBrowserEvent(Widget widget, Event event)
536                    {
537                            final AlertDialog dialog = (AlertDialog) widget;
538                            char keyCode = (char) DomUtil.eventGetKeyCode(event);
539                            if (dialog.getKeyMap().containsKey(keyCode))
540                            {
541                                    Command command = dialog.getKeyMap().get(keyCode);
542                                    new HideAndExecuteCommand(dialog, command).execute();
543                                    return false;
544                            }
545                            return true;
546                    }
547            }
548            
549            /**
550             * A controller which cycles the focus when the arrow keys are pressed.
551             */
552            public static class ArrowKeyFocusController extends ControllerAdaptor
553            {
554                    public ArrowKeyFocusController()
555                    {
556                            super(Event.ONKEYDOWN, ArrowKeyFocusController.class);
557                    }
558                    
559                    protected boolean doBrowserEvent(Widget widget, Event event)
560                    {
561                            boolean result = true;
562                            FocusModel focusModel = ((ModalDialog) widget).getFocusModel();
563                            if (focusModel != null && focusModel.getSize() > 1)
564                            {
565                                    char keyCode = (char) DomUtil.eventGetKeyCode(event);
566                                    if (keyCode == KeyboardListener.KEY_RIGHT || keyCode == KeyboardListener.KEY_DOWN)
567                                    {
568                                            // increment focus
569                                            focusModel.getNextWidget().setFocus(true);
570                                            result = false;
571                                    }
572                                    else if (keyCode == KeyboardListener.KEY_LEFT || keyCode == KeyboardListener.KEY_UP)
573                                    {
574                                            // decrement focus
575                                            focusModel.getPreviousWidget().setFocus(true);
576                                            result = false;
577                                    }
578                            }
579                            return result;
580                    }
581            }
582    }