Source code for squap.widgets.input_widget
from ..helper_funcs import textify, get_type_func, ColorType, get_single_color
import numpy as np
from typing import Any, Callable, Optional, Iterable
from time import time as current_time
import os.path
from pyqtgraph import ColorButton
from PySide6.QtWidgets import (
QTableWidget, QTableWidgetItem,
QLabel, QSlider, QCheckBox, QPushButton, QComboBox
)
from PySide6.QtCore import Qt
[docs]
class Box: # I am not sure if this is required, but I feel like it helps the IDE and is better organized
"""General class for each squap box.
Args:
parent (:class:`InputTable <squap.widgets.input_widget.InputTable>`): The parent :class:`InputTable <squap.widgets.input_widget.InputTable>`.
Attributes:
row (int): Row of the box. Is used when removing it after it has been created.
textbox (:class:`QLabel <PySide6.QtWidgets.QLabel>`): The :class:`QLabel <PySide6.QtWidgets.QLabel>` object
corresponding to this box.
"""
def __init__(self, parent: 'InputTable'): # parent will be the InputWidget
self.parent = parent
self.change_funcs = {} # This is so that they can be reordered later if necessary
# this is used when print_value is set to True during runtime, as it should still be the first function that is
# called. So the printing function would be bound and then all other functions would be rebound.
# It is a dictionary to allow unbinding functions, since the function passed `box.bind` is sometimes
# different from the actual bound function
self.row = None # For removing it later (should be set inside the function)
self.textbox = None # So that you can check if a textbox exists for this box
self.link_funcs = {} # For linking this box to other boxes, change to `None` for unlinkable boxes
# when linking, if multiple boxes have `printing_val` `True`, all but one are set to `False`
self.printing_val = False
[docs]
def change_params(self, **kwargs):
"""
Change box parameters. ``kwargs`` can usually be the same arguments that are given to the ``__init__`` function
of this box, except ``init_value`` and ``row``.
"""
raise NotImplementedError("Subclasses should implement this! Users should not see this.")
[docs]
def bind(self, func: Callable):
"""
Bind function ``func`` to this box, meaning that when the value of the box is changed, the function is called.
Args:
func (:term:`callable`): function to bind to this box.
"""
raise NotImplementedError("Subclasses should implement this! Users should not see this.")
[docs]
def unbind(self, func: Callable):
"""
Unbind function. Is used internally when removing a box.
Args:
func (:term:`callable`): function to unbind from this box.
"""
raise NotImplementedError("Subclasses should implement this! Users should not see this.")
def _on_change(self, *args): # Function that updates the var.var_name value upon value change
"""
Update the associated variable when the user interacts with the box. Not really meant for users.
"""
raise NotImplementedError("Subclasses should implement this! Users should not see this.")
[docs]
def value(self):
"""
Return the current value of :ref:`var.var_name <squap.var>`
"""
raise NotImplementedError("Subclasses should implement this! Users should not see this.")
[docs]
def set_value(self, value):
"""
Set the value of the box and :ref:`var.var_name <squap.var>` to ``value``. Note that for a slider, the new slider position will be
the closest possible position to ``value``, and the new value of :ref:`var.var_name <squap.var>` will be the value corresponding to
the new slider position.
"""
raise NotImplementedError("Subclasses should implement this! Users should not see this.")
[docs]
def print_val(self):
"""
Print the current value of the box. Used for the ``print_value`` keyword argument.
"""
raise NotImplementedError("Subclasses should implement this! Users should not see this.")
[docs]
def remove(self):
"""
Remove this box.
"""
# if self.textbox is not None:
# self.textbox.destroy() # not sure if this does anything
self.parent.remove_row(self.row, self)
[docs]
class InputTable(QTableWidget): # table for all inputs
def __init__(self, width: int, height: int, name: str, window): # takes window as an argument so that it can inherit
# update_funcs and variables easily
super().__init__()
self.name = name
self.resize(width, height)
self.setRowCount(0)
self.setColumnCount(3)
self.horizontalHeader().hide()
self.verticalHeader().hide()
self.col_partition = 1/3 # where is the partition between col 0 & col 1, should be between 0&1
self.rate_slider_space = 60 # pixels of space a rate_slider gets
self.setColumnWidth(0, int(width * self.col_partition))
self.setColumnWidth(1, int(self.rate_slider_space))
self.setColumnWidth(2, int(width * (1-self.col_partition))-self.rate_slider_space)
# this cell is split in 2 for the rate_slider
self.current_row = -1 # row of the last placed widget (so total amount of rows - 1)
self.variables = window.variables
self.update_funcs = window.update_funcs
self.window = window # necessary for renaming tabs
self.input_varnames = [] # the names of every variable indexed by row for stuff like linking and rate_slider
self.boxes = [] # for removing them later (and a nice overview)
self.empty_rows = [] # these are the rows that have been removed out of order, so that these are filled
# up first
def resizeEvent(self, *args) -> None: # event is not used since every necessary parameter is in self
width = self.width()
height = self.height()
self.setGeometry(0, 0, width, height)
if self.rowHeight(0)*(self.current_row+1)+26 <= height:
width -= 2
else:
width -= 18
self.setColumnWidth(0, int(width * self.col_partition))
self.setColumnWidth(1, int(self.rate_slider_space))
self.setColumnWidth(2, int(width * (1-self.col_partition))-self.rate_slider_space)
[docs]
def rename(self, name: str):
"""Renames the table. The name is only visible when multiple tabs are present."""
self.window.table_manager.rename_tab(name, old_name=self.name)
[docs]
def set_partition(self, fraction=1/3):
"""
Sets the position of the partition between the 2 columns of the :class:`~squap.widgets.input_widget.InputTable`.
Args:
fraction (float): float between ``0`` and ``1``, specifying the portion of the window taken up by the partition.
Starts off as ``1/3``.
"""
self.col_partition = fraction
self.resizeEvent()
[docs]
def get_boxes(self) -> list[Box]: # assumes actual widget is the leftmost widget
"""
Returns a list containing all :class:`boxes <squap.widgets.Box>` that exist at this point.
Returns:
list of Box: list of all :class:`boxes <squap.widgets.Box>` added in order.
"""
return [box_row[-1] for box_row in self.boxes]
def add_widget(self, row: None | int, box_row: tuple[Box]) -> int: # if row is specified and row is in empty_rows it is added there
"""
Handles all internal stuff that is required when a :class:`box <squap.widgets.Box>` is added to a
specified row. The row can be the next row or any empty row.
Args:
row (None or int): Row in which to add the box. Pass ``None`` to add it to a next row, or choose a row that has
no :class:`boxes <squap.widgets.Box>` in it yet.
box_row (tuple of Box): `tuple` containing all :class:`boxes <squap.widgets.Box>` in the row
(so usually a :class:`textbox <PySide6.QtWidgets.QLabel>` and :class:`box widget <squap.widgets.Box>`)
Returns:
int: Row in which the :class:`box <squap.widgets.Box>` has been added.
"""
if row is None or row == self.current_row+1:
if not self.empty_rows:
self.current_row += 1
self.setRowCount(self.current_row + 1)
table_height = self.rowHeight(0)*(self.current_row+1)+26
if table_height >= self.height():
self.resizeEvent(None)
self.boxes.append(box_row)
return self.current_row
else:
self.empty_rows.sort()
new_row = self.empty_rows[0]
self.empty_rows.remove(new_row)
self.boxes[new_row] = box_row
return new_row
elif row in self.empty_rows:
self.empty_rows.remove(row)
self.boxes[row] = box_row
return row
elif row > self.current_row + 1:
extra_rows = []
for _ in range(row - self.current_row):
self.current_row += 1
self.setRowCount(self.current_row + 1)
table_height = self.rowHeight(0) * (self.current_row + 1) + 26
if table_height >= self.height():
self.resizeEvent(None)
extra_rows.append(self.current_row)
self.boxes.extend([()]*(len(extra_rows)-1))
self.empty_rows.extend(extra_rows[:-1])
self.boxes.append(box_row)
return self.current_row
else:
raise ValueError(f"row {row} is not empty")
[docs]
def remove_row(self, remove_row: int, remove_box: Box):
"""
Remove box ``remove_box`` from row ``remove_row``.
Args:
remove_row (int): row from which to remove the :class:`box <squap.widgets.Box>`.
remove_box (Box): :class:`Box <squap.widgets.Box>` to remove.
"""
if remove_row in self.empty_rows or remove_row > self.current_row:
raise ValueError(f"row {remove_row} is already empty.")
for func in remove_box.change_funcs:
remove_box.unbind(func)
if not isinstance(remove_box, self.Button):
remove_box.unbind(remove_box._on_change)
for col, box in enumerate(self.boxes[remove_row]):
if isinstance(box, self.InputBox) or isinstance(box, self.RateSlider):
self.setItem(remove_row, col, QTableWidgetItem(""))
else:
self.setCellWidget(remove_row, col, None)
if remove_row == self.current_row:
self.current_row -= 1
self.setRowCount(self.current_row + 1)
self.boxes.pop(-1)
else:
self.setSpan(remove_row, 0, 1, 1)
self.setSpan(remove_row, 1, 1, 1)
self.setSpan(remove_row, 2, 1, 1)
for col in range(3):
item = self.item(remove_row, col)
if item:
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) # Make uneditable
self.empty_rows.append(remove_row)
self.boxes[remove_row] = ()
[docs]
def add_slider(self, name: str, init_value: float = 1.0, min_value: float = 0.0, max_value: float = 10.0,
n_ticks: int = 51, tick_interval: Optional[float] = None, only_ints: bool = False,
logscale: bool = False, custom_arr: Optional[Iterable] = None, var_name: Optional[str] = None,
print_value: bool = False, row: Optional[int] = None) -> 'InputTable.Slider':
"""Creates a :class:`slider <squap.widgets.InputTable.Slider>` with the given parameters, and adds it to this
:class:`input table <squap.widgets.input_widget.InputTable>`. See :func:`squap.add_slider`
for initialisation information.
Returns:
InputTable.Slider: The created :class:`slider widget <squap.widgets.InputTable.Slider>`.
"""
return self.Slider(self, name, init_value, min_value, max_value, n_ticks, tick_interval, only_ints, logscale,
custom_arr, var_name, print_value, row)
[docs]
def add_checkbox(self, name: str, init_value: bool = False, var_name: Optional[str] = None,
print_value: bool = False, row: Optional[int] = None) -> 'InputTable.CheckBox':
"""Creates a :class:`checkbox <squap.widgets.InputTable.CheckBox>` with the given parameters, and adds it to this
:class:`input table <squap.widgets.input_widget.InputTable>`. See :func:`squap.add_checkbox`
for initialisation information.
Returns:
InputTable.CheckBox: The created :class:`checkbox widget <squap.widgets.InputTable.CheckBox>`.
"""
return self.CheckBox(self, name, init_value, var_name, print_value, row)
[docs]
def add_inputbox(self, name: str, init_value: Any = 1.0, type_func: Optional[Callable] = None,
var_name: Optional[str] = None, print_value: bool = False,
row: Optional[int] = None) -> 'InputTable.InputBox':
"""Creates an :class:`inputbox <squap.widgets.InputTable.InputBox>` with the given parameters, and adds it to
this :class:`input table <squap.widgets.input_widget.InputTable>`. See :func:`squap.add_inputbox`
for initialisation information.
Returns:
InputTable.InputBox: The created :class:`inputbox widget <squap.widgets.InputTable.InputBox>`.
"""
return self.InputBox(self, name, init_value, type_func, var_name, print_value, row)
[docs]
def add_button(self, name: str, func: Optional[Callable] = None, row: Optional[int] = None) -> 'InputTable.Button':
"""Creates a :class:`button <squap.widgets.InputTable.Button>` with name ``name`` and bound function ``func``,
and adds it to this :class:`input table <squap.widgets.input_widget.InputTable>`. See :func:`squap.add_button`
for initialisation information.
Returns:
InputTable.Button: The created :class:`button widget <squap.widgets.InputTable.Button>`.
"""
return self.Button(self, name, func, row)
[docs]
def add_dropdown(self, name: str, options: list, init_index: int = 0, option_names: Optional[Iterable[str]] = None,
var_name: Optional[str] = None, print_value: bool = False, row: Optional[int] = None) -> 'InputTable.Dropdown':
"""Creates a :class:`dropdown <squap.widgets.InputTable.Dropdown>` widget with the given parameters, and adds
it to this :class:`input table <squap.widgets.input_widget.InputTable>`. See :func:`squap.add_dropdown` for
initialisation information.
Returns:
InputTable.Dropdown: The created :class:`dropwdown widget <squap.widgets.InputTable.Dropdown>`."""
return self.Dropdown(self, name, options, init_index, option_names, var_name, print_value, row)
[docs]
def add_rate_slider(self, name: str, init_value: float = 1.0, change_rate: float = 10.0, absolute: bool = False,
time_var: Optional[str] = None, custom_func: Optional[Callable] = None,
var_name: Optional[str] = None, print_value: bool = False,
row: Optional[int] = None) -> 'InputTable.RateSlider':
"""Creates a :class:`RateSlider <squap.widgets.InputTable.RateSlider>` with the given parameters, and adds it
to this :class:`input table <squap.widgets.input_widget.InputTable>`. See :func:`squap.add_rate_slider` for
initialisation information.
Returns:
InputTable.RateSlider: The created :class:`rate_slider widget <squap.widgets.InputTable.RateSlider>`.
"""
return self.RateSlider(
self, name, init_value, change_rate,
absolute, time_var, custom_func, var_name, print_value, row
)
[docs]
def add_color_picker(self, name: str, init_value: ColorType = (255, 255, 255), var_name: Optional[str] = None,
print_value: bool = False, row: Optional[int] = None) -> 'InputTable.ColorPicker':
"""Creates a :class:`ColorPicker <squap.widgets.InputTable.ColorPicker>` with the gives parameters, and adds it
to this :class:`input table <squap.widgets.input_widget.InputTable>`. See :func:`squap.add_color_picker` for
initialisation information.
Returns:
InputTable.ColorPicker: The created :class:`color_picker widget <squap.widgets.InputTable.ColorPicker>`.
"""
return self.ColorPicker(
self, name, init_value, var_name, print_value, row
)
class Slider(Box, QSlider):
"""
Class for the slider widget. See :func:`squap.add_slider` for initialisation information.
"""
def __init__(self, parent: 'InputTable', name: str, init_value: float, min_value: float, max_value: float,
n_ticks: int = 51, tick_interval: Optional[float] = None, only_ints: bool = False,
logscale: bool = False, custom_arr: Optional[Iterable] = None, var_name: Optional[str] = None,
print_value: bool = False, row: Optional[int] = None):
Box.__init__(self, parent)
QSlider.__init__(self, parent=parent, orientation=Qt.Orientation.Horizontal)
(self.min_value, self.max_value, self.var_name, self.only_ints, self.logscale, self.custom_arr,
self.n_ticks, self.tick_interval) = (min_value, max_value, var_name, only_ints, logscale, custom_arr,
n_ticks, tick_interval)
# this bit is the same for most widgets:
# <editor-fold desc="add_widget and init name&var_name">
if name == "": # if self.textbox will be created
box_row = (self, )
else:
box_row = (self.textbox, self)
row = parent.add_widget(row=row, box_row=box_row)
self.row = row
if name == "": # if no name the first two columns are merged for one cell, but does require var_name
col = 0
parent.setSpan(row, 0, 1, 3)
if var_name is None:
raise ValueError("name can only be empty if var_name is specified") # #1004
else:
col = 1
parent.setSpan(row, 1, 1, 2)
self.textbox = QLabel(name)
parent.setCellWidget(row, 0, self.textbox)
if var_name is None:
var_name = name
parent.input_varnames.append(var_name)
parent.setCellWidget(row, col, self)
# </editor-fold>
self.setMinimum(0)
self.current_name = var_name
if custom_arr is not None:
self.arr = custom_arr
elif logscale:
if min_value*max_value <= 0:
raise ValueError("`min_value` and `max_value` must have the same sign when `logscale` is enabled.")
if not tick_interval:
self.arr = np.logspace(np.log10(min_value), np.log10(max_value), int(n_ticks))
else:
n_ticks = round(np.emath.logn(tick_interval, max_value/min_value))
self.arr = np.logspace(np.log10(min_value), np.log10(max_value), n_ticks)
if only_ints:
self.arr = np.round(self.arr).astype(int)
elif only_ints:
if tick_interval is None:
tick_interval = 1
else:
tick_interval = int(tick_interval)
# max_value = max_value - (max_value - min_value) % tick_interval
self.arr = np.arange(min_value, max_value+tick_interval, tick_interval)
# n = int((max_value - min_value) / tick_interval)
else:
if not tick_interval:
self.arr = np.linspace(min_value, max_value, int(n_ticks))
else:
self.arr = np.arange(min_value, max_value+tick_interval, tick_interval)
# slider.setStyleSheet("QSlider {height: 20px; width: 200px;}") # makes sliders nicer
n = len(self.arr)-1
self.setMaximum(n)
self.setSingleStep(1) # increment between values
self.setTickPosition(QSlider.TickPosition.TicksBelow)
self.setTickInterval(1) # distance between ticks
if n > 50: # remove ticks after n=50 to not make it too cluttered
self.setTickPosition(QSlider.NoTicks)
else:
self.setTickPosition(QSlider.TickPosition.TicksBelow)
self.valueChanged.connect(self._on_change) # connected before `set_value` because it needs to be
# reconnected in this function
self.set_value(init_value)
if print_value:
self.valueChanged.connect(self.print_val)
self.printing_val = True
def change_params(self, **kwargs):
"""
Changes the parameters of the slider. Only keyword arguments are accepted, and takes all arguments that
:func:`squap.add_slider` accepts, except ``init_value`` and ``row``.
"""
turn_on_print_func = False
update_arr = False # if any parameters are changed such that self.arr changes
arr_kwargs = ["min_value", "max_value", "n_ticks", "tick_interval", "only_ints", "logscale", "custom_arr"]
if "name" in kwargs:
self.textbox.setText(kwargs["name"])
if "var_name" in kwargs:
self.current_name = kwargs["var_name"]
elif self.var_name is not None:
self.current_name = self.var_name
elif "name" in kwargs:
self.current_name = kwargs["name"]
if "print_value" in kwargs:
if not kwargs["print_value"]: # if print_value=False is passed as kwarg
if self.printing_val:
# if print_func already exists (and is not None), remove it and set it to None
self.valueChanged.disconnect(self.print_val)
self.printing_val = False
elif not self.printing_val: # or if it does exist but is set to None at the moment
turn_on_print_func = True
if "n_ticks" in kwargs and "tick_interval" not in kwargs:
self.tick_interval = None
for kwarg in arr_kwargs:
if kwarg in kwargs:
update_arr = True
setattr(self, kwarg, kwargs[kwarg])
if update_arr:
old_arr = self.arr.copy()
if "custom_arr" not in kwargs: # if custom_arr is not provided but other arguments that change self.arr are
self.custom_arr = None
if self.custom_arr is not None:
self.arr = self.custom_arr
elif self.logscale:
if self.min_value * self.max_value <= 0:
raise ValueError("min_value and max_value must have the same sign when logscale is enabled.")
if not self.tick_interval:
self.arr = np.logspace(np.log10(self.min_value), np.log10(self.max_value), self.n_ticks)
else:
n_ticks = round(np.emath.logn(self.tick_interval, self.max_value / self.min_value))
self.arr = np.logspace(np.log10(self.min_value), np.log10(self.max_value), n_ticks)
elif self.only_ints:
if self.tick_interval is None:
tick_interval = 1
else:
tick_interval = int(self.tick_interval)
self.arr = np.arange(self.min_value, self.max_value + tick_interval, tick_interval)
# self.max_value = self.max_value - (self.max_value-self.min_value) % tick_interval
# n = int((self.max_value-self.min_value)/tick_interval)
else:
# print(not float(self.tick_i?nterval))
if self.tick_interval is None:
if "n_ticks" in kwargs.keys():
n_ticks = int(kwargs["n_ticks"])
else:
n_ticks = len(self.arr)
self.arr = np.linspace(self.min_value, self.max_value, n_ticks)
else:
self.arr = np.arange(self.min_value, self.max_value+self.tick_interval, self.tick_interval)
if len(self.arr) == 0:
self.arr = old_arr
raise ValueError("Current parameters give an empty array")
n = len(self.arr) - 1
self.setMaximum(n)
if n > 50:
self.setTickPosition(QSlider.NoTicks)
else:
self.setTickPosition(QSlider.TickPosition.TicksBelow)
current_val = getattr(self.parent.variables, self.current_name)
if self.logscale:
slider_val = np.argmin(np.abs(np.log10(np.array(self.arr)) - np.log10(current_val)))
else:
slider_val = np.argmin(np.abs(np.array(self.arr) - current_val))
self.setValue(slider_val) # also automatically calls _on_change
if turn_on_print_func:
self.valueChanged.connect(self.print_val)
self.printing_val = True
for func in self.change_funcs.values(): # reorders change_funcs so that print_val is first
self.valueChanged.disconnect(func)
self.valueChanged.connect(func)
def bind(self, func):
# makes sure function is called without argument (removing this breaks examples/all_plot_options.py
def new_func():
func()
self.change_funcs[func] = new_func
self.valueChanged.connect(new_func)
return self
def unbind(self, func):
if func not in self.change_funcs and func != self._on_change:
raise ValueError("Function is not bound to this Box.")
if func != self._on_change:
self.valueChanged.disconnect(self.change_funcs[func])
self.change_funcs.pop(func)
else:
self.valueChanged.disconnect(self._on_change)
return self
def _on_change(self, val):
setattr(self.parent.variables, self.current_name, self.arr[val])
def set_value(self, value):
if self.logscale:
slider_val = np.argmin(np.abs(np.log10(np.array(self.arr)) - np.log10(value)))
else:
slider_val = np.argmin(np.abs(np.array(self.arr) - value))
setattr(self.parent.variables, self.current_name, value)
self.valueChanged.disconnect(self._on_change) # so that `var.current_name` is set to `value` not
# the closest possible value in `self.arr`.
self.setValue(slider_val)
self.valueChanged.connect(self._on_change)
def value(self):
return getattr(self.parent.variables, self.current_name)
def set_index(self, index):
"""Sets the index to ``index``."""
self.setValue(index)
def index(self):
"""Returns the current index."""
return super(QSlider, self).value() # self.value is overwritten so we want to access the parent one.
def print_val(self):
print(f"{self.current_name} = {self.value()}")
class CheckBox(Box, QCheckBox):
"""
Class for the checkbox widget. See :func:`squap.add_checkbox` for initialisation information.
"""
def __init__(self, parent: 'InputTable', name: str, init_value: bool, var_name: Optional[str] = None,
print_value: bool = False, row: Optional[int] = None):
Box.__init__(self, parent)
QCheckBox.__init__(self)
self.var_name = var_name
# <editor-fold desc="add_widget and init name&var_name">
if name == "": # if self.textbox will be created
box_row = (self, )
else:
box_row = (self.textbox, self)
row = parent.add_widget(row=row, box_row=box_row)
self.row = row
if name == "": # if no name the first two columns are merged for one cell, but does require var_name
col = 0
parent.setSpan(row, 0, 1, 3)
if var_name is None:
raise ValueError("name can only be empty if var_name is specified") # #1004
else:
col = 1
parent.setSpan(row, 1, 1, 2)
self.textbox = QLabel(name)
parent.setCellWidget(row, 0, self.textbox)
if var_name is None:
var_name = name
parent.input_varnames.append(var_name)
parent.setCellWidget(row, col, self)
# </editor-fold>
self.current_name = var_name
setattr(parent.variables, self.current_name, init_value)
self.setChecked(init_value)
self.stateChanged.connect(self._on_change)
if print_value:
self.stateChanged.connect(self.print_val)
self.printing_val = True
def change_params(self, **kwargs):
"""
Changes the parameters of the checkbox. Only keyword arguments are accepted, and takes all arguments that
:func:`squap.add_checkbox` accepts, except ``init_value`` and ``row``.
"""
turn_on_print_func = False
if "name" in kwargs:
self.textbox.setText(kwargs["name"])
if "var_name" in kwargs:
self.current_name = kwargs["var_name"]
elif self.var_name is not None:
self.current_name = self.var_name
elif "name" in kwargs:
self.current_name = kwargs["name"]
if "print_value" in kwargs:
if not kwargs["print_value"]: # if print_value=False is passed as kwarg
if self.printing_val:
# if print_func already exists (and is not None), remove it and set it to None
self.stateChanged.disconnect(self.print_val)
self.printing_val = False
elif not self.printing_val: # or if it does exist but is set to None at the moment
turn_on_print_func = True
if turn_on_print_func:
self.stateChanged.connect(self.print_val)
self.printing_val = True
for func in self.change_funcs: # reorders change_funcs so that print_val is first
self.stateChanged.disconnect(func)
self.stateChanged.connect(func)
def _val(self): # turns checkbox state into tf value (val because value is already used by some boxes)
if self.checkState() == Qt.CheckState.Checked:
return True
else:
return False
def bind(self, func):
# makes sure function is called without argument (removing this breaks examples/all_plot_options.py
def new_func():
func()
self.change_funcs[func] = new_func
self.stateChanged.connect(new_func)
return self
def unbind(self, func):
if func not in self.change_funcs and func != self._on_change:
raise ValueError("Function is not bound to this Box.")
if func != self._on_change:
self.stateChanged.disconnect(self.change_funcs[func])
self.change_funcs.pop(func)
else:
self.stateChanged.disconnect(self._on_change)
return self
def _on_change(self):
setattr(self.parent.variables, self.current_name, self._val())
def set_value(self, value: bool):
self.setChecked(value)
def value(self):
return getattr(self.parent.variables, self.current_name)
def print_val(self):
print(f"{self.current_name} = {self.value()}")
class InputBox(Box):
"""
Class for the inputbox widget. See :func:`squap.add_inputbox` for initialisation information.
"""
def __init__(self, parent: 'InputTable', name: str, init_value: Any, type_func: Optional[Callable] = None,
var_name: Optional[str] = None, print_value: bool = False, row: Optional[int] = None):
Box.__init__(self, parent)
self.var_name = var_name
# <editor-fold desc="add_widget and init name&var_name">
if name == "":
box_row = (self,)
else:
box_row = (self.textbox, self)
row = parent.add_widget(row=row, box_row=box_row)
self.row = row
if name == "": # if no name the first two columns are merged for one cell, but does require var_name
self.col = 0
parent.setSpan(row, 0, 1, 3)
if var_name is None:
raise ValueError("name can only be empty if var_name is specified") # #1004
else:
self.col = 1
parent.setSpan(row, 1, 1, 2)
self.textbox = QLabel(name)
parent.setCellWidget(row, 0, self.textbox)
if var_name is None:
var_name = name
parent.input_varnames.append(var_name)
# </editor-fold>
if isinstance(init_value, str):
parent.setItem(row, self.col, QTableWidgetItem('"' + init_value + '"'))
else:
parent.setItem(row, self.col, QTableWidgetItem(str(init_value)))
self.current_name = var_name
if type_func is None:
self.type_func = get_type_func(init_value, parent, self.col)
else:
self.type_func = type_func
setattr(parent.variables, self.current_name, init_value)
self.parent.cellChanged.connect(self._on_change)
if print_value:
def print_func(row):
if row == self.row:
self.print_val()
self.parent.cellChanged.connect(print_func)
self.printing_val = True
def change_params(self, **kwargs):
"""
Changes the parameters of the inputbox. Only keyword arguments are accepted, and takes all arguments that
:func:`squap.add_inputbox` accepts, except ``init_value`` and ``row``.
"""
turn_on_print_func = False
if "name" in kwargs:
self.textbox.setText(kwargs["name"])
if "var_name" in kwargs:
self.current_name = kwargs["var_name"]
elif self.var_name is not None:
self.current_name = self.var_name
elif "name" in kwargs:
self.current_name = kwargs["name"]
if "type_func" in kwargs:
if kwargs["type_func"] is None:
self.type_func = get_type_func(self.value(), self.parent, self.col)
else:
self.type_func = kwargs["type_func"]
if "print_value" in kwargs:
if not kwargs["print_value"]: # if print_value=False is passed as kwarg
if self.printing_val:
# if print_func already exists (and is not None), remove it and set it to None
self.parent.cellChanged.disconnect(self.print_val)
self.printing_val = False
elif not self.printing_val: # or if it does exist but is set to None at the moment
turn_on_print_func = True
if turn_on_print_func:
def print_func(row):
if row == self.row:
self.print_val()
self.parent.cellChanged.connect(print_func)
self.printing_val = True
for func in self.change_funcs: # reorders change_funcs so that print_val is first
self.parent.cellChanged.disconnect(func)
self.parent.cellChanged.connect(func)
def bind(self, func):
def new_func(row):
if row == self.row:
func()
self.change_funcs[func] = new_func
self.parent.cellChanged.connect(new_func)
return self
def unbind(self, func):
if func not in self.change_funcs and func != self._on_change:
raise ValueError("Function is not bound to this Box.")
if func != self._on_change:
self.parent.cellChanged.disconnect(self.change_funcs[func])
self.change_funcs.pop(func)
else:
self.parent.cellChanged.disconnect(self._on_change)
return self
def _on_change(self, row):
if row == self.row:
setattr(self.parent.variables, self.current_name, self.type_func(self.parent.item(self.row, self.col).text()))
def value(self):
return getattr(self.parent.variables, self.current_name)
def set_value(self, value):
if isinstance(value, str):
self.parent.setItem(self.row, self.col, QTableWidgetItem('"' + value + '"'))
else:
self.parent.setItem(self.row, self.col, QTableWidgetItem(str(value)))
def print_val(self):
print(f"{self.current_name} = {self.value()}")
def refresh_type_func(self, value: Any): # Extra function for when type_func must be changed during runtime,
"""Updates the type function so that ``value`` would parse successfully."""
self.type_func = get_type_func(value, self.parent, self.col) # is not explained anywhere
return self
class Button(Box, QPushButton):
"""
Class for the button widget. See :func:`squap.add_button` for initialisation information.
"""
def __init__(self, parent: 'InputTable', name: str, func: Optional[Callable] = None, row: Optional[int] = None):
Box.__init__(self, parent)
QPushButton.__init__(self, parent=parent, text=name)
self.link_funcs = None
row = parent.add_widget(row=row, box_row=(self, ))
self.row = row
parent.setSpan(row, 0, 1, 3)
parent.input_varnames.append(None) # so that indexing still works
parent.setCellWidget(row, 0, self)
if func is not None:
self.bind(func) # is a user provided function, so now we can use bind
self.main_func = func
else:
self.main_func = None
def change_params(self, **kwargs):
"""
Changes the parameters of the button. Accepts ``name`` and ``func`` as keyword arguments.
"""
if "name" in kwargs:
self.setText(kwargs["name"])
if "func" in kwargs:
self.unbind(self.main_func)
self.main_func = kwargs["func"]
self.bind(self.main_func)
def bind(self, func):
self.change_funcs[func] = func # dict is not really necessary for button, but for consistency this is ok.
self.clicked.connect(func)
return self
def unbind(self, func):
if func not in self.change_funcs:
raise ValueError("Function is not bound to this Box.")
self.clicked.disconnect(func)
self.change_funcs.pop(func)
return self
def _on_change(self):
raise ValueError("This function is not defined for a Button")
def set_value(self, value):
raise ValueError("This function is not defined for a Button")
def print_val(self):
raise ValueError("This function is not defined for a Button")
class Dropdown(Box, QComboBox):
"""
Class for the dropwdown widget. See :func:`squap.add_dropdown` for initialisation information.
"""
def __init__(self, parent: 'InputTable', name: str, options: list, init_index: int = 0,
option_names: Optional[Iterable[str]] = None, var_name: Optional[str] = None,
print_value: bool = False, row: Optional[int] = None):
Box.__init__(self, parent)
QComboBox.__init__(self)
self.var_name, self.options, self.option_names = var_name, options, option_names
if option_names is None: # for change_params we need the original value, for this function not.
for option in options:
if not isinstance(option, str):
option_names = [str(option) for option in options]
break
else:
option_names = options
# <editor-fold desc="add_widget and init name&var_name">
if name == "": # if self.textbox will be created
box_row = (self,)
else:
box_row = (self.textbox, self)
row = parent.add_widget(row=row, box_row=box_row)
self.row = row
if name == "": # if no name the first two columns are merged for one cell, but does require var_name
col = 0
parent.setSpan(row, 0, 1, 3)
if var_name is None:
raise ValueError("name can only be empty if var_name is specified") # #1004
else:
col = 1
parent.setSpan(row, 1, 1, 2)
self.textbox = QLabel(name)
parent.setCellWidget(row, 0, self.textbox)
if var_name is None:
var_name = name
parent.input_varnames.append(var_name)
parent.setCellWidget(row, col, self)
# </editor-fold>
self.current_name = var_name
self.addItems(option_names)
self.setCurrentIndex(init_index)
setattr(parent.variables, self.current_name, options[init_index])
self.currentTextChanged.connect(self._on_change)
if print_value:
self.currentTextChanged.connect(self.print_val)
self.printing_val = True
def change_params(self, **kwargs):
"""
Changes the parameters of the dropdown. Only keyword arguments are accepted, and takes all arguments that
:func:`squap.add_dropdown` accepts, except ``init_value`` and ``row``.
"""
turn_on_print_func = False
if "name" in kwargs:
self.textbox.setText(kwargs["name"])
if "var_name" in kwargs:
self.current_name = kwargs["var_name"]
elif self.var_name is not None:
self.current_name = self.var_name
elif "name" in kwargs:
self.current_name = kwargs["name"]
if "print_value" in kwargs:
if not kwargs["print_value"]: # if print_value=False is passed as kwarg
if self.printing_val:
# if print_func already exists (and is not None), remove it and set it to None
self.currentTextChanged.disconnect(self.print_val)
self.printing_val = False
elif not self.printing_val: # or if it does exist but is set to None at the moment
turn_on_print_func = True
if "option_names" in kwargs:
self.option_names = kwargs["option_names"]
for _ in range(len(self.options)):
self.removeItem(0)
self.addItems(self.option_names)
if "options" in kwargs:
if self.option_names is None:
for _ in range(len(self.options)):
self.removeItem(0)
for option in kwargs["options"]:
if not isinstance(option, str):
option_names = [str(option) for option in kwargs["options"]]
break
else:
option_names = kwargs["options"]
self.addItems(option_names)
self.options = kwargs["options"]
if turn_on_print_func:
self.currentTextChanged.connect(self.print_val)
self.printing_val = True
for func in self.change_funcs: # reorders change_funcs so that print_val is first
self.currentTextChanged.disconnect(func)
self.currentTextChanged.connect(func)
def bind(self, func):
# makes sure function is called without argument (removing this breaks examples/all_plot_options.py
def new_func():
func()
self.change_funcs[func] = new_func
self.currentTextChanged.connect(new_func)
return self
def unbind(self, func):
if func not in self.change_funcs and func != self._on_change:
raise ValueError("Function is not bound to this Box.")
if func != self._on_change:
self.currentTextChanged.disconnect(self.change_funcs[func])
self.change_funcs.pop(func)
else:
self.currentTextChanged.disconnect(self._on_change)
return self
def _on_change(self):
setattr(self.parent.variables, self.current_name, self.options[self.currentIndex()])
def value(self):
return getattr(self.parent.variables, self.current_name)
def set_value(self, value):
if self.option_names is None: # for change_params we need the original value, for this function not.
option_names = self.option_names
else:
option_names = self.options
self.setCurrentIndex(option_names.index(value))
def set_index(self, index):
"""Sets the index to ``index``."""
self.setCurrentIndex(index)
def index(self):
"""Returns the current index."""
return self.currentIndex()
def print_val(self):
print(f"{self.current_name} = {self.options[self.currentIndex()]}")
class RateSlider(Box, QSlider):
"""
Class for the rate_slider widget. See :func:`squap.add_rate_slider` for initialisation information.
"""
def __init__(self, parent: 'InputTable', name: str, init_value: float, change_rate: float = 10.0,
absolute: bool = False, time_var: Optional[str] = None, custom_func: Optional[Callable] = None,
var_name: Optional[str] = None, print_value: bool = False, row: Optional[int] = None):
Box.__init__(self, parent)
QSlider.__init__(self)
self.slider = QSlider(Qt.Orientation.Horizontal, parent) # done here so it can be added to parent.boxes
self.actual_change_funcs = [] # for being able to unbind functions
(self.var_name, self.change_rate, self.absolute, self.time_var,
self.custom_func) = var_name, change_rate, absolute, time_var, custom_func
# this bit is a bit different for the rate_slider \/\/
# <editor-fold desc="add_widget and init name&var_name">
if name == "": # if self.textbox will be created
box_row = (self.slider, self)
else:
box_row = (self.textbox, self.slider, self)
row = parent.add_widget(row=row, box_row=box_row)
self.row = row
if name == "": # if no name the first two columns are merged for one cell, but does require var_name
rate_slider_col = 0
self.col = 1
parent.setSpan(row, 0, 1, 2)
if var_name is None:
raise ValueError("name can only be empty if var_name is specified") # #1004
else:
rate_slider_col = 1
self.col = 2
self.textbox = QLabel(name)
parent.setCellWidget(row, 0, self.textbox)
if var_name is None:
var_name = name
parent.input_varnames.append(var_name)
# </editor-fold>
self.current_name = var_name
setattr(parent.variables, self.current_name, init_value)
parent.setCellWidget(self.row, rate_slider_col, self.slider)
self.slider.setRange(0, 200)
self.slider.setValue(100)
self.slider.setStyleSheet("""
QSlider::groove:horizontal {
border: 1px solid #aaa;
width: 50px;
height: 8px;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: #000;
border: 1px solid #000;
width: 14px;
height: 16px;
border-radius: 8px;
margin: -4px 0;
}
""")
self.slider.in_middle = True # if the rate_slider is set to the middle, nothing needs to happen
# so if this is set to False, it doesn't run the stuff, so when the rate_slider is released, the impact on
# performance is minimal
def release_func(): # resets rate_slider to 0 when released
self.slider.setValue(100)
self.slider.in_middle = True
def press_func():
self.timer = get_t()
self.slider.in_middle = False
self.slider.sliderReleased.connect(release_func)
self.slider.sliderPressed.connect(press_func)
parent.setItem(self.row, self.col, QTableWidgetItem(str(textify(init_value))))
setattr(parent.variables, self.current_name, init_value)
self.parent.cellChanged.connect(self._on_change)
# <editor-fold desc="create rate_slider update func">
if time_var is None:
self.timer = current_time()
def get_t():
return current_time()
else:
def get_t():
return getattr(parent.variables, time_var)
self.get_t = get_t
if custom_func is None:
if absolute:
def new_calc(dt,
slider_value): # slider_value is a value between -1 & 1 depending on slider position
setattr(parent.variables, self.current_name,
getattr(parent.variables, self.current_name) + self.change_rate * slider_value * dt)
else:
# scales logarithmically, so every dt*slider_value = 1 var gets multiplied by changerate
def new_calc(dt, slider_value):
setattr(parent.variables, self.current_name,
getattr(parent.variables, self.current_name) * self.change_rate ** (dt * slider_value))
else:
def new_calc(dt, slider_value):
setattr(parent.variables, self.current_name,
custom_func(getattr(parent.variables, self.current_name), dt, slider_value))
self.new_calc = new_calc
def update_func():
if not self.slider.in_middle:
old_t = self.timer
self.timer = self.get_t()
dt = self.timer - old_t
slider_value = (self.slider.sliderPosition() - 100) / 100
# 6 is arbitrary, it's how changerate scales with slider_val, linear sucks
self.new_calc(dt, np.sign(slider_value) * (np.exp(np.abs(slider_value) * 6) - 1) / np.exp(6))
val = getattr(parent.variables, var_name)
parent.setItem(self.row, self.col, QTableWidgetItem(textify(val)))
# </editor-fold>
self.parent.update_funcs.append(update_func)
if print_value:
def print_func(row, col):
if row == self.row and col == self.col:
self.print_val()
self.parent.cellChanged.connect(print_func)
self.printing_val = True
def change_params(self, **kwargs):
"""
Changes the parameters of the rate_slider. Only keyword arguments are accepted, and takes all arguments that
:func:`squap.add_rate_slider` accepts, except ``init_value`` and ``row``.
"""
turn_on_print_func = False
update_new_calc = False
if "name" in kwargs:
self.textbox.setText(kwargs["name"])
if "var_name" in kwargs:
self.current_name = kwargs["var_name"]
elif self.var_name is not None:
self.current_name = self.var_name
elif "name" in kwargs:
self.current_name = kwargs["name"]
if "print_value" in kwargs:
if not kwargs["print_value"]: # if print_value=False is passed as kwarg
if self.printing_val:
# if print_func already exists (and is not None), remove it and set it to None
self.parent.cellChanged.disconnect(self.print_val)
self.printing_val = False
elif not self.printing_val: # or if it does exist but is set to None at the moment
turn_on_print_func = True
if "time_var" in kwargs:
if kwargs["time_var"] != self.time_var:
if kwargs["time_var"] is None and self.time_var is not None:
self.timer = current_time()
def get_t():
return current_time()
self.get_t = get_t
elif kwargs["time_var"] is not None and self.time_var is None:
def get_t():
return getattr(self.parent.variables, self.time_var)
self.get_t = get_t
self.time_var = kwargs["time_var"]
if "change_rate" in kwargs:
self.change_rate = kwargs["change_rate"]
if "absolute" in kwargs:
self.absolute = kwargs["absolute"]
update_new_calc = True
if "custom_func" in kwargs:
self.custom_func = kwargs["custom_func"]
update_new_calc = True
if update_new_calc:
if self.custom_func is None:
if self.absolute:
def new_calc(dt,
slider_value): # slider_value is a value between -1 & 1 depending on slider position
setattr(self.parent.variables, self.current_name,
getattr(self.parent.variables, self.current_name)
+ self.change_rate * slider_value * dt)
else:
# scales logarithmically, so every dt*slider_value = 1 var gets multiplied by changerate
def new_calc(dt, slider_value):
setattr(self.parent.variables, self.current_name,
getattr(self.parent.variables, self.current_name) * self.change_rate ** (
dt * slider_value))
else:
def new_calc(dt, slider_value):
setattr(self.parent.variables, self.current_name,
self.custom_func(getattr(self.parent.variables, self.current_name), dt, slider_value))
self.new_calc = new_calc
if turn_on_print_func:
self.parent.cellChanged.connect(self.print_val)
self.printing_val = True
for func in self.change_funcs: # reorders change_funcs so that print_val is first
self.parent.cellChanged.disconnect(func)
self.parent.cellChanged.connect(func)
def bind(self, func):
def new_func(row, col):
if row == self.row and col == self.col:
func()
self.change_funcs[func] = new_func
self.parent.cellChanged.connect(new_func)
return self
def unbind(self, func):
if func not in self.change_funcs and func != self._on_change:
raise ValueError("Function is not bound to this Box.")
if func != self._on_change:
self.parent.cellChanged.disconnect(self.change_funcs[func])
self.change_funcs.pop(func)
else:
self.parent.cellChanged.disconnect(self._on_change)
return self
def _on_change(self, row, col):
if row == self.row and col == self.col:
setattr(self.parent.variables, self.current_name, float(self.parent.item(self.row, self.col).text()))
def value(self):
return getattr(self.parent.variables, self.current_name)
def set_value(self, value):
setattr(self.parent.variables, self.current_name, value)
self.parent.cellChanged.disconnect(self._on_change)
self.parent.setItem(self.row, self.col, QTableWidgetItem(str(textify(value))))
self.parent.cellChanged.connect(self._on_change)
def print_val(self):
print(f"{self.current_name} = {self.value()}")
class ColorPicker(Box, ColorButton):
"""
Class for the color_picker widget. See :func:`squap.add_color_picker` for initialisation information.
"""
def __init__(self, parent: 'InputTable', name: str, init_value: ColorType, var_name: Optional[str] = None,
print_value: bool = False, row: Optional[int] = None):
Box.__init__(self, parent)
ColorButton.__init__(self)
self.var_name = var_name
# this bit is the same for most widgets:
# <editor-fold desc="add_widget and init name&var_name">
if name == "": # if self.textbox will be created
box_row = (self,)
else:
box_row = (self.textbox, self)
row = parent.add_widget(row=row, box_row=box_row)
self.row = row
if name == "": # if no name the first two columns are merged for one cell, but does require var_name
col = 0
parent.setSpan(row, 0, 1, 3)
if var_name is None:
raise ValueError("name can only be empty if var_name is specified") # #1004
else:
col = 1
parent.setSpan(row, 1, 1, 2)
self.textbox = QLabel(name)
parent.setCellWidget(row, 0, self.textbox)
if var_name is None:
var_name = name
parent.input_varnames.append(var_name)
parent.setCellWidget(row, col, self)
# </editor-fold>
self.current_name = var_name
if init_value is None:
self.set_value((0, 0, 0, 0))
else:
self.set_value(init_value)
setattr(parent.variables, self.current_name, init_value)
self.sigColorChanged.connect(self._on_change)
if print_value:
self.sigColorChanged.connect(self.print_val)
self.printing_val = True
def change_params(self, **kwargs):
"""
Changes the parameters of the color_picker. Only keyword arguments are accepted, and takes all arguments
that :func:`squap.add_color_picker` accepts, except ``init_value`` and ``row``.
"""
turn_on_print_func = False
if "name" in kwargs:
self.textbox.setText(kwargs["name"])
if "var_name" in kwargs:
self.current_name = kwargs["var_name"]
elif self.var_name is not None:
self.current_name = self.var_name
elif "name" in kwargs:
self.current_name = kwargs["name"]
if "print_value" in kwargs:
if not kwargs["print_value"]: # if print_value=False is passed as kwarg
if self.printing_val:
# if print_func already exists (and is not None), remove it and set it to None
self.parent.cellChanged.disconnect(self.print_val)
self.printing_val = False
elif not self.printing_val: # or if it does exist but is set to None at the moment
turn_on_print_func = True
if turn_on_print_func:
def print_func(row):
if row == self.row:
self.print_val()
self.parent.cellChanged.connect(print_func)
self.printing_val = True
for func in self.change_funcs: # reorders change_funcs so that print_val is first
self.parent.cellChanged.disconnect(func)
self.parent.cellChanged.connect(func)
def bind(self, func):
self.change_funcs[func] = func # dict is not really necessary for this box, but not a problem either
self.sigColorChanged.connect(func)
return self
def unbind(self, func):
if func not in self.change_funcs and func != self._on_change:
raise ValueError("Function is not bound to this Box.")
if func != self._on_change:
self.sigColorChanged.disconnect(self.change_funcs[func])
self.change_funcs.pop(func)
else:
self.sigColorChanged.disconnect(self._on_change)
return self
def _on_change(self, *args):
setattr(self.parent.variables, self.current_name, self.value())
def set_value(self, value: ColorType):
self.setColor(get_single_color(value))
def value(self):
return self.color(mode="float")
def print_val(self):
print(f"{self.current_name} = {self.value()}")