backlogops-gui 0.8__tar.gz → 0.9__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.
- {backlogops_gui-0.8 → backlogops_gui-0.9}/PKG-INFO +18 -6
- {backlogops_gui-0.8 → backlogops_gui-0.9}/README_pypi.md +14 -3
- {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/PKG-INFO +18 -6
- {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/SOURCES.txt +1 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/requires.txt +3 -2
- {backlogops_gui-0.8 → backlogops_gui-0.9}/setup.py +4 -3
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/application.py +48 -25
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/choice_dialogs.py +26 -2
- backlogops_gui-0.9/src/backlogops_gui/config_edit.py +226 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/file_choosers.py +13 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/LICENSE.txt +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/dependency_links.txt +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/entry_points.txt +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/top_level.txt +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/pyproject.toml +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/setup.cfg +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/__init__.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/__main__.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/_migrate_warn.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/backlog_actions.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/backlog_dialogs.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/backlog_io.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/backlog_window.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/blog_version_reporter.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/format_dialogs.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_actions.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_base.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_dialogs.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_order.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_rank.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_read.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_rename.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_update.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_write.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/key_list_box.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/log_buffer.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/modal_dialog.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/py.typed +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/python_version.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/report_windows.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/table_view.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/tcltk_version.py +0 -0
- {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/token_dialog.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backlogops-gui
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9
|
|
4
4
|
Summary: Graphical user interface for backlog operations.
|
|
5
5
|
Author: Tom Björkholm
|
|
6
6
|
Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
|
|
@@ -21,10 +21,11 @@ Classifier: Typing :: Typed
|
|
|
21
21
|
Requires-Python: >=3.12
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE.txt
|
|
24
|
-
Requires-Dist: backlogops>=0.
|
|
25
|
-
Requires-Dist: argcomplete>=3.7.
|
|
24
|
+
Requires-Dist: backlogops>=0.9
|
|
25
|
+
Requires-Dist: argcomplete>=3.7.2
|
|
26
26
|
Requires-Dist: wizard-ui-bridge>=1.3
|
|
27
27
|
Requires-Dist: wizard-tk-bridge>=1.3
|
|
28
|
+
Requires-Dist: edit-cfg-json-tk==0.0.2
|
|
28
29
|
Requires-Dist: versionreporter>=0.4
|
|
29
30
|
Dynamic: author
|
|
30
31
|
Dynamic: license-file
|
|
@@ -285,9 +286,20 @@ is in the menus.
|
|
|
285
286
|
closed for vacation, and preset configuration for the inputs or
|
|
286
287
|
outputs you want to use.
|
|
287
288
|
|
|
289
|
+
- Edit configuration...: Show the whole configuration in a folding
|
|
290
|
+
editor, so you can change one value without stepping through the
|
|
291
|
+
wizard. It asks whether to edit the configuration the application is
|
|
292
|
+
using or one in a file you pick. Saving validates the configuration
|
|
293
|
+
first, keeps what it writes over as a `.bak` file, and makes what it
|
|
294
|
+
wrote the configuration in use.
|
|
295
|
+
|
|
288
296
|
- Create IO preset file...: Create a stand-alone input or output preset
|
|
289
297
|
configuration file via a wizard.
|
|
290
298
|
|
|
299
|
+
- Edit IO preset file...: Show a stand-alone input or output preset file
|
|
300
|
+
in the same folding editor. Whether the file is an input or an output
|
|
301
|
+
preset is detected from the file itself.
|
|
302
|
+
|
|
291
303
|
- Migrate IO preset file...: Migrate an older input or output preset
|
|
292
304
|
file to the current file format.
|
|
293
305
|
|
|
@@ -350,10 +362,10 @@ one with the list of releases. You will want to use the menus.
|
|
|
350
362
|
|
|
351
363
|
## Test summary
|
|
352
364
|
|
|
353
|
-
- Test result:
|
|
365
|
+
- Test result: 2232 passed, 1 deselected in 44s
|
|
354
366
|
- No flake8 warnings.
|
|
355
367
|
- No mypy errors found.
|
|
356
368
|
- No pylint warnings.
|
|
357
369
|
- No python layout warnings.
|
|
358
|
-
- Built version(s): 0.
|
|
359
|
-
- Build and test using Python 3.14.
|
|
370
|
+
- Built version(s): 0.9
|
|
371
|
+
- Build and test using Python 3.14.7
|
|
@@ -252,9 +252,20 @@ is in the menus.
|
|
|
252
252
|
closed for vacation, and preset configuration for the inputs or
|
|
253
253
|
outputs you want to use.
|
|
254
254
|
|
|
255
|
+
- Edit configuration...: Show the whole configuration in a folding
|
|
256
|
+
editor, so you can change one value without stepping through the
|
|
257
|
+
wizard. It asks whether to edit the configuration the application is
|
|
258
|
+
using or one in a file you pick. Saving validates the configuration
|
|
259
|
+
first, keeps what it writes over as a `.bak` file, and makes what it
|
|
260
|
+
wrote the configuration in use.
|
|
261
|
+
|
|
255
262
|
- Create IO preset file...: Create a stand-alone input or output preset
|
|
256
263
|
configuration file via a wizard.
|
|
257
264
|
|
|
265
|
+
- Edit IO preset file...: Show a stand-alone input or output preset file
|
|
266
|
+
in the same folding editor. Whether the file is an input or an output
|
|
267
|
+
preset is detected from the file itself.
|
|
268
|
+
|
|
258
269
|
- Migrate IO preset file...: Migrate an older input or output preset
|
|
259
270
|
file to the current file format.
|
|
260
271
|
|
|
@@ -317,10 +328,10 @@ one with the list of releases. You will want to use the menus.
|
|
|
317
328
|
|
|
318
329
|
## Test summary
|
|
319
330
|
|
|
320
|
-
- Test result:
|
|
331
|
+
- Test result: 2232 passed, 1 deselected in 44s
|
|
321
332
|
- No flake8 warnings.
|
|
322
333
|
- No mypy errors found.
|
|
323
334
|
- No pylint warnings.
|
|
324
335
|
- No python layout warnings.
|
|
325
|
-
- Built version(s): 0.
|
|
326
|
-
- Build and test using Python 3.14.
|
|
336
|
+
- Built version(s): 0.9
|
|
337
|
+
- Build and test using Python 3.14.7
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backlogops-gui
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9
|
|
4
4
|
Summary: Graphical user interface for backlog operations.
|
|
5
5
|
Author: Tom Björkholm
|
|
6
6
|
Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
|
|
@@ -21,10 +21,11 @@ Classifier: Typing :: Typed
|
|
|
21
21
|
Requires-Python: >=3.12
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE.txt
|
|
24
|
-
Requires-Dist: backlogops>=0.
|
|
25
|
-
Requires-Dist: argcomplete>=3.7.
|
|
24
|
+
Requires-Dist: backlogops>=0.9
|
|
25
|
+
Requires-Dist: argcomplete>=3.7.2
|
|
26
26
|
Requires-Dist: wizard-ui-bridge>=1.3
|
|
27
27
|
Requires-Dist: wizard-tk-bridge>=1.3
|
|
28
|
+
Requires-Dist: edit-cfg-json-tk==0.0.2
|
|
28
29
|
Requires-Dist: versionreporter>=0.4
|
|
29
30
|
Dynamic: author
|
|
30
31
|
Dynamic: license-file
|
|
@@ -285,9 +286,20 @@ is in the menus.
|
|
|
285
286
|
closed for vacation, and preset configuration for the inputs or
|
|
286
287
|
outputs you want to use.
|
|
287
288
|
|
|
289
|
+
- Edit configuration...: Show the whole configuration in a folding
|
|
290
|
+
editor, so you can change one value without stepping through the
|
|
291
|
+
wizard. It asks whether to edit the configuration the application is
|
|
292
|
+
using or one in a file you pick. Saving validates the configuration
|
|
293
|
+
first, keeps what it writes over as a `.bak` file, and makes what it
|
|
294
|
+
wrote the configuration in use.
|
|
295
|
+
|
|
288
296
|
- Create IO preset file...: Create a stand-alone input or output preset
|
|
289
297
|
configuration file via a wizard.
|
|
290
298
|
|
|
299
|
+
- Edit IO preset file...: Show a stand-alone input or output preset file
|
|
300
|
+
in the same folding editor. Whether the file is an input or an output
|
|
301
|
+
preset is detected from the file itself.
|
|
302
|
+
|
|
291
303
|
- Migrate IO preset file...: Migrate an older input or output preset
|
|
292
304
|
file to the current file format.
|
|
293
305
|
|
|
@@ -350,10 +362,10 @@ one with the list of releases. You will want to use the menus.
|
|
|
350
362
|
|
|
351
363
|
## Test summary
|
|
352
364
|
|
|
353
|
-
- Test result:
|
|
365
|
+
- Test result: 2232 passed, 1 deselected in 44s
|
|
354
366
|
- No flake8 warnings.
|
|
355
367
|
- No mypy errors found.
|
|
356
368
|
- No pylint warnings.
|
|
357
369
|
- No python layout warnings.
|
|
358
|
-
- Built version(s): 0.
|
|
359
|
-
- Build and test using Python 3.14.
|
|
370
|
+
- Built version(s): 0.9
|
|
371
|
+
- Build and test using Python 3.14.7
|
|
@@ -18,6 +18,7 @@ src/backlogops_gui/backlog_io.py
|
|
|
18
18
|
src/backlogops_gui/backlog_window.py
|
|
19
19
|
src/backlogops_gui/blog_version_reporter.py
|
|
20
20
|
src/backlogops_gui/choice_dialogs.py
|
|
21
|
+
src/backlogops_gui/config_edit.py
|
|
21
22
|
src/backlogops_gui/file_choosers.py
|
|
22
23
|
src/backlogops_gui/format_dialogs.py
|
|
23
24
|
src/backlogops_gui/jira_actions.py
|
|
@@ -5,7 +5,7 @@ from setuptools import setup
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name='backlogops-gui',
|
|
8
|
-
version='0.
|
|
8
|
+
version='0.9',
|
|
9
9
|
description='Graphical user interface for backlog operations.',
|
|
10
10
|
author='Tom Björkholm',
|
|
11
11
|
author_email='klausuler_linnet0q@icloud.com',
|
|
@@ -19,10 +19,11 @@ setup(
|
|
|
19
19
|
]
|
|
20
20
|
},
|
|
21
21
|
install_requires=[
|
|
22
|
-
'backlogops >= 0.
|
|
23
|
-
'argcomplete >= 3.7.
|
|
22
|
+
'backlogops >= 0.9',
|
|
23
|
+
'argcomplete >= 3.7.2',
|
|
24
24
|
'wizard-ui-bridge >= 1.3',
|
|
25
25
|
'wizard-tk-bridge >= 1.3',
|
|
26
|
+
'edit-cfg-json-tk == 0.0.2',
|
|
26
27
|
'versionreporter >= 0.4'
|
|
27
28
|
]
|
|
28
29
|
)
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
The application opens a main window whose menu reads a backlog from a file
|
|
5
5
|
or from Jira, loads or replaces the active configuration from a file, runs
|
|
6
|
-
the teams configuration wizard,
|
|
6
|
+
the teams configuration wizard, edits a configuration or a stand-alone
|
|
7
|
+
preset file in the folding editor of
|
|
8
|
+
:mod:`backlogops_gui.config_edit`, creates a stand-alone input or output
|
|
7
9
|
preset file, migrates a stand-alone preset file to the current format,
|
|
8
10
|
writes the running configuration to a file, and creates a demonstration
|
|
9
11
|
backlog. The configuration wizard and the preset wizard first ask whether
|
|
@@ -41,10 +43,10 @@ from config_as_json import Config, migrate_cfg
|
|
|
41
43
|
from config_as_json.file_extension import fix_file_extension
|
|
42
44
|
from wizard_tk_bridge import WizardUiBridgeTk
|
|
43
45
|
from backlogops import (
|
|
44
|
-
AvailableTeams, BacklogOpsConfig, BacklogReleases,
|
|
45
|
-
InputFormatConfig, Levels, OutputFormatConfig, Status,
|
|
46
|
-
get_backlog_ops_config, backlog_ops_wizard,
|
|
47
|
-
read_backlog_ops_config, read_io_preset, safe_write_config,
|
|
46
|
+
AvailableTeams, BacklogOpsConfig, BacklogReleases, CONFIG_EXTENSION,
|
|
47
|
+
GuiDisplayConfig, InputFormatConfig, Levels, OutputFormatConfig, Status,
|
|
48
|
+
get_demo_backlog, get_backlog_ops_config, backlog_ops_wizard,
|
|
49
|
+
preset_wizard, read_backlog_ops_config, read_io_preset, safe_write_config,
|
|
48
50
|
encrypt_token_file, encrypt_token_to_file)
|
|
49
51
|
from backlogops_gui.backlog_io import read_backlog
|
|
50
52
|
from backlogops_gui.backlog_window import (
|
|
@@ -53,6 +55,7 @@ from backlogops_gui.blog_version_reporter import BloGuiVersionReporter
|
|
|
53
55
|
from backlogops_gui.choice_dialogs import (
|
|
54
56
|
ConfigChoice, PresetKind, SourceChoice, ask_no_config_choice,
|
|
55
57
|
ask_preset_kind, ask_source_choice)
|
|
58
|
+
from backlogops_gui.config_edit import edit_config, edit_preset_file
|
|
56
59
|
from backlogops_gui.file_choosers import (
|
|
57
60
|
choose_config_file, choose_existing_config, choose_existing_preset,
|
|
58
61
|
choose_input_file, choose_migrated_preset, choose_preset_to_migrate)
|
|
@@ -66,17 +69,17 @@ from backlogops_gui.python_version import check_python_version
|
|
|
66
69
|
from backlogops_gui.tcltk_version import check_tcltk_version
|
|
67
70
|
|
|
68
71
|
APP_TITLE = 'Backlog operations GUI'
|
|
69
|
-
CONFIG_EXTENSION = '.cfg'
|
|
70
72
|
WRAP_LENGTH = 520
|
|
71
73
|
LOG_REFRESH_MS = 800
|
|
72
74
|
HEADING_FONT = ('TkDefaultFont', 14, 'bold')
|
|
73
75
|
INSTRUCTIONS = (
|
|
74
76
|
'Use the menus to read a backlog from a file or from Jira, load a '
|
|
75
|
-
'configuration file, run the teams wizard,
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'is at the top
|
|
77
|
+
'configuration file, run the teams wizard, edit a configuration or a '
|
|
78
|
+
'preset file, create a stand-alone input or output preset file, '
|
|
79
|
+
'migrate a preset file to the current format, write the current '
|
|
80
|
+
'configuration to a file, or create a demonstration backlog. Each '
|
|
81
|
+
'backlog opens in its own window. On macOS the menu bar is at the top '
|
|
82
|
+
'of the display.')
|
|
80
83
|
DESCRIPTION = 'Graphical user interface for backlog operations'
|
|
81
84
|
CONFIG_ERRORS = (FileNotFoundError, NotADirectoryError, RuntimeError,
|
|
82
85
|
ValueError, TypeError, KeyError, OSError)
|
|
@@ -155,7 +158,7 @@ class BacklogApp:
|
|
|
155
158
|
|
|
156
159
|
def __init__(self, root: tk.Tk,
|
|
157
160
|
config: Optional[BacklogOpsConfig] = None) -> None:
|
|
158
|
-
"""Store the window, config, log and
|
|
161
|
+
"""Store the window, config, log and the action collaborators."""
|
|
159
162
|
self.root = root
|
|
160
163
|
self.config = config
|
|
161
164
|
self.log = LogBuffer()
|
|
@@ -164,6 +167,21 @@ class BacklogApp:
|
|
|
164
167
|
self._status: Optional[tk.StringVar] = None
|
|
165
168
|
self.jira = JiraActions(self)
|
|
166
169
|
|
|
170
|
+
def adopt_config(self, config: BacklogOpsConfig, source: str) -> None:
|
|
171
|
+
"""Make one configuration the active one and say where it came from.
|
|
172
|
+
|
|
173
|
+
Every way a configuration becomes the active one goes through here —
|
|
174
|
+
loading a file, the wizard, and the editor — so the status line
|
|
175
|
+
cannot end up saying one thing while another is in use.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
config: The configuration to use from now on.
|
|
179
|
+
source: Where it came from, as a file name or a short phrase.
|
|
180
|
+
"""
|
|
181
|
+
self.config = config
|
|
182
|
+
self.config_source = source
|
|
183
|
+
self._update_status()
|
|
184
|
+
|
|
167
185
|
def in_presets(self) -> Optional[dict[str, InputFormatConfig]]:
|
|
168
186
|
"""Return the input presets of the current configuration."""
|
|
169
187
|
return self.config.input_configs if self.config else None
|
|
@@ -214,9 +232,8 @@ class BacklogApp:
|
|
|
214
232
|
"""
|
|
215
233
|
config, error = initial_config(config_arg, self.log)
|
|
216
234
|
if config is not None:
|
|
217
|
-
self.config
|
|
218
|
-
|
|
219
|
-
else 'the default location')
|
|
235
|
+
self.adopt_config(config, config_arg if config_arg is not None
|
|
236
|
+
else 'the default location')
|
|
220
237
|
return True
|
|
221
238
|
if config_arg is not None and error is not None:
|
|
222
239
|
self.show_error('Configuration error', error)
|
|
@@ -244,8 +261,7 @@ class BacklogApp:
|
|
|
244
261
|
config = self.run_wizard()
|
|
245
262
|
if config is None:
|
|
246
263
|
return False
|
|
247
|
-
self.config
|
|
248
|
-
self.config_source = 'the wizard'
|
|
264
|
+
self.adopt_config(config, 'the wizard')
|
|
249
265
|
return True
|
|
250
266
|
|
|
251
267
|
def _adopt_loaded_config(self) -> bool:
|
|
@@ -258,8 +274,7 @@ class BacklogApp:
|
|
|
258
274
|
self.show_error('Configuration error',
|
|
259
275
|
error or 'Could not load the configuration.')
|
|
260
276
|
return False
|
|
261
|
-
self.config
|
|
262
|
-
self.config_source = path
|
|
277
|
+
self.adopt_config(config, path)
|
|
263
278
|
return True
|
|
264
279
|
|
|
265
280
|
def _run_bridge_wizard(self, wizard: Callable[..., _WizardConfig],
|
|
@@ -352,9 +367,7 @@ class BacklogApp:
|
|
|
352
367
|
self.show_error('Configuration error',
|
|
353
368
|
error or 'Could not load the configuration.')
|
|
354
369
|
return
|
|
355
|
-
self.config
|
|
356
|
-
self.config_source = path
|
|
357
|
-
self._update_status()
|
|
370
|
+
self.adopt_config(config, path)
|
|
358
371
|
self.show_info('Configuration loaded',
|
|
359
372
|
f'Loaded configuration from {path}.')
|
|
360
373
|
|
|
@@ -374,11 +387,17 @@ class BacklogApp:
|
|
|
374
387
|
return
|
|
375
388
|
config = self.run_wizard(default)
|
|
376
389
|
if config is not None:
|
|
377
|
-
self.config
|
|
378
|
-
self.config_source = 'the wizard'
|
|
379
|
-
self._update_status()
|
|
390
|
+
self.adopt_config(config, 'the wizard')
|
|
380
391
|
self.show_info('Wizard', 'The new configuration is now active.')
|
|
381
392
|
|
|
393
|
+
def run_config_editor(self) -> None:
|
|
394
|
+
"""Edit the configuration in use, or one in a file, in the editor."""
|
|
395
|
+
edit_config(self)
|
|
396
|
+
|
|
397
|
+
def run_preset_editor(self) -> None:
|
|
398
|
+
"""Edit a stand-alone input or output preset file in the editor."""
|
|
399
|
+
edit_preset_file(self)
|
|
400
|
+
|
|
382
401
|
def create_preset_file(self) -> None:
|
|
383
402
|
"""Ask the source, run the IO preset wizard, and write the preset.
|
|
384
403
|
|
|
@@ -651,8 +670,12 @@ class BacklogApp:
|
|
|
651
670
|
menu.add_separator()
|
|
652
671
|
menu.add_command(label='Run configuration wizard…',
|
|
653
672
|
command=self.run_config_wizard)
|
|
673
|
+
menu.add_command(label='Edit configuration…',
|
|
674
|
+
command=self.run_config_editor)
|
|
654
675
|
menu.add_command(label='Create IO preset file…',
|
|
655
676
|
command=self.create_preset_file)
|
|
677
|
+
menu.add_command(label='Edit IO preset file…',
|
|
678
|
+
command=self.run_preset_editor)
|
|
656
679
|
menu.add_command(label='Migrate IO preset file…',
|
|
657
680
|
command=self.migrate_preset_file)
|
|
658
681
|
menu.add_command(label='Write configuration…',
|
|
@@ -6,8 +6,10 @@ selecting one enumerated value, with no OK or Cancel. The no-configuration
|
|
|
6
6
|
dialog offers to run the wizard, load a file, or exit at startup. The
|
|
7
7
|
preset-kind dialog asks whether a stand-alone preset file is an input or
|
|
8
8
|
an output preset before it is migrated. The source dialog asks whether to
|
|
9
|
-
start a wizard from scratch, base it on an existing file, or cancel.
|
|
10
|
-
|
|
9
|
+
start a wizard from scratch, base it on an existing file, or cancel. The
|
|
10
|
+
edit-target dialog asks whether the configuration editor opens the
|
|
11
|
+
configuration the application is using or one in a file. All four are built
|
|
12
|
+
from the same :class:`ButtonChoiceDialog`.
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
# Copyright (c) 2026, Tom Björkholm
|
|
@@ -26,6 +28,11 @@ NO_CONFIG_TEXT = (
|
|
|
26
28
|
PRESET_KIND_TEXT = (
|
|
27
29
|
'Is the preset file an input format preset or an output format '
|
|
28
30
|
'preset? The kind decides how the file is migrated.')
|
|
31
|
+
EDIT_TARGET_TITLE = 'Edit configuration'
|
|
32
|
+
EDIT_TARGET_TEXT = (
|
|
33
|
+
'Edit the configuration the application is using, or one in a file '
|
|
34
|
+
'you pick? Saving the configuration in use writes the file it was '
|
|
35
|
+
'loaded from, and Save as… in the editor writes any other file.')
|
|
29
36
|
_Choice = TypeVar('_Choice')
|
|
30
37
|
|
|
31
38
|
|
|
@@ -52,6 +59,14 @@ class SourceChoice(Enum):
|
|
|
52
59
|
CANCEL = 'cancel'
|
|
53
60
|
|
|
54
61
|
|
|
62
|
+
class EditTargetChoice(Enum):
|
|
63
|
+
"""Whether the editor opens the configuration in use or one in a file."""
|
|
64
|
+
|
|
65
|
+
IN_USE = 'in_use'
|
|
66
|
+
FROM_FILE = 'from_file'
|
|
67
|
+
CANCEL = 'cancel'
|
|
68
|
+
|
|
69
|
+
|
|
55
70
|
# pylint: disable-next=too-few-public-methods
|
|
56
71
|
class ButtonChoiceDialog(Generic[_Choice]):
|
|
57
72
|
"""Modal dialog presenting a column of single-choice buttons.
|
|
@@ -130,3 +145,12 @@ def ask_source_choice(parent: tk.Misc, title: str, text: str) -> SourceChoice:
|
|
|
130
145
|
('Cancel', SourceChoice.CANCEL)]
|
|
131
146
|
return ButtonChoiceDialog(parent, title, text, options,
|
|
132
147
|
SourceChoice.CANCEL).choice
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def ask_edit_target(parent: tk.Misc) -> EditTargetChoice:
|
|
151
|
+
"""Ask whether to edit the configuration in use or one in a file."""
|
|
152
|
+
options = [('The configuration in use', EditTargetChoice.IN_USE),
|
|
153
|
+
('A configuration file…', EditTargetChoice.FROM_FILE),
|
|
154
|
+
('Cancel', EditTargetChoice.CANCEL)]
|
|
155
|
+
return ButtonChoiceDialog(parent, EDIT_TARGET_TITLE, EDIT_TARGET_TEXT,
|
|
156
|
+
options, EditTargetChoice.CANCEL).choice
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Edit a configuration in a window of the application.
|
|
3
|
+
|
|
4
|
+
The two edit actions of the configuration menu live here.
|
|
5
|
+
:func:`edit_config` opens the configuration the application is using, or one
|
|
6
|
+
in a file the user picks; :func:`edit_preset_file` opens a stand-alone input
|
|
7
|
+
or output preset file, whose direction is detected from the file itself.
|
|
8
|
+
What can be edited, what a save writes, and what each member is for all
|
|
9
|
+
belong to :mod:`backlogops.config_editing`, so the editor of the terminal
|
|
10
|
+
interface shows exactly the same configuration.
|
|
11
|
+
|
|
12
|
+
These are functions taking the application rather than a collaborator
|
|
13
|
+
object, because an editing session keeps nothing between two of them: the
|
|
14
|
+
model belongs to the session and everything else is the application's.
|
|
15
|
+
|
|
16
|
+
The editor is mounted in a :class:`tkinter.Toplevel` this module creates,
|
|
17
|
+
rather than started through ``edit_cfg_json_tk.edit``. That entry point
|
|
18
|
+
creates a ``tkinter.Tk`` and an event loop of its own, which is for an
|
|
19
|
+
application that runs neither yet: a second Tcl interpreter shares nothing
|
|
20
|
+
with the first, and a nested loop would not end when the editor window
|
|
21
|
+
closed, because Tcl runs its loop while any window of the process lives.
|
|
22
|
+
``EditorWidgets`` is what the library offers for a window an application
|
|
23
|
+
owns, and it takes the close action as an argument so that the editor never
|
|
24
|
+
destroys a window it did not create.
|
|
25
|
+
|
|
26
|
+
The window is not made modal. The editor opens dialogs of its own — a file
|
|
27
|
+
chooser for Save as…, a question before it overwrites a file, and one asking
|
|
28
|
+
for the key of a new entry — and a grab held by the editor window would keep
|
|
29
|
+
their clicks and keys from reaching them.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
33
|
+
# MIT License
|
|
34
|
+
|
|
35
|
+
import tkinter as tk
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Callable, NamedTuple, Optional, TYPE_CHECKING
|
|
38
|
+
from edit_cfg_json import EditModel
|
|
39
|
+
from edit_cfg_json_tk.tk_editor import EditorWidgets
|
|
40
|
+
from wizard_tk_bridge.close_binding import bind_close
|
|
41
|
+
from backlogops import (
|
|
42
|
+
BacklogOpsConfig, default_edit_config, edit_model_for, io_preset_class)
|
|
43
|
+
from backlogops_gui.choice_dialogs import EditTargetChoice, ask_edit_target
|
|
44
|
+
from backlogops_gui.file_choosers import (
|
|
45
|
+
choose_config_to_edit, choose_preset_to_edit)
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from backlogops_gui.application import BacklogApp
|
|
49
|
+
|
|
50
|
+
EDIT_ERRORS = (ValueError, TypeError, KeyError, OSError)
|
|
51
|
+
"""Errors raised when a configuration cannot be opened for editing."""
|
|
52
|
+
|
|
53
|
+
OPEN_FAIL_TITLE = 'Could not open the configuration'
|
|
54
|
+
NO_CONFIG_TEXT = 'There is no configuration to edit.'
|
|
55
|
+
NOT_SAVED_TEXT = 'The editor was closed without saving anything.'
|
|
56
|
+
EDITOR_TITLE = 'Configuration editor'
|
|
57
|
+
ADOPTED = 'The saved configuration is now the one in use.'
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EditorWindow(NamedTuple):
|
|
61
|
+
"""One editor window and the widgets that have to be kept with it.
|
|
62
|
+
|
|
63
|
+
The widgets are carried beside the window because a ``StringVar`` unsets
|
|
64
|
+
its Tcl variable when it is collected, and the field it belongs to would
|
|
65
|
+
then lose both its text and the callback that writes into the model.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
window: tk.Toplevel
|
|
69
|
+
widgets: EditorWidgets
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def editor_window(parent: tk.Misc, model: EditModel,
|
|
73
|
+
title: str) -> EditorWindow:
|
|
74
|
+
"""Create a window of the application with the editor mounted in it.
|
|
75
|
+
|
|
76
|
+
Every way out of the editor — its Close button, its key, the close
|
|
77
|
+
button of the window and the platform close key — goes through the
|
|
78
|
+
editor's own close action, so none of them can drop an unsaved change
|
|
79
|
+
without asking. Closing destroys this window and nothing else.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
parent: Widget the window belongs to, which is the main window.
|
|
83
|
+
model: Model of the editing session to show.
|
|
84
|
+
title: Title of the window, saying what is being edited.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The window and the widgets mounted in it.
|
|
88
|
+
"""
|
|
89
|
+
window = tk.Toplevel(parent)
|
|
90
|
+
window.title(title)
|
|
91
|
+
if isinstance(parent, tk.Wm):
|
|
92
|
+
window.transient(parent)
|
|
93
|
+
widgets = EditorWidgets(parent=window, model=model,
|
|
94
|
+
on_close=window.destroy)
|
|
95
|
+
window.protocol('WM_DELETE_WINDOW', widgets.close_editor)
|
|
96
|
+
bind_close(window, widgets.close_editor)
|
|
97
|
+
return EditorWindow(window=window, widgets=widgets)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def open_editor_window(parent: tk.Misc, model: EditModel, title: str) -> None:
|
|
101
|
+
"""Show one edit model in a window of its own until it is closed.
|
|
102
|
+
|
|
103
|
+
The window and its widgets are held by the local name for as long as
|
|
104
|
+
this call waits for the window, which is as long as they are needed.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
parent: Widget the window belongs to, which is the main window.
|
|
108
|
+
model: Model of the editing session to show.
|
|
109
|
+
title: Title of the window, saying what is being edited.
|
|
110
|
+
"""
|
|
111
|
+
mounted = editor_window(parent, model, title)
|
|
112
|
+
mounted.window.wait_window()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def edit_config(app: 'BacklogApp') -> None:
|
|
116
|
+
"""Edit the configuration in use, or one in a file the user picks.
|
|
117
|
+
|
|
118
|
+
A configuration the editor saved becomes the active configuration,
|
|
119
|
+
whichever of the two was edited, because the user has just said that
|
|
120
|
+
those are the values they want. Cancelling any step, and closing the
|
|
121
|
+
editor without saving, leaves everything as it was.
|
|
122
|
+
"""
|
|
123
|
+
choice = ask_edit_target(app.root)
|
|
124
|
+
if choice is EditTargetChoice.CANCEL:
|
|
125
|
+
return
|
|
126
|
+
model = (_in_use_model(app) if choice is EditTargetChoice.IN_USE
|
|
127
|
+
else _config_file_model(app))
|
|
128
|
+
if model is None:
|
|
129
|
+
return
|
|
130
|
+
open_editor_window(app.root, model, _config_title(model))
|
|
131
|
+
_adopt_saved(app, model)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _config_title(model: EditModel) -> str:
|
|
135
|
+
"""Return the window title, naming the file when a save has one."""
|
|
136
|
+
if model.out_file is None:
|
|
137
|
+
return 'Edit configuration'
|
|
138
|
+
return f'Edit configuration {Path(model.out_file).name}'
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def edit_preset_file(app: 'BacklogApp') -> None:
|
|
142
|
+
"""Edit a stand-alone input or output preset file.
|
|
143
|
+
|
|
144
|
+
The direction of the file is detected from its own contents, so the user
|
|
145
|
+
picks a preset file and nothing else. What the editor writes is a file
|
|
146
|
+
and not a configuration of the application, so nothing is adopted.
|
|
147
|
+
"""
|
|
148
|
+
path = choose_preset_to_edit(app.root)
|
|
149
|
+
if path is None:
|
|
150
|
+
return
|
|
151
|
+
model = _built(app, lambda: edit_model_for(
|
|
152
|
+
default_edit_config(io_preset_class(path)), in_file=path,
|
|
153
|
+
stderr_file=app.log))
|
|
154
|
+
if model is None:
|
|
155
|
+
return
|
|
156
|
+
open_editor_window(app.root, model, f'Edit preset {Path(path).name}')
|
|
157
|
+
_report_saved(app, model, 'Preset saved')
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _in_use_model(app: 'BacklogApp') -> Optional[EditModel]:
|
|
161
|
+
"""Return the model editing the configuration the application uses.
|
|
162
|
+
|
|
163
|
+
A save writes the file the configuration was loaded from, when it came
|
|
164
|
+
from one; a configuration that came from the wizard has no file yet and
|
|
165
|
+
the editor asks for one before it can save.
|
|
166
|
+
"""
|
|
167
|
+
config = app.config
|
|
168
|
+
if config is None:
|
|
169
|
+
app.show_error('No configuration', NO_CONFIG_TEXT)
|
|
170
|
+
return None
|
|
171
|
+
return _built(app, lambda: edit_model_for(config,
|
|
172
|
+
out_file=_loaded_file(app),
|
|
173
|
+
stderr_file=app.log))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _config_file_model(app: 'BacklogApp') -> Optional[EditModel]:
|
|
177
|
+
"""Return the model editing a configuration file the user picks."""
|
|
178
|
+
path = choose_config_to_edit(app.root)
|
|
179
|
+
if path is None:
|
|
180
|
+
return None
|
|
181
|
+
return _built(app, lambda: edit_model_for(
|
|
182
|
+
default_edit_config(BacklogOpsConfig), in_file=path,
|
|
183
|
+
stderr_file=app.log))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _loaded_file(app: 'BacklogApp') -> Optional[str]:
|
|
187
|
+
"""Return the file the active configuration was loaded from, if any.
|
|
188
|
+
|
|
189
|
+
The source of a configuration is either a file name or a phrase saying
|
|
190
|
+
where else it came from, so only a name that is a file now is a
|
|
191
|
+
destination that a save may write.
|
|
192
|
+
"""
|
|
193
|
+
source = app.config_source
|
|
194
|
+
if source is None or not Path(source).is_file():
|
|
195
|
+
return None
|
|
196
|
+
return source
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _built(app: 'BacklogApp',
|
|
200
|
+
build: Callable[[], EditModel]) -> Optional[EditModel]:
|
|
201
|
+
"""Return the model that ``build`` makes, reporting a refusal."""
|
|
202
|
+
try:
|
|
203
|
+
return build()
|
|
204
|
+
except EDIT_ERRORS as error:
|
|
205
|
+
app.log.write(f'{OPEN_FAIL_TITLE}: {error}\n')
|
|
206
|
+
app.show_error(OPEN_FAIL_TITLE, str(error))
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _adopt_saved(app: 'BacklogApp', model: EditModel) -> None:
|
|
211
|
+
"""Make what the editor saved the configuration the application uses."""
|
|
212
|
+
saved = model.saved_config
|
|
213
|
+
if saved is None:
|
|
214
|
+
app.show_info(EDITOR_TITLE, NOT_SAVED_TEXT)
|
|
215
|
+
return
|
|
216
|
+
assert isinstance(saved, BacklogOpsConfig)
|
|
217
|
+
app.adopt_config(saved, str(model.out_file))
|
|
218
|
+
app.show_info('Configuration saved', f'{model.save_message}\n{ADOPTED}')
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _report_saved(app: 'BacklogApp', model: EditModel, title: str) -> None:
|
|
222
|
+
"""Report what the editor wrote, or that it wrote nothing."""
|
|
223
|
+
if model.saved_config is None:
|
|
224
|
+
app.show_info(EDITOR_TITLE, NOT_SAVED_TEXT)
|
|
225
|
+
return
|
|
226
|
+
app.show_info(title, model.save_message)
|
|
@@ -48,6 +48,19 @@ def choose_existing_preset(parent: tk.Misc) -> Optional[str]:
|
|
|
48
48
|
return name or None
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def choose_config_to_edit(parent: tk.Misc) -> Optional[str]:
|
|
52
|
+
"""Ask for an existing configuration file to edit, or None to cancel."""
|
|
53
|
+
name = filedialog.askopenfilename(parent=parent,
|
|
54
|
+
title='Edit configuration file')
|
|
55
|
+
return name or None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def choose_preset_to_edit(parent: tk.Misc) -> Optional[str]:
|
|
59
|
+
"""Ask for an existing preset file to edit, or None when cancelled."""
|
|
60
|
+
name = filedialog.askopenfilename(parent=parent, title='Edit preset file')
|
|
61
|
+
return name or None
|
|
62
|
+
|
|
63
|
+
|
|
51
64
|
def choose_preset_to_migrate(parent: tk.Misc) -> Optional[str]:
|
|
52
65
|
"""Ask for an existing preset file to migrate, or None when cancelled."""
|
|
53
66
|
name = filedialog.askopenfilename(parent=parent,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|