Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add QLineEdit support to UISliderWidget #168

Open
wants to merge 70 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
3423fb0
Rewrite UISliderWidget class to take a QDoubleSpinBox as input
jcornall Dec 16, 2024
5c8dca9
Revert changes to dialog_example_3_save_default.py
jcornall Dec 16, 2024
fe05eae
Add dialog_example_uislider.py
jcornall Dec 16, 2024
ca10630
Update CHANGELOG.md
jcornall Dec 16, 2024
2de3c5b
Add minimum and maximum value parameters and logic to UISliderWidget.py
jcornall Dec 18, 2024
c3f39dc
Update CHANGELOG.md
jcornall Dec 18, 2024
68e58ea
Add UISliderLineEditWidget class, update comments
jcornall Dec 18, 2024
d86f1f7
Replace QDoubleSpinBox in UISliderWidget class with QLineEdit
jcornall Dec 19, 2024
d255d49
Replace QDoubleSpinBox in UISliderLEditWidget class with QLineEdit
jcornall Dec 19, 2024
9916abe
Merge branch 'main' into uislider-dspinbox
jcornall Dec 20, 2024
e93a971
Update CHANGELOG.md
jcornall Dec 20, 2024
a375e9b
Add QLabel to UISliderWidget to display max slider value
jcornall Dec 20, 2024
1313217
Add tick_interval attribute to UISliderWidget class
jcornall Dec 23, 2024
f7517ab
Reformat method names to use camel case, add value getters/setters
jcornall Jan 2, 2025
5c595bf
Add UISliderLEditWidget case to getWidgetState() and applyWidgetState()
jcornall Jan 2, 2025
1545425
Update examples and tests to include UISliderLEditWidget
jcornall Jan 3, 2025
0545d59
Update tests and change defaults for UISlider classes
jcornall Jan 3, 2025
9e6f42b
Replace all instances of UISliderLEditWidget with UISliderEditWidget,
jcornall Jan 3, 2025
c4c9faf
Update CHANGELOG.md
jcornall Jan 3, 2025
a767b0d
Add float support to UISliderWidget value getter and setter
jcornall Jan 3, 2025
8136350
Connect QApplication focusChanged signal to UISlider QLineEdits
jcornall Jan 6, 2025
80e3f6a
Add additional UISliderEditWidget tests
jcornall Jan 7, 2025
9add01a
Merge branch 'main' into uislider-dspinbox
jcornall Jan 8, 2025
d76d8a3
Replace Pyside2 imports with pyqt
jcornall Jan 8, 2025
0d7be4f
Update UISliderWidget, remove UISliderEditWidget, implement scaling
jcornall Jan 9, 2025
c0fa449
Add minimum/maximum argument check to __init__()
jcornall Jan 9, 2025
9f347ac
Correct UISliderWidget minimum/maximum values
jcornall Jan 9, 2025
3114d02
Correct formatting of docstrings
jcornall Jan 10, 2025
162d9a0
Add UISlider tests
jcornall Jan 17, 2025
da49b61
Add checks for non-default UISlider inputs
jcornall Jan 17, 2025
72c14db
Replace references to Pyside2 with qtpy
jcornall Jan 17, 2025
a88d5df
Add parameterized to list of optional dependencies
jcornall Jan 17, 2025
8c6de56
Remove setUp()
jcornall Jan 17, 2025
706db47
Remove parameterized from optional dependencies, add skip_ci decorator
jcornall Jan 17, 2025
150fdbf
Add parameterized to optional dependencies
jcornall Jan 17, 2025
a975d7f
Add returnPressed signal to UISliderWidget
jcornall Jan 17, 2025
112eecf
Fix error when QLineEdit return an empty string
jcornall Jan 17, 2025
e0141c1
Modify UISliderWidget default number_of_ticks, add _updateSlider() call
jcornall Jan 17, 2025
737f93c
Correct recursive method call in setValue()
jcornall Jan 17, 2025
df480fc
Modify UISliderWidget default number_of_ticks
jcornall Jan 17, 2025
b25d151
Correct expected value in parameterized unit test
jcornall Jan 20, 2025
c3431fd
Add additional docstrings to UISliderWidget methods and tests
jcornall Jan 21, 2025
b062bab
Add additional parameterized test cases
jcornall Jan 22, 2025
edd28e0
Update docstrings
jcornall Jan 22, 2025
f7b5d05
Update UISliderWidget docstrings, methods and tests
jcornall Jan 23, 2025
8bf7f3e
Add additional UISliderWidget methods, update tests, docstrings and
jcornall Jan 27, 2025
1775e94
Add UISlider to insert_widgets_examply.py, fix onCancel() behaviour
jcornall Jan 27, 2025
e9e4eab
Update docstrings
jcornall Jan 28, 2025
88014e9
Add UISliderWidget to remove_widgets_example.py, update docstrings
jcornall Jan 29, 2025
1432a80
Modify setValue() to also update the value of the UISlider's QSlider
jcornall Jan 29, 2025
bdee17a
Remove skip_ci decorator from TestUISliderWidget, update recipe
jcornall Jan 29, 2025
4053cde
Modify tearDown() to quit the current QApplication instance after each
jcornall Jan 29, 2025
546aa1b
Patch QApplication in TestUISliderWidget tests
jcornall Jan 29, 2025
fbd0c7c
Modify QApplication patch
jcornall Jan 29, 2025
94fdb7f
Isolate QApplication dependency within UISliderWidget by adding
jcornall Jan 29, 2025
4521810
Add pyqt to qtbindings
jcornall Jan 29, 2025
1c83b6e
Correct 'pyqt' to 'qtpy' in qtbindings
jcornall Jan 29, 2025
3b641d3
Remove qtpy from qtbindings
jcornall Jan 29, 2025
96cb86f
Test adding QApplication to setUp() and tearDown() methods
jcornall Jan 29, 2025
ae3fe75
Remove QApplication from setUp() and tearDown() methods
jcornall Jan 29, 2025
e1f7199
Test empty qtbindings
jcornall Jan 29, 2025
3a999a0
Test adding qtbindings
jcornall Jan 29, 2025
43c2f31
Move pip install of qtbindings
jcornall Jan 29, 2025
604bc08
Reset changes to test.yml
jcornall Jan 29, 2025
edc832a
Add @skip_ci decorator to TestUISliderWidget class
jcornall Jan 30, 2025
a0a074d
Test bare minimum UISlider object instantiation with GitHub Actions
jcornall Jan 30, 2025
3497399
Update testing information in CONTRIBUTING.md
jcornall Jan 30, 2025
d535b10
Update CHANGELOG.md
jcornall Jan 30, 2025
30a8fb0
Add @skip_ci decorator to TestUISliderWidget class
jcornall Jan 30, 2025
11f0ae5
Update CHANGELOG.md, remove redundant test, correct docstring
jcornall Jan 31, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Refactor `UISliderWidget` class to support `QLineEdit` and layouts (#168)
jcornall marked this conversation as resolved.
Show resolved Hide resolved
+ Breaks backwards compatability as `UISliderWidget` no longer accepts a `QLabel`
- Update existing `FormDialog` tests and add new tests for `UISliderWidget`(#168)
jcornall marked this conversation as resolved.
Show resolved Hide resolved
- Update existing examples to demonstrate the `UISliderWidget` (#168)

# Version 1.0.2
- Upgrade python to 3.8 in `test.yml` (#171)
Expand Down
13 changes: 11 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ mamba activate eqt_env

5. Install the dependencies:
```sh
# Install test dependencies
pip install .[dev]
```
The following developer-specific dependencies will be installed:
- pytest
- pytest-cov
- pytest-timeout
- unittest_parametrize

### Merge the `main` Branch
Conflicts may exist if your branch is behind the `main` branch. To resolve conflicts between branches, merge the `main` branch into your current working branch:
Expand All @@ -63,6 +67,8 @@ Before merging a pull request, all tests must pass. These can be run locally fro
```sh
pytest
```
> [!NOTE]
> For files that test the GUI elements, the `@skip_ci` decorator has been included to skip these tests when the GitHub Actions are executed after pushing/merging. Without the decorator, these GUI test files will cause the `pytest` GitHub Action to fail.

### Install and Run `pre-commit`
Adhere to our styling guide by installing [`pre-commit`](https://pre-commit.com) in your local eqt environment:
Expand All @@ -85,6 +91,8 @@ The [`.pre-commit-config.yaml`](./.pre-commit-config.yaml) config file indicates

## Continuous Integration
GitHub Actions automatically runs a subset of the unit tests on every commit via [`test.yml`](.github/workflows/test.yml).
> [!NOTE]
> GitHub Actions does not currently support unit tests that test GUI elements. These tests should include the `@skip_ci` decorator so that they are skipped when the GitHub Actions are executed.

### Testing

Expand All @@ -102,7 +110,8 @@ Runs automatically -- when an annotated tag is pushed -- after builds (above) su

Publishes to [PyPI](https://pypi.org/project/eqt).

:warning: The annotated tag's `title` must be `Version <number without v-prefix>` (separated by a blank line) and the `body` must contain release notes, e.g.:
> [!WARNING]
> The annotated tag's `title` must be `Version <number without v-prefix>` (separated by a blank line) and the `body` must contain release notes, e.g.:

```sh
git tag v1.33.7 -a
Expand Down
308 changes: 171 additions & 137 deletions eqt/ui/UISliderWidget.py

Large diffs are not rendered by default.

57 changes: 33 additions & 24 deletions examples/dialog_example_uislider.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,55 @@

class MainUI(QtWidgets.QMainWindow):
def __init__(self, parent=None):
'''Creates a QMainWindow and adds a QPushButton. Pressing the button opens a FormDialog
containing a UISliderWidget example. The UISliderWidget is connected to a method
that prints the current value of it's QSlider, QLineEdit, and the value of the
UISliderWidget itself (i.e. the value returned when it's value() method is called).
'''
QtWidgets.QMainWindow.__init__(self, parent)

pb = QtWidgets.QPushButton(self)
pb.setText("Open Dialog with form layout")
pb.clicked.connect(lambda: self.openFormDialog())
form_dialog_button = QtWidgets.QPushButton(self)
form_dialog_button.setText("Open FormDialog")
form_dialog_button.clicked.connect(lambda: self._openFormDialog())

layout = QtWidgets.QHBoxLayout()
layout.addWidget(pb)
layout.addWidget(form_dialog_button)
widg = QtWidgets.QWidget()
widg.setLayout(layout)

self.setCentralWidget(widg)

self.show()

def openFormDialog(self):
dialog = FormDialog(parent=self, title='Example')
dialog.Ok.clicked.connect(lambda: self.accepted())

# Create UISliderWidget
def _openFormDialog(self):
'''Creates a FormDialog and adds a UISliderWidget. Connects signals from the widget's
QSlider and QLineEdit to a method than prints the UISliderWidget's values. The values
will be printed when either; the QSlider is released, or the QLineEdit is edited.
Displays the FormDialog.
'''
dialog = FormDialog(parent=self, title='UISliderWidget Example')
uislider = UISliderWidget.UISliderWidget(minimum=-0.5, maximum=0.5, decimals=10,
number_of_steps=10, number_of_ticks=10)

# add to the form widget
number_of_steps=10, number_of_ticks=10,
is_application=True)
dialog.addWidget(uislider, 'UISlider:', 'input_slider')

# store a reference
dialog.widgets['input_slider_field'].slider.sliderReleased.connect(
lambda: self._printValues())
dialog.widgets['input_slider_field'].line_edit.editingFinished.connect(
lambda: self._printValues())

self.dialog = dialog
self.dialog.onCancel = self.rejected
dialog.exec()

def accepted(self):
print("accepted")
print(f"UISlider QSlider: {self.dialog.widgets['input_slider_field']._getSliderValue()}")
print(f"UISlider QLineEdit: {self.dialog.widgets['input_slider_field'].value()}")

self.dialog.close()

def rejected(self):
print("rejected")
def _printValues(self):
'''Prints the values of the QSlider, QLineEdit and the UISliderWidget itself.
Also prints the type of each value.
'''
slider_value = self.dialog.widgets['input_slider_field']._getQSliderValue()
line_edit_value = self.dialog.widgets['input_slider_field']._getQLineEditValue()
uislider_value = self.dialog.widgets['input_slider_field'].value()
print(f"QSlider Value: {slider_value} {type(slider_value)}\n" +
f"QLineEdit Value: {line_edit_value} {type(line_edit_value)}\n" +
f"UISliderWidget Value: {uislider_value} {type(uislider_value)}\n")


if __name__ == "__main__":
Expand Down
166 changes: 112 additions & 54 deletions examples/insert_widgets_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,57 @@

from qtpy import QtWidgets

from eqt.ui import FormDialog, UIFormWidget
from eqt.ui import FormDialog, UIFormWidget, UISliderWidget


class MainUI(QtWidgets.QMainWindow):
def __init__(self, parent=None):
'''Creates a QMainWindow and adds a FormDockWidget, a FormDialog, and a QPushButton.
Opening the FormDockWidget will show the form in a separate window, and clicking
on the QPushButton will also display a form separately.

The form will initially show three widget labels and fields:
- a QLineEdit field
- a spanning QLabel
- a QComboBox field
- a QPushButton field

Clicking the QPushButton will add additional widgets:
- a QLineEdit
- a spanning QPushButton
- a UISlider

Opening the FormDialog displays the form with an additional QPushButton, which
will insert a widget in the FormDialog's vertical layout.
'''
QtWidgets.QMainWindow.__init__(self, parent)

# dialog form
self.dialog = FormDialog(parent=self, title='Form Dialog example insert widget')
self.addWidgetsToExampleForm(self.dialog)
buttoninsertvertical = QtWidgets.QPushButton()
buttoninsertvertical.setText("Insert widget in vertical layout")
self.dialog.addSpanningWidget(buttoninsertvertical, 'Button insert vertical')
buttoninsertvertical.clicked.connect(lambda: self.insert_vertical())

# create a FormDockWidget
dock = UIFormWidget.FormDockWidget(parent=self)
dock.setWindowTitle('Dock Widget Example insert widget')
dock.setWindowTitle('Dock Widget Insert Widget Example')
self.addWidgetsToExampleForm(dock)

# create button for Form Dialog
pb = QtWidgets.QPushButton(self)
pb.setText("Open Form Dialog")
pb.clicked.connect(self.openFormDialog)
form_dialog_button = QtWidgets.QPushButton(self)
form_dialog_button.setText("Open FormDialog")
form_dialog_button.clicked.connect(self.openFormDialog)

self.dialog = FormDialog(parent=self, title='FormDialog Insert Widget Example')
self.addWidgetsToExampleForm(self.dialog)

insert_vertical_button = QtWidgets.QPushButton()
insert_vertical_button.setText("Insert widget in vertical layout")
self.dialog.addSpanningWidget(insert_vertical_button, 'qbutton insert vertical')
insert_vertical_button.clicked.connect(lambda: self.insert_vertical())

# create window layout
layout = QtWidgets.QHBoxLayout()
layout.addWidget(pb)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(dock)
widg = QtWidgets.QWidget()
widg.setLayout(layout)
self.setCentralWidget(widg)
layout.addWidget(form_dialog_button)
widget = QtWidgets.QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)

print('\nDictionary of widgets before insertion in the form layout:')
for widget in dock.getWidgets():
print(widget, dock.getWidgets()[widget])

self.dialog.onCancel = self.onCancel
self.show()
Expand All @@ -42,59 +61,98 @@ def openFormDialog(self):
self.dialog.open()

def addWidgetsToExampleForm(self, form):
form.addWidget(QtWidgets.QLineEdit(), "Initial widget row 0: ", 'Initial widget row 0')
form.addSpanningWidget(QtWidgets.QLabel("Initial widget row 1"), 'Initial widget row 1')
# add QComboBox
'''Creates the 'initial' widgets, i.e. a QLineEdit, a spanning QLabel and
a QComboBox. Adds them to the FormDialog.
Also adds a QPushButton that, when clicked, inserts additional widgets into
the FormDialog.
'''
form.addWidget(QtWidgets.QLineEdit(), "Initial QLineEdit Row 0:",
'initial qlineedit row 0')

form.addSpanningWidget(QtWidgets.QLabel("Initial Spanning QLabel Row 1"),
'initial spanning qlabel row 1')

qwidget = QtWidgets.QComboBox(form)
qwidget.addItem("0")
qwidget.addItem("1")
form.addWidget(qwidget, "Initial widget row 2", 'Initial widget row 2')
buttoninsert = QtWidgets.QPushButton()
buttoninsert.setText("Insert widgets")
form.addSpanningWidget(buttoninsert, 'Button insert widgets')
buttoninsert.clicked.connect(lambda: self.insert_form(form, buttoninsert))
form.addWidget(qwidget, "Initial QComboBox Row 2:", 'initial qcombobox row 2')

insert_button = QtWidgets.QPushButton()
insert_button.setText("Insert widgets")
form.addSpanningWidget(insert_button, 'qbutton insert widgets')
insert_button.clicked.connect(lambda: self.insert_form(form, insert_button))

def insert_vertical(self):
self.dialog.insertWidgetToVerticalLayout(
1, QtWidgets.QPushButton("Inserted widget in vertical layout"))
'''Inserts a QPushButton into the vertical layout. The widget is not added to
the dictionary of FormDialog widgets. Also sets the 'enabled' value of the button
that calls this method to 'False'.
'''
vertical_button = QtWidgets.QPushButton()
vertical_button.setText("Inserted widget in vertical layout")
self.dialog.insertWidgetToVerticalLayout(1, vertical_button)
print(
"\nThe dictionary of widgets does not change after insertion in the vertical layout.")
self.dialog.getWidget('Button insert vertical').setEnabled(False)
self.dialog.getWidget('qbutton insert vertical').setEnabled(False)

def insert_form(self, form, button):
'''Inserts additional widgets into the specified rows of the FormDialog: a QLineEdit,
a spanning QPushButton, and a UISlider. Prints the dictionary containing all widgets
in the FormDialog layout.
'''
qlabel = QtWidgets.QLabel(form)
qlabel.setText("Inserted Widget Row 0:")
qlineedit = QtWidgets.QLineEdit(form)
form.insertWidget(0, 'inserted qlineedit row 0', qlineedit, qlabel)

spanning_button = QtWidgets.QPushButton(self)
spanning_button.setText("Inserted Spanning Widget Row 2")
form.insertWidget(2, 'inserted spanning qpushbutton row 2', spanning_button)

qlabel = QtWidgets.QLabel(form)
qlabel.setText("Widget inserted in row 0: ")
qwidget = QtWidgets.QLineEdit(form)
form.insertWidget(0, 'inserted widget', qwidget, qlabel)
buttonspanning = QtWidgets.QPushButton(self)
buttonspanning.setText("Spanning widget inserted in row 2")
form.insertWidget(2, 'inserted spanning widget', buttonspanning)
print('\nDictionary of widgets after insertion in the form layout:\n' +
str(form.getWidgets()))
qlabel.setText("Inserted Widget Row 4:")
uislider = UISliderWidget.UISliderWidget(minimum=-0.5, maximum=0.5, decimals=10,
number_of_steps=10, number_of_ticks=10)
form.insertWidget(4, 'inserted uislider row 4', uislider, qlabel)

print('\nDictionary of widgets after insertion in the form layout:')
for widget in form.getWidgets():
print(widget, form.getWidgets()[widget])

button.setEnabled(False)

def onCancel(self):
if not hasattr(self.dialog.formWidget, 'widget_states'):
if self.dialog.getWidget('Button insert vertical').isEnabled() is False:
'''Defines the behaviour of the 'cancel' button.
If the button is pressed and the FormDialog widget states have not been saved, it checks
whether the 'insert widget' buttons have been pressed. If they have, the method removes
the inserted widgets from the FormDialog and the dictionary of FormDialog widgets.
Otherwise, it only removes the widgets from the FormDialog.
'''
if bool(self.dialog.formWidget.widget_states) is False:
if self.dialog.getWidget('qbutton insert vertical').isEnabled() is False:
self.dialog.removeWidgetFromVerticalLayout(
self.dialog.getWidgetFromVerticalLayout(1))

if self.dialog.getWidget('Button insert widgets').isEnabled() is False:
if self.dialog.getWidget('qbutton insert widgets').isEnabled() is False:
self.dialog.formWidget._popWidget(self.dialog.formWidget.default_widget_states,
'inserted qlineedit row 0')
self.dialog.formWidget._popWidget(self.dialog.formWidget.default_widget_states,
'inserted widget')
'inserted spanning qpushbutton row 2')
self.dialog.formWidget._popWidget(self.dialog.formWidget.default_widget_states,
'inserted spanning widget')
self.dialog.formWidget.removeWidget('inserted widget')
self.dialog.formWidget.removeWidget('inserted spanning widget')
'inserted uislider row 4')
self.dialog.formWidget.removeWidget('inserted qlineedit row 0')
self.dialog.formWidget.removeWidget('inserted spanning qpushbutton row 2')
self.dialog.formWidget.removeWidget('inserted uislider row 4')
else:
if self.dialog.getWidget('Button insert vertical').isEnabled(
) != self.dialog.getWidgetStates()['Button insert vertical_field']['enabled'] is True:
if self.dialog.getWidget(
'qbutton insert vertical').isEnabled() != self.dialog.getSavedWidgetStates(
)['qbutton insert vertical_field']['enabled'] is True:
self.dialog.removeWidgetFromVerticalLayout(
self.dialog.getWidgetFromVerticalLayout(1))
if self.dialog.getWidget('Button insert widgets').isEnabled(
) != self.dialog.getWidgetStates()['Button insert widgets_field']['enabled'] is True:
self.dialog.formWidget.removeWidget('inserted widget')
self.dialog.formWidget.removeWidget('inserted spanning widget')
if self.dialog.getWidget(
'qbutton insert widgets').isEnabled() != self.dialog.getSavedWidgetStates(
)['qbutton insert widgets_field']['enabled'] is True:
self.dialog.formWidget.removeWidget('inserted qlineedit row 0')
self.dialog.formWidget.removeWidget('inserted spanning qpushbutton row 2')
self.dialog.formWidget.removeWidget('inserted uislider row 4')


if __name__ == "__main__":
Expand Down
Loading
Loading