Skip to content

Commit 070f8ae

Browse files
authored
Merge pull request #109 from Brain-Modulation-Lab/feat/improving-readthedocs
help and electrode in rtd
2 parents c1a1f7e + fd5d521 commit 070f8ae

11 files changed

Lines changed: 133 additions & 89 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
and session scales settings dialogs, and regenerated workflow captures at
1414
native resolution (HiDPI-aware screen grab, improved PNG settings).
1515
- ``docs/_static/custom.css``: full-width RTD content; ``screenshot-full`` (wizard
16-
steps) and ``screenshot-native`` (home, dialogs, electrode diagram) capped at
17-
each PNG's intrinsic width so dialogs are not upscaled on wide screens.
16+
steps) and ``screenshot-native`` (home, dialogs) capped at each PNG's intrinsic
17+
width so dialogs are not upscaled on wide screens.
1818
- ``COPYRIGHT_HOLDERS`` and ``APP_LICENSE_NAME`` in ``config.py`` (shared with
1919
documentation).
2020

@@ -35,9 +35,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3535
and Neuroengineering, and Charité Universitätsmedizin Berlin.
3636
- Screenshot capture uses the app's responsive window geometry (step 0 compact,
3737
steps 1+ full workflow) instead of a fixed 1600×1000 frame.
38+
- ``workflow_session.rst``: drop standalone ``electrode_diagram.png`` (electrode
39+
UI is already visible in the Step 1 screenshot).
3840

3941
### Fixed
4042

43+
- Update checker: HTTPS uses the ``certifi`` CA bundle (fixes failed or empty
44+
GitHub API responses in Briefcase MSI/ZIP on Windows); compares against
45+
``APP_VERSION``; empty or invalid release data surfaces as errors instead of
46+
a misleading “no updates” dialog.
4147
- Electrode diagram export: E0–En labels no longer clipped on the left in PNG
4248
output (wider label box and export left padding in ``ElectrodeCanvas``).
4349
- Docs screenshot pipeline: Windows ``QPixmap.save``/PNG compression; trim

docs/_static/custom.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
}
2020

2121
/*
22-
* Dialogs, home screen, electrode diagram, etc.
22+
* Dialogs, home screen, and other partial captures.
2323
* Never upscale beyond the PNG's intrinsic width; shrink only on narrow viewports.
2424
*/
2525
.rst-content img.screenshot-native,

docs/_static/electrode_diagram.png

-53.6 KB
Binary file not shown.

docs/faq.rst

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,49 @@ General
66

77
**Does the application require an internet connection?**
88

9-
No : session recording, editing, and export work fully
10-
offline and only read and write local files. Optionally, the application can
11-
contact the public GitHub *releases* API (about once per day when enabled) to
12-
see whether a newer build is published; that request does not include patient
13-
or session content. If the network is unavailable or the check fails, the app
14-
continues without blocking. You can turn off automatic update checks from
15-
**Help** (or from the opt-out on an update notification).
9+
No: session recording, editing, and export work fully offline and only read and
10+
write local files. Optionally, the application can contact the public GitHub
11+
*releases* API (about once per day when enabled) to see whether a newer build
12+
is published; that request does not include patient or session content. If the
13+
network is unavailable or the check fails, the app continues without blocking.
14+
You can turn off automatic update checks from Help (or from the opt-out on an
15+
update notification).
1616

1717
**Is my patient data sent anywhere?**
1818

19-
No. All data stays on your local machine. No telemetry, no cloud sync.
19+
No. All data stays on your local machine or the folder you chose when starting
20+
a session. No telemetry, no cloud sync.
2021

2122
**How does the automatic update checker work?**
2223

2324
When automatic checks are enabled, the app compares your installed version with
2425
published releases on the upstream GitHub repository (including pre-releases
2526
when they are the newest applicable tag), and notifies you if a strictly newer
26-
semver is available. Only one candidate release is consideredthe highest
27-
version above yours. Update checks run in the background, do not block startup,
28-
and are skipped silently on errors. Use **Help** to toggle “Automatically check
29-
for updates, or disable them from the checkbox on the update dialog if you do
30-
not want further automatic notifications.
27+
semver is available. Only one candidate release is considered: the highest
28+
version above yours. Update checks run in the background, do not block
29+
startup, and are skipped silently on errors. Use Help to toggle
30+
"Automatically check for updates", or disable them from the checkbox on the
31+
update dialog if you do not want further automatic notifications.
3132

3233
**Which DBS systems are supported?**
3334

