/* * Freeplane - mind map editor * Copyright (C) 2008 Joerg Mueller, Daniel Polansky, Christian Foltin, Dimitry Polivaev * * This file is modified by Dimitry Polivaev in 2008. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.freeplane.view.swing.map; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.util.Collection; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JToolTip; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.border.Border; import javax.swing.text.JTextComponent; import org.freeplane.core.resources.ResourceController; import org.freeplane.core.ui.components.FreeplaneMenuBar; import org.freeplane.core.ui.components.MultipleImage; import org.freeplane.core.ui.components.UITools; import org.freeplane.core.util.HtmlUtils; import org.freeplane.core.util.LogUtils; import org.freeplane.core.util.TextUtils; import org.freeplane.features.icon.IconController; import org.freeplane.features.icon.MindIcon; import org.freeplane.features.icon.UIIcon; import org.freeplane.features.link.LinkController; import org.freeplane.features.link.NodeLinks; import org.freeplane.features.map.HideChildSubtree; import org.freeplane.features.map.MapController; import org.freeplane.features.map.NodeModel; import org.freeplane.features.mode.ModeController; import org.freeplane.features.nodelocation.LocationModel; import org.freeplane.features.nodestyle.NodeStyleController; import org.freeplane.features.styles.MapViewLayout; import org.freeplane.features.text.HighlightedTransformedObject; import org.freeplane.features.text.TextController; /** * Base class for all node views. */ public abstract class MainView extends ZoomableLabel { private static final int FOLDING_CIRCLE_WIDTH = 16; private static final String USE_COMMON_OUT_POINT_FOR_ROOT_NODE_STRING = "use_common_out_point_for_root_node"; public static boolean USE_COMMON_OUT_POINT_FOR_ROOT_NODE = ResourceController.getResourceController().getBooleanProperty(USE_COMMON_OUT_POINT_FOR_ROOT_NODE_STRING); static Dimension maximumSize = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); static Dimension minimumSize = new Dimension(0,0); /** * */ private static final long serialVersionUID = 1L; protected int isDraggedOver = NodeView.DRAGGED_OVER_NO; private boolean isShortened; private TextModificationState textModified = TextModificationState.NONE; private MouseArea mouseArea = MouseArea.OUT; private static final int DRAG_OVAL_WIDTH = 10; boolean isShortened() { return isShortened; } MainView() { setAlignmentX(Component.CENTER_ALIGNMENT); setHorizontalAlignment(SwingConstants.LEFT); setVerticalAlignment(SwingConstants.CENTER); setHorizontalTextPosition(SwingConstants.TRAILING); setVerticalTextPosition(JLabel.TOP); } protected void convertPointFromMap(final Point p) { UITools.convertPointFromAncestor(getMap(), p, this); } protected void convertPointToMap(final Point p) { UITools.convertPointToAncestor(this, p, getMap()); } public boolean dropAsSibling(final double xCoord) { if(dropLeft(xCoord)) return ! isInVerticalRegion(xCoord, 2. / 3); else return isInVerticalRegion(xCoord, 1. / 3); } /** @return true if should be on the left, false otherwise. */ public boolean dropLeft(final double xCoord) { /* here it is the same as me. */ return getNodeView().isLeft(); } public int getDeltaX() { final NodeView nodeView = getNodeView(); final NodeModel model = nodeView.getModel(); if (nodeView.getMap().getModeController().getMapController().isFolded(model) && nodeView.isLeft()) { return getZoomedFoldingSymbolHalfWidth() * 3; } else return 0; } /** get y coordinate including folding symbol */ public int getDeltaY() { return 0; } public int getDraggedOver() { return isDraggedOver; } public abstract Point getLeftPoint(); /** get height including folding symbol */ protected int getMainViewHeightWithFoldingMark() { return getHeight(); } /** get width including folding symbol */ protected int getMainViewWidthWithFoldingMark() { int width = getWidth(); final NodeView nodeView = getNodeView(); final NodeModel model = nodeView.getModel(); if (nodeView.getMap().getModeController().getMapController().isFolded(model)) { width += getZoomedFoldingSymbolHalfWidth() * 3; } return width; } @Override public Dimension getMaximumSize() { return MainView.maximumSize; } @Override public Dimension getMinimumSize() { return MainView.minimumSize; } public abstract Point getRightPoint(); public abstract String getShape(); int getZoomedFoldingSymbolHalfWidth() { return getNodeView().getZoomedFoldingSymbolHalfWidth(); } public boolean isInFollowLinkRegion(final double xCoord) { final NodeView nodeView = getNodeView(); final NodeModel model = nodeView.getModel(); if (NodeLinks.getValidLink(model) == null) return false; Rectangle iconR = ((ZoomableLabelUI)getUI()).getIconR(this); return xCoord >= iconR.x && xCoord < iconR.x + iconR.width; } /** * Determines whether or not the xCoord is in the part p of the node: if * node is on the left: part [1-p,1] if node is on the right: part[ 0,p] of * the total width. */ public boolean isInVerticalRegion(final double xCoord, final double p) { return xCoord < getSize().width * p; } @Override final public void paint(Graphics g){ final PaintingMode paintingMode = getMap().getPaintingMode(); if(!PaintingMode.SELECTED_NODES.equals(paintingMode) && !PaintingMode.NODES.equals(paintingMode)) return; final NodeView nodeView = getNodeView(); final boolean selected = nodeView.isSelected(); if(paintingMode.equals(PaintingMode.SELECTED_NODES) == selected) super.paint(g); } protected void paintBackground(final Graphics2D graphics, final Color color) { graphics.setColor(color); graphics.fillRect(0, 0, getWidth() - 1, getHeight() - 1); } public void paintDragOver(final Graphics2D graphics) { if (isDraggedOver == NodeView.DRAGGED_OVER_SON) { paintDragOverSon(graphics); } if (isDraggedOver == NodeView.DRAGGED_OVER_SIBLING) { paintDragOverSibling(graphics); } } private void paintDragOverSibling(final Graphics2D graphics) { graphics.setPaint(new GradientPaint(0, getHeight() * 3 / 5, getMap().getBackground(), 0, getHeight() / 5, NodeView.dragColor)); graphics.fillRect(0, 0, getWidth() - 1, getHeight() - 1); } private void paintDragOverSon(final Graphics2D graphics) { if (getNodeView().isLeft()) { graphics.setPaint(new GradientPaint(getWidth() * 3 / 4, 0, getMap().getBackground(), getWidth() / 4, 0, NodeView.dragColor)); graphics.fillRect(0, 0, getWidth() * 3 / 4, getHeight() - 1); } else { graphics.setPaint(new GradientPaint(getWidth() / 4, 0, getMap().getBackground(), getWidth() * 3 / 4, 0, NodeView.dragColor)); graphics.fillRect(getWidth() / 4, 0, getWidth() - 1, getHeight() - 1); } } public FoldingMark foldingMarkType(MapController mapController, NodeModel node) { if (mapController.isFolded(node) && (node.isVisible() || node.getFilterInfo().isAncestor())) { return FoldingMark.ITSELF_FOLDED; } for (final NodeModel child : mapController.childrenUnfolded(node)) { if (child.isVisible() && child.containsExtension(HideChildSubtree.class)) { return FoldingMark.ITSELF_FOLDED; } } for (final NodeModel child : mapController.childrenUnfolded(node)) { if (!child.isVisible() && !FoldingMark.UNFOLDED.equals(foldingMarkType(mapController, child))) { return FoldingMark.UNVISIBLE_CHILDREN_FOLDED; } } return FoldingMark.UNFOLDED; } void paintDecoration(final NodeView nodeView, final Graphics2D g) { drawModificationRect(g); paintDragRectangle(g); paintFoldingMark(nodeView, g); if (isShortened()) { final int size = getZoomedFoldingSymbolHalfWidth(); int width = size * 7 / 3; int x = nodeView.isLeft() ? getWidth() : 0 - width; int height = size * 5 / 3; int y = (getHeight() - height) / 2; FoldingMark.SHORTENED.draw(g, nodeView, new Rectangle(x, y, width, height)); } } protected void paintFoldingMark(final NodeView nodeView, final Graphics2D g) { if (! hasChildren()) return; final MapView map = getMap(); final MapController mapController = map.getModeController().getMapController(); final NodeModel node = nodeView.getModel(); final FoldingMark markType = foldingMarkType(mapController, node); Point mousePosition = null; try { mousePosition = getMousePosition(); } catch (Exception e) { } if(mousePosition != null && ! map.isPrinting()){ final int width = Math.max(FOLDING_CIRCLE_WIDTH, getZoomedFoldingSymbolHalfWidth() * 2); final Point p = getNodeView().isLeft() ? getLeftPoint() : getRightPoint(); if(p.y + width/2 > getHeight()) p.y = getHeight() - width; else p.y -= width/2; if(nodeView.isLeft()) p.x -= width; final FoldingMark foldingCircle; if(markType.equals(FoldingMark.UNFOLDED)) { if(mapController.hasHiddenChildren(node)) foldingCircle = FoldingMark.FOLDING_CIRCLE_HIDDEN_CHILD; else foldingCircle = FoldingMark.FOLDING_CIRCLE_UNFOLDED; } else{ foldingCircle = FoldingMark.FOLDING_CIRCLE_FOLDED; } foldingCircle.draw(g, nodeView, new Rectangle(p.x, p.y, width, width)); } else{ final int halfWidth = getZoomedFoldingSymbolHalfWidth(); final Point p = getNodeView().isLeft() ? getLeftPoint() : getRightPoint(); if (p.x <= 0) { p.x -= halfWidth; } else { p.x += halfWidth; } markType.draw(g, nodeView, new Rectangle(p.x - halfWidth, p.y-halfWidth, halfWidth*2, halfWidth*2)); } } private void paintDragRectangle(final Graphics g) { if (! MouseArea.MOTION.equals(mouseArea)) return; final Graphics2D g2 = (Graphics2D) g; final Object renderingHint = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING); final MapView parent = (MapView) SwingUtilities.getAncestorOfClass(MapView.class, this); parent.getModeController().getController().getViewController().setEdgesRenderingHint(g2); final Color color = g2.getColor(); NodeView movedView = getNodeView(); Rectangle r = getDragRectangle(); if (movedView .isFree()) { g2.setColor(Color.BLUE); g.fillOval(r.x, r.y, r.width - 1, r.height - 1); } else if (LocationModel.getModel(movedView.getModel()).getHGap() <= 0) { g2.setColor(Color.RED); g.fillOval(r.x, r.y, r.width- 1, r.height- 1); } g2.setColor(Color.BLACK); g.drawOval(r.x, r.y, r.width- 1, r.height- 1); g2.setColor(color); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, renderingHint); } public Rectangle getDragRectangle() { final int size = getDraggingWidth(); Rectangle r; if(getNodeView().isLeft()) r = new Rectangle(getWidth(), -size, size, getHeight() + size * 2); else r = new Rectangle(-size, -size, size, getHeight() + size * 2); return r; } private void drawModificationRect(Graphics g) { final Color color = g.getColor(); if(TextModificationState.HIGHLIGHT.equals(textModified)){ final boolean markTransformedText = TextController.isMarkTransformedTextSet(); if(! markTransformedText) return; g.setColor(Color.GREEN); } else if(TextModificationState.FAILURE.equals(textModified)){ g.setColor(Color.RED); } else{ return; } g.drawRect(-1, -1, getWidth() + 2, getHeight() + 2); g.setColor(color); } public void paintBackgound(final Graphics2D g) { final Color color; if (getNodeView().useSelectionColors()) { color = getNodeView().getSelectedColor(); paintBackground(g, color); } else { color = getNodeView().getTextBackground(); } paintBackground(g, color); } /* * (non-Javadoc) * @see javax.swing.JComponent#processKeyBinding(javax.swing.KeyStroke, * java.awt.event.KeyEvent, int, boolean) */ @Override protected boolean processKeyBinding(final KeyStroke ks, final KeyEvent e, final int condition, final boolean pressed) { if (super.processKeyBinding(ks, e, condition, pressed)) { return true; } final MapView mapView = (MapView) SwingUtilities.getAncestorOfClass(MapView.class, this); final FreeplaneMenuBar freeplaneMenuBar = mapView.getModeController().getController().getViewController() .getFreeplaneMenuBar(); return !freeplaneMenuBar.isVisible() && freeplaneMenuBar.processKeyBinding(ks, e, JComponent.WHEN_IN_FOCUSED_WINDOW, pressed); } public void setDraggedOver(final int draggedOver) { isDraggedOver = draggedOver; } public void setDraggedOver(final Point p) { setDraggedOver((dropAsSibling(p.getX())) ? NodeView.DRAGGED_OVER_SIBLING : NodeView.DRAGGED_OVER_SON); } public void updateFont(final NodeView node) { final Font font = NodeStyleController.getController(node.getMap().getModeController()).getFont(node.getModel()); setFont(UITools.scale(font)); } void updateIcons(final NodeView node) { // setHorizontalTextPosition(node.isLeft() ? SwingConstants.LEADING : SwingConstants.TRAILING); final MultipleImage iconImages = new MultipleImage(); /* fc, 06.10.2003: images? */ final NodeModel model = node.getModel(); for (final UIIcon icon : IconController.getController().getStateIcons(model)) { iconImages.addImage(icon.getIcon()); } final ModeController modeController = getNodeView().getMap().getModeController(); final Collection<MindIcon> icons = IconController.getController(modeController).getIcons(model); for (final MindIcon myIcon : icons) { iconImages.addImage(myIcon.getIcon()); } addOwnIcons(iconImages, model); setIcon((iconImages.getImageCount() > 0 ? iconImages : null)); } private void addOwnIcons(final MultipleImage iconImages, final NodeModel model) { final URI link = NodeLinks.getLink(model); final Icon icon = LinkController.getLinkIcon(link, model); if(icon != null) iconImages.addImage(icon); } void updateTextColor(final NodeView node) { final Color color = NodeStyleController.getController(node.getMap().getModeController()).getColor( node.getModel()); setForeground(color); } public boolean isEdited() { return getComponentCount() == 1 && getComponent(0) instanceof JTextComponent; } static enum TextModificationState{NONE, HIGHLIGHT, FAILURE}; public void updateText(NodeModel nodeModel) { final NodeView nodeView = getNodeView(); if(nodeView == null) return; final ModeController modeController = nodeView.getMap().getModeController(); final TextController textController = TextController.getController(modeController); isShortened = textController.isMinimized(nodeModel); final Object userObject = nodeModel.getUserObject(); Object content = userObject; String text; try { if(isShortened && (content instanceof String)) content = HtmlUtils.htmlToPlain((String) content); final Object obj = textController.getTransformedObject(content, nodeModel, userObject); if(nodeView.isSelected()){ nodeView.getMap().getModeController().getController().getViewController().addObjectTypeInfo(obj); } text = obj.toString(); textModified = obj instanceof HighlightedTransformedObject ? TextModificationState.HIGHLIGHT : TextModificationState.NONE; } catch (Throwable e) { LogUtils.warn(e.getMessage(), e); text = TextUtils.format("MainView.errorUpdateText", String.valueOf(content), e.getLocalizedMessage()); textModified = TextModificationState.FAILURE; } if(isShortened){ text = shortenText(text); } text = convertTextToHtmlLink(text, nodeModel); updateText(text); } private String convertTextToHtmlLink(String text, NodeModel node) { URI link = NodeLinks.getLink(node); if(link == null || "menuitem".equals(link.getScheme()) || ! LinkController.getController().formatNodeAsHyperlink(node)) return text; if (HtmlUtils.isHtmlNode(text)) text = HtmlUtils.htmlToPlain(text); StringBuilder sb = new StringBuilder("<html><body><a href=\""); sb.append(link.toString()); sb.append("\">"); final String xmlEscapedText = HtmlUtils.toHTMLEscapedText(text); sb.append(xmlEscapedText); sb.append("</a></body></html>"); return sb.toString(); } private String shortenText(String longText) { String text; if(HtmlUtils.isHtmlNode(longText)){ text = HtmlUtils.htmlToPlain(longText).trim(); } else{ text = longText; } int length = text.length(); final int eolPosition = text.indexOf('\n'); final int maxShortenedNodeWidth = ResourceController.getResourceController().getIntProperty("max_shortened_text_length"); if(eolPosition == -1 || eolPosition >= length || eolPosition >= maxShortenedNodeWidth){ if(length <= maxShortenedNodeWidth){ return text; } length = maxShortenedNodeWidth; } else{ length = eolPosition; } text = text.substring(0, length); return text; } @Override public JToolTip createToolTip() { NodeTooltip tip = new NodeTooltip(); tip.setComponent(this); final URL url = getMap().getModel().getURL(); if (url != null) { tip.setBase(url); } else { try { tip.setBase(new URL("file: ")); } catch (MalformedURLException e) { } } return tip; } @Override public void setBorder(Border border) { } static public enum ConnectorLocation{LEFT, RIGHT, TOP, BOTTOM, CENTER}; public ConnectorLocation getConnectorLocation(Point relativeLocation) { if(relativeLocation.x > getWidth()) return ConnectorLocation.RIGHT; if(relativeLocation.x < 0) return ConnectorLocation.LEFT; if(relativeLocation.y > getHeight()) return ConnectorLocation.BOTTOM; if(relativeLocation.y <0) return ConnectorLocation.TOP; return ConnectorLocation.CENTER; } public Point getConnectorPoint(Point relativeLocation) { if(relativeLocation.x > getWidth()) return getRightPoint(); if(relativeLocation.x < 0) return getLeftPoint(); if(relativeLocation.y > getHeight()){ final Point bottomPoint = getBottomPoint(); bottomPoint.y = getNodeView().getContent().getHeight(); return bottomPoint; } if(relativeLocation.y <0) return getTopPoint(); return getCenterPoint(); } private Point getCenterPoint() { return new Point(getWidth()/2, getHeight()/2); } public Point getTopPoint() { return new Point(getWidth()/2, 0); } public Point getBottomPoint() { return new Point(getWidth()/2, getHeight()); } @Override public String getToolTipText() { final String toolTipText = super.getToolTipText(); if(toolTipText != null) return toolTipText; return createToolTipText(); } private String createToolTipText() { final NodeView nodeView = getNodeView(); final ModeController modeController = nodeView.getMap().getModeController(); final NodeModel node = nodeView.getModel(); return modeController.createToolTip(node, this); } @Override public String getToolTipText(MouseEvent event) { final String toolTipText = super.getToolTipText(event); if(toolTipText != null) return toolTipText; return createToolTipText(); } @Override public boolean contains(int x, int y) { final Point p = new Point(x, y); return isInFoldingRegion(p) || isInDragRegion(p)|| super.contains(x, y); } public boolean isInDragRegion(Point p) { if (p.y >= 0 && p.y < getHeight()){ final NodeView nodeView = getNodeView(); if (MapViewLayout.OUTLINE.equals(nodeView.getMap().getLayoutType())) return false; final int draggingWidth = getDraggingWidth(); if(nodeView.isLeft()){ final int width = getWidth(); return p.x >= width && p.x < width + draggingWidth; } else return p.x >= -draggingWidth && p.x < 0; } return false; } public boolean isInFoldingRegion(Point p) { if (hasChildren() && p.y >= 0 && p.y < getHeight()) { final boolean isLeft = getNodeView().isLeft(); final int width = Math.max(FOLDING_CIRCLE_WIDTH, getZoomedFoldingSymbolHalfWidth() * 2); if (isLeft) { final int maxX = 0; return p.x >= -width && p.x < maxX; } else { final int minX = getWidth(); return p.x >= minX && p.x < (getWidth() + width); } } else return false; } private boolean hasChildren() { return getNodeView().getModel().hasChildren(); } public MouseArea getMouseArea() { return mouseArea; } public MouseArea whichMouseArea(Point point) { final int x = point.x; if(isInDragRegion(point)) return MouseArea.MOTION; if(isInFoldingRegion(point)) return MouseArea.FOLDING; if(isInFollowLinkRegion(x)) return MouseArea.LINK; return MouseArea.DEFAULT; } public void setMouseArea(MouseArea mouseArea) { if(mouseArea.equals(this.mouseArea)) return; final boolean repaintDraggingRectangle = isVisible() && (mouseArea.equals(MouseArea.MOTION) || this.mouseArea.equals(MouseArea.MOTION) ); final boolean repaintFoldingRectangle = isVisible() && (mouseArea.equals(MouseArea.OUT) || mouseArea.equals(MouseArea.FOLDING) || this.mouseArea.equals(MouseArea.OUT) || this.mouseArea.equals(MouseArea.FOLDING)); this.mouseArea = mouseArea; if(repaintDraggingRectangle) paintDraggingRectangleImmediately(); if(repaintFoldingRectangle) paintFoldingRectangleImmediately(); } private void paintFoldingRectangleImmediately() { final int zoomedFoldingSymbolHalfWidth = getZoomedFoldingSymbolHalfWidth(); final int width = Math.max(FOLDING_CIRCLE_WIDTH, zoomedFoldingSymbolHalfWidth * 2); final NodeView nodeView = getNodeView(); int height; final int x, y; if (nodeView.isLeft()){ x = -width; } else{ x = getWidth(); } if(FOLDING_CIRCLE_WIDTH >= getHeight()){ height = FOLDING_CIRCLE_WIDTH; y = getHeight() - FOLDING_CIRCLE_WIDTH; } else{ height = getHeight(); y = 0; } height += zoomedFoldingSymbolHalfWidth; final Rectangle foldingRectangle = new Rectangle(x-4, y-4, width+8, height+8); final MapView map = nodeView.getMap(); UITools.convertRectangleToAncestor(this, foldingRectangle, map); map.paintImmediately(foldingRectangle); } private void paintDraggingRectangleImmediately() { final Rectangle dragRectangle = getDragRectangle(); paintDecorationImmediately(dragRectangle); } private void paintDecorationImmediately(final Rectangle rectangle) { final MapView map = getMap(); UITools.convertRectangleToAncestor(this, rectangle, map); map.paintImmediately(rectangle); } @Override public void setVisible(boolean visible) { super.setVisible(visible); if(! visible) setMouseArea(MouseArea.DEFAULT); } private int getDraggingWidth() { return getNodeView().getZoomed(DRAG_OVAL_WIDTH); } }