All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.igormaznitsa.mindmap.swing.panel.MindMapPanelConfig Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2015-2022 Igor A. Maznitsa
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.igormaznitsa.mindmap.swing.panel;

import static java.util.Objects.requireNonNull;

import com.igormaznitsa.mindmap.model.MiscUtils;
import com.igormaznitsa.mindmap.swing.panel.utils.KeyShortcut;
import com.igormaznitsa.mindmap.swing.panel.utils.MouseButton;
import com.igormaznitsa.mindmap.swing.panel.utils.RenderQuality;
import com.igormaznitsa.mindmap.swing.panel.utils.Utils;
import java.awt.Color;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.stream.Stream;
import javax.swing.KeyStroke;
import org.apache.commons.lang3.SystemUtils;

public final class MindMapPanelConfig implements Serializable {

  public static final String KEY_ADD_CHILD_AND_START_EDIT = "addChildAndStartEdit";
  public static final String KEY_ADD_SIBLING_AND_START_EDIT = "addSiblingAndStartEdit";
  public static final String KEY_FOCUS_ROOT_OR_START_EDIT = "focusToRootOrStartEdit";
  public static final String KEY_CANCEL_EDIT = "cancelEdit";
  public static final String KEY_TOPIC_FOLD = "topicFold";
  public static final String KEY_TOPIC_FOLD_ALL = "topicFoldAll";
  public static final String KEY_TOPIC_UNFOLD = "topicUnfold";
  public static final String KEY_TOPIC_UNFOLD_ALL = "topicUnfoldAll";
  public static final String KEY_FOCUS_MOVE_UP = "moveFocusUp";
  public static final String KEY_FOCUS_MOVE_DOWN = "moveFocusDown";
  public static final String KEY_FOCUS_MOVE_LEFT = "moveFocusLeft";
  public static final String KEY_FOCUS_MOVE_RIGHT = "moveFocusRight";
  public static final String KEY_FOCUS_MOVE_UP_ADD_FOCUSED = "moveFocusUpAddFocused";
  public static final String KEY_FOCUS_MOVE_DOWN_ADD_FOCUSED = "moveFocusDownAddFocused";
  public static final String KEY_FOCUS_MOVE_LEFT_ADD_FOCUSED = "moveFocusLeftAddFocused";
  public static final String KEY_FOCUS_MOVE_RIGHT_ADD_FOCUSED = "moveFocusRightAddFocused";
  public static final String KEY_DELETE_TOPIC = "deleteSelectedTopic";
  public static final String KEY_TOPIC_TEXT_NEXT_LINE = "nextLineInTopicText";
  public static final String KEY_ZOOM_IN = "zoomIn";
  public static final String KEY_ZOOM_OUT = "zoomOut";
  public static final String KEY_ZOOM_RESET = "zoomReset";
  public static final String KEY_SHOW_POPUP = "showPopupMenu";
  public static final String KEY_BIRDSEYE_MODIFIERS = "birdsEyeModifiers";
  public static final String PREFIX_OPTIONAL_PROPERTY = "optionalProperty.";
  public static final String PREFIX_SHORTCUT = "mapShortCut.";
  private static final long serialVersionUID = -4263687011484460064L;
  private transient final List> listeners =
      new CopyOnWriteArrayList<>();
  private final Serializable NULL_OPTIONAL_OBJECT = "some_null_object";
  private Map mapShortCut;
  private Map optionalProperties;
  private int collapsatorSize = 16;
  private int textMargins = 10;
  private int otherLevelVerticalInset = 16;
  private int otherLevelHorizontalInset = 32;
  private int firstLevelVerticalInset = 32;
  private int firstLevelHorizontalInset = 48;
  private int paperMargins = 20;
  private int selectLineGap = 5;
  private int horizontalBlockGap = 5;
  private int scaleModifiers = KeyEvent.CTRL_MASK;
  private boolean drawBackground = true;
  private Color paperColor = new Color(0x617B94);
  private Color gridColor = paperColor.darker();
  private boolean showGrid = true;
  private int gridStep = 32;
  private Color rootBackgroundColor = new Color(0x031A31);
  private Color firstLevelBackgroundColor = new Color(0xB1BFCC);
  private Color otherLevelBackgroundColor = new Color(0xFDFDFD);
  private Color rootTextColor = Color.WHITE;
  private Color firstLevelTextColor = Color.BLACK;
  private Color otherLevelTextColor = Color.BLACK;
  private Color elementBorderColor = Color.BLACK;
  private Color connectorColor = Color.WHITE;
  private Color shadowColor = new Color(0x30000000, true);
  private Color collapsatorBorderColor = Color.DARK_GRAY;
  private Color collapsatorBackgroundColor = Color.WHITE;
  private Color selectLineColor = Color.ORANGE;
  private Color jumpLinkColor = Color.CYAN;
  private Color birdseyeBackground = new Color(0x9090C8FF, true);
  private Color birdseyeFront = new Color(0x90004A94, true);
  private float shadowOffset = 5.0f;
  private float elementBorderWidth = 1.0f;
  private float collapsatorBorderWidth = 1.0f;
  private float connectorWidth = 1.5f;
  private float selectLineWidth = 3.0f;
  private float jumpLinkWidth = 1.5f;
  private boolean smartTextPaste = false;
  private Font font;
  private double scale = 1.0d;
  private boolean dropShadow = true;
  private RenderQuality renderQuality = Utils.getDefaultRenderQialityForOs();
  private MouseButton birdseyeMouseButton = MouseButton.BUTTON_3;
  private transient volatile boolean notificationEnabled = true;


