package org.openslx.dozmod.gui;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.AWTEventListener;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.util.concurrent.atomic.AtomicReference;
import javax.management.monitor.Monitor;
import javax.swing.Icon;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.html.HTMLDocument;
import org.apache.log4j.Logger;
import org.openslx.dozmod.Config;
import org.openslx.dozmod.gui.helper.MessageType;
import org.openslx.dozmod.util.DesktopEnvironment;
import org.openslx.dozmod.util.ResourceLoader;
import org.openslx.util.QuickTimer;
public class Gui {
private static final Logger LOGGER = Logger.getLogger(Gui.class);
private static long lastUserActivity = System.currentTimeMillis();
static {
Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
@Override
public void eventDispatched(AWTEvent event) {
lastUserActivity = System.currentTimeMillis();
}
}, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.KEY_EVENT_MASK);
}
private static Rectangle clientArea(GraphicsDevice gd) {
Insets inset = Toolkit.getDefaultToolkit().getScreenInsets(gd.getDefaultConfiguration());
Rectangle bounds = gd.getDefaultConfiguration().getBounds();
bounds.x += inset.left;
bounds.y += inset.top;
bounds.width -= (inset.top + inset.bottom);
bounds.height -= (inset.left + inset.right);
return bounds;
}
/**
* Center the given shell on the {@link Monitor} it is displayed on.
* WARNING: Seems broken on Linux (depending on DE or WM)
*
* @param shell Some shell
*/
public static void centerShell(Window shell) {
GraphicsDevice activeMonitor = getMonitorFromRectangle(shell.getBounds(), true);
Rectangle bounds = clientArea(activeMonitor);
Rectangle rect = shell.getBounds();
int x = bounds.x + (bounds.width - rect.width) / 2;
int y = bounds.y + (bounds.height - rect.height) / 2;
shell.setLocation(x, y);
}
/**
* Take a shell and center it relative to another shell
*
* @param parent Parent shell, used as reference
* @param shellToCenter The shell that should be repositioned
*/
public static void centerShellOverShell(Window parent, Window shellToCenter) {
Rectangle bounds = parent.getBounds();
Rectangle rect = shellToCenter.getBounds();
int x = bounds.x + (bounds.width - rect.width) / 2;
int y = bounds.y + (bounds.height - rect.height) / 2;
if (x < bounds.x)
x = bounds.x;
if (y < bounds.y)
y = bounds.y;
shellToCenter.setLocation(x, y);
}
/**
* Make sure the given shell fits the {@link GraphicsDevice} it is displayed
* on.
*
* @param shell Some shell
*/
public static void limitShellSize(JFrame window) {
GraphicsDevice activeMonitor = getMonitorFromRectangle(window.getBounds(), true);
Rectangle bounds = clientArea(activeMonitor);
Rectangle rect = window.getBounds();
boolean changed = false;
if (rect.width + 20 > bounds.width) {
rect.width = bounds.width - 20;
changed = true;
}
if (rect.height + 20 > bounds.height) {
rect.height = bounds.height - 20;
changed = true;
}
if (changed) {
window.setSize(rect.width, rect.height);
rect = window.getBounds();
}
changed = false;
if (rect.x + rect.width >= bounds.x + bounds.width) {
rect.x = 5;
changed = true;
}
if (rect.y + rect.height >= bounds.y + bounds.height) {
rect.y = 5;
changed = true;
}
if (changed) {
window.setLocation(rect.x, rect.y);
}
}
/**
* Gets the given dimension scaled to the saved scaling factor
*
* @param width starting width to scale
* @param height starting height to scale
* @return scaled dimension
*/
public static Dimension getScaledDimension(int width, int height) {
int scale = Config.getFontScaling();
return new Dimension(width * scale / 100, height * scale / 100);
}
/**
* Load given icon resource, optionally scaling it while taking
* the user's zoom factor into account.
*
* @param path resource path
* @param description resource description
* @param maxHeight maximum height of image, which it will be scaled to
* @param context context component, for alpha blending
* @return scaled icon, if too large, otherwise the unmodified icon
*/
public static Icon getScaledIconResource(String path, String description, int maxHeight, Component context) {
int height = maxHeight * Config.getFontScaling() / 100;
return ResourceLoader.getIcon(path, description, height, context);
}
/**
* Get the {@link GraphicsDevice} which the given {@link Point} lies in.
*
* @param point The point in question
* @param defaultToPrimary if no monitor matches the check, return the
* primary monitor if true, <code>null</code> otherwise
* @return the {@link GraphicsDevice}
*/
private static GraphicsDevice getMonitorFromPoint(GraphicsDevice[] screens, Point point,
boolean defaultToPrimary) {
LOGGER.debug("Finding monitor for point " + point.toString());
for (GraphicsDevice dev : screens) {
Rectangle bounds = dev.getDefaultConfiguration().getBounds();
LOGGER.debug("Checking monitor " + bounds.toString());
if (bounds.contains(point))
return dev;
}
if (defaultToPrimary) {
LOGGER.debug("Defaulting to primary...");
return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
}
return null;
}
/**
* Get the {@link GraphicsDevice} which most of the given rectangle
* overlaps.
*
* @param rect The rectangle to check
* @param defaultToPrimary if no monitor matches the check, return the
* primary monitor if true, <code>null</code> otherwise
* @return the {@link GraphicsDevice}
*/
public static GraphicsDevice getMonitorFromRectangle(Rectangle rect, boolean defaultToPrimary) {
// Make sure rectangle is in bounds. This is not completely accurate
// in case there are multiple monitors that have different resolutions.
GraphicsDevice[] screens = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
Rectangle bounds = new Rectangle();
for (GraphicsDevice dev : screens) {
bounds = bounds.union(dev.getDefaultConfiguration().getBounds());
}
LOGGER.debug("Display bounds are " + bounds.toString() + ", rect is " + rect.toString());
if (rect.x + rect.width >= bounds.x + bounds.width) {
rect.width -= (rect.x + rect.width) - (bounds.x + bounds.width);
if (rect.width < 1)
rect.width = 1;
}
if (rect.y + rect.height >= bounds.y + bounds.height) {
rect.height -= (rect.y + rect.height) - (bounds.y + bounds.height);
if (rect.height < 1)
rect.height = 1;
}
if (rect.x < bounds.x) {
rect.width -= bounds.x - rect.x;
rect.x = bounds.x;
}
if (rect.y < bounds.y) {
rect.height -= bounds.y - rect.y;
rect.y = bounds.y;
}
LOGGER.debug("After correction: " + rect.toString());
// Now just use the same code as *FromPoint by using the rectangle's center
return getMonitorFromPoint(screens, new Point(rect.x + rect.width / 2, rect.y + rect.height / 2),
defaultToPrimary);
}
/**
* Run given task in the GUI thread, blocking the calling thread until the
* task is done.
*
* @param task Task to run
* @return return value of the task
*/
public static <T> T syncExec(final GuiCallable<T> task) {
final AtomicReference<T> instance = new AtomicReference<>();
final AtomicReference<Throwable> thrown = new AtomicReference<>();
if (SwingUtilities.isEventDispatchThread()) {
return task.run();
}
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
try {
instance.set(task.run());
} catch (Throwable e) {
thrown.set(e);
}
}
});
} catch (InvocationTargetException | InterruptedException e) {
LOGGER.warn("syncExec() failed", e);
return null;
}
if (thrown.get() != null) {
throw new RuntimeException("task passed to syncExec() failed", thrown.get());
}
return instance.get();
}
/**
* Run given task as soon as possible in the GUI thread, but don't block
* calling thread.
*
* @param task Task to run
*/
public static void asyncExec(final Runnable task) {
SwingUtilities.invokeLater(task);
}
/**
* Pretty much the same as Callable, but no exceptions are allowed.
*
* @param <T> return value
*/
public static interface GuiCallable<T> {
T run();
}
/**
* Exit application - dispose all shells, so mainloop will terminate.
*
* @param code
*/
public static void exit(int code) {
QuickTimer.cancel();
Window[] ownerlessWindows = Frame.getOwnerlessWindows();
for (Window w : ownerlessWindows) {
w.dispose();
}
System.exit(code);
}
/**
* Generic helper to show a message box to the user, and optionally log the
* message to the log file.
*
* @param parent parent window (used for positioning/modality). If null,
* the active window will be used
* @param message Message to display. Can be multi line.
* @param messageType Type of message (warning, information)
* @param logger Logger instance to log to. Can be null.
* @param exception Exception related to this message. Can be null.
* @return true if OK or YES was clicked, false for CANCEL/NO/(X)
*/
public static boolean showMessageBox(Component parent, String message, MessageType messageType,
Logger logger, Throwable exception) {
if (logger != null)
logger.log(messageType.logPriority, message, exception);
// Only needs to be done, if parent isn't already a window
if (parent == null) {
parent = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
} else if (!(parent instanceof Window)) {
Window ancestor = SwingUtilities.getWindowAncestor(parent);
if (ancestor != null) {
parent = ancestor;
}
}
if (exception != null) {
message += "\n\n" + exception.getClass().getSimpleName() + "\n" + exception.getMessage() + "\n"
+ " (Für Stack-Trace siehe Logdatei)";
}
if (message.startsWith("<html>")) {
JEditorPane ep = new JEditorPane("text/html", message);
ep.setEditable(false);
ep.setOpaque(false);
Font font = UIManager.getFont("Label.font");
String bodyRule = "body { font-family: " + font.getFamily() + "; " +
"font-size: " + font.getSize() + "pt; }";
((HTMLDocument)ep.getDocument()).getStyleSheet().addRule(bodyRule);
ep.addHyperlinkListener(new HyperlinkListener() {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) {
try {
DesktopEnvironment.openWebpageUri(e.getURL().toURI());
} catch (URISyntaxException ex) {
LOGGER.error("Couldn't parse hyperlink", ex);
}
}
}
});
JOptionPane.showMessageDialog(parent, ep, messageType.title, messageType.optionPaneId);
return true;
}
if (messageType.buttons == -1) {
JOptionPane.showMessageDialog(parent, message, messageType.title, messageType.optionPaneId);
return true;
}
// TODO set the default button that has focus
int ret = JOptionPane.showConfirmDialog(parent, message, messageType.title, messageType.buttons,
messageType.optionPaneId);
return ret == JOptionPane.OK_OPTION || ret == JOptionPane.YES_OPTION;
}
/**
* Generic helper to show a message box to the user, and optionally log the
* message to the log file.
*
* @param message Message to display. Can be multi line.
* @param messageType Type of message (warning, information)
* @param logger Logger instance to log to. Can be null.
* @param exception Exception related to this message. Can be null.
* @return true if OK or YES was clicked, false for CANCEL/NO/(X)
*/
public static boolean showMessageBox(String message, MessageType messageType, Logger logger,
Throwable exception) {
return showMessageBox(null, message, messageType, logger, exception);
}
/**
* Show a message box to the user asynchronously, and optionally log the
* message to the log file. This is most useful when working from another
* thread.
*
* @param message Message to display. Can be multi line.
* @param messageType Type of message (warning, information)
* @param logger Logger instance to log to. Can be null.
* @param exception Exception related to this message. Can be null.
* @return true if OK or YES was clicked, false for CANCEL/NO/(X)
*/
public static void asyncMessageBox(final String message, final MessageType messageType,
final Logger logger, final Throwable exception) {
if (SwingUtilities.isEventDispatchThread()) {
Gui.showMessageBox(message, messageType, logger, exception);
return;
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
showMessageBox(null, message, messageType, logger, exception);
}
});
}
/**
* Get last user activity timestamp.
* This considers mouse clicks and key presses.
*/
public static long getLastUserActivityMillis() {
return lastUserActivity;
}
}