blob: dc168a4ab1f6c1094a2b5cb786f3550064963d61 [file] [log] [blame]
/*
* Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.lwawt.macosx;
import sun.awt.AWTAccessor;
import sun.awt.SunToolkit;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.peer.TrayIconPeer;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.concurrent.atomic.AtomicReference;
import static sun.awt.AWTAccessor.*;
public class CTrayIcon extends CFRetainedResource implements TrayIconPeer {
private TrayIcon target;
private PopupMenu popup;
private JDialog messageDialog;
private DialogEventHandler handler;
// In order to construct MouseEvent object, we need to specify a
// Component target. Because TrayIcon isn't Component's subclass,
// we use this dummy frame instead
private final Frame dummyFrame;
// A bitmask that indicates what mouse buttons produce MOUSE_CLICKED events
// on MOUSE_RELEASE. Click events are only generated if there were no drag
// events between MOUSE_PRESSED and MOUSE_RELEASED for particular button
private static int mouseClickButtons = 0;
CTrayIcon(TrayIcon target) {
super(0, true);
this.messageDialog = null;
this.handler = null;
this.target = target;
this.popup = target.getPopupMenu();
this.dummyFrame = new Frame();
setPtr(createModel());
//if no one else is creating the peer.
checkAndCreatePopupPeer();
updateImage();
}
private CPopupMenu checkAndCreatePopupPeer() {
CPopupMenu menuPeer = null;
if (popup != null) {
try {
final MenuComponentAccessor acc = getMenuComponentAccessor();
menuPeer = acc.getPeer(popup);
if (menuPeer == null) {
popup.addNotify();
menuPeer = acc.getPeer(popup);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return menuPeer;
}
private long createModel() {
return nativeCreate();
}
private native long nativeCreate();
//invocation from the AWTTrayIcon.m
public long getPopupMenuModel() {
PopupMenu newPopup = target.getPopupMenu();
if (popup == newPopup) {
if (popup == null) {
return 0L;
}
} else {
if (newPopup != null) {
if (popup != null) {
popup.removeNotify();
popup = newPopup;
} else {
popup = newPopup;
}
} else {
return 0L;
}
}
// This method is executed on Appkit, so if ptr is not zero means that,
// it is still not deallocated(even if we call NSApp postRunnableEvent)
// and sent CFRelease to the native queue
return checkAndCreatePopupPeer().ptr;
}
/**
* We display tray icon message as a small dialog with OK button.
* This is lame, but JDK 1.6 does basically the same. There is a new
* kind of window in Lion, NSPopover, so perhaps it could be used it
* to implement better looking notifications.
*/
public void displayMessage(final String caption, final String text,
final String messageType) {
if (SwingUtilities.isEventDispatchThread()) {
displayMessageOnEDT(caption, text, messageType);
} else {
try {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
displayMessageOnEDT(caption, text, messageType);
}
});
} catch (Exception e) {
throw new AssertionError(e);
}
}
}
@Override
public void dispose() {
if (messageDialog != null) {
disposeMessageDialog();
}
dummyFrame.dispose();
if (popup != null) {
popup.removeNotify();
}
LWCToolkit.targetDisposedPeer(target, this);
target = null;
super.dispose();
}
@Override
public void setToolTip(String tooltip) {
execute(ptr -> nativeSetToolTip(ptr, tooltip));
}
//adds tooltip to the NSStatusBar's NSButton.
private native void nativeSetToolTip(long trayIconModel, String tooltip);
@Override
public void showPopupMenu(int x, int y) {
//Not used. The popupmenu is shown from the native code.
}
@Override
public void updateImage() {
Image image = target.getImage();
if (image == null) return;
MediaTracker tracker = new MediaTracker(new Button(""));
tracker.addImage(image, 0);
try {
tracker.waitForAll();
} catch (InterruptedException ignore) { }
if (image.getWidth(null) <= 0 ||
image.getHeight(null) <= 0)
{
return;
}
CImage cimage = CImage.getCreator().createFromImage(image);
boolean imageAutoSize = target.isImageAutoSize();
cimage.execute(imagePtr -> {
execute(ptr -> {
setNativeImage(ptr, imagePtr, imageAutoSize);
});
});
}
private native void setNativeImage(final long model, final long nsimage, final boolean autosize);
private void postEvent(final AWTEvent event) {
SunToolkit.executeOnEventHandlerThread(target, new Runnable() {
public void run() {
SunToolkit.postEvent(SunToolkit.targetToAppContext(target), event);
}
});
}
//invocation from the AWTTrayIcon.m
private void handleMouseEvent(NSEvent nsEvent) {
int buttonNumber = nsEvent.getButtonNumber();
final SunToolkit tk = (SunToolkit)Toolkit.getDefaultToolkit();
if ((buttonNumber > 2 && !tk.areExtraMouseButtonsEnabled())
|| buttonNumber > tk.getNumberOfButtons() - 1) {
return;
}
int jeventType = NSEvent.nsToJavaEventType(nsEvent.getType());
int jbuttonNumber = MouseEvent.NOBUTTON;
int jclickCount = 0;
if (jeventType != MouseEvent.MOUSE_MOVED) {
jbuttonNumber = NSEvent.nsToJavaButton(buttonNumber);
jclickCount = nsEvent.getClickCount();
}
int jmodifiers = NSEvent.nsToJavaModifiers(
nsEvent.getModifierFlags());
boolean isPopupTrigger = NSEvent.isPopupTrigger(jmodifiers);
int eventButtonMask = (jbuttonNumber > 0)?
MouseEvent.getMaskForButton(jbuttonNumber) : 0;
long when = System.currentTimeMillis();
if (jeventType == MouseEvent.MOUSE_PRESSED) {
mouseClickButtons |= eventButtonMask;
} else if (jeventType == MouseEvent.MOUSE_DRAGGED) {
mouseClickButtons = 0;
}
// The MouseEvent's coordinates are relative to screen
int absX = nsEvent.getAbsX();
int absY = nsEvent.getAbsY();
MouseEvent mouseEvent = new MouseEvent(dummyFrame, jeventType, when,
jmodifiers, absX, absY, absX, absY, jclickCount, isPopupTrigger,
jbuttonNumber);
mouseEvent.setSource(target);
postEvent(mouseEvent);
// fire ACTION event
if (jeventType == MouseEvent.MOUSE_PRESSED && isPopupTrigger) {
final String cmd = target.getActionCommand();
final ActionEvent event = new ActionEvent(target,
ActionEvent.ACTION_PERFORMED, cmd);
postEvent(event);
}
// synthesize CLICKED event
if (jeventType == MouseEvent.MOUSE_RELEASED) {
if ((mouseClickButtons & eventButtonMask) != 0) {
MouseEvent clickEvent = new MouseEvent(dummyFrame,
MouseEvent.MOUSE_CLICKED, when, jmodifiers, absX, absY,
absX, absY, jclickCount, isPopupTrigger, jbuttonNumber);
clickEvent.setSource(target);
postEvent(clickEvent);
}
mouseClickButtons &= ~eventButtonMask;
}
}
private native Point2D nativeGetIconLocation(long trayIconModel);
public void displayMessageOnEDT(String caption, String text,
String messageType) {
if (messageDialog != null) {
disposeMessageDialog();
}
// obtain icon to show along the message
Icon icon = getIconForMessageType(messageType);
if (icon != null) {
icon = new ImageIcon(scaleIcon(icon, 0.75));
}
// We want the message dialog text area to be about 1/8 of the screen
// size. There is nothing special about this value, it's just makes the
// message dialog to look nice
Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
int textWidth = screenSize.width / 8;
// create dialog to show
messageDialog = createMessageDialog(caption, text, textWidth, icon);
// finally, show the dialog to user
showMessageDialog();
}
/**
* Creates dialog window used to display the message
*/
private JDialog createMessageDialog(String caption, String text,
int textWidth, Icon icon) {
JDialog dialog;
handler = new DialogEventHandler();
JTextArea captionArea = null;
if (caption != null) {
captionArea = createTextArea(caption, textWidth, false, true);
}
JTextArea textArea = null;
if (text != null){
textArea = createTextArea(text, textWidth, true, false);
}
Object[] panels = null;
if (captionArea != null) {
if (textArea != null) {
panels = new Object[] {captionArea, new JLabel(), textArea};
} else {
panels = new Object[] {captionArea};
}
} else {
if (textArea != null) {
panels = new Object[] {textArea};
}
}
// We want message dialog with small title bar. There is a client
// property property that does it, however, it must be set before
// dialog's native window is created. This is why we create option
// pane and dialog separately
final JOptionPane op = new JOptionPane(panels);
op.setIcon(icon);
op.addPropertyChangeListener(handler);
// Make Ok button small. Most likely won't work for L&F other then Aqua
try {
JPanel buttonPanel = (JPanel)op.getComponent(1);
JButton ok = (JButton)buttonPanel.getComponent(0);
ok.putClientProperty("JComponent.sizeVariant", "small");
} catch (Throwable t) {
// do nothing, we tried and failed, no big deal
}
dialog = new JDialog((Dialog) null);
JRootPane rp = dialog.getRootPane();
// gives us dialog window with small title bar and not zoomable
rp.putClientProperty(CPlatformWindow.WINDOW_STYLE, "small");
rp.putClientProperty(CPlatformWindow.WINDOW_ZOOMABLE, "false");
dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
dialog.setModal(false);
dialog.setModalExclusionType(Dialog.ModalExclusionType.TOOLKIT_EXCLUDE);
dialog.setAlwaysOnTop(true);
dialog.setAutoRequestFocus(false);
dialog.setResizable(false);
dialog.setContentPane(op);
dialog.addWindowListener(handler);
// suppress security warning for untrusted windows
AWTAccessor.getWindowAccessor().setTrayIconWindow(dialog, true);
dialog.pack();
return dialog;
}
private void showMessageDialog() {
Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
AtomicReference<Point2D> ref = new AtomicReference<>();
execute(ptr -> {
ref.set(nativeGetIconLocation(ptr));
});
Point2D iconLoc = ref.get();
if (iconLoc == null) {
return;
}
int dialogY = (int)iconLoc.getY();
int dialogX = (int)iconLoc.getX();
if (dialogX + messageDialog.getWidth() > screenSize.width) {
dialogX = screenSize.width - messageDialog.getWidth();
}
messageDialog.setLocation(dialogX, dialogY);
messageDialog.setVisible(true);
}
private void disposeMessageDialog() {
if (SwingUtilities.isEventDispatchThread()) {
disposeMessageDialogOnEDT();
} else {
try {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
disposeMessageDialogOnEDT();
}
});
} catch (Exception e) {
throw new AssertionError(e);
}
}
}
private void disposeMessageDialogOnEDT() {
if (messageDialog != null) {
messageDialog.removeWindowListener(handler);
messageDialog.removePropertyChangeListener(handler);
messageDialog.dispose();
messageDialog = null;
handler = null;
}
}
/**
* Scales an icon using specified scale factor
*
* @param icon icon to scale
* @param scaleFactor scale factor to use
* @return scaled icon as BuffedredImage
*/
private static BufferedImage scaleIcon(Icon icon, double scaleFactor) {
if (icon == null) {
return null;
}
int w = icon.getIconWidth();
int h = icon.getIconHeight();
GraphicsEnvironment ge =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = ge.getDefaultScreenDevice();
GraphicsConfiguration gc = gd.getDefaultConfiguration();
// convert icon into image
BufferedImage iconImage = gc.createCompatibleImage(w, h,
Transparency.TRANSLUCENT);
Graphics2D g = iconImage.createGraphics();
icon.paintIcon(null, g, 0, 0);
g.dispose();
// and scale it nicely
int scaledW = (int) (w * scaleFactor);
int scaledH = (int) (h * scaleFactor);
BufferedImage scaledImage = gc.createCompatibleImage(scaledW, scaledH,
Transparency.TRANSLUCENT);
g = scaledImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(iconImage, 0, 0, scaledW, scaledH, null);
g.dispose();
return scaledImage;
}
/**
* Gets Aqua icon used in message dialog.
*/
private static Icon getIconForMessageType(String messageType) {
if (messageType.equals("ERROR")) {
return UIManager.getIcon("OptionPane.errorIcon");
} else if (messageType.equals("WARNING")) {
return UIManager.getIcon("OptionPane.warningIcon");
} else {
// this is just an application icon
return UIManager.getIcon("OptionPane.informationIcon");
}
}
private static JTextArea createTextArea(String text, int width,
boolean isSmall, boolean isBold) {
JTextArea textArea = new JTextArea(text);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textArea.setEditable(false);
textArea.setFocusable(false);
textArea.setBorder(null);
textArea.setBackground(new JLabel().getBackground());
if (isSmall) {
textArea.putClientProperty("JComponent.sizeVariant", "small");
}
if (isBold) {
Font font = textArea.getFont();
Font boldFont = new Font(font.getName(), Font.BOLD, font.getSize());
textArea.setFont(boldFont);
}
textArea.setSize(width, 1);
return textArea;
}
/**
* Implements all the Listeners needed by message dialog
*/
private final class DialogEventHandler extends WindowAdapter
implements PropertyChangeListener {
public void windowClosing(WindowEvent we) {
disposeMessageDialog();
}
public void propertyChange(PropertyChangeEvent e) {
if (messageDialog == null) {
return;
}
String prop = e.getPropertyName();
Container cp = messageDialog.getContentPane();
if (messageDialog.isVisible() && e.getSource() == cp &&
(prop.equals(JOptionPane.VALUE_PROPERTY))) {
disposeMessageDialog();
}
}
}
}