/*
* Copyright 2015 Igor Maznitsa.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.igormaznitsa.mindmap.swing.panel.utils;
import com.igormaznitsa.mindmap.model.Topic;
import com.igormaznitsa.mindmap.model.logger.Logger;
import com.igormaznitsa.mindmap.model.logger.LoggerFactory;
import com.igormaznitsa.mindmap.swing.panel.MindMapPanelConfig;
import com.igormaznitsa.mindmap.swing.panel.ui.AbstractCollapsableElement;
import com.igormaznitsa.mindmap.swing.panel.ui.AbstractElement;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.io.IOUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.igormaznitsa.meta.annotation.ImplementationNote;
import com.igormaznitsa.meta.annotation.MayContainNull;
import com.igormaznitsa.meta.annotation.MustNotContainNull;
import com.igormaznitsa.mindmap.plugins.MindMapPluginRegistry;
import com.igormaznitsa.mindmap.plugins.api.PopUpMenuItemPlugin;
import com.igormaznitsa.mindmap.plugins.PopUpSection;
import com.igormaznitsa.mindmap.swing.panel.DialogProvider;
import com.igormaznitsa.mindmap.swing.panel.MindMapPanel;
import com.igormaznitsa.mindmap.swing.services.IconID;
import com.igormaznitsa.mindmap.swing.services.ImageIconService;
import com.igormaznitsa.mindmap.swing.services.ImageIconServiceProvider;
import com.igormaznitsa.mindmap.swing.services.UIComponentFactory;
import com.igormaznitsa.mindmap.swing.services.UIComponentFactoryProvider;
import com.igormaznitsa.mindmap.plugins.api.CustomJob;
import com.igormaznitsa.mindmap.swing.panel.ui.gfx.MMGraphics2DWrapper;
import com.igormaznitsa.mindmap.swing.panel.ui.gfx.MMGraphics;
public final class Utils {
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
private static final ResourceBundle BUNDLE = java.util.ResourceBundle.getBundle("com/igormaznitsa/mindmap/swing/panel/Bundle");
public static final UIComponentFactory UI_COMPO_FACTORY = UIComponentFactoryProvider.findInstance();
public static final ImageIconService ICON_SERVICE = ImageIconServiceProvider.findInstance();
public static final String PROPERTY_MAX_EMBEDDED_IMAGE_SIDE_SIZE = "mmap.max.image.side.size"; //NOI18N
private static final int MAX_IMAGE_SIDE_SIZE_IN_PIXELS = 350;
private static final Map<RenderingHints.Key, Object> RENDERING_HINTS = new HashMap<RenderingHints.Key, Object>();
static {
RENDERING_HINTS.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
RENDERING_HINTS.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
RENDERING_HINTS.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
RENDERING_HINTS.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
RENDERING_HINTS.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
RENDERING_HINTS.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
}
private Utils() {
}
/**
* Get input stream for resource in zip file.
* @param zipFile zip file
* @param resourcePath path to resource
* @return input stream for resource or null if not found or directory
* @throws IOException if there is any transport error
*/
@Nullable
public static InputStream findInputStreamForResource(@Nonnull final ZipFile zipFile, @Nonnull final String resourcePath) throws IOException {
final ZipEntry entry = zipFile.getEntry(resourcePath);
InputStream result = null;
if (entry != null && !entry.isDirectory()) {
result = zipFile.getInputStream(entry);
}
return result;
}
/**
* Read who;e zip item into byte array.
* @param zipFile zip file
* @param path path to resource
* @return byte array or null if not found
* @throws IOException thrown if there is any transport error
*/
@Nullable
public static byte[] toByteArray(@Nonnull final ZipFile zipFile, @Nonnull final String path) throws IOException {
final InputStream in = findInputStreamForResource(zipFile, path);
byte[] result = null;
if (in != null) {
try {
result = IOUtils.toByteArray(in);
}
finally {
IOUtils.closeQuietly(in);
}
}
return result;
}
/**
* Load and parse XML document from input stream.
*
* @param inStream stream to read document
* @param autoClose true if stream must be closed, false otherwise
* @return parsed document
* @throws IOException
* @throws ParserConfigurationException
* @throws SAXException
*
* @since 1.4.0
*/
@Nonnull
public static Document loadXmlDocument(@Nonnull final InputStream inStream, @Nullable final String charset, final boolean autoClose) throws SAXException, IOException, ParserConfigurationException {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setIgnoringComments(true);
factory.setValidating(false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
final DocumentBuilder builder = factory.newDocumentBuilder();
final Document document;
try {
final InputStream stream;
if (charset == null) {
stream = inStream;
} else {
stream = new ByteArrayInputStream(IOUtils.toString(inStream, charset).getBytes("UTF-8"));
}
document = builder.parse(stream);
}
finally {
if (autoClose) {
IOUtils.closeQuietly(inStream);
}
}
return document;
}
/**
* Get first direct child for name.
*
* @param node element to find children
* @param elementName name of child element
* @return found first child or null if not found
*
* @since 1.4.0
*/
@Nullable
public static Element findFirstElement(@Nonnull final Element node, @Nonnull final String elementName) {
Element result = null;
for (final Element l : Utils.findDirectChildrenForName(node, elementName)) {
result = l;
break;
}
return result;
}
/**
* Find all direct children with defined name.
*
* @param element parent element
* @param childElementname child element name
* @return list of found elements
*
* @since 1.4.0
*/
@Nonnull
@MustNotContainNull
public static List<Element> findDirectChildrenForName(@Nonnull final Element element, @Nonnull final String childElementname) {
final NodeList found = element.getElementsByTagName(childElementname);
final List<Element> resultList = new ArrayList<Element>();
for (int i = 0; i < found.getLength(); i++) {
if (found.item(i).getParentNode().equals(element) && found.item(i) instanceof Element) {
resultList.add((Element) found.item(i));
}
}
return resultList;
}
/**
* Get max image size.
*
* @return max image size
*
* @see #MAX_IMAGE_SIDE_SIZE_IN_PIXELS
* @see #PROPERTY_MAX_EMBEDDED_IMAGE_SIDE_SIZE
*/
public static int getMaxImageSize() {
int result = MAX_IMAGE_SIDE_SIZE_IN_PIXELS;
try {
final String defined = System.getProperty(PROPERTY_MAX_EMBEDDED_IMAGE_SIDE_SIZE);
if (defined != null) {
LOGGER.info("Detected redefined max size for embedded image side : " + defined); //NOI18N
result = Math.max(8, Integer.parseInt(defined.trim()));
}
}
catch (NumberFormatException ex) {
LOGGER.error("Error during image size decoding : ", ex); //NOI18N
}
return result;
}
/**
* Load and encode image into Base64.
*
* @param in stream to read image
* @param maxSize max size of image, if less or zero then don't rescale
* @return null if it was impossible to load image for its format, loaded
* prepared image
* @throws IOException if any error during conversion or loading
*
* @since 1.4.0
*/
@Nullable
public static String rescaleImageAndEncodeAsBase64(@Nonnull final InputStream in, final int maxSize) throws IOException {
final Image image = ImageIO.read(in);
String result = null;
if (image != null) {
result = rescaleImageAndEncodeAsBase64(image, maxSize);
}
return result;
}
/**
* Load and encode image into Base64 from file.
*
* @param file image file
* @param maxSize max size of image, if less or zero then don't rescale
* @return image
* @throws IOException if any error during conversion or loading
*
* @since 1.4.0
*/
@Nonnull
public static String rescaleImageAndEncodeAsBase64(@Nonnull final File file, final int maxSize) throws IOException {
final Image image = ImageIO.read(file);
if (image == null) {
throw new IllegalArgumentException("Can't load image file : " + file); //NOI18N
}
return rescaleImageAndEncodeAsBase64(image, maxSize);
}
/**
* Rescale image and encode into Base64.
*
* @param image image to rescale and encode
* @param maxSize max size of image, if less or zero then don't rescale
* @return scaled and encoded image
* @throws IOException if it was impossible to encode image
*
* @since 1.4.0
*/
@Nonnull
public static String rescaleImageAndEncodeAsBase64(@Nonnull Image image, final int maxSize) throws IOException {
final int width = image.getWidth(null);
final int height = image.getHeight(null);
final int maxImageSideSize = maxSize > 0 ? maxSize : Math.max(width, height);
final float imageScale = width > maxImageSideSize || height > maxImageSideSize ? (float) maxImageSideSize / (float) Math.max(width, height) : 1.0f;
if (!(image instanceof RenderedImage) || Float.compare(imageScale, 1.0f) != 0) {
final int swidth;
final int sheight;
if (Float.compare(imageScale, 1.0f) == 0) {
swidth = width;
sheight = height;
} else {
swidth = Math.round(imageScale * width);
sheight = Math.round(imageScale * height);
}
final BufferedImage buffer = new BufferedImage(swidth, sheight, BufferedImage.TYPE_INT_ARGB);
final Graphics2D gfx = (Graphics2D) buffer.createGraphics();
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
gfx.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
gfx.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
gfx.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
gfx.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
gfx.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
gfx.drawImage(image, AffineTransform.getScaleInstance(imageScale, imageScale), null);
gfx.dispose();
image = buffer;
}
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
if (!ImageIO.write((RenderedImage) image, "png", bos)) {
throw new IOException("Can't encode image as PNG");
}
}
finally {
IOUtils.closeQuietly(bos);
}
return Utils.base64encode(bos.toByteArray());
}
public static int calculateColorBrightness(@Nonnull final Color color) {
return (int) Math.sqrt(color.getRed() * color.getRed() * .241d + color.getGreen() * color.getGreen() * .691d + color.getBlue() * color.getBlue() * .068d);
}
public static boolean isDarkTheme() {
final Color panelBack = UIManager.getColor("Panel.background");
if (panelBack == null) {
return false;
} else {
return calculateColorBrightness(panelBack) < 150;
}
}
public static void prepareGraphicsForQuality(@Nonnull final Graphics2D gfx) {
gfx.setRenderingHints(RENDERING_HINTS);
}
@Nonnull
public static String convertCamelCasedToHumanForm(@Nonnull final String camelCasedString, final boolean capitalizeFirstChar) {
final StringBuilder result = new StringBuilder();
boolean notFirst = false;
for (final char c : camelCasedString.toCharArray()) {
if (notFirst) {
if (Character.isUpperCase(c)) {
result.append(' ');
result.append(Character.toLowerCase(c));
} else {
result.append(c);
}
} else {
notFirst = true;
if (capitalizeFirstChar) {
result.append(Character.toUpperCase(c));
} else {
result.append(c);
}
}
}
return result.toString();
}
@Nonnull
@MustNotContainNull
public static Topic[] getLeftToRightOrderedChildrens(@Nonnull final Topic topic) {
final List<Topic> result = new ArrayList<Topic>();
if (topic.getTopicLevel() == 0) {
for (final Topic t : topic.getChildren()) {
if (AbstractCollapsableElement.isLeftSidedTopic(t)) {
result.add(t);
}
}
for (final Topic t : topic.getChildren()) {
if (!AbstractCollapsableElement.isLeftSidedTopic(t)) {
result.add(t);
}
}
} else {
result.addAll(topic.getChildren());
}
return result.toArray(new Topic[result.size()]);
}
public static void setAttribute(@Nonnull final String name, @Nullable final String value, @Nonnull @MustNotContainNull final Topic[] topics) {
for (final Topic t : topics) {
t.setAttribute(name, value);
}
}
@Nullable
public static Color html2color(@Nullable final String str, final boolean hasAlpha) {
Color result = null;
if (str != null && !str.isEmpty() && str.charAt(0) == '#') {
try {
String color = str.substring(1);
if (color.length() > 6) {
color = color.substring(color.length() - 6);
}
if (color.length() == 6) {
result = new Color(Integer.parseInt(color, 16), hasAlpha);
} else if (color.length() == 3) {
final int r = Integer.parseInt(color.charAt(0) + "0", 16);
final int g = Integer.parseInt(color.charAt(1) + "0", 16);
final int b = Integer.parseInt(color.charAt(2) + "0", 16);
result = new Color(r, g, b);
}
}
catch (NumberFormatException ex) {
LOGGER.warn(String.format("Can't convert %s to color", str));
}
}
return result;
}
@Nullable
public static String color2html(@Nullable final Color color, final boolean hasAlpha) {
String result = null;
if (color != null) {
final StringBuilder buffer = new StringBuilder();
buffer.append('#');
final int[] components;
if (hasAlpha) {
components = new int[]{color.getAlpha(), color.getRed(), color.getGreen(), color.getBlue()};
} else {
components = new int[]{color.getRed(), color.getGreen(), color.getBlue()};
}
for (final int c : components) {
final String str = Integer.toHexString(c & 0xFF).toUpperCase(Locale.ENGLISH);
if (str.length() < 2) {
buffer.append('0');
}
buffer.append(str);
}
result = buffer.toString();
}
return result;
}
@Nonnull
public static String getFirstLine(@Nonnull final String text) {
return text.replace("\r", "").split("\\n")[0]; //NOI18N
}
@Nonnull
public static String makeShortTextVersion(@Nonnull String text, final int maxLength) {
if (text.length() > maxLength) {
text = text.substring(0, maxLength) + "..."; //NOI18N
}
return text;
}
public static void safeSwingCall(@Nonnull final Runnable runnable) {
if (SwingUtilities.isEventDispatchThread()) {
runnable.run();
} else {
SwingUtilities.invokeLater(runnable);
}
}
public static void safeSwingBlockingCall(@Nonnull final Runnable runnable) {
if (SwingUtilities.isEventDispatchThread()) {
runnable.run();
} else {
try {
SwingUtilities.invokeAndWait(runnable);
}
catch (Exception ex) {
throw new RuntimeException("Detected exception during SwingUtilities.invokeAndWait", ex);
}
}
}
@Nonnull
@MustNotContainNull
public static String[] breakToLines(@Nonnull final String text) {
final int lineNum = numberOfLines(text);
final String[] result = new String[lineNum];
final StringBuilder line = new StringBuilder();
int index = 0;
for (int i = 0; i < text.length(); i++) {
if (text.charAt(i) == '\n') {
result[index++] = line.toString();
line.setLength(0);
} else {
line.append(text.charAt(i));
}
}
result[index] = line.toString();
return result;
}
public static int numberOfLines(@Nonnull final String text) {
int result = 1;
for (int i = 0; i < text.length(); i++) {
if (text.charAt(i) == '\n') {
result++;
}
}
return result;
}
@ImplementationNote("Must be called from Swing UI thread")
public static void foldUnfoldTree(@Nonnull final JTree tree, final boolean unfold) {
final TreeModel model = tree.getModel();
if (model != null) {
final Object root = model.getRoot();
if (root != null) {
final TreePath thePath = new TreePath(root);
setTreeState(tree, thePath, true, unfold);
if (!unfold) {
setTreeState(tree, thePath, false, true);
}
}
}
}
private static void setTreeState(@Nonnull final JTree tree, @Nonnull final TreePath path, final boolean recursively, final boolean unfold) {
final Object lastNode = path.getLastPathComponent();
for (int i = 0; i < tree.getModel().getChildCount(lastNode); i++) {
final Object child = tree.getModel().getChild(lastNode, i);
final TreePath pathToChild = path.pathByAddingChild(child);
if (recursively) {
setTreeState(tree, pathToChild, recursively, unfold);
}
}
if (unfold) {
tree.expandPath(path);
} else {
tree.collapsePath(path);
}
}
public static @Nonnull
String removeAllISOControlsButTabs(@Nonnull final String str) {
final StringBuilder result = new StringBuilder(str.length());
for (final char c : str.toCharArray()) {
if (c != '\t' && Character.isISOControl(c)) {
continue;
}
result.append(c);
}
return result.toString();
}
@Nullable
public static Point2D findRectEdgeIntersection(@Nonnull final Rectangle2D rect, final double outboundX, final double outboundY) {
final int detectedSide = rect.outcode(outboundX, outboundY);
if ((detectedSide & (Rectangle2D.OUT_TOP | Rectangle2D.OUT_BOTTOM)) != 0) {
final boolean top = (detectedSide & Rectangle2D.OUT_BOTTOM) == 0;
final double dx = outboundX - rect.getCenterX();
if (dx == 0.0d) {
return new Point2D.Double(rect.getCenterX(), top ? rect.getMinY() : rect.getMaxY());
} else {
final double halfy = top ? rect.getHeight() / 2 : -rect.getHeight() / 2;
final double coeff = (outboundY - rect.getCenterY()) / dx;
final double calculatedX = rect.getCenterX() - (halfy / coeff);
if (calculatedX >= rect.getMinX() && calculatedX <= rect.getMaxX()) {
return new Point2D.Double(calculatedX, top ? rect.getMinY() : rect.getMaxY());
}
}
}
if ((detectedSide & (Rectangle2D.OUT_LEFT | Rectangle2D.OUT_RIGHT)) != 0) {
final boolean left = (detectedSide & Rectangle2D.OUT_RIGHT) == 0;
final double dy = outboundY - rect.getCenterY();
if (dy == 0.0d) {
return new Point2D.Double(left ? rect.getMinX() : rect.getMaxX(), rect.getCenterY());
} else {
final double halfx = left ? rect.getWidth() / 2 : -rect.getWidth() / 2;
final double coeff = (outboundX - rect.getCenterX()) / dy;
final double calculatedY = rect.getCenterY() - (halfx / coeff);
if (calculatedY >= rect.getMinY() && calculatedY <= rect.getMaxY()) {
return new Point2D.Double(left ? rect.getMinX() : rect.getMaxX(), calculatedY);
}
}
}
return null;
}
@Nonnull
public static Image scaleImage(@Nonnull final Image src, final double baseScaleX, final double baseScaleY, final double scale) {
final int imgw = src.getWidth(null);
final int imgh = src.getHeight(null);
final int scaledW = (int) Math.round(imgw * baseScaleX * scale);
final int scaledH = (int) Math.round(imgh * baseScaleY * scale);
final BufferedImage result = new BufferedImage(scaledW, scaledH, BufferedImage.TYPE_INT_ARGB);
final Graphics2D g = (Graphics2D) result.getGraphics();
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g.drawImage(src, 0, 0, scaledW, scaledH, null);
g.dispose();
return result;
}
@Nonnull
public static Image renderWithTransparency(final float opacity, @Nonnull final AbstractElement element, @Nonnull final MindMapPanelConfig config) {
final AbstractElement cloned = element.makeCopy();
final Rectangle2D bounds = cloned.getBounds();
final float increase = config.safeScaleFloatValue(config.getElementBorderWidth() + config.getShadowOffset(), 0.0f);
final int imageWidth = (int) Math.round(bounds.getWidth() + increase);
final int imageHeight = (int) Math.round(bounds.getHeight() + increase);
bounds.setRect(0.0d, 0.0d, bounds.getWidth(), bounds.getHeight());
final BufferedImage result = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < imageHeight; y++) {
for (int x = 0; x < imageWidth; x++) {
result.setRGB(x, y, 0);
}
}
final Graphics2D g = result.createGraphics();
final MMGraphics gfx = new MMGraphics2DWrapper(g);
try {
prepareGraphicsForQuality(g);
cloned.doPaint(gfx, config, false);
}
finally {
gfx.dispose();
}
int alpha;
if (opacity <= 0.0f) {
alpha = 0x00;
} else if (opacity >= 1.0f) {
alpha = 0xFF;
} else {
alpha = Math.round(0xFF * opacity);
}
alpha <<= 24;
for (int y = 0; y < imageHeight; y++) {
for (int x = 0; x < imageWidth; x++) {
final int curAlpha = result.getRGB(x, y) >>> 24;
if (curAlpha == 0xFF) {
result.setRGB(x, y, (result.getRGB(x, y) & 0xFFFFFF) | alpha);
} else if (curAlpha != 0x00) {
final int calculated = Math.round(curAlpha * opacity) << 24;
result.setRGB(x, y, (result.getRGB(x, y) & 0xFFFFFF) | calculated);
}
}
}
return result;
}
@Nonnull
public static Color makeContrastColor(@Nonnull final Color color) {
return calculateColorBrightness(color) < 128 ? Color.WHITE : Color.BLACK;
}
@Nonnull
@MustNotContainNull
private static List<JMenuItem> findPopupMenuItems(
@Nonnull final MindMapPanel panel,
@Nonnull final PopUpSection section,
@Nonnull @MayContainNull final List<JMenuItem> list,
@Nonnull DialogProvider dialogProvider,
@Nullable final Topic topicUnderMouse,
@Nonnull @MustNotContainNull final Topic[] selectedTopics,
@Nonnull @MustNotContainNull final List<PopUpMenuItemPlugin> pluginMenuItems,
@Nonnull Map<Class<? extends PopUpMenuItemPlugin>, CustomJob> customProcessors
) {
list.clear();
for (final PopUpMenuItemPlugin p : pluginMenuItems) {
if (p.getSection() == section) {
if (!(p.needsTopicUnderMouse() || p.needsSelectedTopics())
|| (p.needsTopicUnderMouse() && topicUnderMouse != null)
|| (p.needsSelectedTopics() && selectedTopics.length > 0)) {
final JMenuItem item = p.makeMenuItem(panel, dialogProvider, topicUnderMouse, selectedTopics, customProcessors.get(p.getClass()));
if (item != null) {
item.setEnabled(p.isEnabled(panel, topicUnderMouse, selectedTopics));
list.add(item);
}
}
}
}
return list;
}
public static void assertSwingDispatchThread() {
if (!SwingUtilities.isEventDispatchThread()) {
throw new Error("Must be called in Swing dispatch thread");
}
}
@Nonnull
@MustNotContainNull
private static List<JMenuItem> putAllItemsAsSection(@Nonnull final JPopupMenu menu, @Nullable final JMenu subMenu, @Nonnull @MustNotContainNull final List<JMenuItem> items) {
if (!items.isEmpty()) {
if (menu.getComponentCount() > 0) {
menu.add(UI_COMPO_FACTORY.makeMenuSeparator());
}
for (final JMenuItem i : items) {
if (subMenu == null) {
menu.add(i);
} else {
subMenu.add(i);
}
}
if (subMenu != null) {
menu.add(subMenu);
}
}
return items;
}
@Nonnull
public static JPopupMenu makePopUp(
@Nonnull final MindMapPanel source,
@Nonnull final DialogProvider dialogProvider,
@Nullable final Topic topicUnderMouse,
@Nonnull @MustNotContainNull final Topic[] selectedTopics,
@Nonnull Map<Class<? extends PopUpMenuItemPlugin>, CustomJob> customProcessors
) {
final JPopupMenu result = UI_COMPO_FACTORY.makePopupMenu();
final List<PopUpMenuItemPlugin> pluginMenuItems = MindMapPluginRegistry.getInstance().findFor(PopUpMenuItemPlugin.class);
final List<JMenuItem> tmpList = new ArrayList<JMenuItem>();
final boolean isModelNotEmpty = source.getModel().getRoot() != null;
putAllItemsAsSection(result, null, findPopupMenuItems(source, PopUpSection.MAIN, tmpList, dialogProvider, topicUnderMouse, selectedTopics, pluginMenuItems, customProcessors));
putAllItemsAsSection(result, null, findPopupMenuItems(source, PopUpSection.MANIPULATORS, tmpList, dialogProvider, topicUnderMouse, selectedTopics, pluginMenuItems, customProcessors));
putAllItemsAsSection(result, null, findPopupMenuItems(source, PopUpSection.EXTRAS, tmpList, dialogProvider, topicUnderMouse, selectedTopics, pluginMenuItems, customProcessors));
final JMenu exportMenu = UI_COMPO_FACTORY.makeMenu(BUNDLE.getString("MMDExporters.SubmenuName"));
exportMenu.setIcon(ICON_SERVICE.getIconForId(IconID.POPUP_EXPORT));
final JMenu importMenu = UI_COMPO_FACTORY.makeMenu(BUNDLE.getString("MMDImporters.SubmenuName"));
importMenu.setIcon(ICON_SERVICE.getIconForId(IconID.POPUP_IMPORT));
putAllItemsAsSection(result, importMenu, findPopupMenuItems(source, PopUpSection.IMPORT, tmpList, dialogProvider, topicUnderMouse, selectedTopics, pluginMenuItems, customProcessors));
if (isModelNotEmpty) {
putAllItemsAsSection(result, exportMenu, findPopupMenuItems(source, PopUpSection.EXPORT, tmpList, dialogProvider, topicUnderMouse, selectedTopics, pluginMenuItems, customProcessors));
}
putAllItemsAsSection(result, null, findPopupMenuItems(source, PopUpSection.TOOLS, tmpList, dialogProvider, topicUnderMouse, selectedTopics, pluginMenuItems, customProcessors));
putAllItemsAsSection(result, null, findPopupMenuItems(source, PopUpSection.MISC, tmpList, dialogProvider, topicUnderMouse, selectedTopics, pluginMenuItems, customProcessors));
return result;
}
public static boolean isKeyStrokeEvent(@Nullable final KeyStroke keyStroke, final int keyEventType, @Nullable final KeyEvent event) {
boolean result = false;
if (keyStroke != null && event != null) {
if (keyEventType == keyStroke.getKeyEventType()) {
result = ((keyStroke.getModifiers() & event.getModifiers()) == keyStroke.getModifiers()) && (keyStroke.getKeyCode() == event.getKeyCode());
}
}
return result;
}
private static final char[] BASE64_TABLE = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
@Nonnull
public static String base64encode(@Nonnull final byte[] data) {
final StringBuilder buffer = new StringBuilder(data.length << 1);
int pad = 0;
for (int i = 0; i < data.length; i += 3) {
int b = ((data[i] & 0xFF) << 16) & 0xFFFFFF;
if (i + 1 < data.length) {
b |= (data[i + 1] & 0xFF) << 8;
} else {
pad++;
}
if (i + 2 < data.length) {
b |= (data[i + 2] & 0xFF);
} else {
pad++;
}
for (int j = 0; j < 4 - pad; j++) {
int c = (b & 0xFC0000) >> 18;
buffer.append(BASE64_TABLE[c]);
b <<= 6;
}
}
for (int j = 0; j < pad; j++) {
buffer.append("=");
}
return buffer.toString();
}
}