org.openide.DialogDisplayer Maven / Gradle / Ivy
/*
* 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 java.util.concurrent.CompletableFuture;
import javax.swing.*;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;
import org.openide.util.lookup.Lookups;
/** 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.
* 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 may be called
* from any thread. The thread will return immediately and
* the dialog will be shown later, usually when AWT thread
* is empty and can handle the request.
*
*
* 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.
*
*
* @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);
}
});
}
/**
* Notify the user by a message box. The method may be called from any thread; the method
* returns immediately and the UI will be shown later, as in {@link #notifyLater}. Unlike
* {@link #notifyLater}, this method returns a {@link CompletableFuture} that will be
* completed when the UI closes. The value of the returned {@link CompletableFuture} is
* the descriptor itself: it's {@link NotifyDescriptor#getValue()} will be set to the
* closing option. If a subclass, like {@link NotifyDescriptor.InputLine} is used, the
* task can query other values of NotifyDescriptor, such as the text entered.
* Any exception thrown by {@link NotifyDescriptor} processing will be reported through {@link CompletableFuture#completeExceptionally}.
*
* It is possible to call {@link CompletableFuture#cancel(boolean)} on the returned value.
* The implementation may (through it is not guaranteed) abort and hide the UI. If cancel() is
* called, the returned Future always completes exceptionally, with a {@link CancellationException}.
*
* The thread that will execute the continuation or exception handler is undefined. Use usual
* precautions against EDT blocking and use {@link CompletableFuture#thenAcceptAsync(java.util.function.Consumer, java.util.concurrent.Executor)}
* or similar to execute in a specific thread. Prefer usage of {@link RequestProcessor} to the
* builtin thread pool.
*
* The following snippet is an example of chained dialogs (can be any other processing):
*
* {@snippet file="org/openide/DialogDisplayerTest.java" region="notifyFuture"}
*
*
* @param actual subclass of {@link NotifyDescriptor} passed as a parameter.
* @param descriptor describes the UI / dialog.
* @return the descriptor instance.
* @since 7.61
*/
public CompletableFuture notifyFuture(final T descriptor) {
CompletableFuture r = new CompletableFuture<>();
// preserve potential context
Lookup def = Lookup.getDefault();
Mutex.EVENT.postReadRequest(new Runnable() {
public void run() {
Lookups.executeWith(def, () -> {
try {
DialogDisplayer.this.notify(descriptor);
r.complete(descriptor);
} catch (ThreadDeath td) {
throw td;
} catch (Throwable t) {
r.completeExceptionally(t);
}
});
}
});
return r;
}
/** 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.
* Do not cache 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 {
@Override
public Object notify(NotifyDescriptor nd) {
if (GraphicsEnvironment.isHeadless()) {
return NotifyDescriptor.CLOSED_OPTION;
}
JDialog dialog = new StandardDialog(nd.getTitle(), true, nd, null, null);
dialog.setVisible(true);
return (nd.getValue() != null) ? nd.getValue() : NotifyDescriptor.CLOSED_OPTION;
}
@Override
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 toRem = new ArrayList();
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() {
@Override
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