3435
The application is system-agnostic for data recording. Electrode visualisation
3536
supports leads from Medtronic (including Percept PC / RC), Abbott (Infinity),
36-
Boston Scientific (Vercise) and PINS families.
37-
If your lead is not listed, use the closest equivalent or contact the
38-
development team to request it be added.
37+
Boston Scientific (Vercise), and PINS families. If your lead is not listed,
38+
use the closest equivalent or contact the development team to request it be
39+
added.
3940

4041
**Can I use the application on a shared clinical workstation?**
4142

42-
Yes. The application is easily installable and does not require installation or registry entries.
43+
Yes. The application installs per user without requiring administrator rights
44+
for day-to-day use. Session files are plain TSV files in folders you choose.
4345

4446
----
4547

4648
Files & Data
4749
------------
4850

49-
**Where are my data files saved?**
51+
**Where are TSV files saved?**
5052

5153
In the folder you selected in Step 0 of the Complete Workflow. The application
5254
never writes files outside that folder.
@@ -56,8 +58,8 @@ never writes files outside that folder.
5658
Yes. In Excel: *File → Open*, select the ``.tsv`` file, and in the Text Import
5759
Wizard choose **Tab** as the delimiter.
5860

59-
Alternatively, double-click the fileWindows may open it in Excel
60-
automatically if Excel is installed.
61+
Alternatively, double-click the file; Windows may open it in Excel automatically
62+
if Excel is installed.
6163

6264
**Can I edit the TSV file manually?**
6365

@@ -66,20 +68,14 @@ You can, but be careful:
6668
* Do not change column headers.
6769
* Do not delete or reorder rows.
6870
* Do not change the ``is_initial`` values.
69-
* Save as Tab-delimited TSV, not as ``.xlsx``.
71+
* Save as tab-delimited TSV, not as ``.xlsx``.
7072

7173
**What happens if the application crashes mid-session?**
7274

73-
All entries are written to disk immediately as they are recorded in the TSV file. You will not
74-
lose any data that was successfully recorded before the crash. Reopen the
75-
application, start a new session pointing to the same folder, and the existing
76-
file will be detected.
77-
78-
**Can I merge two TSV files from the same session?**
79-
80-
Manually: open both files in a text editor and copy the rows from the second
81-
file (excluding the header row) to the end of the first. Make sure session IDs
82-
are unique after merging.
75+
All entries are written to disk immediately as they are recorded in the TSV
76+
file. You will not lose any data that was successfully recorded before the
77+
crash. Reopen the application, start a new session pointing to the same folder,
78+
and the existing file will be detected.
8379

8480
----
8581

@@ -107,11 +103,10 @@ without the full entry table, select **Session Data Graph** only.
107103

108104
**Where do the timeline charts in reports come from?**
109105

110-
Session and longitudinal exporters build PNG charts with matplotlib
111-
(``report_chart_utils``). **Session Data Graph** plots session-scale values
112-
(0–10) against ``block_ID`` (configuration index). **Sessions Overview** in the
113-
longitudinal report adds a separate chart of baseline clinical scales across
114-
loaded session files.
106+
Session and longitudinal exporters build PNG charts with matplotlib.
107+
**Session Data Graph** plots session-scale values (0–10 by default) against
108+
``block_ID`` (configuration index). **Sessions Overview** in the longitudinal
109+
report adds a separate chart of clinical scales across loaded session files.
115110

116111
**Word export works but PDF export fails.**
117112

@@ -134,8 +129,8 @@ Troubleshooting
134129

135130
**The application does not start / shows a black window.**
136131

137-
Try running it as administrator (right-click → *Run as administrator*). This
138-
is sometimes needed on machines with strict execution policies.
132+
Try running it as administrator (right-click → *Run as administrator*). This is
133+
sometimes needed on machines with strict execution policies.
139134

140135
**The application is very slow on first launch.**
141136

@@ -156,7 +151,7 @@ Check that:
156151
The longitudinal report requires at least one session file with recorded scale
157152
values (``scale_name`` and ``scale_value`` columns populated) in
158153
``is_initial = 0`` rows. If your files only contain initial entries, the
159-
dialog cannot compute best-entry highlighting proceed by clicking OK without
154+
dialog cannot compute best-entry highlighting; proceed by clicking OK without
160155
making any selection.
161156

162157
----

docs/workflow_session.rst

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,11 @@ This step records the **baseline** state at the beginning of the session
7373
Electrode Model
7474
^^^^^^^^^^^^^^^
7575

