/* * 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; import com.igormaznitsa.mindmap.swing.panel.ui.MouseSelectedArea; import com.igormaznitsa.mindmap.swing.panel.ui.ElementPart; import com.igormaznitsa.mindmap.swing.panel.ui.ElementRoot; import com.igormaznitsa.mindmap.swing.panel.ui.ElementLevelFirst; import com.igormaznitsa.mindmap.swing.panel.ui.ElementLevelOther; import com.igormaznitsa.mindmap.swing.panel.ui.AbstractElement; import com.igormaznitsa.mindmap.swing.panel.ui.AbstractCollapsableElement; import com.igormaznitsa.mindmap.model.*; import com.igormaznitsa.mindmap.model.logger.Logger; import com.igormaznitsa.mindmap.model.logger.LoggerFactory; import com.igormaznitsa.mindmap.swing.panel.utils.MindMapUtils; import com.igormaznitsa.mindmap.swing.panel.utils.Utils; import com.igormaznitsa.mindmap.swing.services.UIComponentFactory; import com.igormaznitsa.mindmap.swing.services.UIComponentFactoryProvider; import java.awt.*; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.geom.Dimension2D; import java.awt.geom.GeneralPath; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; import java.util.concurrent.CopyOnWriteArrayList; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.swing.BorderFactory; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JTextArea; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import org.apache.commons.lang.StringEscapeUtils; import com.igormaznitsa.meta.annotation.MustNotContainNull; import com.igormaznitsa.meta.common.utils.Assertions; import com.igormaznitsa.mindmap.plugins.MindMapPluginRegistry; import com.igormaznitsa.mindmap.plugins.api.VisualAttributePlugin; import java.lang.ref.WeakReference; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicBoolean; import com.igormaznitsa.mindmap.plugins.api.ModelAwarePlugin; import com.igormaznitsa.mindmap.plugins.api.PanelAwarePlugin; import java.util.concurrent.locks.ReentrantLock; import com.igormaznitsa.mindmap.swing.panel.ui.gfx.MMGraphics2DWrapper; import com.igormaznitsa.mindmap.swing.panel.ui.gfx.StrokeType; import com.igormaznitsa.mindmap.swing.panel.ui.gfx.MMGraphics; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import static com.igormaznitsa.meta.common.utils.Assertions.assertNotNull; import static com.igormaznitsa.mindmap.swing.panel.utils.Utils.assertSwingDispatchThread; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.ClipboardOwner; import java.awt.datatransfer.Transferable; import javax.swing.JViewport; import com.igormaznitsa.mindmap.swing.panel.utils.KeyEventType; public class MindMapPanel extends JPanel implements ClipboardOwner { public static final long serialVersionUID = 2783412123454232L; /** * Some Job over mind map model. * * @since 1.3.1 * @see MindMapPanel#executeModelJobs(com.igormaznitsa.mindmap.swing.panel.MindMapPanel.ModelJob...) */ public interface ModelJob { /** * Execute the job. * * @param model model to be processed * @return true if to continue job sequence, false if to interrupt */ boolean doChangeModel(@Nonnull MindMap model); } public static final String ATTR_SHOW_JUMPS = "showJumps"; private static final Logger LOGGER = LoggerFactory.getLogger(MindMapPanel.class); private static final UIComponentFactory UI_COMPO_FACTORY = UIComponentFactoryProvider.findInstance(); private final MindMapPanelController controller; private static final int ALL_SUPPORTED_MODIFIERS = KeyEvent.SHIFT_MASK | KeyEvent.ALT_MASK | KeyEvent.META_MASK | KeyEvent.CTRL_MASK; private final Map<Object, WeakReference<?>> weakTable = new WeakHashMap<Object, WeakReference<?>>(); private final AtomicBoolean disposed = new AtomicBoolean(); private final ReentrantLock panelLocker = new ReentrantLock(); public static class DraggedElement { public enum Modifier { NONE, MAKE_JUMP; } @Nonnull private final AbstractElement element; private final Image prerenderedImage; private final Point mousePointerOffset; private final Point currentPosition; private final DraggedElement.Modifier modifier; public DraggedElement(@Nonnull final AbstractElement element, @Nonnull final MindMapPanelConfig cfg, @Nonnull final Point mousePointerOffset, @Nonnull final DraggedElement.Modifier modifier) { this.element = element; this.prerenderedImage = Utils.renderWithTransparency(0.55f, element, cfg); this.mousePointerOffset = mousePointerOffset; this.currentPosition = new Point(); this.modifier = modifier; } @Nonnull public DraggedElement.Modifier getModifier() { return this.modifier; } public boolean isPositionInside() { return this.element.getBounds().contains(this.currentPosition); } @Nonnull public AbstractElement getElement() { return this.element; } public void updatePosition(@Nonnull final Point point) { this.currentPosition.setLocation(point); } @Nonnull public Point getPosition() { return this.currentPosition; } @Nonnull public Point getMousePointerOffset() { return this.mousePointerOffset; } public int getDrawPositionX() { return this.currentPosition.x - this.mousePointerOffset.x; } public int getDrawPositionY() { return this.currentPosition.y - this.mousePointerOffset.y; } @Nonnull public Image getImage() { return this.prerenderedImage; } public void draw(@Nonnull final Graphics2D gfx) { final int x = getDrawPositionX(); final int y = getDrawPositionY(); gfx.drawImage(this.prerenderedImage, x, y, null); } } private static final ResourceBundle BUNDLE = java.util.ResourceBundle.getBundle("com/igormaznitsa/mindmap/swing/panel/Bundle"); private volatile MindMap model; private volatile String errorText; private final List<MindMapListener> mindMapListeners = new CopyOnWriteArrayList<MindMapListener>(); private static final double SCALE_STEP = 0.2d; private static final double SCALE_MINIMUM = 0.3d; private static final double SCALE_MAXIMUM = 10.0d; private static final Color COLOR_MOUSE_DRAG_SELECTION = new Color(0x80000000, true); private final JTextArea textEditor = UI_COMPO_FACTORY.makeTextArea(); private final JPanel textEditorPanel = UI_COMPO_FACTORY.makePanel(); private transient AbstractElement elementUnderEdit = null; private transient int[] pathToPrevTopicBeforeEdit = null; private final List<Topic> selectedTopics = new ArrayList<Topic>(); private transient MouseSelectedArea mouseDragSelection = null; private transient DraggedElement draggedElement = null; private transient AbstractElement destinationElement = null; private volatile boolean popupMenuActive = false; private final MindMapPanelConfig config; public MindMapPanel(@Nonnull final MindMapPanelController controller) { super(null); final MindMapPanelConfig panelConfig = controller.provideConfigForMindMapPanel(this); this.textEditorPanel.setLayout(new BorderLayout(0, 0)); this.controller = controller; this.config = new MindMapPanelConfig(panelConfig, false); this.textEditor.setMargin(new Insets(5, 5, 5, 5)); this.textEditor.setBorder(BorderFactory.createEtchedBorder()); this.textEditor.setTabSize(4); this.textEditor.addKeyListener(new KeyAdapter() { @Override public void keyPressed(@Nonnull final KeyEvent e) { if (lockIfNotDisposed()) { try { switch (e.getKeyCode()) { case KeyEvent.VK_ENTER: { e.consume(); } break; case KeyEvent.VK_TAB: { if ((e.getModifiers() & ALL_SUPPORTED_MODIFIERS) == 0) { e.consume(); final Topic edited = elementUnderEdit.getModel(); final int[] topicPosition = edited.getPositionPath(); endEdit(true); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { final Topic theTopic = model.findForPositionPath(topicPosition); if (theTopic != null) { makeNewChildAndStartEdit(theTopic, null); } } }); } } break; default: break; } } finally { unlock(); } } } @Override public void keyTyped(@Nonnull final KeyEvent e) { if (lockIfNotDisposed()) { try { if (config.isKeyEvent(MindMapPanelConfig.KEY_TOPIC_TEXT_NEXT_LINE, e)) { e.consume(); textEditor.insert("\n", textEditor.getCaretPosition()); //NOI18N } else if (e.getKeyChar() == KeyEvent.VK_ENTER && (e.getModifiers() & ALL_SUPPORTED_MODIFIERS) == 0) { e.consume(); endEdit(true); } } finally { unlock(); } } } @Override public void keyReleased(@Nonnull final KeyEvent e) { if (lockIfNotDisposed()) { try { if (config.isKeyEvent(MindMapPanelConfig.KEY_CANCEL_EDIT, e)) { e.consume(); final Topic edited = elementUnderEdit == null ? null : elementUnderEdit.getModel(); endEdit(false); if (edited != null && edited.canBeLost()) { deleteTopics(false, edited); if (pathToPrevTopicBeforeEdit != null) { final int[] path = pathToPrevTopicBeforeEdit; pathToPrevTopicBeforeEdit = null; SwingUtilities.invokeLater(new Runnable() { @Override public void run() { final Topic topic = model.findForPositionPath(path); if (topic != null) { select(topic, false); } } }); } } } } finally { unlock(); } } } }); this.textEditor.getDocument().addDocumentListener(new DocumentListener() { private void updateEditorPanelSize(@Nonnull final Dimension newSize) { if (lockIfNotDisposed()) { try { final Dimension editorPanelMinSize = textEditorPanel.getMinimumSize(); final Dimension newDimension = new Dimension(Math.max(editorPanelMinSize.width, newSize.width), Math.max(editorPanelMinSize.height, newSize.height)); textEditorPanel.setSize(newDimension); textEditorPanel.repaint(); } finally { unlock(); } } } @Override public void insertUpdate(@Nonnull final DocumentEvent e) { updateEditorPanelSize(textEditor.getPreferredSize()); } @Override public void removeUpdate(@Nonnull final DocumentEvent e) { updateEditorPanelSize(textEditor.getPreferredSize()); } @Override public void changedUpdate(@Nonnull final DocumentEvent e) { updateEditorPanelSize(textEditor.getPreferredSize()); } }); this.textEditorPanel.add(this.textEditor, BorderLayout.CENTER); super.setOpaque(true); final KeyAdapter keyAdapter = new KeyAdapter() { @Override public void keyPressed(@Nonnull final KeyEvent e) { if (lockIfNotDisposed()){ try{ if (!e.isConsumed()) { fireNotificationNonConsumedKeyEvent(e,KeyEventType.PRESSED); } }finally{ unlock(); } } } @Override public void keyTyped(@Nonnull final KeyEvent e) { if (lockIfNotDisposed()) { try { if (config.isKeyEvent(MindMapPanelConfig.KEY_ADD_CHILD_AND_START_EDIT, e)) { e.consume(); if (!selectedTopics.isEmpty()) { makeNewChildAndStartEdit(selectedTopics.get(0), null); } } else if (config.isKeyEvent(MindMapPanelConfig.KEY_ADD_SIBLING_AND_START_EDIT, e)) { e.consume(); if (!hasActiveEditor() && hasOnlyTopicSelected()) { final Topic baseTopic = selectedTopics.get(0); makeNewChildAndStartEdit(baseTopic.getParent() == null ? baseTopic : baseTopic.getParent(), baseTopic); } } else if (config.isKeyEvent(MindMapPanelConfig.KEY_FOCUS_ROOT_OR_START_EDIT, e)) { e.consume(); if (!hasSelectedTopics()) { select(getModel().getRoot(), false); } else if (hasOnlyTopicSelected()) { startEdit((AbstractElement) selectedTopics.get(0).getPayload()); } } if (!e.isConsumed()) { fireNotificationNonConsumedKeyEvent(e,KeyEventType.TYPED); } } finally { unlock(); } } } @Override public void keyReleased(@Nonnull final KeyEvent e) { if (lockIfNotDisposed()) { try { if (config.isKeyEvent(MindMapPanelConfig.KEY_SHOW_POPUP, e)) { e.consume(); processPopUpForShortcut(); } else if (config.isKeyEvent(MindMapPanelConfig.KEY_DELETE_TOPIC, e)) { e.consume(); focusTo(deleteSelectedTopics(false)); } else if (config.isKeyEventDetected(e, MindMapPanelConfig.KEY_FOCUS_MOVE_LEFT, MindMapPanelConfig.KEY_FOCUS_MOVE_RIGHT, MindMapPanelConfig.KEY_FOCUS_MOVE_UP, MindMapPanelConfig.KEY_FOCUS_MOVE_DOWN, MindMapPanelConfig.KEY_FOCUS_MOVE_LEFT_ADD_FOCUSED, MindMapPanelConfig.KEY_FOCUS_MOVE_RIGHT_ADD_FOCUSED, MindMapPanelConfig.KEY_FOCUS_MOVE_UP_ADD_FOCUSED, MindMapPanelConfig.KEY_FOCUS_MOVE_DOWN_ADD_FOCUSED)) { e.consume(); processMoveFocusByKey(e); } else if (config.isKeyEvent(MindMapPanelConfig.KEY_ZOOM_IN, e)) { e.consume(); setScale(Math.max(SCALE_MINIMUM, Math.min(getScale() + SCALE_STEP, SCALE_MAXIMUM))); updateView(false); } else if (config.isKeyEvent(MindMapPanelConfig.KEY_ZOOM_OUT, e)) { e.consume(); setScale(Math.max(SCALE_MINIMUM, Math.min(getScale() - SCALE_STEP, SCALE_MAXIMUM))); updateView(false); } else if (config.isKeyEvent(MindMapPanelConfig.KEY_ZOOM_RESET, e)) { e.consume(); setScale(1.0); updateView(false); } else if (config.isKeyEvent(MindMapPanelConfig.KEY_TOPIC_FOLD, e)) { e.consume(); final Topic[] selectedTopics = getSelectedTopics(); final AbstractElement elementToProcess = selectedTopics.length == 1 ? (AbstractElement) selectedTopics[0].getPayload() : null; if (elementToProcess != null) { doFoldOrUnfoldTopic(elementToProcess, false, true); } } else if (config.isKeyEvent(MindMapPanelConfig.KEY_TOPIC_UNFOLD, e)) { e.consume(); final Topic[] selectedTopics = getSelectedTopics(); final AbstractElement elementToProcess = selectedTopics.length == 1 ? (AbstractElement) selectedTopics[0].getPayload() : null; if (elementToProcess != null) { doFoldOrUnfoldTopic(elementToProcess, true, true); } } if (!e.isConsumed()) { fireNotificationNonConsumedKeyEvent(e,KeyEventType.RELEASED); } } finally { unlock(); } } } }; this.setFocusTraversalKeysEnabled(false); final MindMapPanel theInstance = this; final MouseAdapter adapter = new MouseAdapter() { @Override public void mouseEntered(@Nonnull final MouseEvent e) { setCursor(Cursor.getDefaultCursor()); } @Override public void mouseMoved(@Nonnull final MouseEvent e) { if (lockIfNotDisposed()) { try { if (!controller.isMouseMoveProcessingAllowed(theInstance)) { return; } final AbstractElement element = findTopicUnderPoint(e.getPoint()); if (element == null) { setCursor(Cursor.getDefaultCursor()); setToolTipText(null); } else { final ElementPart part = element.findPartForPoint(e.getPoint()); switch (part) { case ICONS: { final Extra<?> extra = element.getIconBlock().findExtraForPoint(e.getPoint().getX() - element.getBounds().getX(), e.getPoint().getY() - element.getBounds().getY()); if (extra != null) { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); setToolTipText(makeHtmlTooltipForExtra(extra)); } else { setCursor(null); setToolTipText(null); } } break; case VISUAL_ATTRIBUTES: { final VisualAttributePlugin plugin = element.getVisualAttributeImageBlock().findPluginForPoint(e.getPoint().getX() - element.getBounds().getX(), e.getPoint().getY() - element.getBounds().getY()); if (plugin != null) { final Topic theTopic = element.getModel(); if (plugin.isClickable(theInstance, theTopic)) { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else { setCursor(null); } setToolTipText(plugin.getToolTip(theInstance, theTopic)); } else { setCursor(null); setToolTipText(null); } } break; case COLLAPSATOR: { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); setToolTipText(null); } break; default: { setCursor(Cursor.getDefaultCursor()); setToolTipText(null); } break; } } } finally { unlock(); } } } @Override public void mousePressed(@Nonnull final MouseEvent e) { if (lockIfNotDisposed()) { try { if (!controller.isMouseClickProcessingAllowed(theInstance)) { return; } try { if (e.isPopupTrigger()) { mouseDragSelection = null; MindMap theMap = model; AbstractElement element = null; if (theMap != null) { element = findTopicUnderPoint(e.getPoint()); } processPopUp(e.getPoint(), element); e.consume(); } else { endEdit(elementUnderEdit != null); mouseDragSelection = null; } } catch (Exception ex) { LOGGER.error("Error during mousePressed()", ex); } } finally { unlock(); } } } @Override public void mouseReleased(@Nonnull final MouseEvent e) { if (lockIfNotDisposed()) { try { if (!controller.isMouseClickProcessingAllowed(theInstance)) { return; } try { if (draggedElement != null) { draggedElement.updatePosition(e.getPoint()); if (endDragOfElement(draggedElement, destinationElement)) { updateView(true); } } else if (mouseDragSelection != null) { final List<Topic> covered = mouseDragSelection.getAllSelectedElements(model); if (e.isShiftDown()) { for (final Topic m : covered) { select(m, false); } } else if (e.isControlDown()) { for (final Topic m : covered) { select(m, true); } } else { removeAllSelection(); for (final Topic m : covered) { select(m, false); } } } else if (e.isPopupTrigger()) { mouseDragSelection = null; MindMap theMap = model; AbstractElement element = null; if (theMap != null) { element = findTopicUnderPoint(e.getPoint()); } processPopUp(e.getPoint(), element); e.consume(); } } catch (Exception ex) { LOGGER.error("Error during mouseReleased()", ex); } finally { mouseDragSelection = null; draggedElement = null; destinationElement = null; repaint(); } } finally { unlock(); } } } private boolean isNonOverCollapsator(@Nonnull final MouseEvent e, @Nonnull final AbstractElement element) { final ElementPart part = element.findPartForPoint(e.getPoint()); return part != ElementPart.COLLAPSATOR; } @Override public void mouseDragged(@Nonnull final MouseEvent e) { if (lockIfNotDisposed()) { try { if (!controller.isMouseMoveProcessingAllowed(theInstance)) { return; } scrollRectToVisible(new Rectangle(e.getX(), e.getY(), 1, 1)); if (!popupMenuActive) { if (draggedElement == null && mouseDragSelection == null) { final AbstractElement elementUnderMouse = findTopicUnderPoint(e.getPoint()); if (elementUnderMouse == null) { MindMap theMap = model; if (theMap != null) { final AbstractElement element = findTopicUnderPoint(e.getPoint()); if (controller.isSelectionAllowed(theInstance) && element == null) { mouseDragSelection = new MouseSelectedArea(e.getPoint()); } } } else if (controller.isElementDragAllowed(theInstance)) { if (elementUnderMouse.isMoveable() && isNonOverCollapsator(e, elementUnderMouse)) { selectedTopics.clear(); final Point mouseOffset = new Point((int) Math.round(e.getPoint().getX() - elementUnderMouse.getBounds().getX()), (int) Math.round(e.getPoint().getY() - elementUnderMouse.getBounds().getY())); draggedElement = new DraggedElement(elementUnderMouse, config, mouseOffset, e.isControlDown() || e.isMetaDown() ? DraggedElement.Modifier.MAKE_JUMP : DraggedElement.Modifier.NONE); draggedElement.updatePosition(e.getPoint()); findDestinationElementForDragged(); } else { draggedElement = null; } repaint(); } } else if (mouseDragSelection != null) { if (controller.isSelectionAllowed(theInstance)) { mouseDragSelection.update(e); } else { mouseDragSelection = null; } repaint(); } else if (draggedElement != null) { if (controller.isElementDragAllowed(theInstance)) { draggedElement.updatePosition(e.getPoint()); findDestinationElementForDragged(); } else { draggedElement = null; } repaint(); } } else { mouseDragSelection = null; } } finally { unlock(); } } } @Override public void mouseWheelMoved(@Nonnull final MouseWheelEvent e) { if (lockIfNotDisposed()) { try { if (controller.isMouseWheelProcessingAllowed(theInstance)) { mouseDragSelection = null; draggedElement = null; final MindMapPanelConfig theConfig = config; if (!e.isConsumed() && (theConfig != null && ((e.getModifiers() & theConfig.getScaleModifiers()) == theConfig.getScaleModifiers()))) { endEdit(elementUnderEdit != null); final double oldScale = getScale(); final double newScale = Math.max(SCALE_MINIMUM, Math.min(oldScale + (SCALE_STEP * -e.getWheelRotation()), SCALE_MAXIMUM)); fireNotificationScaledByMouse(e.getPoint(), oldScale, newScale, true); setScale(newScale); updateElementsAndSizeForCurrentGraphics(true, false); fireNotificationScaledByMouse(e.getPoint(), oldScale, newScale, false); e.consume(); repaint(); } else { sendToParent(e); } } } finally { unlock(); } } } @Override public void mouseClicked(@Nonnull final MouseEvent e) { if (lockIfNotDisposed()) { try { if (!controller.isMouseClickProcessingAllowed(theInstance)) { return; } mouseDragSelection = null; draggedElement = null; MindMap theMap = model; AbstractElement element = null; if (theMap != null) { element = findTopicUnderPoint(e.getPoint()); } final boolean isCtrlDown = e.isControlDown(); if (element != null) { final ElementPart part = element.findPartForPoint(e.getPoint()); if (part == ElementPart.COLLAPSATOR) { fireNotificationTopicCollapsatorClick(element.getModel(),true); doFoldOrUnfoldTopic(element, element.isCollapsed(), isCtrlDown); if (selectedTopics.isEmpty() || selectedTopics.size() == 1) { removeAllSelection(); select(element.getModel(), false); } fireNotificationTopicCollapsatorClick(element.getModel(),false); } else if (!isCtrlDown) { switch (part) { case VISUAL_ATTRIBUTES: final VisualAttributePlugin plugin = element.getVisualAttributeImageBlock().findPluginForPoint(e.getPoint().getX() - element.getBounds().getX(), e.getPoint().getY() - element.getBounds().getY()); boolean processedByPlugin = false; if (plugin != null) { if (plugin.isClickable(theInstance, element.getModel())) { processedByPlugin = true; try { if (plugin.onClick(theInstance, element.getModel(), e.getClickCount())) { notifyModelChanged(); repaint(); } } catch (Exception ex) { LOGGER.error("Error during visual attribute processing", ex); controller.getDialogProvider(theInstance).msgError("Detectd critical error! See log!"); } } } if (!processedByPlugin) { removeAllSelection(); select(element.getModel(), false); } break; case ICONS: final Extra<?> extra = element.getIconBlock().findExtraForPoint(e.getPoint().getX() - element.getBounds().getX(), e.getPoint().getY() - element.getBounds().getY()); if (extra != null) { fireNotificationClickOnExtra(element.getModel(), e.getModifiers(), e.getClickCount(), extra); } break; default: // only removeAllSelection(); select(element.getModel(), false); if (e.getClickCount() > 1) { startEdit(element); } break; } } else // group { if (selectedTopics.isEmpty()) { select(element.getModel(), false); } else { select(element.getModel(), true); } } } } finally { unlock(); } } } }; addMouseWheelListener(adapter); addMouseListener(adapter); addMouseMotionListener(adapter); addKeyListener(keyAdapter); this.textEditorPanel.setVisible(false); this.add(this.textEditorPanel); this.addComponentListener(new ComponentAdapter() { @Override public void componentResized(@Nonnull final ComponentEvent e) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { updateView(false); updateEditorAfterResizing(); } }); } }); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (lockIfNotDisposed()) { try { for (final PanelAwarePlugin p : MindMapPluginRegistry.getInstance().findFor(PanelAwarePlugin.class)) { p.onPanelCreate(theInstance); } } finally { unlock(); } } } }); } private void doFoldOrUnfoldTopic(@Nonnull final AbstractElement element, final boolean unfold, final boolean onlyFirstLevel) { if (unfold) { ((AbstractCollapsableElement) element).setCollapse(false); if (onlyFirstLevel) { ((AbstractCollapsableElement) element).collapseAllFirstLevelChildren(); } } else { ((AbstractCollapsableElement) element).setCollapse(true); for (final Topic t : getSelectedTopics()) { if (!MindMapUtils.isTopicVisible(t)) { this.selectedTopics.remove(t); } } } this.model.resetPayload(); notifyModelChanged(); repaint(); } @Nullable public Object findTmpObject(@Nonnull final Object key) { this.lock(); try { final WeakReference ref = this.weakTable.get(key); return ref == null ? null : ref.get(); } finally { this.unlock(); } } public void putTmpObject(@Nonnull final Object key, @Nullable final Object value) { this.lock(); try { if (value == null) { this.weakTable.remove(key); } else { this.weakTable.put(key, new WeakReference<Object>(value)); } } finally { this.unlock(); } } @Nonnull private String makeHtmlTooltipForExtra(@Nonnull final Extra<?> extra) { final StringBuilder builder = new StringBuilder(); builder.append("<html>"); //NOI18N switch (extra.getType()) { case FILE: { builder.append(BUNDLE.getString("MindMapPanel.tooltipOpenFile")).append(StringEscapeUtils.escapeHtml(((ExtraFile) extra).getAsString())); } break; case TOPIC: { final Topic topic = this.getModel().findTopicForLink((ExtraTopic) extra); builder.append(BUNDLE.getString("MindMapPanel.tooltipJumpToTopic")).append(StringEscapeUtils.escapeHtml(ModelUtils.makeShortTextVersion(topic == null ? "----" : topic.getText(), 32))); } break; case LINK: { builder.append(BUNDLE.getString("MindMapPanel.tooltipOpenLink")).append(StringEscapeUtils.escapeHtml(ModelUtils.makeShortTextVersion(((ExtraLink) extra).getAsString(), 48))); } break; case NOTE: { builder.append(BUNDLE.getString("MindMapPanel.tooltipOpenText")).append(StringEscapeUtils.escapeHtml(ModelUtils.makeShortTextVersion(((ExtraNote) extra).getAsString(), 64))); } break; default: { builder.append("<b>Unknown</b>"); //NOI18N } break; } builder.append("</html>"); //NOI18N return builder.toString(); } public void refreshConfiguration() { if (this.lockIfNotDisposed()) { try { final MindMapPanel theInstance = this; final double scale = this.config.getScale(); this.config.makeAtomicChange(new Runnable() { @Override public void run() { config.makeFullCopyOf(controller.provideConfigForMindMapPanel(theInstance), false, false); config.setScale(scale); } }); invalidate(); repaint(); } finally { this.unlock(); } } } private static final int DRAG_POSITION_UNKNOWN = -1; private static final int DRAG_POSITION_LEFT = 1; private static final int DRAG_POSITION_TOP = 2; private static final int DRAG_POSITION_BOTTOM = 3; private static final int DRAG_POSITION_RIGHT = 4; private int calcDropPosition(@Nonnull final AbstractElement destination, @Nonnull final Point dropPoint) { final int result; if (destination.getClass() == ElementRoot.class) { result = dropPoint.getX() < destination.getBounds().getCenterX() ? DRAG_POSITION_LEFT : DRAG_POSITION_RIGHT; } else { final boolean destinationIsLeft = destination.isLeftDirection(); final Rectangle2D bounds = destination.getBounds(); final double edgeOffset = bounds.getWidth() * 0.2d; if (dropPoint.getX() >= (bounds.getX() + edgeOffset) && dropPoint.getX() <= (bounds.getMaxX() - edgeOffset)) { result = dropPoint.getY() < bounds.getCenterY() ? DRAG_POSITION_TOP : DRAG_POSITION_BOTTOM; } else if (destinationIsLeft) { result = dropPoint.getX() < bounds.getCenterX() ? DRAG_POSITION_LEFT : DRAG_POSITION_UNKNOWN; } else { result = dropPoint.getX() > bounds.getCenterX() ? DRAG_POSITION_RIGHT : DRAG_POSITION_UNKNOWN; } } return result; } private boolean endDragOfElement(@Nonnull final DraggedElement draggedElement, @Nonnull final AbstractElement destination) { final AbstractElement dragged = draggedElement.getElement(); final Point dropPoint = draggedElement.getPosition(); final boolean ignore = dragged.getModel() == destination.getModel() || dragged.getBounds().contains(dropPoint) || destination.getModel().hasAncestor(dragged.getModel()); if (ignore) { return false; } boolean changed = true; if (draggedElement.getModifier() == DraggedElement.Modifier.MAKE_JUMP) { // make link return this.controller.processDropTopicToAnotherTopic(this, dropPoint, dragged.getModel(), destination.getModel()); } final int pos = calcDropPosition(destination, dropPoint); switch (pos) { case DRAG_POSITION_TOP: case DRAG_POSITION_BOTTOM: { dragged.getModel().moveToNewParent(assertNotNull(destination.getParent()).getModel()); if (pos == DRAG_POSITION_TOP) { dragged.getModel().moveBefore(destination.getModel()); } else { dragged.getModel().moveAfter(destination.getModel()); } if (destination.getClass() == ElementLevelFirst.class) { AbstractCollapsableElement.makeTopicLeftSided(dragged.getModel(), destination.isLeftDirection()); } else { AbstractCollapsableElement.makeTopicLeftSided(dragged.getModel(), false); } } break; case DRAG_POSITION_RIGHT: case DRAG_POSITION_LEFT: { if (dragged.getParent() == destination) { // the same parent if (destination.getClass() == ElementRoot.class) { // process only for the root, just update direction if (dragged instanceof AbstractCollapsableElement) { ((AbstractCollapsableElement) dragged).setLeftDirection(pos == DRAG_POSITION_LEFT); } } } else { dragged.getModel().moveToNewParent(destination.getModel()); if (destination instanceof AbstractCollapsableElement && destination.isCollapsed() && (controller == null ? true : controller.isUnfoldCollapsedTopicDropTarget(this))) { //NOI18N ((AbstractCollapsableElement) destination).setCollapse(false); } if (dropPoint.getY() < destination.getBounds().getY()) { dragged.getModel().makeFirst(); } else { dragged.getModel().makeLast(); } if (destination.getClass() == ElementRoot.class) { AbstractCollapsableElement.makeTopicLeftSided(dragged.getModel(), pos == DRAG_POSITION_LEFT); } else { AbstractCollapsableElement.makeTopicLeftSided(dragged.getModel(), false); } } } break; default: break; } dragged.getModel().setPayload(null); return changed; } private void sendToParent(@Nonnull final AWTEvent evt) { final Container parent = this.getParent(); if (parent != null) { parent.dispatchEvent(evt); } } private void processMoveFocusByKey(@Nonnull final KeyEvent key) { final AbstractElement lastSelectedTopic = this.selectedTopics.isEmpty() ? null : (AbstractElement) this.selectedTopics.get(this.selectedTopics.size() - 1).getPayload(); if (lastSelectedTopic == null) { return; } AbstractElement nextFocused = null; boolean modelChanged = false; if (lastSelectedTopic.isMoveable()) { boolean processFirstChild = false; if (config.isKeyEventDetected(key, MindMapPanelConfig.KEY_FOCUS_MOVE_LEFT, MindMapPanelConfig.KEY_FOCUS_MOVE_LEFT_ADD_FOCUSED)) { if (lastSelectedTopic.isLeftDirection()) { processFirstChild = true; } else { nextFocused = (AbstractElement) assertNotNull(lastSelectedTopic.getModel().getParent()).getPayload(); } } else if (config.isKeyEventDetected(key, MindMapPanelConfig.KEY_FOCUS_MOVE_RIGHT, MindMapPanelConfig.KEY_FOCUS_MOVE_RIGHT_ADD_FOCUSED)) { if (lastSelectedTopic.isLeftDirection()) { nextFocused = (AbstractElement) assertNotNull(lastSelectedTopic.getModel().getParent()).getPayload(); } else { processFirstChild = true; } } else { final boolean pressedButtonMoveUp = config.isKeyEventDetected(key, MindMapPanelConfig.KEY_FOCUS_MOVE_UP, MindMapPanelConfig.KEY_FOCUS_MOVE_UP_ADD_FOCUSED); final boolean firstLevel = lastSelectedTopic.getClass() == ElementLevelFirst.class; final boolean currentLeft = AbstractCollapsableElement.isLeftSidedTopic(lastSelectedTopic.getModel()); final TopicChecker checker = new TopicChecker() { @Override public boolean check(@Nonnull final Topic topic) { if (!firstLevel) { return true; } else if (currentLeft) { return AbstractCollapsableElement.isLeftSidedTopic(topic); } else { return !AbstractCollapsableElement.isLeftSidedTopic(topic); } } }; final Topic topic = pressedButtonMoveUp ? lastSelectedTopic.getModel().findPrev(checker) : lastSelectedTopic.getModel().findNext(checker); nextFocused = topic == null ? null : (AbstractElement) topic.getPayload(); } if (processFirstChild) { if (lastSelectedTopic.hasChildren()) { if (lastSelectedTopic.isCollapsed()) { ((AbstractCollapsableElement) lastSelectedTopic).setCollapse(false); modelChanged = true; } nextFocused = (AbstractElement) (lastSelectedTopic.getModel().getChildren().get(0)).getPayload(); } } } else if (config.isKeyEventDetected(key, MindMapPanelConfig.KEY_FOCUS_MOVE_LEFT, MindMapPanelConfig.KEY_FOCUS_MOVE_LEFT_ADD_FOCUSED)) { for (final Topic t : lastSelectedTopic.getModel().getChildren()) { final AbstractElement e = (AbstractElement) t.getPayload(); if (e != null && e.isLeftDirection()) { nextFocused = e; break; } } } else if (config.isKeyEventDetected(key, MindMapPanelConfig.KEY_FOCUS_MOVE_RIGHT, MindMapPanelConfig.KEY_FOCUS_MOVE_RIGHT_ADD_FOCUSED)) { for (final Topic t : lastSelectedTopic.getModel().getChildren()) { final AbstractElement e = (AbstractElement) t.getPayload(); if (e != null && !e.isLeftDirection()) { nextFocused = e; break; } } } if (nextFocused != null) { final boolean addFocused = config.isKeyEventDetected(key, MindMapPanelConfig.KEY_FOCUS_MOVE_UP_ADD_FOCUSED, MindMapPanelConfig.KEY_FOCUS_MOVE_DOWN_ADD_FOCUSED, MindMapPanelConfig.KEY_FOCUS_MOVE_LEFT_ADD_FOCUSED, MindMapPanelConfig.KEY_FOCUS_MOVE_RIGHT_ADD_FOCUSED); if (!addFocused || this.selectedTopics.contains(nextFocused.getModel())) { removeAllSelection(); } select(nextFocused.getModel(), false); } if (modelChanged) { notifyModelChanged(); } } /** * Safe Swing thread execution sequence of some jobs over model with model changed notification in the end * * @param jobs sequence of jobs to be executed * * @since 1.3.1 */ public void executeModelJobs(@Nonnull @MustNotContainNull final ModelJob... jobs) { Utils.safeSwingCall(new Runnable() { @Override public void run() { for (final ModelJob j : jobs) { try { if (!j.doChangeModel(model)) { break; } } catch (Exception ex) { LOGGER.error("Errot during job execution", ex); } } notifyModelChanged(); } }); } /** * Send signal that the model has been changed. * * @since 1.2 */ public void notifyModelChanged() { Utils.safeSwingCall(new Runnable() { @Override public void run() { if (lockIfNotDisposed()) { try { invalidate(); fireNotificationMindMapChanged(); } finally { unlock(); } } } }); } private void ensureVisibility(@Nonnull final AbstractElement e) { fireNotificationEnsureTopicVisibility(e.getModel()); } private boolean hasActiveEditor() { if (lockIfNotDisposed()) { try { return this.elementUnderEdit != null; } finally { unlock(); } } return false; } public boolean isShowJumps() { return Boolean.parseBoolean(this.model.getAttribute(ATTR_SHOW_JUMPS)); } public void setShowJumps(final boolean flag) { if (lockIfNotDisposed()) { try { this.model.setAttribute(ATTR_SHOW_JUMPS, flag ? "true" : null); repaint(); fireNotificationMindMapChanged(); } finally { this.unlock(); } } } @Nonnull private Topic makeNewTopic(@Nonnull final Topic parent, @Nullable final Topic afterTopic, @Nonnull final String text) { final Topic result = parent.makeChild(text, afterTopic); for (final ModelAwarePlugin p : MindMapPluginRegistry.getInstance().findFor(ModelAwarePlugin.class)) { p.onCreateTopic(this, parent, result); } return result; } public void makeNewChildAndStartEdit(@Nullable final Topic parent, @Nullable final Topic baseTopic) { if (this.lockIfNotDisposed()) { try { if (parent != null) { final Topic currentSelected = getFirstSelected(); this.pathToPrevTopicBeforeEdit = currentSelected == null ? null : currentSelected.getPositionPath(); removeAllSelection(); final Topic newTopic = makeNewTopic(parent, baseTopic, ""); //NOI18N if (this.controller.isCopyColorInfoFromParentToNewChildAllowed(this) && !parent.isRoot()) { MindMapUtils.copyColorAttributes(parent, newTopic); } final AbstractElement parentElement = (AbstractElement) parent.getPayload(); if (parent.getChildren().size() != 1 && parent.getParent() == null && baseTopic == null) { int numLeft = 0; int numRight = 0; for (final Topic t : parent.getChildren()) { if (AbstractCollapsableElement.isLeftSidedTopic(t)) { numLeft++; } else { numRight++; } } AbstractCollapsableElement.makeTopicLeftSided(newTopic, numLeft < numRight); } else if (baseTopic != null && baseTopic.getPayload() != null) { final AbstractElement element = assertNotNull((AbstractElement) baseTopic.getPayload()); AbstractCollapsableElement.makeTopicLeftSided(newTopic, element.isLeftDirection()); } if (parentElement instanceof AbstractCollapsableElement && parentElement.isCollapsed()) { ((AbstractCollapsableElement) parentElement).setCollapse(false); } select(newTopic, false); updateView(false); startEdit((AbstractElement) newTopic.getPayload()); } } finally { this.unlock(); } } } protected void fireNotificationSelectionChanged() { final Topic[] selected = this.selectedTopics.toArray(new Topic[this.selectedTopics.size()]); for (final MindMapListener l : this.mindMapListeners) { l.onChangedSelection(this, selected); } } protected void fireNotificationMindMapChanged() { for (final MindMapListener l : this.mindMapListeners) { l.onMindMapModelChanged(this); } } protected void fireNotificationComponentElementsLayouted(@Nonnull final Graphics2D graphics) { for (final MindMapListener l : this.mindMapListeners) { l.onComponentElementsLayouted(this,graphics); } } protected void fireNotificationClickOnExtra(@Nonnull final Topic topic, final int modifiers, final int clicks, @Nonnull final Extra<?> extra) { for (final MindMapListener l : this.mindMapListeners) { l.onClickOnExtra(this, modifiers, clicks, topic, extra); } } protected void fireNotificationEnsureTopicVisibility(@Nonnull final Topic topic) { for (final MindMapListener l : this.mindMapListeners) { l.onEnsureVisibilityOfTopic(this, topic); } } protected void fireNotificationTopicCollapsatorClick(@Nonnull final Topic topic, final boolean beforeAction) { for (final MindMapListener l : this.mindMapListeners) { l.onTopicCollapsatorClick(this, topic, beforeAction); } } protected void fireNotificationScaledByMouse(@Nonnull final Point mousePoint, final double oldScale, final double newScale, final boolean beforeAction) { for (final MindMapListener l : this.mindMapListeners) { l.onScaledByMouse(this, mousePoint, oldScale, newScale, beforeAction); } } protected void fireNotificationNonConsumedKeyEvent(@Nonnull final KeyEvent keyEvent, @Nonnull final KeyEventType type) { for (final MindMapListener l : this.mindMapListeners) { if (keyEvent.isConsumed()) break; l.onNonConsumedKeyEvent(this, keyEvent, type); } } public void deleteTopics(final boolean force, @Nonnull @MustNotContainNull final Topic... topics) { if (lockIfNotDisposed()) { try { endEdit(false); final List<ModelAwarePlugin> plugins = MindMapPluginRegistry.getInstance().findFor(ModelAwarePlugin.class); boolean allowed = true; if (!force) { for (final MindMapListener l : this.mindMapListeners) { allowed &= l.allowedRemovingOfTopics(this, topics); } } if (allowed) { removeAllSelection(); for (final Topic t : topics) { for (final ModelAwarePlugin p : plugins) { p.onDeleteTopic(this, t); } this.model.removeTopic(t); } updateView(true); } } finally { unlock(); } } } public void collapseOrExpandAll(final boolean collapse) { if (this.lockIfNotDisposed()) { try { endEdit(false); removeAllSelection(); if (this.model.getRoot() != null) { final AbstractElement root = (AbstractElement) assertNotNull(this.model.getRoot()).getPayload(); if (root != null && root.collapseOrExpandAllChildren(collapse)) { updateView(true); } } } finally { this.unlock(); } } } @Nullable public Topic deleteSelectedTopics(final boolean force) { Topic nextToFocus = null; if (this.lockIfNotDisposed()) { try { if (!this.selectedTopics.isEmpty()) { if (this.selectedTopics.size() == 1) { nextToFocus = this.selectedTopics.get(0).getParent(); } deleteTopics(force, this.selectedTopics.toArray(new Topic[this.selectedTopics.size()])); } } finally { this.unlock(); } } return nextToFocus; } public boolean hasSelectedTopics() { if (this.lockIfNotDisposed()) { try { return !this.selectedTopics.isEmpty(); } finally { this.unlock(); } } else { return false; } } public boolean hasOnlyTopicSelected() { if (this.lockIfNotDisposed()) { try { return this.selectedTopics.size() == 1; } finally { this.unlock(); } } else { return false; } } public void removeFromSelection(@Nonnull final Topic t) { if (this.lockIfNotDisposed()) { try { if (this.selectedTopics.contains(t)) { if (this.selectedTopics.remove(t)) { fireNotificationSelectionChanged(); } repaint(); } } finally { this.unlock(); } } } public void select(@Nullable final Topic t, final boolean removeIfPresented) { if (this.lockIfNotDisposed()) { try { if (this.controller.isSelectionAllowed(this) && t != null) { if (!this.selectedTopics.contains(t)) { if (this.selectedTopics.add(t)) { fireNotificationSelectionChanged(); } fireNotificationEnsureTopicVisibility(t); repaint(); } else if (removeIfPresented) { removeFromSelection(t); } } } finally { this.unlock(); } } } @Nonnull @MustNotContainNull public Topic[] getSelectedTopics() { this.lock(); try { return this.selectedTopics.toArray(new Topic[this.selectedTopics.size()]); } finally { this.unlock(); } } public void updateEditorAfterResizing() { if (this.lockIfNotDisposed()) { try { if (this.elementUnderEdit != null) { final AbstractElement element = this.elementUnderEdit; final Dimension textBlockSize = new Dimension((int) element.getBounds().getWidth(), (int) element.getBounds().getHeight()); this.textEditorPanel.setBounds((int) element.getBounds().getX(), (int) element.getBounds().getY(), textBlockSize.width, textBlockSize.height); this.textEditor.setMinimumSize(textBlockSize); this.textEditorPanel.setVisible(true); this.textEditor.requestFocus(); } } finally { this.unlock(); } } } public void hideEditor() { if (this.lockIfNotDisposed()) { try { this.textEditorPanel.setVisible(false); this.elementUnderEdit = null; } finally { this.unlock(); } } } public boolean endEdit(final boolean commit) { boolean result = false; if (this.lockIfNotDisposed()) { result = this.elementUnderEdit != null; try { if (commit && this.elementUnderEdit != null) { this.pathToPrevTopicBeforeEdit = null; final AbstractElement editedElement = this.elementUnderEdit; final Topic editedTopic = this.elementUnderEdit.getModel(); final String oldText = editedElement.getText(); final String newText = this.textEditor.getText(); if (!oldText.equals(newText)) { editedElement.setText(newText); } this.textEditorPanel.setVisible(false); updateView(true); fireNotificationEnsureTopicVisibility(editedTopic); } } finally { try { this.elementUnderEdit = null; this.textEditorPanel.setVisible(false); this.requestFocus(); } finally { this.unlock(); } } } return result; } public void startEdit(@Nullable final AbstractElement element) { if (this.lockIfNotDisposed()) { try { if (element == null) { this.elementUnderEdit = null; this.textEditorPanel.setVisible(false); } else { this.elementUnderEdit = element; element.fillByTextAndFont(this.textEditor); final Dimension textBlockSize = new Dimension((int) element.getBounds().getWidth(), (int) element.getBounds().getHeight()); this.textEditorPanel.setBounds((int) element.getBounds().getX(), (int) element.getBounds().getY(), textBlockSize.width, textBlockSize.height); this.textEditor.setMinimumSize(textBlockSize); ensureVisibility(this.elementUnderEdit); this.textEditorPanel.setVisible(true); this.textEditor.requestFocus(); } } finally { this.unlock(); } } } private void findDestinationElementForDragged() { final Topic theroot = this.model.getRoot(); if (this.draggedElement != null && theroot != null) { final AbstractElement root = (AbstractElement) assertNotNull(theroot.getPayload()); this.destinationElement = root.findNearestOpenedTopicToPoint(this.draggedElement.getElement(), this.draggedElement.getPosition()); } else { this.destinationElement = null; } } protected void processPopUpForShortcut() { if (this.lockIfNotDisposed()) { try { final Topic topic = this.selectedTopics.isEmpty() ? null : this.selectedTopics.get(0); if (topic != null) { fireNotificationEnsureTopicVisibility(topic); } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (topic == null) { select(getModel().getRoot(), false); } else { final AbstractElement element = (AbstractElement) topic.getPayload(); if (element != null) { final Rectangle2D bounds = element.getBounds(); processPopUp(new Point((int) Math.round(bounds.getCenterX()), (int) Math.round(bounds.getCenterY())), element); } } } }); } finally { unlock(); } } } protected void processPopUp(@Nonnull final Point point, @Nullable final AbstractElement elementUnderMouse) { if (this.lockIfNotDisposed()) { try { if (this.controller != null) { final ElementPart partUnderMouse = elementUnderMouse == null ? null : elementUnderMouse.findPartForPoint(point); if (elementUnderMouse != null && !this.selectedTopics.contains(elementUnderMouse.getModel())) { this.selectedTopics.clear(); this.select(elementUnderMouse.getModel(), false); } final JPopupMenu menu = this.controller.makePopUpForMindMapPanel(this, point, elementUnderMouse, partUnderMouse); if (menu != null) { final MindMapPanel theInstance = this; menu.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(@Nonnull final PopupMenuEvent e) { theInstance.mouseDragSelection = null; theInstance.popupMenuActive = true; } @Override public void popupMenuWillBecomeInvisible(@Nonnull final PopupMenuEvent e) { theInstance.mouseDragSelection = null; theInstance.popupMenuActive = false; } @Override public void popupMenuCanceled(@Nonnull final PopupMenuEvent e) { theInstance.mouseDragSelection = null; theInstance.popupMenuActive = false; } }); menu.show(this, point.x, point.y); } } } finally { unlock(); } } } public void addMindMapListener(@Nonnull final MindMapListener l) { if (this.lockIfNotDisposed()) { try { this.mindMapListeners.add(Assertions.assertNotNull(l)); } finally { this.unlock(); } } } public void removeMindMapListener(@Nonnull final MindMapListener l) { if (this.lockIfNotDisposed()) { try { this.mindMapListeners.remove(Assertions.assertNotNull(l)); } finally { this.unlock(); } } } public void setModel(@Nonnull final MindMap model) { this.setModel(model, false); } /** * Set model for the panel, allows to notify listeners optionally. * * @param model model to be set * @param notifyModelChangeListeners true if to notify model change listeners, false otherwise * @since 1.3.0 */ public void setModel(@Nonnull final MindMap model, final boolean notifyModelChangeListeners) { this.lock(); try { if (this.elementUnderEdit != null) { Utils.safeSwingBlockingCall(new Runnable() { @Override public void run() { endEdit(false); } }); } final List<int[]> selectedPaths = new ArrayList<int[]>(); for (final Topic t : this.selectedTopics) { selectedPaths.add(t.getPositionPath()); } this.selectedTopics.clear(); final MindMap oldModel = this.model; this.model = assertNotNull("Model must not be null", model); for (final PanelAwarePlugin p : MindMapPluginRegistry.getInstance().findFor(PanelAwarePlugin.class)) { p.onPanelModelChange(this, oldModel, this.model); } updateView(false); boolean selectionChanged = false; for (final int[] posPath : selectedPaths) { final Topic topic = this.model.findForPositionPath(posPath); if (topic == null) { selectionChanged = true; } else if (!MindMapUtils.isHidden(topic)) { this.selectedTopics.add(topic); } } if (selectionChanged) { fireNotificationSelectionChanged(); } repaint(); } finally { this.unlock(); if (notifyModelChangeListeners) { notifyModelChanged(); } } } @Override public boolean isFocusable() { return true; } @Nonnull public MindMap getModel() { this.lock(); try { return Assertions.assertNotNull("Model is not provided, it must not be null!",this.model); } finally { this.unlock(); } } public void setScale(final double zoom) { if (this.lockIfNotDisposed()) { try { this.config.setScale(zoom); } finally { this.unlock(); } } } public double getScale() { this.lock(); try { return this.config.getScale(); } finally { this.unlock(); } } private static void drawBackground(@Nonnull final MMGraphics g, @Nonnull final MindMapPanelConfig cfg) { final Rectangle clipBounds = g.getClipBounds(); if (cfg.isDrawBackground()) { if (clipBounds == null) { LOGGER.warn("Can't draw background because clip bounds is not provided!"); } else { g.drawRect(clipBounds.x, clipBounds.y, clipBounds.width, clipBounds.height, null, cfg.getPaperColor()); if (cfg.isShowGrid()) { final double scaledGridStep = cfg.getGridStep() * cfg.getScale(); final float minX = clipBounds.x; final float minY = clipBounds.y; final float maxX = clipBounds.x + clipBounds.width; final float maxY = clipBounds.y + clipBounds.height; final Color gridColor = cfg.getGridColor(); for (float x = 0.0f; x < maxX; x += scaledGridStep) { if (x < minX) { continue; } final int intx = Math.round(x); g.drawLine(intx, (int) minY, intx, (int) maxY, gridColor); } for (float y = 0.0f; y < maxY; y += scaledGridStep) { if (y < minY) { continue; } final int inty = Math.round(y); g.drawLine((int) minX, inty, (int) maxX, inty, gridColor); } } } } } private static boolean isModelValid(@Nullable final MindMap map) { boolean result = true; if (map != null) { final Topic root = map.getRoot(); if (root != null) { result = root.getPayload() != null; } } return result; } public static void drawOnGraphicsForConfiguration(@Nonnull final MMGraphics g, @Nonnull final MindMapPanelConfig config, @Nonnull final MindMap map, final boolean drawSelection, @Nullable @MustNotContainNull final List<Topic> selectedTopics) { drawBackground(g, config); drawTopics(g, config, map); if (drawSelection && selectedTopics != null && !selectedTopics.isEmpty()) { drawSelection(g, config, selectedTopics); } } private void drawDestinationElement(@Nonnull final Graphics2D g, @Nonnull final MindMapPanelConfig cfg) { if (this.destinationElement != null && this.draggedElement != null) { g.setColor(new Color((cfg.getSelectLineColor().getRGB() & 0xFFFFFF) | 0x80000000, true)); g.setStroke(new BasicStroke(this.config.safeScaleFloatValue(3.0f, 0.1f))); final Rectangle2D rectToDraw = new Rectangle2D.Double(); rectToDraw.setRect(this.destinationElement.getBounds()); final double selectLineGap = cfg.getSelectLineGap() * 3.0d * cfg.getScale(); rectToDraw.setRect(rectToDraw.getX() - selectLineGap, rectToDraw.getY() - selectLineGap, rectToDraw.getWidth() + selectLineGap * 2, rectToDraw.getHeight() + selectLineGap * 2); final int position = calcDropPosition(this.destinationElement, this.draggedElement.getPosition()); boolean draw = !this.draggedElement.isPositionInside() && !this.destinationElement.getModel().hasAncestor(this.draggedElement.getElement().getModel()); switch (this.draggedElement.getModifier()) { case NONE: { switch (position) { case DRAG_POSITION_TOP: { rectToDraw.setRect(rectToDraw.getX(), rectToDraw.getY(), rectToDraw.getWidth(), rectToDraw.getHeight() / 2); } break; case DRAG_POSITION_BOTTOM: { rectToDraw.setRect(rectToDraw.getX(), rectToDraw.getY() + rectToDraw.getHeight() / 2, rectToDraw.getWidth(), rectToDraw.getHeight() / 2); } break; case DRAG_POSITION_LEFT: { rectToDraw.setRect(rectToDraw.getX(), rectToDraw.getY(), rectToDraw.getWidth() / 2, rectToDraw.getHeight()); } break; case DRAG_POSITION_RIGHT: { rectToDraw.setRect(rectToDraw.getX() + rectToDraw.getWidth() / 2, rectToDraw.getY(), rectToDraw.getWidth() / 2, rectToDraw.getHeight()); } break; default: draw = false; break; } } break; case MAKE_JUMP: { } break; default: throw new Error("Unexpected state " + this.draggedElement.getModifier()); } if (draw) { g.fill(rectToDraw); } } } private static void drawSelection(@Nonnull final MMGraphics g, @Nonnull final MindMapPanelConfig cfg, @Nullable @MustNotContainNull final List<Topic> selectedTopics) { if (selectedTopics != null && !selectedTopics.isEmpty()) { final Color selectLineColor = cfg.getSelectLineColor(); g.setStroke(cfg.safeScaleFloatValue(cfg.getSelectLineWidth(), 0.1f), StrokeType.DASHES); final double selectLineGap = (double) cfg.safeScaleFloatValue(cfg.getSelectLineGap(), 0.05f); final double selectLineGapX2 = selectLineGap + selectLineGap; for (final Topic s : selectedTopics) { final AbstractElement e = (AbstractElement) s.getPayload(); if (e != null) { final int x = (int) Math.round(e.getBounds().getX() - selectLineGap); final int y = (int) Math.round(e.getBounds().getY() - selectLineGap); final int w = (int) Math.round(e.getBounds().getWidth() + selectLineGapX2); final int h = (int) Math.round(e.getBounds().getHeight() + selectLineGapX2); g.drawRect(x, y, w, h, selectLineColor, null); } } } } private static void drawTopics(@Nonnull final MMGraphics g, @Nonnull final MindMapPanelConfig cfg, @Nullable final MindMap map) { if (map != null) { if (Boolean.parseBoolean(map.getAttribute(ATTR_SHOW_JUMPS))) { drawJumps(g, map, cfg); } final Topic root = map.getRoot(); if (root != null) { drawTopicTree(g, root, cfg); } } } private static double findLineAngle(final double sx, final double sy, final double ex, final double ey) { final double deltax = ex - sx; if (deltax == 0.0d) { return Math.PI / 2; } return Math.atan((ey - sy) / deltax) + (ex < sx ? Math.PI : 0); } private static void drawJumps(@Nonnull final MMGraphics gfx, @Nonnull final MindMap map, @Nonnull final MindMapPanelConfig cfg) { final List<Topic> allTopicsWithJumps = map.findAllTopicsForExtraType(Extra.ExtraType.TOPIC); final float scaledSize = cfg.safeScaleFloatValue(cfg.getJumpLinkWidth(), 0.1f); final float lineWidth = scaledSize; final float arrowWidth = cfg.safeScaleFloatValue(cfg.getJumpLinkWidth() * 1.0f, 0.3f); final Color jumpLinkColor = cfg.getJumpLinkColor(); final float arrowSize = cfg.safeScaleFloatValue(10.0f * cfg.getJumpLinkWidth(), 0.2f); for (Topic src : allTopicsWithJumps) { final ExtraTopic extra = (ExtraTopic) assertNotNull(assertNotNull(src).getExtras()).get(Extra.ExtraType.TOPIC); src = MindMapUtils.isHidden(src) ? MindMapUtils.findFirstVisibleAncestor(src) : src; if (extra != null) { Topic dst = map.findTopicForLink(extra); if (dst != null) { if (MindMapUtils.isHidden(dst)) { dst = MindMapUtils.findFirstVisibleAncestor(dst); if (dst == src) { dst = null; } } if (dst != null) { final AbstractElement dstElement = (AbstractElement) dst.getPayload(); if (!MindMapUtils.isHidden(dst) && dstElement != null) { final AbstractElement srcElement = assertNotNull((AbstractElement) assertNotNull(src).getPayload()); final Rectangle2D srcRect = srcElement.getBounds(); final Rectangle2D dstRect = dstElement.getBounds(); drawArrowToDestination(gfx, srcRect, dstRect, lineWidth, arrowWidth, arrowSize, jumpLinkColor); } } } } } } private static void drawArrowToDestination(@Nonnull final MMGraphics gfx, @Nonnull final Rectangle2D start, @Nonnull final Rectangle2D destination, @Nonnull final float lineWidth, @Nonnull final float arrowWidth, final float arrowSize, @Nonnull final Color color) { final double startx = start.getCenterX(); final double starty = start.getCenterY(); final Point2D arrowPoint = Utils.findRectEdgeIntersection(destination, startx, starty); if (arrowPoint != null) { gfx.setStroke(lineWidth, StrokeType.SOLID); double angle = findLineAngle(arrowPoint.getX(), arrowPoint.getY(), startx, starty); final double arrowAngle = Math.PI / 12.0d; final double x1 = arrowSize * Math.cos(angle - arrowAngle); final double y1 = arrowSize * Math.sin(angle - arrowAngle); final double x2 = arrowSize * Math.cos(angle + arrowAngle); final double y2 = arrowSize * Math.sin(angle + arrowAngle); final double cx = (arrowSize / 2.0f) * Math.cos(angle); final double cy = (arrowSize / 2.0f) * Math.sin(angle); final GeneralPath polygon = new GeneralPath(); polygon.moveTo(arrowPoint.getX(), arrowPoint.getY()); polygon.lineTo(arrowPoint.getX() + x1, arrowPoint.getY() + y1); polygon.lineTo(arrowPoint.getX() + x2, arrowPoint.getY() + y2); polygon.closePath(); gfx.draw(polygon, null, color); gfx.setStroke(lineWidth, StrokeType.DOTS); gfx.drawLine((int) startx, (int) starty, (int) (arrowPoint.getX() + cx), (int) (arrowPoint.getY() + cy), color); } } private static void drawTopicTree(@Nonnull final MMGraphics gfx, @Nonnull final Topic topic, @Nonnull final MindMapPanelConfig cfg) { paintTopic(gfx, topic, cfg); final AbstractElement w = assertNotNull((AbstractElement) topic.getPayload()); if (w.isCollapsed()) { return; } for (final Topic t : topic.getChildren()) { drawTopicTree(gfx, t, cfg); } } private static void paintTopic(@Nonnull final MMGraphics gfx, @Nonnull final Topic topic, @Nonnull final MindMapPanelConfig cfg) { final AbstractElement element = (AbstractElement) topic.getPayload(); if (element != null) { element.doPaint(gfx, cfg, true); // -------------- DRAW BORDERS ABOUND COMPONENT AREAS --------------------------- // // final Dimension2D elementAreaSize = element.getBlockSize(); // final Rectangle2D elementPosition = element.getBounds(); // // if (element instanceof ElementRoot) { // final ElementRoot root = (ElementRoot) element; // final Dimension2D leftSubblock = root.getLeftBlockSize(); // final Dimension2D rightSubblock = root.getRightBlockSize(); // // if (leftSubblock.getWidth()>0) { // gfx.draw(new Rectangle2D.Double(elementPosition.getX() - leftSubblock.getWidth(), elementPosition.getY() - (leftSubblock.getHeight() - elementPosition.getHeight()) / 2d, leftSubblock.getWidth(), leftSubblock.getHeight()), Color.RED, null); // } // // if (rightSubblock.getWidth()>0) { // gfx.draw(new Rectangle2D.Double(elementPosition.getX() + elementPosition.getWidth(), elementPosition.getY() - (rightSubblock.getHeight() - elementPosition.getHeight()) / 2d, rightSubblock.getWidth(), rightSubblock.getHeight()), Color.RED, null); // } // // gfx.draw(new Rectangle2D.Double(elementPosition.getX() - leftSubblock.getWidth(), elementPosition.getY() - (elementAreaSize.getHeight() - elementPosition.getHeight()) / 2d, elementAreaSize.getWidth(), elementAreaSize.getHeight()), Color.CYAN, null); // // }else // if (element.isLeftDirection()){ // gfx.draw(new Rectangle2D.Double(elementPosition.getX()-(elementAreaSize.getWidth()-elementPosition.getWidth()),elementPosition.getY()-(elementAreaSize.getHeight()-elementPosition.getHeight())/2d,elementAreaSize.getWidth(),elementAreaSize.getHeight()), Color.GREEN, null); // } else { // gfx.draw(new Rectangle2D.Double(elementPosition.getX(), elementPosition.getY() - (elementAreaSize.getHeight() - elementPosition.getHeight()) / 2d, elementAreaSize.getWidth(), elementAreaSize.getHeight()), Color.YELLOW, null); // } // ------------------------------------------------------------------------------ } } private static void setElementSizesForElementAndChildren(@Nonnull final MMGraphics gfx, @Nonnull final MindMapPanelConfig cfg, @Nonnull final Topic topic, final int level) { AbstractElement widget = (AbstractElement) topic.getPayload(); if (widget == null) { switch (level) { case 0: widget = new ElementRoot(topic); break; case 1: widget = new ElementLevelFirst(topic); break; default: widget = new ElementLevelOther(topic); break; } topic.setPayload(widget); } widget.updateElementBounds(gfx, cfg); for (final Topic t : topic.getChildren()) { setElementSizesForElementAndChildren(gfx, cfg, t, level + 1); } widget.updateBlockSize(cfg); } public static boolean calculateElementSizes(@Nonnull final MMGraphics gfx, @Nullable final MindMap model, @Nonnull final MindMapPanelConfig cfg) { boolean result = false; final Topic root = model == null ? null : model.getRoot(); if (root != null && model != null) { model.resetPayload(); setElementSizesForElementAndChildren(gfx, cfg, root, 0); result = true; } return result; } @Nullable public static Dimension2D layoutModelElements(@Nullable final MindMap model, @Nonnull final MindMapPanelConfig cfg) { Dimension2D result = null; if (model != null) { final Topic rootTopic = model.getRoot(); if (rootTopic != null) { final AbstractElement root = (AbstractElement) rootTopic.getPayload(); if (root != null) { root.alignElementAndChildren(cfg, true, 0, 0); result = root.getBlockSize(); } } } return result; } protected static void moveDiagram(@Nullable final MindMap model, final double deltaX, final double deltaY) { if (model != null) { final Topic root = model.getRoot(); if (root != null) { final AbstractElement element = (AbstractElement) root.getPayload(); if (element != null) { element.moveWholeTreeBranchCoordinates(deltaX, deltaY); } } } } private void changeSizeOfComponent(@Nullable final Dimension size, final boolean doNotificationThatRealigned) { if (size != null) { setMinimumSize(size); setPreferredSize(size); if (doNotificationThatRealigned) { for (final MindMapListener l : this.mindMapListeners) { l.onMindMapModelRealigned(this, size); } } } } @Nullable public static Dimension layoutFullDiagramWithCenteringToPaper(@Nonnull final MMGraphics gfx, @Nonnull final MindMap map, @Nonnull final MindMapPanelConfig cfg, @Nonnull final Dimension2D paperSize) { Dimension resultSize = null; if (calculateElementSizes(gfx, map, cfg)) { Dimension2D rootBlockSize = layoutModelElements(map, cfg); final double paperMargin = cfg.getPaperMargins() * cfg.getScale(); if (rootBlockSize != null) { final ElementRoot rootElement = assertNotNull((ElementRoot) assertNotNull(map.getRoot()).getPayload()); double rootOffsetXInBlock = rootElement.getLeftBlockSize().getWidth(); double rootOffsetYInBlock = (rootBlockSize.getHeight() - rootElement.getBounds().getHeight()) / 2; rootOffsetXInBlock += (paperSize.getWidth() - rootBlockSize.getWidth()) <= paperMargin ? paperMargin : (paperSize.getWidth() - rootBlockSize.getWidth()) / 2; rootOffsetYInBlock += (paperSize.getHeight() - rootBlockSize.getHeight()) <= paperMargin ? paperMargin : (paperSize.getHeight() - rootBlockSize.getHeight()) / 2; moveDiagram(map, rootOffsetXInBlock, rootOffsetYInBlock); resultSize = new Dimension((int) Math.round(rootBlockSize.getWidth() + paperMargin * 2), (int) Math.round(rootBlockSize.getHeight() + paperMargin * 2)); } } return resultSize; } public void updateView(final boolean structureWasChanged) { if (this.lockIfNotDisposed()) { try { invalidate(); revalidate(); if (structureWasChanged) { fireNotificationMindMapChanged(); } repaint(); } finally { this.unlock(); } } } public boolean updateElementsAndSizeForCurrentGraphics(final boolean enforce, final boolean doListenerNotification) { assertSwingDispatchThread(); boolean result = true; if (enforce || !isValid()) { if (lockIfNotDisposed()) { try { final Graphics2D graph = (Graphics2D) getGraphics(); if (graph != null) { final MMGraphics gfx = new MMGraphics2DWrapper(graph); if (calculateElementSizes(gfx, this.model, this.config)) { Dimension pageSize = getSize(); final Container parent = this.getParent(); if (parent != null) { if (parent instanceof JViewport) { pageSize = ((JViewport) parent).getExtentSize(); } } changeSizeOfComponent(layoutFullDiagramWithCenteringToPaper(gfx, this.model, this.config, pageSize), doListenerNotification); result = true; fireNotificationComponentElementsLayouted(graph); } } } finally { unlock(); } } } return result; } @Override public void revalidate() { final Runnable runnable = new Runnable() { @Override public void run() { updateElementsAndSizeForCurrentGraphics(true, true); } }; if (SwingUtilities.isEventDispatchThread()) { runnable.run(); } else { SwingUtilities.invokeLater(runnable); } } public void setErrorText(@Nullable final String text) { if (this.lockIfNotDisposed()) { try { this.errorText = text; repaint(); } finally { this.unlock(); } } } @Nullable public String getErrorText() { return this.errorText; } @Override public boolean isValid() { if (this.lockIfNotDisposed()) { try { return isModelValid(this.model); } finally { this.unlock(); } } return false; } @Override public boolean isValidateRoot() { return true; } @Override public void invalidate() { if (lockIfNotDisposed()) { try { super.invalidate(); if (this.model != null && this.model.getRoot() != null) { this.model.resetPayload(); } } finally { this.unlock(); } } } private static void drawErrorText(@Nonnull final Graphics2D gfx, @Nonnull final Dimension fullSize, @Nonnull final String error) { final Font font = new Font(Font.DIALOG, Font.BOLD, 24); final FontMetrics metrics = gfx.getFontMetrics(font); final Rectangle2D textBounds = metrics.getStringBounds(error, gfx); gfx.setFont(font); gfx.setColor(Color.DARK_GRAY); gfx.fillRect(0, 0, fullSize.width, fullSize.height); final int x = (int) (fullSize.width - textBounds.getWidth()) / 2; final int y = (int) (fullSize.height - textBounds.getHeight()) / 2; gfx.setColor(Color.BLACK); gfx.drawString(error, x + 5, y + 5); gfx.setColor(Color.RED.brighter()); gfx.drawString(error, x, y); } @Override @SuppressWarnings("unchecked") public void paintComponent(@Nonnull final Graphics g) { super.paintComponent(g); if (this.lockIfNotDisposed()) { try { final Graphics2D gfx = (Graphics2D) g.create(); try { final String error = this.errorText; Utils.prepareGraphicsForQuality(gfx); if (error != null) { drawErrorText(gfx, this.getSize(), error); } else { revalidate(); drawOnGraphicsForConfiguration(new MMGraphics2DWrapper(gfx), this.config, this.model, true, this.selectedTopics); drawDestinationElement(gfx, this.config); } paintChildren(g); if (this.draggedElement != null) { this.draggedElement.draw(gfx); } else if (this.mouseDragSelection != null) { gfx.setColor(COLOR_MOUSE_DRAG_SELECTION); gfx.fill(this.mouseDragSelection.asRectangle()); } } finally { gfx.dispose(); } } finally { this.unlock(); } } } @Nullable public AbstractElement findTopicUnderPoint(@Nonnull final Point point) { if (this.lockIfNotDisposed()) { try { AbstractElement result = null; if (this.model != null) { final Topic root = this.model.getRoot(); if (root != null) { final AbstractElement rootWidget = (AbstractElement) root.getPayload(); if (rootWidget != null) { result = rootWidget.findForPoint(point); } } } return result; } finally { this.unlock(); } } return null; } public void removeAllSelection() { if (this.lockIfNotDisposed()) { try { if (!this.selectedTopics.isEmpty()) { try { this.selectedTopics.clear(); fireNotificationSelectionChanged(); } finally { repaint(); } } } finally { this.unlock(); } } } public void focusTo(@Nullable final Topic theTopic) { if (this.lockIfNotDisposed()) { try { if (theTopic != null) { final AbstractElement element = (AbstractElement) theTopic.getPayload(); if (element != null && element instanceof AbstractCollapsableElement) { final AbstractCollapsableElement cel = (AbstractCollapsableElement) element; if (MindMapUtils.ensureVisibility(cel.getModel())) { updateView(true); } } removeAllSelection(); final int[] path = theTopic.getPositionPath(); this.select(this.model.findForPositionPath(path), false); } } finally { this.unlock(); } } } public boolean cloneTopic(@Nullable final Topic topic) { return this.cloneTopic(topic, true); } public boolean cloneTopic(@Nullable final Topic topic, final boolean cloneSubtree) { this.lock(); try { if (topic == null || topic.getTopicLevel() == 0) { return false; } final Topic cloned = this.model.cloneTopic(topic, cloneSubtree); if (cloned != null) { cloned.moveAfter(topic); updateView(true); } return true; } finally { this.unlock(); } } @Nonnull public MindMapPanelConfig getConfiguration() { this.lock(); try { return this.config; } finally { this.unlock(); } } @Nonnull public MindMapPanelController getController() { this.lock(); try { return this.controller; } finally { this.unlock(); } } @Nullable public Topic getFirstSelected() { if (this.lockIfNotDisposed()) { try { return this.selectedTopics.isEmpty() ? null : this.selectedTopics.get(0); } finally { this.unlock(); } } else { return null; } } @Nullable public static Dimension2D calculateSizeOfMapInPixels(@Nonnull final MindMap model, @Nonnull final MindMapPanelConfig cfg, final boolean expandAll) { final MindMap workMap = new MindMap(model, null); workMap.resetPayload(); BufferedImage img = new BufferedImage(32, 32, cfg.isDrawBackground() ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB); Dimension2D blockSize = null; final Graphics2D g = img.createGraphics(); final MMGraphics gfx = new MMGraphics2DWrapper(g); try { Utils.prepareGraphicsForQuality(g); if (calculateElementSizes(gfx, workMap, cfg)) { if (expandAll) { final AbstractElement root = assertNotNull((AbstractElement) assertNotNull(workMap.getRoot()).getPayload()); root.collapseOrExpandAllChildren(false); calculateElementSizes(gfx, workMap, cfg); } blockSize = assertNotNull(layoutModelElements(workMap, cfg)); final double paperMargin = cfg.getPaperMargins() * cfg.getScale(); blockSize.setSize(blockSize.getWidth() + paperMargin * 2, blockSize.getHeight() + paperMargin * 2); } } finally { gfx.dispose(); } return blockSize; } @Nullable public static BufferedImage renderMindMapAsImage(@Nonnull final MindMap model, @Nonnull final MindMapPanelConfig cfg, final boolean expandAll) { final MindMap workMap = new MindMap(model, null); workMap.resetPayload(); if (expandAll) { MindMapUtils.removeCollapseAttr(workMap); } final Dimension2D blockSize = calculateSizeOfMapInPixels(workMap, cfg, expandAll); if (blockSize == null) { return null; } final BufferedImage img = new BufferedImage((int) blockSize.getWidth(), (int) blockSize.getHeight(), BufferedImage.TYPE_INT_ARGB); final Graphics2D g = img.createGraphics(); final MMGraphics gfx = new MMGraphics2DWrapper(g); try { Utils.prepareGraphicsForQuality(g); gfx.setClip(0, 0, img.getWidth(), img.getHeight()); layoutFullDiagramWithCenteringToPaper(gfx, workMap, cfg, blockSize); drawOnGraphicsForConfiguration(gfx, cfg, workMap, false, null); } finally { gfx.dispose(); } return img; } public boolean isLocked() { return this.panelLocker == null ? false : this.panelLocker.isLocked(); } /** * Try lock the panel if it is not disposed. * * @return true if the panel is locked successfully, false if the panel has been disposed. */ public boolean lockIfNotDisposed() { boolean result = false; if (this.panelLocker != null) { this.panelLocker.lock(); if (this.disposed.get()) { this.panelLocker.unlock(); } else { result = true; } } return result; } /** * Lock the panel. * * @return the panel * @throws IllegalStateException it will be thrown if the panel is disposed */ @Nonnull public MindMapPanel lock() { if (this.panelLocker != null) { this.panelLocker.lock(); if (this.isDisposed()) { this.panelLocker.unlock(); throw new IllegalStateException("Mind map has been already disposed!"); } } return this; } /** * Unlock the panel. it will not throw any exception if the panel is disposed. * * @throws AssertionError if the panel is locked by another thread */ public void unlock() { if (this.panelLocker != null) { Assertions.assertTrue("Panel must be held by the current thread", this.panelLocker.isHeldByCurrentThread()); this.panelLocker.unlock(); } } @Override public void lostOwnership(@Nonnull final Clipboard clipboard, @Nonnull final Transferable contents) { } /** * Create transferable topic list in system clipboard. * * @param cut true shows that remove topics after placing into clipboard * @param topics topics to be placed into clipboard, if there are successors and ancestors then successors will be removed * @return true if topic array is not empty and operation completed successfully, false otherwise * * @since 1.3.1 */ public boolean copyTopicsToClipboard(final boolean cut, @Nonnull @MustNotContainNull final Topic ... topics){ boolean result = false; if (this.lockIfNotDisposed()) { try { if (topics.length>0){ final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(new MMDTopicsTransferable(topics), this); if (cut){ deleteTopics(true, ensureNoRootInArray(topics)); } result = true; } } finally { this.unlock(); } } return result; } @Nonnull @MustNotContainNull private static Topic [] ensureNoRootInArray(@Nonnull @MustNotContainNull final Topic ... topics) { final List<Topic> buffer = new ArrayList<Topic>(topics.length); for(final Topic t : topics){ if (!t.isRoot()) buffer.add(t); } return buffer.toArray(new Topic[buffer.size()]); } /** * Paste topics from clipboard to currently selected ones. * * @return true if there detected topic list in clipboard and these topics added to selected ones, false otherwise * @since 1.3.1 */ public boolean pasteTopicsFromClipboard() { boolean result = false; if (this.lockIfNotDisposed()) { try { final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); if (clipboard.isDataFlavorAvailable(MMDTopicsTransferable.MMD_DATA_FLAVOR)){ try{ final NBMindMapTopicsContainer container = (NBMindMapTopicsContainer) clipboard.getData(MMDTopicsTransferable.MMD_DATA_FLAVOR); if (container!=null && !container.isEmpty()){ final Topic [] selected = this.getSelectedTopics(); if (selected.length>0){ for(final Topic s : selected){ for(final Topic t : container.getTopics()) { final Topic newTopic = new Topic(this.model,t,true); newTopic.removeExtra(Extra.ExtraType.TOPIC); newTopic.moveToNewParent(s); MindMapUtils.ensureVisibility(newTopic); } } } fireNotificationMindMapChanged(); invalidate(); repaint(); } }catch(final Exception ex){ LOGGER.error("Can't get clipboard data", ex); } } } finally { this.unlock(); } } return result; } public boolean isDisposed() { return this.disposed.get(); } public void dispose() { if (this.lockIfNotDisposed()) { try { if (this.disposed.compareAndSet(false, true)) { this.weakTable.clear(); this.selectedTopics.clear(); this.mindMapListeners.clear(); for (final PanelAwarePlugin p : MindMapPluginRegistry.getInstance().findFor(PanelAwarePlugin.class)) { p.onPanelDispose(this); } } } finally { this.unlock(); } } } }