Browse Source

Using JSON for GUI settings (#1015)

Replaces strange custom format with JSON.
Is backwards compatible, if wx.json does not exist, uses wx instead (if exists), always writes json.
Fixes wxplot settings dictionary to contain color as tuple, not wx.Colour.
Anna Petrasova 4 years ago
parent
commit
cea6b84506
2 changed files with 78 additions and 60 deletions
  1. 75 57
      gui/wxpython/core/settings.py
  2. 3 3
      gui/wxpython/wxplot/dialogs.py

+ 75 - 57
gui/wxpython/core/settings.py

@@ -25,18 +25,65 @@ import os
 import sys
 import sys
 import copy
 import copy
 import wx
 import wx
+import json
 
 
 from core import globalvar
 from core import globalvar
 from core.gcmd import GException, GError
 from core.gcmd import GException, GError
 from core.utils import GetSettingsPath, PathJoin, rgb2str
 from core.utils import GetSettingsPath, PathJoin, rgb2str
 
 
 
 
+class SettingsJSONEncoder(json.JSONEncoder):
+    """Custom JSON encoder.
+
+    Encodes color represented internally as tuple
+    to hexadecimal color (tuple is represented as
+    list in JSON, however GRASS expects tuple for colors).
+    """
+    def default(self, obj):
+        """Encode not automatically serializable objects.
+        """
+        # we could use dictionary mapping as in wxplot
+        if isinstance(obj, (wx.FontFamily, wx.FontStyle, wx.FontWeight)):
+            return int(obj)
+        return json.JSONEncoder.default(self, obj)
+
+    def iterencode(self, obj):
+        """Encode color tuple"""
+        def color(item):
+            if isinstance(item, tuple):
+                if len(item) == 3:
+                    return "#{0:02x}{1:02x}{2:02x}".format(*item)
+                if len(item) == 4:
+                    return "#{0:02x}{1:02x}{2:02x}{3:02x}".format(*item)
+            if isinstance(item, list):
+                return [color(e) for e in item]
+            if isinstance(item, dict):
+                return {key: color(value) for key, value in item.items()}
+            else:
+                return item
+
+        return super(SettingsJSONEncoder, self).iterencode(color(obj))
+
+
+def settings_JSON_decode_hook(obj):
+    """Decode hex color saved in settings into tuple"""
+    def colorhex2tuple(hexcode):
+        hexcode = hexcode.lstrip('#')
+        return tuple(int(hexcode[i:i + 2], 16) for i in range(0, len(hexcode), 2))
+
+    for k, v in obj.items():
+        if isinstance(v, str) and v.startswith('#') and len(v) in [7, 9]:
+            obj[k] = colorhex2tuple(v)
+    return obj
+
+
 class Settings:
 class Settings:
     """Generic class where to store settings"""
     """Generic class where to store settings"""
 
 
     def __init__(self):
     def __init__(self):
         # settings file
         # settings file
-        self.filePath = os.path.join(GetSettingsPath(), 'wx')
+        self.filePath = os.path.join(GetSettingsPath(), 'wx.json')
+        self.legacyFilePath = os.path.join(GetSettingsPath(), 'wx')
 
 
         # key/value separator
         # key/value separator
         self.sep = ';'
         self.sep = ';'
@@ -150,7 +197,7 @@ class Settings:
             'appearance': {
             'appearance': {
                 'outputfont': {
                 'outputfont': {
                     'type': 'Courier New',
                     'type': 'Courier New',
-                    'size': '10',
+                    'size': 10,
                 },
                 },
                 # expand/collapse element list
                 # expand/collapse element list
                 'elementListExpand': {
                 'elementListExpand': {
@@ -216,7 +263,7 @@ class Settings:
                     'selection': 0,
                     'selection': 0,
                 },
                 },
                 'nvizDepthBuffer': {
                 'nvizDepthBuffer': {
-                    'value': '16',
+                    'value': 16,
                 },
                 },
             },
             },
             #
             #
@@ -979,7 +1026,10 @@ class Settings:
         if settings is None:
         if settings is None:
             settings = self.userSettings
             settings = self.userSettings
 
 
-        self._readFile(self.filePath, settings)
+        if os.path.exists(self.filePath):
+            self._readFile(settings)
+        elif os.path.exists(self.legacyFilePath):
+            self._readLegacyFile(settings)
 
 
         # set environment variables
         # set environment variables
         font = self.Get(group='display', key='font', subkey='type')
         font = self.Get(group='display', key='font', subkey='type')
@@ -989,24 +1039,34 @@ class Settings:
         if enc:
         if enc:
             os.environ["GRASS_ENCODING"] = enc
             os.environ["GRASS_ENCODING"] = enc
 
 
-    def _readFile(self, filename, settings=None):
-        """Read settings from file to dict
+    def _readFile(self, settings=None):
+        """Read settings from file (wx.json) to dict,
+        assumes file exists.
+
+        :param settings: dict where to store settings (None for self.userSettings)
+        """
+        try:
+            with open(self.filePath, 'r') as f:
+                settings.update(json.load(f, object_hook=settings_JSON_decode_hook))
+        except json.JSONDecodeError as e:
+            sys.stderr.write(
+                _("Unable to read settings file <{path}>:\n{err}").format(path=self.filePath, err=e))
+
+    def _readLegacyFile(self, settings=None):
+        """Read settings from legacy file (wx) to dict,
+        assumes file exists.
 
 
-        :param filename: settings file path
         :param settings: dict where to store settings (None for self.userSettings)
         :param settings: dict where to store settings (None for self.userSettings)
         """
         """
         if settings is None:
         if settings is None:
             settings = self.userSettings
             settings = self.userSettings
 
 
-        if not os.path.exists(filename):
-            return
-
         try:
         try:
-            fd = open(filename, "r")
+            fd = open(self.legacyFilePath, "r")
         except IOError:
         except IOError:
             sys.stderr.write(
             sys.stderr.write(
                 _("Unable to read settings file <%s>\n") %
                 _("Unable to read settings file <%s>\n") %
-                filename)
+                self.legacyFilePath)
             return
             return
 
 
         try:
         try:
@@ -1034,7 +1094,7 @@ class Settings:
                 "Error: Reading settings from file <%(file)s> failed.\n"
                 "Error: Reading settings from file <%(file)s> failed.\n"
                 "\t\tDetails: %(detail)s\n"
                 "\t\tDetails: %(detail)s\n"
                 "\t\tLine: '%(line)s'\n") % {
                 "\t\tLine: '%(line)s'\n") % {
-                'file': filename, 'detail': e, 'line': line},
+                'file': self.legacyFilePath, 'detail': e, 'line': line},
                 file=sys.stderr)
                 file=sys.stderr)
             fd.close()
             fd.close()
 
 
