backlogops-gui 0.9__tar.gz → 1.1__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 (44) hide show
  1. {backlogops_gui-0.9 → backlogops_gui-1.1}/PKG-INFO +14 -11
  2. {backlogops_gui-0.9 → backlogops_gui-1.1}/README_pypi.md +11 -8
  3. {backlogops_gui-0.9 → backlogops_gui-1.1}/backlogops_gui.egg-info/PKG-INFO +14 -11
  4. {backlogops_gui-0.9 → backlogops_gui-1.1}/backlogops_gui.egg-info/requires.txt +2 -2
  5. {backlogops_gui-0.9 → backlogops_gui-1.1}/setup.py +3 -3
  6. backlogops_gui-1.1/src/backlogops_gui/config_edit.py +265 -0
  7. backlogops_gui-0.9/src/backlogops_gui/config_edit.py +0 -226
  8. {backlogops_gui-0.9 → backlogops_gui-1.1}/LICENSE.txt +0 -0
  9. {backlogops_gui-0.9 → backlogops_gui-1.1}/backlogops_gui.egg-info/SOURCES.txt +0 -0
  10. {backlogops_gui-0.9 → backlogops_gui-1.1}/backlogops_gui.egg-info/dependency_links.txt +0 -0
  11. {backlogops_gui-0.9 → backlogops_gui-1.1}/backlogops_gui.egg-info/entry_points.txt +0 -0
  12. {backlogops_gui-0.9 → backlogops_gui-1.1}/backlogops_gui.egg-info/top_level.txt +0 -0
  13. {backlogops_gui-0.9 → backlogops_gui-1.1}/pyproject.toml +0 -0
  14. {backlogops_gui-0.9 → backlogops_gui-1.1}/setup.cfg +0 -0
  15. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/__init__.py +0 -0
  16. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/__main__.py +0 -0
  17. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/_migrate_warn.py +0 -0
  18. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/application.py +0 -0
  19. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/backlog_actions.py +0 -0
  20. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/backlog_dialogs.py +0 -0
  21. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/backlog_io.py +0 -0
  22. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/backlog_window.py +0 -0
  23. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/blog_version_reporter.py +0 -0
  24. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/choice_dialogs.py +0 -0
  25. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/file_choosers.py +0 -0
  26. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/format_dialogs.py +0 -0
  27. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/jira_actions.py +0 -0
  28. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/jira_base.py +0 -0
  29. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/jira_dialogs.py +0 -0
  30. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/jira_order.py +0 -0
  31. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/jira_rank.py +0 -0
  32. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/jira_read.py +0 -0
  33. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/jira_rename.py +0 -0
  34. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/jira_update.py +0 -0
  35. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/jira_write.py +0 -0
  36. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/key_list_box.py +0 -0
  37. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/log_buffer.py +0 -0
  38. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/modal_dialog.py +0 -0
  39. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/py.typed +0 -0
  40. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/python_version.py +0 -0
  41. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/report_windows.py +0 -0
  42. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/table_view.py +0 -0
  43. {backlogops_gui-0.9 → backlogops_gui-1.1}/src/backlogops_gui/tcltk_version.py +0 -0
  44. {backlogops_gui-0.9 → backlogops_gui-1.1}/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.9
3
+ Version: 1.1
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,11 +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.9
24
+ Requires-Dist: backlogops>=1.1
25
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
+ Requires-Dist: edit-cfg-json-tk>=0.1.0
29
29
  Requires-Dist: versionreporter>=0.4
30
30
  Dynamic: author
31
31
  Dynamic: license-file
@@ -286,12 +286,15 @@ is in the menus.
286
286
  closed for vacation, and preset configuration for the inputs or
287
287
  outputs you want to use.
