Release 1.4.2: load-only dialog when nothing is selected
tests / fedora:latest (push) Successful in 1m15s
tests / ubuntu-latest · py3.11 (push) Successful in 1m32s
tests / ubuntu-latest · py3.13 (push) Successful in 1m1s
tests / archlinux:latest (push) Successful in 53s
tests / debian:12 (push) Successful in 51s
tests / ubuntu:24.04 (push) Successful in 1m8s
tests / NixOS (FHS wrapper from docs/NIXOS.md) (push) Skipped
Build PCM package / build (push) Successful in 10s

With neither mode derivable (nothing selected, no marker rectangles,
no config) the dialog now opens anyway instead of failing with an
error figure: both radios disabled with their reasons, OK/Save
disabled, Load config… live - a saved config can bootstrap the run.

README: note that macOS/NixOS field tests last ran with plugin v1.3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjSRLWQ8ywBBYyakvr3YTx
This commit is contained in:
janik
2026-08-31 11:42:51 +07:00
co-authored by Claude Fable 5
parent 19327c23b1
commit 634df87fe0
9 changed files with 144 additions and 41 deletions
+1 -1
View File
@@ -4,4 +4,4 @@ __version__ is the runtime source of truth (metadata.json and
pyproject.toml are not deployed with the plugin); a test keeps the
three in sync.
"""
__version__ = "1.4.1"
__version__ = "1.4.2"
+47 -19
View File
@@ -26,7 +26,11 @@ Two run modes share the dialog, chosen by a radio at the top:
"Load config…" swaps the whole setup for another config file: ask()
then returns a LoadRequest instead of a Selection and main re-derives
everything from that file and reopens the dialog.
everything from that file and reopens the dialog. The dialog even
opens with NO runnable mode at all (nothing selected, no marker
rectangles, no config) - load-only: both radios unchecked and
disabled with their reasons, OK/Save disabled - so a saved config can
bootstrap the run.
Row identity is POSITIONAL: the tables never sort or reorder, so
main.py zips Selection.pdn_rows with its own parallel terminal list.
@@ -205,16 +209,28 @@ class _Dialog(QDialog):
self.mode_pdn = QRadioButton("PDN")
self.mode_classic.setEnabled(self._classic_ok)
self.mode_pdn.setEnabled(self._pdn_ok)
start_pdn = self._pdn_ok and (not self._classic_ok
or start_mode == "pdn")
(self.mode_pdn if start_pdn else self.mode_classic).setChecked(True)
reason = None
# with NEITHER mode derivable (nothing selected, no marker
# rectangles, no config terminals) the dialog still opens
# LOAD-ONLY: no radio checked, OK/Save disabled below - so
# "Load config…" can pull in a saved setup instead of the
# launch dying with an error figure
self._no_mode = not (self._classic_ok or self._pdn_ok)
if not self._no_mode:
start_pdn = self._pdn_ok and (not self._classic_ok
or start_mode == "pdn")
(self.mode_pdn if start_pdn
else self.mode_classic).setChecked(True)
reasons = []
if not self._classic_ok and classic_reason:
reason = f"Classic unavailable: {classic_reason}"
reasons.append(f"Classic unavailable: {classic_reason}")
self.mode_classic.setToolTip(classic_reason)
elif not self._pdn_ok and pdn_reason:
reason = f"PDN unavailable: {pdn_reason}"
if not self._pdn_ok and pdn_reason:
reasons.append(f"PDN unavailable: {pdn_reason}")
self.mode_pdn.setToolTip(pdn_reason)
if self._no_mode and load_dir is not None:
reasons.append("Load config… below can still set up a run "
"from a saved config file.")
reason = "\n".join(reasons) if reasons else None
# --- shared form #1 --------------------------------------------
form1 = QFormLayout()
@@ -387,14 +403,23 @@ class _Dialog(QDialog):
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self._try_accept)
buttons.rejected.connect(self.reject)
self.ok_button = buttons.button(QDialogButtonBox.Ok)
self.load_button = None
self.save_button = None
if load_dir is not None:
load_btn = buttons.addButton("Load config…",
QDialogButtonBox.ActionRole)
load_btn.clicked.connect(self._load_config)
self.load_button = buttons.addButton(
"Load config…", QDialogButtonBox.ActionRole)
self.load_button.clicked.connect(self._load_config)
if save_callback is not None:
save_btn = buttons.addButton("Save config…",
QDialogButtonBox.ActionRole)
save_btn.clicked.connect(self._save_config)
self.save_button = buttons.addButton(
"Save config…", QDialogButtonBox.ActionRole)
self.save_button.clicked.connect(self._save_config)
if self._no_mode:
# nothing to run or to save until a load re-derives; Load
# config… and Cancel stay live
self.ok_button.setEnabled(False)
if self.save_button is not None:
self.save_button.setEnabled(False)
content = QWidget()
lay = QVBoxLayout(content)
@@ -405,11 +430,12 @@ class _Dialog(QDialog):
mode_row.addWidget(self.mode_pdn)
mode_row.addStretch(1)
lay.addLayout(mode_row)
self.reason_label = None
if reason is not None:
rl = QLabel(reason)
rl.setWordWrap(True)
rl.setStyleSheet("color: gray; font-size: 10px;")
lay.addWidget(rl)
self.reason_label = QLabel(reason)
self.reason_label.setWordWrap(True)
self.reason_label.setStyleSheet("color: gray; font-size: 10px;")
lay.addWidget(self.reason_label)
lay.addLayout(form1)
if self.classic_section is not None:
lay.addWidget(self.classic_section)
@@ -997,7 +1023,9 @@ def ask(candidates: dict[str, list[str]], layer_order: list[str],
config file); pdn: the PDN terminal setup (editable tables);
pdn_candidates: net candidates for PDN mode (classic uses
`candidates`); classic_reason / pdn_reason: why a mode is
unavailable (its radio is disabled with the reason shown);
unavailable (its radio is disabled with the reason shown; with
BOTH unavailable the dialog opens load-only - OK and Save disabled
until Load config… returns a LoadRequest);
save_callback(selection, target_path) -> saved name string enables
the "Save config…" button in both modes (the file name is asked
per save, seeded with save_target); load_dir (the board directory)
+15 -6
View File
@@ -7,7 +7,10 @@ config's terminal set) -> gather fills -> dialog with a Classic/PDN
mode selector (classic: the two-contact form; PDN: editable per-role
terminal tables) -> extract vias -> solve -> figures + report. The
dialog's "Load config…" button loops back to the derivation with the
picked file, so a run can be set up from any saved config.
picked file, so a run can be set up from any saved config - and the
dialog opens even when NEITHER mode is derivable (nothing selected, no
marker rectangles, no config): load-only, both radios disabled with
their reasons, so a saved config can bootstrap the run.
Every failure is reported twice: on stdout (lands in the KiCad status-bar
warning list) and as a matplotlib error figure, so it cannot be missed.
@@ -101,8 +104,10 @@ def main() -> None:
configfile.apply_physics(cfg)
# BOTH terminal derivations always run; a failure only
# disables that mode's radio (with the reason shown) - the
# launch dies only when neither mode is possible
# disables that mode's radio (with the reason shown).
# Even with neither mode possible the dialog still
# opens - load-only - so "Load config…" works without
# first selecting pads or drawing marker rectangles
classic_reason = pdn_reason = None
es1: list = []
es2: list = []
@@ -210,9 +215,13 @@ def main() -> None:
f"workable supply+load copper; PDN "
f"candidates: {', '.join(pdn_nets)}")
if classic_reason is not None and pdn_reason is not None:
raise SelectionError(
f"{classic_reason}\n(PDN mode is also "
f"unavailable: {pdn_reason})")
# nothing runnable right now: the dialog opens with
# both radios and OK/Save disabled, offering only
# Load config… (and Cancel) - a loaded file then
# re-derives everything through the loop below
print(f"note: classic mode unavailable: "
f"{classic_reason}")
print(f"note: PDN mode unavailable: {pdn_reason}")
buildups = board_io.gather_mask_buildups(board)
except ApiError as e:
raise UserFacingError(