  public MindMapPanelConfig(final MindMapPanelConfig cfg, final boolean copyListeners) {
    this();
    this.makeFullCopyOf(cfg, copyListeners, false);
  }

  public MindMapPanelConfig() {
    this.mapShortCut = new HashMap<>();
    this.optionalProperties = new HashMap<>();
    this.font =
        findDefaultFont(Font.PLAIN, 18, Font.MONOSPACED, new String[] {"JetBrains Mono SemiBold"});
    if (SystemUtils.IS_OS_MAC) {
      // key map for MAC
      this.mapShortCut.put(KEY_ADD_CHILD_AND_START_EDIT,
          new KeyShortcut(KEY_ADD_CHILD_AND_START_EDIT, KeyEvent.VK_TAB, 0));
      this.mapShortCut.put(KEY_ADD_SIBLING_AND_START_EDIT,
          new KeyShortcut(KEY_ADD_SIBLING_AND_START_EDIT, KeyEvent.VK_ENTER, 0));
      this.mapShortCut
          .put(KEY_CANCEL_EDIT, new KeyShortcut(KEY_CANCEL_EDIT, KeyEvent.VK_ESCAPE, 0));
      this.mapShortCut.put(KEY_TOPIC_FOLD, new KeyShortcut(KEY_TOPIC_FOLD, KeyEvent.VK_MINUS, 0));
      this.mapShortCut.put(KEY_TOPIC_FOLD_ALL,
          new KeyShortcut(KEY_TOPIC_FOLD_ALL, KeyEvent.VK_MINUS, KeyEvent.ALT_MASK));
      this.mapShortCut
          .put(KEY_TOPIC_UNFOLD, new KeyShortcut(KEY_TOPIC_UNFOLD, KeyEvent.VK_EQUALS, 0));
      this.mapShortCut.put(KEY_TOPIC_UNFOLD_ALL,
          new KeyShortcut(KEY_TOPIC_UNFOLD_ALL, KeyEvent.VK_EQUALS, KeyEvent.ALT_MASK));
      this.mapShortCut.put(KEY_FOCUS_ROOT_OR_START_EDIT,
          new KeyShortcut(KEY_FOCUS_ROOT_OR_START_EDIT, KeyEvent.VK_SPACE, KeyEvent.ALT_MASK));
      this.mapShortCut
          .put(KEY_FOCUS_MOVE_DOWN, new KeyShortcut(KEY_FOCUS_MOVE_DOWN, KeyEvent.VK_DOWN, 0));
      this.mapShortCut
          .put(KEY_FOCUS_MOVE_UP, new KeyShortcut(KEY_FOCUS_MOVE_UP, KeyEvent.VK_UP, 0));
      this.mapShortCut
          .put(KEY_FOCUS_MOVE_LEFT, new KeyShortcut(KEY_FOCUS_MOVE_LEFT, KeyEvent.VK_LEFT, 0));
      this.mapShortCut
          .put(KEY_FOCUS_MOVE_RIGHT, new KeyShortcut(KEY_FOCUS_MOVE_RIGHT, KeyEvent.VK_RIGHT, 0));
      this.mapShortCut.put(KEY_FOCUS_MOVE_DOWN_ADD_FOCUSED,
          new KeyShortcut(KEY_FOCUS_MOVE_DOWN_ADD_FOCUSED, KeyEvent.VK_DOWN, KeyEvent.SHIFT_MASK));
      this.mapShortCut.put(KEY_FOCUS_MOVE_UP_ADD_FOCUSED,
          new KeyShortcut(KEY_FOCUS_MOVE_UP_ADD_FOCUSED, KeyEvent.VK_UP, KeyEvent.SHIFT_MASK));
      this.mapShortCut.put(KEY_FOCUS_MOVE_LEFT_ADD_FOCUSED,
          new KeyShortcut(KEY_FOCUS_MOVE_LEFT_ADD_FOCUSED, KeyEvent.VK_LEFT, KeyEvent.SHIFT_MASK));
      this.mapShortCut.put(KEY_FOCUS_MOVE_RIGHT_ADD_FOCUSED,
          new KeyShortcut(KEY_FOCUS_MOVE_RIGHT_ADD_FOCUSED, KeyEvent.VK_RIGHT,
              KeyEvent.SHIFT_MASK));
      this.mapShortCut
          .put(KEY_DELETE_TOPIC, new KeyShortcut(KEY_DELETE_TOPIC, KeyEvent.VK_DELETE, 0));
      this.mapShortCut.put(KEY_TOPIC_TEXT_NEXT_LINE,
          new KeyShortcut(KEY_TOPIC_TEXT_NEXT_LINE, KeyEvent.VK_ENTER, KeyEvent.SHIFT_MASK));
      this.mapShortCut
          .put(KEY_ZOOM_IN, new KeyShortcut(KEY_ZOOM_IN, KeyEvent.VK_EQUALS, KeyEvent.CTRL_MASK));
      this.mapShortCut
          .put(KEY_ZOOM_OUT, new KeyShortcut(KEY_ZOOM_OUT, KeyEvent.VK_MINUS, KeyEvent.CTRL_MASK));
      this.mapShortCut
          .put(KEY_ZOOM_RESET, new KeyShortcut(KEY_ZOOM_RESET, KeyEvent.VK_0, KeyEvent.CTRL_MASK));
      this.mapShortCut.put(KEY_SHOW_POPUP, new KeyShortcut(KEY_SHOW_POPUP, KeyEvent.VK_SPACE,
          KeyEvent.CTRL_MASK | KeyEvent.ALT_MASK));
      this.mapShortCut.put(KEY_BIRDSEYE_MODIFIERS, new KeyShortcut(KEY_BIRDSEYE_MODIFIERS,
          KeyEvent.CTRL_MASK | KeyEvent.SHIFT_MASK));
    } else {
      // key map for Linux and Windows
      this.mapShortCut.put(KEY_ADD_CHILD_AND_START_EDIT,
          new KeyShortcut(KEY_ADD_CHILD_AND_START_EDIT, KeyEvent.VK_TAB, 0));
      this.mapShortCut.put(KEY_ADD_SIBLING_AND_START_EDIT,
          new KeyShortcut(KEY_ADD_SIBLING_AND_START_EDIT, KeyEvent.VK_ENTER, 0));
      this.mapShortCut
          .put(KEY_CANCEL_EDIT, new KeyShortcut(KEY_CANCEL_EDIT, KeyEvent.VK_ESCAPE, 0));
      this.mapShortCut.put(KEY_TOPIC_FOLD, new KeyShortcut(KEY_TOPIC_FOLD, KeyEvent.VK_MINUS, 0));
      this.mapShortCut.put(KEY_TOPIC_FOLD_ALL,
          new KeyShortcut(KEY_TOPIC_FOLD_ALL, KeyEvent.VK_MINUS, KeyEvent.ALT_MASK));
      this.mapShortCut
          .put(KEY_TOPIC_UNFOLD, new KeyShortcut(KEY_TOPIC_UNFOLD, KeyEvent.VK_EQUALS, 0));
      this.mapShortCut.put(KEY_TOPIC_UNFOLD_ALL,
          new KeyShortcut(KEY_TOPIC_UNFOLD_ALL, KeyEvent.VK_EQUALS, KeyEvent.ALT_MASK));
      this.mapShortCut.put(KEY_FOCUS_ROOT_OR_START_EDIT,
          new KeyShortcut(KEY_FOCUS_ROOT_OR_START_EDIT, KeyEvent.VK_SPACE, KeyEvent.CTRL_MASK));
      this.mapShortCut
          .put(KEY_FOCUS_MOVE_DOWN, new KeyShortcut(KEY_FOCUS_MOVE_DOWN, KeyEvent.VK_DOWN, 0));
      this.mapShortCut
          .put(KEY_FOCUS_MOVE_UP, new KeyShortcut(KEY_FOCUS_MOVE_UP, KeyEvent.VK_UP, 0));
      this.mapShortCut
          .put(KEY_FOCUS_MOVE_LEFT, new KeyShortcut(KEY_FOCUS_MOVE_LEFT, KeyEvent.VK_LEFT, 0));
      this.mapShortCut
          .put(KEY_FOCUS_MOVE_RIGHT, new KeyShortcut(KEY_FOCUS_MOVE_RIGHT, KeyEvent.VK_RIGHT, 0));
      this.mapShortCut.put(KEY_FOCUS_MOVE_DOWN_ADD_FOCUSED,
          new KeyShortcut(KEY_FOCUS_MOVE_DOWN_ADD_FOCUSED, KeyEvent.VK_DOWN, KeyEvent.SHIFT_MASK));
      this.mapShortCut.put(KEY_FOCUS_MOVE_UP_ADD_FOCUSED,
          new KeyShortcut(KEY_FOCUS_MOVE_UP_ADD_FOCUSED, KeyEvent.VK_UP, KeyEvent.SHIFT_MASK));
      this.mapShortCut.put(KEY_FOCUS_MOVE_LEFT_ADD_FOCUSED,
          new KeyShortcut(KEY_FOCUS_MOVE_LEFT_ADD_FOCUSED, KeyEvent.VK_LEFT, KeyEvent.SHIFT_MASK));
      this.mapShortCut.put(KEY_FOCUS_MOVE_RIGHT_ADD_FOCUSED,
          new KeyShortcut(KEY_FOCUS_MOVE_RIGHT_ADD_FOCUSED, KeyEvent.VK_RIGHT,
              KeyEvent.SHIFT_MASK));
      this.mapShortCut
          .put(KEY_DELETE_TOPIC, new KeyShortcut(KEY_DELETE_TOPIC, KeyEvent.VK_DELETE, 0));
      this.mapShortCut.put(KEY_TOPIC_TEXT_NEXT_LINE,
          new KeyShortcut(KEY_TOPIC_TEXT_NEXT_LINE, KeyEvent.VK_ENTER, KeyEvent.SHIFT_MASK));
      this.mapShortCut
          .put(KEY_ZOOM_IN, new KeyShortcut(KEY_ZOOM_IN, KeyEvent.VK_EQUALS, KeyEvent.CTRL_MASK));
      this.mapShortCut
          .put(KEY_ZOOM_OUT, new KeyShortcut(KEY_ZOOM_OUT, KeyEvent.VK_MINUS, KeyEvent.CTRL_MASK));
      this.mapShortCut
          .put(KEY_ZOOM_RESET, new KeyShortcut(KEY_ZOOM_RESET, KeyEvent.VK_0, KeyEvent.CTRL_MASK));
      this.mapShortCut.put(KEY_SHOW_POPUP, new KeyShortcut(KEY_SHOW_POPUP, KeyEvent.VK_SPACE,
          KeyEvent.CTRL_MASK | KeyEvent.ALT_MASK));
      this.mapShortCut.put(KEY_BIRDSEYE_MODIFIERS, new KeyShortcut(KEY_BIRDSEYE_MODIFIERS,
          KeyEvent.CTRL_MASK | KeyEvent.SHIFT_MASK));
    }
  }