288
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.
289
+ - Edit configuration...: Show the whole configuration in a folding,
290
+ searchable editor, so you can change one value without stepping
291
+ through the wizard. It asks whether to edit the configuration the
292
+ application is using or one in a file you pick. A preset, a Jira
293
+ connection, a person, a team, a level and an entry of any of the maps
294
+ can be added beside the ones that are there, taken out again, and
295
+ moved within a list. Saving validates the configuration first, keeps
296
+ what it writes over as a `.bak` file, and makes what it wrote the
297
+ configuration in use.
295
298
 
296
299
  - Create IO preset file...: Create a stand-alone input or output preset
297
300
  configuration file via a wizard.
@@ -362,10 +365,10 @@ one with the list of releases. You will want to use the menus.
362
365
 
363
366
  ## Test summary
364
367
 
365
- - Test result: 2232 passed, 1 deselected in 44s
368
+ - Test result: 2298 passed, 1 deselected in 57s
366
369
  - No flake8 warnings.
367
370
  - No mypy errors found.
368
371
  - No pylint warnings.
369
372
  - No python layout warnings.
370
- - Built version(s): 0.9
373
+ - Built version(s): 1.1
371
374
  - Build and test using Python 3.14.7
@@ -252,12 +252,15 @@ 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.
255
+ - Edit configuration...: Show the whole configuration in a folding,
256
+ searchable editor, so you can change one value without stepping
257
+ through the wizard. It asks whether to edit the configuration the
258
+ application is using or one in a file you pick. A preset, a Jira
259
+ connection, a person, a team, a level and an entry of any of the maps
260
+ can be added beside the ones that are there, taken out again, and
261
+ moved within a list. Saving validates the configuration first, keeps
262
+ what it writes over as a `.bak` file, and makes what it wrote the
263
+ configuration in use.
261
264
 
262
265
  - Create IO preset file...: Create a stand-alone input or output preset
263
266
  configuration file via a wizard.
@@ -328,10 +331,10 @@ one with the list of releases. You will want to use the menus.
328
331
 
329
332
  ## Test summary
330
333
 
331
- - Test result: 2232 passed, 1 deselected in 44s
334
+ - Test result: 2298 passed, 1 deselected in 57s
332
335
  - No flake8 warnings.
333
336
  - No mypy errors found.
334
337
  - No pylint warnings.
335
338
  - No python layout warnings.
336
- - Built version(s): 0.9
339
+ - Built version(s): 1.1
337
340
  - 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.9
3
+ Version: 1.1
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,11 +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.9
24
+ Requires-Dist: backlogops>=1.1
25
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
+ Requires-Dist: edit-cfg-json-tk>=0.1.0
29
29
  Requires-Dist: versionreporter>=0.4
30
30
  Dynamic: author
31
31
  Dynamic: license-file
@@ -286,12 +286,15 @@ is in the menus.
286
286
  closed for vacation, and preset configuration for the inputs or
287
287
  outputs you want to use.
288
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.
289
+ - Edit configuration...: Show the whole configuration in a folding,
290
+ searchable editor, so you can change one value without stepping
291
+ through the wizard. It asks whether to edit the configuration the
292
+ application is using or one in a file you pick. A preset, a Jira
293
+ connection, a person, a team, a level and an entry of any of the maps
294
+ can be added beside the ones that are there, taken out again, and
295
+ moved within a list. Saving validates the configuration first, keeps
296
+ what it writes over as a `.bak` file, and makes what it wrote the
297
+ configuration in use.
295
298
 
296
299
  - Create IO preset file...: Create a stand-alone input or output preset
297
300
  configuration file via a wizard.
@@ -362,10 +365,10 @@ one with the list of releases. You will want to use the menus.
362
365
 
363
366
  ## Test summary
364
367
 
365
- - Test result: 2232 passed, 1 deselected in 44s
368
+ - Test result: 2298 passed, 1 deselected in 57s
366
369
  - No flake8 warnings.
367
370
  - No mypy errors found.
368
371
  - No pylint warnings.
369
372
  - No python layout warnings.
370
- - Built version(s): 0.9
373
+ - Built version(s): 1.1
371
374
  - Build and test using Python 3.14.7