76-
Select the implanted electrode model from the dropdown. The application
77-
supports all common Medtronic Percept, Abbott, and Boston Scientific leads.
78-
An interactive diagram of the selected electrode is displayed immediately.
79-
80-
.. image:: _static/electrode_diagram.png
81-
:alt: Interactive electrode contact diagram
82-
:class: screenshot-native
76+
Select the implanted electrode model from the **Model** dropdown. Optionally,
77+
pick a **Manufacturer** to narrow the list. The application supports common DBS
78+
leads from Medtronic, Abbott, Boston Scientific, and other vendors. The Step 1
79+
screenshot above shows the interactive diagram (example:
80+
Medtronic SenSight B33005, left and right hemispheres).
8381

8482
Contact Selection
8583
^^^^^^^^^^^^^^^^^

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ classifiers = [
2121
"Operating System :: OS Independent",
2222
]
2323
dependencies = [
24+
"certifi>=2024.0.0",
2425
"tzdata>=2026.2",
2526
"pandas>=2.1",
2627
"python-docx>=1.1",

src/dbs_annotator/utils/updater.py

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,19 @@
2424

2525
import json
2626
import logging
27+
import ssl
2728
import urllib.error
2829
import urllib.request
2930
from collections.abc import Callable
3031
from dataclasses import dataclass
3132
from datetime import UTC, datetime, timedelta
3233
from typing import Any, cast
3334

35+
import certifi
3436
from packaging.version import InvalidVersion, Version
3537
from PySide6.QtCore import QObject, QRunnable, QSettings, QThreadPool, Signal
3638

37-
from .. import __version__
39+
from ..version import get_version
3840

3941
logger = logging.getLogger(__name__)
4042

@@ -49,6 +51,11 @@
4951
_MAX_RELEASE_PAGES = 5
5052

5153

54+
def _ssl_context() -> ssl.SSLContext:
55+
"""CA bundle for HTTPS in packaged apps (Briefcase MSI/ZIP on Windows)."""
56+
return ssl.create_default_context(cafile=certifi.where())
57+
58+
5259
@dataclass(frozen=True)
5360
class ReleaseInfo:
5461
"""Metadata for a GitHub release that is newer than the running app."""
@@ -145,10 +152,12 @@ def _request(self, url: str) -> urllib.request.Request:
145152

146153
def _urlopen_json(self, url: str) -> object:
147154
request = self._request(url)
148-
with urllib.request.urlopen(request, timeout=self._timeout) as response:
155+
with urllib.request.urlopen(
156+
request, timeout=self._timeout, context=_ssl_context()
157+
) as response:
149158
return json.loads(response.read().decode("utf-8"))
150159

151-
def _fetch_releases_page(self, page: int) -> list[dict] | None:
160+
def _fetch_releases_page(self, page: int) -> list[dict]:
152161
url = (
153162
f"https://api.github.com/repos/{self._repo}/releases"
154163
f"?per_page={_RELEASES_PAGE_SIZE}&page={page}"
@@ -157,12 +166,9 @@ def _fetch_releases_page(self, page: int) -> list[dict] | None:
157166
payload = self._urlopen_json(url)
158167
except urllib.error.HTTPError as exc:
159168
if exc.code == 404:
160-
logger.debug(
161-
"No GitHub releases list for %s (HTTP %s); treat as no update",
162-
self._repo,
163-
exc.code,
164-
)
165-
return None
169+
raise RuntimeError(
170+
f"GitHub releases API returned 404 for {self._repo}."
171+
) from exc
166172
raise
167173
if not isinstance(payload, list):
168174
return []
@@ -172,26 +178,26 @@ def _fetch_all_releases(self) -> list[dict]:
172178
merged: list[dict] = []
173179
for page in range(1, _MAX_RELEASE_PAGES + 1):
174180
batch = self._fetch_releases_page(page)
175-
if batch is None:
176-
return []
177181
merged.extend(batch)
178182
if len(batch) < _RELEASES_PAGE_SIZE:
179183
break
180184
return merged
181185

182186
def _fetch_newest_applicable_release(self) -> ReleaseInfo | None:
183187
"""Return single newest published release with version *>* local."""
184-
payloads = self._fetch_all_releases()
185-
if not payloads:
186-
return None
187-
188188
local = _parse_version(self._current_version)
189189
if local is None:
190-
logger.debug(
191-
"Skipping update comparison; local version not PEP 440: %r",
192-
self._current_version,
190+
raise ValueError(
191+
f"Installed version {self._current_version!r} is not a valid "
192+
"PEP 440 version."
193+
)
194+
195+
payloads = self._fetch_all_releases()
196+
if not payloads:
197+
raise RuntimeError(
198+
f"No published releases returned for {self._repo} "
199+
"(empty list or network issue)."
193200
)
194-
return None
195201

196202
best_remote: Version | None = None
197203
best_payload: dict | None = None
@@ -249,7 +255,7 @@ def __init__(
249255
) -> None:
250256
super().__init__(parent)
251257
self._repo = repo
252-
self._current_version = current_version or __version__
258+
self._current_version = current_version or get_version()
253259
self._cooldown = cooldown
254260
self._timeout = timeout
255261
self._settings = QSettings()

src/dbs_annotator/views/wizard_window.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
QSpacerItem,
2929
QStackedWidget,
3030
QStyle,
31-
QTextEdit,
31+
QTextBrowser,
3232
QVBoxLayout,
3333
QWidget,
3434
)
@@ -109,7 +109,7 @@ def __init__(self, app):
109109

110110
# Background update check. Runs once per cooldown window (24h by
111111
# default); offline / rate-limited failures are logged silently.
112-
self._update_checker = UpdateChecker(parent=self)
112+
self._update_checker = UpdateChecker(current_version=APP_VERSION, parent=self)
113113
self._update_checker.update_available.connect(self._on_update_available)
114114
# Defer slightly so the window is painted before any dialog appears.
115115
QTimer.singleShot(1500, self._run_deferred_update_check)
@@ -368,19 +368,34 @@ def _update_theme_button_icon(self) -> None:
368368
else:
369369
self.theme_toggle_btn.setText("🌙") # Moon = will switch to dark
370370

371+
@staticmethod
372+
def _help_link(href: str, label: str, *, dark: bool) -> str:
373+
"""Styled, readable hyperlink for the Help dialog (light vs dark theme)."""
374+
color = "#93c5fd" if dark else "#b45309"
375+
safe_href = html.escape(href, quote=True)
376+
safe_label = html.escape(label)
377+
return (
378+
f'<a href="{safe_href}" style="color: {color}; '
379+
f'text-decoration: underline;">{safe_label}</a>'
380+
)
381+
371382
def _help_dialog_html(self) -> str:
372383
"""HTML body for the Help / About dialog."""
373384
year = datetime.now().year
374-
repo = html.escape(APP_REPOSITORY_URL)
375-
issues = html.escape(APP_ISSUES_URL)
376-
email = html.escape(UPDATE_FEEDBACK_EMAIL)
385+
dark = get_theme_manager().is_dark_mode()
386+
repo_url = APP_REPOSITORY_URL
387+
issues_url = APP_ISSUES_URL
388+
mailto = f"mailto:{UPDATE_FEEDBACK_EMAIL}"
389+
repo = self._help_link(repo_url, repo_url, dark=dark)
390+
issues = self._help_link(issues_url, issues_url, dark=dark)
391+
email = self._help_link(mailto, UPDATE_FEEDBACK_EMAIL, dark=dark)
377392
return f"""
378393
<h3>General Overview</h3>
379394
<p>The main use of {html.escape(APP_NAME)} is a <b>standard pipeline</b> for
380395
DBS <b>programming sessions</b>: baseline setup, session scales, and
381396
real-time recording of each stimulation configuration you test in clinic.</p>
382397
<ol>
383-
<li><b>File &amp; patient setup</b>: choose where to save the session file
398+
<li><b>File setup</b>: choose where to save the session file
384399
(BIDS-style name).</li>
385400
<li><b>Initial configuration</b>: electrode model, starting stimulation
386401
parameters, and baseline clinical scales.</li>
@@ -412,9 +427,9 @@ def _help_dialog_html(self) -> str:
412427
<h3>Credits &amp; support</h3>
413428
<p><b>Publisher:</b> {html.escape(ORGANIZATION_PUBLISHER)}<br/>
414429
<b>Lead developer:</b> {html.escape(APP_LEAD_AUTHOR)}</p>
415-
<p><b>Repository:</b> <a href="{repo}">{repo}</a><br/>
416-
<b>Issues:</b> <a href="{issues}">{issues}</a><br/>
417-
<b>Contact:</b> <a href="mailto:{email}">{email}</a></p>
430+
<p><b>Repository:</b> {repo}<br/>
431+
<b>Issues:</b> {issues}<br/>
432+
<b>Contact:</b> {email}</p>
418433
"""
419434

420435
def _show_info_dialog(self) -> None:
@@ -431,10 +446,10 @@ def _show_info_dialog(self) -> None:
431446
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
432447
layout.addWidget(title_label)
433448

434-
# Description
435-
desc_text = QTextEdit()
436-
desc_text.setReadOnly(True)
449+
desc_text = QTextBrowser()
450+
desc_text.setOpenExternalLinks(True)
437451
desc_text.setHtml(self._help_dialog_html())
452+
desc_text.anchorClicked.connect(QDesktopServices.openUrl)
438453

439454
layout.addWidget(desc_text)
440455

0 commit comments

Comments
 (0)