package org.openslx.dozmod.gui;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.log4j.Logger;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Monitor;
import org.eclipse.swt.widgets.Shell;
import org.openslx.dozmod.gui.helper.MessageType;
public class Gui {
private static final Logger LOGGER = Logger.getLogger(Gui.class);
/**
* The one and only display to use throughout the application
*/
public static final Display display = new Display();
/**
* All active shells - we don't use display.getShells() as it is slow...
*/
private static final List<Shell> shells = new ArrayList<>();
private static volatile int exitCode = 0;
/**
* Center the given shell on the {@link Monitor} it is displayed on.
*
* @param shell Some shell
*/
public static void centerShell(Shell shell) {
Monitor activeMonitor = getMonitorFromRectangle(shell.getBounds(), true);
Rectangle bounds = activeMonitor.getClientArea();
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);
}
/**
* Make sure the given shell fits the {@link Monitor} it is displayed on.
*
* @param shell Some shell
*/
public static void limitShellSize(Shell shell) {
Monitor activeMonitor = getMonitorFromRectangle(shell.getBounds(), true);
Rectangle bounds = activeMonitor.getClientArea();
Rectangle rect = shell.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) {
shell.setSize(rect.width, rect.height);
rect = shell.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) {
shell.setLocation(rect.x, rect.y);
}
}
/**
* Get the {@link Monitor} 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 Monitor}
*/
public static Monitor getMonitorFromPoint(Point point, boolean defaultToPrimary) {
Monitor[] monitors = display.getMonitors();
for (Monitor monitor : monitors) {
Rectangle bounds = monitor.getBounds();
if (bounds.contains(point))
return monitor;
}
if (defaultToPrimary)
return display.getPrimaryMonitor();
return null;
}
/**
* Get the {@link Monitor} 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 Monitor}
*/
public static Monitor 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.
Rectangle bounds = display.getBounds();
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;
}
// Now just use the same code as *FromPoint by using the rectangle's center
return getMonitorFromPoint(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<>();
display.syncExec(new Runnable() {
@Override
public void run() {
try {
instance.set(task.run());
} catch (Throwable e) {
LOGGER.warn("syncExec failed!", e);
}
}
});
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) {
display.asyncExec(task);
}
/**
* Pretty much the same as Callable, but no exceptions are allowed.
*
* @param <T> return value
*/
public static interface GuiCallable<T> {
T run();
}
/**
* Generic helper to show a message box to the user, and optionally log the
* message to the log file.
*
* @param parent parent shell this message box belongs to
* @param message Message to display. Can be multiline.
* @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, YES or RETRY was clicked, false for CANCEL or NO
*/
public static boolean showMessageBox(Shell parent, String message, MessageType messageType,
Logger logger, Throwable exception) {
if (logger != null)
logger.log(messageType.logPriority, message, exception);
if (exception != null)
message += "\n\n" + exception.getClass().getSimpleName() + " (Siehe Logdatei)";
MessageBox box = new MessageBox(parent, messageType.style);
box.setMessage(message);
box.setText(messageType.title);
int ret = box.open();
return ret == SWT.OK || ret == SWT.RETRY || ret == SWT.YES;
}
/**
* Run the GUI mainloop as long as a window exists. This method does not
* return.
*/
public static void mainloop() {
do {
Iterator<Shell> it = shells.iterator();
while (it.hasNext()) {
if (it.next().isDisposed())
it.remove();
}
if (!display.readAndDispatch())
display.sleep();
} while (!shells.isEmpty());
display.dispose();
System.exit(exitCode);
}
/**
* Exit application - dispose all shells, so mainloop will terminate.
*
* @param code
*/
public static void exit(int code) {
exitCode = code;
asyncExec(new Runnable() {
@Override
public void run() {
for (Shell shell : shells) {
shell.dispose();
}
}
});
}
/**
* Wrapper to get a new shell. This adds the shell to the list of shells,
* which we need to determine whether the app should still be running.
*
* @param style the style of the {@link Shell}
* @return a new {@link Shell} that has no other {@link Shell} as its
* parent.
*/
public static Shell newShell(int style) {
Shell shell = new Shell(display, style);
shells.add(shell);
return shell;
}
/**
* Wrapper to get a new shell that is a child of another shell. This adds
* the shell to the list of shells, which we need to determine whether the
* app should still be running.
*
* @param parent the parent {@link Shell}
* @param style the style of the {@link Shell}
* @return a new {@link Shell} that has no other {@link Shell} as its
* parent.
*/
public static Shell newShell(Shell parent, int style) {
Shell shell = new Shell(parent, style);
shells.add(shell);
return shell;
}
}