Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
os: [windows-latest, macos-latest]
python-version: ['3.8', '3.9', '3.10', '3.11']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v2
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Remember last save location (issue 136)
- Create a grid layout / matrix box as option (issue 102)
- Move within this matrix box with the arrows (issue 101)
- Use insert in FloatBox (issue 103)
- Value Hint (issue 151)

## Fixed
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ This can be found on [ScenarioGUI.readthedocs.io](https://scenariogui.readthedoc


## Requirements
This code is tested with Python 3.10 and 3.11 and requires the following libraries (the versions mentioned are the ones with which the code is tested)
This code is tested with Python 3.8 to 3.11 and requires the following libraries (the versions mentioned are the ones with which the code is tested)

* PySide6>=6.4.1
* PySide6>=6.5.3
* matplotlib>=3.5.2
* numpy>=1.23.1
* pandas>=1.4.3
Expand All @@ -34,7 +34,6 @@ For the tests
* pytest-cov>=3.0.0
* pytest-timeout>=2.1.0
* pytest-qt>=4.1.0
* keyboard>=0.13.5

## Quick start
### Installation
Expand Down
106 changes: 76 additions & 30 deletions ScenarioGUI/gui_classes/gui_structure_classes/float_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import PySide6.QtCore as QtC # type: ignore
import PySide6.QtGui as QtG
import PySide6.QtWidgets as QtW # type: ignore
import numpy as np

import ScenarioGUI.global_settings as globs

Expand Down Expand Up @@ -46,11 +47,56 @@ def validate(self, float_str: str, pos: int) -> object:
-------
object
"""
# position is index +1 in input
nb_of_chars = len(float_str) - 1 if "," in float_str else 0
nb_of_decimals = 0 if "," not in float_str else nb_of_chars - float_str.index(",")
# get sign and decimal symbols
decimal_sign = self.locale().decimalPoint()
sep_sign = self.locale().groupSeparator()
has_sep = sep_sign in float_str
len_bef = float_str[:pos].count(sep_sign)
if pos > len(float_str) or float_str == "":
return QtW.QDoubleSpinBox.validate(self, float_str, pos)
is_number = not (float_str[pos-1] in [sep_sign, decimal_sign])
has_no_decimal_sign = decimal_sign not in float_str and self.decimals() > 0
if has_no_decimal_sign:
float_str += decimal_sign
# move the curser to the next number if the current one is not
# float_str = float_str.replace(sep_sign, "")
nb_of_chars = len(float_str) - 1 if decimal_sign in float_str else 0
nb_of_decimals = 0 if decimal_sign not in float_str else nb_of_chars - float_str.index(decimal_sign)
# overwrite decimals
float_str = float_str[:-1] if nb_of_decimals > self.decimals() and pos != nb_of_chars + 1 else float_str
if nb_of_decimals > self.decimals() and pos != nb_of_chars + 1:
float_str = float_str[:-1]
# move values if the current one is above the maximum
limit_reached: bool = False
if self.maximum() > 1 or self.minimum() < -1:
float_str = float_str.replace(sep_sign, "")
strings = float_str.split(decimal_sign)
strings[0] = "0" if strings[0] == "" else strings[0]
strings[0] = "-0" if strings[0] == "-" else strings[0]
new_float_str = f"{strings[0]}.{strings[1]}" if len(strings) > 1 else strings[0]
limit_reached = (float(new_float_str) > self.maximum() and float(new_float_str) > 0) or float(new_float_str) < self.minimum()
if limit_reached:
float_str = f"{strings[0][:-1]}{decimal_sign}{strings[0][-1]}{strings[1][:-1]}" if len(strings) > 1 else strings[0][:-1]
if has_sep:
minus = float_str[0] == "-"
if minus:
float_str = float_str[1:]
dec_idx = float_str.index(decimal_sign) if self.decimals() > 0 and float_str.index(decimal_sign) > 2 else len(float_str)
# Initialize an empty result string
result_string = ""
# Iterate through the original string in reverse
for i, char in enumerate(reversed(float_str[: dec_idx])):
# Check if it's time to insert the symbol
if i > 0 and i % 3 == 0:
result_string = sep_sign + result_string # Insert the symbol
result_string = char + result_string # Add the character
float_str = result_string + float_str[dec_idx:]
if minus:
float_str = f"-{float_str}"

pos = (pos + 1) if limit_reached and is_number and len(float_str) > pos and float_str[pos-1] == decimal_sign else pos
pos = (pos + 1) if len_bef < float_str[:pos].count(sep_sign) else pos
pos = (pos - 1) if len_bef > float_str[:pos].count(sep_sign) else pos
float_str = float_str[:-1] if has_no_decimal_sign else float_str
return QtW.QDoubleSpinBox.validate(self, float_str, pos)


Expand All @@ -61,15 +107,15 @@ class FloatBox(Option):
"""

def __init__( # noqa: PLR0913
self,
label: str | list[str],
default_value: float,
category: Category,
*,
decimal_number: int = 0,
minimal_value: float = 0.0,
maximal_value: float = 100.0,
step: float = 1.0,
self,
label: str | list[str],
default_value: float,
category: Category,
*,
decimal_number: int = 0,
minimal_value: float = 0.0,
maximal_value: float = 100.0,
step: float = 1.0,
):
"""

Expand Down Expand Up @@ -153,10 +199,10 @@ def _check_value(self) -> bool:
return self.minimal_value <= self.get_value() <= self.maximal_value

def add_link_2_show(
self,
option: Option | Category | FunctionButton | Hint,
below: float = None,
above: float = None,
self,
option: Option | Category | FunctionButton | Hint,
below: float = None,
above: float = None,
) -> None:
"""
This function couples the visibility of an option to the value of the FloatBox object.
Expand Down Expand Up @@ -186,11 +232,11 @@ def add_link_2_show(
check_conditional_visibility(option)

def show_option(
self,
option: Option | Category | FunctionButton | Hint,
below: float | None,
above: float | None,
args=None,
self,
option: Option | Category | FunctionButton | Hint,
below: float | None,
above: float | None,
args=None,
):
"""
This function shows the option if the value of the FloatBox is between the below and above value.
Expand Down Expand Up @@ -261,12 +307,12 @@ def create_function_2_check_linked_value(self, value: tuple[float | None, float
return ft_partial(self.check_linked_value, value, value_if_hidden)

def create_widget(
self,
frame: QtW.QFrame,
layout_parent: QtW.QLayout,
*,
row: int | None = None,
column: int | None = None,
self,
frame: QtW.QFrame,
layout_parent: QtW.QLayout,
*,
row: int | None = None,
column: int | None = None,
) -> None:
"""
This functions creates the FloatBox widget in the frame.
Expand All @@ -291,10 +337,10 @@ def create_widget(
layout = self.create_frame(frame, layout_parent)
self.widget.setParent(self.frame)
self.widget.setStyleSheet(
f'QDoubleSpinBox{"{"}selection-color: {globs.WHITE};selection-background-color: {globs.LIGHT};' f'border: 1px solid {globs.WHITE};{"}"}'
f'QDoubleSpinBox{"{"}selection-color: {globs.WHITE};selection-background-color: {globs.LIGHT};border: 1px solid {globs.WHITE};{"}"}'
)
self.widget.setAlignment(QtC.Qt.AlignRight | QtC.Qt.AlignTrailing | QtC.Qt.AlignVCenter)
self.widget.setProperty("showGroupSeparator", True)
self.widget.setGroupSeparatorShown(True)
self.widget.setMinimum(self.minimal_value)
self.widget.setMaximum(self.maximal_value)
self.widget.setDecimals(self.decimal_number)
Expand Down
17 changes: 6 additions & 11 deletions ScenarioGUI/gui_classes/gui_structure_classes/int_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import PySide6.QtWidgets as QtW # type: ignore

import ScenarioGUI.global_settings as globs
from .float_box import DoubleSpinBox

from ...utils import set_default_font
from .functions import check_and_set_max_min_values, check_conditional_visibility
Expand All @@ -25,14 +26,6 @@
from .hint import Hint


class SpinBox(QtW.QSpinBox): # pragma: no cover
def wheelEvent(self, event: QtG.QWheelEvent):
if self.hasFocus():
super().wheelEvent(event)
return
self.parent().wheelEvent(event)


class IntBox(Option):
"""
This class contains all the functionalities of the IntBox (integer box) option in the GUI.
Expand Down Expand Up @@ -85,7 +78,8 @@ def __init__(
self.minimal_value: int = minimal_value
self.maximal_value: int = maximal_value
self.step: int = step
self.widget: SpinBox = SpinBox(self.default_parent, valueChanged=self.valueChanged.emit)
self.widget: DoubleSpinBox = DoubleSpinBox(self.default_parent, valueChanged=self.valueChanged.emit)
self.widget.setDecimals(0)

def get_value(self) -> int:
"""
Expand All @@ -96,7 +90,7 @@ def get_value(self) -> int:
int
Value of the IntBox
"""
return self.widget.value()
return int(self.widget.value())

def set_value(self, value: int) -> None:
"""
Expand Down Expand Up @@ -265,9 +259,10 @@ def create_widget(
layout = self.create_frame(frame, layout_parent)
self.widget.setParent(self.frame)
self.widget.setStyleSheet(
f'QSpinBox{"{"}selection-color: {globs.WHITE};selection-background-color: {globs.LIGHT};border: 1px solid {globs.WHITE};{"}"}'
f'QDoubleSpinBox{"{"}selection-color: {globs.WHITE};selection-background-color: {globs.LIGHT};border: 1px solid {globs.WHITE};{"}"}'
)
self.widget.setAlignment(QtC.Qt.AlignRight | QtC.Qt.AlignTrailing | QtC.Qt.AlignVCenter)
self.widget.setGroupSeparatorShown(True)
self.widget.setMinimum(self.minimal_value)
self.widget.setMaximum(self.maximal_value)
self.widget.setValue(self.default_value)
Expand Down
18 changes: 6 additions & 12 deletions ScenarioGUI/gui_classes/gui_structure_classes/matrix_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import numpy as np

import ScenarioGUI.global_settings as globs
from .float_box import DoubleSpinBox
from .functions import check_and_set_max_min_values, check_conditional_visibility

from ...utils import set_default_font
Expand All @@ -23,7 +24,7 @@
from .hint import Hint


class DoubleSpinBox(QtW.QDoubleSpinBox): # pragma: no cover
class ArrowDoubleSpinBox(DoubleSpinBox): # pragma: no cover
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.up = self
Expand All @@ -32,7 +33,7 @@ def __init__(self, *args, **kwargs):
self.right = self
self.setButtonSymbols(QtW.QDoubleSpinBox.NoButtons)

def set_boxes(self, up: DoubleSpinBox, down: DoubleSpinBox, left: DoubleSpinBox, right: DoubleSpinBox):
def set_boxes(self, up: ArrowDoubleSpinBox, down: ArrowDoubleSpinBox, left: ArrowDoubleSpinBox, right: ArrowDoubleSpinBox):
self.up = up
self.down = down
self.left = left
Expand All @@ -54,13 +55,6 @@ def keyPressEvent(self, event):
else:
super().keyPressEvent(event)

def wheelEvent(self, event: QtG.QWheelEvent):
if self.hasFocus():
super().wheelEvent(event)
return
self.parent().wheelEvent(event)


class MatrixBox(Option):
"""
This class contains all the functionalities of the MatrixBox option in the GUI.
Expand Down Expand Up @@ -118,8 +112,8 @@ def __init__(
self.maximal_value: list[list[float]] = ([[maximal_value] * column] * row) if isinstance(maximal_value, (float, int)) else maximal_value
self.column = column
self.row = row
self.widget: list[list[DoubleSpinBox]] = [
[DoubleSpinBox(self.default_parent, valueChanged=self.valueChanged.emit) for _ in range(column)] for _ in range(row)
self.widget: list[list[ArrowDoubleSpinBox]] = [
[ArrowDoubleSpinBox(self.default_parent, valueChanged=self.valueChanged.emit) for _ in range(column)] for _ in range(row)
]

def get_value(self) -> list[list[float]]:
Expand Down Expand Up @@ -361,7 +355,7 @@ def create_widget(
f'QDoubleSpinBox{"{"}selection-color: {globs.WHITE};selection-background-color: {globs.LIGHT};' f'border: 1px solid {globs.WHITE};{"}"}'
)
widget.setAlignment(QtC.Qt.AlignRight | QtC.Qt.AlignTrailing | QtC.Qt.AlignVCenter)
widget.setProperty("showGroupSeparator", True)
widget.setGroupSeparatorShown(True)
widget.setMinimum(self.minimal_value[row][column])
widget.setMaximum(self.maximal_value[row][column])
widget.setDecimals(self.decimal_number[row][column])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from ...utils import set_default_font
from .functions import check_conditional_visibility
from .int_box import SpinBox
from .int_box import DoubleSpinBox
from .option import Option

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -78,7 +78,8 @@ def __init__( # noqa: PLR0913
self.minimal_value: list[int] = [minimal_value for _ in default_value] if not isinstance(minimal_value, Iterable) else minimal_value
self.maximal_value: list[int] = [maximal_value for _ in default_value] if not isinstance(maximal_value, Iterable) else maximal_value
self.step: list[int] = [step for _ in default_value] if not isinstance(step, Iterable) else step
self.widget: list[SpinBox] = [SpinBox(self.default_parent, valueChanged=self.valueChanged.emit) for _ in default_value]
self.widget: list[DoubleSpinBox] = [DoubleSpinBox(self.default_parent, valueChanged=self.valueChanged.emit) for _ in default_value]
_ = [wid.setDecimals(0) for wid in self.widget]

def get_value(self) -> tuple[int]:
"""
Expand Down Expand Up @@ -260,7 +261,7 @@ def create_widget(
for widget, max_val, min_val, step, def_val in zip(self.widget, self.maximal_value, self.minimal_value, self.step, self.default_value):
widget.setParent(self.frame)
widget.setStyleSheet(
f'QSpinBox{"{"}selection-color: {globs.WHITE};selection-background-color: {globs.LIGHT};' f'border: 1px solid {globs.WHITE};{"}"}'
f'QDoubleSpinBox{"{"}selection-color: {globs.WHITE};selection-background-color: {globs.LIGHT};' f'border: 1px solid {globs.WHITE};{"}"}'
)
widget.setAlignment(QtC.Qt.AlignRight | QtC.Qt.AlignTrailing | QtC.Qt.AlignVCenter)
widget.setMinimum(min_val)
Expand Down
15 changes: 12 additions & 3 deletions examples/example_classes/gui_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(self, default_parent: QtW.QWidget, translations: Translations):
label="a",
default_value=2,
minimal_value=0,
maximal_value=200,
maximal_value=30000,
category=self.category_inputs,
)
self.int_a.set_tool_tip("This is an explanation\nfor the value a")
Expand Down Expand Up @@ -112,7 +112,16 @@ def __init__(self, default_parent: QtW.QWidget, translations: Translations):
label=["b", "b"],
default_value=100,
minimal_value=0,
maximal_value=1000,
maximal_value=100_000,
decimal_number=2,
category=self.sub_category,
)

self.negative_float = els.FloatBox(
label="negative_float",
default_value=-1000,
minimal_value=-100_000,
maximal_value=-100,
decimal_number=2,
category=self.sub_category,
)
Expand Down Expand Up @@ -218,7 +227,7 @@ def __init__(self, default_parent: QtW.QWidget, translations: Translations):
entries=["entry 1", "entry 2", "entry 3"],
)
self.hint_flex = els.Hint(
hint="wrong length of flexible option",
hint=["wrong length of flexible option", "Falscher Wert in flexible option"],
category=self.category_inputs,
warning=True,
)
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
matplotlib>=3.5.2
numpy>=1.23.1
PySide6>=6.5.2
PySide6>=6.6.0
pandas>=1.4.3
black>=23.1.0
5 changes: 3 additions & 2 deletions requirements_docs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ matplotlib>=3.5.2
numpy>=1.23.1
pandas>=1.4.3
pygfunction>=2.2.1
PySide6>=6.3.1
PySide6>=6.5.3
scipy>=1.8.1
numpydoc >= 1.2.0
sphinx_design >= 0.3.0
sphinx_design >= 0.3.0
sphinx-rtd-theme>=1.3.0
Loading