org.nuiton.jaxx.runtime.swing.SwingUtil Maven / Gradle / Ivy
/*
* #%L
* JAXX :: Runtime
* %%
* Copyright (C) 2008 - 2024 Code Lutin, Ultreia.io
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* .
* #L%
*/
package org.nuiton.jaxx.runtime.swing;
import com.google.common.collect.ImmutableSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.LayerUI;
import org.jdesktop.swingx.JXTreeTable;
import org.nuiton.jaxx.runtime.JAXXObject;
import org.nuiton.jaxx.runtime.JAXXUtil;
import org.nuiton.jaxx.runtime.swing.editor.BooleanCellEditor;
import org.nuiton.jaxx.runtime.swing.model.JaxxDefaultComboBoxModel;
import org.nuiton.jaxx.runtime.swing.model.JaxxDefaultListModel;
import org.nuiton.jaxx.runtime.swing.renderer.BooleanCellRenderer;
import org.nuiton.jaxx.runtime.swing.renderer.EmptyNumberTableCellRenderer;
import org.nuiton.jaxx.runtime.swing.renderer.EnumTableCellRenderer;
import org.nuiton.jaxx.runtime.swing.renderer.I18nTableCellRenderer;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListModel;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JRootPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.HyperlinkEvent;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Properties;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* The runtime swing util class with some nice stuff.
*
* Note : Replace previous class jaxx.runtime.swing.Utils in previous versions.
*
* @author Tony Chemit - [email protected]
* @since 1.2
*/
public class SwingUtil extends JAXXUtil {
public static final String DEFAULT_ICON_PATH = "/icons/";
public static final String DEFAULT_ICON_PATH_PROPERTY = "default.icon.path";
public static final String ICON_PREFIX = "icon.";
public static final String COLOR_PREFIX = "color.";
/**
* Pattern to use for short numeric values in editors with max 3 digits.
*
* @since 4.0
*/
public static final String INT_3_DIGITS_PATTERN = "\\d{0,3}";
/**
* Pattern to use for short numeric values in editors with max 4 digits.
*
* @since 3.0
*/
public static final String INT_4_DIGITS_PATTERN = "\\d{0,4}";
/**
* Pattern to use for integer numeric values in editors with max 6 digits.
*
* @since 4.0
*/
public static final String INT_6_DIGITS_PATTERN = "\\d{0,6}";
/**
* Pattern to use for integer numeric values in editors with max 7 digits.
*
* @since 4.0
*/
public static final String INT_7_DIGITS_PATTERN = "\\d{0,7}";
/**
* Pattern to use for long numeric values in editors with max 10 digits.
*
* @since 4.0
*/
public static final String LONG_10_DIGITS_PATTERN = "\\d{0,10}";
/**
* Pattern to use for decimal numeric values with 1 decimal digits in editors.
*
* @since 4.0
*/
public static final String DECIMAL1_PATTERN = "\\d{0,6}|\\d{1,6}\\.\\d{0,1}";
/**
* Pattern to use for decimal numeric values with 2 decimal digits in editors.
*
* @since 4.0
*/
public static final String DECIMAL2_PATTERN = "\\d{0,6}|\\d{1,6}\\.\\d{0,2}";
/**
* Pattern to use for decimal numeric values with 3 decimal digits in editors.
*
* @since 4.0
*/
public static final String DECIMAL3_PATTERN = "\\d{0,6}|\\d{1,6}\\.\\d{0,3}";
/**
* Pattern to use for decimal numeric values with 4 decimal digits in editors.
*
* @since 4.0
*/
public static final String DECIMAL4_PATTERN = "\\d{0,6}|\\d{1,6}\\.\\d{0,4}";
/**
* Pattern to use for long numeric values in editors with max 3 digits.
*
* @since 4.0
*/
public static final String LONG_3_DIGITS_PATTERN = "\\d{0,3}";
static private final Logger log = LogManager.getLogger(SwingUtil.class);
private static final ImmutableSet OPEN_PROTOCOLS = ImmutableSet.of("mailto", "http", "https", "ftp", "file");
/**
* A simple iterator on a {@link JTabbedPane}.
*
* Implements the method {@link #get(int, Component)} to obtain
* the data required given the component (or index).
*
* You can also inverse the order by usin the method {@link #reverse()}.
*
* Note: After the use of the method {@link #reverse()} the iterator returns
* to the first element.
*
* @param the type of return elements.
* @since 1.4
*/
public static abstract class TabbedPaneIterator implements Iterator {
final JTabbedPane tabs;
boolean reverse;
int index;
int increment;
public TabbedPaneIterator(boolean reverse, JTabbedPane tabs) {
this.tabs = tabs;
setReverse(reverse);
}
protected abstract O get(int index, Component comp);
public void reset() {
setReverse(reverse);
}
public int size() {
return tabs.getTabCount();
}
public TabbedPaneIterator reverse() {
setReverse(!reverse);
return this;
}
@Override
public boolean hasNext() {
return reverse ? index > 0 : index < tabs.getTabCount();
}
public int getIndex() {
return index;
}
@Override
public O next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Component next = tabs.getComponentAt(index);
O result = get(index, next);
index += increment;
return result;
}
@Override
public void remove() {
throw new IllegalStateException("not implemented for " + this);
}
@Override
public String toString() {
return super.toString() + "< reverse:" + reverse + ", index:" +
index + ", size:" + tabs.getTabCount() + " >";
}
protected void setReverse(boolean reverse) {
if (reverse) {
index = tabs.getTabCount() - 1;
increment = -1;
} else {
index = 0;
increment = 1;
}
this.reverse = reverse;
}
}
public static Dimension newMinDimension() {
return new Dimension(0, 0);
}
public static Dimension newMaxXDimension() {
return new Dimension(Short.MAX_VALUE, 0);
}
public static Dimension newMaxYDimension() {
return new Dimension(0, Short.MAX_VALUE);
}
public static Dimension newMaxXYDimension() {
return new Dimension(Short.MAX_VALUE, Short.MAX_VALUE);
}
/**
* Fill a combo box model with some datas, and select after all the given object
*
* @param combo the combo to fill
* @param data data ot inject in combo
* @param select the object to select in combo after reflling his model
*/
public static void fillComboBox(JComboBox combo, Collection> data, Object select) {
ComboBoxModel comboBoxModel = combo.getModel();
if (comboBoxModel instanceof JaxxDefaultComboBoxModel) {
JaxxDefaultComboBoxModel model =
(JaxxDefaultComboBoxModel) comboBoxModel;
// evince the model
model.removeListDataListener(combo);
// set data
model.setAllElements(data);
// attach the model
model.addListDataListener(combo);
model.setSelectedItem(select);
} else if (comboBoxModel instanceof DefaultComboBoxModel) {
DefaultComboBoxModel model = (DefaultComboBoxModel) combo.getModel();
// evince the model
model.removeListDataListener(combo);
model.removeAllElements();
if (data != null) {
for (Object o : data) {
model.addElement(o);
}
}
// attach the model
model.addListDataListener(combo);
model.setSelectedItem(select);
} else {
throw new IllegalArgumentException(
"this method need a DefaultComboBoxModel for " +
"this model but was " + combo.getModel().getClass());
}
}
/**
* Fill a list model with some datas, and select after all the given object
*
* @param list the list to fill
* @param data data ot inject in combo
* @param select the object to select in combo after reflling his model
*/
public static void fillList(JList list, Collection> data, Object select) {
ListModel listModel = list.getModel();
if (listModel instanceof JaxxDefaultListModel) {
JaxxDefaultListModel model = (JaxxDefaultListModel) listModel;
// evince the model
// model.removeListDataListener(combo);
model.setAllElements(data);
// attach the model
// model.addListDataListener(combo);
list.setSelectedValue(select, true);
} else if (listModel instanceof DefaultListModel) {
DefaultListModel model = (DefaultListModel) listModel;
// evince the model
// model.removeListDataListener(combo);
model.removeAllElements();
if (data != null) {
for (Object o : data) {
model.addElement(o);
}
}
// attach the model
// model.addListDataListener(combo);
list.setSelectedValue(select, true);
} else {
throw new IllegalArgumentException(
"this method need a DefaultListModel for this model " +
"but was " + listModel.getClass());
}
}
/**
* TODO move this to JAXXComboBox.
*
* Fill a combo box model with some datas, and select after all the given object
*
* @param combo the combo to fill
* @param data data ot inject in combo
* @param select the object to select in combo after reflling his model
* @param firstNull add a first null element
* @deprecated since 1.7.XXX this code is moved to JAXXComboBox
*/
@Deprecated
public static void fillComboBox(JAXXComboBox combo,
Collection> data,
Object select, boolean firstNull) {
List- items = new ArrayList<>();
if (firstNull) {
items.add(new Item("null", " ", null, false));
}
if (data != null) {
for (Object d : data) {
Item item = new Item(d.toString(), d.toString(), d,
d.equals(select));
items.add(item);
}
}
combo.setItems(items);
}
/**
* Return parent's container corresponding to the Class clazz
*
* @param
type of container to obtain from context
* @param top the top container
* @param clazz desired
* @return parent's container
*/
@SuppressWarnings({"unchecked"})
public static O getParentContainer(Object top,
Class clazz) {
return getParent(top, clazz);
}
/**
* Find a parent of the given {@code top} object using the container api to get up.
*
* Stop on parent when it is of the given{@code clazz} type.
*
* @param type of container to obtain from context
* @param top the top container
* @param clazz desired
* @return parent's container
* @since 2.5.14
*/
public static O getParent(Object top, Class clazz) {
if (top == null) {
throw new IllegalArgumentException("top parameter can not be null");
}
if (!Container.class.isAssignableFrom(top.getClass())) {
throw new IllegalArgumentException("top parameter " + top +
" is not a " + Container.class);
}
Container parent = ((Container) top).getParent();
if (parent != null && !clazz.isAssignableFrom(parent.getClass())) {
parent = (Container) getParent(parent, clazz);
}
return (O) parent;
}
public static int computeTableColumnWidth(JTable table, Font font, int columnIndex, String suffix) {
int width = 0;
if (font == null) {
font = table.getFont();
}
// if (font == null) {
// TableColumn column = table.getColumnModel().getColumn(columnIndex);
// font = ((JComponent) column.getCellRenderer()).getFont();
// }
FontMetrics fontMetrics = table.getFontMetrics(font);
for (int i = 0, rowCount = table.getRowCount(); i < rowCount; i++) {
String key = (String) table.getModel().getValueAt(i, 0);
int w = fontMetrics.stringWidth(key + suffix);
if (w > width) {
width = w;
}
}
return width;
}
public static void fixTableColumnWidth(JTable table,
int columnIndex,
int width) {
TableColumn column = table.getColumnModel().getColumn(columnIndex);
column.setMaxWidth(width);
column.setMinWidth(width);
column.setWidth(width);
column.setPreferredWidth(width);
}
public static void setTableColumnEditor(JTable table,
int columnIndex,
TableCellEditor editor) {
TableColumn column = table.getColumnModel().getColumn(columnIndex);
column.setCellEditor(editor);
}
public static void setTableColumnRenderer(JTable table,
int columnIndex,
TableCellRenderer editor) {
TableColumn column = table.getColumnModel().getColumn(columnIndex);
column.setCellRenderer(editor);
}
public static void setI18nTableHeaderRenderer(JTable table,
String... libelles) {
I18nTableCellRenderer defaultRenderer =
new I18nTableCellRenderer(
table.getTableHeader().getDefaultRenderer(), libelles);
table.getTableHeader().setDefaultRenderer(defaultRenderer);
}
public static TableCellRenderer newStringTableCellRenderer(
final DefaultTableCellRenderer renderer,
final int length,
final boolean tooltip) {
return new DefaultTableCellRenderer() {
private static final long serialVersionUID = 1L;
@Override
public Component getTableCellRendererComponent(
JTable table,
Object value,
boolean isSelected,
boolean hasFocus,
int row,
int column) {
renderer.getTableCellRendererComponent(
table,
value,
isSelected,
hasFocus,
row,
column
);
String val = renderer.getText();
String val2 = val;
if (val.length() > length) {
val2 = val.substring(0, length - 3) + "...";
}
JComponent comp = (JComponent)
super.getTableCellRendererComponent(
table,
val2,
isSelected,
hasFocus,
row,
column
);
if (tooltip) {
comp.setToolTipText(val);
}
return comp;
}
};
}
/**
* Box a component in a {@link JXLayer}.
*
* @param component the component to box
* @return the {@link JXLayer} boxing the component
*/
public static JXLayer boxComponentWithJxLayer(V component) {
JXLayer layer = getLayer(component);
if (layer != null) {
return layer;
}
layer = new JXLayer<>();
layer.setView(component);
return layer;
}
public static List getLayeredComponents(JAXXObject object) {
List result = new ArrayList<>();
for (Entry child : object.get$objectMap().entrySet()) {
if (child.getValue() == null) {
log.warn("find a null object in $objectMap " + child.getKey());
continue;
}
if (JComponent.class.isAssignableFrom(child.getValue().getClass())) {
JComponent comp = (JComponent) child.getValue();
if (isLayered(comp)) {
result.add(comp);
}
}
}
return result;
}
public static JXLayer getLayer(V comp) {
if (!isLayered(comp)) {
return null;
}
return (JXLayer) comp.getParent();
}
public static void setLayerUI(JComponent comp, LayerUI ui) {
JXLayer layer = getLayer(comp);
layer.setUI(ui);
}
public static boolean isLayered(JComponent comp) {
Container parent = comp.getParent();
return parent != null && parent instanceof JXLayer>;
}
/**
* recherche les composants portant le meme nom que les champs de la classe
* clazz. Cette methode est statique pour pouvoir eventuellement l'utiliser
* dans un autre context (je pense par exemple a la generation jaxx).
*
* Si la recherche echoue pour quelque raison que se soit, aucune exception
* n'est leve, et la map retournee est tout simplement vide ou incomplete
*
* @param clazz la classe ou recherche les champs
* @param container le container ou rechercher les composants d'edition
* @return le dictionnaire des composants recherches.
*/
public static Map lookingForEditor(
Class> clazz,
Container container) {
Map result = new HashMap<>();
try {
// looking for all component with name set
Map allNamedComponent =
new HashMap<>();
List todo = new LinkedList<>();
todo.add(container);
while (todo.size() > 0) {
for (ListIterator i = todo.listIterator();
i.hasNext(); ) {
Container parent = i.next();
i.remove();
for (Component c : parent.getComponents()) {
if (c instanceof Container) {
i.add((Container) c);
String name = c.getName();
if (c instanceof JComponent &&
name != null && !"".equals(name)) {
allNamedComponent.put(name, (JComponent) c);
}
}
}
}
}
// looking for all properties on class
BeanInfo info = Introspector.getBeanInfo(clazz);
PropertyDescriptor[] props = info.getPropertyDescriptors();
// find if one properties have same name that component
for (PropertyDescriptor prop : props) {
String name = prop.getName();
if (allNamedComponent.containsKey(name)) {
result.put(name, allNamedComponent.get(name));
}
}
} catch (IntrospectionException eee) {
log.warn("Can't introspect bean", eee);
}
if (log.isDebugEnabled()) {
log.debug("Result: " + result);
}
return result;
}
/**
* Centrer un component graphique au center d'un autre component.
*
* Note: si le parent est null, alors on ne fait rien.
*
* @param parent le component parent
* @param component le component à centrer
*/
public static void center(Component parent, Component component) {
if (parent == null) {
return;
}
Rectangle r = parent.getBounds();
int x = r.x + (r.width - component.getSize().width) / 2;
int y = r.y + (r.height - component.getSize().height) / 2;
component.setLocation(x, y);
}
/**
* Try to load the Nimbus look and feel.
*
* @throws UnsupportedLookAndFeelException if nimbus is not applicable
* @throws ClassNotFoundException
* @throws InstantiationException
* @throws IllegalAccessException
*/
public static void initNimbusLoookAndFeel() throws UnsupportedLookAndFeelException, ClassNotFoundException, InstantiationException, IllegalAccessException {
for (UIManager.LookAndFeelInfo laf : UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(laf.getName())) {
UIManager.setLookAndFeel(laf.getClassName());
}
}
}
/**
* Load the ui.properties file and set in {@link UIManager} colors and
* icons found.
*
* @param defaultUIConfig le path vers le fichier de la config d'ui par
* défaut (doit etre dansle class-path)
* @param extraUIConfig le path vers une surcharge de la config d'ui
* (doit etre dans le class-path)
* @throws IOException if could not load the ui.properties file
*/
public static void loadUIConfig(String defaultUIConfig,
String extraUIConfig) throws IOException {
Properties p = new Properties();
log.info("loading default UI config " + defaultUIConfig);
p.load(SwingUtil.class.getResourceAsStream(defaultUIConfig));
if (log.isDebugEnabled()) {
log.debug(p.toString());
}
if (extraUIConfig != null) {
InputStream extraStream =
SwingUtil.class.getResourceAsStream(extraUIConfig);
if (extraStream == null) {
log.warn("could not find extraUIConfig : " + extraUIConfig);
} else {
log.info("loading extra UI config " + extraUIConfig);
Properties p2 = new Properties(p);
p2.load(extraStream);
if (log.isDebugEnabled()) {
log.debug(p2.toString());
}
p.putAll(p2);
}
}
for (Entry