@@ -1052,57 +1112,15 @@ class Settings:
             except:
             except:
                 GError(_('Unable to create settings directory'))
                 GError(_('Unable to create settings directory'))
                 return
                 return
-
         try:
         try:
-            newline = '\n'
-            file = open(self.filePath, "w")
-            for group in list(settings.keys()):
-                for key in list(settings[group].keys()):
-                    subkeys = list(settings[group][key].keys())
-                    file.write('%s%s%s%s' % (group, self.sep, key, self.sep))
-                    for idx in range(len(subkeys)):
-                        value = settings[group][key][subkeys[idx]]
-                        if isinstance(value, dict):
-                            if idx > 0:
-                                file.write(
-                                    '%s%s%s%s%s' %
-                                    (newline, group, self.sep, key, self.sep))
-                            file.write('%s%s' % (subkeys[idx], self.sep))
-                            kvalues = list(settings[group][key][subkeys[idx]].keys())
-                            srange = range(len(kvalues))
-                            for sidx in srange:
-                                svalue = self._parseValue(
-                                    settings[group][key][
-                                        subkeys[idx]][
-                                        kvalues[sidx]])
-                                file.write('%s%s%s' % (kvalues[sidx], self.sep,
-                                                       svalue))
-                                if sidx < len(kvalues) - 1:
-                                    file.write('%s' % self.sep)
-                        else:
-                            if idx > 0 and isinstance(
-                                    settings[group][key][subkeys[idx - 1]],
-                                    dict):
-                                file.write(
-                                    '%s%s%s%s%s' %
-                                    (newline, group, self.sep, key, self.sep))
-                            value = self._parseValue(
-                                settings[group][key][subkeys[idx]])
-                            file.write(
-                                '%s%s%s' %
-                                (subkeys[idx], self.sep, value))
-                            if idx < len(subkeys) - 1 and not isinstance(
-                                    settings[group][key][subkeys[idx + 1]],
-                                    dict):
-                                file.write('%s' % self.sep)
-                    file.write(newline)
+            with open(self.filePath, 'w') as f:
+                json.dump(settings, f, indent=2, cls=SettingsJSONEncoder)
         except IOError as e:
         except IOError as e:
             raise GException(e)
             raise GException(e)
         except Exception as e:
         except Exception as e:
             raise GException(_('Writing settings to file <%(file)s> failed.'
             raise GException(_('Writing settings to file <%(file)s> failed.'
                                '\n\nDetails: %(detail)s') %
                                '\n\nDetails: %(detail)s') %
                              {'file': self.filePath, 'detail': e})
                              {'file': self.filePath, 'detail': e})
