ide/settings: Fixed color scheme issues
[sdk] / ide / src / IDESettings.ec
index 2bbc100..b0feab9 100644 (file)
 #ifdef ECERE_STATIC
-import static "ecere"
+public import static "ecere"
 #else
-import "ecere"
+public import "ecere"
 #endif
 
+// *** Color Schemes ***
+class IDEColorScheme
+{
+   String name;
+   SyntaxColorScheme syntaxColors;
+public:
+
+   property const String name
+   {
+      set { delete name; name = CopyString(value); }
+      get { return name; }
+   }
+
+   Color selectionColor;
+   Color selectionText;
+   Color viewsBackground;
+   Color viewsText;
+   Color outputBackground;
+   Color outputText;
+   Color projectViewBackground;
+   Color projectViewText;
+   Color codeEditorBG;
+   Color codeEditorFG;
+   Color marginColor;
+   Color selectedMarginColor;
+   Color lineNumbersColor;
+   Color sheetSelectionColor;
+   Color sheetSelectionText;
+
+   property SyntaxColorScheme syntaxColors
+   {
+      set { delete syntaxColors; syntaxColors = value; if(value) incref value; }
+      get { return syntaxColors; }
+   }
+
+   ~IDEColorScheme()
+   {
+      delete syntaxColors;
+      delete name;
+   }
+}
+
+IDEColorScheme colorScheme;
+
+// Default Color Schemes
+IDEColorScheme darkColorScheme
+{
+   name = "Classic Dark";
+   selectionColor = lightYellow;
+   selectionText = Color { 30, 40, 50 };
+   viewsBackground = Color { 30, 40, 50 };
+   viewsText = lightGray;
+   outputBackground = black;
+   outputText = lime;
+   sheetSelectionColor = Color { 170, 220, 255 };
+   sheetSelectionText = black;
+   projectViewBackground = Color { 30, 40, 50 };
+   projectViewText = lightGray;
+   codeEditorBG = black;
+   codeEditorFG = ivory;
+   marginColor = Color {24, 24, 24};
+   selectedMarginColor = Color {64, 64, 64};
+   lineNumbersColor = Color {160, 160, 160};
+   syntaxColors =
+   {
+      keywordColors = [ skyBlue, skyBlue ];
+      commentColor = Color { 125, 125, 125 };
+      charLiteralColor = Color { 245, 50, 245 };
+      stringLiteralColor = Color { 245, 50, 245 };
+      preprocessorColor = { 120, 220, 140 };
+      numberColor = Color {   0, 192, 192 };
+   };
+};
+
+IDEColorScheme lightColorScheme
+{
+   name = "Classic Light";
+   selectionColor = Color { 10, 36, 106 };
+   selectionText = white;
+   viewsBackground = white;
+   viewsText = black;
+   outputBackground = white;
+   outputText = black;
+   sheetSelectionColor = Color { 170, 220, 255 };
+   sheetSelectionText = black;
+   projectViewBackground = white;
+   projectViewText = black;
+   codeEditorBG = white;
+   codeEditorFG = black;
+   marginColor = Color {230, 230, 230};
+   selectedMarginColor = Color {200, 200, 200};
+   lineNumbersColor = Color {60, 60, 60};
+   syntaxColors =
+   {
+      keywordColors = [ blue, blue ];
+      commentColor = dimGray;
+      charLiteralColor = crimson;
+      stringLiteralColor = crimson;
+      preprocessorColor = green;
+      numberColor = teal;
+   };
+};
+
+IDEColorScheme greenColorScheme
+{
+   name = "Green",
+   selectionColor = 0x00FFFFE0,
+   selectionText = 0x001E2832,
+   viewsBackground = 0x001E2832,
+   viewsText = 0x00D3D3D3,
+   outputBackground = 0x00000000,
+   outputText = 0x0000FF00,
+   projectViewBackground = 0x001E2832,
+   projectViewText = 0x00D3D3D3,
+   codeEditorBG = 0x00000000,
+   codeEditorFG = 0x00FFFFF0,
+   marginColor = 0x001100,
+   selectedMarginColor = 0x002200,
+   lineNumbersColor = 0x00FF00,
+   sheetSelectionColor = 0x00AADCFF,
+   sheetSelectionText = 0x00000000,
+   syntaxColors = {
+      commentColor = 0x008c00,
+      charLiteralColor = 0x89de00,
+      stringLiteralColor = 0x89de00,
+      preprocessorColor = 0x0078DC8C,
+      numberColor = { 8, 237, 141 },
+      keywordColors = [
+         0x00e09d,
+         0x00e09d
+      ]
+   }
+};
+
+IDEColorScheme grayColorScheme
+{
+   name = "Gray & Orange",
+   selectionColor = 0x00FFFFE0,
+   selectionText = 0x001E2832,
+   viewsBackground = 0x001E2832,
+   viewsText = 0x00D3D3D3,
+   outputBackground = 0x00000000,
+   outputText = 0x0000FF00,
+   projectViewBackground = 0x001E2832,
+   projectViewText = 0x00D3D3D3,
+   codeEditorBG = 0x00202020,
+   codeEditorFG = 0x00E2E2E5,
+   marginColor = 0x00303030,
+   selectedMarginColor = 0x00404040,
+   lineNumbersColor = 0x00939395,
+   sheetSelectionColor = 0x00AADCFF,
+   sheetSelectionText = 0x00000000,
+   syntaxColors = {
+      commentColor = 0x005F5F5F,
+      charLiteralColor = 0x0089DE00,
+      stringLiteralColor = 0x00707070,
+      preprocessorColor = 0x00FAF4C6,
+      numberColor = 0x00FF9800,
+      keywordColors = [
+         0x00FF9800,
+         0x00FF9800
+      ]
+   }
+};
+
+define ecpDefaultCommand = "ecp";
+define eccDefaultCommand = "ecc";
+define ecsDefaultCommand = "ecs";
+define earDefaultCommand = "ear";
+define cppDefaultCommand = "gcc"; // As per #624 we decided to default to "gcc"...
+define ccDefaultCommand = "gcc";
+define cxxDefaultCommand = "g++";
+//define ldDefaultCommand = "gcc";
+define arDefaultCommand = "ar";
+define objectDefaultFileExt = "o";
+define outputDefaultFileExt = "";
+
 import "StringsBox"
 
 import "OldIDESettings"
 
+#ifdef __WIN32__
+#define WIN32_LEAN_AND_MEAN
+#define Sleep _Sleep
+#include <windows.h>
+#undef MoveFileEx
+#undef Sleep
+#undef MessageBox
+#undef GetObject
+#endif
+
 define MaxRecent = 9;
 
 enum DirTypes { includes, libraries, executables };
@@ -16,17 +203,42 @@ define defaultCompilerName = "Default";
 
 define defaultObjDirExpression = "obj/$(CONFIG).$(PLATFORM)$(COMPILER_SUFFIX)$(DEBUG_SUFFIX)";
 
-char * settingsDirectoryNames[DirTypes] = 
+const char * settingsDirectoryNames[DirTypes] =
 {
    "Include Files",
    "Library Files",
    "Executable Files"
 };
 
+// This function cannot accept same pointer for source and output
+// todo: rename ReplaceSpaces to EscapeSpaceAndSpecialChars or something
+void ReplaceSpaces(char * output, const char * source)
+{
+   int c, dc;
+   char ch, pch = 0;
+
+   for(c = 0, dc = 0; (ch = source[c]); c++, dc++)
+   {
+      if(ch == ' ') output[dc++] = '\\';
+      if(ch == '\"') output[dc++] = '\\';
+      if(ch == '&') output[dc++] = '\\';
+      if(pch != '$')
+      {
+         if(ch == '(' || ch == ')') output[dc++] = '\\';
+         pch = ch;
+      }
+      else if(ch == ')')
+         pch = 0;
+      output[dc] = ch;
+   }
+   output[dc] = '\0';
+}
+
+
 enum GlobalSettingsChange { none, editorSettings, projectOptions, compilerSettings };
 
 enum PathRelationship { unrelated, identical, siblings, subPath, parentPath, insuficientInput, pathEmpty, toEmpty, pathNull, toNull, bothEmpty, bothNull };
-PathRelationship eString_PathRelated(char * path, char * to, char * pathDiff)
+PathRelationship eString_PathRelated(const char * path, const char * to, char * pathDiff)
 {
    PathRelationship result;
    if(pathDiff) *pathDiff = '\0';
@@ -67,24 +279,24 @@ PathRelationship eString_PathRelated(char * path, char * to, char * pathDiff)
    return result;
 }
 
