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 }