Java tutorial
/* * 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.UIComponentFactoryService; 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.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.common.utils.Assertions; public class MindMapPanel extends JPanel { public static final long serialVersionUID = 2783412123454232L; public static final String ATTR_SHOW_JUMPS = "showJumps"; private static final Logger LOGGER = LoggerFactory.getLogger(MindMapPanel.class); private static final UIComponentFactory UI_COMPO_FACTORY = UIComponentFactoryService.findInstance(); private final MindMapPanelController controller; private static final int ALL_SUPPORTED_MODIFIERS = KeyEvent.SHIFT_MASK | KeyEvent.ALT_MASK | KeyEvent.META_MASK | KeyEvent.CTRL_MASK; public static class DraggedElement { public enum Modifier { NONE, MAKE_JUMP; } private final AbstractElement element; private final Image prerenderedImage; private final Point mousePointerOffset; private final Point currentPosition; private final DraggedElement.Modifier modifier; public DraggedElement(final AbstractElement element, final MindMapPanelConfig cfg, final Point mousePointerOffset, final DraggedElement.Modifier modifier) { this.element = element; this.prerenderedImage = Utils.renderWithTransparency(0.65f, element, cfg); this.mousePointerOffset = mousePointerOffset; this.currentPosition = new Point(); this.modifier = modifier; } public DraggedElement.Modifier getModifier() { return this.modifier; } public boolean isPositionInside() { return this.element.getBounds().contains(this.currentPosition); } public AbstractElement getElement() { return this.element; } public void updatePosition(final Point point) { this.currentPosition.setLocation(point); } public Point getPosition() { return this.currentPosition; } 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; } public Image getImage() { return this.prerenderedImage; } public void draw(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 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(final MindMapPanelController controller) { super(null); this.textEditorPanel.setLayout(new BorderLayout(0, 0)); this.controller = controller; this.config = new MindMapPanelConfig(controller.provideConfigForMindMapPanel(this), 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(final KeyEvent e) { 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; } } @Override public void keyTyped(final KeyEvent e) { if (e.getKeyChar() == KeyEvent.VK_ENTER) { if ((e.getModifiers() & ALL_SUPPORTED_MODIFIERS) == 0) { e.consume(); endEdit(true); } else { e.consume(); textEditor.insert("\n", textEditor.getCaretPosition()); //NOI18N } } } @Override public void keyReleased(final KeyEvent e) { 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(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); } } }); } } } } }); this.textEditor.getDocument().addDocumentListener(new DocumentListener() { private void updateEditorPanelSize(final Dimension newSize) { 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(); } @Override public void insertUpdate(DocumentEvent e) { updateEditorPanelSize(textEditor.getPreferredSize()); } @Override public void removeUpdate(DocumentEvent e) { updateEditorPanelSize(textEditor.getPreferredSize()); } @Override public void changedUpdate(DocumentEvent e) { updateEditorPanelSize(textEditor.getPreferredSize()); } }); this.textEditorPanel.add(this.textEditor, BorderLayout.CENTER); super.setOpaque(true); final KeyAdapter keyAdapter = new KeyAdapter() { @Override public void keyTyped(KeyEvent e) { if (config.isKeyEvent(MindMapPanelConfig.KEY_ADD_CHILD_AND_START_EDIT, e)) { if (!selectedTopics.isEmpty()) { makeNewChildAndStartEdit(selectedTopics.get(0), null); } } else if (config.isKeyEvent(MindMapPanelConfig.KEY_ADD_SIBLING_AND_START_EDIT, e)) { 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)) { if (!hasSelectedTopics()) { select(getModel().getRoot(), false); } else if (hasOnlyTopicSelected()) { startEdit((AbstractElement) selectedTopics.get(0).getPayload()); } } } @Override public void keyReleased(final KeyEvent e) { if (config.isKeyEvent(MindMapPanelConfig.KEY_DELETE_TOPIC, e)) { e.consume(); deleteSelectedTopics(); } 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)) { e.consume(); processMoveFocusByKey(e); } } }; this.setFocusTraversalKeysEnabled(false); final MindMapPanel theInstance = this; final MouseAdapter adapter = new MouseAdapter() { @Override public void mouseEntered(final MouseEvent e) { setCursor(Cursor.getDefaultCursor()); } @Override public void mouseMoved(final MouseEvent e) { 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()); setCursor(part == ElementPart.ICONS || part == ElementPart.COLLAPSATOR ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : Cursor.getDefaultCursor()); if (part == ElementPart.ICONS) { final Extra<?> extra = element.getIconBlock().findExtraForPoint( e.getPoint().getX() - element.getBounds().getX(), e.getPoint().getY() - element.getBounds().getY()); if (extra != null) { setToolTipText(makeHtmlTooltipForExtra(extra)); } else { setToolTipText(null); } } else { setToolTipText(null); } } } @Override public void mousePressed(final MouseEvent e) { 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); } } @Override public void mouseReleased(final MouseEvent e) { 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(); } } @Override public void mouseDragged(final MouseEvent e) { 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()) { 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; } } @Override public void mouseWheelMoved(final MouseWheelEvent e) { 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); setScale( Math.max(0.3d, Math.min(getScale() + (SCALE_STEP * -e.getWheelRotation()), 10.0d))); updateView(false); e.consume(); } else { sendToParent(e); } } } @Override public void mouseClicked(final MouseEvent e) { if (!controller.isMouseClickProcessingAllowed(theInstance)) { return; } mouseDragSelection = null; draggedElement = null; MindMap theMap = model; AbstractElement element = null; if (theMap != null) { element = findTopicUnderPoint(e.getPoint()); } if (element != null) { final ElementPart part = element.findPartForPoint(e.getPoint()); if (part == ElementPart.COLLAPSATOR) { removeAllSelection(); if (element.isCollapsed()) { ((AbstractCollapsableElement) element).setCollapse(false); if ((e.getModifiers() & KeyEvent.CTRL_MASK) != 0) { ((AbstractCollapsableElement) element).collapseAllFirstLevelChildren(); } } else { ((AbstractCollapsableElement) element).setCollapse(true); } invalidate(); fireNotificationMindMapChanged(); repaint(); } else if (part != ElementPart.ICONS && e.getClickCount() > 1) { startEdit(element); } else if (part == ElementPart.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.getClickCount(), extra); } } else { if (!e.isControlDown()) { // only removeAllSelection(); select(element.getModel(), false); } else // group if (selectedTopics.isEmpty()) { select(element.getModel(), false); } else { select(element.getModel(), true); } } } } }; addMouseWheelListener(adapter); addMouseListener(adapter); addMouseMotionListener(adapter); addKeyListener(keyAdapter); this.textEditorPanel.setVisible(false); this.add(this.textEditorPanel); } private String makeHtmlTooltipForExtra(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() { 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(); } 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(final AbstractElement destination, final Point dropPoint) { int result = DRAG_POSITION_UNKNOWN; 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(); if (bounds != null && dropPoint != null) { 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(final DraggedElement draggedElement, 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; final AbstractElement destParent = destination.getParent(); 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(destParent.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(final AWTEvent evt) { final Container parent = this.getParent(); if (parent != null) { parent.dispatchEvent(evt); } } private void processMoveFocusByKey(final KeyEvent key) { if (hasOnlyTopicSelected()) { final AbstractElement current = (AbstractElement) this.selectedTopics.get(0).getPayload(); if (current == null) { return; } AbstractElement nextFocused = null; boolean modelChanged = false; if (current.isMoveable()) { boolean processFirstChild = false; if (config.isKeyEvent(MindMapPanelConfig.KEY_FOCUS_MOVE_LEFT, key)) { if (current.isLeftDirection()) { processFirstChild = true; } else { nextFocused = (AbstractElement) current.getModel().getParent().getPayload(); } } else if (config.isKeyEvent(MindMapPanelConfig.KEY_FOCUS_MOVE_RIGHT, key)) { if (current.isLeftDirection()) { nextFocused = (AbstractElement) current.getModel().getParent().getPayload(); } else { processFirstChild = true; } } else { final boolean buttonUp = config.isKeyEventDetected(key, MindMapPanelConfig.KEY_FOCUS_MOVE_UP); final boolean firstLevel = current.getClass() == ElementLevelFirst.class; final boolean currentLeft = AbstractCollapsableElement.isLeftSidedTopic(current.getModel()); final TopicChecker checker = new TopicChecker() { @Override public boolean check(final Topic topic) { if (!firstLevel) { return true; } else if (currentLeft) { return AbstractCollapsableElement.isLeftSidedTopic(topic); } else { return !AbstractCollapsableElement.isLeftSidedTopic(topic); } } }; final Topic topic = buttonUp ? current.getModel().findPrev(checker) : current.getModel().findNext(checker); nextFocused = topic == null ? null : (AbstractElement) topic.getPayload(); } if (processFirstChild) { if (current.hasChildren()) { if (current.isCollapsed()) { ((AbstractCollapsableElement) current).setCollapse(false); modelChanged = true; } nextFocused = (AbstractElement) (current.getModel().getChildren().get(0)).getPayload(); } } } else if (config.isKeyEvent(MindMapPanelConfig.KEY_FOCUS_MOVE_LEFT, key)) { for (final Topic t : current.getModel().getChildren()) { final AbstractElement e = (AbstractElement) t.getPayload(); if (e != null && e.isLeftDirection()) { nextFocused = e; break; } } } else if (config.isKeyEvent(MindMapPanelConfig.KEY_FOCUS_MOVE_RIGHT, key)) { for (final Topic t : current.getModel().getChildren()) { final AbstractElement e = (AbstractElement) t.getPayload(); if (e != null && !e.isLeftDirection()) { nextFocused = e; break; } } } if (nextFocused != null) { removeAllSelection(); select(nextFocused.getModel(), false); } if (modelChanged) { invalidate(); fireNotificationMindMapChanged(); } } } private void ensureVisibility(final AbstractElement e) { fireNotificationEnsureTopicVisibility(e.getModel()); } private boolean hasActiveEditor() { return this.elementUnderEdit != null; } public boolean isShowJumps() { return Boolean.parseBoolean(this.model.getAttribute(ATTR_SHOW_JUMPS)); } public void setShowJumps(final boolean flag) { this.model.setAttribute(ATTR_SHOW_JUMPS, flag ? "true" : null); repaint(); fireNotificationMindMapChanged(); } public void makeNewChildAndStartEdit(final Topic parent, final Topic baseTopic) { if (parent != null) { final Topic currentSelected = getFirstSelected(); this.pathToPrevTopicBeforeEdit = currentSelected == null ? null : currentSelected.getPositionPath(); removeAllSelection(); final Topic newTopic = parent.makeChild("", 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 = (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()); } } 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 fireNotificationClickOnExtra(final Topic topic, final int clicks, final Extra<?> extra) { for (final MindMapListener l : this.mindMapListeners) { l.onClickOnExtra(this, clicks, topic, extra); } } protected void fireNotificationEnsureTopicVisibility(final Topic topic) { for (final MindMapListener l : this.mindMapListeners) { l.onEnsureVisibilityOfTopic(this, topic); } } public void deleteTopics(final Topic... topics) { endEdit(false); removeAllSelection(); boolean allowed = true; for (final MindMapListener l : this.mindMapListeners) { allowed &= l.allowedRemovingOfTopics(this, topics); } if (allowed) { for (final Topic t : topics) { this.model.removeTopic(t); } updateView(true); } } public void collapseOrExpandAll(final boolean collapse) { endEdit(false); removeAllSelection(); if (this.model.getRoot() != null) { final AbstractElement root = (AbstractElement) this.model.getRoot().getPayload(); if (root != null && root.collapseOrExpandAllChildren(collapse)) { updateView(true); } } } public void deleteSelectedTopics() { if (!this.selectedTopics.isEmpty()) { deleteTopics(this.selectedTopics.toArray(new Topic[this.selectedTopics.size()])); } } public boolean hasSelectedTopics() { return !this.selectedTopics.isEmpty(); } public boolean hasOnlyTopicSelected() { return this.selectedTopics.size() == 1; } public void removeFromSelection(final Topic t) { if (this.selectedTopics.contains(t)) { if (this.selectedTopics.remove(t)) { fireNotificationSelectionChanged(); } repaint(); } } public void select(final Topic t, final boolean removeIfPresented) { 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); } } } public Topic[] getSelectedTopics() { return this.selectedTopics.toArray(new Topic[this.selectedTopics.size()]); } public void updateEditorAfterResizing() { 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(); } } public void hideEditor() { this.textEditorPanel.setVisible(false); this.elementUnderEdit = null; } public void endEdit(final boolean commit) { 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 { this.elementUnderEdit = null; this.textEditorPanel.setVisible(false); this.requestFocus(); } } public void startEdit(final AbstractElement element) { 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(); } } private void findDestinationElementForDragged() { if (this.draggedElement != null) { final AbstractElement root = (AbstractElement) this.model.getRoot().getPayload(); this.destinationElement = root.findNearestOpenedTopicToPoint(this.draggedElement.getElement(), this.draggedElement.getPosition()); } else { this.destinationElement = null; } } protected void processPopUp(final Point point, final AbstractElement elementUnderMouse) { 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(final PopupMenuEvent e) { theInstance.mouseDragSelection = null; theInstance.popupMenuActive = true; } @Override public void popupMenuWillBecomeInvisible(final PopupMenuEvent e) { theInstance.mouseDragSelection = null; theInstance.popupMenuActive = false; } @Override public void popupMenuCanceled(final PopupMenuEvent e) { theInstance.mouseDragSelection = null; theInstance.popupMenuActive = false; } }); menu.show(this, point.x, point.y); } } } public void addMindMapListener(@Nonnull final MindMapListener l) { this.mindMapListeners.add(Assertions.assertNotNull(l)); } public void removeMindMapListener(@Nonnull final MindMapListener l) { this.mindMapListeners.remove(Assertions.assertNotNull(l)); } public void setModel(final MindMap model) { 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(); this.model = 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(); } @Override public boolean isFocusable() { return true; } public MindMap getModel() { return this.model; } public void setScale(final double zoom) { this.config.setScale(zoom); } public double getScale() { return this.config.getScale(); } private static void drawBackground(final Graphics2D g, final MindMapPanelConfig cfg) { final Rectangle clipBounds = g.getClipBounds(); if (cfg.isDrawBackground()) { g.setColor(cfg.getPaperColor()); g.fillRect(clipBounds.x, clipBounds.y, clipBounds.width, clipBounds.height); 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; g.setColor(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); } 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); } } } } private static boolean isModelValid(final MindMap map) { return map == null || map.getRoot() == null ? true : map.getRoot().getPayload() != null; } public static void drawOnGraphicsForConfiguration(final Graphics2D g, final MindMapPanelConfig config, final MindMap map, final boolean drawSelection, final List<Topic> selectedTopics) { drawBackground(g, config); drawTopics(g, config, map); if (drawSelection && selectedTopics != null && !selectedTopics.isEmpty()) { drawSelection(g, config, selectedTopics); } } private void drawDestinationElement(final Graphics2D g, 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(final Graphics2D g, final MindMapPanelConfig cfg, final List<Topic> selectedTopics) { if (selectedTopics != null && !selectedTopics.isEmpty()) { g.setColor(cfg.getSelectLineColor()); final Stroke dashed = new BasicStroke(cfg.safeScaleFloatValue(cfg.getSelectLineWidth(), 0.1f), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[] { cfg.safeScaleFloatValue(1.0f, 0.1f), cfg.safeScaleFloatValue(4.0f, 0.1f) }, 0); g.setStroke(dashed); 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); } } } } private static void drawTopics(final Graphics2D g, final MindMapPanelConfig cfg, 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(final Graphics2D gfx, final MindMap map, final MindMapPanelConfig cfg) { final List<Topic> allTopicsWithJumps = map.findAllTopicsForExtraType(Extra.ExtraType.TOPIC); final float scaledSize = cfg.safeScaleFloatValue(cfg.getJumpLinkWidth(), 0.1f); final Stroke lineStroke = new BasicStroke(scaledSize, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 0, new float[] { scaledSize, scaledSize * 3.0f }, 0); final Stroke arrowStroke = new BasicStroke(cfg.safeScaleFloatValue(cfg.getJumpLinkWidth() * 1.0f, 0.3f)); gfx.setColor(cfg.getJumpLinkColor()); final float arrowSize = cfg.safeScaleFloatValue(10.0f * cfg.getJumpLinkWidth(), 0.2f); for (Topic src : allTopicsWithJumps) { final ExtraTopic extra = (ExtraTopic) src.getExtras().get(Extra.ExtraType.TOPIC); src = MindMapUtils.isHidden(src) ? MindMapUtils.findFirstVisibleAncestor(src) : src; final AbstractElement srcElement = (AbstractElement) src.getPayload(); 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)) { final Rectangle2D srcRect = srcElement.getBounds(); final Rectangle2D dstRect = dstElement.getBounds(); drawArrowToDestination(gfx, srcRect, dstRect, lineStroke, arrowStroke, arrowSize); } } } } } } private static void drawArrowToDestination(final Graphics2D gfx, final Rectangle2D start, final Rectangle2D destination, final Stroke lineStroke, final Stroke arrowStroke, final float arrowSize) { final double startx = start.getCenterX(); final double starty = start.getCenterY(); final Point2D arrowPoint = Utils.findRectEdgeIntersection(destination, startx, starty); if (arrowPoint != null) { gfx.setStroke(arrowStroke); 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.fill(polygon); gfx.setStroke(lineStroke); gfx.drawLine((int) startx, (int) starty, (int) (arrowPoint.getX() + cx), (int) (arrowPoint.getY() + cy)); } } private static void drawTopicTree(final Graphics2D gfx, final Topic topic, final MindMapPanelConfig cfg) { paintTopic(gfx, topic, cfg); final AbstractElement w = (AbstractElement) topic.getPayload(); if (w.isCollapsed()) { return; } for (final Topic t : topic.getChildren()) { drawTopicTree(gfx, t, cfg); } } private static void paintTopic(final Graphics2D gfx, final Topic topic, final MindMapPanelConfig cfg) { ((AbstractElement) topic.getPayload()).doPaint(gfx, cfg, true); } private static void setElementSizesForElementAndChildren(final Graphics2D gfx, final MindMapPanelConfig cfg, 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(final Graphics2D gfx, final MindMap model, final MindMapPanelConfig cfg) { boolean result = false; final Topic root = model == null ? null : model.getRoot(); if (root != null) { if (gfx.getFontMetrics() != null) { model.resetPayload(); setElementSizesForElementAndChildren(gfx, cfg, root, 0); result = true; } else { root.setPayload(null); } } return result; } public static Dimension2D layoutModelElements(final MindMap model, final MindMapPanelConfig cfg) { Dimension2D result = null; final AbstractElement root = model == null ? null : model.getRoot() == null ? null : (AbstractElement) model.getRoot().getPayload(); if (root != null) { root.alignElementAndChildren(cfg, true, 0, 0); result = root.getBlockSize(); } return result; } protected static void moveDiagram(final MindMap model, final double deltaX, final double deltaY) { final AbstractElement root = model == null ? null : model.getRoot() == null ? null : (AbstractElement) model.getRoot().getPayload(); if (root != null) { root.moveWholeTreeBranchCoordinates(deltaX, deltaY); } } private void changeSizeOfComponentWithNotification(final Dimension size) { if (size != null) { setMinimumSize(size); setPreferredSize(size); for (final MindMapListener l : this.mindMapListeners) { l.onMindMapModelRealigned(this, size); } } } public static Dimension layoutFullDiagramWithCenteringToPaper(final Graphics2D gfx, final MindMap map, final MindMapPanelConfig cfg, 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) { if (paperSize != null) { final ElementRoot rootElement = (ElementRoot) 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) { invalidate(); revalidate(); if (structureWasChanged) { fireNotificationMindMapChanged(); } repaint(); } @Override public void revalidate() { final Runnable runnable = new Runnable() { @Override public void run() { if (!isValid()) { final Graphics2D gfx = (Graphics2D) getGraphics(); if (gfx != null && calculateElementSizes(gfx, model, config)) { changeSizeOfComponentWithNotification( layoutFullDiagramWithCenteringToPaper(gfx, model, config, getSize())); } } } }; if (SwingUtilities.isEventDispatchThread()) { runnable.run(); } else { SwingUtilities.invokeLater(runnable); } } public void setErrorText(final String text) { this.errorText = text; repaint(); } public String getErrorText() { return this.errorText; } @Override public boolean isValid() { return isModelValid(this.model); } @Override public boolean isValidateRoot() { return true; } @Override public void invalidate() { super.invalidate(); if (this.model != null && this.model.getRoot() != null) { this.model.resetPayload(); } } private static void drawErrorText(final Graphics2D gfx, final Dimension fullSize, 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(final Graphics g) { 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(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(); } } public AbstractElement findTopicUnderPoint(final Point point) { 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; } public void removeAllSelection() { if (!this.selectedTopics.isEmpty()) { try { this.selectedTopics.clear(); fireNotificationSelectionChanged(); } finally { repaint(); } } } public void focusTo(final Topic theTopic) { 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); } } public boolean cloneTopic(final Topic topic) { if (topic == null || topic.getTopicLevel() == 0) { return false; } final Boolean cloneFullTree = this.controller == null ? Boolean.TRUE : this.controller.getDialogProvider(this).msgConfirmYesNoCancel( BUNDLE.getString("MindMapPanel.titleCloneTopicRequest"), BUNDLE.getString("MindMapPanel.cloneTopicSubtreeRequestMsg")); if (cloneFullTree == null) { return false; } final Topic cloned = this.model.cloneTopic(topic, cloneFullTree); if (cloned != null) { cloned.moveAfter(topic); updateView(true); } return true; } public MindMapPanelConfig getConfiguration() { return this.config; } public MindMapPanelController getController() { return this.controller; } public Topic getFirstSelected() { return this.selectedTopics.isEmpty() ? null : this.selectedTopics.get(0); } public static Dimension2D calculateSizeOfMapInPixels(final MindMap model, 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; Graphics2D gfx = img.createGraphics(); try { Utils.prepareGraphicsForQuality(gfx); if (calculateElementSizes(gfx, workMap, cfg)) { if (expandAll) { final AbstractElement root = (AbstractElement) workMap.getRoot().getPayload(); root.collapseOrExpandAllChildren(false); calculateElementSizes(gfx, workMap, cfg); } blockSize = 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; } public static BufferedImage renderMindMapAsImage(final MindMap model, 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 gfx = img.createGraphics(); try { Utils.prepareGraphicsForQuality(gfx); gfx.setClip(0, 0, img.getWidth(), img.getHeight()); layoutFullDiagramWithCenteringToPaper(gfx, workMap, cfg, blockSize); drawOnGraphicsForConfiguration(gfx, cfg, workMap, false, null); } finally { gfx.dispose(); } return img; } }