/*
* Copyright 2016 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.sciareto.ui.editors;
import static com.igormaznitsa.mindmap.swing.panel.StandardTopicAttribute.*;
import static com.igormaznitsa.mindmap.swing.panel.utils.Utils.assertSwingDispatchThread;
import static com.igormaznitsa.sciareto.ui.UiUtils.BUNDLE;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
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.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.KeyEvent;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileFilter;
import org.apache.commons.io.FileUtils;
import com.igormaznitsa.meta.annotation.MustNotContainNull;
import com.igormaznitsa.meta.common.utils.Assertions;
import com.igormaznitsa.mindmap.ide.commons.DnDUtils;
import com.igormaznitsa.mindmap.model.Extra;
import com.igormaznitsa.mindmap.model.ExtraFile;
import com.igormaznitsa.mindmap.model.ExtraLink;
import com.igormaznitsa.mindmap.model.ExtraNote;
import com.igormaznitsa.mindmap.model.ExtraTopic;
import com.igormaznitsa.mindmap.model.MMapURI;
import com.igormaznitsa.mindmap.model.MindMap;
import com.igormaznitsa.mindmap.model.MindMapController;
import com.igormaznitsa.mindmap.model.Topic;
import com.igormaznitsa.mindmap.model.logger.Logger;
import com.igormaznitsa.mindmap.model.logger.LoggerFactory;
import com.igormaznitsa.mindmap.plugins.api.CustomJob;
import com.igormaznitsa.mindmap.plugins.api.PopUpMenuItemPlugin;
import com.igormaznitsa.mindmap.plugins.processors.ExtraFilePlugin;
import com.igormaznitsa.mindmap.plugins.processors.ExtraJumpPlugin;
import com.igormaznitsa.mindmap.plugins.processors.ExtraNotePlugin;
import com.igormaznitsa.mindmap.plugins.processors.ExtraURIPlugin;
import com.igormaznitsa.mindmap.plugins.tools.ChangeColorPlugin;
import com.igormaznitsa.mindmap.swing.panel.DialogProvider;
import com.igormaznitsa.mindmap.swing.panel.MMDTopicsTransferable;
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 com.igormaznitsa.mindmap.swing.panel.ui.AbstractElement;
import com.igormaznitsa.mindmap.swing.panel.ui.ElementPart;
import com.igormaznitsa.mindmap.swing.panel.utils.KeyEventType;
import com.igormaznitsa.mindmap.swing.panel.utils.MindMapUtils;
import com.igormaznitsa.mindmap.swing.panel.utils.Utils;
import com.igormaznitsa.sciareto.Context;
import com.igormaznitsa.sciareto.Main;
import com.igormaznitsa.sciareto.preferences.PreferencesManager;
import com.igormaznitsa.sciareto.ui.tree.NodeProject;
import com.igormaznitsa.sciareto.ui.editors.mmeditors.ColorAttributePanel;
import com.igormaznitsa.sciareto.ui.misc.ColorChooserButton;
import com.igormaznitsa.sciareto.ui.DialogProviderManager;
import com.igormaznitsa.sciareto.ui.editors.mmeditors.FileEditPanel;
import com.igormaznitsa.sciareto.ui.editors.mmeditors.MindMapTreePanel;
import com.igormaznitsa.sciareto.ui.tabs.TabTitle;
import com.igormaznitsa.sciareto.ui.UiUtils;
import com.igormaznitsa.sciareto.ui.tree.FileTransferable;
import com.igormaznitsa.sciareto.ui.FindTextScopeProvider;
public final class MMDEditor extends AbstractEditor implements MindMapPanelController, MindMapController, MindMapListener, DropTargetListener {
private static final long serialVersionUID = -1011638261448046208L;
private static final Logger LOGGER = LoggerFactory.getLogger(MMDEditor.class);
private final MindMapPanel mindMapPanel;
private final TabTitle title;
private static final String FILELINK_ATTR_OPEN_IN_SYSTEM = "useSystem"; //NOI18N
private final Context context;
private boolean dragAcceptableType;
private final transient UndoRedoStorage<String> undoStorage = new UndoRedoStorage<>(5);
private boolean preventAddUndo = false;
private String currentModelState;
private boolean firstLayouting = true;
private final JScrollPane scrollPane;
public void refreshConfig() {
this.mindMapPanel.refreshConfiguration();
}
public static final FileFilter MMD_FILE_FILTER = new FileFilter() {
@Override
public boolean accept(@Nonnull final File f) {
return f.isDirectory() || f.getName().endsWith(".mmd"); //NOI18N
}
@Override
@Nonnull
public String getDescription() {
return "Mind Map document (*.mmd)";
}
};
@Override
@Nonnull
public FileFilter getFileFilter() {
return MMD_FILE_FILTER;
}
public MMDEditor(@Nonnull final Context context, @Nullable File file) throws IOException {
super();
this.context = context;
this.title = new TabTitle(context, this, file);
this.mindMapPanel = new MindMapPanel(this);
this.mindMapPanel.addMindMapListener(this);
this.scrollPane = new JScrollPane(this.mindMapPanel);
final AdjustmentListener listener = new AdjustmentListener() {
@Override
public void adjustmentValueChanged(@Nonnull final AdjustmentEvent e) {
mindMapPanel.repaint();
}
};
this.scrollPane.getHorizontalScrollBar().addAdjustmentListener(listener);
this.scrollPane.getVerticalScrollBar().addAdjustmentListener(listener);
this.mindMapPanel.setDropTarget(new DropTarget(this.mindMapPanel, this));
final MindMap map;
if (file == null) {
map = new MindMap(this, true);
} else {
map = new MindMap(this, new StringReader(FileUtils.readFileToString(file, "UTF-8"))); //NOI18N
}
this.mindMapPanel.setModel(Assertions.assertNotNull(map), false);
loadContent(file);
this.currentModelState = this.mindMapPanel.getModel().packToString();
}
public void rootToCentre() {
final Topic root = this.mindMapPanel.getModel().getRoot();
if (root != null) {
topicToCentre(root);
}
}
public boolean topicToCentre(@Nullable final Topic topic) {
boolean result = false;
assertSwingDispatchThread();
if (topic != null) {
AbstractElement element = (AbstractElement) topic.getPayload();
if (element == null && this.mindMapPanel.updateElementsAndSizeForCurrentGraphics(true, true)) {
element = (AbstractElement) topic.getPayload();
this.scrollPane.getViewport().doLayout();
}
if (element != null) {
final Rectangle2D bounds = element.getBounds();
final Dimension viewPortSize = this.scrollPane.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.scrollPane.getViewport().setViewPosition(new Point(x, y));
result = true;
}
}
return result;
}
@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 onTopicCollapsatorClick(@Nonnull final MindMapPanel source, @Nonnull final Topic topic, final boolean beforeAction) {
if (!beforeAction) {
topicToCentre(topic);
}
}
@Override
@Nonnull
public JComponent getMainComponent() {
return this.mindMapPanel;
}
@Override
@Nonnull
public EditorContentType getEditorContentType() {
return EditorContentType.MINDMAP;
}
@Override
public void focusToEditor() {
this.mindMapPanel.requestFocus();
}
@Override
public boolean isEditable() {
return true;
}
@Override
public boolean isSaveable() {
return true;
}
@Override
public boolean isRedo() {
return this.undoStorage.hasRedo();
}
@Override
public boolean isUndo() {
return this.undoStorage.hasUndo();
}
@Override
public void loadContent(@Nullable File file) throws IOException {
final MindMap map;
if (file == null) {
map = new MindMap(this, true);
} else {
map = new MindMap(this, new StringReader(FileUtils.readFileToString(file, "UTF-8"))); //NOI18N
}
this.mindMapPanel.setModel(Assertions.assertNotNull(map), false);
this.undoStorage.clearRedo();
this.undoStorage.clearUndo();
this.title.setChanged(false);
this.scrollPane.revalidate();
}
@Override
public boolean saveDocument() throws IOException {
boolean result = false;
if (this.title.isChanged()) {
File file = this.title.getAssociatedFile();
if (file == null) {
file = DialogProviderManager.getInstance().getDialogProvider().msgSaveFileDialog("mmd-editor-document", "Save Mind Map", null, true, getFileFilter(), "Save");
if (file == null) {
return result;
}
}
FileUtils.write(file, this.mindMapPanel.getModel().write(new StringWriter(16384)).toString(), "UTF-8", false); //NOI18N
this.title.setChanged(false);
result = true;
this.undoStorage.setFlagThatSomeStateLost();
} else {
result = true;
}
return result;
}
@Override
public void onComponentElementsLayouted(@Nonnull final MindMapPanel source, @Nonnull final Graphics2D g) {
if (this.firstLayouting) {
this.firstLayouting = false;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
topicToCentre(mindMapPanel.getModel().getRoot());
scrollPane.revalidate();
scrollPane.repaint();
}
});
}
}
@Override
public boolean isUnfoldCollapsedTopicDropTarget(@Nonnull final MindMapPanel source) {
return PreferencesManager.getInstance().getPreferences().getBoolean("unfoldCollapsedTarget", true); //NOI18N
}
@Override
public boolean isCopyColorInfoFromParentToNewChildAllowed(@Nonnull final MindMapPanel source) {
return PreferencesManager.getInstance().getPreferences().getBoolean("copyColorInfoToNewChildAllowed", true); //NOI18N
}
@Override
public boolean isSelectionAllowed(@Nonnull final MindMapPanel source) {
return true;
}
@Override
public boolean isElementDragAllowed(@Nonnull final MindMapPanel source) {
return true;
}
@Override
public boolean isMouseMoveProcessingAllowed(@Nonnull final MindMapPanel source) {
return true;
}
@Override
public boolean isMouseWheelProcessingAllowed(@Nonnull final MindMapPanel source) {
return true;
}
@Override
public boolean isMouseClickProcessingAllowed(@Nonnull final MindMapPanel source) {
return true;
}
@Override
@Nonnull
public MindMapPanelConfig provideConfigForMindMapPanel(@Nonnull final MindMapPanel source) {
final MindMapPanelConfig config = new MindMapPanelConfig();
config.loadFrom(PreferencesManager.getInstance().getPreferences());
return config;
}
private transient Map<Class<? extends PopUpMenuItemPlugin>, CustomJob> customProcessors = null;
@Nonnull
public MindMapPanel getMindMapPanel() {
return this.mindMapPanel;
}
@Override
@Nonnull
public TabTitle getTabTitle() {
return this.title;
}
@Override
@Nonnull
public JComponent getContainerToShow() {
return this.scrollPane;
}
@Override
@Nonnull
public AbstractEditor getEditor() {
return this;
}
@Override
public void onMindMapModelChanged(@Nonnull final MindMapPanel source) {
if (!this.preventAddUndo && this.currentModelState != null) {
this.undoStorage.addToUndo(this.currentModelState);
this.undoStorage.clearRedo();
this.currentModelState = source.getModel().packToString();
}
try {
this.title.setChanged(true);
this.scrollPane.revalidate();
this.scrollPane.repaint();
}
finally {
this.context.notifyUpdateRedoUndo();
}
}
@Override
public boolean redo() {
if (!this.mindMapPanel.endEdit(false)) {
if (this.undoStorage.hasRedo()) {
this.undoStorage.addToUndo(this.currentModelState);
this.currentModelState = this.undoStorage.fromRedo();
this.preventAddUndo = true;
try {
this.mindMapPanel.setModel(new MindMap(null, new StringReader(this.currentModelState)), true);
this.title.setChanged(this.undoStorage.hasUndo() || this.undoStorage.hasRemovedUndoStateForFullBuffer());
}
catch (IOException ex) {
LOGGER.error("Can't redo mind map", ex); //NOI18N
}
finally {
this.preventAddUndo = false;
}
}
}
return this.undoStorage.hasRedo();
}
@Override
public boolean undo() {
if (!this.mindMapPanel.endEdit(false)) {
if (this.undoStorage.hasUndo()) {
this.undoStorage.addToRedo(this.currentModelState);
this.currentModelState = this.undoStorage.fromUndo();
this.preventAddUndo = true;
try {
this.mindMapPanel.setModel(new MindMap(null, new StringReader(this.currentModelState)), true);
this.title.setChanged(this.undoStorage.hasUndo() || this.undoStorage.hasRemovedUndoStateForFullBuffer());
}
catch (IOException ex) {
LOGGER.error("Can't redo mind map", ex); //NOI18N
}
finally {
this.preventAddUndo = false;
}
}
}
return this.undoStorage.hasUndo();
}
@Override
public void onMindMapModelRealigned(@Nonnull final MindMapPanel source, @Nonnull final Dimension coveredAreaSize) {
this.scrollPane.getViewport().revalidate();
this.scrollPane.repaint();
}
@Override
public void onEnsureVisibilityOfTopic(@Nonnull final MindMapPanel source, @Nullable final Topic topic) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (topic == null) {
return;
}
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 = scrollPane.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
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.scrollPane.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 void onClickOnExtra(@Nonnull final MindMapPanel source, final int modifiers, final int clicks, @Nonnull final Topic topic, @Nonnull final Extra<?> extra) {
if (clicks > 1) {
switch (extra.getType()) {
case FILE: {
Main.getApplicationFrame().endFullScreenIfActive();
final MMapURI uri = (MMapURI) extra.getValue();
final File theFile = uri.asFile(getProjectFolder());
if (Boolean.parseBoolean(uri.getParameters().getProperty(FILELINK_ATTR_OPEN_IN_SYSTEM, "false"))) { //NOI18N
UiUtils.openInSystemViewer(theFile);
} else if (theFile.isDirectory()) {
this.context.openProject(theFile, false);
} else if (!this.context.openFileAsTab(theFile)) {
UiUtils.openInSystemViewer(theFile);
}
}
break;
case LINK: {
Main.getApplicationFrame().endFullScreenIfActive();
final MMapURI uri = ((ExtraLink) extra).getValue();
if (!UiUtils.browseURI(uri.asURI(), PreferencesManager.getInstance().getPreferences().getBoolean("useInsideBrowser", false))) { //NOI18N
DialogProviderManager.getInstance().getDialogProvider().msgError(String.format(BUNDLE.getString("MMDGraphEditor.onClickOnExtra.msgCantBrowse"), uri.toString()));
}
}
break;
case NOTE: {
editTextForTopic(topic);
}
break;
case TOPIC: {
final Topic theTopic = source.getModel().findTopicForLink((ExtraTopic) extra);
if (theTopic == null) {
// not presented
DialogProviderManager.getInstance().getDialogProvider().msgWarn(BUNDLE.getString("MMDGraphEditor.onClickOnExtra.msgCantFindTopic"));
} else {
// detected
this.mindMapPanel.focusTo(theTopic);
}
}
break;
}
}
}
@Override
public void onChangedSelection(@Nonnull final MindMapPanel source, @Nonnull @MustNotContainNull final Topic[] currentSelectedTopics) {
}
@Override
public boolean allowedRemovingOfTopics(@Nonnull final MindMapPanel source, @Nonnull @MustNotContainNull 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 = DialogProviderManager.getInstance().getDialogProvider().msgConfirmYesNo(BUNDLE.getString("MMDGraphEditor.allowedRemovingOfTopics,title"), String.format(BUNDLE.getString("MMDGraphEditor.allowedRemovingOfTopics.message"), topics.length));
}
return result;
}
@Override
public void dragEnter(@Nonnull final DropTargetDragEvent dtde) {
this.dragAcceptableType = checkDragType(dtde);
if (!this.dragAcceptableType) {
dtde.rejectDrag();
} else {
dtde.acceptDrag(DnDConstants.ACTION_MOVE);
}
this.scrollPane.repaint();
}
@Override
public void dragOver(@Nonnull final DropTargetDragEvent dtde) {
if (acceptOrRejectDragging(dtde)) {
dtde.acceptDrag(DnDConstants.ACTION_MOVE);
} else {
dtde.rejectDrag();
}
this.scrollPane.repaint();
}
@Override
public void dropActionChanged(@Nonnull final DropTargetDragEvent dtde) {
}
@Override
public void dragExit(@Nonnull final DropTargetEvent dte) {
}
@Nullable
private File extractDropFile(@Nonnull final DropTargetDropEvent dtde) throws Exception {
File result = null;
for (final DataFlavor df : dtde.getCurrentDataFlavors()) {
final Class<?> representation = df.getRepresentationClass();
if (FileTransferable.class.isAssignableFrom(representation)) {
final FileTransferable t = (FileTransferable) dtde.getTransferable();
final List<File> listOfFiles = t.getFiles();
result = listOfFiles.isEmpty() ? null : listOfFiles.get(0);
break;
} else if (df.isFlavorJavaFileListType()) {
try {
final List list = (List) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
if (list != null && !list.isEmpty()) {
result = (File) list.get(0);
}
break;
}
catch (final Exception ex) {
LOGGER.error("Can't extract file from DnD", ex); //NOI18N
}
}
}
return result;
}
@Override
public void drop(@Nonnull final DropTargetDropEvent dtde) {
dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
File detectedFile;
String detectedLink;
String detectedNote;
URI decodedLink;
try{
detectedFile = extractDropFile(dtde);
detectedLink = DnDUtils.extractDropLink(dtde);
detectedNote = DnDUtils.extractDropNote(dtde);
decodedLink = null;
if (detectedLink!=null){
try{
decodedLink = new URI(detectedLink);
}catch(final URISyntaxException ex){
decodedLink = null;
}
}
dtde.dropComplete(true);
}catch(final Exception ex){
LOGGER.error("Error during DnD processing", ex); //NOI18N
dtde.dropComplete(false);
return;
}
final AbstractElement element = this.mindMapPanel.findTopicUnderPoint(dtde.getLocation());
if (detectedFile != null) {
decodedLink = DnDUtils.extractUrlLinkFromFile(detectedFile);
if (decodedLink!=null){
addURItoElement(decodedLink, element);
} else {
addFileToElement(detectedFile, element);
}
dtde.dropComplete(true);
} else if (decodedLink != null) {
addURItoElement(decodedLink, element);
dtde.dropComplete(true);
} else if (detectedNote != null) {
addNoteToElement(detectedNote, element);
dtde.dropComplete(true);
} else {
dtde.dropComplete(false);
}
}
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(@Nonnull final File theFile, @Nullable final AbstractElement element) {
if (element != null) {
final Topic topic = element.getModel();
final MMapURI theURI;
if (PreferencesManager.getInstance().getPreferences().getBoolean("makeRelativePathToProject", true)) { //NOI18N
final File projectFolder = getProjectFolder();
if (theFile.equals(projectFolder)) {
theURI = new MMapURI(projectFolder, new File("."), null); //NOI18N
} else {
theURI = new MMapURI(projectFolder, theFile, null);
}
} else {
theURI = new MMapURI(null, theFile, null);
}
if (topic.getExtras().containsKey(Extra.ExtraType.FILE)) {
if (!DialogProviderManager.getInstance().getDialogProvider().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);
}
}
protected boolean acceptOrRejectDragging(@Nonnull 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(@Nonnull final DropTargetDragEvent dtde) {
boolean result = DnDUtils.isFileOrLinkOrText(dtde);
if (!result) {
for (final DataFlavor flavor : dtde.getCurrentDataFlavors()) {
final Class dataClass = flavor.getRepresentationClass();
if (FileTransferable.class.isAssignableFrom(dataClass)) {
result = true;
break;
}
}
}
return result;
}
@Nonnull
private Map<Class<? extends PopUpMenuItemPlugin>, CustomJob> getCustomProcessors() {
if (this.customProcessors == null) {
this.customProcessors = new HashMap<>();
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) {
if (topic != null) {
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});
}
});
}
return this.customProcessors;
}
private void editTextForTopic(@Nonnull final Topic topic) {
final ExtraNote note = (ExtraNote) topic.getExtras().get(Extra.ExtraType.NOTE);
final String result;
if (note == null) {
// create new
result = UiUtils.editText(String.format(BUNDLE.getString("MMDGraphEditor.editTextForTopic.dlfAddNoteTitle"), Utils.makeShortTextVersion(topic.getText(), 16)), ""); //NOI18N
} else {
// edit
result = UiUtils.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);
}
}
@Nullable
private File getProjectFolder() {
File result = null;
final File associatedFile = this.title.getAssociatedFile();
if (associatedFile != null) {
final NodeProject project = context.findProjectForFile(associatedFile);
result = project == null ? null : project.getFolder();
}
return result;
}
private void editFileLinkForTopic(@Nullable final Topic topic) {
if (topic != null) {
final ExtraFile file = (ExtraFile) topic.getExtras().get(Extra.ExtraType.FILE);
final FileEditPanel.DataContainer path;
final File projectFolder = getProjectFolder();
if (file == null) {
path = UiUtils.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;
origPath = new FileEditPanel.DataContainer(uri.asFile(projectFolder).getAbsolutePath(), flagOpenInSystem);
path = UiUtils.editFilePath(BUNDLE.getString("MMDGraphEditor.editFileLinkForTopic.addPathTitle"), projectFolder, origPath);
}
if (path != null) {
final boolean valueChanged;
if (path.isEmpty()) {
valueChanged = 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(PreferencesManager.getInstance().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));
valueChanged = true;
} else {
DialogProviderManager.getInstance().getDialogProvider().msgError(String.format(BUNDLE.getString("MMDGraphEditor.editFileLinkForTopic.errorCantFindFile"), path.getPath()));
valueChanged = false;
}
}
if (valueChanged) {
this.mindMapPanel.invalidate();
this.mindMapPanel.repaint();
onMindMapModelChanged(this.mindMapPanel);
}
}
}
}
private void editTopicLinkForTopic(@Nullable final Topic topic) {
if (topic != null) {
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 (DialogProviderManager.getInstance().getDialogProvider().msgOkCancel(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 (DialogProviderManager.getInstance().getDialogProvider().msgOkCancel(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(@Nullable final Topic topic) {
if (topic != null) {
final ExtraLink link = (ExtraLink) topic.getExtras().get(Extra.ExtraType.LINK);
final MMapURI result;
if (link == null) {
// create new
result = UiUtils.editURI(String.format(BUNDLE.getString("MMDGraphEditor.editLinkForTopic.dlgAddURITitle"), Utils.makeShortTextVersion(topic.getText(), 16)), null);
} else {
// edit
result = UiUtils.editURI(String.format(BUNDLE.getString("MMDGraphEditor.editLinkForTopic.dlgEditURITitle"), Utils.makeShortTextVersion(topic.getText(), 16)), link.getValue());
}
if (result != null) {
if (result == UiUtils.EMPTY_URI) {
topic.removeExtra(Extra.ExtraType.LINK);
} else {
topic.setExtra(new ExtraLink(result));
}
this.mindMapPanel.invalidate();
this.mindMapPanel.repaint();
onMindMapModelChanged(this.mindMapPanel);
}
}
}
private void processColorDialogForTopics(@Nonnull final MindMapPanel source, @Nonnull @MustNotContainNull final Topic[] topics) {
final Color borderColor = UiUtils.extractCommonColorForColorChooserButton(ATTR_BORDER_COLOR.getText(), topics);
final Color fillColor = UiUtils.extractCommonColorForColorChooserButton(ATTR_FILL_COLOR.getText(), topics);
final Color textColor = UiUtils.extractCommonColorForColorChooserButton(ATTR_TEXT_COLOR.getText(), topics);
final ColorAttributePanel panel = new ColorAttributePanel(borderColor, fillColor, textColor);
if (DialogProviderManager.getInstance().getDialogProvider().msgOkCancel(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);
}
onMindMapModelChanged(source);
source.updateView(true);
}
}
@Override
@Nonnull
public JPopupMenu makePopUpForMindMapPanel(@Nonnull final MindMapPanel source, @Nonnull final Point point, @Nullable final AbstractElement elementUnderMouse, @Nullable final ElementPart elementPartUnderMouse) {
return Utils.makePopUp(source, DialogProviderManager.getInstance().getDialogProvider(), elementUnderMouse == null ? null : elementUnderMouse.getModel(), source.getSelectedTopics(), getCustomProcessors());
}
@Override
@Nonnull
public DialogProvider getDialogProvider(@Nonnull final MindMapPanel source) {
return DialogProviderManager.getInstance().getDialogProvider();
}
@Override
public boolean processDropTopicToAnotherTopic(@Nonnull final MindMapPanel source, @Nonnull final Point dropPoint, @Nullable final Topic draggedTopic, @Nullable final Topic destinationTopic) {
boolean result = false;
if (draggedTopic != null && destinationTopic != null && draggedTopic != destinationTopic) {
if (destinationTopic.getExtras().containsKey(Extra.ExtraType.TOPIC)) {
if (!DialogProviderManager.getInstance().getDialogProvider().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;
}
@Override
public boolean canBeDeletedSilently(@Nonnull final MindMap map, @Nonnull final Topic topic) {
return topic.getText().isEmpty() && topic.getExtras().isEmpty() && doesContainOnlyStandardAttributes(topic);
}
@Override
public boolean findNext(@Nonnull final Pattern pattern, @Nonnull final FindTextScopeProvider provider) {
Topic startTopic = null;
if (this.mindMapPanel.hasSelectedTopics()) {
final Topic[] selected = this.mindMapPanel.getSelectedTopics();
startTopic = selected[selected.length - 1];
}
final File projectBaseFolder = this.getProjectFolder();
final Set<Extra.ExtraType> extras = EnumSet.noneOf(Extra.ExtraType.class);
if (provider.toSearchIn(FindTextScopeProvider.SearchTextScope.IN_TOPIC_NOTES)) {
extras.add(Extra.ExtraType.NOTE);
}
if (provider.toSearchIn(FindTextScopeProvider.SearchTextScope.IN_TOPIC_FILES)) {
extras.add(Extra.ExtraType.FILE);
}
if (provider.toSearchIn(FindTextScopeProvider.SearchTextScope.IN_TOPIC_URI)) {
extras.add(Extra.ExtraType.LINK);
}
final boolean inTopicText = provider.toSearchIn(FindTextScopeProvider.SearchTextScope.IN_TOPIC_TEXT);
Topic found = this.mindMapPanel.getModel().findNext(projectBaseFolder, startTopic, pattern, inTopicText, extras);
if (found == null && startTopic != null) {
found = this.mindMapPanel.getModel().findNext(projectBaseFolder, null, pattern, inTopicText, extras);
}
if (found != null) {
this.mindMapPanel.removeAllSelection();
this.mindMapPanel.focusTo(found);
}
return found != null;
}
@Override
public boolean findPrev(@Nonnull final Pattern pattern, @Nonnull final FindTextScopeProvider provider) {
Topic startTopic = null;
if (this.mindMapPanel.hasSelectedTopics()) {
final Topic[] selected = this.mindMapPanel.getSelectedTopics();
startTopic = selected[0];
}
final File projectBaseFolder = this.getProjectFolder();
final Set<Extra.ExtraType> extras = new HashSet<>();
if (provider.toSearchIn(FindTextScopeProvider.SearchTextScope.IN_TOPIC_NOTES)) {
extras.add(Extra.ExtraType.NOTE);
}
if (provider.toSearchIn(FindTextScopeProvider.SearchTextScope.IN_TOPIC_FILES)) {
extras.add(Extra.ExtraType.FILE);
}
if (provider.toSearchIn(FindTextScopeProvider.SearchTextScope.IN_TOPIC_URI)) {
extras.add(Extra.ExtraType.LINK);
}
final boolean inTopicText = provider.toSearchIn(FindTextScopeProvider.SearchTextScope.IN_TOPIC_TEXT);
Topic found = this.mindMapPanel.getModel().findPrev(projectBaseFolder, startTopic, pattern, inTopicText, extras);
if (found == null && startTopic != null) {
found = this.mindMapPanel.getModel().findPrev(projectBaseFolder, null, pattern, inTopicText, extras);
}
if (found != null) {
this.mindMapPanel.removeAllSelection();
this.mindMapPanel.focusTo(found);
}
return found != null;
}
@Override
public boolean doesSupportPatternSearch() {
return true;
}
@Override
public boolean doesSupportCutCopyPaste() {
return true;
}
@Override
public boolean isCutAllowed() {
return this.mindMapPanel.getSelectedTopics().length > 0;
}
@Override
public boolean doCut() {
assertSwingDispatchThread();
return this.mindMapPanel.copyTopicsToClipboard(true, MindMapUtils.removeSuccessorsAndDuplications(this.mindMapPanel.getSelectedTopics()));
}
@Override
public boolean isCopyAllowed() {
return this.mindMapPanel.getSelectedTopics().length>0;
}
@Override
public boolean isPasteAllowed() {
final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
return this.mindMapPanel.hasSelectedTopics() && clipboard.isDataFlavorAvailable(MMDTopicsTransferable.MMD_DATA_FLAVOR);
}
@Override
public boolean doCopy() {
assertSwingDispatchThread();
return this.mindMapPanel.copyTopicsToClipboard(false, MindMapUtils.removeSuccessorsAndDuplications(this.mindMapPanel.getSelectedTopics()));
}
@Override
public boolean doPaste() {
assertSwingDispatchThread();
return this.mindMapPanel.pasteTopicsFromClipboard();
}
}