"""
Author: HECE - University of Liege, Pierre Archambeau
Date: 2024
Copyright (c) 2024 University of Liege. All rights reserved.
This script and its content are protected by copyright law. Unauthorized
copying or distribution of this file, via any medium, is strictly prohibited.
"""
import os
import json
from enum import Enum
from pathlib import Path
import logging
import wx
from .PyTranslate import _
[docs]
def _default_plugins_directory() -> Path:
"""Return the built-in plugins directory and make sure it exists."""
from wolfhece._plugin_loader import PLUGINS_DIR
directory = Path(PLUGINS_DIR).resolve()
directory.mkdir(parents=True, exist_ok=True)
return directory
[docs]
class ConfigurationKeys(Enum):
""" Using enumerated keys make sure we
can check value names at code write time
(i.e. we don't use string which are brittle)
"""
[docs]
PLAY_WELCOME_SOUND = "PlayWelcomeSound"
[docs]
TICKS_SIZE = "TicksSize"
[docs]
TICKS_BOUNDS = "TicksBounds"
[docs]
TICKS_XROTATION = "TicksXRotation"
[docs]
TICKS_FONTSIZE = "TicksFontSize"
[docs]
OVERLAY_XY_FONT_NAME = "OverlayXYFontName"
[docs]
OVERLAY_XY_FONT_SIZE = "OverlayXYFontSize"
[docs]
COLOR_BACKGROUND = "ColorBackground"
[docs]
ACTIVE_ARRAY_PALETTE_FOR_IMAGE = "Use active array palette for image"
[docs]
ACTIVE_RES2D_PALETTE_FOR_IMAGE = "Use active result palette for image"
[docs]
ASSEMBLY_IMAGES = "AssemblyImages"
[docs]
DIRECTORY_DEM = "Default DEM directory"
[docs]
DIRECTORY_DTM = "Default DTM directory"
[docs]
DIRECTORY_LAZ = "Default LAZ directory"
[docs]
ACTIVE_VECTOR_COLOR = "Active vector color"
[docs]
ACTIVE_VECTOR_SIZE_SQUARE = "Active vector square size"
[docs]
XLSX_HECE_DATABASE = "Hece Database XLSX file"
[docs]
EPSG_CODE = "Default EPSG code"
[docs]
SNAP_GRID_UNIT = "Snap grid base unit"
[docs]
SNAP_GRID_ROUND_BASE = "Snap grid origin round base"
[docs]
OVERLAY_BG_COLOR = "OverlayBgColor"
[docs]
PLUGINS_DIRECTORY = "PluginsDirectory"
[docs]
PLUGINS_DISABLED = "PluginsDisabled"
[docs]
class WolfConfiguration:
""" Holds the PyWolf configuration """
def __init__(self, path=None):
# We make sure we use a standard location
# to store the configuration
if path is None:
if os.name == "nt":
# On Windows NT, LOCALAPPDATA is expected to be defined.
# (might not be true in the future, who knows)
self._options_file_path = Path(os.getenv("LOCALAPPDATA")) / "wolf.conf"
else:
self._options_file_path = Path("wolf.conf")
else:
self._options_file_path = path
#Set default -- useful if new options are inserted
# --> ensuring that default values are created even if not stored in the options file
self.set_default_config()
if self._options_file_path.exists():
self.load()
else:
# self.set_default_config()
# This save is not 100% necessary but it helps
# to make sure a config file exists.
self.save()
@property
[docs]
def path(self) -> Path:
""" Where the configuration is read/saved."""
return self._options_file_path
[docs]
def set_default_config(self):
default_plugins_dir = str(_default_plugins_directory())
self._config = {
ConfigurationKeys.VERSION.value: 1,
ConfigurationKeys.PLAY_WELCOME_SOUND.value: True,
ConfigurationKeys.TICKS_SIZE.value: 500.,
ConfigurationKeys.ACTIVE_ARRAY_PALETTE_FOR_IMAGE.value: True,
ConfigurationKeys.ACTIVE_RES2D_PALETTE_FOR_IMAGE.value: False,
ConfigurationKeys.TICKS_BOUNDS.value: True,
ConfigurationKeys.COLOR_BACKGROUND.value: [255, 255, 255, 255],
ConfigurationKeys.ASSEMBLY_IMAGES.value: 0,
ConfigurationKeys.TICKS_XROTATION.value: 30.,
ConfigurationKeys.TICKS_FONTSIZE.value: 12,
ConfigurationKeys.OVERLAY_XY_FONT_NAME.value: "Arial",
ConfigurationKeys.OVERLAY_XY_FONT_SIZE.value: 13,
ConfigurationKeys.TOOLBAR_TOOLTIP_FONT_SIZE.value: 13,
ConfigurationKeys.DIRECTORY_DEM.value: "",
ConfigurationKeys.DIRECTORY_DTM.value: "",
ConfigurationKeys.DIRECTORY_LAZ.value: "",
ConfigurationKeys.ACTIVE_VECTOR_COLOR.value: [0, 0, 0, 255],
ConfigurationKeys.ACTIVE_VECTOR_SIZE_SQUARE.value: 5,
ConfigurationKeys.XLSX_HECE_DATABASE.value: "",
ConfigurationKeys.EPSG_CODE.value: "EPSG:31370",
ConfigurationKeys.SNAP_GRID_UNIT.value: 0.01,
ConfigurationKeys.SNAP_GRID_ROUND_BASE.value: 1000.0,
ConfigurationKeys.OVERLAY_BG_COLOR.value: [26, 26, 31, 191],
ConfigurationKeys.PLUGINS_DIRECTORY.value: default_plugins_dir,
ConfigurationKeys.PLUGINS_DISABLED.value: [],
}
self._types = {
ConfigurationKeys.VERSION.value: int,
ConfigurationKeys.PLAY_WELCOME_SOUND.value: bool,
ConfigurationKeys.TICKS_SIZE.value: float,
ConfigurationKeys.ACTIVE_ARRAY_PALETTE_FOR_IMAGE.value: bool,
ConfigurationKeys.ACTIVE_RES2D_PALETTE_FOR_IMAGE.value: bool,
ConfigurationKeys.TICKS_BOUNDS.value: bool,
ConfigurationKeys.COLOR_BACKGROUND.value: list,
ConfigurationKeys.ASSEMBLY_IMAGES.value: int,
ConfigurationKeys.TICKS_XROTATION.value: float,
ConfigurationKeys.TICKS_FONTSIZE.value: int,
ConfigurationKeys.OVERLAY_XY_FONT_NAME.value: str,
ConfigurationKeys.OVERLAY_XY_FONT_SIZE.value: int,
ConfigurationKeys.TOOLBAR_TOOLTIP_FONT_SIZE.value: int,
ConfigurationKeys.DIRECTORY_DEM.value: str,
ConfigurationKeys.DIRECTORY_DTM.value: str,
ConfigurationKeys.DIRECTORY_LAZ.value: str,
ConfigurationKeys.ACTIVE_VECTOR_COLOR.value: list,
ConfigurationKeys.ACTIVE_VECTOR_SIZE_SQUARE.value: int,
ConfigurationKeys.XLSX_HECE_DATABASE.value: str,
ConfigurationKeys.EPSG_CODE.value: str,
ConfigurationKeys.SNAP_GRID_UNIT.value: float,
ConfigurationKeys.SNAP_GRID_ROUND_BASE.value: float,
ConfigurationKeys.OVERLAY_BG_COLOR.value: list,
ConfigurationKeys.PLUGINS_DIRECTORY.value: str,
ConfigurationKeys.PLUGINS_DISABLED.value: list,
}
self._check_config()
[docs]
def _check_config(self):
assert self._config.keys() == self._types.keys()
for idx, (key,val) in enumerate(self._config.items()):
assert isinstance(val, self._types[key])
[docs]
def load(self):
with open(self._options_file_path, "r", encoding="utf-8") as configfile:
filecfg = json.loads(configfile.read())
for curkey in filecfg.keys():
if curkey in self._config.keys():
self._config[curkey] = filecfg[curkey]
plugins_dir = str(self._config[ConfigurationKeys.PLUGINS_DIRECTORY.value]).strip()
if not plugins_dir:
self._config[ConfigurationKeys.PLUGINS_DIRECTORY.value] = str(_default_plugins_directory())
else:
try:
Path(plugins_dir).mkdir(parents=True, exist_ok=True)
except Exception as exc:
logging.warning(_("Could not create plugins directory '{}': {}").format(plugins_dir, exc))
self._check_config()
[docs]
def save(self):
# Make sure to write the config file only if it can
# be dumped by JSON.
txt = json.dumps(self._config, indent=1)
with open(self._options_file_path, "w", encoding="utf-8") as configfile:
configfile.write(txt)
def __getitem__(self, key: ConfigurationKeys):
assert isinstance(key, ConfigurationKeys), "Please only use enum's for configuration keys."
return self._config[key.value]
def __setitem__(self, key: ConfigurationKeys, value):
# A half-measure to ensure the config structure
# can be somehow validated before run time.
assert isinstance(key, ConfigurationKeys), "Please only use enum's for configuration keys."
self._config[key.value] = value
self._check_config()
[docs]
class GlobalOptionsDialog(wx.Dialog):
""" A dialog to set global options for a WolfMapViewer. """
def __init__(self, *args, **kw):
super(GlobalOptionsDialog, self).__init__(*args, **kw)
self.InitUI()
self.SetSize((600, 600))
self.SetTitle(_("Global options"))
[docs]
def push_configuration(self, configuration):
self.cfg_welcome_voice.SetValue(configuration[ConfigurationKeys.PLAY_WELCOME_SOUND])
self.cfg_ticks_size.SetValue(str(configuration[ConfigurationKeys.TICKS_SIZE]))
self.cfg_ticks_bounds.SetValue(configuration[ConfigurationKeys.TICKS_BOUNDS])
self.cfg_bkg_color.SetColour(configuration[ConfigurationKeys.COLOR_BACKGROUND])
self.cfg_active_array_pal.SetValue(configuration[ConfigurationKeys.ACTIVE_ARRAY_PALETTE_FOR_IMAGE])
self.cfg_active_res_pal.SetValue(configuration[ConfigurationKeys.ACTIVE_RES2D_PALETTE_FOR_IMAGE])
self.cfg_assembly_images.SetSelection(configuration[ConfigurationKeys.ASSEMBLY_IMAGES])
self.cfg_ticks_xrotation.SetValue(str(configuration[ConfigurationKeys.TICKS_XROTATION]))
self.cfg_ticks_fontsize.SetValue(str(configuration[ConfigurationKeys.TICKS_FONTSIZE]))
self.cfg_overlay_xy_font_name.SetValue(str(configuration[ConfigurationKeys.OVERLAY_XY_FONT_NAME]))
self.cfg_overlay_xy_font_size.SetValue(str(configuration[ConfigurationKeys.OVERLAY_XY_FONT_SIZE]))
self.cfg_toolbar_tooltip_font_size.SetValue(str(configuration[ConfigurationKeys.TOOLBAR_TOOLTIP_FONT_SIZE]))
self.cfg_directory_dem.SetValue(str(configuration[ConfigurationKeys.DIRECTORY_DEM]))
self.cfg_directory_dtm.SetValue(str(configuration[ConfigurationKeys.DIRECTORY_DTM]))
self.cfg_directory_laz.SetValue(str(configuration[ConfigurationKeys.DIRECTORY_LAZ]))
self.cfg_vector_color.SetColour(configuration[ConfigurationKeys.ACTIVE_VECTOR_COLOR])
self.cfg_square_size.SetValue(str(configuration[ConfigurationKeys.ACTIVE_VECTOR_SIZE_SQUARE]))
self.cfg_xlsx_hece_database.SetValue(str(configuration[ConfigurationKeys.XLSX_HECE_DATABASE]))
self.cfg_epsg_code.SetValue(str(configuration[ConfigurationKeys.EPSG_CODE]))
self.cfg_snap_grid_unit.SetValue(str(configuration[ConfigurationKeys.SNAP_GRID_UNIT]))
self.cfg_snap_grid_round_base.SetValue(str(configuration[ConfigurationKeys.SNAP_GRID_ROUND_BASE]))
self.cfg_overlay_bg_color.SetColour(configuration[ConfigurationKeys.OVERLAY_BG_COLOR][:3] + [255])
self.cfg_overlay_bg_alpha.SetValue(str(configuration[ConfigurationKeys.OVERLAY_BG_COLOR][3]))
self.cfg_plugins_directory.SetValue(str(configuration[ConfigurationKeys.PLUGINS_DIRECTORY]))
self._populate_plugins_list(configuration[ConfigurationKeys.PLUGINS_DIRECTORY],
configuration[ConfigurationKeys.PLUGINS_DISABLED])
[docs]
def pull_configuration(self, configuration):
configuration[ConfigurationKeys.PLAY_WELCOME_SOUND] = self.cfg_welcome_voice.IsChecked()
configuration[ConfigurationKeys.TICKS_SIZE] = float(self.cfg_ticks_size.Value)
configuration[ConfigurationKeys.TICKS_BOUNDS] = self.cfg_ticks_bounds.IsChecked()
configuration[ConfigurationKeys.COLOR_BACKGROUND] = list(self.cfg_bkg_color.GetColour())
configuration[ConfigurationKeys.ACTIVE_ARRAY_PALETTE_FOR_IMAGE] = self.cfg_active_array_pal.IsChecked()
configuration[ConfigurationKeys.ACTIVE_RES2D_PALETTE_FOR_IMAGE] = self.cfg_active_res_pal.IsChecked()
configuration[ConfigurationKeys.ASSEMBLY_IMAGES] = self.cfg_assembly_images.GetSelection()
configuration[ConfigurationKeys.TICKS_XROTATION] = float(self.cfg_ticks_xrotation.Value)
configuration[ConfigurationKeys.TICKS_FONTSIZE] = int(self.cfg_ticks_fontsize.Value)
configuration[ConfigurationKeys.OVERLAY_XY_FONT_NAME] = str(self.cfg_overlay_xy_font_name.Value).strip()
configuration[ConfigurationKeys.OVERLAY_XY_FONT_SIZE] = max(6, int(self.cfg_overlay_xy_font_size.Value))
configuration[ConfigurationKeys.TOOLBAR_TOOLTIP_FONT_SIZE] = max(6, int(self.cfg_toolbar_tooltip_font_size.Value))
configuration[ConfigurationKeys.DIRECTORY_DEM] = str(self.cfg_directory_dem.Value)
configuration[ConfigurationKeys.DIRECTORY_DTM] = str(self.cfg_directory_dtm.Value)
configuration[ConfigurationKeys.DIRECTORY_LAZ] = str(self.cfg_directory_laz.Value)
configuration[ConfigurationKeys.ACTIVE_VECTOR_COLOR] = list(self.cfg_vector_color.GetColour())
configuration[ConfigurationKeys.ACTIVE_VECTOR_SIZE_SQUARE] = int(self.cfg_square_size.Value)
configuration[ConfigurationKeys.XLSX_HECE_DATABASE] = str(self.cfg_xlsx_hece_database.Value)
configuration[ConfigurationKeys.SNAP_GRID_UNIT] = max(1e-12, float(self.cfg_snap_grid_unit.Value))
configuration[ConfigurationKeys.SNAP_GRID_ROUND_BASE] = max(1e-12, float(self.cfg_snap_grid_round_base.Value))
_oc = list(self.cfg_overlay_bg_color.GetColour())[:3]
_oa = max(0, min(255, int(self.cfg_overlay_bg_alpha.Value)))
configuration[ConfigurationKeys.OVERLAY_BG_COLOR] = _oc + [_oa]
configuration[ConfigurationKeys.PLUGINS_DIRECTORY] = \
str(self.cfg_plugins_directory.Value).strip() or str(_default_plugins_directory())
configuration[ConfigurationKeys.PLUGINS_DISABLED] = \
self._collect_disabled_plugins()
epsg = str(self.cfg_epsg_code.Value).strip()
if not epsg:
epsg = "EPSG:31370"
if not epsg.upper().startswith("EPSG:"):
epsg = epsg.strip().lower()
if 'belgium 2008' in epsg or 'belgique 2008' in epsg:
epsg = "EPSG:3812"
elif 'belgium 1972' in epsg or 'belgium' in epsg or 'belgique 1972' in epsg or 'belgique' in epsg:
epsg = "EPSG:31370"
elif 'rgf93' in epsg or 'france' in epsg:
epsg = "EPSG:2154"
elif 'wgs 84' in epsg:
epsg = "EPSG:4326"
elif 'germany' in epsg or 'allemagne' in epsg:
epsg = "EPSG:25832"
else:
try:
code = int(epsg)
epsg = f"EPSG:{code}"
except:
logging.warning(_('Could not interpret EPSG code: {} -- keeping original value').format(epsg))
return
configuration[ConfigurationKeys.EPSG_CODE] = epsg
[docs]
def InitUI(self):
vbox = wx.BoxSizer(wx.VERTICAL)
self.notebook = wx.Notebook(self)
self._init_tab_general(self.notebook)
self._init_tab_display(self.notebook)
self._init_tab_directories(self.notebook)
self._init_tab_vectors(self.notebook)
self._init_tab_plugins(self.notebook)
vbox.Add(self.notebook, proportion=1, flag=wx.ALL | wx.EXPAND, border=5)
# Buttons
hbox2 = wx.BoxSizer(wx.HORIZONTAL)
okButton = wx.Button(self, wx.ID_OK, label=_('Ok'))
okButton.SetDefault()
closeButton = wx.Button(self, label=_('Close'))
hbox2.Add(okButton)
hbox2.Add(closeButton, flag=wx.LEFT, border=5)
vbox.Add(hbox2, flag=wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, border=10)
self.SetSizer(vbox)
self.Layout()
okButton.Bind(wx.EVT_BUTTON, self.OnOk)
closeButton.Bind(wx.EVT_BUTTON, self.OnClose)
[docs]
def _init_tab_general(self, notebook):
pnl = wx.Panel(notebook)
sizer = wx.BoxSizer(wx.VERTICAL)
# Welcome voice
self.cfg_welcome_voice = wx.CheckBox(pnl, label=_('Welcome voice'))
self.cfg_welcome_voice.SetToolTip(_('Play a welcome message when opening the application'))
sizer.Add(self.cfg_welcome_voice, 0, wx.ALL, 5)
# Background color
hsizer = wx.BoxSizer(wx.HORIZONTAL)
self.label_background_color = wx.StaticText(pnl, label=_('Background color'))
self.cfg_bkg_color = wx.ColourPickerCtrl(pnl, colour=(255, 255, 255, 255))
self.cfg_bkg_color.SetToolTip(_('Background color for the viewer'))
hsizer.Add(self.label_background_color, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
hsizer.Add(self.cfg_bkg_color, 1, wx.EXPAND)
sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 5)
# EPSG code
epsg_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.label_epsg_code = wx.StaticText(pnl, label=_('Default EPSG code'))
self.cfg_epsg_code = wx.TextCtrl(pnl, value='EPSG:31370', style=wx.TE_CENTRE)
self.cfg_epsg_code.SetToolTip(_('Default EPSG code for new arrays added to the viewer\nExamples: EPSG:31370, EPSG:2154, EPSG:4326\nor simply the code number: 31370, 2154, 4326\nor a descriptive name: Belgium 1972, Belgium 2008, France RGF93, WGS 84'))
epsg_sizer.Add(self.label_epsg_code, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
epsg_sizer.Add(self.cfg_epsg_code, 1, wx.EXPAND)
sizer.Add(epsg_sizer, 0, wx.ALL | wx.EXPAND, 5)
pnl.SetSizer(sizer)
notebook.AddPage(pnl, _("General"))
[docs]
def _init_tab_display(self, notebook):
pnl = wx.Panel(notebook)
sizer = wx.BoxSizer(wx.VERTICAL)
# -- Ticks section --
sb_ticks = wx.StaticBox(pnl, label=_('Ticks'))
sbs_ticks = wx.StaticBoxSizer(sb_ticks, orient=wx.VERTICAL)
hboxticks = wx.BoxSizer(wx.HORIZONTAL)
self.label_ticks_size = wx.StaticText(pnl, label=_('Default ticks size [m]'))
self.cfg_ticks_size = wx.TextCtrl(pnl, value='500.', style=wx.TE_CENTRE)
hboxticks.Add(self.label_ticks_size, 1, wx.ALIGN_CENTER_VERTICAL)
hboxticks.Add(self.cfg_ticks_size, 1, wx.EXPAND)
sbs_ticks.Add(hboxticks, 0, wx.ALL | wx.EXPAND, 3)
hboxrot = wx.BoxSizer(wx.HORIZONTAL)
self.label_ticks_xrotation = wx.StaticText(pnl, label=_('X rotation of ticks [°]'))
self.cfg_ticks_xrotation = wx.TextCtrl(pnl, value='30.', style=wx.TE_CENTRE)
hboxrot.Add(self.label_ticks_xrotation, 1, wx.ALIGN_CENTER_VERTICAL)
hboxrot.Add(self.cfg_ticks_xrotation, 1, wx.EXPAND)
sbs_ticks.Add(hboxrot, 0, wx.ALL | wx.EXPAND, 3)
hboxfs = wx.BoxSizer(wx.HORIZONTAL)
self.label_ticks_fontsize = wx.StaticText(pnl, label=_('Font size of ticks'))
self.cfg_ticks_fontsize = wx.TextCtrl(pnl, value='12', style=wx.TE_CENTRE)
hboxfs.Add(self.label_ticks_fontsize, 1, wx.ALIGN_CENTER_VERTICAL)
hboxfs.Add(self.cfg_ticks_fontsize, 1, wx.EXPAND)
sbs_ticks.Add(hboxfs, 0, wx.ALL | wx.EXPAND, 3)
self.cfg_ticks_bounds = wx.CheckBox(pnl, label=_('Add bounds to ticks'))
self.cfg_ticks_bounds.SetToolTip(_('If not checked, the extreme values of the ticks will not be displayed'))
sbs_ticks.Add(self.cfg_ticks_bounds, 0, wx.ALL, 3)
sizer.Add(sbs_ticks, 0, wx.ALL | wx.EXPAND, 5)
# -- Overlay XY section --
sb_overlay = wx.StaticBox(pnl, label=_('Overlay XY'))
sbs_overlay = wx.StaticBoxSizer(sb_overlay, orient=wx.VERTICAL)
hboxfn = wx.BoxSizer(wx.HORIZONTAL)
self.label_overlay_xy_font_name = wx.StaticText(pnl, label=_('Overlay XY font name'))
self.cfg_overlay_xy_font_name = wx.TextCtrl(pnl, value='Arial', style=wx.TE_CENTRE)
self.cfg_overlay_xy_font_name.SetToolTip(_('Font file name used by OpenGL XY overlay, e.g. arial.ttf'))
hboxfn.Add(self.label_overlay_xy_font_name, 1, wx.ALIGN_CENTER_VERTICAL)
hboxfn.Add(self.cfg_overlay_xy_font_name, 1, wx.EXPAND)
sbs_overlay.Add(hboxfn, 0, wx.ALL | wx.EXPAND, 3)
hboxfsize = wx.BoxSizer(wx.HORIZONTAL)
self.label_overlay_xy_font_size = wx.StaticText(pnl, label=_('Overlay XY font size [px]'))
self.cfg_overlay_xy_font_size = wx.TextCtrl(pnl, value='13', style=wx.TE_CENTRE)
self.cfg_overlay_xy_font_size.SetToolTip(_('Font pixel size used by OpenGL XY overlay'))
hboxfsize.Add(self.label_overlay_xy_font_size, 1, wx.ALIGN_CENTER_VERTICAL)
hboxfsize.Add(self.cfg_overlay_xy_font_size, 1, wx.EXPAND)
sbs_overlay.Add(hboxfsize, 0, wx.ALL | wx.EXPAND, 3)
hbox_ttfs = wx.BoxSizer(wx.HORIZONTAL)
self.label_toolbar_tooltip_font_size = wx.StaticText(pnl, label=_('Toolbar tooltip font size [px]'))
self.cfg_toolbar_tooltip_font_size = wx.TextCtrl(pnl, value='13', style=wx.TE_CENTRE)
self.cfg_toolbar_tooltip_font_size.SetToolTip(_('Font pixel size used by toolbar tooltip text'))
hbox_ttfs.Add(self.label_toolbar_tooltip_font_size, 1, wx.ALIGN_CENTER_VERTICAL)
hbox_ttfs.Add(self.cfg_toolbar_tooltip_font_size, 1, wx.EXPAND)
sbs_overlay.Add(hbox_ttfs, 0, wx.ALL | wx.EXPAND, 3)
hbox_obg = wx.BoxSizer(wx.HORIZONTAL)
self.label_overlay_bg_color = wx.StaticText(pnl, label=_('Overlay background color'))
self.cfg_overlay_bg_color = wx.ColourPickerCtrl(pnl, colour=(26, 26, 31, 255))
self.cfg_overlay_bg_color.SetToolTip(_('Background colour shared by all HUD overlays (toolbar, palette, hillshade)'))
hbox_obg.Add(self.label_overlay_bg_color, 1, wx.ALIGN_CENTER_VERTICAL)
hbox_obg.Add(self.cfg_overlay_bg_color, 0)
sbs_overlay.Add(hbox_obg, 0, wx.ALL | wx.EXPAND, 3)
hbox_oba = wx.BoxSizer(wx.HORIZONTAL)
self.label_overlay_bg_alpha = wx.StaticText(pnl, label=_('Overlay background opacity [0-255]'))
self.cfg_overlay_bg_alpha = wx.TextCtrl(pnl, value='191', style=wx.TE_CENTRE)
self.cfg_overlay_bg_alpha.SetToolTip(_('Alpha channel (0 = transparent, 255 = opaque) for HUD overlay backgrounds'))
hbox_oba.Add(self.label_overlay_bg_alpha, 1, wx.ALIGN_CENTER_VERTICAL)
hbox_oba.Add(self.cfg_overlay_bg_alpha, 1, wx.EXPAND)
sbs_overlay.Add(hbox_oba, 0, wx.ALL | wx.EXPAND, 3)
sizer.Add(sbs_overlay, 0, wx.ALL | wx.EXPAND, 5)
# -- Snap grid section --
sb_snap = wx.StaticBox(pnl, label=_('Snap grid'))
sbs_snap = wx.StaticBoxSizer(sb_snap, orient=wx.VERTICAL)
hbox_snap_unit = wx.BoxSizer(wx.HORIZONTAL)
self.label_snap_grid_unit = wx.StaticText(pnl, label=_('Base unit [m]'))
self.cfg_snap_grid_unit = wx.TextCtrl(pnl, value='0.01', style=wx.TE_CENTRE)
self.cfg_snap_grid_unit.SetToolTip(_('Base spacing used by snap (adaptive levels are powers of two of this value)'))
hbox_snap_unit.Add(self.label_snap_grid_unit, 1, wx.ALIGN_CENTER_VERTICAL)
hbox_snap_unit.Add(self.cfg_snap_grid_unit, 1, wx.EXPAND)
sbs_snap.Add(hbox_snap_unit, 0, wx.ALL | wx.EXPAND, 3)
hbox_snap_round = wx.BoxSizer(wx.HORIZONTAL)
self.label_snap_grid_round_base = wx.StaticText(pnl, label=_('Origin rounding base [m]'))
self.cfg_snap_grid_round_base = wx.TextCtrl(pnl, value='1000.0', style=wx.TE_CENTRE)
self.cfg_snap_grid_round_base.SetToolTip(_('Snap origin is aligned on a multiple of this value to keep a stable global grid'))
hbox_snap_round.Add(self.label_snap_grid_round_base, 1, wx.ALIGN_CENTER_VERTICAL)
hbox_snap_round.Add(self.cfg_snap_grid_round_base, 1, wx.EXPAND)
sbs_snap.Add(hbox_snap_round, 0, wx.ALL | wx.EXPAND, 3)
sizer.Add(sbs_snap, 0, wx.ALL | wx.EXPAND, 5)
# -- Palettes & Images section --
sb_pal = wx.StaticBox(pnl, label=_('Palettes & Images'))
sbs_pal = wx.StaticBoxSizer(sb_pal, orient=wx.VERTICAL)
self.cfg_active_array_pal = wx.CheckBox(pnl, label=_('Use active array palette for image'))
self.cfg_active_array_pal.SetToolTip(_('If checked, the active array palette will be used for the image'))
sbs_pal.Add(self.cfg_active_array_pal, 0, wx.ALL, 3)
self.cfg_active_res_pal = wx.CheckBox(pnl, label=_('Use active result palette for image'))
self.cfg_active_res_pal.SetToolTip(_('If checked, the active result palette will be used for the image (but priority to active array palette if checked)'))
sbs_pal.Add(self.cfg_active_res_pal, 0, wx.ALL, 3)
locsizer = wx.BoxSizer(wx.HORIZONTAL)
self.label_assembly_images = wx.StaticText(pnl, label=_('Assembly mode for images (if linked viewers)'))
self.cfg_assembly_images = wx.ListBox(pnl, choices=['horizontal', 'vertical', 'square'], style=wx.LB_SINGLE)
self.cfg_assembly_images.SetToolTip(_('Choose the assembly mode for images -- horizontal, vertical or square'))
locsizer.Add(self.label_assembly_images, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
locsizer.Add(self.cfg_assembly_images, 1, wx.EXPAND)
sbs_pal.Add(locsizer, 0, wx.ALL | wx.EXPAND, 3)
sizer.Add(sbs_pal, 0, wx.ALL | wx.EXPAND, 5)
pnl.SetSizer(sizer)
notebook.AddPage(pnl, _("Display / Export"))
[docs]
def _init_tab_directories(self, notebook):
pnl = wx.Panel(notebook)
sizer = wx.BoxSizer(wx.VERTICAL)
# DEM directory
dir_dem = wx.BoxSizer(wx.HORIZONTAL)
self.label_directory_dem = wx.StaticText(pnl, label=_('Default DEM directory'))
self.cfg_directory_dem = wx.TextCtrl(pnl, value='', style=wx.TE_CENTRE)
self.btn_choose_dem = wx.Button(pnl, label=_('Choose'))
self.btn_choose_dem.Bind(wx.EVT_BUTTON, self.OnChooseDem)
dir_dem.Add(self.label_directory_dem, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
dir_dem.Add(self.cfg_directory_dem, 2, wx.EXPAND | wx.RIGHT, 5)
dir_dem.Add(self.btn_choose_dem, 0)
sizer.Add(dir_dem, 0, wx.ALL | wx.EXPAND, 5)
# DTM directory
dir_dtm = wx.BoxSizer(wx.HORIZONTAL)
self.label_directory_dtm = wx.StaticText(pnl, label=_('Default DTM directory'))
self.cfg_directory_dtm = wx.TextCtrl(pnl, value='', style=wx.TE_CENTRE)
self.btn_choose_dtm = wx.Button(pnl, label=_('Choose'))
self.btn_choose_dtm.Bind(wx.EVT_BUTTON, self.OnChooseDtm)
dir_dtm.Add(self.label_directory_dtm, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
dir_dtm.Add(self.cfg_directory_dtm, 2, wx.EXPAND | wx.RIGHT, 5)
dir_dtm.Add(self.btn_choose_dtm, 0)
sizer.Add(dir_dtm, 0, wx.ALL | wx.EXPAND, 5)
# LAZ directory
dir_laz = wx.BoxSizer(wx.HORIZONTAL)
self.label_directory_laz = wx.StaticText(pnl, label=_('Default LAZ directory'))
self.cfg_directory_laz = wx.TextCtrl(pnl, value='', style=wx.TE_CENTRE)
self.btn_choose_laz = wx.Button(pnl, label=_('Choose'))
self.btn_choose_laz.Bind(wx.EVT_BUTTON, self.OnChooseLaz)
dir_laz.Add(self.label_directory_laz, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
dir_laz.Add(self.cfg_directory_laz, 2, wx.EXPAND | wx.RIGHT, 5)
dir_laz.Add(self.btn_choose_laz, 0)
sizer.Add(dir_laz, 0, wx.ALL | wx.EXPAND, 5)
# XLSX HECE database
dir_xlsx = wx.BoxSizer(wx.HORIZONTAL)
self.label_xlsx_hece_database = wx.StaticText(pnl, label=_('HECE Database file'))
self.cfg_xlsx_hece_database = wx.TextCtrl(pnl, value='', style=wx.TE_CENTRE)
self.btn_choose_xlsx = wx.Button(pnl, label=_('Choose'))
self.btn_choose_xlsx.Bind(wx.EVT_BUTTON, self.OnChooseXLSX)
dir_xlsx.Add(self.label_xlsx_hece_database, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
dir_xlsx.Add(self.cfg_xlsx_hece_database, 2, wx.EXPAND | wx.RIGHT, 5)
dir_xlsx.Add(self.btn_choose_xlsx, 0)
sizer.Add(dir_xlsx, 0, wx.ALL | wx.EXPAND, 5)
pnl.SetSizer(sizer)
notebook.AddPage(pnl, _("Directories"))
[docs]
def _init_tab_vectors(self, notebook):
pnl = wx.Panel(notebook)
sizer = wx.BoxSizer(wx.VERTICAL)
# Vector color
color_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.label_vector_color = wx.StaticText(pnl, label=_('Default vector color'))
self.cfg_vector_color = wx.ColourPickerCtrl(pnl, colour=(0, 0, 0, 255))
self.cfg_vector_color.SetToolTip(_('Color for active vector in the viewer'))
color_sizer.Add(self.label_vector_color, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
color_sizer.Add(self.cfg_vector_color, 1, wx.EXPAND)
sizer.Add(color_sizer, 0, wx.ALL | wx.EXPAND, 5)
# Square size
square_sizer = wx.BoxSizer(wx.HORIZONTAL)
label_sq = wx.StaticText(pnl, label=_('Square size [px]'))
self.cfg_square_size = wx.TextCtrl(pnl, value='5', style=wx.TE_CENTRE)
self.cfg_square_size.SetToolTip(_('Size of the square for active vector in the viewer'))
square_sizer.Add(label_sq, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
square_sizer.Add(self.cfg_square_size, 1, wx.EXPAND)
sizer.Add(square_sizer, 0, wx.ALL | wx.EXPAND, 5)
pnl.SetSizer(sizer)
notebook.AddPage(pnl, _("Vectors"))
# ------------------------------------------------------------------
# Plugins tab
# ------------------------------------------------------------------
[docs]
def _init_tab_plugins(self, notebook):
"""Create the *Plugins* tab in the options notebook."""
pnl = wx.Panel(notebook)
sizer = wx.BoxSizer(wx.VERTICAL)
# ---- Plugins directory row ----
dir_sizer = wx.BoxSizer(wx.HORIZONTAL)
label_dir = wx.StaticText(pnl, label=_('Plugins directory'))
self.cfg_plugins_directory = wx.TextCtrl(pnl, value='')
self.cfg_plugins_directory.SetToolTip(
_('Directory to scan for companion plugins '
'(leave empty to use the built-in wolfhece/data/plugins folder)'))
btn_choose = wx.Button(pnl, label=_('Choose\u2026'))
btn_choose.SetToolTip(_('Browse for a plugins directory'))
btn_choose.Bind(wx.EVT_BUTTON, self._OnChoosePluginsDir)
dir_sizer.Add(label_dir, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
dir_sizer.Add(self.cfg_plugins_directory, 1, wx.EXPAND | wx.RIGHT, 5)
dir_sizer.Add(btn_choose, 0, wx.ALIGN_CENTER_VERTICAL)
sizer.Add(dir_sizer, 0, wx.ALL | wx.EXPAND, 5)
# ---- Plugins check-list ----
lbl_list = wx.StaticText(
pnl,
label=_(
'Available plugins (check to enable) — '
'\u2605 built-in \u2713 approved \u26a0 changed ? unknown '
'| \u2630 menu \u26a1 direct'
),
)
sizer.Add(lbl_list, 0, wx.LEFT | wx.RIGHT | wx.TOP, 5)
self.cfg_plugins_list = wx.CheckListBox(pnl, style=wx.LB_SINGLE)
self.cfg_plugins_list.SetToolTip(
_('Unchecked plugins are disabled — they will not be loaded at startup.\n'
'Enabling an external plugin will ask for your approval.'))
self.cfg_plugins_list.Bind(wx.EVT_CHECKLISTBOX, self._OnPluginChecked)
sizer.Add(self.cfg_plugins_list, 1, wx.ALL | wx.EXPAND, 5)
# ---- "New plugin" button ----
btn_new = wx.Button(pnl, label=_('New plugin from template\u2026'))
btn_new.SetToolTip(
_('Create a new plugin directory pre-filled with the template files'))
btn_new.Bind(wx.EVT_BUTTON, self._OnNewPlugin)
sizer.Add(btn_new, 0, wx.ALL, 5)
pnl.SetSizer(sizer)
notebook.AddPage(pnl, _('Plugins'))
[docs]
def _populate_plugins_list(self, plugins_dir: str, disabled: list) -> None:
"""Refresh the plugins check-list (built-in first, then user plugins)."""
from wolfhece._plugin_loader import discover_all_plugins
from wolfhece._plugin_trust import get_default_store
from pathlib import Path as _Path
directory = _Path(plugins_dir) if plugins_dir else None
self._plugins_cache = discover_all_plugins(directory)
trust_store = get_default_store()
self.cfg_plugins_list.Clear()
for info in self._plugins_cache:
label = _plugin_badges(info, trust_store) + info.display_name
if not info.loaded:
label += _(' \u26a0 load error')
idx = self.cfg_plugins_list.Append(label)
enabled = info.name not in disabled
self.cfg_plugins_list.Check(idx, enabled)
[docs]
def _collect_disabled_plugins(self) -> list:
"""Return the list of plugin names (strings) that are *unchecked*."""
plugins = getattr(self, '_plugins_cache', [])
disabled = []
for i, info in enumerate(plugins):
if i < self.cfg_plugins_list.GetCount() and not self.cfg_plugins_list.IsChecked(i):
disabled.append(info.name)
return disabled
[docs]
def _OnPluginChecked(self, event) -> None:
"""Ask for approval when the user enables an unknown/changed external plugin."""
idx = event.GetInt()
if not self.cfg_plugins_list.IsChecked(idx):
return # unchecking — no approval needed
plugins = getattr(self, '_plugins_cache', [])
if idx >= len(plugins):
return
info = plugins[idx]
from wolfhece._plugin_trust import get_default_store, TrustStatus
trust_store = get_default_store()
status = trust_store.get_status(info)
if status in (TrustStatus.BUILTIN, TrustStatus.APPROVED):
return # already trusted
if status == TrustStatus.CHANGED:
title = _('Plugin files changed — re-approval needed')
intro = _(
'The files of plugin "{name}" have changed since you last '
'approved it.\nPath: {path}\n\n'
'Do you trust this plugin in its current state?'
).format(name=info.display_name, path=info.path)
else: # UNKNOWN
title = _('Approve external plugin?')
intro = _(
'Plugin "{name}" is external and has not been reviewed.\n'
'Path: {path}\n\nAuthor: {author}\n{desc}\n\n'
'Loading a plugin executes arbitrary Python code.\n'
'Only approve plugins from sources you trust.'
).format(
name=info.display_name,
path=info.path,
author=info.manifest.author,
desc=info.manifest.description,
)
answer = wx.MessageBox(
intro, title,
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_WARNING, self,
)
if answer == wx.YES:
trust_store.approve(info)
# Refresh label to show ✓ badge
disabled = self._collect_disabled_plugins()
plugins_dir = str(self.cfg_plugins_directory.Value).strip()
self._populate_plugins_list(plugins_dir, disabled)
else:
self.cfg_plugins_list.Check(idx, False)
[docs]
def _OnNewPlugin(self, event):
"""Open the 'New plugin from template' dialog and scaffold the files."""
from wolfhece._plugin_loader import PLUGINS_DIR
from pathlib import Path as _Path
import datetime
# Determine target directory (configured or built-in)
configured = str(self.cfg_plugins_directory.Value).strip()
target_dir = _Path(configured) if configured else PLUGINS_DIR
dlg = _NewPluginDialog(self, target_dir)
try:
if dlg.ShowModal() != wx.ID_OK:
return
slug = dlg.slug
display = dlg.display_name
author = dlg.author
description = dlg.description
finally:
dlg.Destroy()
try:
created_path = _create_plugin_from_template(
slug, display, author, description, target_dir
)
except Exception as exc:
wx.MessageBox(
_('Could not create plugin:\n\n{err}').format(err=exc),
_('New plugin'),
wx.OK | wx.ICON_ERROR,
self,
)
return
wx.MessageBox(
_('Plugin "{name}" created in:\n{path}\n\n'
'Open companion.py to implement your logic.').format(
name=display, path=created_path),
_('New plugin'),
wx.OK | wx.ICON_INFORMATION,
self,
)
# Refresh the plugin list so the new entry appears
self._populate_plugins_list(configured, [])
[docs]
def _OnChoosePluginsDir(self, event):
"""Browse for a custom plugins directory."""
dlg = wx.DirDialog(self, _('Choose a plugins directory'), style=wx.DD_DEFAULT_STYLE)
if dlg.ShowModal() == wx.ID_OK:
chosen = dlg.GetPath()
self.cfg_plugins_directory.SetValue(chosen)
cfg_disabled = [] # reload list with no pre-existing disabled set
self._populate_plugins_list(chosen, cfg_disabled)
dlg.Destroy()
[docs]
def OnChooseDem(self, e):
""" Choose a directory for DEM files """
dlg = wx.DirDialog(self, _("Choose a directory"), style=wx.DD_DEFAULT_STYLE)
if dlg.ShowModal() == wx.ID_OK:
self.cfg_directory_dem.SetValue(str(dlg.GetPath()))
dlg.Destroy()
[docs]
def OnChooseDtm(self, e):
""" Choose a directory for DTM files """
dlg = wx.DirDialog(self, _("Choose a directory"), style=wx.DD_DEFAULT_STYLE)
if dlg.ShowModal() == wx.ID_OK:
self.cfg_directory_dtm.SetValue(str(dlg.GetPath()))
dlg.Destroy()
[docs]
def OnChooseLaz(self, e):
""" Choose a directory for LAZ files """
dlg = wx.DirDialog(self, _("Choose a directory"), style=wx.DD_DEFAULT_STYLE)
if dlg.ShowModal() == wx.ID_OK:
self.cfg_directory_laz.SetValue(str(dlg.GetPath()))
dlg.Destroy()
[docs]
def OnChooseXLSX(self, e):
""" Choose a XLSX file for HECE database """
dlg = wx.FileDialog(self, _("Choose a HECE database file"), style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
dlg.SetWildcard("Excel files (*.xlsx)|*.xlsx")
if dlg.ShowModal() == wx.ID_OK:
self.cfg_xlsx_hece_database.SetValue(str(dlg.GetPath()))
dlg.Destroy()
[docs]
def OnOk(self, e):
if self.IsModal():
self.EndModal(wx.ID_OK)
else:
self.Close()
[docs]
def OnClose(self, e):
self.Destroy()
# ---------------------------------------------------------------------------
# New-plugin helper dialog
# ---------------------------------------------------------------------------
[docs]
def _plugin_badges(info, trust_store) -> str:
"""Return a combined trust + menu badge prefix for *info*.
Format: ``'<trust> <menu> '`` (e.g. ``'★ ☰ '``, ``'✓ ⚡ '``, ``'? ☰ '``)
"""
from wolfhece._plugin_trust import TrustStatus
status = trust_store.get_status(info)
trust_badge = {
TrustStatus.BUILTIN: '\u2605 ', # ★
TrustStatus.APPROVED: '\u2713 ', # ✓
TrustStatus.CHANGED: '\u26a0 ', # ⚠ (files changed)
TrustStatus.UNKNOWN: '? ',
}.get(status, '')
return trust_badge + _plugin_menu_badge(info)
[docs]
def _slug_to_class_name(slug: str) -> str:
"""Convert a slug like ``my_feature`` to a class name ``MyFeatureCompanion``."""
import re
parts = re.split(r'[-_]+', slug)
return ''.join(p.capitalize() for p in parts if p) + 'Companion'
[docs]
def _create_plugin_from_template(
slug: str,
display_name: str,
author: str,
description: str,
target_dir,
) -> 'Path':
"""Copy the template directory to *target_dir*/<slug> with substituted values.
:raises FileExistsError: if the target directory already exists.
:raises FileNotFoundError: if the template directory is missing.
:return: Path to the created plugin directory.
"""
import datetime
import re
import shutil
from pathlib import Path as _Path
from wolfhece._plugin_loader import BUILTIN_PLUGINS_DIR
today = datetime.date.today().isoformat()
class_name = _slug_to_class_name(slug)
template = BUILTIN_PLUGINS_DIR / '_template'
plugin_dir = _Path(target_dir) / slug
if not template.is_dir():
raise FileNotFoundError(
f"Template directory not found: {template}"
)
if plugin_dir.exists():
raise FileExistsError(
f"A plugin named '{slug}' already exists at: {plugin_dir}"
)
# -- plugin.toml ---------------------------------------------------------
toml_src = (template / 'plugin.toml').read_text(encoding='utf-8')
toml_dst = toml_src
toml_dst = toml_dst.replace('name = "my_plugin"',
f'name = "{slug}"')
toml_dst = toml_dst.replace('display_name = "My Plugin"',
f'display_name = "{display_name}"')
toml_dst = toml_dst.replace('author = "Author Name"',
f'author = "{author}"')
toml_dst = toml_dst.replace('description = "Short description of what this plugin does."',
f'description = "{description}"')
toml_dst = toml_dst.replace('entry_class = "MyPluginCompanion"',
f'entry_class = "{class_name}"')
toml_dst = re.sub(r'created\s*=\s*"[^"]*"', f'created = "{today}"', toml_dst)
toml_dst = re.sub(r'updated\s*=\s*"[^"]*"', f'updated = "{today}"', toml_dst)
# -- companion.py --------------------------------------------------------
py_src = (template / 'companion.py').read_text(encoding='utf-8')
py_dst = py_src
py_dst = py_dst.replace('MyPluginCompanion', class_name)
py_dst = py_dst.replace("'My Plugin'", f"'{display_name}'")
py_dst = py_dst.replace(
'Template companion plugin.\n\nReplace every occurrence of '
'``MyPlugin`` / ``my_plugin`` with your own name,\nthen implement '
'the methods below.\n\nRename this directory to match the ``name`` '
'field in ``plugin.toml``\n(removing the leading ``_`` so it is '
'auto-discovered).',
f'{display_name} companion plugin.\n\nImplement the TODO sections '
f'below to add your own behaviour.',
)
plugin_dir.mkdir(parents=True)
(plugin_dir / 'plugin.toml').write_text(toml_dst, encoding='utf-8')
(plugin_dir / 'companion.py').write_text(py_dst, encoding='utf-8')
# -- tests/ directory ---------------------------------------------------
tests_template = template / 'tests'
if tests_template.is_dir():
tests_dir = plugin_dir / 'tests'
tests_dir.mkdir()
for src_file in tests_template.iterdir():
if src_file.is_file():
test_src = src_file.read_text(encoding='utf-8')
test_dst = test_src.replace('MyPluginCompanion', class_name)
(tests_dir / src_file.name).write_text(test_dst, encoding='utf-8')
return plugin_dir
[docs]
class _NewPluginDialog(wx.Dialog):
"""Small form to collect metadata for a new plugin."""
def __init__(self, parent, target_dir):
super().__init__(
parent,
title=_('New plugin from template'),
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
)
[docs]
self._target_dir = target_dir
self._build_ui()
self.SetMinSize((420, -1))
self.Fit()
self.CentreOnParent()
# -- public properties ---------------------------------------------------
@property
[docs]
def slug(self) -> str:
return self._txt_slug.GetValue().strip()
@property
[docs]
def display_name(self) -> str:
v = self._txt_display.GetValue().strip()
return v if v else self.slug.replace('_', ' ').replace('-', ' ').title()
@property
[docs]
def author(self) -> str:
return self._txt_author.GetValue().strip()
@property
[docs]
def description(self) -> str:
return self._txt_desc.GetValue().strip()
# -- private -------------------------------------------------------------
[docs]
def _build_ui(self):
outer = wx.BoxSizer(wx.VERTICAL)
grid = wx.FlexGridSizer(rows=0, cols=2, vgap=6, hgap=8)
grid.AddGrowableCol(1)
def _row(label, ctrl, tip=''):
lbl = wx.StaticText(self, label=label)
if tip:
ctrl.SetToolTip(tip)
grid.Add(lbl, 0, wx.ALIGN_CENTER_VERTICAL)
grid.Add(ctrl, 1, wx.EXPAND)
self._txt_slug = wx.TextCtrl(self, value='')
_row(
_('Plugin name (slug) *'),
self._txt_slug,
_('Unique identifier — letters, digits, hyphens and underscores only.\n'
'This will also be the name of the new directory.\nExample: my_feature'),
)
self._txt_display = wx.TextCtrl(self, value='')
_row(
_('Display name'),
self._txt_display,
_('Human-readable label shown in menus (leave empty to derive from slug).'),
)
self._txt_author = wx.TextCtrl(self, value='')
_row(_('Author'), self._txt_author, _('Your name or organisation.'))
self._txt_desc = wx.TextCtrl(self, value='')
_row(_('Description'), self._txt_desc, _('One-line description (optional).'))
# Target directory (informational, read-only)
lbl_target = wx.StaticText(
self,
label=_('Target directory: {d}').format(d=self._target_dir),
)
lbl_target.SetForegroundColour(wx.Colour(100, 100, 100))
outer.Add(grid, 0, wx.ALL | wx.EXPAND, 12)
outer.Add(lbl_target, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 12)
# Buttons
btn_sizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
outer.Add(btn_sizer, 0, wx.ALL | wx.ALIGN_RIGHT, 8)
self.SetSizer(outer)
self.Bind(wx.EVT_BUTTON, self._on_ok, id=wx.ID_OK)
[docs]
def _on_ok(self, event):
import re
slug = self.slug
if not slug:
wx.MessageBox(
_('Plugin name (slug) is required.'),
_('New plugin'),
wx.OK | wx.ICON_WARNING,
self,
)
return
if not re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]*', slug):
wx.MessageBox(
_('Invalid slug "{s}".\n\n'
'Use only letters, digits, hyphens and underscores, '
'starting with a letter.').format(s=slug),
_('New plugin'),
wx.OK | wx.ICON_WARNING,
self,
)
return
event.Skip() # accept the dialog
[docs]
def handle_configuration_dialog(wxparent, configuration):
dlg = GlobalOptionsDialog(wxparent)
try:
dlg.push_configuration(configuration)
if dlg.ShowModal() == wx.ID_OK:
# do something here
dlg.pull_configuration(configuration)
configuration.save()
logging.info(_('Configuration saved in {}').format(str(configuration.path)))
else:
# handle dialog being cancelled or ended by some other button
pass
finally:
# explicitly cause the dialog to destroy itself
dlg.Destroy()
if __name__ == "__main__":
[docs]
cfg = WolfConfiguration(Path("test.conf"))
cfg[ConfigurationKeys.PLAY_WELCOME_SOUND] = False
print(cfg._config)
cfg.save()
cfg = WolfConfiguration(Path("test.conf"))
cfg.load()
print(cfg[ConfigurationKeys.PLAY_WELCOME_SOUND])