@@ -1,6 +1,6 @@
1
- backlogops>=0.9
1
+ backlogops>=1.1
2
2
  argcomplete>=3.7.2
3
3
  wizard-ui-bridge>=1.3
4
4
  wizard-tk-bridge>=1.3
5
- edit-cfg-json-tk==0.0.2
5
+ edit-cfg-json-tk>=0.1.0
6
6
  versionreporter>=0.4
@@ -5,7 +5,7 @@ from setuptools import setup
5
5
 
6
6
  setup(
7
7
  name='backlogops-gui',
8
- version='0.9',
8
+ version='1.1',
9
9
  description='Graphical user interface for backlog operations.',
10
10
  author='Tom Björkholm',
11
11
  author_email='klausuler_linnet0q@icloud.com',
@@ -19,11 +19,11 @@ setup(
19
19
  ]
20
20
  },
21
21
  install_requires=[
22
- 'backlogops >= 0.9',
22
+ 'backlogops >= 1.1',
23
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
+ 'edit-cfg-json-tk >= 0.1.0',
27
27
  'versionreporter >= 0.4'
28
28
  ]
29
29
  )
@@ -0,0 +1,265 @@
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
+ session belongs to the window 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
+ ``TkEditorPanel`` is what the library offers for an application that already
23
+ runs Tk. It is given the window as its ``area``, so that the window stays
24
+ this module's — its title names the file being edited, and the editor
25
+ destroys only what it built itself. Its ``parent`` is the other way of
26
+ saying this, and it is not used here: the library would then own the window
27
+ and name it after the configuration class, and the file being edited is
28
+ what a user of this application needs to read there.
29
+
30
+ The window is not made modal. The editor opens dialogs of its own — a file
31
+ chooser for Save as…, one asking for the key of a new entry, a question
32
+ before it overwrites a file and a question before it drops unsaved changes
33
+ — and a grab held by the editor window would keep their clicks and keys
34
+ from reaching them.
35
+ """
36
+
37
+ # Copyright (c) 2026, Tom Björkholm
38
+ # MIT License
39
+
40
+ import tkinter as tk
41
+ from pathlib import Path
42
+ from typing import NamedTuple, Optional, TYPE_CHECKING
43
+ from config_as_json import Config, PathOrStr
44
+ from edit_cfg_json import ConfigLoadError, EditModel, default_config
45
+ from edit_cfg_json_tk import TkEditorPanel
46
+ from wizard_tk_bridge.close_binding import bind_close
47
+ from backlogops import (
48
+ BacklogOpsConfig, EDIT_SETTINGS, descriptions_for, io_preset_class)
49
+ from backlogops_gui.choice_dialogs import EditTargetChoice, ask_edit_target
50
+ from backlogops_gui.file_choosers import (
51
+ choose_config_to_edit, choose_preset_to_edit)
52
+
53
+ if TYPE_CHECKING:
54
+ from backlogops_gui.application import BacklogApp
55
+
56
+ EDIT_ERRORS = (ConfigLoadError, ValueError, TypeError, KeyError, OSError)
57
+ """Errors raised when a configuration cannot be opened for editing."""
58
+
59
+ OPEN_FAIL_TITLE = 'Could not open the configuration'
60
+ NO_CONFIG_TEXT = 'There is no configuration to edit.'
61
+ NOT_SAVED_TEXT = 'The editor was closed without saving anything.'
62
+ EDITOR_TITLE = 'Configuration editor'
63
+ ADOPTED = 'The saved configuration is now the one in use.'
64
+
65
+
66
+ class EditorWindow(NamedTuple):
67
+ """One editor window and the panel running the session inside it."""
68
+
69
+ window: tk.Toplevel
70
+ panel: TkEditorPanel
71
+
72
+
73
+ def editor_window(app: 'BacklogApp', config: Config, title: str, *,
74
+ in_file: Optional[PathOrStr] = None,
75
+ out_file: Optional[PathOrStr] = None) -> EditorWindow:
76
+ """Create a window of the application with the editor mounted in it.
77
+
78
+ Every way out of the editor — its Close button, its key, the close
79
+ button of the window and the platform close key — goes through the
80
+ editor's own close action, so none of them can drop an unsaved change
81
+ without asking. Closing takes the editor off this window, and the
82
+ window then goes with it.
83
+
84
+ Args:
85
+ app: The application, whose window this one belongs to.
86
+ config: Configuration object of the class to edit, holding the
87
+ values to start from when there is no input file.
88
+ title: Title of the window, saying what is being edited.
89
+ in_file: Configuration file to read, or None to edit the values
90
+ that ``config`` holds.
91
+ out_file: Configuration file a save writes, or None to write the
92
+ input file. With neither, the editor asks the user for one.
93
+
94
+ Returns:
95
+ The window and the panel that the editor runs in.
96
+
97
+ Raises:
98
+ ConfigLoadError: The input file cannot be opened for editing. The
99
+ window is taken away again before this is raised.
100
+ """
101
+ window = tk.Toplevel(app.root)
102
+ window.title(title)
103
+ window.transient(app.root)
104
+ try:
105
+ panel = TkEditorPanel(config, area=window, modal=False,
106
+ on_close=window.destroy,
107
+ descriptions=descriptions_for(config),
108
+ in_file=in_file, out_file=out_file,
109
+ settings=EDIT_SETTINGS, stderr_file=app.log)
110
+ except EDIT_ERRORS:
111
+ window.destroy()
112
+ raise
113
+ window.protocol('WM_DELETE_WINDOW', panel.close)
114
+ bind_close(window, panel.close)
115
+ return EditorWindow(window=window, panel=panel)
116
+
117
+
118
+ def edit_in_window(app: 'BacklogApp', config: Config, title: str, *,
119
+ in_file: Optional[PathOrStr] = None,
120
+ out_file: Optional[PathOrStr] = None) -> EditModel:
121
+ """Show one configuration in a window of its own until it is closed.
122
+
123
+ Args:
124
+ app, config, title, in_file, out_file: As of :func:`editor_window`.
125
+
126
+ Returns:
127
+ The model of the session that has just ended, which says what it
128
+ saved.
129
+
130
+ Raises:
131
+ ConfigLoadError: The input file cannot be opened for editing.
132
+ """
133
+ mounted = editor_window(app, config, title, in_file=in_file,
134
+ out_file=out_file)
135
+ mounted.window.wait_window()
136
+ return mounted.panel.model
137
+
138
+
139
+ def open_editor_window(app: 'BacklogApp', config: Config, title: str, *,
140
+ in_file: Optional[PathOrStr] = None,
141
+ out_file: Optional[PathOrStr] = None
142
+ ) -> Optional[EditModel]:
143
+ """Run one editing session in a window, or report why it cannot run.
144
+
145
+ Args:
146
+ app, config, title, in_file, out_file: As of :func:`editor_window`.
147
+
148
+ Returns:
149
+ The model of the session that ran, or None when the configuration
150
+ could not be opened for editing, which is then reported.
151
+ """
152
+ try:
153
+ return edit_in_window(app, config, title, in_file=in_file,
154
+ out_file=out_file)
155
+ except EDIT_ERRORS as error:
156
+ _report_failure(app, error)
157
+ return None
158
+
159
+
160
+ def edit_config(app: 'BacklogApp') -> None:
161
+ """Edit the configuration in use, or one in a file the user picks.
162
+
163
+ A configuration the editor saved becomes the active configuration,
164
+ whichever of the two was edited, because the user has just said that
165
+ those are the values they want. Cancelling any step, and closing the
166
+ editor without saving, leaves everything as it was.
167
+ """
168
+ choice = ask_edit_target(app.root)
169
+ if choice is EditTargetChoice.CANCEL:
170
+ return
171
+ model = (_edit_in_use(app) if choice is EditTargetChoice.IN_USE
172
+ else _edit_config_file(app))
173
+ if model is not None:
174
+ _adopt_saved(app, model)
175
+
176
+
177
+ def edit_preset_file(app: 'BacklogApp') -> None:
178
+ """Edit a stand-alone input or output preset file.
179
+
180
+ The direction of the file is detected from its own contents, so the user
181
+ picks a preset file and nothing else. What the editor writes is a file
182
+ and not a configuration of the application, so nothing is adopted.
183
+ """
184
+ path = choose_preset_to_edit(app.root)
185
+ if path is None:
186
+ return
187
+ try:
188
+ config = default_config(io_preset_class(path))
189
+ except EDIT_ERRORS as error:
190
+ _report_failure(app, error)
191
+ return
192
+ model = open_editor_window(app, config, f'Edit preset {Path(path).name}',
193
+ in_file=path)
194
+ if model is not None:
195
+ _report_saved(app, model, 'Preset saved')
196
+
197
+
198
+ def _edit_in_use(app: 'BacklogApp') -> Optional[EditModel]:
199
+ """Edit the configuration the application uses, in a window.
200
+
201
+ A save writes the file the configuration was loaded from, when it came
202
+ from one; a configuration that came from the wizard has no file yet and
203
+ the editor asks for one before it can save.
204
+ """
205
+ config = app.config
206
+ if config is None:
207
+ app.show_error('No configuration', NO_CONFIG_TEXT)
208
+ return None
209
+ out_file = _loaded_file(app)
210
+ return open_editor_window(app, config, _config_title(out_file),
211
+ out_file=out_file)
212
+
213
+
214
+ def _edit_config_file(app: 'BacklogApp') -> Optional[EditModel]:
215
+ """Edit a configuration file the user picks, in a window."""
216
+ path = choose_config_to_edit(app.root)
217
+ if path is None:
218
+ return None
219
+ return open_editor_window(app, default_config(BacklogOpsConfig),
220
+ _config_title(path), in_file=path)
221
+
222
+
223
+ def _config_title(name: Optional[str]) -> str:
224
+ """Return the window title, naming the file when the session has one."""
225
+ if name is None:
226
+ return 'Edit configuration'
227
+ return f'Edit configuration {Path(name).name}'
228
+
229
+
230
+ def _loaded_file(app: 'BacklogApp') -> Optional[str]:
231
+ """Return the file the active configuration was loaded from, if any.
232
+
233
+ The source of a configuration is either a file name or a phrase saying
234
+ where else it came from, so only a name that is a file now is a
235
+ destination that a save may write.
236
+ """
237
+ source = app.config_source
238
+ if source is None or not Path(source).is_file():
239
+ return None
240
+ return source
241
+
242
+
243
+ def _report_failure(app: 'BacklogApp', error: Exception) -> None:
244
+ """Log and show why a configuration cannot be opened for editing."""
245
+ app.log.write(f'{OPEN_FAIL_TITLE}: {error}\n')
246
+ app.show_error(OPEN_FAIL_TITLE, str(error))
247
+
248
+
249
+ def _adopt_saved(app: 'BacklogApp', model: EditModel) -> None:
250
+ """Make what the editor saved the configuration the application uses."""
251
+ saved = model.saved_config
252
+ if saved is None:
253
+ app.show_info(EDITOR_TITLE, NOT_SAVED_TEXT)
254
+ return
255
+ assert isinstance(saved, BacklogOpsConfig)
256
+ app.adopt_config(saved, str(model.out_file))
257
+ app.show_info('Configuration saved', f'{model.save_message}\n{ADOPTED}')
258
+
259
+
260
+ def _report_saved(app: 'BacklogApp', model: EditModel, title: str) -> None:
261
+ """Report what the editor wrote, or that it wrote nothing."""
262
+ if model.saved_config is None:
263
+ app.show_info(EDITOR_TITLE, NOT_SAVED_TEXT)
264
+ return
265
+ app.show_info(title, model.save_message)
@@ -1,226 +0,0 @@
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)
File without changes
File without changes