-char * CopyValidateMakefilePath(char * path)
+char * CopyValidateMakefilePath(const char * path)
 {
-   const int map[]  =    { 0,           1,           2,             3,             4,           5,                    6,                 0,             1,                 7         };
-   const char * vars[] = { "$(MODULE)", "$(CONFIG)", "$(PLATFORM)", "$(COMPILER)", "$(TARGET)", "$(COMPILER_SUFFIX)", "$(DEBUG_SUFFIX)", "$(PROJECT)",  "$(CONFIGURATION)",(char *)0 };
+   const int map[]  =    {           0,           1,             2,             3,           4,                    5,                 6,            0,                   1,                    2,        7 };
+   const char * vars[] = { "$(MODULE)", "$(CONFIG)", "$(PLATFORM)", "$(COMPILER)", "$(TARGET)", "$(COMPILER_SUFFIX)", "$(DEBUG_SUFFIX)", "$(PROJECT)",  "$(CONFIGURATION)", "$(TARGET_PLATFORM)",(char *)0 };
 
    char * copy = null;
    if(path)
    {
       int len;
-      len = strlen(path);
+      len = (int)strlen(path);
       copy = CopyString(path);
       if(len)
       {
          int c;
          char * tmp = copy;
          char * start = tmp;
-         Array<char *> parts { };
-         
+         Array<const char *> parts { };
+
          for(c=0; c<len; c++)
          {
             if(tmp[c] == '$')
@@ -125,14 +337,58 @@ char * CopyValidateMakefilePath(char * path)
    return copy;
 }
 
-CompilerConfig MakeDefaultCompiler(char * name, bool readOnly)
+void ValidPathBufCopy(char *output, const char *input)
+{
+#ifdef __WIN32__
+   bool volumePath = false;
+#endif
+   strcpy(output, input);
+   TrimLSpaces(output, output);
+   TrimRSpaces(output, output);
+   MakeSystemPath(output);
+#ifdef __WIN32__
+   if(output[0] && output[1] == ':')
+   {
+      output[1] = '_';
+      volumePath = true;
+   }
+#endif
+   {
+      const char * chars = "*|:\",<>?";
+      char ch, * s = output, * o = output;
+      while((ch = *s++)) { if(!strchr(chars, ch)) *o++ = ch; }
+      *o = '\0';
+   }
+#ifdef __WIN32__
+   if(volumePath && output[0])
+      output[1] = ':';
+#endif
+}
+
+void RemoveTrailingPathSeparator(char *path)
+{
+   int len;
+   len = (int)strlen(path);
+   if(len>1 && path[len-1] == DIR_SEP)
+      path[--len] = '\0';
+}
+
+void BasicValidatePathBoxPath(PathBox pathBox)
+{
+   char path[MAX_LOCATION];
+   ValidPathBufCopy(path, pathBox.path);
+   RemoveTrailingPathSeparator(path);
+   pathBox.path = path;
+}
+
+CompilerConfig MakeDefaultCompiler(const char * name, bool readOnly)
 {
    CompilerConfig defaultCompiler
    {
       name,
       readOnly,
       gcc,
-      GetRuntimePlatform(),
+      __runtimePlatform,
       1,
       makeDefaultCommand,
       ecpDefaultCommand,
@@ -141,25 +397,170 @@ CompilerConfig MakeDefaultCompiler(char * name, bool readOnly)
       earDefaultCommand,
       cppDefaultCommand,
       ccDefaultCommand,
-      cxxDefaultCommand
+      cxxDefaultCommand,
+      arDefaultCommand
+      //ldDefaultCommand
    };
    incref defaultCompiler;
    return defaultCompiler;
 }
 
-class IDESettingsContainer : GlobalSettings
-{
+//#define SETTINGS_TEST
 #ifdef SETTINGS_TEST
-   settingsName = "ecereIDESettingsTest";
+define settingsDir = ".ecereIDE-SettingsTest";
+define ideSettingsName = "ecereIDE-SettingsTest";
 #else
-   settingsName = "ecereIDE";
+define settingsDir = ".ecereIDE";
+define ideSettingsName = "ecereIDE";
 #endif
 
-   virtual void OnLoad(GlobalSettingsData data);
+class IDEConfigHolder
+{
+   CompilerConfigs compilers { };
+   RecentFiles recentFiles { };
+   RecentWorkspaces recentWorkspaces { };
+
+   property CompilerConfigs compilers
+   {
+      set { compilers.Free(); delete compilers; compilers = value; }
+      get { return compilers; }
+   }
+   property RecentFiles recentFiles
+   {
+      set { recentFiles.Free(); delete recentFiles; recentFiles = value; }
+      get { return recentFiles; }
+   }
+   property RecentWorkspaces recentProjects
+   {
+      set { recentWorkspaces.Free(); delete recentWorkspaces; recentWorkspaces = value; }
+      get { return recentWorkspaces; }
+   }
+
+   ~IDEConfigHolder()
+   {
+      compilers.Free();
+      recentFiles.Free();
+      recentWorkspaces.Free();
+   }
+
+   void forcePathSeparatorStyle(bool unixStyle)
+   {
+      char from, to;
+      if(unixStyle)
+         from = '\\', to = '/';
+      else
+         from = '/', to = '\\';
+      recentFiles.changeChar(from, to);
+      recentWorkspaces.changeChar(from, to);
+   }
+}
+
+class IDESettingsContainer : GlobalSettings
+{
+   virtual void onLoadCompilerConfigs();
+   virtual void onLoadRecentFiles();
+   virtual void onLoadRecentProjects();
+   virtual void onLoad();
+
+   CompilerConfigs compilerConfigs;
+   RecentFiles recentFiles;
+   RecentWorkspaces recentProjects;
+
+   property bool useNewConfigurationFiles
+   {
+      set
+      {
+         if(value)
+         {
+            driver = "ECON";
+            settingsName = "config";
+            settingsExtension = "econ";
+            settingsDirectory = settingsDir;
+         }
+         else
+         {
+            driver = "JSON";
+            settingsName = ideSettingsName;
+            settingsExtension = null;
+            settingsDirectory = null;
+         }
+      }
+   }
 
    char moduleLocation[MAX_LOCATION];
 
+   FileMonitor recentFilesMonitor
+   {
+      this, fileChange = { modified = true };
+
+      bool OnFileNotify(FileChange action, const char * param)
+      {
+         File f;
+         recentFilesMonitor.StopMonitoring();
+         f = FileOpen(recentFilesMonitor.fileName, read);
+         if(f)
+         {
+            int c;
+            bool locked;
+            for(c = 0; c < 10 && !(locked = f.Lock(shared, 0, 0, false)); c++) ecere::sys::Sleep(0.01);
+            recentFiles.read(this);
+            f.Unlock(0,0,true);
+            delete f;
+         }
+         return true;
+      }
+   };
+
+   FileMonitor recentProjectsMonitor
+   {
+      this, fileChange = { modified = true };
+
+      bool OnFileNotify(FileChange action, const char * param)
+      {
+         File f;
+         recentProjectsMonitor.StopMonitoring();
+         f = FileOpen(recentProjectsMonitor.fileName, read);
+         if(f)
+         {
+            int c;
+            bool locked;
+            for(c = 0; c < 10 && !(locked = f.Lock(shared, 0, 0, false)); c++) ecere::sys::Sleep(0.01);
+            recentProjects.read(this);
+            f.Unlock(0,0,true);
+            delete f;
+         }
+         return true;
+      }
+   };
+
+   static void getConfigFilePath(char * path, Class _class, char * dir, const char * configName)
+   {
+      if(dir) *dir = 0;
+      strcpy(path, settingsFilePath);
+      StripLastDirectory(path, path);
+      if(oldConfig)
+         PathCatSlash(path, settingsDir);
+      if(_class == class(CompilerConfig))
+      {
+         PathCatSlash(path, "compilerConfigs");
+         if(dir)
+            strcpy(dir, path);
+         if(configName)
+         {
+            PathCatSlash(path, configName);
+            strcat(path, ".econ");
+         }
+      }
+      else if(_class == class(RecentFilesData))
+         PathCatSlash(path, "recentFiles.econ");
+      else if(_class == class(RecentWorkspacesData))
+         PathCatSlash(path, "recentWorkspaces.econ");
+   }
+
 private:
+   bool oldConfig;
+   FileSize settingsFileSize;
+
    IDESettingsContainer()
    {
       char path[MAX_LOCATION];
@@ -169,7 +570,7 @@ private:
       StripLastDirectory(moduleLocation, moduleLocation);
       ChangeCh(moduleLocation, '\\', '/');
       // PortableApps.com directory structure
-      if((start = strstr(path, "\\App\\EcereSDK\\bin\\ide.exe")))
+      if((start = strstr(path, "\\App\\EcereSDK\\bin\\ecere-ide.exe")))
       {
          char configFilePath[MAX_LOCATION];
          char defaultConfigFilePath[MAX_LOCATION];
@@ -178,13 +579,15 @@ private:
 
          strcpy(configFilePath, path);
          PathCat(configFilePath, "Data");
-         PathCat(configFilePath, "ecereIDE.ini");
+         PathCat(configFilePath, ideSettingsName);
+         ChangeExtension(configFilePath, "ini", configFilePath);
 
          strcpy(defaultConfigFilePath, path);
          PathCat(defaultConfigFilePath, "App");
          PathCat(defaultConfigFilePath, "DefaultData");
-         PathCat(defaultConfigFilePath, "ecereIDE.ini");
-         
+         PathCat(defaultConfigFilePath, ideSettingsName);
+         ChangeExtension(defaultConfigFilePath, "ini", defaultConfigFilePath);
+
          if(FileExists(defaultConfigFilePath))
          {
             if(!FileExists(configFilePath))
@@ -196,9 +599,9 @@ private:
             }
             PathCat(path, "Data");
             // the forced settings location will only be
-            // used if the running ide's path matches 
+            // used if the running ide's path matches
             // the PortableApps.com directory structure
-            // and the default ini file is found in 
+            // and the default ini file is found in
             // the DefaultData directory
             settingsLocation = path;
             portable = true;
@@ -208,20 +611,47 @@ private:
 
    void OnAskReloadSettings()
    {
-      /*if(MessageBox { type = YesNo, master = this, 
-            text = "Global Settings Modified Externally", 
-            contents = "The global settings were modified by another instance.\n"
-            "Would you like to reload them?" }.Modal() == Yes)*/
+      FileSize newSettingsFileSize;
+
+      if(OpenAndLock(&newSettingsFileSize))
       {
-         Load();
+         //if((double)settingsFileSize - (double)newSettingsFileSize < 2048)
+            Load();
+            onLoad();
+         /*else
+         {
+            GuiApplication app = ((GuiApplication)__thisModule.application);
+            Window w;
+            for(w = app.desktop.firstChild; w && (!w.created || !w.visible); w = w.next);
+
+            CloseAndMonitor();
+
+            MessageBox { master = w, type = ok, isModal = true,
+                  creationActivation = flash,
+                  text = "Global Settings Modified Externally",
+                  contents = "The global settings were modified by another process and a drastic shrinking of the settings file was detected.\n"
+                  "The new settings will not be loaded to prevent loss of your ide settings.\n"
+                  "Please check your settings file and make sure to save this IDE's global settings if your settings file has been compromised."
+                  }.Create();
+         }*/
       }
    }
 
-   void Load()
+   SettingsIOResult Load()
    {
-      SettingsIOResult result = GlobalSettings::Load();
-      IDESettings data = (IDESettings)this.data;
-      CompilerConfig defaultCompiler = null;
+      IDESettings data;
+      SettingsIOResult result;
+      useNewConfigurationFiles = true;
+      result = GlobalSettings::Load();
+      data = (IDESettings)this.data;
+      oldConfig = false;
+      if(result == fileNotFound)
+      {
+         oldConfig = true;
+         useNewConfigurationFiles = false;
+         result = GlobalSettings::Load();
+      }
+      data = (IDESettings)this.data;
       if(!data)
       {
          this.data = IDESettings { };
@@ -233,7 +663,7 @@ private:
             bool loaded;
             OldIDESettings oldSettings { };
             Close();
-            loaded = oldSettings.Load();
+            loaded = oldSettings.Load() == success;
             oldSettings.Close();
             if(loaded)
             {
@@ -242,8 +672,8 @@ private:
                for(c : oldSettings.compilerConfigs)
                   data.compilerConfigs.Add(c.Copy());
 
-               for(s : oldSettings.recentFiles) data.recentFiles.Add(CopyString(s));
-               for(s : oldSettings.recentProjects) data.recentProjects.Add(CopyString(s));
+               for(s : oldSettings.recentFiles) data.recentFiles.Add(s);
+               for(s : oldSettings.recentProjects) data.recentProjects.Add(s);
 
                data.docDir = oldSettings.docDir;
                data.ideFileDialogLocation = oldSettings.ideFileDialogLocation;
@@ -254,7 +684,7 @@ private:
                delete data.displayDriver; data.displayDriver = CopyString(oldSettings.displayDriver);
                data.projectDefaultTargetDir = oldSettings.projectDefaultTargetDir;
                data.projectDefaultIntermediateObjDir = oldSettings.projectDefaultIntermediateObjDir;
-                        
+
                Save();
                result = success;
             }
@@ -263,71 +693,341 @@ private:
          if(result == fileNotFound || !data)
          {
             data = (IDESettings)this.data;
-            data.useFreeCaret = true;
+            data.useFreeCaret = false; //true;
             data.showLineNumbers = true;
-            data.caretFollowsScrolling = true;
+            data.caretFollowsScrolling = false; //true;
          }
       }
-      // Ensure we have a default compiler
-      defaultCompiler = data.GetCompilerConfig(defaultCompilerName);
-      if(!defaultCompiler)
-      {
-         defaultCompiler = MakeDefaultCompiler(defaultCompilerName, true);
-         data.compilerConfigs.Add(defaultCompiler);
-      }
-
-      // We incref the compilers below, so reset refCount to 0
-      defaultCompiler._refCount = 0;
 
       CloseAndMonitor();
-      if(data.compilerConfigs)
+      FileGetSize(settingsFilePath, &settingsFileSize);
+      if(portable && moduleLocation[0] && FileExists(moduleLocation).isDirectory)
+         data.ManagePortablePaths(moduleLocation, true);
+      data.ForcePathSeparatorStyle(true);
+
+      if(!data.colorSchemes || !data.colorSchemes.count)
+      {
+         if(!data.colorSchemes) data.colorSchemes = { };
+
+         data.colorSchemes.Add(darkColorScheme);   incref darkColorScheme;
+         data.colorSchemes.Add(lightColorScheme);  incref lightColorScheme;
+         data.colorSchemes.Add(greenColorScheme);  incref greenColorScheme;
+         data.colorSchemes.Add(grayColorScheme);   incref grayColorScheme;
+      }
+      colorScheme = null;
+      if(data.activeColorScheme)
       {
-         for(c : data.compilerConfigs)
+         for(cs : data.colorSchemes; cs.name && !strcmp(cs.name, data.activeColorScheme))
          {
-            CompilerConfig compiler = c;
-            incref compiler;
+            colorScheme = cs;
+            break;
          }
       }
-      if(portable && moduleLocation[0] && FileExists(moduleLocation).isDirectory)
-         data.ManagePortablePaths(moduleLocation, true);
-      data.ForcePathSeparatorStyle(true);
-      OnLoad(data);
+      if(!colorScheme)
+      {
+         colorScheme = data.colorSchemes[0];
+         data.activeColorScheme = colorScheme.name;
+      }
+
+      // Import from previous ecereIDE settings
+      if(oldConfig)
+      {
+         // Save first so that settingsFilePath get set up
+         Save();
+
+         data.compilerConfigs.ensureDefaults();
+         data.compilerConfigs.write(this, null);
+         data.compilerConfigs.Free();
+
+         data.recentFiles.write(this);
+         data.recentFiles.Free();
+
+         data.recentProjects.write(this);
+         data.recentProjects.Free();
+      }
+      return result;
    }
 
-   void Save()
+   SettingsIOResult Save()
    {
-      IDESettings data = (IDESettings)this.data;
-      Platform runtimePlatform = GetRuntimePlatform();
+      SettingsIOResult result;
+      IDESettings data;
+      useNewConfigurationFiles = true;
+      data = (IDESettings)this.data;
       if(portable && moduleLocation[0] && FileExists(moduleLocation).isDirectory)
          data.ManagePortablePaths(moduleLocation, false);
       data.ForcePathSeparatorStyle(true);
-      if(!GlobalSettings::Save())
+      if(oldConfig)
+         settingsFilePath = null;
+      result = GlobalSettings::Save();
+      if(result != success)
          PrintLn("Error saving IDE settings");
+      else
+         oldConfig = false;
       if(portable && moduleLocation[0] && FileExists(moduleLocation).isDirectory)
          data.ManagePortablePaths(moduleLocation, true);
+
       CloseAndMonitor();
+      FileGetSize(settingsFilePath, &settingsFileSize);
+
+      return result;
+   }
+}
+
+static Map<String, String> getCompilerConfigFilePathsByName(const char * path)
+{
+   Map<String, String> map { };
+   FileListing fl { path, extensions = "econ" };
+   while(fl.Find())
+   {
+      if(fl.stats.attribs.isFile)
+      {
+         char name[MAX_FILENAME];
+         char * path = CopyString(fl.path);
+         MakeSlashPath(path);
+         GetLastDirectory(path, name);
+         StripExtension(name);
+         map[name] = path;
+      }
+   }
+   return map;
+}
+
+static Map<String, CompilerConfig> getCompilerConfigsByName(const char * path)
+{
+   Map<String, CompilerConfig> map { };
+   FileListing fl { path, extensions = "econ" };
+   while(fl.Find())
+   {
+      if(fl.stats.attribs.isFile)
+      {
+         char name[MAX_FILENAME];
+         char * path = CopyString(fl.path);
+         MakeSlashPath(path);
+         GetLastDirectory(path, name);
+         StripExtension(name);
+         {
+            CompilerConfig ccfg = CompilerConfig::read(path);
+            if(ccfg)
+               map[name] = ccfg;
+         }
+         delete path;
+      }
+   }
+   return map;
+}
+
+static SettingsIOResult writeConfigFile(const char * path, Class dataType, void * data)
+{
+   SettingsIOResult result = error;
+   SafeFile sf = SafeFile::open(path, write);
+   if(sf)
+   {
+      WriteECONObject(sf.file, dataType, data, 0);
+      sf.sync();
+      delete sf;
+      result = success;
+   }
+   else
+      PrintLn($"error: could not safely open file for writing configuration: ", path);
+   return result;
+}
+
+static SettingsIOResult readConfigFile(const char * path, Class dataType, void ** data)
+{
+   SettingsIOResult result = error;
+   SafeFile sf;
+   if(!FileExists(path))
+      result = fileNotFound;
+   else if((sf = SafeFile::open(path, read)))
+   {
+      JSONResult jsonResult;
+      {
+         ECONParser parser { f = sf.file };
+         sf.file.Seek(0, start);
+         jsonResult = parser.GetObject(dataType, data);
+         if(jsonResult != success)
+            delete *data;
+         delete parser;
+      }
+      if(jsonResult == success)
+         result = success;
+      else
+      {
+         result = fileNotCompatibleWithDriver;
+         PrintLn($"error: could not parse configuration file: ", path);
+      }
+      delete sf;
    }
+   return result;
+}
+
+class SafeFile
+{
+   File file;
+   FileOpenMode mode;
+   char path[MAX_LOCATION];
+   char tmp[MAX_LOCATION];
+
+   SafeFile ::open(const char * path, FileOpenMode mode)
+   {
+      SafeFile result = null;
+      if(mode == write || mode == read)
+      {
+         SafeFile sf { mode = mode };
+         int c;
+         bool locked = false;
+         FileLock lockType = mode == write ? exclusive : shared;
+
+         strcpy(sf.path, path);
+         strcpy(sf.tmp, path);
+         strcat(sf.tmp, ".tmp");
+         if(mode == write && FileExists(sf.tmp).isFile)
+            DeleteFile(sf.tmp);
+
+         if(mode == write)
+         {
+            sf.file = FileOpen(sf.tmp, readWrite);
+            if(!sf.file)
+            {
+               sf.file = FileOpen(sf.tmp, writeRead);
+               if(sf.file)
+               {
+                  delete sf.file;
+                  sf.file = FileOpen(sf.tmp, readWrite);
+               }
+            }
+         }
+         else
+            sf.file = FileOpen(path, mode);
+         if(sf.file)
+         {
+            for(c = 0; c < 10 && !(locked = sf.file.Lock(lockType, 0, 0, false)); c++) Sleep(0.01);
+            if(locked)
+            {
+               sf.file.Truncate(0);
+               sf.file.Seek(0, start);
+               result = sf;
+            }
+            else if(mode == write)
+               PrintLn($"warning: SafeFile::open: unable to obtain exclusive lock on temporary file for writing: ", sf.tmp);
+            else
+               PrintLn($"warning: SafeFile::open: unable to obtain shared lock on file for reading: ", path);
+         }
+         else if(mode == write)
+            PrintLn($"warning: SafeFile::open: unable to open temporary file for writing: ", sf.tmp);
+         else
+            PrintLn($"warning: SafeFile::open: unable to open file for reading: ", path);
+
+         if(!result)
+            delete sf;
+      }
+      else
+         PrintLn($"warning: SafeFile::open: does not yet support FileOpenMode::", mode);
+      return result;
+   }
+
+   void sync()
+   {
+      if(file && mode == write)
+      {
+         int c;
+         File f = FileOpen(path, readWrite);
+         if(!f)
+         {
+            f = FileOpen(path, writeRead);
+            if(f)
+            {
+               delete f;
+               f = FileOpen(path, readWrite);
+            }
+         }
+         if(f)
+         {
+            bool locked = true;
+            for(c = 0; c < 10 && !(locked = f.Lock(exclusive, 0,0, false)); c++) Sleep(0.01);
+
+            if(locked)
+            {
+               f.Unlock(0,0, false);
+               delete f;
+               file.Unlock(0,0, false);
+               delete file;
+
+               for(c = 0; c < 10; c++)
+               {
+                  if(MoveFileEx(tmp, path, { true, true }))
+                     break;
+                  else
+                     Sleep(0.01);
+               }
+            }
+            else
+            {
+               delete f;
+               PrintLn($"warning: SafeFile::sync: failed to lock file for ", mode);
+            }
+         }
+      }
+   }
+
+
+   ~SafeFile()
+   {
+      if(file)
+      {
+         file.Unlock(0,0, false);
+         delete file;
+      }
+   }
+}
+
+class RecentFilesData
+{
+public:
+   RecentFiles recentFiles;
+}
+
+class RecentWorkspacesData
+{
+public:
+   RecentWorkspaces recentWorkspaces;
 }
 
 class IDESettings : GlobalSettingsData
 {
 public:
-   List<CompilerConfig> compilerConfigs { };
-   Array<String> recentFiles { };
-   Array<String> recentProjects { };
-   property char * docDir
+   property CompilerConfigs compilerConfigs
+   {
+      set { /*if(settingsContainer.oldConfig)*/ { if(compilerConfigs) compilerConfigs.Free(); delete compilerConfigs; if(value) compilerConfigs = value; } }
+      get { return compilerConfigs; }
+      isset { return false; }
+   }
+   property RecentFiles recentFiles
+   {
+      set { if(recentFiles) recentFiles.Free(); delete recentFiles; if(value) recentFiles = value; }
+      get { return recentFiles; }
+      isset { return false; }
+   }
+   property RecentWorkspaces recentProjects
+   {
+      set { if(recentProjects) recentProjects.Free(); delete recentProjects; if(value) recentProjects = value; }
+      get { return recentProjects; }
+      isset { return false; }
+   }
+   property const char * docDir
    {
       set { delete docDir; if(value && value[0]) docDir = CopyString(value); }
       get { return docDir ? docDir : ""; }
       isset { return docDir && docDir[0]; }
    }
-   property char * ideFileDialogLocation
+   property const char * ideFileDialogLocation
    {
       set { delete ideFileDialogLocation; if(value && value[0]) ideFileDialogLocation = CopyString(value); }
       get { return ideFileDialogLocation ? ideFileDialogLocation : ""; }
       isset { return ideFileDialogLocation && ideFileDialogLocation[0]; }
    }
-   property char * ideProjectFileDialogLocation
+   property const char * ideProjectFileDialogLocation
    {
       set { delete ideProjectFileDialogLocation; if(value && value[0]) ideProjectFileDialogLocation = CopyString(value); }
       get { return ideProjectFileDialogLocation ? ideProjectFileDialogLocation : ""; }
@@ -337,38 +1037,87 @@ public:
    bool showLineNumbers;
    bool caretFollowsScrolling;
    char * displayDriver;
-   
+
    // TODO: Classify settings
    //EditorSettings editor { };
 
-   property char * projectDefaultTargetDir
+   property const char * projectDefaultTargetDir
    {
       set { delete projectDefaultTargetDir; if(value && value[0]) projectDefaultTargetDir = CopyValidateMakefilePath(value); }
       get { return projectDefaultTargetDir ? projectDefaultTargetDir : ""; }
       isset { return projectDefaultTargetDir && projectDefaultTargetDir[0]; }
    }
-   property char * projectDefaultIntermediateObjDir
+   property const char * projectDefaultIntermediateObjDir
    {
       set { delete projectDefaultIntermediateObjDir; if(value && value[0]) projectDefaultIntermediateObjDir = CopyValidateMakefilePath(value); }
       get { return projectDefaultIntermediateObjDir ? projectDefaultIntermediateObjDir : ""; }
       isset { return projectDefaultIntermediateObjDir && projectDefaultIntermediateObjDir[0]; }
    }
 
-   property char * compilerConfigsDir
+   property const char * compilerConfigsDir
    {
       set { delete compilerConfigsDir; if(value && value[0]) compilerConfigsDir = CopyString(value); }
       get { return compilerConfigsDir ? compilerConfigsDir : ""; }
       isset { return compilerConfigsDir && compilerConfigsDir[0]; }
    }
 
-   property char * defaultCompiler
+   property const char * defaultCompiler
    {
       set { delete defaultCompiler; if(value && value[0]) defaultCompiler = CopyString(value); }
       get { return defaultCompiler && defaultCompiler[0] ? defaultCompiler : defaultCompilerName; }
       isset { return defaultCompiler && defaultCompiler[0]; }
    }
 
+   property const String language
+   {
+      set
+      {
+         delete language;
+         language = CopyString(value);
+      }
+      get { return language; }
+      isset { return language != null; }
+   }
+
+   property const String codeEditorFont
+   {
+      set
+      {
+         delete codeEditorFont;
+         codeEditorFont = CopyString(value);
+      }
+      get { return codeEditorFont; }
+   }
+
+   float codeEditorFontSize;
+   bool showFixedPitchFontsOnly;
+
+   property Array<IDEColorScheme> colorSchemes
+   {
+      set
+      {
+         if(colorSchemes && colorSchemes._refCount < 2)
+            colorSchemes.Free();
+         delete colorSchemes;
+         colorSchemes = value;
+         if(value)
+            incref colorSchemes;
+      }
+      get { return colorSchemes; }
+   }
+
+   property const String activeColorScheme
+   {
+      set
+      {
+         delete activeColorScheme;
+         activeColorScheme = CopyString(value);
+      }
+      get { return activeColorScheme; }
+   }
+
 private:
+   CompilerConfigs compilerConfigs { };
    char * docDir;
    char * ideFileDialogLocation;
    char * ideProjectFileDialogLocation;
@@ -376,44 +1125,42 @@ private:
    char * projectDefaultIntermediateObjDir;
    char * compilerConfigsDir;
    char * defaultCompiler;
+   String language;
+   RecentFiles recentFiles { };
+   RecentWorkspaces recentProjects { };
 
-   CompilerConfig GetCompilerConfig(String compilerName)
-   {
-      char * name = compilerName && compilerName[0] ? compilerName : defaultCompilerName;
-      CompilerConfig compilerConfig = null;
-      for(compiler : compilerConfigs)
-      {
-         if(!strcmp(compiler.name, name))
-         {
-            compilerConfig = compiler;
-            break;
-         }
-      }
-      if(!compilerConfig && compilerConfigs.count)
-         compilerConfig = compilerConfigs.firstIterator.data;
-      if(compilerConfig)
-         incref compilerConfig;
-      return compilerConfig;
-   }
+   Array<IDEColorScheme> colorSchemes;
+
+   String codeEditorFont;
+
+   String activeColorScheme;
+
+   showFixedPitchFontsOnly = true;
+   codeEditorFontSize = 12;
+   codeEditorFont = CopyString("Courier New");
 
    ~IDESettings()
    {
       compilerConfigs.Free();
       delete compilerConfigs;
-      recentFiles.Free();
-      delete recentFiles;
-      recentProjects.Free();
-      delete recentProjects;
+      if(recentProjects) { recentFiles.Free(); delete recentFiles; }
+      if(recentProjects) { recentProjects.Free(); delete recentProjects; }
       delete docDir;
-   
+
       delete projectDefaultTargetDir;
       delete projectDefaultIntermediateObjDir;
       delete compilerConfigsDir;
       delete defaultCompiler;
+      delete language;
 
       delete ideFileDialogLocation;
       delete ideProjectFileDialogLocation;
       delete displayDriver;
+
+      delete codeEditorFont;
+
+      colorSchemes.Free();
+      delete activeColorScheme;
    }
 
    void ForcePathSeparatorStyle(bool unixStyle)
@@ -454,24 +1201,8 @@ private:
             }
          }
       }
-      if(recentFiles && recentFiles.count)
-      {
-         int c;
-         for(c = 0; c < recentFiles.count; c++)
-         {
-            if(recentFiles[c] && recentFiles[c][0])
-               ChangeCh(recentFiles[c], from, to);
-         }
-      }
-      if(recentProjects && recentProjects.count)
-      {
-         int c;
-         for(c = 0; c < recentProjects.count; c++)
-         {
-            if(recentProjects[c] && recentProjects[c][0])
-               ChangeCh(recentProjects[c], from, to);
-         }
-      }
+      recentFiles.changeChar(from, to);
+      recentProjects.changeChar(from, to);
       if(docDir && docDir[0])
          ChangeCh(docDir, from, to);
       if(ideFileDialogLocation && ideFileDialogLocation[0])
@@ -545,7 +1276,7 @@ private:
          compilerConfigsDir = UpdatePortablePath(compilerConfigsDir, location, makeAbsolute);
    }
 
-   char * UpdatePortablePath(char * path, char * location, bool makeAbsolute)
+   char * UpdatePortablePath(char * path, const char * location, bool makeAbsolute)
    {
       char * output;
       if(makeAbsolute)
@@ -571,45 +1302,127 @@ private:
          else
             output = path;
       }
-      return output;
+      return output;
+   }
+}
+
+class RecentFiles : RecentPaths
+{
+   void read(IDESettingsContainer settingsContainer)
+   {
+      char path[MAX_LOCATION];
+      RecentFilesData d = null;
+      Class _class = class(RecentFilesData);
+      settingsContainer.getConfigFilePath(path, _class, null, null);
+      readConfigFile(path, _class, &d);
+      if(d && d.recentFiles && d.recentFiles.count)
+      {
+         Free();
+         Copy((void *)d.recentFiles);
+         settingsContainer.recentFiles = this; // Merge IDEConfigHolder / IDESettingsContainer?
+      }
+      delete d;
+      settingsContainer.recentFilesMonitor.fileName = path;
+      settingsContainer.recentFilesMonitor.StartMonitoring();
+      settingsContainer.onLoadRecentFiles();
+   }
+
+   void write(IDESettingsContainer settingsContainer)
+   {
+      char path[MAX_LOCATION];
+      RecentFilesData d { };
+      Class _class = class(RecentFilesData);
+      settingsContainer.getConfigFilePath(path, _class, null, null);
+      d.recentFiles = this;
+      writeConfigFile(path, _class, d);
+      d.recentFiles = null;
+      delete d;
+   }
+}
+
+class RecentWorkspaces : RecentPaths
+{
+   void read(IDESettingsContainer settingsContainer)
+   {
+      char path[MAX_LOCATION];
+      RecentWorkspacesData d = null;
+      Class _class = class(RecentWorkspacesData);
+      settingsContainer.getConfigFilePath(path, _class, null, null);
+      readConfigFile(path, _class, &d);
+      if(d && d.recentWorkspaces && d.recentWorkspaces.count)
+      {
+         Free();
+         Copy((void *)d.recentWorkspaces);
+         settingsContainer.recentProjects = this; // Merge IDEConfigHolder / IDESettingsContainer?
+      }
+      delete d;
+      settingsContainer.recentProjectsMonitor.fileName = path;
+      settingsContainer.recentProjectsMonitor.StartMonitoring();
+      settingsContainer.onLoadRecentProjects();
+   }
+
+   void write(IDESettingsContainer settingsContainer)
+   {
+      char path[MAX_LOCATION];
+      RecentWorkspacesData d { };
+      Class _class = class(RecentWorkspacesData);
+      settingsContainer.getConfigFilePath(path, _class, null, null);
+      d.recentWorkspaces = this;
+      writeConfigFile(path, _class, d);
+      d.recentWorkspaces = null;
+      delete d;
    }
+}
 
-   void AddRecentFile(char * fileName)
+class RecentPaths : Array<String>
+{
+   IteratorPointer Add(T value)
    {
       int c;
-      char * filePath = CopyString(fileName);
+      char * filePath = (char *)value;
       ChangeCh(filePath, '\\', '/');
-      for(c = 0; c<recentFiles.count; c++)
+      for(c = 0; c < count; c++)
       {
-         if(recentFiles[c] && !fstrcmp(recentFiles[c], filePath))
+         if(this[c] && !fstrcmp(this[c], filePath))
          {
-            recentFiles.Delete((void *)&recentFiles[c]);
+            Delete((void *)&this[c]);
             c--;
          }
       }
-      while(recentFiles.count >= MaxRecent)
-         recentFiles.Delete(recentFiles.GetLast());
-      recentFiles.Insert(null, filePath);
-      //UpdateRecentMenus(owner);
+      return Array::Add((T)filePath);
    }
 
-   void AddRecentProject(char * projectName)
+   IteratorPointer addRecent(const String value)
    {
       int c;
-      char * filePath = CopyString(projectName);
+      char * filePath = CopyString((char *)value);
+      IteratorPointer ip;
       ChangeCh(filePath, '\\', '/');
-      for(c = 0; c<recentProjects.count; c++)
+      for(c = 0; c < count; c++)
       {
-         if(recentProjects[c] && !fstrcmp(recentProjects[c], filePath))
+         if(this[c] && !fstrcmp(this[c], filePath))
          {
-            recentProjects.Delete((void *)&recentProjects[c]);
+            Delete((void *)&this[c]);
             c--;
          }
       }
-      while(recentProjects.count >= MaxRecent)
-         recentProjects.Delete(recentProjects.GetLast());
-      recentProjects.Insert(null, filePath);
-      //UpdateRecentMenus(owner);
+      while(count >= MaxRecent)
+         Delete(GetLast());
+      ip = Insert(null, filePath);
+      return ip;
+   }
+
+   void changeChar(char from, char to)
+   {
+      if(this && count)
+      {
+         int c;
+         for(c = 0; c < count; c++)
+         {
+            if(this[c] && this[c][0])
+               ChangeCh(this[c], from, to);
+         }
+      }
    }
 }
 
@@ -619,7 +1432,7 @@ static const char * compilerTypeSolutionFileVersionString[CompilerType] = { "",
 static const char * compilerTypeYearString[CompilerType] = { "", "", "", "2005", "2008", "2010" };
 static const char * compilerTypeProjectFileExtension[CompilerType] = { "", "", "", "vcproj", "vcproj", "vcxproj" };
 // TODO: i18n with Array
-static Array<String> compilerTypeLongNames
+static Array<const String> compilerTypeLongNames
 { [
    $"GNU Compiler Collection (GCC) / GNU Make",
    $"Tiny C Compiler / GNU Make",
@@ -639,14 +1452,14 @@ public enum CompilerType
       get { return this == vs8 || this == vs9 || this == vs10; }
    }
 
-   property char *
+   property const char *
    {
       get { return OnGetString(null, null, null); }
       set
-      {  
+      {
          if(value)
          {
-            Platform c;
+            CompilerType c;
             for(c = firstCompilerType; c <= lastCompilerType; c++)
                if(!strcmpi(value, compilerTypeNames[c]))
                   return c;
@@ -655,13 +1468,13 @@ public enum CompilerType
       }
    };
 
-   property char * longName { get { return OnGetString(null, (void*)1, null); } };
-   property char * versionString { get { return OnGetString(null, (void*)2, null); } };
-   property char * yearString { get { return OnGetString(null, (void*)3, null); } };
-   property char * projectFileExtension { get { return OnGetString(null, (void*)4, null); } };
-   property char * solutionFileVersionString { get { return OnGetString(null, (void*)5, null); } };
+   property const char * longName { get { return OnGetString(null, (void*)1, null); } };
+   property const char * versionString { get { return OnGetString(null, (void*)2, null); } };
+   property const char * yearString { get { return OnGetString(null, (void*)3, null); } };
+   property const char * projectFileExtension { get { return OnGetString(null, (void*)4, null); } };
+   property const char * solutionFileVersionString { get { return OnGetString(null, (void*)5, null); } };
 
-   char * OnGetString(char * tempString, void * fieldData, bool * needClass)
+   const char * OnGetString(char * tempString, void * fieldData, bool * needClass)
    {
       if(this >= firstCompilerType && this <= lastCompilerType)
       {
@@ -690,82 +1503,131 @@ class CompilerConfig
 
    numJobs = 1;
 public:
-   property char * name
+   property const char * name
    {
-      set
-      {
-         delete name;
-         if(value)
-            name = CopyString(value);
-      }
+      set { delete name; if(value) name = CopyString(value); }
       get { return name; }
    }
    bool readOnly;
    CompilerType type;
    Platform targetPlatform;
    int numJobs;
-   property char * makeCommand
+   property const char * makeCommand
    {
       set { delete makeCommand; if(value && value[0]) makeCommand = CopyString(value); }
       get { return makeCommand; }
       isset { return makeCommand && makeCommand[0]; }
    }
-   property char * ecpCommand
+   property const char * ecpCommand
    {
       set { delete ecpCommand; if(value && value[0]) ecpCommand = CopyString(value); }
       get { return ecpCommand; }
       isset { return ecpCommand && ecpCommand[0]; }
    }
-   property char * eccCommand
+   property const char * eccCommand
    {
       set { delete eccCommand; if(value && value[0]) eccCommand = CopyString(value); }
       get { return eccCommand; }
       isset { return eccCommand && eccCommand[0]; }
    }
-   property char * ecsCommand
+   property const char * ecsCommand
    {
       set { delete ecsCommand; if(value && value[0]) ecsCommand = CopyString(value); }
       get { return ecsCommand; }
       isset { return ecsCommand && ecsCommand[0]; }
    }
-   property char * earCommand
+   property const char * earCommand
    {
       set { delete earCommand; if(value && value[0]) earCommand = CopyString(value); }
       get { return earCommand; }
       isset { return earCommand && earCommand[0]; }
    }
-   property char * cppCommand
+   property const char * cppCommand
    {
       set { delete cppCommand; if(value && value[0]) cppCommand = CopyString(value); }
       get { return cppCommand; }
       isset { return cppCommand && cppCommand[0]; }
    }
-   property char * ccCommand
+   property const char * ccCommand
    {
       set { delete ccCommand; if(value && value[0]) ccCommand = CopyString(value); }
       get { return ccCommand; }
       isset { return ccCommand && ccCommand[0]; }
    }
-   property char * cxxCommand
+   property const char * cxxCommand
    {
       set { delete cxxCommand; if(value && value[0]) cxxCommand = CopyString(value); }
       get { return cxxCommand; }
       isset { return cxxCommand && cxxCommand[0]; }
    }
-   property char * execPrefixCommand
+   property const char * arCommand
+   {
+      set { delete arCommand; if(value && value[0]) arCommand = CopyString(value); }
+      get { return arCommand; }
+      isset { return arCommand && arCommand[0]; }
+   }
+   property const char * ldCommand
+   {
+      set { delete ldCommand; if(value && value[0]) ldCommand = CopyString(value); }
+      get { return ldCommand; }
+      isset { return ldCommand && ldCommand[0]; }
+   }
+   property const char * objectFileExt
+   {
+      set { delete objectFileExt; if(value && value[0]) objectFileExt = CopyString(value); }
+      get { return objectFileExt && objectFileExt[0] ? objectFileExt : objectDefaultFileExt ; }
+      isset { return objectFileExt && objectFileExt[0] && strcmp(objectFileExt, objectDefaultFileExt); }
+   }
+   property const char * staticLibFileExt
+   {
+      set { delete staticLibFileExt; if(value && value[0]) staticLibFileExt = CopyString(value); }
+      get { return staticLibFileExt; }
+      isset { return staticLibFileExt && staticLibFileExt[0]; }
+   }
+   property const char * sharedLibFileExt
+   {
+      set { delete sharedLibFileExt; if(value && value[0]) sharedLibFileExt = CopyString(value); }
+      get { return sharedLibFileExt; }
+      isset { return sharedLibFileExt && sharedLibFileExt[0]; }
+   }
+   property const char * executableFileExt
+   {
+      set { delete executableFileExt; if(value && value[0]) executableFileExt = CopyString(value); }
+      get { return executableFileExt; }
+      isset { return executableFileExt && executableFileExt[0]; }
+   }
+   property const char * executableLauncher
    {
-      set { delete execPrefixCommand; if(value && value[0]) execPrefixCommand = CopyString(value); }
-      get { return execPrefixCommand; }
-      isset { return execPrefixCommand && execPrefixCommand[0]; }
+      set { delete executableLauncher; if(value && value[0]) executableLauncher = CopyString(value); }
+      get { return executableLauncher; }
+      isset { return executableLauncher && executableLauncher[0]; }
    }
+   // TODO: implement CompilerConfig::windresCommand
    bool ccacheEnabled;
    bool distccEnabled;
-   property char * distccHosts
+   // deprecated
+   property bool supportsBitDepth { set { } get { return true; } isset { return false; } }
+
+   property const char * distccHosts
    {
       set { delete distccHosts; if(value && value[0]) distccHosts = CopyString(value); }
       get { return distccHosts; }
       isset { return distccHosts && distccHosts[0]; }
    }
+   property const char * gnuToolchainPrefix
+   {
+      set { delete gnuToolchainPrefix; if(value && value[0]) gnuToolchainPrefix = CopyString(value); }
+      get { return gnuToolchainPrefix; }
+      isset { return gnuToolchainPrefix && gnuToolchainPrefix[0]; }
+   }
+   property const char * sysroot
+   {
+      set { delete sysroot; if(value && value[0]) sysroot = CopyString(value); }
+      get { return sysroot; }
+      isset { return sysroot && sysroot[0]; }
+   }
+   bool resourcesDotEar;
+   bool noStripTarget;
    property Array<String> includeDirs
    {
       set
@@ -850,6 +1712,92 @@ public:
       get { return excludeLibs; }
       isset { return excludeLibs.count != 0; }
    }
+   property Array<String> eCcompilerFlags
+   {
+      set
+      {
+         eCcompilerFlags.Free();
+         if(value)
+         {
+            delete eCcompilerFlags;
+            eCcompilerFlags = value;
+         }
+      }
+      get { return eCcompilerFlags; }
+      isset { return eCcompilerFlags.count != 0; }
+   }
+   property Array<String> compilerFlags
+   {
+      set
+      {
+         compilerFlags.Free();
+         if(value)
+         {
+            delete compilerFlags;
+            compilerFlags = value;
+         }
+      }
+      get { return compilerFlags; }
+      isset { return compilerFlags.count != 0; }
+   }
+   property Array<String> cxxFlags
+   {
+      set
+      {
+         cxxFlags.Free();
+         if(value)
+         {
+            delete cxxFlags;
+            cxxFlags = value;
+         }
+      }
+      get { return cxxFlags; }
+      isset { return cxxFlags.count != 0; }
+   }
+   property Array<String> linkerFlags
+   {
+      set
+      {
+         linkerFlags.Free();
+         if(value)
+         {
+            delete linkerFlags;
+            linkerFlags = value;
+         }
+      }
+      get { return linkerFlags; }
+      isset { return linkerFlags.count != 0; }
+   }
+   // json backward compatibility
+   property const char * gccPrefix
+   {
+      set { delete gnuToolchainPrefix; if(value && value[0]) gnuToolchainPrefix = CopyString(value); }
+      get { return gnuToolchainPrefix; }
+      isset { return false; }
+   }
+   property const char * execPrefixCommand
+   {
+      set { delete executableLauncher; if(value && value[0]) executableLauncher = CopyString(value); }
+      get { return executableLauncher; }
+      isset { return false; }
+   }
+   property const char * outputFileExt
+   {
+      set { delete executableFileExt; if(value && value[0]) executableFileExt = CopyString(value); }
+      get { return executableFileExt; }
+      isset { return false; }
+   }
+   // utility
+   property bool hasDocumentOutput
+   {
+      get
+      {
+         bool result = executableFileExt && executableFileExt[0] &&
+               (!strcmpi(executableFileExt, "htm") || !strcmpi(executableFileExt, "html"));
+         return result;
+      }
+      isset { return false; }
+   }
 private:
    Array<String> includeDirs { };
    Array<String> libraryDirs { };
@@ -859,6 +1807,10 @@ private:
    Array<NamedString> environmentVars { };
    Array<String> prepDirectives { };
    Array<String> excludeLibs { };
+   Array<String> eCcompilerFlags { };
+   Array<String> compilerFlags { };
+   Array<String> cxxFlags { };
+   Array<String> linkerFlags { };
    char * name;
    char * makeCommand;
    char * ecpCommand;
@@ -868,8 +1820,16 @@ private:
    char * cppCommand;
    char * ccCommand;
    char * cxxCommand;
-   char * execPrefixCommand;
+   char * ldCommand;
+   char * arCommand;
+   char * objectFileExt;
+   char * staticLibFileExt;
+   char * sharedLibFileExt;
+   char * executableFileExt;
+   char * executableLauncher;
    char * distccHosts;
+   char * gnuToolchainPrefix;
+   char * sysroot;
    /*union
    {
       struct { Array<String> includes, libraries, executables; };
@@ -886,16 +1846,76 @@ private:
       delete cppCommand;
       delete ccCommand;
       delete cxxCommand;
+      delete ldCommand;
+      delete arCommand;
+      delete objectFileExt;
+      delete staticLibFileExt;
+      delete sharedLibFileExt;
+      delete executableFileExt;
       delete makeCommand;
-      delete execPrefixCommand;
+      delete executableLauncher;
       delete distccHosts;
+      delete gnuToolchainPrefix;
+      delete sysroot;
       if(environmentVars) environmentVars.Free();
       if(includeDirs) { includeDirs.Free(); }
       if(libraryDirs) { libraryDirs.Free(); }
       if(executableDirs) { executableDirs.Free(); }
       if(prepDirectives) { prepDirectives.Free(); }
       if(excludeLibs) { excludeLibs.Free(); }
+      if(compilerFlags) { compilerFlags.Free(); }
+      if(cxxFlags) { cxxFlags.Free(); }
+      if(eCcompilerFlags) { eCcompilerFlags.Free(); }
+      if(linkerFlags) { linkerFlags.Free(); }
+   }
+
+   int OnCompare(CompilerConfig b)
+   {
+      int result;
+      if(
+         !(result = type.OnCompare(b.type)) &&
+         !(result = targetPlatform.OnCompare(b.targetPlatform)) &&
+         !(result = numJobs.OnCompare(b.numJobs)) &&
+         !(result = ccacheEnabled.OnCompare(b.ccacheEnabled)) &&
+         !(result = distccEnabled.OnCompare(b.distccEnabled)) &&
+         !(result = resourcesDotEar.OnCompare(b.resourcesDotEar)) &&
+         !(result = noStripTarget.OnCompare(b.noStripTarget))
+         );
+
+      if(!result &&
+         !(result = name.OnCompare(b.name)) &&
+         !(result = ecpCommand.OnCompare(b.ecpCommand)) &&
+         !(result = eccCommand.OnCompare(b.eccCommand)) &&
+         !(result = ecsCommand.OnCompare(b.ecsCommand)) &&
+         !(result = earCommand.OnCompare(b.earCommand)) &&
+         !(result = cppCommand.OnCompare(b.cppCommand)) &&
+         !(result = ccCommand.OnCompare(b.ccCommand)) &&
+         !(result = cxxCommand.OnCompare(b.cxxCommand)) &&
+         !(result = ldCommand.OnCompare(b.ldCommand)) &&
+         !(result = arCommand.OnCompare(b.arCommand)) &&
+         !(result = objectFileExt.OnCompare(b.objectFileExt)) &&
+         !(result = outputFileExt.OnCompare(b.outputFileExt)) &&
+         !(result = makeCommand.OnCompare(b.makeCommand)) &&
+         !(result = executableLauncher.OnCompare(b.executableLauncher)) &&
+         !(result = distccHosts.OnCompare(b.distccHosts)) &&
+         !(result = gnuToolchainPrefix.OnCompare(b.gnuToolchainPrefix)) &&
+         !(result = sysroot.OnCompare(b.sysroot)));
+
+      if(!result &&
+         !(result = includeDirs.OnCompare(b.includeDirs)) &&
+         !(result = libraryDirs.OnCompare(b.libraryDirs)) &&
+         !(result = executableDirs.OnCompare(b.executableDirs)) &&
+         !(result = environmentVars.OnCompare(b.environmentVars)) &&
+         !(result = prepDirectives.OnCompare(b.prepDirectives)) &&
+         !(result = excludeLibs.OnCompare(b.excludeLibs)) &&
+         !(result = cxxFlags.OnCompare(b.cxxFlags)) &&
+         !(result = eCcompilerFlags.OnCompare(b.eCcompilerFlags)) &&
+         !(result = compilerFlags.OnCompare(b.compilerFlags)) &&
+         !(result = linkerFlags.OnCompare(b.linkerFlags)));
+      return result;
    }
+
+public:
    CompilerConfig Copy()
    {
       CompilerConfig copy
@@ -913,10 +1933,21 @@ private:
          cppCommand,
          ccCommand,
          cxxCommand,
-         execPrefixCommand,
+         arCommand,
+         ldCommand,
+         objectFileExt,
+         staticLibFileExt,
+         sharedLibFileExt,
+         executableFileExt,
+         executableLauncher,
          ccacheEnabled,
          distccEnabled,
-         distccHosts
+         false,
+         distccHosts,
+         gnuToolchainPrefix,
+         sysroot,
+         resourcesDotEar,
+         noStripTarget
       };
       for(s : includeDirs) copy.includeDirs.Add(CopyString(s));
       for(s : libraryDirs) copy.libraryDirs.Add(CopyString(s));
@@ -924,8 +1955,436 @@ private:
       for(ns : environmentVars) copy.environmentVars.Add(NamedString { name = ns.name, string = ns.string });
       for(s : prepDirectives) copy.prepDirectives.Add(CopyString(s));
       for(s : excludeLibs) copy.excludeLibs.Add(CopyString(s));
+      for(s : compilerFlags) copy.compilerFlags.Add(CopyString(s));
+      for(s : cxxFlags) copy.cxxFlags.Add(CopyString(s));
+      for(s : eCcompilerFlags) copy.eCcompilerFlags.Add(CopyString(s));
+      for(s : linkerFlags) copy.linkerFlags.Add(CopyString(s));
 
       incref copy;
       return copy;
    }
+
+   CompilerConfig ::read(const char * path)
+   {
+      CompilerConfig d = null;
+      readConfigFile(path, class(CompilerConfig), &d);
+      return d;
+   }
+
+   void write(IDESettingsContainer settingsContainer)
+   {
+      char dir[MAX_LOCATION];
+      char path[MAX_LOCATION];
+      const char * settingsFilePath = settingsContainer.settingsFilePath;
+      settingsContainer.getConfigFilePath(path, _class, dir, name);
+      if(FileExists(settingsFilePath) && !FileExists(dir))
+      {
+         MakeDir(dir);
+         if(!FileExists(dir))
+            PrintLn($"Error creating compiler configs directory at ", dir, " location.");
+      }
+      writeConfigFile(path, _class, this);
+   }
+}
+
+class CompilerConfigs : List<CompilerConfig>
+{
+   CompilerConfig GetCompilerConfig(const String compilerName)
+   {
+      const char * name = compilerName && compilerName[0] ? compilerName : defaultCompilerName;
+      CompilerConfig compilerConfig = null;
+      for(compiler : this)
+      {
+         if(!strcmp(compiler.name, name))
+         {
+            compilerConfig = compiler;
+            break;
+         }
+      }
+      if(!compilerConfig && count)
+         compilerConfig = this[0];
+      if(compilerConfig)
+      {
+         incref compilerConfig;
+         if(compilerConfig._refCount == 1)
+            incref compilerConfig;
+      }
+      return compilerConfig;
+   }
+
+   void ensureDefaults()
+   {
+      // Ensure we have a default compiler
+      CompilerConfig defaultCompiler = GetCompilerConfig(defaultCompilerName);
+      if(!defaultCompiler)
+      {
+         defaultCompiler = MakeDefaultCompiler(defaultCompilerName, true);
+         Insert(null, defaultCompiler);
+         defaultCompiler = null;
+      }
+      delete defaultCompiler;
+
+      for(ccfg : this)
+      {
+         if(!ccfg.ecpCommand || !ccfg.ecpCommand[0])
+            ccfg.ecpCommand = ecpDefaultCommand;
+         if(!ccfg.eccCommand || !ccfg.eccCommand[0])
+            ccfg.eccCommand = eccDefaultCommand;
+         if(!ccfg.ecsCommand || !ccfg.ecsCommand[0])
+            ccfg.ecsCommand = ecsDefaultCommand;
+         if(!ccfg.earCommand || !ccfg.earCommand[0])
+            ccfg.earCommand = earDefaultCommand;
+         if(!ccfg.cppCommand || !ccfg.cppCommand[0])
+            ccfg.cppCommand = cppDefaultCommand;
+         if(!ccfg.ccCommand || !ccfg.ccCommand[0])
+            ccfg.ccCommand = ccDefaultCommand;
+         if(!ccfg.cxxCommand || !ccfg.cxxCommand[0])
+            ccfg.cxxCommand = cxxDefaultCommand;
+         /*if(!ccfg.ldCommand || !ccfg.ldCommand[0])
+            ccfg.ldCommand = ldDefaultCommand;*/
+         if(!ccfg.arCommand || !ccfg.arCommand[0])
+            ccfg.arCommand = arDefaultCommand;
+         if(!ccfg.objectFileExt || !ccfg.objectFileExt[0])
+            ccfg.objectFileExt = objectDefaultFileExt;
+         /*if(!ccfg.staticLibFileExt || !ccfg.staticLibFileExt[0])
+            ccfg.staticLibFileExt = staticLibDefaultFileExt;*/
+         /*if(!ccfg.sharedLibFileExt || !ccfg.sharedLibFileExt[0])
+            ccfg.sharedLibFileExt = sharedLibDefaultFileExt;*/
+         /*if(!ccfg.executableFileExt || !ccfg.executableFileExt[0])
+            ccfg.executableFileExt = outputDefaultFileExt;*/
+         if(!ccfg._refCount) incref ccfg;
+      }
+   }
+
+   AVLTree<String> getWriteRequiredList(CompilerConfigs oldConfigs)
+   {
+      AVLTree<String> list { };
+      for(ccfg : this)
+      {
+         bool found = false;
+         for(occfg : oldConfigs; !strcmp(ccfg.name, occfg.name))
+         {
+            found = true;
+            if(ccfg.OnCompare(occfg))
+               list.Add(CopyString(ccfg.name));
+            break;
+         }
+         if(!found)
+            list.Add(CopyString(ccfg.name));
+      }
+      return list;
+   }
+
+   bool read(IDESettingsContainer settingsContainer)
+   {
+      if(settingsContainer.settingsFilePath)
+      {
+         char dir[MAX_LOCATION];
+         char path[MAX_LOCATION];
+         Class _class = class(CompilerConfig);
+         settingsContainer.getConfigFilePath(path, _class, dir, null);
+         if(dir[0])
+         {
+            AVLTree<const String> addedConfigs { };
+            Map<String, CompilerConfig> compilerConfigsByName = getCompilerConfigsByName(dir);
+            MapIterator<const String, CompilerConfig> it { map = compilerConfigsByName };
+            Free();
+            settingsContainer.compilerConfigs = this; // Merge IDEConfigHolder / IDESettingsContainer?
+            if(it.Index("Default", false))
+            {
+               CompilerConfig ccfg = it.data;
+               Add(ccfg.Copy());
+               addedConfigs.Add(ccfg.name);
+            }
+            for(ccfg : compilerConfigsByName)
+            {
+               if(!addedConfigs.Find(ccfg.name))
+               {
+                  Add(ccfg.Copy());
+                  addedConfigs.Add(ccfg.name);
+               }
+            }
+            delete addedConfigs;
+            ensureDefaults();
+            compilerConfigsByName.Free();
+            delete compilerConfigsByName;
+            settingsContainer.onLoadCompilerConfigs();
+            return true;
+         }
+      }
+      return false;
+   }
+
+   void write(IDESettingsContainer settingsContainer, AVLTree<String> cfgsToWrite)
+   {
+      char dir[MAX_LOCATION];
+      char path[MAX_LOCATION];
+      Map<String, String> paths;
+      settingsContainer.getConfigFilePath(path, class(CompilerConfig), dir, null);
+      paths = getCompilerConfigFilePathsByName(dir);
+      {
+         MapIterator<String, String> it { map = paths };
+         for(c : this)
+         {
+            CompilerConfig ccfg = c;
+            if(!cfgsToWrite || cfgsToWrite.Find(ccfg.name))
+               ccfg.write(settingsContainer);
+            if(it.Index(ccfg.name, false))
+            {
+               delete it.data;
+               it.Remove();
+            }
+         }
+      }
+      for(p : paths)
+      {
+         const char * path = p;
+         DeleteFile(path);
+      }
+      paths.Free();
+      delete paths;
+   }
+}
+
+#if !defined(ECERE_DOCUMENTOR) && !defined(ECERE_EPJ2MAKE)
+struct LanguageOption
+{
+   const String name;
+   const String bitmap;
+   const String code;
+   BitmapResource res;
+
+   const char * OnGetString(char * tempString, void * fieldData, bool * needClass)
+   {
+      return name;
+   }
+
+   void OnDisplay(Surface surface, int x, int y, int width, void * data, Alignment alignment, DataDisplayFlags flags)
+   {
+      Bitmap icon = res ? res.bitmap : null;
+      int w = 8 + 16;
+      if(icon)
+         surface.Blit(icon, x + (16 - icon.width) / 2,y+2,0,0, icon.width, icon.height);
+      class::OnDisplay(surface, x + w, y, width - w, null, alignment, flags);
+   }
+};
+
+Array<LanguageOption> languages
+{ [
+   { "English",            ":countryCode/gb.png", "" },
+   { "汉语",                ":countryCode/cn.png", "zh_CN" },
+   { "Español",            ":countryCode/es.png", "es" },
+   { "Português (Brazil)", ":countryCode/br.png", "pt_BR" },
+   { "Русский (43%)",      ":countryCode/ru.png", "ru" },
+   { "Nederlandse (13%)",  ":countryCode/nl.png", "nl" },
+   { "Tiếng Việt (12%)",   ":countryCode/vn.png", "vi" },
+   { "मराठी (10%)",          ":countryCode/in.png", "mr" },
+   { "Hebrew (8%)",        ":countryCode/il.png", "he" },
+   { "Magyar (8%)",        ":countryCode/hu.png", "hu" }
+] };
+
+const String GetLanguageString()
+{
+   char * dot, * colon;
+   static char lang[256];
+   const String language = getenv("ECERE_LANGUAGE");
+   if(!language) language = getenv("LANGUAGE");
+   if(!language) language = getenv("LC_ALL");
+   if(!language) language = getenv("LC_MESSAGES");
+   if(!language) language = getenv("LANG");
+   if(!language) language = "";
+   if(language && (colon = strchr(language, ':')))
+   {
+      if(lang != language)
+         strncpy(lang, language, sizeof(lang));
+      lang[sizeof(lang)-1] = 0;
+      lang[colon - language] = 0;
+      language = lang;
+   }
+   if(language && (dot = strchr(language, '.')))
+   {
+      if(lang != language)
+         strncpy(lang, language, sizeof(lang));
+      lang[sizeof(lang)-1] = 0;
+      lang[dot - language] = 0;
+      language = lang;
+   }
+   return language;
+}
+
+void setEcereLanguageInWinRegEnvironment(const char * languageCode)
+{
+#ifdef __WIN32__
+   HKEY key = null;
+   uint16 wLanguage[256];
+   DWORD status;
+   wLanguage[0] = 0;
+
+   RegCreateKeyEx(HKEY_CURRENT_USER, "Environment", 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, null, &key, &status);
+   if(key)
+   {
+      UTF8toUTF16Buffer(languageCode, wLanguage, sizeof(wLanguage) / sizeof(uint16));
+      RegSetValueExW(key, L"ECERE_LANGUAGE", 0, REG_EXPAND_SZ, (byte *)wLanguage, (uint)(wcslen(wLanguage)+1) * 2);
+      RegCloseKey(key);
+   }
+#endif
+}
+
+bool LanguageRestart(const char * code, Application app, IDESettings settings, IDESettingsContainer settingsContainer, Window ide, Window projectView, bool wait)
+{
+   bool restart = true;
+   String command = null;
+   int arg0Len = (int)strlen(app.argv[0]);
+   int len = arg0Len;
+   int j;
+   char ch;
+
+   if(ide)
+   {
+      Window w;
+
+      if(projectView)
+      {
+         Window w;
+         for(w = ide.firstChild; w; w = w.next)
+         {
+            if(w.isActiveClient && w.isDocument)
+            {
+               if(!w.CloseConfirmation(true))
+               {
+                  restart = false;
+                  break;
+               }
+            }
+         }
+         if(restart)
+         {
+            if(!projectView.CloseConfirmation(true))
+               restart = false;
+            if(projectView.fileName)
+            {
+               const char * name = projectView.fileName;
+               if(name)
+               {
+                  for(j = 0; (ch = name[j]); j++)
+                     len += (ch == ' ' || ch == '\"' || ch == '&' || ch == '$' || ch == '(' || ch == ')') ? 2 : 1;
+               }
+            }
+
+            command = new char[len + 1];
+
+            strcpy(command, app.argv[0]);
+            len = arg0Len;
+            if(projectView.fileName)
+            {
+               strcat(command, " ");
+               len++;
+               ReplaceSpaces(command + len, projectView.fileName);
+            }
+         }
+         if(restart)
+         {
+            for(w = ide.firstChild; w; w = w.next)
+               if(w.isActiveClient && w.isDocument)
+                  w.modifiedDocument = false;
+            projectView.modifiedDocument = false;
+         }
+      }
+      else
+      {
+         for(w = ide.firstChild; w; w = w.next)
+         {
+            if(w.isActiveClient && w.isDocument)
+            {
+               if(!w.CloseConfirmation(true))
+               {
+                  restart = false;
+                  break;
+               }
+               if(w.fileName)
+               {
+                  const char * name = w.fileName;
+                  len++;
+                  for(j = 0; (ch = name[j]); j++)
+                     len += (ch == ' ' || ch == '\"' || ch == '&' || ch == '$' || ch == '(' || ch == ')') ? 2 : 1;
+               }
+            }
+         }
+
+         if(restart)
+         {
+            command = new char[len + 1];
+            strcpy(command, app.argv[0]);
+            len = arg0Len;
+
+            for(w = ide.firstChild; w; w = w.next)
+            {
+               if(w.isActiveClient && w.isDocument)
+               {
+                  const char * name = w.fileName;
+                  if(name)
+                  {
+                     strcat(command, " ");
+                     len++;
+                     ReplaceSpaces(command + len, name);
+                     len = (int)strlen(command);
+                  }
+               }
+            }
+         }
+         if(restart)
+         {
+            for(w = ide.firstChild; w; w = w.next)
+               if(w.isActiveClient && w.isDocument)
+                  w.modifiedDocument = false;
+         }
+      }
+      if(restart)
+      {
+         settings.language = code;
+         settingsContainer.Save();
+
+         setEcereLanguageInWinRegEnvironment(code);
+
+         if(eClass_IsDerived(app._class, class(GuiApplication)))
+         {
+            GuiApplication guiApp = (GuiApplication)app;
+            guiApp.desktop.Destroy(0);
+         }
+      }
+   }
+   else
+   {
+      int i;
+      for(i = 1; i < app.argc; i++)
+      {
+         const char * arg = app.argv[i];
+         len++;
+         for(j = 0; (ch = arg[j]); j++)
+            len += (ch == ' ' || ch == '\"' || ch == '&' || ch == '$' || ch == '(' || ch == ')') ? 2 : 1;
+      }
+
+      command = new char[len + 1];
+      strcpy(command, app.argv[0]);
+      len = arg0Len;
+      for(i = 1; i < app.argc; i++)
+      {
+         strcat(command, " ");
+         len++;
+         ReplaceSpaces(command + len, app.argv[i]);
+         len = (int)strlen(command);
+      }
+   }
+
+   if(restart)
+   {
+      SetEnvironment("ECERE_LANGUAGE", code);
+      if(wait)
+         ExecuteWait(command);
+      else
+         Execute(command);
+   }
+   delete command;
+   return restart;
 }
+#endif