pyflashgui 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {pyflashgui-0.2.1/pyflashgui.egg-info → pyflashgui-0.2.2}/PKG-INFO +2 -2
  2. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/README.md +6 -0
  3. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/__init__.py +1 -1
  4. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/backends/pyocd_packs.py +27 -5
  5. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/events.py +12 -0
  6. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/pack_manager.py +9 -1
  7. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/help/changelog.md +28 -1
  8. pyflashgui-0.2.2/pyflashgui/help/main-window.png +0 -0
  9. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/help/manual.md +10 -2
  10. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/help/probe.md +2 -2
  11. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/help_menu.py +6 -2
  12. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/main_window.py +32 -2
  13. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/theme.py +58 -4
  14. {pyflashgui-0.2.1 → pyflashgui-0.2.2/pyflashgui.egg-info}/PKG-INFO +2 -2
  15. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui.egg-info/SOURCES.txt +3 -0
  16. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui.egg-info/requires.txt +1 -1
  17. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyproject.toml +1 -1
  18. pyflashgui-0.2.2/tests/test_docking.py +713 -0
  19. pyflashgui-0.2.2/tests/test_event_log_times.py +72 -0
  20. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_flash_panes.py +17 -2
  21. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_help.py +7 -0
  22. pyflashgui-0.2.2/tests/test_layout.py +166 -0
  23. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_packs.py +35 -4
  24. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_theme.py +46 -2
  25. pyflashgui-0.2.1/pyflashgui/help/main-window.png +0 -0
  26. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/LICENSE +0 -0
  27. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/NOTICE +0 -0
  28. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/PYPI.md +0 -0
  29. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/__main__.py +0 -0
  30. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/backends/__init__.py +0 -0
  31. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/backends/base.py +0 -0
  32. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/backends/jlink_backend.py +0 -0
  33. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/backends/pyocd_backend.py +0 -0
  34. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/backends/simulated.py +0 -0
  35. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/__init__.py +0 -0
  36. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/checkout.py +0 -0
  37. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/compare.py +0 -0
  38. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/context.py +0 -0
  39. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/excepthook.py +0 -0
  40. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/flasher.py +0 -0
  41. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/images.py +0 -0
  42. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/installation.py +0 -0
  43. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/layout.py +0 -0
  44. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/library_log.py +0 -0
  45. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/memory_map.py +0 -0
  46. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/packages.py +0 -0
  47. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/paths.py +0 -0
  48. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/settings.py +0 -0
  49. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/shortcut.py +0 -0
  50. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/slow_start.py +0 -0
  51. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/timing.py +0 -0
  52. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/updates.py +0 -0
  53. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/worker.py +0 -0
  54. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/core/workspaces.py +0 -0
  55. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/help/__init__.py +0 -0
  56. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/help/about.md +0 -0
  57. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/help/console.md +0 -0
  58. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/help/files.md +0 -0
  59. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/help/memory-map.md +0 -0
  60. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/help/memory.md +0 -0
  61. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/resources/__init__.py +0 -0
  62. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui.ico +0 -0
  63. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui.png +0 -0
  64. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui.svg +0 -0
  65. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui_small.svg +0 -0
  66. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/resources/tick.svg +0 -0
  67. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/__init__.py +0 -0
  68. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/buttons.py +0 -0
  69. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/confirm.py +0 -0
  70. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/console_view.py +0 -0
  71. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/crop_dialog.py +0 -0
  72. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/detached.py +0 -0
  73. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/diagnostics.py +0 -0
  74. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/erase_dialog.py +0 -0
  75. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/event_colours.py +0 -0
  76. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/files_view.py +0 -0
  77. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/folders.py +0 -0
  78. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/fonts.py +0 -0
  79. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/map_view.py +0 -0
  80. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/memory_view.py +0 -0
  81. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/messages.py +0 -0
  82. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/packs_dialog.py +0 -0
  83. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/pane_bar.py +0 -0
  84. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/panes.py +0 -0
  85. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/persist.py +0 -0
  86. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/probe_bar.py +0 -0
  87. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/read_dialog.py +0 -0
  88. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/scripting.py +0 -0
  89. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/scripts_menu.py +0 -0
  90. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/session.py +0 -0
  91. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui/ui/workspace_menu.py +0 -0
  92. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui.egg-info/dependency_links.txt +0 -0
  93. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui.egg-info/entry_points.txt +0 -0
  94. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/pyflashgui.egg-info/top_level.txt +0 -0
  95. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/setup.cfg +0 -0
  96. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_backends.py +0 -0
  97. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_build_version.py +0 -0
  98. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_changelog.py +0 -0
  99. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_checkout.py +0 -0
  100. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_compare.py +0 -0
  101. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_console.py +0 -0
  102. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_diagnostics.py +0 -0
  103. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_erase.py +0 -0
  104. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_events.py +0 -0
  105. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_excepthook.py +0 -0
  106. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_flasher.py +0 -0
  107. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_folders.py +0 -0
  108. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_fonts.py +0 -0
  109. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_headers.py +0 -0
  110. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_icon.py +0 -0
  111. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_images.py +0 -0
  112. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_installation.py +0 -0
  113. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_jlink_backend.py +0 -0
  114. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_launcher.py +0 -0
  115. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_memory_map.py +0 -0
  116. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_messages.py +0 -0
  117. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_notice.py +0 -0
  118. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_packages.py +0 -0
  119. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_packaging.py +0 -0
  120. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_persist.py +0 -0
  121. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_read_back.py +0 -0
  122. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_readme.py +0 -0
  123. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_scripts_menu.py +0 -0
  124. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_shortcut.py +0 -0
  125. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_slow_start.py +0 -0
  126. {pyflashgui-0.2.1 → pyflashgui-0.2.2}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyflashgui
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Graphical flash programming tool: Arm Cortex-M with any probe pyOCD supports, and more through J-Link Commander
5
5
  Author: davhodg
