Skip to content

Commit be63f3b

Browse files
authored
Merge pull request #125 from Brain-Modulation-Lab/releasing-fix
RTD dark canvas, favicon, scale/preset sizing, update-now fix
2 parents dbc3ce2 + 03850f1 commit be63f3b

16 files changed

Lines changed: 282 additions & 53 deletions

File tree

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- Dependency audit: pin ``uv>=0.11.15`` (GHSA-4gg8-gxpx-9rph); upgrade ``pip`` to
13+
26.1.2 (PYSEC-2026-196, Briefcase transitive dep).
14+
15+
## [0.4.0] - 2026-06-01
16+
17+
### Changed
18+
19+
- Step 1 clinical preset buttons scroll horizontally when they do not fit on one row.
20+
21+
### Fixed
22+
23+
- Wizard window: title-bar maximize and resize on steps 1+ (step 0 stays compact).
24+
- Step 1 initial settings: vertical scrollbar in its own column; no overlap with electrode canvases.
25+
26+
## [0.4.0b2] - 2026-05-21
27+
28+
### Added
29+
30+
- Read the Docs: dark outer page background (matching the sidebar); browser tab
31+
favicon uses the application icon.
32+
33+
### Changed
34+
35+
- Clinical and session preset pill buttons grow to fit their full label (theme
36+
``max-width`` cap removed).
37+
- Step 1 and Step 2 scale name/value fields widen as needed to show the full text.
38+
39+
### Fixed
40+
41+
- In-app **Update now** on Windows: keep the install script on disk until PowerShell
42+
finishes (avoids a flash-and-close with no install) and pause the console when
43+
the installer fails so the error message stays visible.
44+
- Step 1 clinical preset buttons disappearing after adding a preset in settings
45+
(horizontal scroll strip sizing).
46+
- Step 3 session settings: external vertical scrollbar column (same layout as Step 1;
47+
no overlap with electrode canvases).
1248
- Dependency audit: pin ``uv>=0.11.15`` (GHSA-4gg8-gxpx-9rph).
1349

1450
## [0.4.0] - 2026-06-01

docs/_static/custom.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,25 @@
22
* sphinx-rtd-theme caps main content at ~800px for readability.
33
* Override so clinical screenshots and wide tables use the full viewport.
44
*/
5+
:root {
6+
/* Match sphinx-rtd-theme sidebar (.wy-nav-side). */
7+
--dbs-docs-canvas: #343131;
8+
}
9+
10+
/* Dark outer canvas (same tone as the left navigation column). */
11+
html,
12+
body,
13+
.wy-body-for-nav,
14+
.wy-grid-for-nav,
15+
.wy-nav-content-wrap {
16+
background: var(--dbs-docs-canvas);
17+
}
18+
19+
/* Readable "window" for prose, screenshots, and tables. */
520
.wy-nav-content {
621
max-width: none;
22+
background: #ffffff;
23+
box-shadow: 0 0 18px rgba(0, 0, 0, 0.28);
724
}
825

