Source code for hynet.visual.capability.settings

"""
Settings view to edit parameters in the capability region visualizer.
"""

import logging
from re import compile as compile_regex

import tkinter as tk
from tkinter import ttk

from hynet.scenario.capability import HalfSpace, CapRegion
from hynet.visual.capability.utilities import Orientation

_log = logging.getLogger(__name__)

DIGITS = 5


[docs]class SettingsView(tk.ttk.Frame): # pylint: disable=too-many-ancestors """ Settings view to edit parameters in the capability region visualizer. """ def __init__(self, master=None, cap_region=CapRegion()): super(SettingsView, self).__init__(master) self.error_callback = None self.container = tk.Frame(master, padx=0, pady=0, bg="white") self.capability_region = cap_region power_tuple = (cap_region.p_min, cap_region.p_max) reactive_tuple = (cap_region.q_min, cap_region.q_max) # active power view (i.e., first row) self.active_power = ValueView(self.container, 'Active power:', left_value_name='Minimum:', right_value_name='Maximum:', initial_value_tuple=power_tuple) self.active_power.container.grid(row=0, column=0, sticky=[tk.W, tk.E]) # reactive power view (i.e., second row) self.reactive_power = ValueView(self.container, 'Reactive power:', left_value_name='Minimum:', right_value_name='Maximum:', initial_value_tuple=reactive_tuple) self.reactive_power.container.grid(row=1, column=0, sticky=[tk.W, tk.E]) # halfplane views (i.e., third to sixth row) self.rt_half_space = ValueView(self.container, 'Right-top half-space:', initial_value_tuple=cap_region.rt, checkbox_state=cap_region.rt is not None) self.rt_half_space.container.grid(row=2, column=0, sticky=[tk.W, tk.E]) self.rb_half_space = ValueView(self.container, 'Right-bottom half-space:', initial_value_tuple=cap_region.rb, checkbox_state=cap_region.rb is not None) self.rb_half_space.container.grid(row=3, column=0, sticky=[tk.W, tk.E]) self.lt_half_space = ValueView(self.container, 'Left-top half-space:', initial_value_tuple=cap_region.lt, checkbox_state=cap_region.lt is not None) self.lt_half_space.container.grid(row=4, column=0, sticky=[tk.W, tk.E]) self.lb_half_space = ValueView(self.container, 'Left-bottom half-space:', initial_value_tuple=cap_region.lb, checkbox_state=cap_region.lb is not None) self.lb_half_space.container.grid(row=5, column=0, sticky=[tk.W, tk.E]) self.power_factor = PowerFactorView(self.container) self.power_factor.container.grid(row=6, column=0, sticky=[tk.W, tk.E]) spacer = tk.Frame(self.container, bg="white") spacer.grid(row=7, column=0, sticky=[tk.W, tk.E, tk.S, tk.N]) self.rowconfigure(7, weight=1) # setup all error_callbacks for value_view in [self.lt_half_space, self.lb_half_space, self.rt_half_space, self.rb_half_space, self.active_power, self.reactive_power]: value_view.error_callback = self._proxy_error_callback
[docs] def setup_halfspace_callbacks(self, update_view_callback): """ Set the callback that updates the view with changes of the half-spaces. Parameters ---------- update_view_callback: function This function is called when the checkbox is (de)activated and should update/reload the view of the capability region. """ def _create_callback(value_view, cap_region, orientation_tuple): def _callback(value): if value: slope = value_view.right_entry.get_value() offset = value_view.left_entry.get_value() new_halfspace = HalfSpace(slope=slope, offset=offset) else: new_halfspace = None active_power, reactive_power = orientation_tuple raise_error = False try: if active_power is Orientation.RIGHT and \ reactive_power is Orientation.TOP: cap_region.rt = new_halfspace elif active_power is Orientation.RIGHT and \ reactive_power is Orientation.BOTTOM: cap_region.rb = new_halfspace elif active_power is Orientation.LEFT and \ reactive_power is Orientation.TOP: cap_region.lt = new_halfspace elif active_power is Orientation.LEFT and \ reactive_power is Orientation.BOTTOM: cap_region.lb = new_halfspace else: raise_error = True except ValueError as halfspace_invalid_error: # halfspace is invalid and thus capregion.(l|r)(t|b) = x # raises an error => load the values of the capregion into # the visual again / reset the user's input self._proxy_error_callback( halfspace_invalid_error, undo_callback=self.update_from_capability_region) if raise_error: raise ValueError( f"Unhandled case: ({active_power}, {reactive_power}") update_view_callback() return _callback self.lt_half_space.callback = _create_callback( self.lt_half_space, self.capability_region, (Orientation.LEFT, Orientation.TOP)) self.lb_half_space.callback = _create_callback( self.lb_half_space, self.capability_region, (Orientation.LEFT, Orientation.BOTTOM)) self.rt_half_space.callback = _create_callback( self.rt_half_space, self.capability_region, (Orientation.RIGHT, Orientation.TOP)) self.rb_half_space.callback = _create_callback( self.rb_half_space, self.capability_region, (Orientation.RIGHT, Orientation.BOTTOM))
[docs] def deactivate_all_halfspaces(self): """ Deactivate all checkboxes and, therewith, remove all half-spaces. """ self.lt_half_space.checkbox_value.set(False) self.lb_half_space.checkbox_value.set(False) self.rt_half_space.checkbox_value.set(False) self.rb_half_space.checkbox_value.set(False)
[docs] def update_from_capability_region(self): """ Update the views with the (possibly) changed capability region. """ self.lt_half_space.update_from_capability_region( self.capability_region.lt) self.lb_half_space.update_from_capability_region( self.capability_region.lb) self.rt_half_space.update_from_capability_region( self.capability_region.rt) self.rb_half_space.update_from_capability_region( self.capability_region.rb)
def _proxy_error_callback(self, message, undo_callback=None): if self.error_callback: # pylint: disable=not-callable self.error_callback(message, undo_callback) else: raise ValueError("Received an error call, " "but no callback is configured.")
[docs]class PowerFactorView(tk.ttk.Frame): # pylint: disable=too-many-ancestors """View for setting the power factor limit.""" def __init__(self, master=None): super().__init__(master) self.container = tk.Frame(master, padx=0, pady=0, bg="white") self._title = tk.Label(self.container, text="Set power factor:", anchor=tk.SW, pady=1, bg="white") self._title.grid(row=0, column=0, columnspan=2, sticky=[tk.W, tk.E]) self._callback = lambda x: x self._value = tk.StringVar() self._entry = tk.Entry(self.container, textvariable=self._value, bg="white", width=10) self._entry.grid(row=1, column=0, padx=(25, 5), sticky=(tk.W, tk.E)) self._trigger = tk.Button(self.container, text="Apply", padx=5, command=self._proxy_callback, bg="white", width=10) self._trigger.grid(row=1, column=1, padx=0, pady=0, sticky=(tk.W, tk.E, tk.S, tk.N)) def _proxy_callback(self): value = self._value.get() if not value: return try: self._callback(float(value)) except ValueError: _log.warning(value + " is not a float!") self._entry.delete(0, tk.END)
[docs] def set_callback(self, callback_function): """ Set the callback for updating the power factor limit. Parameters ---------- callback_function: function Function that takes the power factor as an argument and updates the capability region. """ self._callback = callback_function
[docs]class ValueView(tk.ttk.Frame): # pylint: disable=too-many-ancestors """ Container view for two descriptive entries. Furthermore, the value view handles the update of the associated half-space (if provided) as well as the error and update callbacks. """ callback = None def __init__(self, master=None, name='Unnamed ValueView', right_value_name='Slope:', left_value_name='Offset:', initial_value_tuple=(0, 0), checkbox_state=None): super(ValueView, self).__init__(master) self.error_callback = None if initial_value_tuple: left, right = initial_value_tuple else: left, right = (0.0, 0.0) self.container = tk.Frame(master, padx=5, pady=5, bg='white') if checkbox_state is None: self._title = tk.Label(self.container, anchor=tk.SW, text=name, bg="white") else: self.checkbox_value = tk.BooleanVar() self.checkbox_value.set(checkbox_state) self._title = tk.Checkbutton(self.container, text=name, variable=self.checkbox_value, command=self._proxy_callback, anchor=tk.SW, bg="white") self._title.grid(row=0, column=0, columnspan=2, padx=0, pady=0, sticky=[tk.W, tk.E]) self.left_entry = DescriptiveEntry(self.container, left_value_name, alignment=tk.VERTICAL, value=left) self.left_entry.container.grid(row=1, column=0, padx=(20, 5), pady=0, sticky=[tk.W, tk.E]) self.right_entry = DescriptiveEntry(self.container, right_value_name, alignment=tk.VERTICAL, value=right) self.right_entry.container.grid(row=1, column=1, padx=(0, 5), pady=0, sticky=[tk.W, tk.E]) self.left_entry.error_callback = self._proxy_error_callback self.right_entry.error_callback = self._proxy_error_callback def _proxy_error_callback(self, message, undo_callback=None): if self.error_callback: # pylint: disable=not-callable self.error_callback(message, undo_callback) else: raise ValueError("Received an error call, " "but no callback is configured.") def _proxy_callback(self): if self.callback: try: # pylint: disable=not-callable self.callback(self.checkbox_value.get()) except ValueError as error: self._proxy_error_callback(str(error)) else: raise ValueError("Received a call, but no callback is configured.")
[docs] def update_from_capability_region(self, halfspace): """ Updates this view based on the provided half-space specification. Parameters ---------- halfspace: HalfSpace The half-space associated with this value view. """ if halfspace: left, right = halfspace self.left_entry.reset_to_normal_state(round(left, DIGITS)) self.right_entry.reset_to_normal_state(round(right, DIGITS)) self.checkbox_value.set(True) else: self.checkbox_value.set(False)
[docs]class DescriptiveEntry(ttk.Frame): # pylint: disable=too-many-ancestors """Container that bundles a label with an input field.""" VALIDATION_REGEX = compile_regex(r'^[+-]?([0-9]*[.])?[0-9]+$') def __init__(self, master=None, name='Label', alignment=tk.HORIZONTAL, value=0.0): super(DescriptiveEntry, self).__init__(master) self.container = tk.Frame(master, padx=0, pady=0, bg="white") self._label = tk.Label(self.container, text=name, anchor=tk.SW, bg="white", width=10) self._label.grid(row=0, column=0, padx=0, pady=0) self._value = tk.StringVar(value=value) self._last_value = value self._validator = (self.register(self._validation_callback), '%P') self._entry = tk.Entry(self.container, textvariable=self._value, validate="focus", validatecommand=self._validator, bg="white", width=10) if alignment is tk.HORIZONTAL: self._entry.grid(row=0, column=1, padx=0, pady=0, sticky=tk.W) else: self._entry.grid(row=1, column=0, padx=0, pady=0, sticky=tk.W) def _validation_callback(self, new_text): if self.VALIDATION_REGEX.match(new_text): return True undo = self.error_state() message = f"{new_text} is not a valid number!" self._proxy_error_callback(message, undo_callback=undo) _log.warning(message) return False
[docs] def error_state(self, old_value=None): """ Set the foreground to red and return the callback to undo this change. Parameters ---------- old_value: float If this value is set, then the value of the input component is set to this value when this callback is called. Returns ------- function Callback to undo this change (set the foreground back to black). """ self._entry.configure(foreground='red') return lambda: self.reset_to_normal_state(old_value)
[docs] def reset_to_normal_state(self, old_value=None): """ Reset the element to the normal state (black font on white background). Parameters ---------- old_value: float If this value is set, then the value of the input component is set to this value when this callback is called. """ self._entry.configure(foreground='black') if old_value: self._value.set(old_value) self._last_value = old_value
def _proxy_error_callback(self, message, undo_callback=None): if self.error_callback: self.error_callback(message, undo_callback) else: raise ValueError("Received an error call, " "but no callback is configured.")
[docs] def set_callback(self, callback): """ Set a callback that listens for value changes. Parameters ---------- callback: function The function that should be called when a value has changed. """ def _get_value(*_args): try: value = float(self._value.get()) # the order is important as the callback is used to check for # conformance of the new value! callback(value) self._last_value = value except ValueError as error: undo = self.error_state(old_value=self._last_value) self._proxy_error_callback(str(error), undo_callback=undo) self._entry.bind("<FocusOut>", _get_value)
[docs] def get_value(self): """ Return the current value of the field. Returns ------- float """ try: value = float(self._value.get()) except ValueError: value = self._last_value return value
[docs] def set_value(self, value): """ Set the value for the field. Parameters ---------- value: float or str """ self._value.set(value)