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.
Files changed (43) hide show
  1. {backlogops_gui-0.8 → backlogops_gui-0.9}/PKG-INFO +18 -6
  2. {backlogops_gui-0.8 → backlogops_gui-0.9}/README_pypi.md +14 -3
  3. {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/PKG-INFO +18 -6
  4. {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/SOURCES.txt +1 -0
  5. {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/requires.txt +3 -2
  6. {backlogops_gui-0.8 → backlogops_gui-0.9}/setup.py +4 -3
  7. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/application.py +48 -25
  8. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/choice_dialogs.py +26 -2
  9. backlogops_gui-0.9/src/backlogops_gui/config_edit.py +226 -0
  10. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/file_choosers.py +13 -0
  11. {backlogops_gui-0.8 → backlogops_gui-0.9}/LICENSE.txt +0 -0
  12. {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/dependency_links.txt +0 -0
  13. {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/entry_points.txt +0 -0
  14. {backlogops_gui-0.8 → backlogops_gui-0.9}/backlogops_gui.egg-info/top_level.txt +0 -0
  15. {backlogops_gui-0.8 → backlogops_gui-0.9}/pyproject.toml +0 -0
  16. {backlogops_gui-0.8 → backlogops_gui-0.9}/setup.cfg +0 -0
  17. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/__init__.py +0 -0
  18. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/__main__.py +0 -0
  19. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/_migrate_warn.py +0 -0
  20. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/backlog_actions.py +0 -0
  21. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/backlog_dialogs.py +0 -0
  22. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/backlog_io.py +0 -0
  23. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/backlog_window.py +0 -0
  24. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/blog_version_reporter.py +0 -0
  25. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/format_dialogs.py +0 -0
  26. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_actions.py +0 -0
  27. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_base.py +0 -0
  28. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_dialogs.py +0 -0
  29. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_order.py +0 -0
  30. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_rank.py +0 -0
  31. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_read.py +0 -0
  32. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_rename.py +0 -0
  33. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_update.py +0 -0
  34. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/jira_write.py +0 -0
  35. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/key_list_box.py +0 -0
  36. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/log_buffer.py +0 -0
  37. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/modal_dialog.py +0 -0
  38. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/py.typed +0 -0
  39. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/python_version.py +0 -0
  40. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/report_windows.py +0 -0
  41. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/table_view.py +0 -0
  42. {backlogops_gui-0.8 → backlogops_gui-0.9}/src/backlogops_gui/tcltk_version.py +0 -0
  43. {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.8
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.8
25
- Requires-Dist: argcomplete>=3.7.1
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: 2173 passed, 1 deselected in 35s
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.8
359
- - Build and test using Python 3.14.6
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: 2173 passed, 1 deselected in 35s
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.8
326
- - Build and test using Python 3.14.6
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.8
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.8
25
- Requires-Dist: argcomplete>=3.7.1
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: 2173 passed, 1 deselected in 35s
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.8
359
- - Build and test using Python 3.14.6
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
@@ -1,5 +1,6 @@
1
- backlogops>=0.8
2
- argcomplete>=3.7.1
1
+ backlogops>=0.9
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
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.8',
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.8',
23
- 'argcomplete >= 3.7.1',
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, creates a stand-alone input or output
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, GuiDisplayConfig,
45
- InputFormatConfig, Levels, OutputFormatConfig, Status, get_demo_backlog,
46
- get_backlog_ops_config, backlog_ops_wizard, preset_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, create a stand-alone input '
76
- 'or output preset file, migrate a preset file to the current format, '
77
- 'write the current configuration to a file, or create a demonstration '
78
- 'backlog. Each backlog opens in its own window. On macOS the menu bar '
79
- 'is at the top of the display.')
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 Jira collaborators."""
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 = config
218
- self.config_source = (config_arg if config_arg is not None
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 = 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 = 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 = 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 = 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. All
10
- three are built from the same :class:`ButtonChoiceDialog`.
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