backlogops-gui 0.4__tar.gz → 0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {backlogops_gui-0.4 → backlogops_gui-0.6}/PKG-INFO +8 -11
- {backlogops_gui-0.4 → backlogops_gui-0.6}/README_pypi.md +5 -8
- {backlogops_gui-0.4 → backlogops_gui-0.6}/backlogops_gui.egg-info/PKG-INFO +8 -11
- {backlogops_gui-0.4 → backlogops_gui-0.6}/backlogops_gui.egg-info/SOURCES.txt +5 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/backlogops_gui.egg-info/requires.txt +2 -1
- {backlogops_gui-0.4 → backlogops_gui-0.6}/pyproject.toml +0 -1
- {backlogops_gui-0.4 → backlogops_gui-0.6}/setup.py +3 -2
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/application.py +5 -3
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/backlog_dialogs.py +7 -23
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/backlog_io.py +2 -1
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/gui_wizard.py +6 -1
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/jira_dialogs.py +7 -27
- backlogops_gui-0.6/src/backlogops_gui/key_list_box.py +45 -0
- backlogops_gui-0.6/src/backlogops_gui/wizard_calendar.py +170 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/wizard_form.py +228 -44
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/wizard_path.py +12 -1
- backlogops_gui-0.6/src/backlogops_gui/wizard_pick_row.py +154 -0
- backlogops_gui-0.6/src/backlogops_gui/wizard_prefill.py +138 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/wizard_table.py +1 -1
- backlogops_gui-0.6/src/backlogops_gui/wizard_typed.py +286 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/wizard_window.py +1 -1
- {backlogops_gui-0.4 → backlogops_gui-0.6}/LICENSE.txt +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/backlogops_gui.egg-info/dependency_links.txt +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/backlogops_gui.egg-info/entry_points.txt +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/backlogops_gui.egg-info/top_level.txt +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/setup.cfg +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/__init__.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/__main__.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/_migrate_warn.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/auto_scroll.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/backlog_actions.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/backlog_window.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/blog_version_reporter.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/choice_dialogs.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/close_binding.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/file_choosers.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/format_dialogs.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/gui_style.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/jira_actions.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/jira_base.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/jira_order.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/jira_rank.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/jira_read.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/jira_rename.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/jira_update.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/jira_write.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/log_buffer.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/modal_dialog.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/py.typed +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/python_version.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/report_windows.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/table_view.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/tcltk_version.py +0 -0
- {backlogops_gui-0.4 → backlogops_gui-0.6}/src/backlogops_gui/token_dialog.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backlogops-gui
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6
|
|
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>
|
|
@@ -14,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
|
-
Classifier: Development Status :: 4 - Beta
|
|
18
17
|
Classifier: Intended Audience :: End Users/Desktop
|
|
19
18
|
Classifier: Intended Audience :: Information Technology
|
|
20
19
|
Classifier: Topic :: Office/Business :: Scheduling
|
|
@@ -22,8 +21,9 @@ Classifier: Typing :: Typed
|
|
|
22
21
|
Requires-Python: >=3.12
|
|
23
22
|
Description-Content-Type: text/markdown
|
|
24
23
|
License-File: LICENSE.txt
|
|
25
|
-
Requires-Dist: backlogops>=0.
|
|
24
|
+
Requires-Dist: backlogops>=0.6
|
|
26
25
|
Requires-Dist: argcomplete>=3.7.0
|
|
26
|
+
Requires-Dist: wizard-ui-bridge>=1.1
|
|
27
27
|
Requires-Dist: versionreporter>=0.4
|
|
28
28
|
Dynamic: author
|
|
29
29
|
Dynamic: license-file
|
|
@@ -48,15 +48,11 @@ There are 3 related packages for backlog operations:
|
|
|
48
48
|
|
|
49
49
|
## Project status
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
place, so the main work from here is on the user experience rather than on new
|
|
53
|
-
core functionality. The three packages share a version number and are released
|
|
54
|
-
together.
|
|
51
|
+
The three packages share a version number and are released together.
|
|
55
52
|
|
|
56
53
|
The library is not meant to ever be "finished": using it for real planning keeps
|
|
57
54
|
surfacing new ideas. Some of those will be added to backlogops itself, and
|
|
58
|
-
others will fit better in separate packages built on top of backlogops.
|
|
59
|
-
adopters are very welcome, and their feedback shapes what comes next.
|
|
55
|
+
others will fit better in separate packages built on top of backlogops.
|
|
60
56
|
|
|
61
57
|
## Available functionality
|
|
62
58
|
|
|
@@ -353,9 +349,10 @@ one with the list of releases. You will want to use the menus.
|
|
|
353
349
|
|
|
354
350
|
## Test summary
|
|
355
351
|
|
|
356
|
-
- Test result:
|
|
352
|
+
- Test result: 2450 passed, 9 deselected in 39s
|
|
357
353
|
- No flake8 warnings.
|
|
358
354
|
- No mypy errors found.
|
|
355
|
+
- No pylint warnings.
|
|
359
356
|
- No python layout warnings.
|
|
360
|
-
- Built version(s): 0.
|
|
357
|
+
- Built version(s): 0.6
|
|
361
358
|
- Build and test using Python 3.14.6
|
|
@@ -16,15 +16,11 @@ There are 3 related packages for backlog operations:
|
|
|
16
16
|
|
|
17
17
|
## Project status
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
place, so the main work from here is on the user experience rather than on new
|
|
21
|
-
core functionality. The three packages share a version number and are released
|
|
22
|
-
together.
|
|
19
|
+
The three packages share a version number and are released together.
|
|
23
20
|
|
|
24
21
|
The library is not meant to ever be "finished": using it for real planning keeps
|
|
25
22
|
surfacing new ideas. Some of those will be added to backlogops itself, and
|
|
26
|
-
others will fit better in separate packages built on top of backlogops.
|
|
27
|
-
adopters are very welcome, and their feedback shapes what comes next.
|
|
23
|
+
others will fit better in separate packages built on top of backlogops.
|
|
28
24
|
|
|
29
25
|
## Available functionality
|
|
30
26
|
|
|
@@ -321,9 +317,10 @@ one with the list of releases. You will want to use the menus.
|
|
|
321
317
|
|
|
322
318
|
## Test summary
|
|
323
319
|
|
|
324
|
-
- Test result:
|
|
320
|
+
- Test result: 2450 passed, 9 deselected in 39s
|
|
325
321
|
- No flake8 warnings.
|
|
326
322
|
- No mypy errors found.
|
|
323
|
+
- No pylint warnings.
|
|
327
324
|
- No python layout warnings.
|
|
328
|
-
- Built version(s): 0.
|
|
325
|
+
- Built version(s): 0.6
|
|
329
326
|
- Build and test using Python 3.14.6
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backlogops-gui
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6
|
|
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>
|
|
@@ -14,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
|
-
Classifier: Development Status :: 4 - Beta
|
|
18
17
|
Classifier: Intended Audience :: End Users/Desktop
|
|
19
18
|
Classifier: Intended Audience :: Information Technology
|
|
20
19
|
Classifier: Topic :: Office/Business :: Scheduling
|
|
@@ -22,8 +21,9 @@ Classifier: Typing :: Typed
|
|
|
22
21
|
Requires-Python: >=3.12
|
|
23
22
|
Description-Content-Type: text/markdown
|
|
24
23
|
License-File: LICENSE.txt
|
|
25
|
-
Requires-Dist: backlogops>=0.
|
|
24
|
+
Requires-Dist: backlogops>=0.6
|
|
26
25
|
Requires-Dist: argcomplete>=3.7.0
|
|
26
|
+
Requires-Dist: wizard-ui-bridge>=1.1
|
|
27
27
|
Requires-Dist: versionreporter>=0.4
|
|
28
28
|
Dynamic: author
|
|
29
29
|
Dynamic: license-file
|
|
@@ -48,15 +48,11 @@ There are 3 related packages for backlog operations:
|
|
|
48
48
|
|
|
49
49
|
## Project status
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
place, so the main work from here is on the user experience rather than on new
|
|
53
|
-
core functionality. The three packages share a version number and are released
|
|
54
|
-
together.
|
|
51
|
+
The three packages share a version number and are released together.
|
|
55
52
|
|
|
56
53
|
The library is not meant to ever be "finished": using it for real planning keeps
|
|
57
54
|
surfacing new ideas. Some of those will be added to backlogops itself, and
|
|
58
|
-
others will fit better in separate packages built on top of backlogops.
|
|
59
|
-
adopters are very welcome, and their feedback shapes what comes next.
|
|
55
|
+
others will fit better in separate packages built on top of backlogops.
|
|
60
56
|
|
|
61
57
|
## Available functionality
|
|
62
58
|
|
|
@@ -353,9 +349,10 @@ one with the list of releases. You will want to use the menus.
|
|
|
353
349
|
|
|
354
350
|
## Test summary
|
|
355
351
|
|
|
356
|
-
- Test result:
|
|
352
|
+
- Test result: 2450 passed, 9 deselected in 39s
|
|
357
353
|
- No flake8 warnings.
|
|
358
354
|
- No mypy errors found.
|
|
355
|
+
- No pylint warnings.
|
|
359
356
|
- No python layout warnings.
|
|
360
|
-
- Built version(s): 0.
|
|
357
|
+
- Built version(s): 0.6
|
|
361
358
|
- Build and test using Python 3.14.6
|
|
@@ -33,6 +33,7 @@ src/backlogops_gui/jira_read.py
|
|
|
33
33
|
src/backlogops_gui/jira_rename.py
|
|
34
34
|
src/backlogops_gui/jira_update.py
|
|
35
35
|
src/backlogops_gui/jira_write.py
|
|
36
|
+
src/backlogops_gui/key_list_box.py
|
|
36
37
|
src/backlogops_gui/log_buffer.py
|
|
37
38
|
src/backlogops_gui/modal_dialog.py
|
|
38
39
|
src/backlogops_gui/py.typed
|
|
@@ -41,7 +42,11 @@ src/backlogops_gui/report_windows.py
|
|
|
41
42
|
src/backlogops_gui/table_view.py
|
|
42
43
|
src/backlogops_gui/tcltk_version.py
|
|
43
44
|
src/backlogops_gui/token_dialog.py
|
|
45
|
+
src/backlogops_gui/wizard_calendar.py
|
|
44
46
|
src/backlogops_gui/wizard_form.py
|
|
45
47
|
src/backlogops_gui/wizard_path.py
|
|
48
|
+
src/backlogops_gui/wizard_pick_row.py
|
|
49
|
+
src/backlogops_gui/wizard_prefill.py
|
|
46
50
|
src/backlogops_gui/wizard_table.py
|
|
51
|
+
src/backlogops_gui/wizard_typed.py
|
|
47
52
|
src/backlogops_gui/wizard_window.py
|
|
@@ -21,7 +21,6 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.13",
|
|
22
22
|
"Programming Language :: Python :: 3.14",
|
|
23
23
|
"Operating System :: OS Independent",
|
|
24
|
-
"Development Status :: 4 - Beta",
|
|
25
24
|
"Intended Audience :: End Users/Desktop",
|
|
26
25
|
"Intended Audience :: Information Technology",
|
|
27
26
|
"Topic :: Office/Business :: Scheduling",
|
|
@@ -5,7 +5,7 @@ from setuptools import setup
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name='backlogops-gui',
|
|
8
|
-
version='0.
|
|
8
|
+
version='0.6',
|
|
9
9
|
description='Graphical user interface for backlog operations.',
|
|
10
10
|
author='Tom Björkholm',
|
|
11
11
|
author_email='klausuler_linnet0q@icloud.com',
|
|
@@ -19,8 +19,9 @@ setup(
|
|
|
19
19
|
]
|
|
20
20
|
},
|
|
21
21
|
install_requires=[
|
|
22
|
-
'backlogops >= 0.
|
|
22
|
+
'backlogops >= 0.6',
|
|
23
23
|
'argcomplete >= 3.7.0',
|
|
24
|
+
'wizard-ui-bridge >= 1.1',
|
|
24
25
|
'versionreporter >= 0.4'
|
|
25
26
|
]
|
|
26
27
|
)
|
|
@@ -538,8 +538,9 @@ class BacklogApp:
|
|
|
538
538
|
return
|
|
539
539
|
value = options.config_value
|
|
540
540
|
try:
|
|
541
|
-
data = read_backlog(path, value, presets, self.log,
|
|
542
|
-
self.
|
|
541
|
+
data = read_backlog(path, value, presets, self.log,
|
|
542
|
+
levels=self.levels(),
|
|
543
|
+
status_map=self.status_map())
|
|
543
544
|
except IO_ERRORS as error:
|
|
544
545
|
self.show_error('Could not read file', str(error))
|
|
545
546
|
return
|
|
@@ -564,7 +565,8 @@ class BacklogApp:
|
|
|
564
565
|
"""Re-read the file, reporting a failure and applying success."""
|
|
565
566
|
try:
|
|
566
567
|
data = read_backlog(path, value, self.in_presets(), self.log,
|
|
567
|
-
self.levels(),
|
|
568
|
+
levels=self.levels(),
|
|
569
|
+
status_map=self.status_map())
|
|
568
570
|
except IO_ERRORS as error:
|
|
569
571
|
self.show_error('Could not read file', str(error))
|
|
570
572
|
return
|
|
@@ -13,15 +13,15 @@ returns it, or None when the dialog is cancelled.
|
|
|
13
13
|
# MIT License
|
|
14
14
|
|
|
15
15
|
import tkinter as tk
|
|
16
|
-
from tkinter import
|
|
16
|
+
from tkinter import messagebox, ttk
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
from datetime import date
|
|
19
19
|
from typing import Optional, TextIO
|
|
20
|
-
from backlogops import DependencyMode, DEFAULT_LEVELS
|
|
20
|
+
from backlogops import DependencyMode, DEFAULT_LEVELS
|
|
21
21
|
from backlogops_gui.gui_style import style_input
|
|
22
|
+
from backlogops_gui.key_list_box import build_key_box, load_keys_into
|
|
22
23
|
from backlogops_gui.modal_dialog import ModalDialog
|
|
23
24
|
|
|
24
|
-
KEY_READ_ERRORS = (ValueError, TypeError, KeyError, OSError)
|
|
25
25
|
DEFAULT_BUFFER_DAYS = 5
|
|
26
26
|
|
|
27
27
|
|
|
@@ -63,29 +63,13 @@ class KeysDialog(ModalDialog):
|
|
|
63
63
|
|
|
64
64
|
def _build_text(self) -> tk.Text:
|
|
65
65
|
"""Add the entry label, text box and the load-from-file button."""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
style_input(text)
|
|
70
|
-
text.pack(padx=12, pady=2)
|
|
71
|
-
tk.Button(self._win, text='Load from file…',
|
|
72
|
-
command=self._load).pack(anchor='w', padx=12, pady=4)
|
|
73
|
-
return text
|
|
66
|
+
return build_key_box(self._win,
|
|
67
|
+
'Enter keys separated by spaces or newlines:',
|
|
68
|
+
self._load)
|
|
74
69
|
|
|
75
70
|
def _load(self) -> None:
|
|
76
71
|
"""Read a key list file into the text box, reporting failures."""
|
|
77
|
-
|
|
78
|
-
title='Read key list')
|
|
79
|
-
if not name:
|
|
80
|
-
return
|
|
81
|
-
try:
|
|
82
|
-
keys = read_key_list(name, stderr_file=self._sink)
|
|
83
|
-
except KEY_READ_ERRORS as error:
|
|
84
|
-
messagebox.showerror('Could not read key list', str(error),
|
|
85
|
-
parent=self._win)
|
|
86
|
-
return
|
|
87
|
-
self._text.delete('1.0', 'end')
|
|
88
|
-
self._text.insert('end', '\n'.join(keys))
|
|
72
|
+
load_keys_into(self._win, self._text, self._sink)
|
|
89
73
|
|
|
90
74
|
def _confirm(self) -> None:
|
|
91
75
|
"""Split the text on whitespace and close the dialog."""
|
|
@@ -27,9 +27,10 @@ def _sink(sink: Optional[TextIO]) -> TextIO:
|
|
|
27
27
|
return sink if sink is not None else NoTextIO()
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
# pylint: disable-next=too-many-arguments
|
|
30
31
|
def read_backlog(path: str, value: Optional[str],
|
|
31
32
|
presets: Optional[dict[str, InputFormatConfig]],
|
|
32
|
-
sink: Optional[TextIO] = None,
|
|
33
|
+
sink: Optional[TextIO] = None, *,
|
|
33
34
|
levels: Optional[Levels] = None,
|
|
34
35
|
status_map: Optional[dict[str, Status]] = None
|
|
35
36
|
) -> BacklogReleases:
|
|
@@ -18,10 +18,11 @@ session happens in a single pop-up that does not jump around the display.
|
|
|
18
18
|
from typing import Optional, Sequence, TextIO
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
import tkinter as tk
|
|
21
|
-
from
|
|
21
|
+
from wizard_ui_bridge import AnswerFields, AskField, AskFields, PartialCheck, \
|
|
22
22
|
PartialFormValidator, PathAskOptions, TableCell, TableColumn, \
|
|
23
23
|
WizardUiBridge
|
|
24
24
|
from backlogops import NoTextIO
|
|
25
|
+
from backlogops_gui.wizard_form import handles_field
|
|
25
26
|
from backlogops_gui.wizard_window import WizardWindow
|
|
26
27
|
|
|
27
28
|
|
|
@@ -112,6 +113,10 @@ class TkWizardBridge(WizardUiBridge):
|
|
|
112
113
|
return self._window_obj().ask_form(long_question, ask_fields,
|
|
113
114
|
re_ask_reason, partial_validator)
|
|
114
115
|
|
|
116
|
+
def supports_form_field(self, field: AskField) -> bool:
|
|
117
|
+
"""Report that the Tk form shows every current field type."""
|
|
118
|
+
return handles_field(field)
|
|
119
|
+
|
|
115
120
|
def show(self, message: str) -> None:
|
|
116
121
|
"""Show an informational message to the user."""
|
|
117
122
|
self._window_obj().show(message)
|
|
@@ -18,18 +18,15 @@ masked pass phrase for an encrypted Jira API token.
|
|
|
18
18
|
# MIT License
|
|
19
19
|
|
|
20
20
|
import tkinter as tk
|
|
21
|
-
from tkinter import
|
|
21
|
+
from tkinter import messagebox, ttk
|
|
22
22
|
from dataclasses import dataclass
|
|
23
23
|
from collections.abc import Mapping
|
|
24
24
|
from typing import Optional, Sequence, TextIO
|
|
25
|
-
from backlogops import
|
|
26
|
-
JiraRankAnchor, OnMissingKey, ReleaseRename, read_key_list)
|
|
25
|
+
from backlogops import JiraRankAnchor, OnMissingKey, ReleaseRename
|
|
27
26
|
from backlogops_gui.gui_style import style_input
|
|
27
|
+
from backlogops_gui.key_list_box import build_key_box, load_keys_into
|
|
28
28
|
from backlogops_gui.modal_dialog import ModalDialog
|
|
29
29
|
|
|
30
|
-
KEY_READ_ERRORS = (ValueError, TypeError, KeyError, OSError)
|
|
31
|
-
"""Errors caught when loading a key list file into the rank dialog."""
|
|
32
|
-
|
|
33
30
|
MISSING_MODE_TEXT = {
|
|
34
31
|
OnMissingKey.RAISE: 'Stop with an error',
|
|
35
32
|
OnMissingKey.IGNORE: 'Ignore it',
|
|
@@ -550,15 +547,9 @@ class JiraRankDialog(ModalDialog):
|
|
|
550
547
|
|
|
551
548
|
def _build_keys(self) -> tk.Text:
|
|
552
549
|
"""Add the key entry label, text box and load-from-file button."""
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
text = tk.Text(self._win, width=40, height=8)
|
|
557
|
-
style_input(text)
|
|
558
|
-
text.pack(padx=12, pady=2)
|
|
559
|
-
tk.Button(self._win, text='Load from file…',
|
|
560
|
-
command=self._load).pack(anchor='w', padx=12, pady=4)
|
|
561
|
-
return text
|
|
550
|
+
return build_key_box(self._win,
|
|
551
|
+
'Keys to move (separated by spaces or newlines):',
|
|
552
|
+
self._load, label_pady=(8, 2))
|
|
562
553
|
|
|
563
554
|
def _preset_changed(self, _event: object) -> None:
|
|
564
555
|
"""Show the selected preset's default issue filter."""
|
|
@@ -566,18 +557,7 @@ class JiraRankDialog(ModalDialog):
|
|
|
566
557
|
|
|
567
558
|
def _load(self) -> None:
|
|
568
559
|
"""Read a key list file into the text box, reporting failures."""
|
|
569
|
-
|
|
570
|
-
title='Read key list')
|
|
571
|
-
if not name:
|
|
572
|
-
return
|
|
573
|
-
try:
|
|
574
|
-
keys = read_key_list(name, stderr_file=self._sink)
|
|
575
|
-
except KEY_READ_ERRORS as error:
|
|
576
|
-
messagebox.showerror('Could not read key list', str(error),
|
|
577
|
-
parent=self._win)
|
|
578
|
-
return
|
|
579
|
-
self._text.delete('1.0', 'end')
|
|
580
|
-
self._text.insert('end', '\n'.join(keys))
|
|
560
|
+
load_keys_into(self._win, self._text, self._sink)
|
|
581
561
|
|
|
582
562
|
def _confirm(self) -> None:
|
|
583
563
|
"""Store the preset, filter, keys and end, requiring preset+keys."""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Shared key-list text box with a load-from-file button for dialogs.
|
|
3
|
+
|
|
4
|
+
The order-by-keys dialog and the Jira rank dialog both let the user type
|
|
5
|
+
or paste a list of keys and load it from a file. This module holds that
|
|
6
|
+
shared widget and the file reading so the two dialogs do not repeat it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
10
|
+
# MIT License
|
|
11
|
+
|
|
12
|
+
import tkinter as tk
|
|
13
|
+
from tkinter import filedialog, messagebox
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from typing import TextIO
|
|
16
|
+
from backlogops import read_key_list
|
|
17
|
+
from backlogops_gui.gui_style import style_input
|
|
18
|
+
|
|
19
|
+
KEY_READ_ERRORS = (ValueError, TypeError, KeyError, OSError)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_key_box(win: tk.Misc, label: str, command: Callable[[], None], *,
|
|
23
|
+
label_pady: tuple[int, int] = (10, 2)) -> tk.Text:
|
|
24
|
+
"""Add a key-entry label, text box and load-from-file button."""
|
|
25
|
+
tk.Label(win, text=label).pack(anchor='w', padx=12, pady=label_pady)
|
|
26
|
+
text = tk.Text(win, width=40, height=8)
|
|
27
|
+
style_input(text)
|
|
28
|
+
text.pack(padx=12, pady=2)
|
|
29
|
+
tk.Button(win, text='Load from file…',
|
|
30
|
+
command=command).pack(anchor='w', padx=12, pady=4)
|
|
31
|
+
return text
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_keys_into(win: tk.Misc, text: tk.Text, sink: TextIO) -> None:
|
|
35
|
+
"""Read a key list file into the text box, reporting failures."""
|
|
36
|
+
name = filedialog.askopenfilename(parent=win, title='Read key list')
|
|
37
|
+
if not name:
|
|
38
|
+
return
|
|
39
|
+
try:
|
|
40
|
+
keys = read_key_list(name, stderr_file=sink)
|
|
41
|
+
except KEY_READ_ERRORS as error:
|
|
42
|
+
messagebox.showerror('Could not read key list', str(error), parent=win)
|
|
43
|
+
return
|
|
44
|
+
text.delete('1.0', 'end')
|
|
45
|
+
text.insert('end', '\n'.join(keys))
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""A modal month calendar for the Tkinter date and date-time fields.
|
|
3
|
+
|
|
4
|
+
A date field, and the date part of a date-time field, are shown in the
|
|
5
|
+
Tkinter form as a text entry paired with a Pick button. Pressing that
|
|
6
|
+
button, or typing the ``?`` token into the entry, opens this calendar.
|
|
7
|
+
The user steps between months and years and clicks a day to return it;
|
|
8
|
+
Cancel or closing the window returns nothing so the entry keeps its
|
|
9
|
+
value. Days outside a field's inclusive bounds are shown disabled, so the
|
|
10
|
+
calendar only offers acceptable dates.
|
|
11
|
+
|
|
12
|
+
The calendar is event driven: a day or Cancel button calls the picked
|
|
13
|
+
callback and destroys the window. No nested wait loop is entered, so the
|
|
14
|
+
one already running for the wizard window keeps processing events while
|
|
15
|
+
the calendar is open.
|
|
16
|
+
|
|
17
|
+
The wizard window holds a modal grab, which would otherwise starve this
|
|
18
|
+
separate window of pointer and keyboard events. The calendar therefore
|
|
19
|
+
takes the grab (and the keyboard focus) while it is open and hands it back
|
|
20
|
+
to the wizard window when it closes.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
24
|
+
# MIT License
|
|
25
|
+
|
|
26
|
+
import calendar
|
|
27
|
+
import tkinter as tk
|
|
28
|
+
from datetime import date
|
|
29
|
+
from functools import partial
|
|
30
|
+
from typing import Callable, Optional
|
|
31
|
+
from backlogops_gui.wizard_typed import value_out_of_range
|
|
32
|
+
|
|
33
|
+
CALENDAR_TITLE = 'Pick a date'
|
|
34
|
+
WEEKDAYS = ('Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def month_weeks(year: int, month: int) -> list[list[int]]:
|
|
38
|
+
"""Return the weeks of a month as day numbers, 0 for padding days."""
|
|
39
|
+
return calendar.Calendar().monthdayscalendar(year, month)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def shift_month(year: int, month: int, action: str) -> tuple[int, int]:
|
|
43
|
+
"""Return the year and month reached by one navigation action."""
|
|
44
|
+
if action == 'prev_year':
|
|
45
|
+
year -= 1
|
|
46
|
+
elif action == 'next_year':
|
|
47
|
+
year += 1
|
|
48
|
+
elif action == 'prev_month':
|
|
49
|
+
year, month = (year, month - 1) if month > 1 else (year - 1, 12)
|
|
50
|
+
else:
|
|
51
|
+
year, month = (year, month + 1) if month < 12 else (year + 1, 1)
|
|
52
|
+
return (min(max(year, date.min.year), date.max.year), month)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def day_out_of_range(day: date, minimum: Optional[date],
|
|
56
|
+
maximum: Optional[date]) -> bool:
|
|
57
|
+
"""Return whether a day lies outside the inclusive date bounds."""
|
|
58
|
+
return value_out_of_range(day, minimum, maximum)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _restore_grab(widget: Optional[tk.Misc]) -> None:
|
|
62
|
+
"""Give the modal grab back to the widget's window, if it survives."""
|
|
63
|
+
if widget is None or not widget.winfo_exists():
|
|
64
|
+
return
|
|
65
|
+
try:
|
|
66
|
+
widget.winfo_toplevel().grab_set()
|
|
67
|
+
except tk.TclError:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# pylint: disable-next=too-few-public-methods
|
|
72
|
+
class CalendarPicker:
|
|
73
|
+
"""A month calendar window returning the date the user clicks."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, parent: tk.Misc, seed: date, minimum: Optional[date],
|
|
76
|
+
maximum: Optional[date],
|
|
77
|
+
on_pick: Callable[[Optional[date]], None]) -> None:
|
|
78
|
+
"""Build the calendar window on the seed month and show it."""
|
|
79
|
+
self._bounds = (minimum, maximum)
|
|
80
|
+
self._on_pick = on_pick
|
|
81
|
+
self._year = seed.year
|
|
82
|
+
self._month = seed.month
|
|
83
|
+
self._win = tk.Toplevel(parent)
|
|
84
|
+
self._win.title(CALENDAR_TITLE)
|
|
85
|
+
self._win.transient(parent.winfo_toplevel())
|
|
86
|
+
self._win.protocol('WM_DELETE_WINDOW', self._cancel)
|
|
87
|
+
self._win.bind('<Escape>', lambda _event: self._cancel())
|
|
88
|
+
self._title = tk.Label(self._win)
|
|
89
|
+
self._title.pack(pady=4)
|
|
90
|
+
self._add_nav()
|
|
91
|
+
self._grid = tk.Frame(self._win)
|
|
92
|
+
self._grid.pack(padx=6, pady=6)
|
|
93
|
+
tk.Button(self._win, text='Cancel', command=self._cancel).pack(pady=4)
|
|
94
|
+
self._show_month()
|
|
95
|
+
self._grab()
|
|
96
|
+
|
|
97
|
+
def _grab(self) -> None:
|
|
98
|
+
"""Take the modal grab and focus, retrying until the window shows.
|
|
99
|
+
|
|
100
|
+
Grabbing fails while the new window is not yet viewable, so it is
|
|
101
|
+
retried on the wizard's event loop until it succeeds.
|
|
102
|
+
"""
|
|
103
|
+
if not self._win.winfo_exists():
|
|
104
|
+
return
|
|
105
|
+
try:
|
|
106
|
+
self._win.grab_set()
|
|
107
|
+
except tk.TclError:
|
|
108
|
+
self._win.after(50, self._grab)
|
|
109
|
+
return
|
|
110
|
+
self._win.focus_set()
|
|
111
|
+
|
|
112
|
+
def _add_nav(self) -> None:
|
|
113
|
+
"""Add the previous and next month and year navigation buttons."""
|
|
114
|
+
box = tk.Frame(self._win)
|
|
115
|
+
box.pack()
|
|
116
|
+
for text, action in (('<< year', 'prev_year'), ('< month',
|
|
117
|
+
'prev_month'), ('month >', 'next_month'),
|
|
118
|
+
('year >>', 'next_year')):
|
|
119
|
+
tk.Button(box, text=text,
|
|
120
|
+
command=partial(self._navigate, action)).pack(
|
|
121
|
+
side='left', padx=2)
|
|
122
|
+
|
|
123
|
+
def _navigate(self, action: str) -> None:
|
|
124
|
+
"""Move the shown month by one navigation action and redraw."""
|
|
125
|
+
self._year, self._month = shift_month(self._year, self._month, action)
|
|
126
|
+
self._show_month()
|
|
127
|
+
|
|
128
|
+
def _show_month(self) -> None:
|
|
129
|
+
"""Show the current month's title and rebuild the day grid."""
|
|
130
|
+
self._title.configure(text=f'{calendar.month_name[self._month]} '
|
|
131
|
+
f'{self._year}')
|
|
132
|
+
for child in self._grid.winfo_children():
|
|
133
|
+
child.destroy()
|
|
134
|
+
for column, name in enumerate(WEEKDAYS):
|
|
135
|
+
tk.Label(self._grid, text=name).grid(row=0, column=column, padx=1)
|
|
136
|
+
self._place_days()
|
|
137
|
+
|
|
138
|
+
def _place_days(self) -> None:
|
|
139
|
+
"""Place one day button, or a blank cell, per day of the month."""
|
|
140
|
+
for week, days in enumerate(month_weeks(self._year, self._month), 1):
|
|
141
|
+
for column, day in enumerate(days):
|
|
142
|
+
self._day_widget(day).grid(row=week, column=column, padx=1,
|
|
143
|
+
pady=1)
|
|
144
|
+
|
|
145
|
+
def _day_widget(self, day: int) -> tk.Widget:
|
|
146
|
+
"""Return a blank cell for a padding day, else a day button."""
|
|
147
|
+
if day == 0:
|
|
148
|
+
return tk.Label(self._grid, text='')
|
|
149
|
+
button = tk.Button(self._grid, text=str(day), width=2,
|
|
150
|
+
command=partial(self._pick, day))
|
|
151
|
+
minimum, maximum = self._bounds
|
|
152
|
+
if day_out_of_range(date(self._year, self._month, day), minimum,
|
|
153
|
+
maximum):
|
|
154
|
+
button['state'] = 'disabled'
|
|
155
|
+
return button
|
|
156
|
+
|
|
157
|
+
def _pick(self, day: int) -> None:
|
|
158
|
+
"""Return the clicked day's date and close the calendar."""
|
|
159
|
+
self._finish(date(self._year, self._month, day))
|
|
160
|
+
|
|
161
|
+
def _cancel(self) -> None:
|
|
162
|
+
"""Close the calendar without returning a date."""
|
|
163
|
+
self._finish(None)
|
|
164
|
+
|
|
165
|
+
def _finish(self, chosen: Optional[date]) -> None:
|
|
166
|
+
"""Report the outcome, destroy the window and restore the grab."""
|
|
167
|
+
parent = self._win.master
|
|
168
|
+
self._on_pick(chosen)
|
|
169
|
+
self._win.destroy()
|
|
170
|
+
_restore_grab(parent)
|