  private static boolean isPreferDefaultFont() {
    final String hostLang = Locale.getDefault().getLanguage();
    return !(Locale.ENGLISH.getLanguage().equalsIgnoreCase(hostLang)
        || Locale.GERMAN.getLanguage().equalsIgnoreCase(hostLang)
        || Locale.FRENCH.getLanguage().equalsIgnoreCase(hostLang)
        || "ru".equalsIgnoreCase(hostLang)
        || "ua".equalsIgnoreCase(hostLang)
        || "es".equalsIgnoreCase(hostLang));
  }

  public static Font findDefaultFont(final int style, final int size,
                                     final String defaultFontFamily,
                                     final String[] fontNameStartsWith) {
    if (isPreferDefaultFont()) {
      return new Font(defaultFontFamily, style, size);
    } else {
      return Stream.of(
              GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames())
          .filter(x -> {
            for (final String s : fontNameStartsWith) {
              if (x.startsWith(s)) {
                return true;
              }
            }
            return false;
          })
          .map(x -> new Font(x, style, size))
          .findFirst()
          .orElse(new Font(defaultFontFamily, style, size));
    }
  }

  public static MindMapPanelConfig deserialize(final byte[] value) throws IOException {
    try {
      final ObjectInputStream stream =
          new ObjectInputStream(new ByteArrayInputStream(value));
      return (MindMapPanelConfig) stream.readObject();
    } catch (ClassNotFoundException ex) {
      throw new IOException("Can;t find class during deserialization", ex);
    }
  }