6
6
  License-Expression: Apache-2.0
@@ -34,7 +34,7 @@ Requires-Dist: bincopy>=20.0
34
34
  Provides-Extra: dev
35
35
  Requires-Dist: pytest; extra == "dev"
36
36
  Requires-Dist: pytest-xdist; extra == "dev"
37
- Requires-Dist: ruff==0.16.*; extra == "dev"
37
+ Requires-Dist: ruff==0.17.*; extra == "dev"
38
38
  Requires-Dist: trove-classifiers; extra == "dev"
39
39
  Dynamic: license-file
40
40
 
@@ -126,6 +126,10 @@ pyflashgui can instead drive a J-Link through SEGGER's own J-Link Commander, whi
126
126
 
127
127
  On Linux a probe is usable without root once a udev rule allows it; pyOCD publishes [a set of them](https://github.com/pyocd/pyOCD/tree/main/udev). Vendor software is never bundled with pyflashgui: install the vendor's own, and pyflashgui finds it.
128
128
 
129
+ ### Isolating the probe
130
+
131
+ A USB isolator between the PC and the probe breaks a ground loop between them. [usb2-480mbps-isolator](https://github.com/davhodg/usb2-480mbps-isolator) is an open-source, hand-solderable one in the works, for USB 2.0 at every speed up to 480 Mbit/s, that limits the current it takes to what the PC's port can supply. **Available soon:** it is still being designed, and is for functional isolation only, not certified for safety isolation.
132
+
129
133
  ## Built with
130
134
 
131
135
  | Package | For | Licence |
@@ -151,6 +155,8 @@ and no way of uninstalling touches them; delete that folder to remove them too.
151
155
 
152
156
  See [CONTRIBUTING.md](CONTRIBUTING.md): the policy for changes, setting up to work on pyflashgui, running the tests, and building the Windows installer. Everyone taking part is asked to follow the [code of conduct](CODE_OF_CONDUCT.md). Security problems go through [SECURITY.md](SECURITY.md).
153
157
 
158
+ What is aimed for with a keyboard, a screen reader, large text and high contrast, what has been checked, and how to report a barrier, is in [ACCESSIBILITY.md](ACCESSIBILITY.md).
159
+
154
160
  ## Safety notice
155
161
 
156
162
  pyflashgui talks to real equipment. It is intended only for people trained and experienced in working with microcontrollers, debug probes and the equipment they are part of. It is a tool for development and the bench, and is not meant for production programming.
@@ -2,5 +2,5 @@
2
2
  # SPDX-FileCopyrightText: 2026 davhodg
3
3
  """pyflashgui — a graphical flash programming tool."""
4
4
 
5
- __version__ = "0.2.1"
5
+ __version__ = "0.2.2"
6
6
  APP_NAME = "pyflashgui" # single place to change the display name
@@ -93,7 +93,15 @@ class Device:
93
93
  installed: bool
94
94
 
95
95
 
96
- def open_cache(tick: Tick | None = None) -> Any:
96
+ #: Called once, when fetching the list moves on from downloading to what
97
+ #: comes after: the library reads every description -- over fifteen hundred --
98
+ #: and writes the list of devices from them, twenty megabytes, which takes
99
+ #: half a minute or so with no count of its own to show.
100
+ Stage = Callable[[str], None]
101
+ PROCESSING = "Processing what was downloaded"
102
+
103
+
104
+ def open_cache(tick: Tick | None = None, stage: Stage | None = None) -> Any:
97
105
  """pyOCD's own store of packs. Replaced in the tests, which have no network."""
98
106
  import cmsis_pack_manager
99
107
 
@@ -103,6 +111,14 @@ def open_cache(tick: Tick | None = None) -> Any:
103
111
  if tick is not None:
104
112
  tick(current, total or None)
105
113
 
114
+ # The first thing the library does after the last download. Should a
115
+ # later version of it name this otherwise, it is not called and the
116
+ # step is not said: nothing else changes.
117
+ def _call_rust_parse(self, pdsc_index):
118
+ if stage is not None:
119
+ stage(PROCESSING)
120
+ return super()._call_rust_parse(pdsc_index)
121
+
106
122
  return Cache(tick is None, False)
107
123
 
108
124
 
@@ -158,14 +174,20 @@ def list_fetched() -> datetime | None:
158
174
  return None
159
175
 
160
176
 
161
- def fetch_list(tick: Tick | None = None) -> int:
162
- """Download the index of every pack. Returns how many devices it has."""
163
- cache = open_cache(tick)
177
+ def fetch_list(tick: Tick | None = None, stage: Stage | None = None) -> int:
178
+ """Download the index of every pack. Returns how many devices it has.
179
+
180
+ ``stage`` is told when the downloads have ended and what they brought is
181
+ processed, which is as long again.
182
+ """
183
+ cache = open_cache(tick, stage)
164
184
  try:
165
185
  cache.cache_descriptors()
166
186
  except Exception as exc:
167
187
  raise _said("Fetching the list of devices", exc) from exc
168
- count = len(open_cache().index)
188
+ # Through the copy kept of it, which the list of packs and the search
189
+ # that follow then use rather than reading twenty megabytes again.
190
+ count = len(_index(open_cache()))
169
191
  if not count:
170
192
  raise PackError(
171
193
  "Fetching the list of devices brought nothing back. It comes from the internet: "
@@ -22,6 +22,8 @@ plain text, and nothing has to be imported to name one.
22
22
 
23
23
  from __future__ import annotations
24
24
 
25
+ from datetime import datetime
26
+
25
27
  from PySide6.QtCore import QObject, Signal
26
28
 
27
29
  INFORMATION = "information"
@@ -43,6 +45,16 @@ LEVELS = (INFORMATION, GOOD, WARNING, ERROR)
43
45
  PROBLEMS = (WARNING, ERROR)
44
46
 
45
47
 
48
+ def with_time(line: str, at: datetime) -> str:
49
+ """A line with the time of day it was written in front, to the second.
50
+
51
+ To the second and no finer: a line is written a moment after it was said,
52
+ and the time is for reading a log back against a clock, not for timing
53
+ anything -- erasing, programming and reading say how long they took.
54
+ """
55
+ return f"{at:%H:%M:%S} {line}"
56
+
57
+
46
58
  class EventLog(QObject):
47
59
  """The one way in. What listens is somebody else's business."""
48
60
 
@@ -126,7 +126,15 @@ class PackManager(QObject):
126
126
  self.events.good(message)
127
127
  self.progress.emit(message)
128
128
 
129
- self._run(doing, lambda: pyocd_packs.fetch_list(self._tick(doing)), done)
129
+ def stage(now: str) -> None:
130
+ # A step with no count of its own, after the last download: the
131
+ # bar goes back and forth, and this says why it still does.
132
+ try:
133
+ self.progress.emit(f"{now}, which takes a while...")
134
+ except RuntimeError: # the window went while it was running
135
+ pass
136
+
137
+ self._run(doing, lambda: pyocd_packs.fetch_list(self._tick(doing), stage), done)
130
138
 
131
139
  def install(self, pack: Pack) -> None:
132
140
  doing = f"Downloading {pack}"
@@ -9,6 +9,32 @@ since the last version, and comes out in the next one.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.2.2] - 2026-11-08
13
+
14
+ ### Added
15
+
16
+ - **The Qt style in use** is shown greyed out at the bottom of *Tools > Settings > Theme*
17
+ and in its tooltip, and *Help > Diagnostics* gives it with the theme -- and, for *Match
18
+ the system*, whether the system gave light or dark.
19
+ - **Time on Event Log lines**, under *Tools > Settings*: the time of day in front of each
20
+ line, to the second, kept when it is copied. Off to begin with, and kept with this
21
+ computer rather than the workspace, as the theme is.
22
+ - An accessibility statement, `ACCESSIBILITY.md`, and a form for reporting a barrier.
23
+
24
+ ### Changed
25
+
26
+ - The privacy policy in `CODE_SIGNING.md`, and `SECURITY.md`, name fetching the list of
27
+ device packs and installing a pack as the third thing pyflashgui goes online for, and a
28
+ CMSIS-Pack among the files that are data.
29
+ - *Tools > pyOCD device packs*: fetching the list says *Processing what was downloaded* once
30
+ every file has arrived, for the half minute or so that takes, in place of a count that
31
+ had reached its end.
32
+
33
+ ### Fixed
34
+
35
+ - *View > Reset layout* left the Files pane or the Event Log floating, if it had been
36
+ undocked, rather than putting it back in the window.
37
+
12
38
  ## [0.2.1] - 2026-10-25
13
39
 
14
40
  ### Added
@@ -176,7 +202,8 @@ since the last version, and comes out in the next one.
176
202
  for updates.
177
203
  - Launchers for a source folder on Windows and Linux, and the build of the Windows installer.
178
204
 
179
- [Unreleased]: https://github.com/davhodg/pyflashgui/compare/v0.2.1...HEAD
205
+ [Unreleased]: https://github.com/davhodg/pyflashgui/compare/v0.2.2...HEAD
206
+ [0.2.2]: https://github.com/davhodg/pyflashgui/compare/v0.2.1...v0.2.2
180
207
  [0.2.1]: https://github.com/davhodg/pyflashgui/compare/v0.2.0...v0.2.1
181
208
  [0.2.0]: https://github.com/davhodg/pyflashgui/compare/v0.1.0...v0.2.0
182
209
  [0.1.0]: https://github.com/davhodg/pyflashgui/releases/tag/v0.1.0
@@ -27,11 +27,19 @@ With no hardware, choose *Simulated* under **Using** and the same steps work on
27
27
 
28
28
  ## The window
29
29
 
30
- Everything pyflashgui has to say arrives in the **Event Log**, with warnings and errors in their own colours. A warning or an error opens the pane if it has been closed; an ordinary note does not. Right-click in it to clear it. An error pyOCD or pylink reports for itself is put here too, named after the library; their other lines are kept for *Help > Diagnostics*.
30
+ Everything pyflashgui has to say arrives in the **Event Log**, with warnings and errors in their own colours. A warning or an error opens the pane if it has been closed; an ordinary note does not. Right-click in it to clear it. An error pyOCD or pylink reports for itself is put here too, named after the library; their other lines are kept for *Help > Diagnostics*. *Tools > Settings > Time on Event Log lines* puts the time of day each line was written in front of it, to the second -- *14:03:27 Connected ...* -- from the next line on, and it is kept when the log is copied. It is off to begin with, and kept with this computer rather than the workspace, like the theme.
31
31
 
32
32
  Each pane can be dragged to another edge of the window, floated, or closed, and **View** brings one back. *View > Reset layout* puts every pane back where it starts and the window back to the size it opens at.
33
33
 
34
- *Tools > Settings > Theme* chooses light, dark, or whichever the operating system is set to.
34
+ *Tools > Settings > Theme* chooses light, dark, or whichever the operating system is set to. Which look Qt draws it in depends on the system:
35
+
36
+ | System | Style |
37
+ |---|---|
38
+ | Windows 11 | Qt's Windows 11 style, light and dark |
39
+ | Windows 10 and earlier | Qt's style for these has no dark version, so Qt's Fusion style, light and dark alike |
40
+ | Linux | Usually Fusion, or the desktop's own Qt style where it sets one |
41
+
42
+ The style in use is shown greyed out at the bottom of the *Theme* menu, and in the menu's tooltip, and *Help > Diagnostics...* gives it with the theme.
35
43
 
36
44
  ## Workspaces
37
45
 
@@ -36,12 +36,12 @@ pyOCD has a few dozen targets built in. Every other Cortex-M device is described
36
36
 
37
37
  *Tools > pyOCD device packs...* installs one, from the internet or from a file:
38
38
 
39
- 1. Press **Fetch the list**. It downloads the list of every device there is a pack for, which takes a little while and is needed once. Every device is listed.
39
+ 1. Press **Fetch the list**. It downloads the list of every device there is a pack for, which takes a little while and is needed once. Once the count of files downloaded has reached its end, what they brought is processed into the list, which says so and takes about as long again. Every device is listed.
40
40
  2. Type any part of the device's part number. The list narrows to the devices with that in their name as you type.
41
41
  3. Select the device and press **Install pack**. The pack is downloaded from its maker; it holds the whole family the device belongs to, and can be over a hundred megabytes.
42
42
  4. Press **Use as target**, or close the dialog and choose the device in the bar.
43
43
 
44
- **A download cannot be stopped once it has started**, the list's or a pack's. The dialog can be closed while one runs: the [Event Log](manual.md) says when it has arrived, or why it did not. Both downloads need the internet; nothing else in pyflashgui does.
44
+ **A download cannot be stopped once it has started**, the list's or a pack's. The dialog can be closed while one runs: the [Event Log](manual.md) says when it has arrived, or why it did not. Both downloads need the internet; apart from *Help > Check for updates*, nothing else in pyflashgui does.
45
45
 
46
46
  Under the list is where the selected device's pack is downloaded from. The *From* column of the packs installed says the same for each of them.
47
47
 
@@ -48,7 +48,7 @@ from pyflashgui.core.updates import is_newer as version_is_newer
48
48
  from pyflashgui.core.worker import Worker
49
49
  from pyflashgui.help import manual_text
50
50
  from pyflashgui.ui import diagnostics as diagnostics_report
51
- from pyflashgui.ui import fonts, messages
51
+ from pyflashgui.ui import fonts, messages, theme
52
52
  from pyflashgui.ui.diagnostics import DiagnosticsDialog
53
53
 
54
54
 
@@ -340,7 +340,7 @@ class AboutDialog(QDialog):
340
340
 
341
341
 
342
342
  def _label_width(names) -> int:
343
- return max(len(name) for name in ("Python", "Platform", *names)) + 2
343
+ return max(len(name) for name in ("Python", "Platform", "Qt style", "Theme", *names)) + 2
344
344
 
345
345
 
346
346
  def environment_report() -> str:
@@ -356,6 +356,10 @@ def environment_report() -> str:
356
356
  found = "not installed" + (" (optional)" if requirement.extra else "")
357
357
  lines.append(f"{requirement.name:{width}}{found}")
358
358
  lines.append(f"{'Platform':{width}}{platform.platform()}")
359
+ # How it looks, for a report about how something looks.
360
+ if style := theme.style_name():
361
+ lines.append(f"{'Qt style':{width}}{style}")
362
+ lines.append(f"{'Theme':{width}}{theme.describe()}")
359
363
  # Only a checkout has one, and only a checkout needs one: between two
360
364
  # releases every build calls itself the same version, which is no help
361
365
  # when the question is whether somebody had pulled.
@@ -34,7 +34,7 @@ from pyflashgui.backends import jlink_backend
34
34
  from pyflashgui.backends.base import TargetInfo
35
35
  from pyflashgui.core import shortcut, timing, workspaces
36
36
  from pyflashgui.core.context import Context
37
- from pyflashgui.core.events import PROBLEMS, EventLog
37
+ from pyflashgui.core.events import PROBLEMS, EventLog, with_time
38
38
  from pyflashgui.core.excepthook import ExceptionLogger
39
39
  from pyflashgui.core.flasher import Flasher, size_text
40
40
  from pyflashgui.core.library_log import LibraryLog
@@ -101,6 +101,14 @@ DEFAULT_WIDTH = 1100
101
101
  SHORTCUT_TOLD_KEY = "shortcut/told"
102
102
  #: Where J-Link Commander is, when it is not where it is looked for.
103
103
  COMMANDER_SETTING = "jlink/commander"
104
+ #: In QSettings, like the theme: how somebody likes to read the log, not
105
+ #: something about a workspace.
106
+ LOG_TIMES_KEY = "event_log/times"
107
+ LOG_TIMES_TIP = (
108
+ "The time of day each Event Log line was written, to the second, in front\n"
109
+ "of it: 14:03:27 Connected ... From the next line on; kept when lines are\n"
110
+ "copied."
111
+ )
104
112
  DEFAULT_HEIGHT = 720
105
113
 
106
114
 
@@ -150,6 +158,8 @@ class MainWindow(QMainWindow):
150
158
  #: the only thing that writes to the pane. Warnings and errors open it
151
159
  #: if it has been closed; notes do not.
152
160
  self.events = EventLog()
161
+ #: Whether each Event Log line has the time of day in front of it.
162
+ self._log_times = QSettings().value(LOG_TIMES_KEY, False, type=bool)
153
163
  #: The last warnings and errors, for Help > Diagnostics: (time, level, text).
154
164
  self.recent_problems: deque[tuple[str, str, str]] = deque(maxlen=RECENT_PROBLEMS)
155
165
  self._surfacing = False
@@ -308,6 +318,11 @@ class MainWindow(QMainWindow):
308
318
  "it by itself. It comes with SEGGER's J-Link software, and is only\n"
309
319
  "needed to use a J-Link through it."
310
320
  )
321
+ self.log_times_action = self.settings_menu.addAction("Time on Event Log lines")
322
+ self.log_times_action.setCheckable(True)
323
+ self.log_times_action.setChecked(self._log_times)
324
+ self.log_times_action.setToolTip(LOG_TIMES_TIP)
325
+ self.log_times_action.toggled.connect(self._set_log_times)
311
326
  # Below the line: not how pyflashgui behaves, but how this copy of it
312
327
  # is set up on this machine. For a source folder and for pip: the
313
328
  # installer makes its own entries.
@@ -477,6 +492,10 @@ class MainWindow(QMainWindow):
477
492
  under both, because its lines are long.
478
493
  """
479
494
  files, log = (self.panes.docks[n] for n in DEFAULT_VISIBLE)
495
+ # splitDockWidget leaves a floating pane floating, so Reset layout
496
+ # left an undocked Event Log out of the layout it put back.
497
+ for dock in (files, log):
498
+ dock.setFloating(False)
480
499
  self.addDockWidget(Qt.LeftDockWidgetArea, files)
481
500
  self.splitDockWidget(files, log, Qt.Vertical)
482
501
  self.resizeDocks([files, log], [7, 3], Qt.Vertical)
@@ -1020,7 +1039,8 @@ class MainWindow(QMainWindow):
1020
1039
  shape.setForeground(colour)
1021
1040
  if event_colours.bold_for(level):
1022
1041
  shape.setFontWeight(QFont.Bold)
1023
- cursor.insertText(message, shape)
1042
+ line = with_time(message, datetime.now()) if self._log_times else message
1043
+ cursor.insertText(line, shape)
1024
1044
  # Painted again as a whole. Written through a cursor, a line could be
1025
1045
  # drawn before its colour was laid out, and stayed in the plain text
1026
1046
  # colour until scrolling redrew it -- an error that did not look like one.
@@ -1031,6 +1051,16 @@ class MainWindow(QMainWindow):
1031
1051
  self.recent_problems.append((f"{datetime.now():%H:%M:%S}", level, message))
1032
1052
  self._surface_log()
1033
1053
 
1054
+ @Slot(bool)
1055
+ def _set_log_times(self, on: bool) -> None:
1056
+ QSettings().setValue(LOG_TIMES_KEY, on)
1057
+ self._log_times = on
1058
+ self.events.information(
1059
+ "Event Log lines are written with the time of day."
1060
+ if on
1061
+ else "Event Log lines are written without the time."
1062
+ )
1063
+
1034
1064
  def log_menu(self):
1035
1065
  """Copy and Select All as usual, and Clear. Built apart from being shown."""
1036
1066
  menu = self.log.createStandardContextMenu()
@@ -48,9 +48,24 @@ TICK_BOX_OUTLINE = (
48
48
  "QAbstractItemView::indicator:checked, QGroupBox::indicator:checked "
49
49
  f'{{ image: url("{resources.path("tick.svg").as_posix()}"); }}'
50
50
  )
51
+ #: Qt's names for its styles, as the menu and the diagnostics say them.
52
+ STYLE_NAMES = {
53
+ "windows11": "Windows 11",
54
+ "windowsvista": "Windows Vista", # Qt's native one up to Windows 10
55
+ "windows": "Windows",
56
+ "fusion": "Fusion",
57
+ "macos": "macOS",
58
+ }
59
+ STYLE_TIP = (
60
+ "The look Qt draws pyflashgui in. Windows 11 has its own, light and dark. "
61
+ "Qt's style for Windows 10 and earlier has no dark version, so Fusion is used there "
62
+ "for both. On Linux it is Fusion, or the desktop's own Qt style where it sets one."
63
+ )
51
64
  #: Whether the style in use is Fusion, noted before a style sheet is set:
52
65
  #: with one set, the application's style is no longer the one to ask.
53
66
  _fusion = False
67
+ #: The style in use as Qt names it, noted with _fusion and for the same reason.
68
+ _style = ""
54
69
  #: The applications whose colour scheme is already being followed.
55
70
  _followed: set[int] = set()
56
71
  #: The editor factory in use, kept here: Qt holds it, and Python must too.
@@ -71,11 +86,12 @@ def apply(choice: str, app: QApplication | None = None) -> None:
71
86
  app = app or QApplication.instance()
72
87
  if app is None:
73
88
  return
74
- global _fusion
89
+ global _fusion, _style
75
90
  if not app.styleSheet(): # see _fusion
76
91
  if app.style().name().lower() in LIGHT_ONLY:
77
92
  app.setStyle(FUSION)
78
- _fusion = app.style().name().lower() == FUSION.lower()
93
+ _style = app.style().name()
94
+ _fusion = _style.lower() == FUSION.lower()
79
95
  hints = app.styleHints()
80
96
  if choice == LIGHT:
81
97
  hints.setColorScheme(Qt.ColorScheme.Light)
@@ -125,6 +141,27 @@ def opaque_cell_editors() -> None:
125
141
  QItemEditorFactory.setDefaultFactory(_editors)
126
142
 
127
143
 
144
+ def style_name(app: QApplication | None = None) -> str:
145
+ """The style pyflashgui is drawn in, as a person would say it."""
146
+ app = app or QApplication.instance()
147
+ if app is None:
148
+ return ""
149
+ name = _style if (app.styleSheet() and _style) else app.style().name() # see _fusion
150
+ return STYLE_NAMES.get(name.lower(), name)
151
+
152
+
153
+ def describe(settings: QSettings | None = None, app: QApplication | None = None) -> str:
154
+ """The choice, and for *Match the system* what the system gave."""
155
+ choice = chosen(settings)
156
+ label = dict(CHOICES)[choice]
157
+ app = app or QApplication.instance()
158
+ if choice != SYSTEM or app is None:
159
+ return label
160
+ scheme = app.styleHints().colorScheme()
161
+ got = {Qt.ColorScheme.Dark: "dark", Qt.ColorScheme.Light: "light"}.get(scheme)
162
+ return f"{label} ({got})" if got else label
163
+
164
+
128
165
  def choose(choice: str, settings: QSettings | None = None, app: QApplication | None = None) -> None:
129
166
  """Remember a choice and apply it straight away."""
130
167
  (settings or QSettings()).setValue(SETTING, choice)
@@ -132,10 +169,14 @@ def choose(choice: str, settings: QSettings | None = None, app: QApplication | N
132
169
 
133
170
 
134
171
  def menu(parent: QWidget, settings: QSettings | None = None) -> QMenu:
135
- """*Tools > Settings > Theme*: the three choices, the current one ticked."""
172
+ """*Tools > Settings > Theme*: the three choices, the current one ticked,
173
+ and the style in use greyed out below them.
174
+
175
+ A greyed entry is skipped by the keyboard and by most screen readers, so
176
+ the menu's own tooltip says the style as well.
177
+ """
136
178
  out = QMenu(TITLE, parent)
137
179
  out.setToolTipsVisible(True)
138
- out.menuAction().setToolTip(TIP)
139
180
  group = QActionGroup(out)
140
181
  group.setExclusive(True)
141
182
  current = chosen(settings)
@@ -146,4 +187,17 @@ def menu(parent: QWidget, settings: QSettings | None = None) -> QMenu:
146
187
  action.triggered.connect(lambda _=False, v=value: choose(v, settings))
147
188
  group.addAction(action)
148
189
  out.addAction(action)
190
+ out.addSeparator()
191
+ style = QAction(out, enabled=False)
192
+ style.setToolTip(STYLE_TIP)
193
+ out.addAction(style)
194
+ out.style_action = style
195
+
196
+ def show_style() -> None:
197
+ name = style_name()
198
+ style.setText(f"Style: {name}")
199
+ out.menuAction().setToolTip(f"{TIP} Drawn in Qt's {name} style." if name else TIP)
200
+
201
+ show_style()
202
+ out.aboutToShow.connect(show_style)
149
203
  return out
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyflashgui
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Graphical flash programming tool: Arm Cortex-M with any probe pyOCD supports, and more through J-Link Commander
5
5
  Author: davhodg
6
6
  License-Expression: Apache-2.0
@@ -34,7 +34,7 @@ Requires-Dist: bincopy>=20.0
34
34
  Provides-Extra: dev
35
35
  Requires-Dist: pytest; extra == "dev"
36
36
  Requires-Dist: pytest-xdist; extra == "dev"
37
- Requires-Dist: ruff==0.16.*; extra == "dev"
37
+ Requires-Dist: ruff==0.17.*; extra == "dev"
38
38
  Requires-Dist: trove-classifiers; extra == "dev"
39
39
  Dynamic: license-file
40
40
 
@@ -90,7 +90,9 @@ tests/test_checkout.py
90
90
  tests/test_compare.py
91
91
  tests/test_console.py
92
92
  tests/test_diagnostics.py
93
+ tests/test_docking.py
93
94
  tests/test_erase.py
95
+ tests/test_event_log_times.py
94
96
  tests/test_events.py
95
97
  tests/test_excepthook.py
96
98
  tests/test_flash_panes.py
@@ -104,6 +106,7 @@ tests/test_images.py
104
106
  tests/test_installation.py
105
107
  tests/test_jlink_backend.py
106
108
  tests/test_launcher.py
109
+ tests/test_layout.py
107
110
  tests/test_memory_map.py
108
111
  tests/test_messages.py
109
112
  tests/test_notice.py
@@ -5,5 +5,5 @@ bincopy>=20.0
5
5
  [dev]
6
6
  pytest
7
7
  pytest-xdist
8
- ruff==0.16.*
8
+ ruff==0.17.*
9
9
  trove-classifiers
@@ -71,7 +71,7 @@ Changelog = "https://github.com/davhodg/pyflashgui/blob/master/pyflashgui/help/c
71
71
  dev = [
72
72
  "pytest",
73
73
  "pytest-xdist",
74
- "ruff==0.16.*",
74
+ "ruff==0.17.*",
75
75
  "trove-classifiers",
76
76
  ]
77
77