Undo & Redo¶
QtPie provides built-in undo/redo functionality for Widget[T] model fields through Observant's ObservableProxy (PyPI).
Enable Undo¶
Add undo=True to the @widget decorator:
from dataclasses import dataclass
from qtpie import Widget, make, widget
from qtpy.QtWidgets import QLineEdit, QWidget
@dataclass
class User:
name: str = ""
@widget(undo=True)
class UserEditor(QWidget, Widget[User]):
name: QLineEdit = make(QLineEdit)
Now every change to name is tracked in an undo history.
Configuration¶
Control undo behavior with additional parameters:
@widget(
undo=True,
undo_max=50, # Keep max 50 undo steps (default: unlimited)
undo_debounce_ms=500 # Wait 500ms before recording (useful for text)
)
class TextEditor(QWidget, Widget[Document]):
content: QLineEdit = make(QLineEdit)
Parameters¶
undo- Enable/disable undo tracking (default:False)undo_max- Maximum history depth (default: unlimited)undo_debounce_ms- Debounce time in milliseconds before recording a change
The debounce is particularly useful for text input - without it, every keystroke creates a separate undo step. With undo_debounce_ms=500, typing rapidly only creates one undo step per 500ms pause.
Undo & Redo¶
Use the undo() and redo() methods:
@widget(undo=True)
class UserEditor(QWidget, Widget[User]):
name: QLineEdit = make(QLineEdit)
def setup(self) -> None:
# Make some changes
self.name.setText("Alice")
# Undo the change
self.undo("name") # name is now ""
# Redo the change
self.redo("name") # name is "Alice" again
Check Availability¶
Before calling undo/redo, check if the operation is available:
This is essential for enabling/disabling undo/redo buttons.
Example: Text Editor with Undo Buttons¶
Here's a complete text editor with undo/redo buttons:
from dataclasses import dataclass
from qtpie import Widget, make, widget
from qtpy.QtWidgets import QLineEdit, QPushButton, QWidget
@dataclass
class Document:
content: str = ""
@widget(undo=True, undo_debounce_ms=500)
class TextEditor(QWidget, Widget[Document]):
content: QLineEdit = make(QLineEdit)
undo_btn: QPushButton = make(QPushButton, "Undo", clicked="do_undo")
redo_btn: QPushButton = make(QPushButton, "Redo", clicked="do_redo")
def setup(self) -> None:
# Update button states whenever content changes
self.model_observable_proxy.observable(str, "content").on_change(self.update_buttons)
self.update_buttons()
def update_buttons(self, _value: str | None = None) -> None:
"""Enable/disable buttons based on undo/redo availability."""
self.undo_btn.setEnabled(self.can_undo("content"))
self.redo_btn.setEnabled(self.can_redo("content"))
def do_undo(self) -> None:
if self.can_undo("content"):
self.undo("content")
def do_redo(self) -> None:
if self.can_redo("content"):
self.redo("content")
How It Works¶
- Undo enabled -
@widget(undo=True)creates the proxy with undo tracking - Changes tracked - Each field change is recorded as a history entry
- Debouncing - Rapid changes within
undo_debounce_msare grouped - Per-field - Each field has its own independent undo/redo stack
- Max depth - Old entries are discarded when
undo_maxis exceeded
Multiple Fields¶
Each field maintains its own undo history:
from dataclasses import dataclass
from qtpie import Widget, make, widget
from qtpy.QtWidgets import QLineEdit, QSpinBox, QWidget
@dataclass
class User:
name: str = ""
age: int = 0
@widget(undo=True)
class UserEditor(QWidget, Widget[User]):
name: QLineEdit = make(QLineEdit)
age: QSpinBox = make(QSpinBox)
def setup(self) -> None:
# Make changes to both fields
self.name.setText("Alice")
self.age.setValue(30)
# Undo only affects the specified field
self.undo("name") # name reverts, age stays 30
self.undo("age") # age reverts, name stays reverted
API Reference¶
Widget[T] Methods¶
undo(field: str)- Undo the last change to a fieldredo(field: str)- Redo the last undone changecan_undo(field: str) -> bool- Check if undo is availablecan_redo(field: str) -> bool- Check if redo is available
Decorator Parameters¶
undo: bool- Enable undo/redo (default:False)undo_max: int | None- Max history depth (default:None= unlimited)undo_debounce_ms: int | None- Debounce time in milliseconds (default:None)
See Also¶
- Record Widgets - Understanding
Widget[T] - Dirty Tracking - Detect unsaved changes
- Validation - Validate field values
- @widget decorator - Full decorator API
- Observant Integration - Understanding the reactive layer