926
.wy-nav-content-wrap {

docs/_static/favicon-32.png

2.83 KB
Loading

docs/_static/favicon.ico

954 Bytes
Binary file not shown.

docs/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
html_theme = "sphinx_rtd_theme"
5959
html_static_path = ["_static"]
6060
html_css_files = ["custom.css"]
61+
# Browser tab icon (same asset as the packaged Qt / Briefcase application).
62+
html_favicon = "_static/favicon.ico"
6163

6264
html_theme_options = {
6365
"logo_only": False,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ quiet = false
144144

145145
[tool.uv]
146146
exclude-newer = "1 week"
147-
exclude-newer-package = { lxml = false, idna = false }
147+
exclude-newer-package = { lxml = false, idna = false, pip = false }
148148

149149
[tool.towncrier]
150150
package = "dbs_annotator"

src/dbs_annotator/ui/widgets.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
QLineEdit,
1717
QProgressBar,
1818
QPushButton,
19+
QSizePolicy,
1920
QVBoxLayout,
2021
QWidget,
2122
)
@@ -24,6 +25,24 @@
2425
from ..utils import create_arrow_icon
2526

2627

28+
def line_edit_min_width_for_text(
29+
edit: QLineEdit, text: str, *, floor: int, padding: int = 20
30+
) -> None:
31+
"""Ensure a line edit is wide enough to show *text* (or its placeholder)."""
32+
sample = text.strip() or edit.placeholderText() or ""
33+
width = edit.fontMetrics().horizontalAdvance(sample) + padding
34+
edit.setMinimumWidth(max(floor, width))
35+
36+
37+
def push_button_min_width_for_label(
38+
button: QPushButton, label: str, *, floor: int = 40, padding: int = 28
39+
) -> None:
40+
"""Ensure a pill-style button is wide enough for the full label."""
41+
width = button.fontMetrics().horizontalAdvance(label) + padding
42+
button.setMinimumWidth(max(floor, width))
43+
button.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
44+
45+
2746
def create_horizontal_line() -> QFrame:
2847
"""
2948
Create a styled horizontal separator line.

src/dbs_annotator/utils/auto_update.py

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ def _install_script_url(filename: str) -> str:
3131
)
3232

3333

34+
def _install_script_dir() -> Path:
35+
"""Stable temp dir so the install script is not deleted before PowerShell starts."""
36+
directory = Path(tempfile.gettempdir()) / "dbs_annotator_update"
37+
directory.mkdir(parents=True, exist_ok=True)
38+
return directory
39+
40+
3441
def _download_install_script(filename: str) -> Path:
3542
url = _install_script_url(filename)
3643
request = urllib.request.Request(
@@ -40,16 +47,47 @@ def _download_install_script(filename: str) -> Path:
4047
ctx = ssl.create_default_context(cafile=certifi.where())
4148
with urllib.request.urlopen(request, timeout=60, context=ctx) as response:
4249
data = response.read()
43-
suffix = ".ps1" if filename.endswith(".ps1") else ".sh"
44-
fd, path_str = tempfile.mkstemp(prefix="dbs_annotator_install_", suffix=suffix)
45-
os.close(fd)
46-
path = Path(path_str)
50+
path = _install_script_dir() / filename
4751
path.write_bytes(data)
48-
if suffix == ".sh":
52+
if filename.endswith(".sh"):
4953
path.chmod(0o755)
5054
return path
5155

5256

57+
def _escape_ps_single_quoted(value: str) -> str:
58+
return value.replace("'", "''")
59+
60+
61+
def _windows_powershell_command(
62+
script: Path, tag: str, *, dry_run: bool = False
63+
) -> list[str]:
64+
"""Build a PowerShell invocation that keeps the console open on install failure."""
65+
whatif = " -WhatIf" if dry_run else ""
66+
ps = (
67+
"$ErrorActionPreference = 'Stop'; "
68+
f"try {{ & '{_escape_ps_single_quoted(script.as_posix())}' "
69+
f"-VersionTag '{_escape_ps_single_quoted(tag)}' "
70+
f"-GitHubRepository '{_escape_ps_single_quoted(RELEASES_GITHUB_REPO)}'{whatif} "
71+
"} catch { "
72+
"Write-Host $_.Exception.Message -ForegroundColor Red; "
73+
"exit 1 "
74+
"}; "
75+
"if ($LASTEXITCODE -ne 0) { "
76+
"Write-Host ''; "
77+
"Write-Host 'Install did not finish (exit' $LASTEXITCODE ').'; "
78+
"Read-Host 'Press Enter to close' "
79+
"}"
80+
)
81+
return [
82+
"powershell.exe",
83+
"-NoProfile",
84+
"-ExecutionPolicy",
85+
"Bypass",
86+
"-Command",
87+
ps,
88+
]
89+
90+
5391
def automatic_update_supported() -> bool:
5492
"""True on platforms where ``scripts/install.*`` can be launched."""
5593
return (
@@ -102,32 +140,12 @@ def launch_automatic_update(
102140

103141
def _launch_windows(tag: str, *, dry_run: bool = False) -> tuple[bool, str]:
104142
script = _download_install_script("install.ps1")
105-
try:
106-
cmd = [
107-
"powershell.exe",
108-
"-NoProfile",
109-
"-ExecutionPolicy",
110-
"Bypass",
111-
"-File",
112-
str(script),
113-
"-VersionTag",
114-
tag,
115-
"-GitHubRepository",
116-
RELEASES_GITHUB_REPO,
117-
]
118-
if dry_run:
119-
cmd.append("-WhatIf")
120-
subprocess.Popen(
121-
cmd,
122-
creationflags=_WINDOWS_NEW_CONSOLE,
123-
close_fds=True,
124-
)
125-
finally:
126-
if not dry_run:
127-
try:
128-
script.unlink(missing_ok=True)
129-
except OSError:
130-
pass
143+
cmd = _windows_powershell_command(script, tag, dry_run=dry_run)
144+
subprocess.Popen(
145+
cmd,
146+
creationflags=_WINDOWS_NEW_CONSOLE,
147+
close_fds=True,
148+
)
131149

132150
if dry_run:
133151
return True, (

src/dbs_annotator/views/step1_view.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
get_cathode_labels,
5858
)
5959
from ..ui.clinical_scales_settings_dialog import ClinicalScalesSettingsDialog
60+
from ..ui.widgets import line_edit_min_width_for_text, push_button_min_width_for_label
6061
from ..utils.program_config_manager import (
6162
ProgramConfigManager,
6263
get_program_config_manager,
@@ -1805,14 +1806,24 @@ def _add_clinical_scale_row(
18051806

18061807
name_edit = QLineEdit()
18071808
name_edit.setPlaceholderText(PLACEHOLDERS["scale_name"])
1808-
name_edit.setMaximumWidth(80)
18091809
name_edit.setText(name)
1810+
line_edit_min_width_for_text(name_edit, name, floor=56)
1811+
name_edit.textChanged.connect(
1812+
lambda text, edit=name_edit: line_edit_min_width_for_text(
1813+
edit, text, floor=56
1814+
)
1815+
)
18101816

18111817
score_edit = QLineEdit()
18121818
score_edit.setPlaceholderText(PLACEHOLDERS["scale_score"])
1813-
score_edit.setMaximumWidth(50)
18141819
score_edit.setValidator(QIntValidator())
18151820
score_edit.setText(value)
1821+
line_edit_min_width_for_text(score_edit, value, floor=44)
1822+
score_edit.textChanged.connect(
1823+
lambda text, edit=score_edit: line_edit_min_width_for_text(
1824+
edit, text, floor=44
1825+
)
1826+
)
18161827

18171828
btn = None
18181829
if with_plus:
@@ -1908,14 +1919,39 @@ def _on_presets_changed(self, new_presets: dict[str, list[str]]):
19081919
# Preset was deleted - clear scales
19091920
self._apply_preset_scales([])
19101921

1922+
def _preset_buttons_content_size(self) -> QSize:
1923+
"""Measure preset row size from buttons.
1924+
1925+
Container ``sizeHint()`` can be 0 immediately after a rebuild.
1926+
"""
1927+
layout = self.preset_row_layout
1928+
if layout is None or not self.preset_buttons:
1929+
return QSize(0, 0)
1930+
1931+
margins = layout.contentsMargins()
1932+
spacing = layout.spacing()
1933+
width = margins.left() + margins.right()
1934+
height = margins.top() + margins.bottom()
1935+
for index, btn in enumerate(self.preset_buttons):
1936+
hint = btn.sizeHint()
1937+
width += hint.width()
1938+
height = max(height, hint.height() + margins.top() + margins.bottom())
1939+
if index > 0:
1940+
width += spacing
1941+
return QSize(width, height)
1942+
19111943
def _update_preset_buttons_geometry(self) -> None:
19121944
"""Size the preset strip so horizontal scrolling appears when needed."""
19131945
if not hasattr(self, "preset_scroll_content"):
19141946
return
1947+
if self.preset_row_layout is not None:
1948+
self.preset_row_layout.activate()
1949+
1950+
measured = self._preset_buttons_content_size()
19151951
self.preset_scroll_content.adjustSize()
19161952
hint = self.preset_scroll_content.sizeHint()
1917-
width = max(hint.width(), 1)
1918-
content_height = max(hint.height(), 1)
1953+
width = max(measured.width(), hint.width(), 1)
1954+
content_height = max(measured.height(), hint.height(), 1)
19191955
self.preset_scroll_content.setMinimumSize(width, content_height)
19201956
self.preset_scroll_content.resize(width, content_height)
19211957

@@ -1953,10 +1989,13 @@ def _refresh_preset_buttons(self):
19531989
for preset_name in ordered_names:
19541990
btn = QPushButton(preset_name)
19551991
btn.setObjectName(f"preset_{preset_name}")
1992+
push_button_min_width_for_label(btn, preset_name)
19561993
self.preset_buttons.append(btn)
19571994
preset_row.addWidget(btn)
19581995

1959-
self._update_preset_buttons_geometry()
1996+
# Defer sizing until Qt has laid out the new buttons; immediate sizeHint()
1997+
# on the scroll content is often 0 and collapses the strip to 1px wide.
1998+
QTimer.singleShot(0, self._update_preset_buttons_geometry)
19601999

19612000
if hasattr(self, "on_add_callback") and hasattr(self, "on_remove_callback"):
19622001
self._connect_preset_buttons()

src/dbs_annotator/views/step2_view.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from ..config import PLACEHOLDERS, PRESET_BUTTONS
2828
from ..ui.session_scales_settings_dialog import SessionScalesSettingsDialog
29+
from ..ui.widgets import line_edit_min_width_for_text, push_button_min_width_for_label
2930
from ..utils.scale_preset_manager import get_scale_preset_manager
3031
from .base_view import BaseStepView
3132

@@ -232,6 +233,7 @@ def _refresh_preset_buttons(self):
232233
for preset_name in ordered_names:
233234
btn = QPushButton(preset_name)
234235
btn.setObjectName(f"preset2_{preset_name}")
236+
push_button_min_width_for_label(btn, preset_name)
235237
self.preset_buttons.append(btn)
236238
preset_row.insertWidget(insert_index, btn)
237239
insert_index += 1
@@ -406,18 +408,33 @@ def _add_session_scale_row(
406408

407409
name_edit = QLineEdit()
408410
name_edit.setPlaceholderText(PLACEHOLDERS["scale_name"])
409-
name_edit.setMaximumWidth(100)
410411
name_edit.setText(name)
412+
line_edit_min_width_for_text(name_edit, name, floor=64)
413+
name_edit.textChanged.connect(
414+
lambda text, edit=name_edit: line_edit_min_width_for_text(
415+
edit, text, floor=64
416+
)
417+
)
411418

412419
scale1_edit = QLineEdit()
413420
scale1_edit.setPlaceholderText(PLACEHOLDERS["scale_min"])
414-
scale1_edit.setMaximumWidth(40)
415421
scale1_edit.setText(minval)
422+
line_edit_min_width_for_text(scale1_edit, minval, floor=40)
423+
scale1_edit.textChanged.connect(
424+
lambda text, edit=scale1_edit: line_edit_min_width_for_text(
425+
edit, text, floor=40
426+
)
427+
)
416428

417429
scale2_edit = QLineEdit()
418430
scale2_edit.setPlaceholderText(PLACEHOLDERS["scale_max"])
419-
scale2_edit.setMaximumWidth(40)
420431
scale2_edit.setText(maxval)
432+
line_edit_min_width_for_text(scale2_edit, maxval, floor=40)
433+
scale2_edit.textChanged.connect(
434+
lambda text, edit=scale2_edit: line_edit_min_width_for_text(
435+
edit, text, floor=40
436+
)
437+
)
421438

422439
if with_plus:
423440
btn = QPushButton("+")

0 commit comments

Comments
 (0)