/* * 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.nbmindmap.nb.editor; import com.igormaznitsa.mindmap.model.*; import com.igormaznitsa.mindmap.model.logger.Logger; import com.igormaznitsa.mindmap.model.logger.LoggerFactory; import com.igormaznitsa.mindmap.print.MMDPrint; import com.igormaznitsa.nbmindmap.utils.NbUtils; import com.igormaznitsa.mindmap.swing.panel.DialogProvider; import com.igormaznitsa.mindmap.swing.panel.MindMapListener; import com.igormaznitsa.mindmap.swing.panel.MindMapPanel; import com.igormaznitsa.mindmap.swing.panel.MindMapPanelConfig; import com.igormaznitsa.mindmap.swing.panel.MindMapPanelController; import static com.igormaznitsa.mindmap.swing.panel.StandardTopicAttribute.*; import static com.igormaznitsa.mindmap.swing.panel.utils.Utils.assertSwingDispatchThread; import com.igormaznitsa.mindmap.swing.panel.ui.AbstractElement; import com.igormaznitsa.mindmap.swing.panel.ui.ElementPart; 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 com.igormaznitsa.nbmindmap.nb.print.PrintPageAdapter; import com.igormaznitsa.nbmindmap.nb.swing.ColorAttributePanel; import com.igormaznitsa.nbmindmap.nb.swing.AboutPanel; import com.igormaznitsa.nbmindmap.nb.swing.ColorChooserButton; import com.igormaznitsa.nbmindmap.nb.swing.FileEditPanel; import com.igormaznitsa.nbmindmap.nb.swing.MindMapTreePanel; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.datatransfer.DataFlavor; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.geom.Rectangle2D; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.util.Date; import java.util.List; import java.util.Properties; import java.util.ResourceBundle; import javax.swing.*; import org.netbeans.api.actions.Openable; import org.netbeans.api.options.OptionsDisplayer; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectManager; import org.netbeans.api.project.ui.OpenProjects; import org.netbeans.core.spi.multiview.CloseOperationState; import org.netbeans.core.spi.multiview.MultiViewElement; import org.netbeans.core.spi.multiview.MultiViewElementCallback; import org.netbeans.spi.print.PrintPage; import org.netbeans.spi.print.PrintProvider; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.loaders.DataFolder; import org.openide.loaders.DataObject; import org.openide.nodes.Node; import org.openide.text.CloneableEditor; import org.openide.util.ImageUtilities; import org.openide.util.Lookup; import org.openide.util.WeakSet; import org.openide.util.lookup.Lookups; import org.openide.util.lookup.ProxyLookup; import org.openide.windows.TopComponent; import static org.openide.windows.TopComponent.PERSISTENCE_NEVER; import java.awt.Graphics2D; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.FlavorEvent; import java.awt.datatransfer.FlavorListener; import java.awt.event.ActionEvent; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.KeyEvent; import java.net.URI; import java.net.URISyntaxException; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.swing.text.DefaultEditorKit; import org.openide.actions.CopyAction; import org.openide.actions.CutAction; import org.openide.actions.PasteAction; import org.openide.util.actions.SystemAction; import org.openide.util.datatransfer.ClipboardEvent; import org.openide.util.datatransfer.ClipboardListener; import org.openide.util.datatransfer.ExClipboard; import com.igormaznitsa.meta.annotation.MustNotContainNull; import com.igormaznitsa.mindmap.ide.commons.DnDUtils; import com.igormaznitsa.mindmap.plugins.processors.ExtraFilePlugin; import com.igormaznitsa.mindmap.plugins.processors.ExtraJumpPlugin; import com.igormaznitsa.mindmap.plugins.processors.ExtraURIPlugin; import com.igormaznitsa.mindmap.plugins.misc.AboutPlugin; import com.igormaznitsa.mindmap.plugins.misc.OptionsPlugin; import com.igormaznitsa.mindmap.plugins.processors.ExtraNotePlugin; import com.igormaznitsa.mindmap.plugins.tools.ChangeColorPlugin; import com.igormaznitsa.mindmap.plugins.api.PopUpMenuItemPlugin; import com.igormaznitsa.mindmap.plugins.api.CustomJob; import com.igormaznitsa.mindmap.swing.panel.MMDTopicsTransferable; import com.igormaznitsa.mindmap.swing.panel.utils.KeyEventType; import com.igormaznitsa.nbmindmap.utils.DialogProviderManager; @MultiViewElement.Registration( displayName = "#MMDGraphEditor.displayName", mimeType = MMDDataObject.MIME, persistenceType = PERSISTENCE_NEVER, iconBase = "com/igormaznitsa/nbmindmap/icons/logo/logo16.png", preferredID = MMDGraphEditor.ID, position = 1 ) public final class MMDGraphEditor extends CloneableEditor implements AdjustmentListener, MindMapController, PrintProvider, MultiViewElement, MindMapListener, DropTargetListener, MindMapPanelController, FlavorListener, ClipboardListener { private static final long serialVersionUID = -8776707243607267446L; private static final ResourceBundle BUNDLE = java.util.ResourceBundle.getBundle("com/igormaznitsa/nbmindmap/i18n/Bundle"); private static final Logger LOGGER = LoggerFactory.getLogger(MMDGraphEditor.class); private static final UIComponentFactory UI_COMPO_FACTORY = UIComponentFactoryProvider.findInstance(); private static final String FILELINK_ATTR_OPEN_IN_SYSTEM = "useSystem"; //NOI18N public static final String ID = "mmd-graph-editor"; //NOI18N private volatile boolean rootToCentre = true; private MultiViewElementCallback callback; private final MMDEditorSupport editorSupport; private final JScrollPane mainScrollPane; private final MindMapPanel mindMapPanel; private boolean dragAcceptableType = false; private final JToolBar toolBar = UI_COMPO_FACTORY.makeToolBar(); private static final WeakSet<MMDGraphEditor> ALL_EDITORS = new WeakSet<MMDGraphEditor>(); private final Action actionCopy = new AbstractAction() { private static final long serialVersionUID = 935382113400815225L; @Override public void actionPerformed(@Nonnull final ActionEvent e) { mindMapPanel.copyTopicsToClipboard(false, MindMapUtils.removeSuccessorsAndDuplications(mindMapPanel.getSelectedTopics())); } @Override public boolean isEnabled() { return mindMapPanel.hasSelectedTopics(); } }; private final Action actionCut = new AbstractAction() { private static final long serialVersionUID = 935382113400815225L; @Override public void actionPerformed(@Nonnull final ActionEvent e) { mindMapPanel.copyTopicsToClipboard(true, MindMapUtils.removeSuccessorsAndDuplications(mindMapPanel.getSelectedTopics())); } @Override public boolean isEnabled() { return mindMapPanel.hasSelectedTopics(); } }; private final Action actionPaste = new AbstractAction() { private static final long serialVersionUID = -5644390861803492172L; @Override public void actionPerformed(@Nonnull final ActionEvent e) { mindMapPanel.pasteTopicsFromClipboard(); } }; public MMDGraphEditor() { this(Lookup.getDefault().lookup(MMDEditorSupport.class)); } public MMDGraphEditor(final MMDEditorSupport support) { super(support); this.editorSupport = support; this.mainScrollPane = UI_COMPO_FACTORY.makeScrollPane(); this.mindMapPanel = new MindMapPanel(this); this.mindMapPanel.addMindMapListener(this); this.mindMapPanel.setDropTarget(new DropTarget(this.mindMapPanel, this)); this.mainScrollPane.setViewportView(this.mindMapPanel); this.mainScrollPane.setWheelScrollingEnabled(true); this.mainScrollPane.setAutoscrolls(true); this.setLayout(new BorderLayout(0, 0)); this.add(this.mainScrollPane, BorderLayout.CENTER); this.mindMapPanel.addComponentListener(new ComponentAdapter() { @Override public void componentResized(final ComponentEvent e) { processEditorResizing(mindMapPanel); } }); updateName(); synchronized (ALL_EDITORS) { ALL_EDITORS.add(this); } this.actionCut.setEnabled(false); this.actionCopy.setEnabled(false); this.actionPaste.setEnabled(false); this.mainScrollPane.getHorizontalScrollBar().addAdjustmentListener(this); this.mainScrollPane.getVerticalScrollBar().addAdjustmentListener(this); } @Override public void adjustmentValueChanged(@Nonnull final AdjustmentEvent e) { this.mindMapPanel.repaint(); } private void registerCustomCCPActions(@Nonnull final JComponent component) { final ActionMap actionMap = component.getActionMap(); actionMap.put(DefaultEditorKit.cutAction, this.actionCut); actionMap.put(DefaultEditorKit.copyAction, this.actionCopy); actionMap.put(DefaultEditorKit.pasteAction, this.actionPaste); } private void registerAsClipboardListener() { final Clipboard clipboard = NbUtils.findClipboard(); if (clipboard instanceof ExClipboard) { ((ExClipboard) clipboard).addClipboardListener(this); } else { clipboard.addFlavorListener(this); } processClipboardChange(clipboard); } private void unregisterFromClipboardListeners() { final Clipboard clipboard = NbUtils.findClipboard(); if (clipboard instanceof ExClipboard) { ((ExClipboard) clipboard).removeClipboardListener(this); } else { clipboard.removeFlavorListener(this); } } private void restoreSystemCCPActions(@Nonnull final JComponent component) { final ActionMap actionMap = component.getActionMap(); actionMap.put(DefaultEditorKit.copyAction, SystemAction.get(CopyAction.class)); actionMap.put(DefaultEditorKit.cutAction, SystemAction.get(CutAction.class)); actionMap.put(DefaultEditorKit.pasteAction, SystemAction.get(PasteAction.class)); } @Override public void flavorsChanged(@Nonnull final FlavorEvent e) { processClipboardChange((Clipboard) e.getSource()); } @Override public void clipboardChanged(@Nonnull final ClipboardEvent ev) { processClipboardChange(ev.getClipboard()); } private boolean processClipboardChange(@Nonnull final Clipboard clipboard) { final boolean result = clipboard.isDataFlavorAvailable(MMDTopicsTransferable.MMD_DATA_FLAVOR); Utils.safeSwingCall(new Runnable() { @Override public void run() { actionPaste.setEnabled(mindMapPanel.hasSelectedTopics() && result); } }); return result; } public boolean isPanelDisposed() { return this.mindMapPanel.isDisposed(); } @Override public JComponent getVisualRepresentation() { return this; } @Override public void componentActivated() { this.editorSupport.onEditorActivated(); if (this.rootToCentre) { this.rootToCentre = false; SwingUtilities.invokeLater(new Runnable() { @Override public void run() { final Topic root = mindMapPanel.getModel().getRoot(); if (mindMapPanel.hasSelectedTopics()) { topicToCentre(mindMapPanel.getFirstSelected()); } else if (root != null) { mindMapPanel.select(root, false); mindMapPanel.getModel().resetPayload(); topicToCentre(root); } } }); } registerCustomCCPActions(this); registerAsClipboardListener(); super.componentActivated(); } @Override public void componentClosed() { try { this.mindMapPanel.dispose(); LOGGER.info("MMD Editor is disposed : " + this.mindMapPanel.toString()); } finally { final Clipboard clipboard = NbUtils.findClipboard(); if (clipboard instanceof ExClipboard) { ((ExClipboard) clipboard).removeClipboardListener(this); } else { clipboard.removeFlavorListener(this); } super.componentClosed(); } } @Override public void componentOpened() { super.componentOpened(); } @Override public void componentShowing() { updateModel(); } private void copyNameToCallbackTopComponent() { final MultiViewElementCallback c = this.callback; if (c != null) { final TopComponent tc = c.getTopComponent(); if (tc != null) { tc.setHtmlDisplayName(this.getHtmlDisplayName()); tc.setDisplayName(this.getDisplayName()); tc.setName(this.getName()); tc.setToolTipText(this.getToolTipText()); } } if (this.editorSupport != null) { this.editorSupport.updateTitles(); } } @Override public void componentDeactivated() { restoreSystemCCPActions(this); unregisterFromClipboardListeners(); super.componentDeactivated(); } @Override public void componentHidden() { super.componentHidden(); } private void updateModel() { if (!this.mindMapPanel.isDisposed()) { this.mindMapPanel.hideEditor(); final String text = this.editorSupport.getDocumentText(); if (text == null) { this.mindMapPanel.setErrorText(BUNDLE.getString("MMDGraphEditor.updateModel.cantLoadDocument")); } else { try { this.mindMapPanel.setModel(new MindMap(this, new StringReader(text)), false); } catch (IllegalArgumentException ex) { LOGGER.warn("Can't detect mind map"); //NOI18N this.mindMapPanel.setErrorText(BUNDLE.getString("MMDGraphEditor.updateModel.cantDetectMMap")); } catch (IOException ex) { LOGGER.error("Can't parse mind map text", ex); //NOI18N this.mindMapPanel.setErrorText(BUNDLE.getString("MMDGraphEditor.updateModel.cantParseDoc")); } } } } @Override public void setMultiViewCallback(final MultiViewElementCallback callback) { this.callback = callback; updateName(); copyNameToCallbackTopComponent(); } @Override public boolean allowedRemovingOfTopics(final MindMapPanel source, final Topic[] topics) { boolean topicsNotImportant = true; for (final Topic t : topics) { topicsNotImportant &= t.canBeLost(); if (!topicsNotImportant) { break; } } final boolean result; if (topicsNotImportant) { result = true; } else { result = NbUtils.msgConfirmYesNo(BUNDLE.getString("MMDGraphEditor.allowedRemovingOfTopics,title"), String.format(BUNDLE.getString("MMDGraphEditor.allowedRemovingOfTopics.message"), topics.length)); } return result; } @Override public void onMindMapModelChanged(final MindMapPanel source) { try { final StringWriter writer = new StringWriter(16384); final MindMap theMap = this.mindMapPanel.getModel(); MindMapUtils.removeCollapseAttributeFromTopicsWithoutChildren(theMap); theMap.write(writer); this.editorSupport.replaceDocumentText(writer.toString()); this.editorSupport.getDataObject().setModified(true); } catch (Exception ex) { LOGGER.error("Can't get document text", ex); //NOI18N } finally { copyNameToCallbackTopComponent(); } } @Override public void onComponentElementsLayouted(@Nonnull final MindMapPanel source, @Nonnull final Graphics2D g) { } @Override public void onMindMapModelRealigned(final MindMapPanel source, final Dimension coveredAreaSize) { this.mainScrollPane.getViewport().revalidate(); } public boolean topicToCentre(@Nullable Topic topic) { boolean result = false; assertSwingDispatchThread(); if (topic != null) { // to make it sure that topic is from the same model topic = this.mindMapPanel.getModel().findForPositionPath(topic.getPositionPath()); if (topic != null) { AbstractElement element = (AbstractElement) topic.getPayload(); if (element == null && this.mindMapPanel.updateElementsAndSizeForCurrentGraphics(true, true)) { element = (AbstractElement) topic.getPayload(); this.mainScrollPane.getViewport().doLayout(); } if (element != null) { final Rectangle2D bounds = element.getBounds(); final Dimension viewPortSize = mainScrollPane.getViewport().getExtentSize(); final int x = Math.max(0, (int) Math.round(bounds.getX() - (viewPortSize.getWidth() - bounds.getWidth()) / 2)); final int y = Math.max(0, (int) Math.round(bounds.getY() - (viewPortSize.getHeight() - bounds.getHeight()) / 2)); this.mainScrollPane.getViewport().setViewPosition(new Point(x, y)); result = true; } } } return result; } @Override public void onTopicCollapsatorClick(@Nonnull final MindMapPanel source, @Nonnull final Topic topic, final boolean beforeAction) { if (!beforeAction) { this.mindMapPanel.getModel().resetPayload(); topicToCentre(topic); } } @Override public void onNonConsumedKeyEvent(@Nonnull final MindMapPanel source, @Nonnull final KeyEvent e, @Nonnull final KeyEventType type) { if (type == KeyEventType.PRESSED && e.getModifiers() == 0 && (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_RIGHT || e.getKeyCode() == KeyEvent.VK_DOWN)) { e.consume(); } } @Override public void onEnsureVisibilityOfTopic(@Nonnull final MindMapPanel source, @Nonnull final Topic topic) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { final AbstractElement element = (AbstractElement) topic.getPayload(); if (element == null) { return; } final Rectangle2D orig = element.getBounds(); final int GAP = 30; final Rectangle bounds = orig.getBounds(); bounds.setLocation(Math.max(0, bounds.x - GAP), Math.max(0, bounds.y - GAP)); bounds.setSize(bounds.width + GAP * 2, bounds.height + GAP * 2); final JViewport viewport = mainScrollPane.getViewport(); final Rectangle visible = viewport.getViewRect(); if (visible.contains(bounds)) { return; } bounds.setLocation(bounds.x - visible.x, bounds.y - visible.y); viewport.scrollRectToVisible(bounds); } }); } @Override @SuppressWarnings("unchecked") public void onClickOnExtra(final MindMapPanel source, final int modifiers, final int clicks, final Topic topic, final Extra<?> extra) { if (clicks > 1) { switch (extra.getType()) { case FILE: { final FileObject fileObj; final MMapURI uri = (MMapURI) extra.getValue(); final File theFile = FileUtil.normalizeFile(uri.asFile(this.editorSupport.getProjectDirectory())); try { fileObj = FileUtil.toFileObject(theFile); if (fileObj == null) { LOGGER.warn("Can't find FileObject for " + theFile); if (theFile.exists()) { NbUtils.openInSystemViewer(theFile); } else { NbUtils.msgError(String.format(BUNDLE.getString("MMDGraphEditor.onClickExtra.errorCanfFindFile"), theFile.getAbsolutePath())); } return; } } catch (Exception ex) { LOGGER.error("onClickExtra#FILE", ex); //NOI18N NbUtils.msgError(String.format(BUNDLE.getString("MMDGraphEditor.onClickOnExtra.msgWrongFilePath"), uri.toString())); return; } try { if (Boolean.parseBoolean(uri.getParameters().getProperty(FILELINK_ATTR_OPEN_IN_SYSTEM, "false"))) { //NOI18N NbUtils.openInSystemViewer(theFile); } else { Project projectOwner; if (fileObj.isFolder()) { projectOwner = FileOwnerQuery.getOwner(fileObj); if (projectOwner != null) { final DataObject dataObj = DataObject.find(fileObj); if (dataObj instanceof DataFolder) { final ProjectManager manager = ProjectManager.getDefault(); if (manager.isProject(fileObj)) { final Project project = manager.findProject(fileObj); final OpenProjects openProjects = OpenProjects.getDefault(); NbUtils.SelectIn browserType = NbUtils.SelectIn.PROJECTS; if (!openProjects.isProjectOpen(project)) { openProjects.open(new Project[]{project}, false); } else { final FileObject mapProjectFolder = getProjectFolderAsFileObject(); if (mapProjectFolder != null && mapProjectFolder.equals(fileObj)) { browserType = NbUtils.SelectIn.FAVORITES; } } if (browserType.select(this, project.getProjectDirectory())) { return; } } } } NbUtils.SelectIn selector = NbUtils.SelectIn.FAVORITES; final ProjectManager manager = ProjectManager.getDefault(); final OpenProjects openManager = OpenProjects.getDefault(); if (manager.isProject(fileObj)) { final Project project = manager.findProject(fileObj); if (project != null && !openManager.isProjectOpen(project)) { openManager.open(new Project[]{project}, false, true); } } else if (projectOwner != null) { openManager.open(new Project[]{projectOwner}, false); if (!NbUtils.isInProjectKnowledgeFolder(projectOwner, fileObj)) { if (NbUtils.isFileInProjectScope(projectOwner, fileObj)) { selector = NbUtils.SelectIn.PROJECTS; } else if (FileUtil.isParentOf(projectOwner.getProjectDirectory(), fileObj)) { selector = NbUtils.SelectIn.FILES; } } } if (!selector.select(this, fileObj)) { NbUtils.openInSystemViewer(theFile); } } else { final DataObject dobj = DataObject.find(fileObj); final Openable openable = dobj.getLookup().lookup(Openable.class); if (openable != null) { openable.open(); } NbUtils.SelectIn.PROJECTS.select(this, fileObj); } } } catch (Exception ex) { LOGGER.error("Cant't find or open data object", ex); //NOI18N NbUtils.msgError(String.format(BUNDLE.getString("MMDGraphEditor.onClickExtra.cantFindFileObj"), uri.toString())); } } break; case LINK: { final MMapURI uri = ((ExtraLink) extra).getValue(); if (!NbUtils.browseURI(uri.asURI(), NbUtils.getPreferences().getBoolean("useInsideBrowser", false))) { //NOI18N NbUtils.msgError(String.format(BUNDLE.getString("MMDGraphEditor.onClickOnExtra.msgCantBrowse"), uri.toString())); } } break; case NOTE: { editTextForTopic(topic); } break; case TOPIC: { final Topic theTopic = this.mindMapPanel.getModel().findTopicForLink((ExtraTopic) extra); if (theTopic == null) { // not presented NbUtils.msgWarn(BUNDLE.getString("MMDGraphEditor.onClickOnExtra.msgCantFindTopic")); } else { // detected this.mindMapPanel.focusTo(theTopic); } } break; default: throw new Error("Unexpected type " + extra.getType()); //NOI18N } } } @Override public void onChangedSelection(final MindMapPanel source, final Topic[] currentSelectedTopics) { this.actionCopy.setEnabled(currentSelectedTopics.length != 0); this.actionCut.setEnabled(currentSelectedTopics.length != 0); this.actionPaste.setEnabled(currentSelectedTopics.length != 0 && NbUtils.findClipboard().isDataFlavorAvailable(MMDTopicsTransferable.MMD_DATA_FLAVOR)); } private static void processEditorResizing(final MindMapPanel panel) { panel.updateView(false); panel.updateEditorAfterResizing(); } public void updateView() { if (SwingUtilities.isEventDispatchThread()) { this.updateModel(); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { updateModel(); } }); } } @Override public void dragEnter(DropTargetDragEvent dtde) { this.dragAcceptableType = checkDragType(dtde); if (!this.dragAcceptableType) { dtde.rejectDrag(); } } @Override public void dragOver(final DropTargetDragEvent dtde) { if (acceptOrRejectDragging(dtde)) { dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE); } else { dtde.rejectDrag(); } } @Override public Image getIcon() { return ImageUtilities.loadImage("com/igormaznitsa/nbmindmap/icons/logo/logo16.png"); //NOI18N } @Override public void dropActionChanged(DropTargetDragEvent dtde) { } @Override public void dragExit(DropTargetEvent dte) { } @Override public void onScaledByMouse(@Nonnull final MindMapPanel source, @Nonnull final Point mousePoint, final double oldScale, final double newScale, final boolean beforeAction) { if (!beforeAction && Double.compare(oldScale, newScale) != 0) { final JViewport viewport = this.mainScrollPane.getViewport(); final Point viewPos = viewport.getViewPosition(); final int dx = mousePoint.x - viewPos.x; final int dy = mousePoint.y - viewPos.y; final double scaleRelation = newScale / oldScale; final int newMouseX = (int) (Math.round(mousePoint.x * scaleRelation)); final int newMouseY = (int) (Math.round(mousePoint.y * scaleRelation)); viewport.doLayout(); viewport.setViewPosition(new Point(Math.max(0, newMouseX - dx), Math.max(0, newMouseY - dy))); } } @Override public boolean processDropTopicToAnotherTopic(final MindMapPanel source, final Point dropPoint, final Topic draggedTopic, final Topic destinationTopic) { boolean result = false; if (draggedTopic != null && destinationTopic != null && draggedTopic != destinationTopic) { if (destinationTopic.getExtras().containsKey(Extra.ExtraType.TOPIC)) { if (!NbUtils.msgConfirmOkCancel(BUNDLE.getString("MMDGraphEditor.addTopicToElement.confirmTitle"), BUNDLE.getString("MMDGraphEditor.addTopicToElement.confirmMsg"))) { return result; } } final ExtraTopic topicLink = ExtraTopic.makeLinkTo(this.mindMapPanel.getModel(), draggedTopic); destinationTopic.setExtra(topicLink); result = true; } return result; } private void addURItoElement(@Nonnull final URI uri, @Nullable final AbstractElement element) { if (element != null) { final Topic topic = element.getModel(); final MMapURI mmapUri = new MMapURI(uri); if (topic.getExtras().containsKey(Extra.ExtraType.LINK)) { if (!DialogProviderManager.getInstance().getDialogProvider().msgConfirmOkCancel(BUNDLE.getString("MMDGraphEditor.addDataObjectLinkToElement.confirmTitle"), BUNDLE.getString("MMDGraphEditor.addDataObjectLinkToElement.confirmMsg"))) { return; } } topic.setExtra(new ExtraLink(mmapUri)); this.mindMapPanel.invalidate(); this.mindMapPanel.repaint(); onMindMapModelChanged(this.mindMapPanel); } } private void addNoteToElement(@Nonnull final String text, @Nullable final AbstractElement element) { if (element != null) { final Topic topic = element.getModel(); if (topic.getExtras().containsKey(Extra.ExtraType.NOTE)) { if (!DialogProviderManager.getInstance().getDialogProvider().msgConfirmOkCancel(BUNDLE.getString("MMDGraphEditor.addDataObjectTextToElement.confirmTitle"), BUNDLE.getString("MMDGraphEditor.addDataObjectTextToElement.confirmMsg"))) { return; } } topic.setExtra(new ExtraNote(text)); this.mindMapPanel.invalidate(); this.mindMapPanel.repaint(); onMindMapModelChanged(this.mindMapPanel); } } private void addFileToElement(final File theFile, final AbstractElement element) { if (element != null) { final Topic topic = element.getModel(); final MMapURI theURI; if (NbUtils.getPreferences().getBoolean("makeRelativePathToProject", true)) { //NOI18N final File projectFolder = getProjectFolder(); if (theFile.equals(projectFolder)) { theURI = new MMapURI(projectFolder, new File("."), null); } else { theURI = new MMapURI(projectFolder, theFile, null); } } else { theURI = new MMapURI(null, theFile, null); } if (topic.getExtras().containsKey(Extra.ExtraType.FILE)) { if (!NbUtils.msgConfirmOkCancel(BUNDLE.getString("MMDGraphEditor.addDataObjectToElement.confirmTitle"), BUNDLE.getString("MMDGraphEditor.addDataObjectToElement.confirmMsg"))) { return; } } topic.setExtra(new ExtraFile(theURI)); this.mindMapPanel.invalidate(); this.mindMapPanel.repaint(); onMindMapModelChanged(this.mindMapPanel); } } private void addDataObjectToElement(final DataObject dataObject, final AbstractElement element) { addFileToElement(FileUtil.toFile(dataObject.getPrimaryFile()), element); } @Nullable private Object extractDataObjectOrFileFromDnD(@Nonnull final DropTargetDropEvent dtde) throws Exception { DataFlavor dataObjectFlavor = null; DataFlavor nodeObjectFlavor = null; DataFlavor projectObjectFlavor = null; File detectedFileObject = null; for (final DataFlavor df : dtde.getCurrentDataFlavors()) { final Class<?> representation = df.getRepresentationClass(); if (Node.class.isAssignableFrom(representation)) { nodeObjectFlavor = df; break; } else if (DataObject.class.isAssignableFrom(representation)) { dataObjectFlavor = df; break; } else if (Project.class.isAssignableFrom(representation)) { projectObjectFlavor = df; break; } else if (df.isFlavorJavaFileListType()) { try { final List list = (List) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); if (list != null && !list.isEmpty()) { detectedFileObject = (File) list.get(0); } } catch (Exception ex) { LOGGER.error("Can't extract file from DnD", ex); throw ex; } } } DataObject dataObject; if (nodeObjectFlavor != null) { try { final Node theNode = (Node) dtde.getTransferable().getTransferData(nodeObjectFlavor); dataObject = theNode.getLookup().lookup(DataObject.class); if (dataObject == null) { final Project proj = theNode.getLookup().lookup(Project.class); if (proj != null) { dataObject = DataObject.find(proj.getProjectDirectory()); } } } catch (Exception ex) { LOGGER.error("Can't extract node from dragged element", ex); throw ex; } } else if (dataObjectFlavor != null) { try { dataObject = (DataObject) dtde.getTransferable().getTransferData(dataObjectFlavor); } catch (Exception ex) { LOGGER.error("Can't extract data object from dragged element", ex); throw ex; } } else if (projectObjectFlavor != null) { try { dataObject = DataObject.find(((Project) dtde.getTransferable().getTransferData(projectObjectFlavor)).getProjectDirectory()); } catch (Exception ex) { LOGGER.error("Can't extract data object from project", ex); throw ex; } } else { dataObject = null; } return dataObject == null ? detectedFileObject : dataObject; } @Override public void drop(final DropTargetDropEvent dtde) { DataObject detectedDataObject = null; File detectedFileObject = null; String detectedNote = null; URI decodedLink = null; dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); try { final Object dataObjOrFile = extractDataObjectOrFileFromDnD(dtde); if (dataObjOrFile instanceof DataObject) { detectedDataObject = (DataObject) dataObjOrFile; } else { detectedFileObject = (File) dataObjOrFile; } final String detectedLink = DnDUtils.extractDropLink(dtde); detectedNote = DnDUtils.extractDropNote(dtde); if (detectedLink != null) { try { decodedLink = new URI(detectedLink); } catch (final URISyntaxException ex) { decodedLink = null; } } dtde.dropComplete(true); } catch (final Exception ex) { LOGGER.error("Can't extract data from DnD", ex); dtde.dropComplete(false); } final AbstractElement element = this.mindMapPanel.findTopicUnderPoint(dtde.getLocation()); if (detectedDataObject != null) { addDataObjectToElement(detectedDataObject, element); } else if (detectedFileObject != null) { decodedLink = DnDUtils.extractUrlLinkFromFile(detectedFileObject); if (decodedLink != null) { addURItoElement(decodedLink, element); } else { addFileToElement(detectedFileObject, element); } } else if (decodedLink != null) { addURItoElement(decodedLink, element); } else if (detectedNote != null) { addNoteToElement(detectedNote, element); } } @Nullable public FileObject getProjectFolderAsFileObject() { final Project proj = this.editorSupport.getProject(); FileObject result = null; if (proj != null) { result = proj.getProjectDirectory(); } return result; } @Nullable public File getProjectFolder() { final FileObject projectFolder = getProjectFolderAsFileObject(); return projectFolder == null ? null : FileUtil.toFile(projectFolder); } protected boolean acceptOrRejectDragging(final DropTargetDragEvent dtde) { final int dropAction = dtde.getDropAction(); boolean result = false; if (this.dragAcceptableType && (dropAction & DnDConstants.ACTION_COPY_OR_MOVE) != 0 && this.mindMapPanel.findTopicUnderPoint(dtde.getLocation()) != null) { result = true; } return result; } protected static boolean checkDragType(final DropTargetDragEvent dtde) { boolean result = DnDUtils.isFileOrLinkOrText(dtde); if (!result) { for (final DataFlavor flavor : dtde.getCurrentDataFlavors()) { final Class dataClass = flavor.getRepresentationClass(); if (Node.class.isAssignableFrom(dataClass) || DataObject.class.isAssignableFrom(dataClass)) { result = true; break; } } } return result; } @Override public CloseOperationState canCloseElement() { return CloseOperationState.STATE_OK; } @Override public JComponent getToolbarRepresentation() { return this.toolBar; } private void editFileLinkForTopic(final Topic topic) { final ExtraFile file = (ExtraFile) topic.getExtras().get(Extra.ExtraType.FILE); final FileEditPanel.DataContainer path; final File projectFolder = getProjectFolder(); if (file == null) { path = NbUtils.editFilePath(BUNDLE.getString("MMDGraphEditor.editFileLinkForTopic.dlgTitle"), projectFolder, null); } else { final MMapURI uri = file.getValue(); final boolean flagOpenInSystem = Boolean.parseBoolean(uri.getParameters().getProperty(FILELINK_ATTR_OPEN_IN_SYSTEM, "false")); //NOI18N final FileEditPanel.DataContainer origPath; if (uri.isAbsolute()) { origPath = new FileEditPanel.DataContainer(uri.asFile(getProjectFolder()).getAbsolutePath(), flagOpenInSystem); } else if (projectFolder == null) { NbUtils.msgWarn(BUNDLE.getString("MMDGraphEditor.editFileLinkForTopic.warnText")); origPath = new FileEditPanel.DataContainer(uri.asFile(getProjectFolder()).getPath(), flagOpenInSystem); } else { origPath = new FileEditPanel.DataContainer(uri.asFile(projectFolder).getAbsolutePath(), flagOpenInSystem); } path = NbUtils.editFilePath(BUNDLE.getString("MMDGraphEditor.editFileLinkForTopic.addPathTitle"), projectFolder, origPath); } if (path != null) { final boolean changed; if (path.isEmpty()) { changed = topic.removeExtra(Extra.ExtraType.FILE); } else { final Properties props = new Properties(); if (path.isShowWithSystemTool()) { props.put(FILELINK_ATTR_OPEN_IN_SYSTEM, "true"); //NOI18N } final MMapURI fileUri = MMapURI.makeFromFilePath(NbUtils.getPreferences().getBoolean("makeRelativePathToProject", true) ? projectFolder : null, path.getPath(), props); //NOI18N final File theFile = fileUri.asFile(projectFolder); LOGGER.info(String.format("Path %s converted to uri: %s", path.getPath(), fileUri.asString(false, true))); //NOI18N if (theFile.exists()) { topic.setExtra(new ExtraFile(fileUri)); changed = true; } else { NbUtils.msgError(String.format(BUNDLE.getString("MMDGraphEditor.editFileLinkForTopic.errorCantFindFile"), path.getPath())); changed = false; } } if (changed) { this.mindMapPanel.invalidate(); this.mindMapPanel.repaint(); onMindMapModelChanged(this.mindMapPanel); } } } private void editTopicLinkForTopic(final Topic topic) { final ExtraTopic link = (ExtraTopic) topic.getExtras().get(Extra.ExtraType.TOPIC); ExtraTopic result = null; final ExtraTopic remove = new ExtraTopic("_______"); //NOI18N if (link == null) { final MindMapTreePanel treePanel = new MindMapTreePanel(this.mindMapPanel.getModel(), null, true, null); if (NbUtils.plainMessageOkCancel(BUNDLE.getString("MMDGraphEditor.editTopicLinkForTopic.dlgSelectTopicTitle"), treePanel)) { final Topic selected = treePanel.getSelectedTopic(); treePanel.dispose(); if (selected != null) { result = ExtraTopic.makeLinkTo(this.mindMapPanel.getModel(), selected); } else { result = remove; } } } else { final MindMapTreePanel panel = new MindMapTreePanel(this.mindMapPanel.getModel(), link, true, null); if (NbUtils.plainMessageOkCancel(BUNDLE.getString("MMDGraphEditor.editTopicLinkForTopic.dlgEditSelectedTitle"), panel)) { final Topic selected = panel.getSelectedTopic(); if (selected != null) { result = ExtraTopic.makeLinkTo(this.mindMapPanel.getModel(), selected); } else { result = remove; } } } if (result != null) { if (result == remove) { topic.removeExtra(Extra.ExtraType.TOPIC); } else { topic.setExtra(result); } this.mindMapPanel.invalidate(); this.mindMapPanel.repaint(); onMindMapModelChanged(this.mindMapPanel); } } private void editLinkForTopic(final Topic topic) { final ExtraLink link = (ExtraLink) topic.getExtras().get(Extra.ExtraType.LINK); final MMapURI result; if (link == null) { // create new result = NbUtils.editURI(String.format(BUNDLE.getString("MMDGraphEditor.editLinkForTopic.dlgAddURITitle"), Utils.makeShortTextVersion(topic.getText(), 16)), null); } else { // edit result = NbUtils.editURI(String.format(BUNDLE.getString("MMDGraphEditor.editLinkForTopic.dlgEditURITitle"), Utils.makeShortTextVersion(topic.getText(), 16)), link.getValue()); } if (result != null) { if (result == NbUtils.EMPTY_URI) { topic.removeExtra(Extra.ExtraType.LINK); } else { topic.setExtra(new ExtraLink(result)); } this.mindMapPanel.invalidate(); this.mindMapPanel.repaint(); onMindMapModelChanged(this.mindMapPanel); } } private void editTextForTopic(final Topic topic) { final ExtraNote note = (ExtraNote) topic.getExtras().get(Extra.ExtraType.NOTE); final String result; if (note == null) { // create new result = NbUtils.editText(String.format(BUNDLE.getString("MMDGraphEditor.editTextForTopic.dlfAddNoteTitle"), Utils.makeShortTextVersion(topic.getText(), 16)), ""); //NOI18N } else { // edit result = NbUtils.editText(String.format(BUNDLE.getString("MMDGraphEditor.editTextForTopic.dlgEditNoteTitle"), Utils.makeShortTextVersion(topic.getText(), 16)), note.getValue()); } if (result != null) { if (result.isEmpty()) { topic.removeExtra(Extra.ExtraType.NOTE); } else { topic.setExtra(new ExtraNote(result)); } this.mindMapPanel.invalidate(); this.mindMapPanel.repaint(); onMindMapModelChanged(this.mindMapPanel); } } @Override public int getPersistenceType() { return TopComponent.PERSISTENCE_NEVER; } public boolean focusToPath(final boolean enforceVisibility, final int[] positionPath) { this.mindMapPanel.removeAllSelection(); Topic topic = this.mindMapPanel.getModel().findForPositionPath(positionPath); boolean mapChanged = false; if (topic != null) { if (enforceVisibility) { mapChanged = MindMapUtils.ensureVisibility(topic); } else { topic = MindMapUtils.findFirstVisibleAncestor(topic); } if (mapChanged) { this.mindMapPanel.updateView(true); topic = this.mindMapPanel.getModel().findForPositionPath(positionPath); } this.mindMapPanel.select(topic, false); } return mapChanged; } @Override public Lookup getLookup() { return new ProxyLookup(Lookups.singleton(this), super.getLookup()); } @Override public PrintPage[][] getPages(final int paperWidthInPixels, final int paperHeightInPixels, final double pageZoomFactor) { final com.igormaznitsa.mindmap.print.PrintPage[][] pages = new MMDPrint(this.mindMapPanel, paperWidthInPixels, paperHeightInPixels, 1.0d).getPages(); final PrintPage[][] result = new PrintPage[pages.length][]; for (int i = 0; i < pages.length; i++) { result[i] = new PrintPage[pages[i].length]; for (int p = 0; p < pages[i].length; p++) { result[i][p] = new PrintPageAdapter(pages[i][p]); } } return result; } @Override public Date lastModified() { if (this.editorSupport.isModified()) { return new Date(); } else { return this.editorSupport.getDataObject().getPrimaryFile().lastModified(); } } @Override public boolean isCopyColorInfoFromParentToNewChildAllowed(MindMapPanel source) { return NbUtils.getPreferences().getBoolean("copyColorInfoToNewChildAllowed", true); //NOI18N } @Override public boolean isUnfoldCollapsedTopicDropTarget(final MindMapPanel source) { return NbUtils.getPreferences().getBoolean("unfoldCollapsedTarget", true); //NOI18N } @Override public MindMapPanelConfig provideConfigForMindMapPanel(final MindMapPanel source) { final MindMapPanelConfig config = new MindMapPanelConfig(); config.loadFrom(NbUtils.getPreferences()); return config; } private Map<Class<? extends PopUpMenuItemPlugin>, CustomJob> customProcessors = null; private Map<Class<? extends PopUpMenuItemPlugin>, CustomJob> getCustomProcessors() { if (this.customProcessors == null) { this.customProcessors = new HashMap<Class<? extends PopUpMenuItemPlugin>, CustomJob>(); this.customProcessors.put(ExtraNotePlugin.class, new CustomJob() { @Override public void doJob(@Nonnull final PopUpMenuItemPlugin plugin, @Nonnull final MindMapPanel panel, @Nonnull final DialogProvider dialogProvider, @Nullable final Topic topic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { editTextForTopic(topic); panel.requestFocus(); } }); this.customProcessors.put(ExtraFilePlugin.class, new CustomJob() { @Override public void doJob(@Nonnull final PopUpMenuItemPlugin plugin, @Nonnull final MindMapPanel panel, @Nonnull final DialogProvider dialogProvider, @Nullable final Topic topic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { editFileLinkForTopic(topic); panel.requestFocus(); } }); this.customProcessors.put(ExtraURIPlugin.class, new CustomJob() { @Override public void doJob(@Nonnull final PopUpMenuItemPlugin plugin, @Nonnull final MindMapPanel panel, @Nonnull final DialogProvider dialogProvider, @Nullable final Topic topic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { editLinkForTopic(topic); panel.requestFocus(); } }); this.customProcessors.put(ExtraJumpPlugin.class, new CustomJob() { @Override public void doJob(@Nonnull final PopUpMenuItemPlugin plugin, @Nonnull final MindMapPanel panel, @Nonnull final DialogProvider dialogProvider, @Nullable final Topic topic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { editTopicLinkForTopic(topic); panel.requestFocus(); } }); this.customProcessors.put(ChangeColorPlugin.class, new CustomJob() { @Override public void doJob(@Nonnull final PopUpMenuItemPlugin plugin, @Nonnull final MindMapPanel panel, @Nonnull final DialogProvider dialogProvider, @Nullable final Topic topic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { processColorDialogForTopics(panel, selectedTopics.length > 0 ? selectedTopics : new Topic[]{topic}); } }); this.customProcessors.put(AboutPlugin.class, new CustomJob() { @Override public void doJob(@Nonnull final PopUpMenuItemPlugin plugin, @Nonnull final MindMapPanel panel, @Nonnull final DialogProvider dialogProvider, @Nullable final Topic topic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { NbUtils.plainMessageOk(BUNDLE.getString("MMDGraphEditor.makePopUp.msgAboutTitle"), new AboutPanel());//NOI18N } }); this.customProcessors.put(OptionsPlugin.class, new CustomJob() { @Override public void doJob(@Nonnull final PopUpMenuItemPlugin plugin, @Nonnull final MindMapPanel panel, @Nonnull final DialogProvider dialogProvider, @Nullable final Topic topic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { OptionsDisplayer.getDefault().open("nb-mmd-config-main"); //NOI18N } }); } return this.customProcessors; } @Override public JPopupMenu makePopUpForMindMapPanel(@Nonnull final MindMapPanel source, @Nonnull final Point point, @Nullable final AbstractElement element, @Nullable final ElementPart partUnderMouse) { return Utils.makePopUp(source, DialogProviderManager.getInstance().getDialogProvider(), element == null ? null : element.getModel(), source.getSelectedTopics(), getCustomProcessors()); } private void processColorDialogForTopics(final MindMapPanel source, final Topic[] topics) { final Color borderColor = NbUtils.extractCommonColorForColorChooserButton(ATTR_BORDER_COLOR.getText(), topics); final Color fillColor = NbUtils.extractCommonColorForColorChooserButton(ATTR_FILL_COLOR.getText(), topics); final Color textColor = NbUtils.extractCommonColorForColorChooserButton(ATTR_TEXT_COLOR.getText(), topics); final ColorAttributePanel panel = new ColorAttributePanel(borderColor, fillColor, textColor); if (NbUtils.plainMessageOkCancel(String.format(BUNDLE.getString("MMDGraphEditor.colorEditDialogTitle"), topics.length), panel)) { ColorAttributePanel.Result result = panel.getResult(); if (result.getBorderColor() != ColorChooserButton.DIFF_COLORS) { Utils.setAttribute(ATTR_BORDER_COLOR.getText(), Utils.color2html(result.getBorderColor(), false), topics); } if (result.getTextColor() != ColorChooserButton.DIFF_COLORS) { Utils.setAttribute(ATTR_TEXT_COLOR.getText(), Utils.color2html(result.getTextColor(), false), topics); } if (result.getFillColor() != ColorChooserButton.DIFF_COLORS) { Utils.setAttribute(ATTR_FILL_COLOR.getText(), Utils.color2html(result.getFillColor(), false), topics); } source.updateView(true); } } @Override public DialogProvider getDialogProvider(final MindMapPanel source) { return DialogProviderManager.getInstance().getDialogProvider(); } private void updateConfigFromPreferences() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (mindMapPanel.lockIfNotDisposed()) { try { mindMapPanel.refreshConfiguration(); mindMapPanel.invalidate(); mindMapPanel.revalidate(); mindMapPanel.repaint(); } finally { mindMapPanel.unlock(); } } else { LOGGER.warn("Attempt tp update disposed panel : " + mindMapPanel); } } }); } public static void notifyReloadConfig() { synchronized (ALL_EDITORS) { final Iterator<MMDGraphEditor> iterator = ALL_EDITORS.iterator(); while (iterator.hasNext()) { final MMDGraphEditor next = iterator.next(); if (next.isPanelDisposed()) { LOGGER.warn("Detected disposed mind map panel among active editors set : " + next); iterator.remove(); } else { next.updateConfigFromPreferences(); } } } } @Override public boolean isSelectionAllowed(MindMapPanel source) { return true; } @Override public boolean isElementDragAllowed(MindMapPanel source) { return true; } @Override public boolean isMouseMoveProcessingAllowed(MindMapPanel source) { return true; } @Override public boolean isMouseWheelProcessingAllowed(MindMapPanel source) { return true; } @Override public boolean isMouseClickProcessingAllowed(final MindMapPanel source) { return true; } @Override public boolean canBeDeletedSilently(final MindMap map, final Topic topic) { return topic.getText().isEmpty() && topic.getExtras().isEmpty() && doesContainOnlyStandardAttributes(topic); } }