package chatty.gui;
import chatty.Helper;
import chatty.util.MiscUtil;
import java.awt.Component;
import java.awt.Container;
import java.awt.Frame;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.JRootPane;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.RowSorter;
import javax.swing.SortOrder;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;
import javax.swing.text.Element;
/**
* Some Utility functions or constants for GUI related stuff
*
* @author tduva
*/
public class GuiUtil {
private static final Logger LOGGER = Logger.getLogger(GuiUtil.class.getName());
public final static Insets NORMAL_BUTTON_INSETS = new Insets(2, 14, 2, 14);
public final static Insets SMALL_BUTTON_INSETS = new Insets(-1, 10, -1, 10);
public final static Insets SPECIAL_BUTTON_INSETS = new Insets(2, 12, 2, 6);
public final static Insets SPECIAL_SMALL_BUTTON_INSETS = new Insets(-1, 12, -1, 6);
private static final String CLOSE_DIALOG_ACTION_MAP_KEY = "CLOSE_DIALOG_ACTION_MAP_KEY";
private static final KeyStroke ESCAPE_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
public static void installEscapeCloseOperation(final JDialog dialog) {
Action closingAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
dialog.dispatchEvent(
new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING
));
}
};
JRootPane root = dialog.getRootPane();
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
ESCAPE_STROKE,
CLOSE_DIALOG_ACTION_MAP_KEY);
root.getActionMap().put(CLOSE_DIALOG_ACTION_MAP_KEY, closingAction);
}
/**
* Shows a JOptionPane that doesn't steal focus when opened, but is
* focusable afterwards.
*
* @param parent The parent Component
* @param title The title
* @param message The message
* @param messageType The type of message as in JOptionPane
* @param optionType The option type as in JOptionPane
* @param options The options as in JOptionPane
* @return The selected option or -1 if none was selected
*/
public static int showNonAutoFocusOptionPane(Component parent, String title, String message,
int messageType, int optionType, Object[] options) {
JOptionPane p = new JOptionPane(message, messageType, optionType);
p.setOptions(options);
final JDialog d = p.createDialog(parent, title);
d.setAutoRequestFocus(false);
d.setFocusableWindowState(false);
// Make focusable after showing the dialog, so that it can be focused
// by the user, but doesn't steal focus from the user when it opens.
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
d.setFocusableWindowState(true);
}
});
d.setVisible(true);
// Find index of result
Object value = p.getValue();
for (int i = 0; i < options.length; i++) {
if (options[i] == value) {
return i;
}
}
return -1;
}
public static void showNonModalMessage(Component parent, String title, String message, int type) {
showNonModalMessage(parent, title, message, type, false);
}
public static void showNonModalMessage(Component parent, String title, String message, int type, boolean allowHtml) {
if (!allowHtml) {
message = Helper.htmlspecialchars_encode(message);
}
message = "<html><body style='font-family: Monospaced;width:400px;'>"+message;
JOptionPane pane = new JOptionPane(message, type);
JDialog dialog = pane.createDialog(parent, title);
dialog.setModal(false);
dialog.setVisible(true);
}
/**
* Checks if the given {@code Point} is on a screen. The point can be moved
* horizontally before checking by specifying a {@code xOffset}. The
* original point is not modified.
*
* @param p The {@code Point} to check
* @param xOffset The horizontal offset in pixels
* @return {@code true} if the point is on screen, {@code false} otherwise
*/
public static boolean isPointOnScreen(Point p, int xOffset) {
Point moved = new Point(p.x + xOffset, p.y);
return isPointOnScreen(moved);
}
/**
* Checks if the given {@code Point} is on a screen.
*
* @param p The {@code Point} to check
* @return {@code true} if the point is on screen, {@code false} otherwise
*/
public static boolean isPointOnScreen(Point p) {
GraphicsDevice[] screens = GraphicsEnvironment
.getLocalGraphicsEnvironment().getScreenDevices();
for (GraphicsDevice screen : screens) {
if (screen.getDefaultConfiguration().getBounds().contains(p)) {
return true;
}
}
return false;
}
public static GridBagConstraints makeGbc(int x, int y, int w, int h) {
return makeGbc(x, y, w, h, GridBagConstraints.EAST);
}
public static GridBagConstraints makeGbc(int x, int y, int w, int h, int anchor) {
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = x;
gbc.gridy = y;
gbc.gridwidth = w;
gbc.gridheight = h;
gbc.anchor = anchor;
gbc.insets = new Insets(5, 5, 5, 5);
return gbc;
}
/**
* Output the text of the subelements of the given element.
*
* @param line
*/
public static void debugLineContents(Element line) {
Document doc = line.getDocument();
System.out.print("[");
for (int i = 0; i < line.getElementCount(); i++) {
Element l = line.getElement(i);
//System.out.println(l);
try {
System.out.print("'" + doc.getText(l.getStartOffset(), l.getEndOffset() - l.getStartOffset()) + "'");
} catch (BadLocationException ex) {
System.out.println("Bad location");
}
}
System.out.println("]");
}
/**
* Detect retina display.
*
* http://stackoverflow.com/questions/20767708/how-do-you-detect-a-retina-display-in-java
*
* @return
*/
public static boolean hasRetinaDisplay() {
Object obj = Toolkit.getDefaultToolkit().getDesktopProperty(
"apple.awt.contentScaleFactor");
if (obj instanceof Float) {
int scale = ((Float)obj).intValue();
return (scale == 2); // 1 indicates a regular mac display.
}
return false;
}
/**
* Recursively set the font size of the given component and all
* subcomponents.
*
* @param fontSize
* @param component
*/
public static void setFontSize(float fontSize, Component component) {
if (fontSize <= 0) {
return;
}
if (component instanceof Container) {
synchronized(component.getTreeLock()) {
for (Component c : ((Container) component).getComponents()) {
GuiUtil.setFontSize(fontSize, c);
}
}
}
component.setFont(component.getFont().deriveFont(fontSize));
}
public static void setLookAndFeel(String lafCode) {
try {
String laf = null;
switch (lafCode) {
case "system":
laf = UIManager.getSystemLookAndFeelClassName();
break;
case "jgwindows":
laf = "com.jgoodies.looks.windows.WindowsLookAndFeel";
break;
case "jgplastic":
laf = "com.jgoodies.looks.plastic.PlasticLookAndFeel";
break;
case "jgplastic3d":
laf = "com.jgoodies.looks.plastic.Plastic3DLookAndFeel";
break;
case "jgplasticxp":
laf = "com.jgoodies.looks.plastic.PlasticXPLookAndFeel";
break;
default:
laf = UIManager.getCrossPlatformLookAndFeelClassName();
}
LOGGER.info("Setting LAF to " + laf);
UIManager.setLookAndFeel(laf);
addMacKeyboardActions();
} catch (Exception ex) {
LOGGER.warning("Failed setting LAF: "+ex);
}
}
public static void updateLookAndFeel() {
for (Frame frame : Frame.getFrames()) {
updateLookAndFeel(frame);
}
}
private static void updateLookAndFeel(Window window) {
for (Window childWindow : window.getOwnedWindows()) {
updateLookAndFeel(childWindow);
}
SwingUtilities.updateComponentTreeUI(window);
}
/**
* Returns the current sort keys of the given table encoded in a String.
*
* <p>This is intended to be used together with
* {@link setSortingForTable(JTable, String)}.</p>
*
* @param table
* @return
*/
public static String getSortingFromTable(JTable table) {
List<? extends RowSorter.SortKey> keys = table.getRowSorter().getSortKeys();
String result = "";
for (RowSorter.SortKey key : keys) {
int order = 0;
if (key.getSortOrder() == SortOrder.ASCENDING) {
order = 1;
} else if (key.getSortOrder() == SortOrder.DESCENDING) {
order = 2;
}
result += String.format("%s:%s;", key.getColumn(), order);
}
return result;
}
/**
* Sets the sort keys for the RowSorter of the given JTable. Doesn't change
* the sorting if the sorting parameter doesn't contain any valid sort key.
*
* <p>This is intended to be used together with
* {@link getSortingFromTable(JTable)}.</p>
*
* @param table
* @param sorting
*/
public static void setSortingForTable(JTable table, String sorting) {
List<RowSorter.SortKey> keys = new ArrayList<>();
StringTokenizer t = new StringTokenizer(sorting, ";");
while (t.hasMoreTokens()) {
String[] split = t.nextToken().split(":");
if (split.length == 2) {
try {
int rowId = Integer.parseInt(split[0]);
int orderId = Integer.parseInt(split[1]);
SortOrder order;
switch (orderId) {
case 1: order = SortOrder.ASCENDING; break;
case 2: order = SortOrder.DESCENDING; break;
default: order = SortOrder.UNSORTED;
}
keys.add(new RowSorter.SortKey(rowId, order));
} catch (NumberFormatException ex) {
// Just don't add anything
}
}
}
try {
if (!keys.isEmpty()) {
table.getRowSorter().setSortKeys(keys);
}
} catch (IllegalArgumentException ex) {
// Don't change sorting
}
}
/**
* Adds the Copy/Paste/Cut shortcuts for Mac (Command instead of Ctrl).
*
* <p>Normally the Look&Feel should do that automatically, but for some
* reason it doesn't seem to do it.</p>
*/
public static void addMacKeyboardActions() {
if (MiscUtil.OS_MAC) {
addMacKeyboardActionsTo("TextField.focusInputMap");
addMacKeyboardActionsTo("TextArea.focusInputMap");
addMacKeyboardActionsTo("TextPane.focusInputMap");
}
}
/**
* Based on: http://stackoverflow.com/a/7253059/2375667
*/
private static void addMacKeyboardActionsTo(String key) {
InputMap im = (InputMap) UIManager.get(key);
// Copy/paste actions
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.META_DOWN_MASK), DefaultEditorKit.copyAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.META_DOWN_MASK), DefaultEditorKit.pasteAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.META_DOWN_MASK), DefaultEditorKit.cutAction);
// Navigation actions
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.META_DOWN_MASK), DefaultEditorKit.beginLineAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.META_DOWN_MASK), DefaultEditorKit.endLineAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.ALT_DOWN_MASK), DefaultEditorKit.previousWordAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.ALT_DOWN_MASK), DefaultEditorKit.nextWordAction);
// Navigation selection actions
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.META_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK), DefaultEditorKit.selectionBeginLineAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.META_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK), DefaultEditorKit.selectionEndLineAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.ALT_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK), DefaultEditorKit.selectionPreviousWordAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.ALT_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK), DefaultEditorKit.selectionNextWordAction);
// Other actions
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.META_DOWN_MASK), DefaultEditorKit.selectAllAction);
}
}