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