blob: 976b62919122bddec16fe4ff47ed3ef200a503d7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.openide;
import org.openide.util.Utilities;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.swing.*;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
/** Permits dialogs to be displayed.
* @author Jesse Glick
* @since 3.14
*/
public abstract class DialogDisplayer {
/** Subclass constructor. */
protected DialogDisplayer() {
}
/** Get the default dialog displayer.
* @return the default instance from lookup
*/
public static DialogDisplayer getDefault() {
DialogDisplayer dd = Lookup.getDefault ().lookup (DialogDisplayer.class);
if (dd == null) {
dd = new Trivial();
}
return dd;
}
/** Notify the user of something in a message box, possibly with feedback.
* <p>To support both GUI and non-GUI use, this method may be called
* from any thread (providing you are not holding any locks), and
* will block the caller's thread. In GUI mode, it will be run in the AWT
* event thread automatically. If you wish to hold locks, or do not
* need the result object immediately or at all, please make this call
* asynchronously (e.g. from the request processor).
* @param descriptor description of the notification
* @return the option that caused the message box to be closed
*/
public abstract Object notify(NotifyDescriptor descriptor);
/** Notify the user of something in a message box, possibly with feedback,
* this method method may be called
* from any thread. The thread will return immediately and
* the dialog will be shown <q>later</q>, usually when AWT thread
* is empty and can handle the request.
*
* <p class="non-normative">
* Implementation note: Since version 7.3, implementation improved to work
* also before main window is opened. For example: When method is called
* from ModuleInstall.restored, then modal dialog is opened and blocks main
* window until dialog is closed. Typical use case is login dialog.
* </p>
*
* @param descriptor description of the notification
* @since 7.0
*/
public void notifyLater(final NotifyDescriptor descriptor) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
DialogDisplayer.this.notify(descriptor);
}
});
}
/** Get a new standard dialog.
* The dialog is designed and created as specified in the parameter.
* Anyone who wants a dialog with standard buttons and
* standard behavior should use this method.
* <p><strong>Do not cache</strong> the resulting dialog if it
* is modal and try to reuse it! Always create a new dialog
* using this method if you need to show a dialog again.
* Otherwise previously closed windows can reappear.
* @param descriptor general description of the dialog
* @return the new dialog
*/
public abstract Dialog createDialog(DialogDescriptor descriptor);
/**
* Same as #createDialog(org.openide.DialogDescriptor) except that it's possible
* to specify dialog's parent Frame window. When a document window is floated
* and has focus then new dialog window will use it as a parent window by default.
* That means non-modal dialogs will close when that document window is closed.
* To avoid such situation pass WindowManager.getDefault().getMainWindow() as
* dialog parent window.
* @param descriptor general description of the dialog
* @param parent Dialgo parent frame.
* @return New dialog
* @since 7.38
*/
public Dialog createDialog(DialogDescriptor descriptor, Frame parent) {
return createDialog(descriptor);
}
/**
* Minimal implementation suited for standalone usage.
* @see "#30031"
*/
private static final class Trivial extends DialogDisplayer {
public Object notify(NotifyDescriptor nd) {
JDialog dialog = new StandardDialog(nd.getTitle(), true, nd, null, null);
dialog.setVisible(true);
return (nd.getValue() != null) ? nd.getValue() : NotifyDescriptor.CLOSED_OPTION;
}
public Dialog createDialog(final DialogDescriptor dd) {
final StandardDialog dialog = new StandardDialog(
dd.getTitle(), dd.isModal(), dd, dd.getClosingOptions(), dd.getButtonListener()
);
dd.addPropertyChangeListener(new DialogUpdater(dialog, dd));
return dialog;
}
/**
* Given a message object, create a displayable component from it.
*/
private static Component message2Component(Object message) {
if (message instanceof Component) {
return (Component) message;
} else if (message instanceof Object[]) {
Object[] sub = (Object[]) message;
JPanel panel = new JPanel();
panel.setLayout(new FlowLayout());
for (int i = 0; i < sub.length; i++) {
panel.add(message2Component(sub[i]));
}
return panel;
} else if (message instanceof Icon) {
return new JLabel((Icon) message);
} else {
// bugfix #35742, used JTextArea to correctly word-wrapping
String text = message.toString();
JTextArea area = new JTextArea(text);
Color c = UIManager.getColor("Label.background"); // NOI18N
if (c != null) {
area.setBackground(c);
}
area.setLineWrap(true);
area.setWrapStyleWord(true);
area.setEditable(false);
area.setTabSize(4); // looks better for module sys messages than 8
area.setColumns(40);
if (text.indexOf('\n') != -1) {
// Complex multiline message.
return new JScrollPane(area);
} else {
// Simple message.
return area;
}
}
}
private static Component option2Button(Object option, NotifyDescriptor nd, ActionListener l, JRootPane rp) {
if (option instanceof AbstractButton) {
AbstractButton b = (AbstractButton) option;
removeOldListeners(b);
b.addActionListener(l);
return b;
} else if (option instanceof Component) {
return (Component) option;
} else if (option instanceof Icon) {
return new JLabel((Icon) option);
} else {
String text;
boolean defcap;
if (option == NotifyDescriptor.OK_OPTION) {
text = NbBundle.getMessage(DialogDisplayer.class, "CTL_OK");
defcap = true;
} else if (option == NotifyDescriptor.CANCEL_OPTION) {
text = NbBundle.getMessage(DialogDisplayer.class, "CTL_CANCEL");
defcap = false;
} else if (option == NotifyDescriptor.YES_OPTION) {
text = NbBundle.getMessage(DialogDisplayer.class, "CTL_YES");
defcap = true;
} else if (option == NotifyDescriptor.NO_OPTION) {
text = NbBundle.getMessage(DialogDisplayer.class, "CTL_NO");
defcap = false;
} else if (option == NotifyDescriptor.CLOSED_OPTION) {
throw new IllegalArgumentException();
} else {
text = option.toString();
defcap = false;
}
JButton b = new JButton(text);
if (defcap && (rp.getDefaultButton() == null)) {
rp.setDefaultButton(b);
}
// added a simple accessible name to buttons
b.getAccessibleContext().setAccessibleName(text);
b.addActionListener(l);
return b;
}
}
private static void removeOldListeners( AbstractButton button ) {
ArrayList<ActionListener> toRem = new ArrayList<ActionListener>();
for( ActionListener al : button.getActionListeners() ) {
if( al instanceof StandardDialog.ButtonListener ) {
toRem.add( al );
}
}
for( ActionListener al : toRem ) {
button.removeActionListener( al );
}
}
private static final class StandardDialog extends JDialog {
final NotifyDescriptor nd;
private Component messageComponent;
private final JPanel buttonPanel;
private final Object[] closingOptions;
private final ActionListener buttonListener;
private boolean haveFinalValue = false;
private Color nbErrorForeground;
private Color nbWarningForeground;
private Color nbInfoForeground;
private JLabel notificationLine;
private static final int MSG_TYPE_ERROR = 1;
private static final int MSG_TYPE_WARNING = 2;
private static final int MSG_TYPE_INFO = 3;
public StandardDialog(
String title, boolean modal, NotifyDescriptor nd, Object[] closingOptions, ActionListener buttonListener
) {
super((Frame) null, title, modal);
this.nd = nd;
this.closingOptions = closingOptions;
this.buttonListener = buttonListener;
getContentPane().setLayout(new BorderLayout());
setDefaultCloseOperation(nd.isNoDefaultClose()
? WindowConstants.DO_NOTHING_ON_CLOSE
: WindowConstants.DISPOSE_ON_CLOSE);
updateMessage();
buttonPanel = new JPanel();
buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
updateOptions();
getContentPane().add(buttonPanel, BorderLayout.SOUTH, 1);
KeyStroke k = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
Object actionKey = "cancel"; // NOI18N
getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(k, actionKey);
Action cancelAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent ev) {
if( !StandardDialog.this.nd.isNoDefaultClose() )
cancel();
}
};
getRootPane().getActionMap().put(actionKey, cancelAction);
addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent ev) {
if (!haveFinalValue) {
StandardDialog.this.nd.setValue(NotifyDescriptor.CLOSED_OPTION);
}
}
}
);
pack();
Rectangle r = Utilities.getUsableScreenBounds();
int maxW = (r.width * 9) / 10;
int maxH = (r.height * 9) / 10;
Dimension d = getPreferredSize();
d.width = Math.min(d.width, maxW);
d.height = Math.min(d.height, maxH);
setBounds(Utilities.findCenterBounds(d));
}
private void cancel() {
nd.setValue(NotifyDescriptor.CANCEL_OPTION);
haveFinalValue = true;
dispose();
}
public void updateMessage() {
if (messageComponent != null) {
getContentPane().remove(messageComponent);
}
//System.err.println("updateMessage: " + nd.getMessage());
messageComponent = message2Component(nd.getMessage());
if (! (nd instanceof WizardDescriptor) && nd.getNotificationLineSupport () != null) {
JComponent toAdd = new JPanel (new BorderLayout ());
toAdd.add (messageComponent, BorderLayout.CENTER);
nbErrorForeground = UIManager.getColor("nb.errorForeground"); //NOI18N
if (nbErrorForeground == null) {
//nbErrorForeground = new Color(89, 79, 191); // RGB suggested by Bruce in #28466
nbErrorForeground = new Color(255, 0, 0); // RGB suggested by jdinga in #65358
}
nbWarningForeground = UIManager.getColor("nb.warningForeground"); //NOI18N
if (nbWarningForeground == null) {
nbWarningForeground = new Color(51, 51, 51); // Label.foreground
}
nbInfoForeground = UIManager.getColor("nb.warningForeground"); //NOI18N
if (nbInfoForeground == null) {
nbInfoForeground = UIManager.getColor("Label.foreground"); //NOI18N
}
notificationLine = new FixedHeightLabel ();
NotificationLineSupport nls = nd.getNotificationLineSupport ();
if (nls.getInformationMessage () != null) {
updateNotificationLine (this, MSG_TYPE_INFO, nls.getInformationMessage ());
} else if (nls.getWarningMessage () != null) {
updateNotificationLine (this, MSG_TYPE_WARNING, nls.getWarningMessage ());
} else if (nls.getErrorMessage () != null) {
updateNotificationLine (this, MSG_TYPE_ERROR, nls.getErrorMessage ());
}
toAdd.add (notificationLine, BorderLayout.SOUTH);
messageComponent = toAdd;
}
getContentPane().add(messageComponent, BorderLayout.CENTER);
}
public void updateOptions() {
Set<Object> addedOptions = new HashSet<Object>(5);
Object[] options = nd.getOptions();
if (options == null) {
switch (nd.getOptionType()) {
case NotifyDescriptor.DEFAULT_OPTION:
case NotifyDescriptor.OK_CANCEL_OPTION:
if (!Utilities.isMac()) {
// Windows UI Guidelines
options = new Object[] { NotifyDescriptor.OK_OPTION, NotifyDescriptor.CANCEL_OPTION, };
} else {
// see http://netbeans.org/bugzilla/show_bug.cgi?id=202784 - according to
// Apple HIG guidelines 'Cancel' should be on the left
// http://developer.apple.com/library/mac/#documentation/UserExperience/Conceptual/AppleHIGuidelines/Windows/Windows.html#//apple_ref/doc/uid/20000961-TP9
options = new Object[] { NotifyDescriptor.CANCEL_OPTION, NotifyDescriptor.OK_OPTION, };
}
break;
case NotifyDescriptor.YES_NO_OPTION:
if (!Utilities.isMac()) {
// Windows UI Guidelines
options = new Object[] { NotifyDescriptor.YES_OPTION, NotifyDescriptor.NO_OPTION, };
} else {
// see http://netbeans.org/bugzilla/show_bug.cgi?id=202784 - according to
// Apple HIG guidelines 'Cancel' should be on the left
// http://developer.apple.com/library/mac/#documentation/UserExperience/Conceptual/AppleHIGuidelines/Windows/Windows.html#//apple_ref/doc/uid/20000961-TP9
options = new Object[] { NotifyDescriptor.NO_OPTION, NotifyDescriptor.YES_OPTION, };
}
break;
case NotifyDescriptor.YES_NO_CANCEL_OPTION:
if (!Utilities.isMac()) {
// Windows UI Guidelines
options = new Object[] {
NotifyDescriptor.YES_OPTION, NotifyDescriptor.NO_OPTION, NotifyDescriptor.CANCEL_OPTION,
};
} else {
// see http://netbeans.org/bugzilla/show_bug.cgi?id=202784 - according to
// Apple HIG guidelines 'Cancel' should be on the left
// http://developer.apple.com/library/mac/#documentation/UserExperience/Conceptual/AppleHIGuidelines/Windows/Windows.html#//apple_ref/doc/uid/20000961-TP9
options = new Object[] {
NotifyDescriptor.NO_OPTION, NotifyDescriptor.CANCEL_OPTION, NotifyDescriptor.YES_OPTION,
};
}
break;
default:
throw new IllegalArgumentException();
}
}
//System.err.println("prep: " + Arrays.asList(options) + " " + Arrays.asList(closingOptions) + " " + buttonListener);
buttonPanel.removeAll();
JRootPane rp = getRootPane();
for (int i = 0; i < options.length; i++) {
addedOptions.add(options[i]);
buttonPanel.add(option2Button(options[i], nd, makeListener(options[i]), rp));
}
options = nd.getAdditionalOptions();
if (options != null) {
for (int i = 0; i < options.length; i++) {
addedOptions.add(options[i]);
buttonPanel.add(option2Button(options[i], nd, makeListener(options[i]), rp));
}
}
if (closingOptions != null) {
for (int i = 0; i < closingOptions.length; i++) {
if (addedOptions.add(closingOptions[i])) {
ActionListener l = makeListener(closingOptions[i]);
attachActionListener(closingOptions[i], l);
}
}
}
}
private void attachActionListener(Object comp, ActionListener l) {
// on JButtons attach simply by method call
if (comp instanceof JButton) {
JButton b = (JButton) comp;
b.addActionListener(l);
return;
} else {
// we will have to use dynamic method invocation to add the action listener
// to generic component (and we succeed only if it has the addActionListener method)
java.lang.reflect.Method m;
try {
m = comp.getClass().getMethod("addActionListener", new Class[] { ActionListener.class }); // NOI18N
try {
m.setAccessible(true);
} catch (SecurityException se) {
m = null; // no jo, we cannot make accessible
}
} catch (NoSuchMethodException e) {
m = null; // no jo, we cannot attach ActionListener to this Component
} catch (SecurityException e2) {
m = null; // no jo, we cannot attach ActionListener to this Component
}
if (m != null) {
try {
m.invoke(comp, new Object[] { l });
} catch (Exception e) {
// not succeeded, so give up
}
}
}
}
private ActionListener makeListener(final Object option) {
return new ButtonListener( option );
}
private class ButtonListener implements ActionListener {
private final Object option;
public ButtonListener( Object option ) {
this.option = option;
}
@Override
public void actionPerformed( ActionEvent e ) {
nd.setValue(option);
if (buttonListener != null) {
// #34485: some listeners expect that the action source is the option, not the button
ActionEvent e2 = new ActionEvent(
option, e.getID(), e.getActionCommand(), e.getWhen(), e.getModifiers()
);
buttonListener.actionPerformed(e2);
}
if ((closingOptions == null) || Arrays.asList(closingOptions).contains(option)) {
haveFinalValue = true;
setVisible(false);
}
}
}
}
private static class DialogUpdater implements PropertyChangeListener {
private StandardDialog dialog;
private DialogDescriptor dd;
public DialogUpdater(StandardDialog dialog, DialogDescriptor dd) {
super();
this.dialog = dialog;
this.dd = dd;
}
public void propertyChange(final PropertyChangeEvent ev) {
if( !SwingUtilities.isEventDispatchThread() ) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
propertyChange(ev);
}
});
return;
}
String pname = ev.getPropertyName();
if (NotifyDescriptor.PROP_TITLE.equals(pname)) {
dialog.setTitle(dd.getTitle());
} else if (NotifyDescriptor.PROP_NO_DEFAULT_CLOSE.equals(pname)) {
dialog.setDefaultCloseOperation(dd.isNoDefaultClose() ? JDialog.DO_NOTHING_ON_CLOSE : JDialog.DISPOSE_ON_CLOSE);
} else
if (NotifyDescriptor.PROP_MESSAGE.equals(pname)) {
dialog.updateMessage();
dialog.validate();
dialog.repaint();
} else
if (NotifyDescriptor.PROP_OPTIONS.equals(pname) || NotifyDescriptor.PROP_OPTION_TYPE.equals(pname)) {
dialog.updateOptions();
dialog.validate();
dialog.repaint();
} else if (NotifyDescriptor.PROP_INFO_NOTIFICATION.equals (ev.getPropertyName ())) {
updateNotificationLine (dialog, StandardDialog.MSG_TYPE_INFO, ev.getNewValue ());
} else if (NotifyDescriptor.PROP_WARNING_NOTIFICATION.equals (ev.getPropertyName ())) {
updateNotificationLine (dialog, StandardDialog.MSG_TYPE_WARNING, ev.getNewValue ());
} else if (NotifyDescriptor.PROP_ERROR_NOTIFICATION.equals (ev.getPropertyName ())) {
updateNotificationLine (dialog, StandardDialog.MSG_TYPE_ERROR, ev.getNewValue ());
}
}
}
private static void updateNotificationLine (StandardDialog dialog, int msgType, Object o) {
String msg = o == null ? null : o.toString ();
if (msg != null && msg.trim().length() > 0) {
switch (msgType) {
case StandardDialog.MSG_TYPE_ERROR:
prepareMessage(dialog.notificationLine, ImageUtilities.loadImageIcon("org/netbeans/modules/dialogs/error.gif", false), //NOI18N
dialog.nbErrorForeground);
break;
case StandardDialog.MSG_TYPE_WARNING:
prepareMessage(dialog.notificationLine, ImageUtilities.loadImageIcon("org/netbeans/modules/dialogs/warning.gif", false), //NOI18N
dialog.nbWarningForeground);
break;
case StandardDialog.MSG_TYPE_INFO:
prepareMessage(dialog.notificationLine, ImageUtilities.loadImageIcon("org/netbeans/modules/dialogs/info.png", false), //NOI18N
dialog.nbInfoForeground);
break;
default:
}
dialog.notificationLine.setToolTipText (msg);
} else {
prepareMessage(dialog.notificationLine, null, null);
dialog.notificationLine.setToolTipText (null);
}
dialog.notificationLine.setText(msg);
}
private static void prepareMessage(JLabel label, ImageIcon icon, Color fgColor) {
label.setIcon(icon);
label.setForeground(fgColor);
}
private static final class FixedHeightLabel extends JLabel {
private static final int ESTIMATED_HEIGHT = 16;
public FixedHeightLabel () {
super ();
}
@Override
public Dimension getPreferredSize() {
Dimension preferredSize = super.getPreferredSize();
assert ESTIMATED_HEIGHT == ImageUtilities.loadImage ("org/netbeans/modules/dialogs/warning.gif").getHeight (null) : "Use only 16px icon."; //NOI18N
preferredSize.height = Math.max (ESTIMATED_HEIGHT, preferredSize.height);
return preferredSize;
}
}
}
}