-        file.close()
         return self.filePath
         return self.filePath
 
 
     def _parseValue(self, value, read=False):
     def _parseValue(self, value, read=False):

+ 3 - 3
gui/wxpython/wxplot/dialogs.py

@@ -1518,7 +1518,7 @@ class OptDialog(wx.Dialog):
     def UpdateSettings(self):
     def UpdateSettings(self):
         """Apply settings to each map and to entire plot"""
         """Apply settings to each map and to entire plot"""
         self.raster[self.map]['pcolor'] = self.FindWindowById(
         self.raster[self.map]['pcolor'] = self.FindWindowById(
-            self.wxId['pcolor']).GetColour()
+            self.wxId['pcolor']).GetColour().Get()
         self.properties['raster']['pcolor'] = self.raster[self.map]['pcolor']
         self.properties['raster']['pcolor'] = self.raster[self.map]['pcolor']
 
 
         self.raster[self.map]['plegend'] = self.FindWindowById(
         self.raster[self.map]['plegend'] = self.FindWindowById(
@@ -1561,7 +1561,7 @@ class OptDialog(wx.Dialog):
 
 
         if self.plottype == 'profile':
         if self.plottype == 'profile':
             self.properties['marker']['color'] = self.FindWindowById(
             self.properties['marker']['color'] = self.FindWindowById(
-                self.wxId['marker']['color']).GetColour()
+                self.wxId['marker']['color']).GetColour().Get()
             self.properties['marker']['fill'] = self.FindWindowById(
             self.properties['marker']['fill'] = self.FindWindowById(
                 self.wxId['marker']['fill']).GetStringSelection()
                 self.wxId['marker']['fill']).GetStringSelection()
             self.properties['marker']['size'] = self.FindWindowById(
             self.properties['marker']['size'] = self.FindWindowById(
@@ -1572,7 +1572,7 @@ class OptDialog(wx.Dialog):
                 self.wxId['marker']['legend']).GetValue()
                 self.wxId['marker']['legend']).GetValue()
 
 
         self.properties['grid']['color'] = self.FindWindowById(
         self.properties['grid']['color'] = self.FindWindowById(
-            self.wxId['grid']['color']).GetColour()
+            self.wxId['grid']['color']).GetColour().Get()
         self.properties['grid']['enabled'] = self.FindWindowById(
         self.properties['grid']['enabled'] = self.FindWindowById(
             self.wxId['grid']['enabled']).IsChecked()
             self.wxId['grid']['enabled']).IsChecked()