瀏覽代碼

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 年之前
父節點
當前提交
cea6b84506
共有 2 個文件被更改,包括 78 次插入60 次删除
  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 copy
 import wx
+import json
 
 from core import globalvar
 from core.gcmd import GException, GError
 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:
     """Generic class where to store settings"""
 
     def __init__(self):
         # 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
         self.sep = ';'
@@ -150,7 +197,7 @@ class Settings:
             'appearance': {
                 'outputfont': {
                     'type': 'Courier New',
-                    'size': '10',
+                    'size': 10,
                 },
                 # expand/collapse element list
                 'elementListExpand': {
@@ -216,7 +263,7 @@ class Settings:
                     'selection': 0,
                 },
                 'nvizDepthBuffer': {
-                    'value': '16',
+                    'value': 16,
                 },
             },
             #
@@ -979,7 +1026,10 @@ class Settings:
         if settings is None:
             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
         font = self.Get(group='display', key='font', subkey='type')
@@ -989,24 +1039,34 @@ class Settings:
         if 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)
         """
         if settings is None:
             settings = self.userSettings
 
-        if not os.path.exists(filename):
-            return
-
         try:
-            fd = open(filename, "r")
+            fd = open(self.legacyFilePath, "r")
         except IOError:
             sys.stderr.write(
                 _("Unable to read settings file <%s>\n") %
-                filename)
+                self.legacyFilePath)
             return
 
         try:
@@ -1034,7 +1094,7 @@ class Settings:
                 "Error: Reading settings from file <%(file)s> failed.\n"
                 "\t\tDetails: %(detail)s\n"
                 "\t\tLine: '%(line)s'\n") % {
-                'file': filename, 'detail': e, 'line': line},
+                'file': self.legacyFilePath, 'detail': e, 'line': line},
                 file=sys.stderr)
             fd.close()
 
@@ -1052,57 +1112,15 @@ class Settings:
             except:
                 GError(_('Unable to create settings directory'))
                 return
-
         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:
             raise GException(e)
         except Exception as e:
             raise GException(_('Writing settings to file <%(file)s> failed.'
                                '\n\nDetails: %(detail)s') %
                              {'file': self.filePath, 'detail': e})
-        file.close()
         return self.filePath
 
     def _parseValue(self, value, read=False):

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

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