docs(qt.py): Add comprehensive documentation for BalWizardDialog class

This commit is contained in:
2026-04-09 02:39:04 +00:00
parent a27df11dfa
commit d613438800

327
qt.py
View File

@@ -1274,324 +1274,37 @@ class _LockTimeEditor:
<<<<<<< HEAD <<<<<<< HEAD
======= =======
""" """
HeirsLockTimeEdit - A custom QWidget for editing locktime values in the context of heirs distribution. BalWizardDialog - A custom QDialog that implements a multi-step wizard interface.
This widget combines raw locktime editing with date-based selection and provides This dialog provides a structured, step-by-step workflow for complex operations
additional functionality for managing locktime values in a heir inheritance scenario. in the Bal Electrum plugin, guiding users through a sequence of pages with
forward/backward navigation and validation.
Features: Features:
- Supports both raw locktime values and human-readable date formats - Multi-page navigation with Previous/Next buttons
- Emits valueEdited signal when the locktime value is edited - Automatic validation before proceeding to next page
- Provides threshold-based validation for locktime values - Progress tracking with visual indicators
- Integrates with heir distribution workflows - Customizable page flow and validation rules
- Integration with BalDialog base class for consistent styling
Behavior: Usage:
The class handles three types of locktime values: The wizard follows a standard pattern:
1. **Timestamps**: Raw integer values representing Unix timestamps 1. Initialize with a list of page constructors
2. **Day intervals**: Values ending with 'd' (e.g., '3d' = 3 days from now) 2. Each page is responsible for its own setup and validation
3. **Year intervals**: Values ending with 'y' (e.g., '2y' = 2 years from now) 3. The dialog manages navigation and state between pages
4. Finalize action is triggered when all pages are completed
Only these formats are valid:
- Examples: '1609459200', '3d', '2y'
- Invalid: 'invalid', '5m', '10x'
The widget automatically converts day/year intervals to appropriate timestamps.
Attributes: Attributes:
valueEdited (pyqtSignal): Signal emitted when the locktime value is edited pages (list): List of page constructors for the wizard
locktime_threshold (int): Minimum threshold value for locktime (default: 50000000) current_page (int): Index of the currently displayed page
page_widgets (list): List of instantiated page widgets
Args: Args:
parent: Optional parent QWidget parent: Optional parent QWidget
default_index (int): Default index for the combo box (default: 1) title (str): Title to display in the dialog header
pages (list): List of page constructors (callables) for each step
""" """
class HeirsLockTimeEdit(QWidget, _LockTimeEditor):
valueEdited = pyqtSignal()
locktime_threshold = 50000000
def __init__(self, parent=None, default_index=1):
QWidget.__init__(self, parent)
hbox = QHBoxLayout()
self.setLayout(hbox)
hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(0)
self.locktime_raw_e = LockTimeRawEdit(self, time_edit=self)
self.locktime_date_e = LockTimeDateEdit(self, time_edit=self)
self.editors = [self.locktime_raw_e, self.locktime_date_e]
self.combo = QComboBox()
options = [_("Raw"), _("Date")]
self.option_index_to_editor_map = {
0: self.locktime_raw_e,
1: self.locktime_date_e,
}
self.combo.addItems(options)
self.editor = self.option_index_to_editor_map[default_index]
self.combo.currentIndexChanged.connect(self.on_current_index_changed)
self.combo.setCurrentIndex(default_index)
self.on_current_index_changed(default_index)
hbox.addWidget(self.combo)
for w in self.editors:
hbox.addWidget(w)
hbox.addStretch(1)
# spacer_widget = QWidget()
# spacer_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# hbox.addWidget(spacer_widget)
self.locktime_raw_e.editingFinished.connect(self.valueEdited.emit)
self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit)
self.combo.currentIndexChanged.connect(self.valueEdited.emit)
def on_current_index_changed(self, i):
for w in self.editors:
w.setVisible(False)
w.setEnabled(False)
prev_locktime = self.editor.get_locktime()
self.editor = self.option_index_to_editor_map[i]
if self.editor.is_acceptable_locktime(prev_locktime):
self.editor.set_locktime(prev_locktime, force=True)
self.editor.setVisible(True)
self.editor.setEnabled(True)
def get_locktime(self) -> Optional[str]:
return self.editor.get_locktime()
def set_index(self, index):
self.combo.setCurrentIndex(index)
self.on_current_index_changed(index)
def set_locktime(self, x: Any, force=True) -> None:
self.editor.set_locktime(x, force)
class LockTimeRawEdit(QLineEdit, _LockTimeEditor):
def __init__(self, parent=None, time_edit=None):
QLineEdit.__init__(self, parent)
self.setFixedWidth(14 * char_width_in_lineedit())
self.textChanged.connect(self.numbify)
self.isdays = False
self.isyears = False
self.isblocks = False
self.time_edit = time_edit
def replace_str(self, text):
return str(text).replace("d", "").replace("y", "").replace("b", "")
def checkbdy(self, s, pos, appendix):
try:
charpos = pos - 1
charpos = max(0, charpos)
charpos = min(len(s) - 1, charpos)
if appendix == s[charpos]:
s = self.replace_str(s) + appendix
pos = charpos
except Exception:
pass
return pos, s
def numbify(self):
text = self.text().strip()
# chars = '0123456789bdy' removed the option to choose locktime by block
chars = "0123456789dy"
pos = self.cursorPosition()
pos = len("".join([i for i in text[:pos] if i in chars]))
s = "".join([i for i in text if i in chars])
self.isdays = False
self.isyears = False
self.isblocks = False
pos, s = self.checkbdy(s, pos, "d")
pos, s = self.checkbdy(s, pos, "y")
pos, s = self.checkbdy(s, pos, "b")
if "d" in s:
self.isdays = True
if "y" in s:
self.isyears = True
if "b" in s:
self.isblocks = True
if self.isdays:
s = self.replace_str(s) + "d"
if self.isyears:
s = self.replace_str(s) + "y"
if self.isblocks:
s = self.replace_str(s) + "b"
self.set_locktime(s, force=False)
# setText sets Modified to False. Instead we want to remember
# if updates were because of user modification.
self.setModified(self.hasFocus())
self.setCursorPosition(pos)
def get_locktime(self) -> Optional[str]:
try:
return str(self.text())
except Exception:
return None
def set_locktime(self, x: Any, force=True) -> None:
out = str(x)
if "d" in out:
out = self.replace_str(x) + "d"
elif "y" in out:
out = self.replace_str(x) + "y"
elif "b" in out:
out = self.replace_str(x) + "b"
else:
try:
out = int(x)
except Exception:
self.setText("")
return
out = max(out, self.min_allowed_value)
out = min(out, self.max_allowed_value)
self.setText(str(out))
class LockTimeHeightEdit(LockTimeRawEdit):
max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX
def __init__(self, parent=None, time_edit=None):
LockTimeRawEdit.__init__(self, parent)
self.setFixedWidth(20 * char_width_in_lineedit())
self.time_edit = time_edit
def paintEvent(self, event):
super().paintEvent(event)
panel = QStyleOptionFrame()
self.initStyleOption(panel)
textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)
textRect.adjust(2, 0, -10, 0)
painter = QPainter(self)
painter.setPen(ColorScheme.GRAY.as_color())
painter.drawText(textRect, int(Qt.AlignRight | Qt.AlignVCenter), "height")
def get_max_allowed_timestamp() -> int:
ts = NLOCKTIME_MAX
# Test if this value is within the valid timestamp limits (which is platform-dependent).
# see #6170
try:
datetime.fromtimestamp(ts)
except (OSError, OverflowError):
ts = 2**31 - 1 # INT32_MAX
datetime.fromtimestamp(ts) # test if raises
return ts
class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1
max_allowed_value = get_max_allowed_timestamp()
def __init__(self, parent=None, time_edit=None):
QDateTimeEdit.__init__(self, parent)
self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value))
self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value))
self.setDateTime(QDateTime.currentDateTime())
self.time_edit = time_edit
def get_locktime(self) -> Optional[int]:
dt = self.dateTime().toPyDateTime()
locktime = int(time.mktime(dt.timetuple()))
return locktime
def set_locktime(self, x: Any, force=False) -> None:
if not self.is_acceptable_locktime(x):
self.setDateTime(QDateTime.currentDateTime())
return
try:
x = int(x)
except Exception:
self.setDateTime(QDateTime.currentDateTime())
return
dt = datetime.fromtimestamp(x)
self.setDateTime(dt)
_NOT_GIVEN = object() # sentinel value
class PercAmountEdit(BTCAmountEdit):
def __init__(
self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN
):
super().__init__(decimal_point, is_int, parent, max_amount=max_amount)
def numbify(self):
text = self.text().strip()
if text == "!":
self.shortcut.emit()
return
pos = self.cursorPosition()
chars = "0123456789%"
chars += DECIMAL_POINT
s = "".join([i for i in text if i in chars])
if "%" in s:
self.is_perc = True
s = s.replace("%", "")
else:
self.is_perc = False
if DECIMAL_POINT in s:
p = s.find(DECIMAL_POINT)
s = s.replace(DECIMAL_POINT, "")
s = s[:p] + DECIMAL_POINT + s[p : p + 8]
if self.is_perc:
s += "%"
self.setText(s)
self.setModified(self.hasFocus())
self.setCursorPosition(pos)
def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:
try:
text = text.replace(DECIMAL_POINT, ".")
text = text.replace("%", "")
return (Decimal)(text)
except Exception:
return None
def _get_text_from_amount(self, amount):
out = super()._get_text_from_amount(amount)
if self.is_perc:
out += "%"
return out
def paintEvent(self, event):
QLineEdit.paintEvent(self, event)
if self.base_unit:
panel = QStyleOptionFrame()
self.initStyleOption(panel)
textRect = self.style().subElementRect(
QStyle.SubElement.SE_LineEditContents, panel, self
)
textRect.adjust(2, 0, -10, 0)
painter = QPainter(self)
painter.setPen(ColorScheme.GRAY.as_color())
if len(self.text()) == 0:
painter.drawText(
textRect,
int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter),
self.base_unit() + " or perc value",
)
class BalDialog(WindowModalDialog):
def __init__(self, parent, bal_plugin, title=None, icon="icons/bal32x32.png"):
self.parent = parent
WindowModalDialog.__init__(self, parent, title)
# WindowModalDialog.__init__(self,parent)
self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon)))
class BalWizardDialog(BalDialog): class BalWizardDialog(BalDialog):
def __init__(self, bal_window: "BalWindow"): def __init__(self, bal_window: "BalWindow"):
assert bal_window assert bal_window