  public boolean isKeyEvent(final String id, final KeyEvent event,
                            final int modifiersMask) {
    final KeyShortcut shortCut = this.mapShortCut.get(id);
    return shortCut != null && shortCut.isEvent(event, modifiersMask);
  }

  /**
   * Check that modifiers match input event.
   *
   * @param id    identifier of shortcut
   * @param event event, can be null
   * @return true if event is not null and modifiers match
   * @since 1.6.2
   */
  public boolean isModifiers(final String id, final InputEvent event) {
    final KeyShortcut shortCut = this.mapShortCut.get(id);
    return shortCut != null && shortCut.matchModifiers(event);
  }

  public boolean isKeyEvent(final String id, final KeyEvent event) {
    return this.isKeyEvent(id, event, KeyShortcut.ALL_MODIFIERS_MASK);
  }

  @SuppressWarnings("unchecked")
  public  T getOptionalProperty(final String id, final T defaultValue) {
    synchronized (this.optionalProperties) {
      final Serializable result = this.optionalProperties.get(id);
      return (T) (result == null || result == NULL_OPTIONAL_OBJECT ? defaultValue : result);
    }
  }

  public void setOptionalProperty(final String id, final Serializable value) {
    synchronized (this.optionalProperties) {
      this.optionalProperties.put(id, value == null ? NULL_OPTIONAL_OBJECT : value);
    }
  }

  public KeyShortcut getKeyShortCut(final String id) {
    return this.mapShortCut.get(id);
  }

  public void setKeyShortCut(final KeyShortcut shortCut) {
    this.mapShortCut.put(shortCut.getID(), shortCut);
  }

  @SettingsAccessor(name = "optionalProperties")
  public Map getOptionalProperties() {
    return new HashMap<>(this.optionalProperties);
  }

  @SettingsAccessor(name = "optionalProperties")
  public void setOptionalProperties(final Map properties) {
    synchronized (this.optionalProperties) {
      this.optionalProperties.clear();
      this.optionalProperties.putAll(properties);
    }
  }

  @SettingsAccessor(name = "mapShortCut")
  public Map getKeyShortcutMap() {
    return new HashMap<>(this.mapShortCut);
  }

  @SettingsAccessor(name = "mapShortCut")
  public void setKeyShortcutMap(final Map map) {
    this.mapShortCut.clear();
    this.mapShortCut.putAll(map);
  }

  public boolean hasDifferenceInParameters(final MindMapPanelConfig etalon) {
    for (final Field f : MindMapPanelConfig.class.getDeclaredFields()) {
      if ((f.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT | Modifier.FINAL)) != 0) {
        continue;
      }
      try {
        final Object thisValue = f.get(this);
        final Object thatValue = f.get(etalon);

        if (thisValue == null && thatValue == null) {
          continue;
        }
        if (thisValue == null || !thisValue.equals(thatValue)) {
          return true;
        }

      } catch (IllegalAccessException ex) {
        throw new Error("IllegalAccessException [" + f.getName() + ']', ex);
      } catch (IllegalArgumentException ex) {
        throw new Error("IllegalArgumentException [" + f.getName() + ']', ex);
      }
    }

