a946ceb3663b3361d263d48fe4c82db41fb0efb8
[idea/community.git] / platform / editor-ui-ex / src / com / intellij / openapi / editor / colors / impl / AbstractColorsScheme.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.openapi.editor.colors.impl;
18
19 import com.intellij.application.options.EditorFontsConstants;
20 import com.intellij.configurationStore.SerializableScheme;
21 import com.intellij.ide.ui.ColorBlindness;
22 import com.intellij.ide.ui.UISettings;
23 import com.intellij.openapi.application.ex.ApplicationInfoEx;
24 import com.intellij.openapi.diagnostic.Logger;
25 import com.intellij.openapi.editor.HighlighterColors;
26 import com.intellij.openapi.editor.colors.*;
27 import com.intellij.openapi.editor.colors.ex.DefaultColorSchemesManager;
28 import com.intellij.openapi.editor.markup.EffectType;
29 import com.intellij.openapi.editor.markup.TextAttributes;
30 import com.intellij.openapi.options.FontSize;
31 import com.intellij.openapi.options.SchemeState;
32 import com.intellij.openapi.util.*;
33 import com.intellij.util.JdomKt;
34 import com.intellij.util.PlatformUtils;
35 import com.intellij.util.containers.ContainerUtilRt;
36 import com.intellij.util.containers.HashMap;
37 import gnu.trove.THashMap;
38 import org.jdom.Element;
39 import org.jetbrains.annotations.NonNls;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import java.awt.*;
44 import java.text.SimpleDateFormat;
45 import java.util.*;
46 import java.util.List;
47 import java.util.function.Function;
48 import java.util.function.Predicate;
49
50 import static com.intellij.openapi.editor.colors.CodeInsightColors.*;
51 import static com.intellij.openapi.editor.colors.EditorColors.*;
52 import static com.intellij.openapi.editor.markup.TextAttributes.USE_INHERITED_MARKER;
53 import static com.intellij.openapi.util.Couple.of;
54 import static com.intellij.ui.ColorUtil.fromHex;
55
56 @SuppressWarnings("UseJBColor")
57 public abstract class AbstractColorsScheme extends EditorFontCacheImpl implements EditorColorsScheme, SerializableScheme {
58   private static final int CURR_VERSION = 142;
59
60   // todo: unify with UIUtil.DEF_SYSTEM_FONT_SIZE
61   private static final FontSize DEFAULT_FONT_SIZE = FontSize.SMALL;
62
63   protected EditorColorsScheme myParentScheme;
64
65   protected FontSize myQuickDocFontSize = DEFAULT_FONT_SIZE;
66
67   @NotNull private FontPreferences                 myFontPreferences
68     = new DelegatingFontPreferences(() -> AppEditorFontOptions.getInstance().getFontPreferences());
69   @NotNull private FontPreferences                 myConsoleFontPreferences = new DelegatingFontPreferences(() -> myFontPreferences);
70
71   private final ValueElementReader myValueReader = new TextAttributesReader();
72   private String mySchemeName;
73
74   private boolean myIsSaveNeeded;
75
76   private boolean myCanBeDeleted = true;
77
78   // version influences XML format and triggers migration
79   private int myVersion = CURR_VERSION;
80
81   protected Map<ColorKey, Color>                   myColorsMap     = ContainerUtilRt.newHashMap();
82   protected Map<TextAttributesKey, TextAttributes> myAttributesMap = new THashMap<>();
83
84   @NonNls private static final String EDITOR_FONT       = "font";
85   @NonNls private static final String CONSOLE_FONT      = "console-font";
86   @NonNls private static final String EDITOR_FONT_NAME  = "EDITOR_FONT_NAME";
87   @NonNls private static final String CONSOLE_FONT_NAME = "CONSOLE_FONT_NAME";
88   private                      Color  myDeprecatedBackgroundColor    = null;
89   @NonNls private static final String SCHEME_ELEMENT                 = "scheme";
90   @NonNls public static final  String NAME_ATTR                      = "name";
91   @NonNls private static final String VERSION_ATTR                   = "version";
92   @NonNls private static final String BASE_ATTRIBUTES_ATTR           = "baseAttributes";
93   @NonNls private static final String DEFAULT_SCHEME_ATTR            = "default_scheme";
94   @NonNls private static final String PARENT_SCHEME_ATTR             = "parent_scheme";
95   @NonNls private static final String OPTION_ELEMENT                 = "option";
96   @NonNls private static final String COLORS_ELEMENT                 = "colors";
97   @NonNls private static final String ATTRIBUTES_ELEMENT             = "attributes";
98   @NonNls private static final String VALUE_ELEMENT                  = "value";
99   @NonNls private static final String BACKGROUND_COLOR_NAME          = "BACKGROUND";
100   @NonNls private static final String LINE_SPACING                   = "LINE_SPACING";
101   @NonNls private static final String CONSOLE_LINE_SPACING           = "CONSOLE_LINE_SPACING";
102   @NonNls private static final String FONT_SCALE                     = "FONT_SCALE";
103   @NonNls private static final String EDITOR_FONT_SIZE               = "EDITOR_FONT_SIZE";
104   @NonNls private static final String CONSOLE_FONT_SIZE              = "CONSOLE_FONT_SIZE";
105   @NonNls private static final String EDITOR_LIGATURES               = "EDITOR_LIGATURES";
106   @NonNls private static final String CONSOLE_LIGATURES              = "CONSOLE_LIGATURES";
107   @NonNls private static final String EDITOR_QUICK_JAVADOC_FONT_SIZE = "EDITOR_QUICK_DOC_FONT_SIZE";
108
109
110   //region Meta info-related fields
111   private final Properties myMetaInfo = new Properties();
112   private final static SimpleDateFormat META_INFO_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
113
114   @NonNls private static final String META_INFO_ELEMENT       = "metaInfo";
115   @NonNls private static final String PROPERTY_ELEMENT        = "property";
116   @NonNls private static final String PROPERTY_NAME_ATTR      = "name";
117
118   @NonNls private static final String META_INFO_CREATION_TIME = "created";
119   @NonNls private static final String META_INFO_MODIFIED_TIME = "modified";
120   @NonNls private static final String META_INFO_IDE           = "ide";
121   @NonNls private static final String META_INFO_IDE_VERSION   = "ideVersion";
122   @NonNls private static final String META_INFO_ORIGINAL      = "originalScheme";
123
124   //endregion
125
126   protected AbstractColorsScheme(EditorColorsScheme parentScheme) {
127     myParentScheme = parentScheme;
128   }
129
130   public AbstractColorsScheme() {
131   }
132
133   public void setDefaultMetaInfo(@Nullable AbstractColorsScheme parentScheme) {
134     myMetaInfo.setProperty(META_INFO_CREATION_TIME, META_INFO_DATE_FORMAT.format(new Date()));
135     myMetaInfo.setProperty(META_INFO_IDE,           PlatformUtils.getPlatformPrefix());
136     myMetaInfo.setProperty(META_INFO_IDE_VERSION,   ApplicationInfoEx.getInstanceEx().getStrictVersion());
137     if (parentScheme != null && parentScheme != EmptyColorScheme.INSTANCE) {
138       myMetaInfo.setProperty(META_INFO_ORIGINAL, parentScheme.getName());
139     }
140   }
141
142   @NotNull
143   @Override
144   public Color getDefaultBackground() {
145     final Color c = getAttributes(HighlighterColors.TEXT).getBackgroundColor();
146     return c != null ? c : Color.white;
147   }
148
149   @NotNull
150   @Override
151   public Color getDefaultForeground() {
152     final Color c = getAttributes(HighlighterColors.TEXT).getForegroundColor();
153     return c != null ? c : Color.black;
154   }
155
156   @NotNull
157   @Override
158   public String getName() {
159     return mySchemeName;
160   }
161
162   @Override
163   public void setFont(EditorFontType key, Font font) {
164   }
165
166   @Override
167   public abstract Object clone();
168
169   public void copyTo(AbstractColorsScheme newScheme) {
170     newScheme.myQuickDocFontSize = myQuickDocFontSize;
171     if (myConsoleFontPreferences instanceof DelegatingFontPreferences) {
172       newScheme.setUseEditorFontPreferencesInConsole();
173     }
174     else {
175       newScheme.setConsoleFontPreferences(myConsoleFontPreferences);
176     }
177     if (myFontPreferences instanceof DelegatingFontPreferences) {
178       newScheme.setUseAppFontPreferencesInEditor();
179     }
180     else {
181       newScheme.setFontPreferences(myFontPreferences);
182     }
183
184     newScheme.myAttributesMap = new THashMap<>(myAttributesMap);
185     newScheme.myColorsMap = new HashMap<>(myColorsMap);
186     newScheme.myVersion = myVersion;
187   }
188
189   public void clearColors(@NotNull Predicate<ColorKey> predicate) {
190     Iterator<ColorKey> iterator = myColorsMap.keySet().iterator();
191     while (iterator.hasNext()) {
192       ColorKey key = iterator.next();
193       if (predicate.test(key)) iterator.remove();
194     }
195   }
196
197   public Map<ColorKey,Color> getColors(@NotNull Predicate<ColorKey> predicate) {
198     Map<ColorKey,Color> colorMap = ContainerUtilRt.newHashMap();
199     for (ColorKey key : myColorsMap.keySet()) {
200       if (predicate.test(key)) {
201         colorMap.put(key, myColorsMap.get(key));
202       }
203     }
204     return colorMap;
205   }
206
207   @Override
208   public void setEditorFontName(String fontName) {
209     int editorFontSize = getEditorFontSize();
210     ensureEditableFontPreferences().clear();
211     ensureEditableFontPreferences().register(fontName, editorFontSize);
212     initFonts();
213   }
214
215   @Override
216   public void setEditorFontSize(int fontSize) {
217     fontSize = EditorFontsConstants.checkAndFixEditorFontSize(fontSize);
218     ensureEditableFontPreferences().register(myFontPreferences.getFontFamily(), fontSize);
219     initFonts();
220   }
221
222   @Override
223   public void setUseAppFontPreferencesInEditor() {
224     myFontPreferences = new DelegatingFontPreferences(()-> AppEditorFontOptions.getInstance().getFontPreferences());
225     initFonts();
226   }
227
228   @Override
229   public boolean isUseAppFontPreferencesInEditor() {
230     return myFontPreferences instanceof DelegatingFontPreferences;
231   }
232
233   @Override
234   public void setQuickDocFontSize(@NotNull FontSize fontSize) {
235     if (myQuickDocFontSize != fontSize) {
236       myQuickDocFontSize = fontSize;
237       myIsSaveNeeded = true;
238     }
239   }
240
241   @Override
242   public void setLineSpacing(float lineSpacing) {
243     ensureEditableFontPreferences().setLineSpacing(lineSpacing);
244   }
245
246   @NotNull
247   @Override
248   public Font getFont(EditorFontType key) {
249     return myFontPreferences instanceof DelegatingFontPreferences ? EditorFontCache.getInstance().getFont(key) : super.getFont(key);
250   }
251
252   @Override
253   public void setName(@NotNull String name) {
254     mySchemeName = name;
255   }
256
257   @NotNull
258   @Override
259   public FontPreferences getFontPreferences() {
260     return myFontPreferences;
261   }
262
263   @Override
264   public void setFontPreferences(@NotNull FontPreferences preferences) {
265     preferences.copyTo(ensureEditableFontPreferences());
266     initFonts();
267   }
268
269   @Override
270   public String getEditorFontName() {
271     return getFont(EditorFontType.PLAIN).getFamily();
272   }
273
274   @Override
275   public int getEditorFontSize() {
276     return myFontPreferences.getSize(myFontPreferences.getFontFamily());
277   }
278
279   @NotNull
280   @Override
281   public FontSize getQuickDocFontSize() {
282     return myQuickDocFontSize;
283   }
284
285   @Override
286   public float getLineSpacing() {
287     return myFontPreferences.getLineSpacing();
288   }
289
290   protected void initFonts() {
291     reset();
292   }
293
294   @Override
295   protected EditorColorsScheme getFontCacheScheme() {
296     return this;
297   }
298
299   public String toString() {
300     return getName();
301   }
302
303   @Override
304   public void readExternal(@NotNull Element parentNode) {
305     UISettings settings = UISettings.getInstanceOrNull();
306     ColorBlindness blindness = settings == null ? null : settings.getColorBlindness();
307     myValueReader.setAttribute(blindness == null ? null : blindness.name());
308     if (SCHEME_ELEMENT.equals(parentNode.getName())) {
309       readScheme(parentNode);
310     }
311     else {
312       List<Element> children = parentNode.getChildren(SCHEME_ELEMENT);
313       if (children.isEmpty()) {
314         throw new InvalidDataException("Scheme is not valid");
315       }
316
317       for (Element element : children) {
318         readScheme(element);
319       }
320     }
321     initFonts();
322     myVersion = CURR_VERSION;
323   }
324
325   private void readScheme(Element node) {
326     myDeprecatedBackgroundColor = null;
327     if (!SCHEME_ELEMENT.equals(node.getName())) {
328       return;
329     }
330
331     setName(node.getAttributeValue(NAME_ATTR));
332     int readVersion = Integer.parseInt(node.getAttributeValue(VERSION_ATTR, "0"));
333     if (readVersion > CURR_VERSION) {
334       throw new IllegalStateException("Unsupported color scheme version: " + readVersion);
335     }
336
337     myVersion = readVersion;
338     String isDefaultScheme = node.getAttributeValue(DEFAULT_SCHEME_ATTR);
339     boolean isDefault = isDefaultScheme != null && Boolean.parseBoolean(isDefaultScheme);
340     if (!isDefault) {
341       myParentScheme = getDefaultScheme(node.getAttributeValue(PARENT_SCHEME_ATTR, EmptyColorScheme.NAME));
342     }
343
344     myMetaInfo.clear();
345     Ref<Float> fontScale = Ref.create();
346     boolean clearEditorFonts = true;
347     boolean clearConsoleFonts = true;
348     for (Element childNode : node.getChildren()) {
349       String childName = childNode.getName();
350       switch (childName) {
351         case OPTION_ELEMENT:
352           readSettings(childNode, isDefault, fontScale);
353           break;
354         case EDITOR_FONT:
355           readFontSettings(ensureEditableFontPreferences(), childNode, isDefault, fontScale.get(), clearEditorFonts);
356           clearEditorFonts = false;
357           break;
358         case CONSOLE_FONT:
359           readFontSettings(ensureEditableConsoleFontPreferences(), childNode, isDefault, fontScale.get(), clearConsoleFonts);
360           clearConsoleFonts = false;
361           break;
362         case COLORS_ELEMENT:
363           readColors(childNode);
364           break;
365         case ATTRIBUTES_ELEMENT:
366           readAttributes(childNode);
367           break;
368         case META_INFO_ELEMENT:
369           readMetaInfo(childNode);
370           break;
371       }
372     }
373
374     if (myDeprecatedBackgroundColor != null) {
375       TextAttributes textAttributes = myAttributesMap.get(HighlighterColors.TEXT);
376       if (textAttributes == null) {
377         textAttributes = new TextAttributes(Color.black, myDeprecatedBackgroundColor, null, EffectType.BOXED, Font.PLAIN);
378         myAttributesMap.put(HighlighterColors.TEXT, textAttributes);
379       }
380       else {
381         textAttributes.setBackgroundColor(myDeprecatedBackgroundColor);
382       }
383     }
384
385     if (myConsoleFontPreferences.getEffectiveFontFamilies().isEmpty()) {
386       myFontPreferences.copyTo(myConsoleFontPreferences);
387     }
388
389     initFonts();
390   }
391
392   @NotNull
393   private static EditorColorsScheme getDefaultScheme(@NotNull String name) {
394     DefaultColorSchemesManager manager = DefaultColorSchemesManager.getInstance();
395     EditorColorsScheme defaultScheme = manager.getScheme(name);
396     if (defaultScheme == null) {
397       defaultScheme = new TemporaryParent(name);
398     }
399     return defaultScheme;
400   }
401
402
403   private void readMetaInfo(@NotNull Element metaInfoElement) {
404     myMetaInfo.clear();
405     for (Element e: metaInfoElement.getChildren()) {
406       if (PROPERTY_ELEMENT.equals(e.getName())) {
407         String propertyName = e.getAttributeValue(PROPERTY_NAME_ATTR);
408         if (propertyName != null) {
409           myMetaInfo.setProperty(propertyName, e.getText());
410         }
411       }
412     }
413   }
414
415   public void readAttributes(@NotNull Element childNode) {
416     for (Element e : childNode.getChildren(OPTION_ELEMENT)) {
417       Element valueElement = e.getChild(VALUE_ELEMENT);
418       TextAttributesKey key = TextAttributesKey.find(e.getAttributeValue(NAME_ATTR));
419       if (valueElement == null) {
420         if (e.getAttributeValue(BASE_ATTRIBUTES_ATTR) != null) {
421           myAttributesMap.put(key, USE_INHERITED_MARKER);
422         }
423         continue;
424       }
425       TextAttributes attr = myValueReader.read(TextAttributes.class, valueElement);
426       if (attr != null) {
427         myAttributesMap.put(key, attr);
428         migrateErrorStripeColorFrom14(key, attr);
429       }
430     }
431   }
432
433   private void migrateErrorStripeColorFrom14(@NotNull TextAttributesKey name, @NotNull TextAttributes attr) {
434     if (myVersion >= 141 || myParentScheme == null) return;
435
436     Couple<Color> m = DEFAULT_STRIPE_COLORS.get(name.getExternalName());
437     if (m != null && Comparing.equal(m.first, attr.getErrorStripeColor())) {
438       attr.setErrorStripeColor(m.second);
439     }
440   }
441
442   @SuppressWarnings("UseJBColor")
443   private static final Map<String, Couple<Color>> DEFAULT_STRIPE_COLORS = new THashMap<String, Couple<Color>>() {
444     {
445       put(ERRORS_ATTRIBUTES.getExternalName(),                        of(Color.red,          fromHex("CF5B56")));
446       put(WARNINGS_ATTRIBUTES.getExternalName(),                      of(Color.yellow,       fromHex("EBC700")));
447       put("EXECUTIONPOINT_ATTRIBUTES",                                of(Color.blue,         fromHex("3763b0")));
448       put(IDENTIFIER_UNDER_CARET_ATTRIBUTES.getExternalName(),        of(fromHex("CCCFFF"),  fromHex("BAA8FF")));
449       put(WRITE_IDENTIFIER_UNDER_CARET_ATTRIBUTES.getExternalName(),  of(fromHex("FFCCE5"),  fromHex("F0ADF0")));
450       put(TEXT_SEARCH_RESULT_ATTRIBUTES.getExternalName(),            of(fromHex("586E75"),  fromHex("71B362")));
451       put(TODO_DEFAULT_ATTRIBUTES.getExternalName(),                  of(fromHex("268BD2"),  fromHex("54AAE3")));
452     }
453   };
454
455   private void readColors(Element childNode) {
456     for (Element colorElement : childNode.getChildren(OPTION_ELEMENT)) {
457       Color valueColor = myValueReader.read(Color.class, colorElement);
458       final String colorName = colorElement.getAttributeValue(NAME_ATTR);
459       if (BACKGROUND_COLOR_NAME.equals(colorName)) {
460         // This setting has been deprecated to usages of HighlighterColors.TEXT attributes.
461         myDeprecatedBackgroundColor = valueColor;
462       }
463
464       ColorKey name = ColorKey.find(colorName);
465       myColorsMap.put(name, valueColor);
466     }
467   }
468
469   private void readSettings(@NotNull Element childNode, boolean isDefault, @NotNull Ref<Float> fontScale) {
470     switch (childNode.getAttributeValue(NAME_ATTR)) {
471       case FONT_SCALE: {
472         fontScale.set(myValueReader.read(Float.class, childNode));
473         break;
474       }
475       case LINE_SPACING: {
476         Float value = myValueReader.read(Float.class, childNode);
477         if (value != null) setLineSpacing(value);
478         break;
479       }
480       case EDITOR_FONT_SIZE: {
481         int value = readFontSize(childNode, isDefault, fontScale.get());
482         if (value > 0) setEditorFontSize(value);
483         break;
484       }
485       case EDITOR_FONT_NAME: {
486         String value = myValueReader.read(String.class, childNode);
487         if (value != null) setEditorFontName(value);
488         break;
489       }
490       case CONSOLE_LINE_SPACING: {
491         Float value = myValueReader.read(Float.class, childNode);
492         if (value != null) setConsoleLineSpacing(value);
493         break;
494       }
495       case CONSOLE_FONT_SIZE: {
496         int value = readFontSize(childNode, isDefault, fontScale.get());
497         if (value > 0) setConsoleFontSize(value);
498         break;
499       }
500       case CONSOLE_FONT_NAME: {
501         String value = myValueReader.read(String.class, childNode);
502         if (value != null) setConsoleFontName(value);
503         break;
504       }
505       case EDITOR_QUICK_JAVADOC_FONT_SIZE: {
506         FontSize value = myValueReader.read(FontSize.class, childNode);
507         if (value != null) myQuickDocFontSize = value;
508         break;
509       }
510       case EDITOR_LIGATURES: {
511         Boolean value = myValueReader.read(Boolean.class, childNode);
512         if (value != null) ensureEditableFontPreferences().setUseLigatures(value);
513         break;
514       }
515       case CONSOLE_LIGATURES: {
516         Boolean value = myValueReader.read(Boolean.class, childNode);
517         if (value != null) {
518           ensureEditableConsoleFontPreferences().setUseLigatures(value);
519         }
520         break;
521       }
522     }
523   }
524
525   private int readFontSize(Element element, boolean isDefault, Float fontScale) {
526     if (isDefault) {
527       return UISettings.getDefFontSize();
528     }
529     Integer intSize = myValueReader.read(Integer.class, element);
530     if (intSize == null) {
531       return -1;
532     }
533     return UISettings.restoreFontSize(intSize, fontScale);
534   }
535
536   private void readFontSettings(@NotNull ModifiableFontPreferences preferences,
537                                 @NotNull Element element,
538                                 boolean isDefaultScheme,
539                                 @Nullable Float fontScale,
540                                 boolean clearFonts) {
541     if (clearFonts) preferences.clearFonts();
542     List children = element.getChildren(OPTION_ELEMENT);
543     String fontFamily = null;
544     int size = -1;
545     for (Object child : children) {
546       Element e = (Element)child;
547       if (EDITOR_FONT_NAME.equals(e.getAttributeValue(NAME_ATTR))) {
548         fontFamily = myValueReader.read(String.class, e);
549       }
550       else if (EDITOR_FONT_SIZE.equals(e.getAttributeValue(NAME_ATTR))) {
551         size = readFontSize(e, isDefaultScheme, fontScale);
552       }
553     }
554     if (fontFamily != null && size > 1) {
555       preferences.register(fontFamily, size);
556     }
557     else if (fontFamily != null) {
558       preferences.addFontFamily(fontFamily);
559     }
560   }
561
562   public void writeExternal(Element parentNode) {
563     parentNode.setAttribute(NAME_ATTR, getName());
564     parentNode.setAttribute(VERSION_ATTR, Integer.toString(myVersion));
565
566     /*
567      * FONT_SCALE is used to correctly identify the font size in both the JRE-managed HiDPI mode and
568      * the IDE-managed HiDPI mode: {@link UIUtil#isJreHiDPIEnabled()}. Also, it helps to distinguish
569      * the "hidpi-aware" scheme version from the previous one. Namely, the absence of the FONT_SCALE
570      * attribute in the scheme indicates the previous "hidpi-unaware" scheme and the restored font size
571      * is reset to default. It's assumed this (transition case) happens only once, after which the IDE
572      * will be able to restore the font size according to its scale and the IDE HiDPI mode. The default
573      * FONT_SCALE value should also be written by that reason.
574      */
575     if (!(myFontPreferences instanceof DelegatingFontPreferences) || !(myConsoleFontPreferences instanceof DelegatingFontPreferences)) {
576       JdomKt.addOptionTag(parentNode, FONT_SCALE, String.valueOf(UISettings.getDefFontScale())); // must precede font options
577     }
578
579     if (myParentScheme != null && myParentScheme != EmptyColorScheme.INSTANCE) {
580       parentNode.setAttribute(PARENT_SCHEME_ATTR, myParentScheme.getName());
581     }
582     
583     if (!myMetaInfo.isEmpty()) {
584       parentNode.addContent(metaInfoToElement());
585     }
586
587     if (getLineSpacing() != FontPreferences.DEFAULT_LINE_SPACING) {
588       JdomKt.addOptionTag(parentNode, LINE_SPACING, String.valueOf(getLineSpacing()));
589     }
590
591     // IJ has used a 'single customizable font' mode for ages. That's why we want to support that format now, when it's possible
592     // to specify fonts sequence (see getFontPreferences()), there are big chances that many clients still will use a single font.
593     // That's why we want to use old format when zero or one font is selected and 'extended' format otherwise.
594     boolean useOldFontFormat = myFontPreferences.getEffectiveFontFamilies().size() <= 1;
595     if (!(myFontPreferences instanceof DelegatingFontPreferences)) {
596       if (useOldFontFormat) {
597         JdomKt.addOptionTag(parentNode, EDITOR_FONT_SIZE, String.valueOf(getEditorFontSize()));
598         JdomKt.addOptionTag(parentNode, EDITOR_FONT_NAME, myFontPreferences.getFontFamily());
599       }
600       else {
601         writeFontPreferences(EDITOR_FONT, parentNode, myFontPreferences);
602       }
603       writeLigaturesPreferences(parentNode, myFontPreferences, EDITOR_LIGATURES);
604     }
605     
606     if (!(myConsoleFontPreferences instanceof DelegatingFontPreferences)) {
607       if (myConsoleFontPreferences.getEffectiveFontFamilies().size() <= 1) {
608         JdomKt.addOptionTag(parentNode, CONSOLE_FONT_NAME, getConsoleFontName());
609
610         if (getConsoleFontSize() != getEditorFontSize()) {
611           JdomKt.addOptionTag(parentNode, CONSOLE_FONT_SIZE, Integer.toString(getConsoleFontSize()));
612         }
613       }
614       else {
615         writeFontPreferences(CONSOLE_FONT, parentNode, myConsoleFontPreferences);
616       }
617       writeLigaturesPreferences(parentNode, myConsoleFontPreferences, CONSOLE_LIGATURES);
618       if (getConsoleLineSpacing() != FontPreferences.DEFAULT_LINE_SPACING) {
619         JdomKt.addOptionTag(parentNode, CONSOLE_LINE_SPACING, Float.toString(getConsoleLineSpacing()));
620       }
621     }
622
623     if (DEFAULT_FONT_SIZE != getQuickDocFontSize()) {
624       JdomKt.addOptionTag(parentNode, EDITOR_QUICK_JAVADOC_FONT_SIZE, getQuickDocFontSize().toString());
625     }
626
627     Element colorElements = new Element(COLORS_ELEMENT);
628     Element attrElements = new Element(ATTRIBUTES_ELEMENT);
629
630     writeColors(colorElements);
631     writeAttributes(attrElements);
632
633     if (!colorElements.getChildren().isEmpty()) {
634       parentNode.addContent(colorElements);
635     }
636     if (!attrElements.getChildren().isEmpty()) {
637       parentNode.addContent(attrElements);
638     }
639     
640     myIsSaveNeeded = false;
641   }
642
643   private static void writeLigaturesPreferences(Element parentNode, FontPreferences preferences, String optionName) {
644     if (preferences.useLigatures()) {
645       JdomKt.addOptionTag(parentNode, optionName, String.valueOf(true));
646     }
647   }
648
649   private static void writeFontPreferences(@NotNull String key, @NotNull Element parent, @NotNull FontPreferences preferences) {
650     for (String fontFamily : preferences.getRealFontFamilies()) {
651       Element element = new Element(key);
652       JdomKt.addOptionTag(element, EDITOR_FONT_NAME, fontFamily);
653       JdomKt.addOptionTag(element, EDITOR_FONT_SIZE, String.valueOf(preferences.getSize(fontFamily)));
654       parent.addContent(element);
655     }
656   }
657
658   private boolean isParentOverwritingInheritance(@NotNull TextAttributesKey key) {
659     TextAttributes parentAttributes =
660       myParentScheme instanceof AbstractColorsScheme ? ((AbstractColorsScheme)myParentScheme).getDirectlyDefinedAttributes(key) : null;
661     return parentAttributes != null && parentAttributes != USE_INHERITED_MARKER;
662   }
663
664   private void writeAttributes(@NotNull Element attrElements) throws WriteExternalException {
665     List<TextAttributesKey> list = new ArrayList<>(myAttributesMap.keySet());
666     list.sort(null);
667     for (TextAttributesKey key : list) {
668       TextAttributes attributes = myAttributesMap.get(key);
669       TextAttributesKey baseKey = key.getFallbackAttributeKey();
670       if (attributes == USE_INHERITED_MARKER) {
671         // do not store if  inheritance = on in the parent scheme (https://youtrack.jetbrains.com/issue/IDEA-162774)
672         if (baseKey != null && isParentOverwritingInheritance(key)) {
673           attrElements.addContent(new Element(OPTION_ELEMENT)
674                                     .setAttribute(NAME_ATTR, key.getExternalName())
675                                     .setAttribute(BASE_ATTRIBUTES_ATTR, baseKey.getExternalName()));
676         }
677         continue;
678       }
679
680       if (myParentScheme != null) {
681         // fallback attributes must be not used, otherwise derived scheme as copy will not have such key
682         TextAttributes parentAttributes = myParentScheme instanceof AbstractColorsScheme
683                                           ? ((AbstractColorsScheme)myParentScheme).getDirectlyDefinedAttributes(key)
684                                           : myParentScheme.getAttributes(key);
685         if (parentAttributes != null && attributes.equals(parentAttributes)) {
686           continue;
687         }
688       }
689
690       Element valueElement = new Element(VALUE_ELEMENT);
691       attributes.writeExternal(valueElement);
692       attrElements.addContent(new Element(OPTION_ELEMENT).setAttribute(NAME_ATTR, key.getExternalName()).addContent(valueElement));
693     }
694   }
695
696   public void optimizeAttributeMap() {
697     EditorColorsScheme parentScheme = myParentScheme;
698     if (parentScheme == null) {
699       return;
700     }
701
702     for (TextAttributesKey key : new ArrayList<>(myAttributesMap.keySet())) {
703       TextAttributes attributes = myAttributesMap.get(key);
704       if (attributes == USE_INHERITED_MARKER) {
705         if (key.getFallbackAttributeKey() == null) {
706           myAttributesMap.remove(key);
707         }
708         continue;
709       }
710
711       TextAttributes parentAttributes = parentScheme instanceof DefaultColorsScheme
712                                         ? ((DefaultColorsScheme)parentScheme).getAttributes(key, false)
713                                         : parentScheme.getAttributes(key);
714       if (Comparing.equal(parentAttributes, attributes)) {
715         myAttributesMap.remove(key);
716       }
717     }
718   }
719   
720   @NotNull
721   private Element metaInfoToElement() {
722     Element metaInfoElement = new Element(META_INFO_ELEMENT);
723     myMetaInfo.setProperty(META_INFO_MODIFIED_TIME, META_INFO_DATE_FORMAT.format(new Date()));
724     List<String> sortedPropertyNames = new ArrayList<>(myMetaInfo.stringPropertyNames());
725     sortedPropertyNames.sort(null);
726     for (String propertyName : sortedPropertyNames) {
727       String value = myMetaInfo.getProperty(propertyName);
728       Element propertyInfo = new Element(PROPERTY_ELEMENT);
729       propertyInfo.setAttribute(PROPERTY_NAME_ATTR, propertyName);
730       propertyInfo.setText(value);
731       metaInfoElement.addContent(propertyInfo);
732     }
733     return metaInfoElement;
734   }
735
736   protected Color getOwnColor(ColorKey key) {
737     return myColorsMap.get(key);
738   }
739
740   private void writeColors(Element colorElements) {
741     List<ColorKey> list = new ArrayList<>(myColorsMap.keySet());
742     list.sort(null);
743     for (ColorKey key : list) {
744       if (haveToWrite(key)) {
745         Color value = myColorsMap.get(key);
746         String value1 = value == null ? "" : Integer.toString(value.getRGB() & 0xFFFFFF, 16);
747         JdomKt.addOptionTag(colorElements, key.getExternalName(), value1);
748       }
749     }
750   }
751
752   private boolean haveToWrite(@NotNull ColorKey key) {
753     Color value = myColorsMap.get(key);
754     if (myParentScheme != null) {
755       if (myParentScheme instanceof AbstractColorsScheme) {
756         if (Comparing.equal(((AbstractColorsScheme)myParentScheme).getOwnColor(key), value) && ((AbstractColorsScheme)myParentScheme).myColorsMap.containsKey(key)) {
757           return false;
758         }
759       }
760       else if (Comparing.equal((myParentScheme).getColor(key), value)) {
761         return false;
762       }
763     }
764     return true;
765   }
766
767   private ModifiableFontPreferences ensureEditableFontPreferences() {
768     if (!(myFontPreferences instanceof ModifiableFontPreferences)) {
769       ModifiableFontPreferences editablePrefs = new FontPreferencesImpl();
770       myFontPreferences.copyTo(editablePrefs);
771       myFontPreferences = editablePrefs;
772       ((FontPreferencesImpl)myFontPreferences).setChangeListener(() -> initFonts());
773     }
774     return (ModifiableFontPreferences)myFontPreferences;
775   }
776
777   @NotNull
778   @Override
779   public FontPreferences getConsoleFontPreferences() {
780     return myConsoleFontPreferences;
781   }
782   
783   @Override
784   public void setUseEditorFontPreferencesInConsole() {
785     myConsoleFontPreferences = new DelegatingFontPreferences(() -> myFontPreferences);
786     initFonts();
787   }
788
789   @Override
790   public boolean isUseEditorFontPreferencesInConsole() {
791     return myConsoleFontPreferences instanceof DelegatingFontPreferences;
792   }
793
794   @Override
795   public void setConsoleFontPreferences(@NotNull FontPreferences preferences) {
796     preferences.copyTo(ensureEditableConsoleFontPreferences());
797     initFonts();
798   }
799
800   @Override
801   public String getConsoleFontName() {
802     return myConsoleFontPreferences.getFontFamily();
803   }
804
805   private ModifiableFontPreferences ensureEditableConsoleFontPreferences() {
806     if (!(myConsoleFontPreferences instanceof ModifiableFontPreferences)) {
807       ModifiableFontPreferences editablePrefs = new FontPreferencesImpl();
808       myConsoleFontPreferences.copyTo(editablePrefs);
809       myConsoleFontPreferences = editablePrefs;
810     }
811     return (ModifiableFontPreferences)myConsoleFontPreferences;
812   }
813
814   @Override
815   public void setConsoleFontName(String fontName) {
816     ModifiableFontPreferences consolePreferences = ensureEditableConsoleFontPreferences();
817     int consoleFontSize = getConsoleFontSize();
818     consolePreferences.clear();
819     consolePreferences.register(fontName, consoleFontSize);
820   }
821
822   @Override
823   public int getConsoleFontSize() {
824     String font = getConsoleFontName();
825     UISettings uiSettings = UISettings.getInstanceOrNull();
826     if ((uiSettings == null || !uiSettings.getPresentationMode()) && myConsoleFontPreferences.hasSize(font)) {
827       return myConsoleFontPreferences.getSize(font);
828     }
829     return getEditorFontSize();
830   }
831
832   @Override
833   public void setConsoleFontSize(int fontSize) {
834     ModifiableFontPreferences consoleFontPreferences = ensureEditableConsoleFontPreferences();
835     fontSize = EditorFontsConstants.checkAndFixEditorFontSize(fontSize);
836     consoleFontPreferences.register(getConsoleFontName(), fontSize);
837     initFonts();
838   }
839
840   @Override
841   public float getConsoleLineSpacing() {
842     return myConsoleFontPreferences.getLineSpacing();
843   }
844
845   @Override
846   public void setConsoleLineSpacing(float lineSpacing) {
847     ensureEditableConsoleFontPreferences().setLineSpacing(lineSpacing);
848   }
849
850   protected TextAttributes getFallbackAttributes(@NotNull TextAttributesKey fallbackKey) {
851     TextAttributes fallbackAttributes = getDirectlyDefinedAttributes(fallbackKey);
852     TextAttributesKey fallbackKeyFallbackKey = fallbackKey.getFallbackAttributeKey();
853     if (fallbackAttributes != null && (fallbackAttributes != USE_INHERITED_MARKER || fallbackKeyFallbackKey == null)) {
854       return fallbackAttributes;
855     }
856     return fallbackKeyFallbackKey == null ? null : getFallbackAttributes(fallbackKeyFallbackKey);
857   }
858
859   /**
860    * Looks for explicitly specified attributes either in the scheme or its parent scheme. No fallback keys are used.
861    *
862    * @param key The key to use for search.
863    * @return Explicitly defined attribute or <code>null</code> if not found.
864    */
865   @Nullable
866   public TextAttributes getDirectlyDefinedAttributes(@NotNull TextAttributesKey key) {
867     TextAttributes attributes = myAttributesMap.get(key);
868     if (attributes != null) {
869       return attributes;
870     }
871     return myParentScheme instanceof AbstractColorsScheme ? ((AbstractColorsScheme)myParentScheme).getDirectlyDefinedAttributes(key) : null;
872   }
873
874   @NotNull
875   @Override
876   public SchemeState getSchemeState() {
877     return myIsSaveNeeded ? SchemeState.POSSIBLY_CHANGED : SchemeState.UNCHANGED;
878   }
879
880   public void setSaveNeeded(boolean value) {
881     myIsSaveNeeded = value;
882   }
883   
884   public boolean isReadOnly() { return  false; }
885
886   @NotNull
887   @Override
888   public Properties getMetaProperties() {
889     return myMetaInfo;
890   }
891   
892   public boolean canBeDeleted() {
893     return myCanBeDeleted;
894   }
895   
896   public void setCanBeDeleted(boolean canBeDeleted) {
897     myCanBeDeleted = canBeDeleted;
898   }
899   
900   public boolean isVisible() {
901     return true;
902   }
903
904   public static boolean isVisible(@NotNull EditorColorsScheme scheme) {
905     return !(scheme instanceof AbstractColorsScheme) || ((AbstractColorsScheme)scheme).isVisible();
906   }
907
908   @Nullable
909   public AbstractColorsScheme getOriginal() {
910     String originalSchemeName = getMetaProperties().getProperty(META_INFO_ORIGINAL);
911     if (originalSchemeName != null) {
912       EditorColorsScheme originalScheme = EditorColorsManager.getInstance().getScheme(originalSchemeName);
913       if (originalScheme instanceof AbstractColorsScheme) return (AbstractColorsScheme)originalScheme;
914     }
915     return null;
916   }
917
918   public EditorColorsScheme getParentScheme() {
919     return myParentScheme;
920   }
921
922   @NotNull
923   @Override
924   public Element writeScheme() {
925     Element root = new Element("scheme");
926     writeExternal(root);
927     return root;
928   }
929
930   public boolean settingsEqual(Object other) {
931     return settingsEqual(other, null);
932   }
933
934   public boolean settingsEqual(Object other, @Nullable Predicate<ColorKey> colorKeyFilter) {
935     if (!(other instanceof AbstractColorsScheme)) return false;
936     AbstractColorsScheme otherScheme = (AbstractColorsScheme)other;
937     
938     // parent is used only for default schemes (e.g. Darcula bundled in all ide (opposite to IDE-specific, like Cobalt))
939     if (getBaseDefaultScheme(this) != getBaseDefaultScheme(otherScheme)) {
940       return false;
941     }
942
943     for (String propertyName : myMetaInfo.stringPropertyNames()) {
944       if (propertyName.equals(META_INFO_CREATION_TIME) ||
945           propertyName.equals(META_INFO_MODIFIED_TIME) ||
946           propertyName.equals(META_INFO_IDE) ||
947           propertyName.equals(META_INFO_IDE_VERSION) ||
948           propertyName.equals(META_INFO_ORIGINAL)
949         ) {
950         continue;
951       }                                                                                                               
952
953       if (!Comparing.equal(myMetaInfo.getProperty(propertyName), otherScheme.myMetaInfo.getProperty(propertyName))) {
954         return false;
955       }
956     }
957
958     return areDelegatingOrEqual(myFontPreferences, otherScheme.getFontPreferences()) &&
959            areDelegatingOrEqual(myConsoleFontPreferences, otherScheme.getConsoleFontPreferences()) &&
960            attributesEqual(otherScheme) &&
961            colorsEqual(otherScheme, colorKeyFilter);
962   }
963
964   protected static boolean areDelegatingOrEqual(@NotNull FontPreferences preferences1, @NotNull FontPreferences preferences2) {
965       boolean isDelegating1 = preferences1 instanceof DelegatingFontPreferences;
966       boolean isDelegating2 = preferences2 instanceof DelegatingFontPreferences;
967       return isDelegating1 || isDelegating2 ? isDelegating1 && isDelegating2 : preferences1.equals(preferences2);
968     }
969
970   protected boolean attributesEqual(AbstractColorsScheme otherScheme) {
971     return myAttributesMap.equals(otherScheme.myAttributesMap);
972   }
973
974   protected boolean colorsEqual(AbstractColorsScheme otherScheme, @Nullable Predicate<ColorKey> colorKeyFilter) {
975     return myColorsMap.equals(otherScheme.myColorsMap);
976   }
977
978   @Nullable
979   private static EditorColorsScheme getBaseDefaultScheme(@NotNull EditorColorsScheme scheme) {
980     if (!(scheme instanceof AbstractColorsScheme)) {
981       return null;
982     }
983     if (scheme instanceof DefaultColorsScheme) {
984       return scheme;
985     }
986     EditorColorsScheme parent = ((AbstractColorsScheme)scheme).myParentScheme;
987     return parent != null ? getBaseDefaultScheme(parent) : null;
988   }
989   
990   private static class TemporaryParent extends EditorColorsSchemeImpl {
991
992     private static Logger LOG = Logger.getInstance(TemporaryParent.class);
993     
994     private String myParentName;
995     private boolean isErrorReported;
996
997     public TemporaryParent(@NotNull String parentName) {
998       super(EmptyColorScheme.INSTANCE);
999       myParentName = parentName;
1000     }
1001
1002     public String getParentName() {
1003       return myParentName;
1004     }
1005
1006     @Override
1007     public TextAttributes getAttributes(@Nullable TextAttributesKey key) {
1008       reportError();
1009       return super.getAttributes(key);
1010     }
1011
1012     @Nullable
1013     @Override
1014     public Color getColor(ColorKey key) {
1015       reportError();
1016       return super.getColor(key);
1017     }
1018
1019     private void reportError() {
1020       if (!isErrorReported) {
1021         LOG.error("Unresolved link to " + myParentName);
1022         isErrorReported = true;
1023       }
1024     }
1025   }
1026
1027   public void setParent(@NotNull EditorColorsScheme newParent) {
1028     assert newParent instanceof ReadOnlyColorsScheme : "New parent scheme must be read-only";
1029     myParentScheme = newParent;
1030   }
1031
1032   void resolveParent(@NotNull Function<String,EditorColorsScheme> nameResolver) {
1033     if (myParentScheme instanceof TemporaryParent) {
1034       String parentName = ((TemporaryParent)myParentScheme).getParentName();
1035       EditorColorsScheme newParent = nameResolver.apply(parentName);
1036       if (newParent == null || !(newParent instanceof ReadOnlyColorsScheme)) {
1037         throw new InvalidDataException(parentName);
1038       }
1039       myParentScheme = newParent;
1040     }
1041   }
1042 }