"""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: Displayed (visible) width of the canvas.
real_canvas_width: Actual width of the canvas, some of which may be hidden
canvas_height: Displayed (visible) height of the canvas.
real_canvas_height: Actual height of the canvas, some of which may be hidden
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)
real_canvas_width = Pixel(missing=10000)
canvas_height = Pixel(missing=620)
real_canvas_height = Pixel(missing=6200)
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=400)
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=8)
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