    return !this.mapShortCut.equals(etalon.mapShortCut)
        || !this.optionalProperties.equals(etalon.optionalProperties);
  }

  public Preferences saveTo(final Preferences prefs) {
    if (prefs != null) {
      final String prefix = MindMapPanelConfig.class.getSimpleName();

      for (final Field f : MindMapPanelConfig.class.getDeclaredFields()) {
        if ((f.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT | Modifier.FINAL)) != 0) {
          continue;
        }

        final Class fieldClass = f.getType();
        final String fieldName = prefix + '.' + f.getName();

        try {
          if (fieldClass == boolean.class) {
            prefs.putBoolean(fieldName, f.getBoolean(this));
          } else if (fieldClass == int.class) {
            prefs.putInt(fieldName, f.getInt(this));
          } else if (fieldClass == float.class) {
            prefs.putFloat(fieldName, f.getFloat(this));
          } else if (fieldClass == double.class) {
            prefs.putDouble(fieldName, f.getDouble(this));
          } else if (fieldClass == Font.class) {
            final Font theFont = (Font) f.get(this);
            prefs.put(fieldName + ".name", theFont.getName());
            prefs.putInt(fieldName + ".size", theFont.getSize());
            prefs.putInt(fieldName + ".style", theFont.getStyle());
          } else if (fieldClass == Color.class) {
            prefs.putInt(fieldName, ((Color) f.get(this)).getRGB());
          } else if (fieldClass == String.class) {
            prefs.put(fieldName, (String) f.get(this));
          } else if (fieldClass == RenderQuality.class) {
            prefs.put(fieldName, ((RenderQuality) f.get(this)).name());
          } else if (fieldClass == MouseButton.class) {
            prefs.put(fieldName, ((MouseButton) f.get(this)).name());
          } else if (fieldClass == Map.class) {
            if (f.getName().equals("mapShortCut")) {
              for (final Map.Entry e : this.mapShortCut.entrySet()) {
                prefs.put(PREFIX_SHORTCUT + e.getValue().getID(), e.getValue().packToString());
              }
            } else {
              synchronized (this.optionalProperties) {
                for (final Map.Entry e : this.optionalProperties.entrySet()) {
                  final String key = PREFIX_OPTIONAL_PROPERTY + e.getKey();
                  if (e.getValue() == NULL_OPTIONAL_OBJECT) {
                    prefs.remove(key);
                  } else {
                    final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                    try {
                      final ObjectOutputStream objectOutputStream =
                          new ObjectOutputStream(outputStream);
                      objectOutputStream.writeObject(e.getValue());
                      objectOutputStream.close();
                      final String encodedValue =
                          Base64.getEncoder().encodeToString(outputStream.toByteArray());
                      prefs.put(key, encodedValue);
                    } catch (final IOException ex) {
                      throw new RuntimeException("Error during write optional property: " + e, ex);
                    }
                  }
                }
                this.optionalProperties.values().removeIf(x -> x == NULL_OPTIONAL_OBJECT);
              }
            }
          } else {
            throw new Error("Unexpected field type " + fieldClass.getName());
          }
        } catch (IllegalAccessException ex) {
          throw new Error("IllegalAccessException [" + fieldClass.getName() + ']', ex);
        } catch (IllegalArgumentException ex) {
          throw new Error("IllegalArgumentException [" + fieldClass.getName() + ']', ex);
        }
      }
    }
    return prefs;
  }

  public Preferences loadFrom(final Preferences prefs) {
    if (prefs != null) {
      final String prefix = MindMapPanelConfig.class.getSimpleName();

      final MindMapPanelConfig etalon = new MindMapPanelConfig();

      for (final Field f : MindMapPanelConfig.class.getDeclaredFields()) {
        if ((f.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT | Modifier.FINAL)) != 0) {
          continue;
        }

        final Class fieldClass = f.getType();
        final String fieldName = prefix + '.' + f.getName();

        try {
          if (fieldClass == boolean.class) {
            f.setBoolean(this, prefs.getBoolean(fieldName, f.getBoolean(etalon)));
          } else if (fieldClass == int.class) {
            f.setInt(this, prefs.getInt(fieldName, f.getInt(etalon)));
          } else if (fieldClass == float.class) {
            f.setFloat(this, prefs.getFloat(fieldName, f.getFloat(etalon)));
          } else if (fieldClass == double.class) {
            f.setDouble(this, prefs.getDouble(fieldName, f.getDouble(etalon)));
          } else if (fieldClass == Font.class) {
            final Font etalonFont = etalon.getFont();

            final String fontName = prefs.get(fieldName + ".name", etalonFont.getName());
            final int fontSize = prefs.getInt(fieldName + ".size", etalonFont.getSize());
            final int fontStyle = prefs.getInt(fieldName + ".style", etalonFont.getStyle());

            f.set(this, new Font(fontName, fontStyle, fontSize));
          } else if (fieldClass == Color.class) {
            final int argb = prefs.getInt(fieldName, ((Color) f.get(etalon)).getRGB());
            f.set(this, new Color(argb, true));
          } else if (fieldClass == String.class) {
            f.set(this, prefs.get(fieldName, (String) f.get(etalon)));
          } else if (fieldClass == RenderQuality.class) {
            final String name = prefs.get(fieldName, ((RenderQuality) f.get(etalon)).name());
            f.set(this, RenderQuality.valueOf(name));
          } else if (fieldClass == MouseButton.class) {
            final String name = prefs.get(fieldName, ((MouseButton) f.get(etalon)).name());
            f.set(this, MouseButton.valueOf(name));
          } else if (fieldClass == Map.class) {
            if (f.getName().equals("mapShortCut")) {
              this.mapShortCut.clear();
              this.mapShortCut.putAll(etalon.mapShortCut);
              try {
                for (final String k : prefs.keys()) {
                  if (k.startsWith(PREFIX_SHORTCUT)) {
//            final int dotIndex = k.indexOf('.');
//            final String id = k.substring(dotIndex + 1);
                    final String packedValue = prefs.get(k, "");
                    if (packedValue.isEmpty()) {
                      throw new Error("Unexpected situation, short cut value is empty [" + k + ']');
                    }
                    final KeyShortcut unpacked = new KeyShortcut(packedValue);
                    this.mapShortCut.put(unpacked.getID(), unpacked);
                  }
                }
              } catch (BackingStoreException ex) {
                throw new Error("Can't get list of keys from storage", ex);
              }
            } else {
              this.optionalProperties.clear();
              this.optionalProperties.putAll(etalon.optionalProperties);

              try {
                for (final String k : prefs.keys()) {
                  if (k.startsWith(PREFIX_OPTIONAL_PROPERTY)) {
                    final String propertyName = k.substring(k.indexOf('.') + 1);
                    final String value = prefs.get(k, null);
                    if (value == null) {
                      throw new Error("Unexpected situation, property value is null [" + k + ']');
                    }
                    try {
                      final Serializable readValue =
                          (Serializable) new ObjectInputStream(
                              new ByteArrayInputStream(
                                  Base64.getDecoder().decode(value))).readObject();
                      if (readValue != null) {
                        this.optionalProperties.put(propertyName, readValue);
                      }
                    } catch (ClassNotFoundException | IOException ex) {
                      // ignore error to save possibility to load config
                    }
                  }
                }
              } catch (BackingStoreException ex) {
                throw new Error("Can't get list of keys from storage", ex);
              }
            }
          } else {
            throw new Error("Unexpected field type " + fieldClass.getName());
          }
        } catch (IllegalAccessException ex) {
          throw new Error("IllegalAccessException [" + fieldClass.getName() + ']', ex);
        } catch (IllegalArgumentException ex) {
          throw new Error("IllegalArgumentException [" + fieldClass.getName() + ']', ex);
        }
      }
    }
    return prefs;
  }

  public void makeAtomicChange(final Runnable runnable) {
    this.notificationEnabled = false;
    try {
      runnable.run();
    } finally {
      this.notificationEnabled = true;
      this.notifyCfgListenersAboutChange();
    }
  }

  @Override
  public int hashCode() {
    return Objects.hash(this.optionalProperties, this.mapShortCut);
  }

  @Override
  public boolean equals(final Object object) {
    if (object == null) {
      return false;
    }
    if (object == this) {
      return true;
    }
    return object instanceof MindMapPanelConfig &&
        !this.hasDifferenceInParameters((MindMapPanelConfig) object);
  }

  public float safeScaleFloatValue(final float value, final float minimal) {
    final float result = (float) (this.scale * (double) value);
    return Float.compare(result, minimal) >= 0 ? result : minimal;
  }

  private static Font cloneFont(final Font font) {
    return new Font(font.getName(), font.getStyle(), font.getSize());
  }
  public void makeFullCopyOf(final MindMapPanelConfig src, final boolean copyListeners,
                             final boolean makeNotification) {
    if (src != null) {
      for (final Field f : MindMapPanelConfig.class.getDeclaredFields()) {
        if (f.getName().equals("listeners")) {
          if (copyListeners) {
            this.listeners.clear();
            this.listeners.addAll(src.listeners);
          }
        } else if ((f.getModifiers() & (Modifier.STATIC | Modifier.FINAL)) == 0 &&
            f.getType() != Map.class) {
          try {
            Object value = f.get(src);
            if (value instanceof Font) {
              final Font font = (Font)value;
              value = cloneFont(font); // prevent possible bugs in deserialization of font object in IDE like IDEA
            }
            f.set(this, value);
          } catch (Exception ex) {
            throw new Error("Unexpected state during cloning field " + f, ex);
          }
        }
      }

      this.mapShortCut.clear();
      this.mapShortCut.putAll(src.mapShortCut);

      this.optionalProperties.clear();
      this.optionalProperties.putAll(src.optionalProperties);

      if (makeNotification) {
        this.notifyCfgListenersAboutChange();
      }
    }
  }

  public void addConfigurationListener(final MindMapConfigListener l) {
    this.listeners.add(new WeakReference<>(requireNonNull(l)));
  }

  public void removeConfigurationListener(final MindMapConfigListener l) {
    this.listeners.removeIf(x -> {
      final MindMapConfigListener listener = x.get();
      return listener == null || listener.equals(l);
    });
  }

  public boolean isShortcutConflict(final KeyStroke keyStroke) {
    boolean result = false;
    if (keyStroke != null) {
      for (final KeyShortcut s : this.mapShortCut.values()) {
        if (s.doesConflictWith(keyStroke)) {
          result = true;
          break;
        }
      }
    }
    return result;
  }

  private void notifyCfgListenersAboutChange() {
    if (this.notificationEnabled) {
      this.listeners.stream().map(Reference::get)
          .filter(Objects::nonNull)
          .forEach(x -> x.onConfigurationPropertyChanged(this));
    }
    this.listeners.removeIf(x -> x.get() == null);
  }

  public byte[] serialize() throws IOException {
    final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
    final ObjectOutputStream objectStream = new ObjectOutputStream(outStream);
    objectStream.writeObject(this);
    objectStream.close();
    return outStream.toByteArray();
  }

  public boolean isKeyEventDetected(final KeyEvent event,
                                    final int effectiveModifiers,
                                    final String... shortCutIDs) {
    for (final String k : shortCutIDs) {
      final KeyShortcut shortCut = this.mapShortCut.get(k);
      if (shortCut != null && shortCut.isEvent(event, effectiveModifiers)) {
        return true;
      }
    }
    return false;
  }

  public boolean isKeyEventDetected(final KeyEvent event,
                                    final String... shortCutIDs) {
    for (final String k : shortCutIDs) {
      final KeyShortcut shortCut = this.mapShortCut.get(k);
      if (shortCut != null && shortCut.isEvent(event, KeyShortcut.ALL_MODIFIERS_MASK)) {
        return true;
      }
    }
    return false;
  }

  @SettingsAccessor(name = "horizontalBlockGap")
  public int getHorizontalBlockGap() {
    return this.horizontalBlockGap;
  }

  @SettingsAccessor(name = "horizontalBlockGap")
  public void setHorizontalBlockGap(final int gap) {
    this.horizontalBlockGap = gap;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "selectLineWidth")
  public float getSelectLineWidth() {
    return this.selectLineWidth;
  }

  @SettingsAccessor(name = "selectLineWidth")
  public void setSelectLineWidth(final float f) {
    this.selectLineWidth = f;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "jumpLinkWidth")
  public float getJumpLinkWidth() {
    return this.jumpLinkWidth;
  }

  @SettingsAccessor(name = "jumpLinkWidth")
  public void setJumpLinkWidth(final float f) {
    this.jumpLinkWidth = f;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "jumpLinkColor")
  public Color getJumpLinkColor() {
    return this.jumpLinkColor;
  }

  @SettingsAccessor(name = "jumpLinkColor")
  public void setJumpLinkColor(final Color color) {
    this.jumpLinkColor = color;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "birdseyeBackground")
  public Color getBirdseyeBackground() {
    return this.birdseyeBackground;
  }

  @SettingsAccessor(name = "birdseyeBackground")
  public void setBirdseyeBackground(final Color color) {
    this.birdseyeBackground = color;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "birdseyeFront")
  public Color getBirdseyeFront() {
    return this.birdseyeFront;
  }

  @SettingsAccessor(name = "birdseyeFront")
  public void setBirdseyeFront(final Color color) {
    this.birdseyeFront = color;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "scaleModifiers")
  public int getScaleModifiers() {
    return this.scaleModifiers;
  }

  @SettingsAccessor(name = "scaleModifiers")
  public void setScaleModifiers(final int value) {
    this.scaleModifiers = value;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "selectLineColor")
  public Color getSelectLineColor() {
    return this.selectLineColor;
  }

  @SettingsAccessor(name = "selectLineColor")
  public void setSelectLineColor(final Color color) {
    this.selectLineColor = color;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "paperMargins")
  public int getPaperMargins() {
    return this.paperMargins;
  }

  @SettingsAccessor(name = "paperMargins")
  public void setPaperMargins(final int size) {
    this.paperMargins = size;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "smartTextPaste")
  public boolean isSmartTextPaste() {
    return this.smartTextPaste;
  }

  @SettingsAccessor(name = "smartTextPaste")
  public void setSmartTextPaste(final boolean flag) {
    this.smartTextPaste = flag;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "drawBackground")
  public boolean isDrawBackground() {
    return this.drawBackground;
  }

  @SettingsAccessor(name = "drawBackground")
  public void setDrawBackground(final boolean flag) {
    this.drawBackground = flag;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "otherLevelVerticalInset")
  public int getOtherLevelVerticalInset() {
    return this.otherLevelVerticalInset;
  }

  @SettingsAccessor(name = "otherLevelVerticalInset")
  public void setOtherLevelVerticalInset(final int value) {
    this.otherLevelVerticalInset = value;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "otherLevelHorizontalInset")
  public int getOtherLevelHorizontalInset() {
    return this.otherLevelHorizontalInset;
  }

  @SettingsAccessor(name = "otherLevelHorizontalInset")
  public void setOtherLevelHorizontalInset(final int value) {
    this.otherLevelHorizontalInset = value;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "firstLevelVerticalInset")
  public int getFirstLevelVerticalInset() {
    return this.firstLevelVerticalInset;
  }

  @SettingsAccessor(name = "firstLevelVerticalInset")
  public void setFirstLevelVerticalInset(final int value) {
    this.firstLevelVerticalInset = value;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "firstLevelHorizontalInset")
  public int getFirstLevelHorizontalInset() {
    return this.firstLevelHorizontalInset;
  }

  @SettingsAccessor(name = "firstLevelHorizontalInset")
  public void setFirstLevelHorizontalInset(final int value) {
    this.firstLevelHorizontalInset = value;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "paperColor")
  public Color getPaperColor() {
    return this.paperColor;
  }

  @SettingsAccessor(name = "paperColor")
  public void setPaperColor(final Color color) {
    this.paperColor = requireNonNull(color);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "gridColor")
  public Color getGridColor() {
    return this.gridColor;
  }

  @SettingsAccessor(name = "gridColor")
  public void setGridColor(final Color color) {
    this.gridColor = color;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "showGrid")
  public boolean isShowGrid() {
    return this.showGrid;
  }

  @SettingsAccessor(name = "showGrid")
  public void setShowGrid(final boolean flag) {
    this.showGrid = flag;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "gridStep")
  public int getGridStep() {
    return this.gridStep;
  }

  @SettingsAccessor(name = "gridStep")
  public void setGridStep(final int step) {
    this.gridStep = step;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "rootBackgroundColor")
  public Color getRootBackgroundColor() {
    return this.rootBackgroundColor;
  }

  @SettingsAccessor(name = "rootBackgroundColor")
  public void setRootBackgroundColor(final Color color) {
    this.rootBackgroundColor = requireNonNull(color);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "firstLevelBackgroundColor")
  public Color getFirstLevelBackgroundColor() {
    return this.firstLevelBackgroundColor;
  }

  @SettingsAccessor(name = "firstLevelBackgroundColor")
  public void setFirstLevelBackgroundColor(final Color color) {
    this.firstLevelBackgroundColor = color;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "otherLevelBackgroundColor")
  public Color getOtherLevelBackgroundColor() {
    return this.otherLevelBackgroundColor;
  }

  @SettingsAccessor(name = "otherLevelBackgroundColor")
  public void setOtherLevelBackgroundColor(final Color color) {
    this.otherLevelBackgroundColor = color;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "rootTextColor")
  public Color getRootTextColor() {
    return this.rootTextColor;
  }

  @SettingsAccessor(name = "rootTextColor")
  public void setRootTextColor(final Color color) {
    this.rootTextColor = requireNonNull(color);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "firstLevelTextColor")
  public Color getFirstLevelTextColor() {
    return this.firstLevelTextColor;
  }

  @SettingsAccessor(name = "firstLevelTextColor")
  public void setFirstLevelTextColor(final Color color) {
    this.firstLevelTextColor = requireNonNull(color);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "otherLevelTextColor")
  public Color getOtherLevelTextColor() {
    return this.otherLevelTextColor;
  }

  @SettingsAccessor(name = "otherLevelTextColor")
  public void setOtherLevelTextColor(final Color color) {
    this.otherLevelTextColor = requireNonNull(color);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "elementBorderColor")
  public Color getElementBorderColor() {
    return this.elementBorderColor;
  }

  @SettingsAccessor(name = "elementBorderColor")
  public void setElementBorderColor(final Color color) {
    this.elementBorderColor = requireNonNull(color);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "connectorColor")
  public Color getConnectorColor() {
    return this.connectorColor;
  }

  @SettingsAccessor(name = "connectorColor")
  public void setConnectorColor(final Color color) {
    this.connectorColor = requireNonNull(color);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "shadowColor")
  public Color getShadowColor() {
    return this.shadowColor;
  }

  @SettingsAccessor(name = "shadowColor")
  public void setShadowColor(final Color color) {
    this.shadowColor = requireNonNull(color);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "collapsatorBorderColor")
  public Color getCollapsatorBorderColor() {
    return this.collapsatorBorderColor;
  }

  @SettingsAccessor(name = "collapsatorBorderColor")
  public void setCollapsatorBorderColor(final Color color) {
    this.collapsatorBorderColor = requireNonNull(color);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "collapsatorBackgroundColor")
  public Color getCollapsatorBackgroundColor() {
    return this.collapsatorBackgroundColor;
  }

  @SettingsAccessor(name = "collapsatorBackgroundColor")
  public void setCollapsatorBackgroundColor(final Color color) {
    this.collapsatorBackgroundColor = requireNonNull(color);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "elementBorderWidth")
  public float getElementBorderWidth() {
    return this.elementBorderWidth;
  }

  @SettingsAccessor(name = "elementBorderWidth")
  public void setElementBorderWidth(final float value) {
    this.elementBorderWidth = value;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "collapsatorBorderWidth")
  public float getCollapsatorBorderWidth() {
    return this.collapsatorBorderWidth;
  }

  @SettingsAccessor(name = "collapsatorBorderWidth")
  public void setCollapsatorBorderWidth(final float width) {
    this.collapsatorBorderWidth = width;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "shadowOffset")
  public float getShadowOffset() {
    return this.shadowOffset;
  }

  @SettingsAccessor(name = "shadowOffset")
  public void setShadowOffset(final float value) {
    this.shadowOffset = value;
  }

  @SettingsAccessor(name = "connectorWidth")
  public float getConnectorWidth() {
    return this.connectorWidth;
  }

  @SettingsAccessor(name = "connectorWidth")
  public void setConnectorWidth(final float value) {
    this.connectorWidth = value;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "font")
  public Font getFont() {
    return this.font;
  }

  @SettingsAccessor(name = "font")
  public void setFont(final Font f) {
    this.font = requireNonNull(f);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "scale")
  public double getScale() {
    return this.scale;
  }

  @SettingsAccessor(name = "scale")
  public void setScale(final double value) {
    this.scale = Math.max(0.01d, value);
    notifyCfgListenersAboutChange();
  }

  public void setScaleWithoutListenerNotification(final double value) {
    this.scale = Math.max(0.01d, value);
  }

  @SettingsAccessor(name = "dropShadow")
  public boolean isDropShadow() {
    return this.dropShadow;
  }

  @SettingsAccessor(name = "dropShadow")
  public void setDropShadow(final boolean value) {
    this.dropShadow = value;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "renderQuality")
  public RenderQuality getRenderQuality() {
    return this.renderQuality;
  }

  @SettingsAccessor(name = "renderQuality")
  public void setRenderQuality(final RenderQuality value) {
    this.renderQuality = MiscUtils.ensureNotNull(value, Utils.getDefaultRenderQialityForOs());
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "birdseyeMouseButton")
  public MouseButton getBirdseyeMouseButton() {
    return this.birdseyeMouseButton;
  }

  @SettingsAccessor(name = "birdseyeMouseButton")
  public void setBirdseyeMouseButton(final MouseButton value) {
    this.birdseyeMouseButton = MiscUtils.ensureNotNull(value, MouseButton.BUTTON_3);
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "collapsatorSize")
  public int getCollapsatorSize() {
    return this.collapsatorSize;
  }

  @SettingsAccessor(name = "collapsatorSize")
  public void setCollapsatorSize(final int size) {
    this.collapsatorSize = size;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "textMargins")
  public int getTextMargins() {
    return this.textMargins;
  }

  @SettingsAccessor(name = "textMargins")
  public void setTextMargins(final int value) {
    this.textMargins = value;
    notifyCfgListenersAboutChange();
  }

  @SettingsAccessor(name = "selectLineGap")
  public int getSelectLineGap() {
    return this.selectLineGap;
  }

  @SettingsAccessor(name = "selectLineGap")
  public void setSelectLineGap(final int value) {
    this.selectLineGap = value;
    notifyCfgListenersAboutChange();
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy