Source code for rkviewer.config

"""Configuration parameters.
"""
# pylint: disable=maybe-no-member
# from __future__ import annotations

import copy
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any, List, Tuple

import commentjson
import wx
from commentjson.commentjson import JSONLibraryException
from marshmallow import Schema, ValidationError, fields
from marshmallow import missing as missing_
from marshmallow import validate

from rkviewer.canvas.geometry import Rect, Vec2
from rkviewer.utils import get_local_path

# TODO rename get_local_path
#config_dir = "" #get_local_path('.rkviewer')  # This will be set by either appsettings (see below) or CreateConfigDir in view.poy
#settings_theme_path = os.path.join(config_dir, 'settings.json')
#default_settings_path = os.path.join(config_dir, '.default-settings.json')


[docs]def GetConfigDir (): sp = wx.StandardPaths.Get() return sp.GetUserConfigDir()
[docs]def GetThemeSettingsPath (): sp = wx.StandardPaths.Get() config_dir = sp.GetUserConfigDir() return os.path.join(config_dir, 'rkViewer', 'settings.json')
[docs]@dataclass(frozen=True, eq=True) class Color: r: int g: int b: int a: int = 255
[docs] def swapped(self, r: int = None, g: int = None, b: int = None, a: int = None) -> 'Color': if r is None: r = self.r if g is None: g = self.g if b is None: b = self.b if a is None: a = self.a return Color(r, g, b, a)
[docs] def to_wxcolour(self): return wx.Colour(self.r, self.g, self.b, self.a)
[docs] @classmethod def from_rgb(cls, val: int) -> 'Color': """Create color from RGB hex value: #00BBGGRR""" return Color((val >> 0) & 255, (val >> 8) & 255, (val >> 16) & 255)
[docs]@dataclass class Font: pointSize: int color: Color
# family: str # TODO change to enum # style: str # weight: str # name: str # def __init__(self): # self.pointSize = 20 # self.color = Color(0, 0, 0, 255) # #self.family = "default" # #self.style = "normal" # #self.weight = "default" # #self.name = "" # Application based settings stored in an ini file
[docs]class AppSettings: # Default positions def __init__(self): self.position = wx.Point (0,0) self.size = wx.Size (1366,737) self.displaySize = wx.DisplaySize() # Find the center poistion of the frame self.position.x = self.displaySize[0] // 2 - self.size.x // 2 self.position.y = self.displaySize[1] // 2 - self.size.y // 2
[docs] def load_appSettings(self): sp = wx.StandardPaths.Get() configDir = sp.GetUserConfigDir() if not os.path.exists(os.path.join(configDir, 'rkViewer')): os.mkdir(os.path.join(configDir, 'rkViewer')) fileName = os.path.join(configDir, 'rkViewer', 'appSettings.ini') if not os.path.exists (fileName): # leave with defaults intact. return config = wx.FileConfig(localFilename=fileName) config.SetPath ('PositionAndSize') self.position.x = config.ReadInt ('frame_position_x', self.position.x) self.position.y = config.ReadInt ('frame_position_y', self.position.y) self.size.x = config.ReadInt ('frame_size_w', self.size.x) self.size.y = config.ReadInt ('frame_size_h', self.size.y)
[docs] def save_appSettings(self): sp = wx.StandardPaths.Get() config_dir = sp.GetUserConfigDir() if not os.path.exists(os.path.join(config_dir, 'rkViewer')): os.mkdir(os.path.join(config_dir, 'rkViewer')) fileName = os.path.join(config_dir, 'rkViewer', 'appSettings.ini') if os.path.exists(fileName): os.remove (fileName) config = wx.FileConfig(localFilename=fileName) config.SetPath ('PositionAndSize') config.WriteInt ('frame_position_x', self.position.x) config.WriteInt ('frame_position_y', self.position.y) config.WriteInt ('frame_size_w', self.size.x) config.WriteInt ('frame_size_h', self.size.y)
# TODO merge these schema with those in iodine?
[docs]class ColorField(fields.Field): """Field that represents an RGBA color. To represent the color red, you would write: >>> { "some_color": [255, 0, 0] } You may also specify its opacity. To make the color red half transparent: >>> { "some": [255, 0, 0, 127] } In short, you may specify four integer arguments RGBA in an array, which the alpha value being optional and defaulting to 255, or fully opaque. Each value must be in range [0, 255]. """ list_field = fields.List(fields.Int(), validate=validate.Length(min=3, max=4)) range_validate = validate.Range(min=0, max=255, error='RGBA values must be between 0 and 255.') def __init__(self, **kw): super().__init__(**kw) def _serialize(self, value: Color, attr, obj, **kwargs): ret = [value.r, value.g, value.b] if value.a != 255: ret += [value.a] return ret def _deserialize(self, value, attr, data, **kwargs) -> Color: self.list_field.validate(value) for val in value: ColorField.range_validate(val) return Color(*value)
[docs]class Pixel(fields.Int): """Field that represents some length in pixels. The only current restriction is that this must be a nonnegative integer, or >>> { "some_width": 23 } """ def __init__(self, **kwargs): super().__init__(validate=validate.Range(min=0), **kwargs)
[docs]class Dim(fields.Float): """Field that represents some real dimension (length).""" def __init__(self, **kwargs): # TODO should we allow 0? Also decide for pixel super().__init__(validate=validate.Range(min=0), **kwargs)
[docs]class Dim2(fields.List): def __init__(self, *args, **kw): super().__init__(Dim(), *args, validate=validate.Length(equal=2), **kw) def _serialize(self, value: Vec2, attr, obj, **kwargs): if value is None: return None return (value.x, value.y) def _deserialize(self, value, attr, data, **kwargs): self._validate(value) return Vec2(value)
[docs]class FontField(fields.Field): def __init__(self, *args, **kw): pass def _serialize(self, value: Font, attr, obj, **kwargs): return vars(value) def _validate(self, value): print('hi') # TODO pass def _deserialize(self, value, attr, data, **kwargs): return Font(**value)
[docs]class ThemeSchema(Schema): """Schema for the overall theme, i.e. appearance, of the application. Attributes: overall_bg: Overall background color of the application. canvas_bg: Background color of the canvas. toolbar_bg: Background color of the toolbar. btn_bg : Background color of a button btn_fg: Foreground color of a button, ie the color used by the button text btn_hover_bg: BG color of a button when the mouse is hovering over it. By default this should be equal to the normal BG since MSW already provides a faint blue hovering effect. But in the case that btn_border is False, the native hovering effects vanish, so this is needed as a replacement visual indicator. btn_hover_fg: FG color of a button when the mouse is hovering over it. See btn_hover_bg. btn_border: True for border, false for no border canvas_width: Starting (and minimum) width of the canvas. canvas_height: Starting (and minimum) height of the canvas. vgap: Vertical gap between toolbars and canvas. hgap: Horizontal gap between toolbars and canvas. canvas_outside_bg: Background color of the part outside of the bounds of canvas. mode_panel_width: Width of the mode selection panel. node_fill: Default node fill color. node_border: Default node border color. node_width: Default node width. node_height: Default node height. node_border_width: Default node border width. node_font_size: Default font size of the node. node_font_color: Default font color of the node. select_outline_width: Width of the selection outlines (i.e. outline around each selected item). select_outline_padding: Padding of the selection outlines. handle_color: ColorField of the Reaction Bezier curves. highlighted_handle_color: ColorField of the Reaction Bezier curves when the cursor hovers over it select_box_padding: Padding of the selection rectangle (i.e. the large rectangle that encompasses all the selected items). select_handle_length: Side length of the squares one uses to resize nodes/compartments. zoom_slider_bg: Background color of the zoom slider. drag_fill: The fill color of the drag selection rectangle. drag_border: The border color of the drag selection rectangle. drag_border_width: The border width of the drag selection rectangle. react_node_padding: The outline padding for nodes prepped as reactants or products. react_node_border_width: The border width for nodes prepped as reactants or products. reactant_border: The outline color for nodes prepped as reactants. product_border: The outline color for nodes prepped as products. reaction_fill: The default fill color of reaction curves. reaction_line_thickness: The default thickness of reaction curves. selected_reaction_fill: The fill color of selected reaction curves. comp_fill: The default fill color of compartments. comp_border: The default border color of compartments. comp_border_width: The default border width of compartments. reaction_radius: The radius of the reaction centroid circles. active_tab_fg: The foreground (font) color of the active tab in a FlatNotebook. There is no option to set the BG color since the BG color is always a brighter version of the toolbar BG. text_field_bg: The background color of text fields. text_field_fg: The foreground (font) color of text fields text_field_border: Whether the border of text fields should be drawn. TODO more documentation under attributes and link to this document in Help or settings.json """ # overall background of the application overall_bg = ColorField(missing=Color(240, 240, 240)) canvas_bg = ColorField(missing=Color(255, 255, 255)) # Background color of the toolbars (i.e. panels around the canvas) toolbar_bg = ColorField(missing=Color(230, 230, 230)) # Text color of the toolbars (i.e. panels around the canvas) toolbar_fg = ColorField(missing=Color(0, 0, 0)) canvas_width = Pixel(missing=1000) canvas_height = Pixel(missing=620) btn_bg = ColorField(missing=Color(240,240,240)) btn_fg = ColorField(missing=Color(0, 0, 0)) btn_hover_bg = ColorField(missing=Color(240,240,240)) btn_hover_fg = ColorField(missing=Color(0, 0, 0)) btn_border = fields.Boolean(missing=True) # vertical gap between toolbars and canvas vgap = Pixel(missing=2) # horizontal gap between toolbars and canvas hgap = Pixel(missing=2) canvas_outside_bg = ColorField(missing=Color(160, 160, 160)) mode_panel_width = Pixel(missing=100) toolbar_height = Pixel(missing=75) edit_panel_width = Pixel(missing=260) node_fill = ColorField(missing=Color(255, 204, 153, 200)) node_border = ColorField(missing=Color(255, 108, 9)) node_width = Dim(missing=50) node_height = Dim(missing=30) # node_corner_radius = Dim(missing=0.15) node_border_width = Dim(missing=2) node_font_size = Pixel(missing=10) node_font_color = ColorField(missing=Color(255, 0, 0, 100)) # Width of the outline around each selected node select_outline_width = Pixel(missing=2) # Padding of the outline around each selected node select_outline_padding = Pixel(missing=3) # ColorField of control handles, e.g. resize handles, Bezier handles handle_color = ColorField(missing=Color(0, 140, 255)) # ColorField of control handles when they are highlighted, if applicable highlighted_handle_color = ColorField(missing=Color(128, 198, 255)) # Padding of the select box, relative to the mininum possible bbox select_box_padding = Pixel(missing=5) # Length of the squares one uses to drag resize nodes select_handle_length = Pixel(missing=8) zoom_slider_bg = ColorField(missing=Color(180, 180, 180)) drag_fill = ColorField(missing=Color(0, 140, 255, 20)) drag_border = ColorField(missing=Color(0, 140, 255)) drag_border_width = Pixel(missing=1) react_node_padding = Pixel(missing=5) react_node_border_width = Pixel(missing=3) reactant_border = ColorField(missing=Color(255, 100, 100)) product_border = ColorField(missing=Color(0, 214, 125)) reaction_fill = ColorField(missing=Color(91, 176, 253)) reaction_line_thickness = Dim(missing=2) selected_reaction_fill = ColorField(missing=Color(0, 140, 255)) comp_fill = ColorField(missing=Color(158, 169, 255, 200)) comp_border = ColorField(missing=Color(0, 29, 255)) comp_border_width = Dim(missing=2) comp_corner_radius = Dim(missing=6) reaction_radius = Dim(missing=6) modifier_line_color = ColorField(missing=Color(202, 148, 255)) modifier_line_width = Dim(missing=2) active_tab_fg = ColorField(missing=Color(0, 0, 0)) text_field_bg = ColorField(missing=Color(255, 255, 255)) text_field_fg = ColorField(missing=Color(0, 0, 0)) text_field_border = fields.Boolean(missing=True)
# using ribbon style for notebooks, so no need for this # active_tab_bg = ColorField(missing=Color(100, 100, 100)) # panel_font = FontField(missing=Font(pointSize=))
[docs]class RootSchema(Schema): """The overall root schema. Attributes: theme: The theme settings (i.e. colors and dimensions) of the application. """ theme = fields.Nested(ThemeSchema, missing=ThemeSchema().load({}))
# TODO put this in the schema somewhere DEFAULT_ARROW_TIP = [Vec2(1, 15), Vec2(4, 8), Vec2(1, 1), Vec2(21, 8)] # These settings are not meant to be modifiable by the user BUILTIN_SETTINGS = { "init_zoom": 1, "status_fields": [ ("mode", 100), ("cursor", 100), ("zoom", 100), ("fps", 100), ], # first element: status field identifier; second element: field width "decimal_precision": 2, "min_node_width": 20, "min_node_height": 15, "min_comp_width": 350, "min_comp_height": 200, } INIT_SETTING_TEXT = '''// settings.json auto-generated by RKViewer. // Edit this file to override default application settings. // After you have finished editing, click File > Reload Settings to load the new settings. // Note that some settings may not take effect until the application is restarted. { // Theme and appearance settings "theme": { } } ''' DEFAULT_SETTING_FMT = '''// default-settings.json auto-generated by RKViewer. // Note that you should *not* edit this file, since this is only for reference on the system // default settings, and this will overwritten each time "Default Settings..." is clicked. // If you wish to override these settings, go to // File > Edit Settings... instead. // // The settings generated here may change depending on the loaded plugins, which have their own // default settings. {} ''' root_schema = RootSchema()
[docs]def validate_schema(schema): for field in schema.fields.values(): # make sure fields have a default replacement for missing fields assert field.missing != missing_
_settings = BUILTIN_SETTINGS _theme = None _settings_err = None
[docs]@dataclass class RuntimeVariables: '''Variables pertaining to the application runtime''' enable_plugins: bool = True
_RUNTIME_VARS = RuntimeVariables()
[docs]def runtime_vars() -> RuntimeVariables: return _RUNTIME_VARS
[docs]def reset_runtime_vars(): global _RUNTIME_VARS _RUNTIME_VARS = RuntimeVariables()
[docs]def load_theme_settings(): """Reload all settings from the default settings path. The exceptions are not immediately thrown since the first time the settings are loaded, the app has not been initialized. So we wait until it is, and then display an error dialog if there is a previously recorded error. """ global _theme, _settings, _settings_err cur_settings = dict() if os.path.isfile(GetThemeSettingsPath ()): with open(GetThemeSettingsPath (), 'r') as fp: try: cur_settings = commentjson.load(fp) except JSONLibraryException as e: _settings_err = e if _theme is not None: # theme has already been loaded, so don't modify it. Otherwise load all # defaults. return try: temp = root_schema.load(cur_settings) config = temp except ValidationError as e: _settings_err = e if _theme is not None: return config = root_schema.load({}) _theme = copy.copy(config['theme']) _settings = copy.copy(BUILTIN_SETTINGS)
[docs]def pop_settings_err(): """If there was error when loading the settings JSON, this would be not None. Used by canvas to display a warning message after the window is created. The error is cleared when this is called. """ global _settings_err ret = _settings_err _settings_err = None return ret
[docs]def get_default_raw_settings(): return root_schema.dump(root_schema.load({}))
[docs]def get_setting(setting_attr) -> Any: return _settings[setting_attr]
[docs]def get_theme(theme_attr, convert_color=True) -> Any: '''convert_color: if True, convert Color instances to wx.Colour instances automatically ''' global _theme tmp = _theme[theme_attr] # automatically convert Color to wx.Colour if convert_color and isinstance(tmp, Color): tmp = wx.Colour(tmp.r, tmp.g, tmp.b, tmp.a) return tmp
[docs]def add_plugin_schema(name: str, schema: Schema): # TODO pass