DVR-Scan-Headless 1.8__py3-none-any.whl

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 (45) hide show
  1. dvr_scan/LICENSE +47 -0
  2. dvr_scan/__init__.py +55 -0
  3. dvr_scan/__main__.py +80 -0
  4. dvr_scan/app/__init__.py +10 -0
  5. dvr_scan/app/__main__.py +218 -0
  6. dvr_scan/app/about_window.py +137 -0
  7. dvr_scan/app/application.py +1700 -0
  8. dvr_scan/app/common.py +31 -0
  9. dvr_scan/app/region_editor.py +1416 -0
  10. dvr_scan/app/scan_window.py +258 -0
  11. dvr_scan/app/widgets.py +213 -0
  12. dvr_scan/cli.py +511 -0
  13. dvr_scan/config.py +607 -0
  14. dvr_scan/controller.py +215 -0
  15. dvr_scan/detector.py +124 -0
  16. dvr_scan/docs/404.html +1 -0
  17. dvr_scan/docs/assets/javascripts/bundle.88dd0f4e.min.js +15 -0
  18. dvr_scan/docs/assets/stylesheets/main.6f8fc17f.min.css +1 -0
  19. dvr_scan/docs/assets/stylesheets/palette.06af60db.min.css +1 -0
  20. dvr_scan/docs/changelog.html +1 -0
  21. dvr_scan/docs/docs.html +73 -0
  22. dvr_scan/docs/faq.html +13 -0
  23. dvr_scan/docs/guide.html +1 -0
  24. dvr_scan/docs/index.html +5 -0
  25. dvr_scan/docs/resources.html +30 -0
  26. dvr_scan/docs/style.min.css +1 -0
  27. dvr_scan/dvr-scan-logo.png +0 -0
  28. dvr_scan/dvr-scan.ico +0 -0
  29. dvr_scan/dvr-scan.png +0 -0
  30. dvr_scan/opencv_loader.py +37 -0
  31. dvr_scan/overlays.py +251 -0
  32. dvr_scan/platform.py +236 -0
  33. dvr_scan/region.py +65 -0
  34. dvr_scan/scanner.py +1303 -0
  35. dvr_scan/shared/__init__.py +287 -0
  36. dvr_scan/shared/cli.py +291 -0
  37. dvr_scan/shared/settings.py +72 -0
  38. dvr_scan/subtractor.py +163 -0
  39. dvr_scan/video_joiner.py +188 -0
  40. dvr_scan_headless-1.8.dist-info/METADATA +119 -0
  41. dvr_scan_headless-1.8.dist-info/RECORD +45 -0
  42. dvr_scan_headless-1.8.dist-info/WHEEL +5 -0
  43. dvr_scan_headless-1.8.dist-info/entry_points.txt +2 -0
  44. dvr_scan_headless-1.8.dist-info/licenses/LICENSE +47 -0
  45. dvr_scan_headless-1.8.dist-info/top_level.txt +1 -0
dvr_scan/LICENSE ADDED
@@ -0,0 +1,47 @@
1
+ -----------------------------------------------------------------------
2
+ DVR-Scan License (BSD 2-Clause)
3
+ < https://github.com/Breakthrough/DVR-Scan >
4
+ -----------------------------------------------------------------------
5
+
6
+ Copyright (C) 2016, Brandon Castellano.
7
+ All rights reserved.
8
+
9
+ Redistribution and use in source and binary forms, with or without
10
+ modification, are permitted provided that the following conditions
11
+ are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright
14
+ notice, this list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above
17
+ copyright notice, this list of conditions and the following
18
+ disclaimer in the documentation and/or other materials
19
+ provided with the distribution.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+
33
+ -----------------------------------------------------------------------
34
+ Ancillary Software Licenses
35
+ -----------------------------------------------------------------------
36
+
37
+ DVR-Scan uses the following third-party software components:
38
+
39
+ > NumPy [Copyright (C) 2005-2022, Numpy Developers]
40
+ > OpenCV [Copyright (C) 2022, OpenCV Team]
41
+ > FFmpeg [Copyright (C) 2001, Fabrice Bellard]
42
+ > CUDA [Copyright (C) 2020, Nvidia Corporation]
43
+
44
+ Some binary distributions may redistribute these components. See the
45
+ included LICENSE-THIRDPARTY file for details. By downloading, copying,
46
+ or using this software, you agree to the terms of this license and all
47
+ third-party software licenses.
dvr_scan/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ #
2
+ # DVR-Scan: Video Motion Event Detection & Extraction Tool
3
+ # --------------------------------------------------------------
4
+ # [ Site: https://www.dvr-scan.com/ ]
5
+ # [ Repo: https://github.com/Breakthrough/DVR-Scan ]
6
+ #
7
+ # Copyright (C) 2016 Brandon Castellano <http://www.bcastell.com>.
8
+ # DVR-Scan is licensed under the BSD 2-Clause License; see the included
9
+ # LICENSE file, or visit one of the above pages for details.
10
+ #
11
+ """``dvr_scan`` Module
12
+
13
+ This is the main DVR-Scan module containing all application logic,
14
+ motion detection implementation, and command line processing. The
15
+ main modules under `dvr_scan` are organized as follows:
16
+
17
+ ``cli``: command-line interface
18
+
19
+ ``scanner``: scans a video for motion and extracts events
20
+
21
+ ``detector``: motion detection algorithms
22
+
23
+ ``overlays``: overlays which can be drawn when outputting events
24
+
25
+ There are also a few helper modules:
26
+
27
+ ``video_joiner``: concatenates multiple input videos
28
+
29
+ ``opencv_loader``: helper for resolving dynamic libraries used by OpenCV
30
+
31
+ ``platform``: library/platform specific helpers
32
+ """
33
+
34
+ import pkgutil
35
+ import sys
36
+
37
+ # Used for module/distribution identification.
38
+ __version__ = "1.8"
39
+
40
+ # Handle loading OpenCV. This **MUST** be first before any other DVR-Scan or third-party
41
+ # packages are imported which might attempt to import the `cv2` module.
42
+ import dvr_scan.opencv_loader as _ # noqa: F401
43
+ from dvr_scan.platform import init_logger
44
+
45
+ # Initialize logger.
46
+ init_logger(show_stdout=True)
47
+
48
+
49
+ def get_license_info() -> str:
50
+ """Get license/copyright information for the package or standalone executable."""
51
+ license_text = pkgutil.get_data(__name__, "LICENSE").decode()
52
+ # Include additional third-party license text if they were bundled into this release.
53
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
54
+ license_text += pkgutil.get_data(__name__, "LICENSE-THIRDPARTY").decode()
55
+ return license_text
dvr_scan/__main__.py ADDED
@@ -0,0 +1,80 @@
1
+ #
2
+ # DVR-Scan: Video Motion Event Detection & Extraction Tool
3
+ # --------------------------------------------------------------
4
+ # [ Site: https://www.dvr-scan.com/ ]
5
+ # [ Repo: https://github.com/Breakthrough/DVR-Scan ]
6
+ #
7
+ # Copyright (C) 2016 Brandon Castellano <http://www.bcastell.com>.
8
+ # DVR-Scan is licensed under the BSD 2-Clause License; see the included
9
+ # LICENSE file, or visit one of the above pages for details.
10
+ #
11
+ """``dvr_scan.__main__`` Module
12
+
13
+ Provides entry point for DVR-Scan's command-line interface (CLI).
14
+ """
15
+
16
+ import logging
17
+ import sys
18
+ from subprocess import CalledProcessError
19
+
20
+ from scenedetect import VideoOpenFailure
21
+
22
+ from dvr_scan.controller import parse_settings, run_dvr_scan
23
+ from dvr_scan.shared import logging_redirect_tqdm
24
+
25
+ EXIT_SUCCESS: int = 0
26
+ EXIT_ERROR: int = 1
27
+
28
+
29
+ def main():
30
+ """Main entry-point for DVR-Scan."""
31
+ settings = parse_settings()
32
+ if settings is None:
33
+ sys.exit(EXIT_ERROR)
34
+ logger = logging.getLogger("dvr_scan")
35
+ # TODO(1.7): The logging redirect does not respect the original log level, which is now set to
36
+ # DEBUG mode for rolling log files.
37
+ # We might have to just roll our own instead of relying on this one.
38
+ # TODO: Use Python __debug__ mode instead of hard-coding as config option.
39
+ debug_mode = settings.get("debug")
40
+ show_traceback = getattr(logging, settings.get("verbosity").upper()) == logging.DEBUG
41
+ with logging_redirect_tqdm(loggers=[logger]):
42
+ try:
43
+ run_dvr_scan(settings)
44
+ except ValueError as ex:
45
+ logger.critical("Setting Error: %s", str(ex), exc_info=show_traceback)
46
+ if debug_mode:
47
+ raise
48
+ except VideoOpenFailure as ex:
49
+ logger.critical("Failed to load input: %s", str(ex), exc_info=show_traceback)
50
+ if debug_mode:
51
+ raise
52
+ except KeyboardInterrupt:
53
+ logger.info("Stopping (interrupt received)...", exc_info=show_traceback)
54
+ if debug_mode:
55
+ raise
56
+ except CalledProcessError as ex:
57
+ logger.error(
58
+ "Failed to run command:\n %s\nCommand returned %d, output:\n\n%s",
59
+ " ".join(ex.cmd),
60
+ ex.returncode,
61
+ ex.output,
62
+ exc_info=show_traceback,
63
+ )
64
+ if debug_mode:
65
+ raise
66
+ except NotImplementedError as ex:
67
+ logger.critical("Error (Not Implemented): %s", str(ex), exc_info=show_traceback)
68
+ if debug_mode:
69
+ raise
70
+ except Exception as ex:
71
+ logger.critical("Critical Error: %s", str(ex), exc_info=True)
72
+ if debug_mode:
73
+ raise
74
+ else:
75
+ sys.exit(EXIT_SUCCESS)
76
+ sys.exit(EXIT_ERROR)
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
@@ -0,0 +1,10 @@
1
+ #
2
+ # DVR-Scan: Video Motion Event Detection & Extraction Tool
3
+ # --------------------------------------------------------------
4
+ # [ Site: https://www.dvr-scan.com/ ]
5
+ # [ Repo: https://github.com/Breakthrough/DVR-Scan ]
6
+ #
7
+ # Copyright (C) 2024 Brandon Castellano <http://www.bcastell.com>.
8
+ # DVR-Scan is licensed under the BSD 2-Clause License; see the included
9
+ # LICENSE file, or visit one of the above pages for details.
10
+ #
@@ -0,0 +1,218 @@
1
+ #
2
+ # DVR-Scan: Video Motion Event Detection & Extraction Tool
3
+ # --------------------------------------------------------------
4
+ # [ Site: https://www.dvr-scan.com/ ]
5
+ # [ Repo: https://github.com/Breakthrough/DVR-Scan ]
6
+ #
7
+ # Copyright (C) 2024 Brandon Castellano <http://www.bcastell.com>.
8
+ # DVR-Scan is licensed under the BSD 2-Clause License; see the included
9
+ # LICENSE file, or visit one of the above pages for details.
10
+ #
11
+
12
+ import argparse
13
+ import logging
14
+ import sys
15
+ from subprocess import CalledProcessError
16
+
17
+ from scenedetect import VideoOpenFailure
18
+
19
+ import dvr_scan
20
+ from dvr_scan import get_license_info
21
+ from dvr_scan.app.application import Application
22
+ from dvr_scan.config import CHOICE_MAP, USER_CONFIG_FILE_PATH, ConfigLoadFailure, ConfigRegistry
23
+ from dvr_scan.shared import (
24
+ ScanSettings,
25
+ init_logging,
26
+ logfile_path,
27
+ logging_redirect_tqdm,
28
+ setup_logger,
29
+ )
30
+ from dvr_scan.shared.cli import VERSION_STRING, LicenseAction, VersionAction, string_type_check
31
+
32
+ logger = logging.getLogger("dvr_scan")
33
+
34
+
35
+ EXIT_SUCCESS: int = 0
36
+ EXIT_ERROR: int = 1
37
+
38
+ LOGFILE_PATH = logfile_path(name_prefix="dvr-scan-app")
39
+
40
+
41
+ def get_cli_parser():
42
+ parser = argparse.ArgumentParser(
43
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
44
+ argument_default=argparse.SUPPRESS,
45
+ )
46
+ if hasattr(parser, "_optionals"):
47
+ parser._optionals.title = "arguments"
48
+
49
+ parser.add_argument(
50
+ "-c",
51
+ "--config",
52
+ metavar="dvr-scan.cfg",
53
+ type=str,
54
+ help=(
55
+ "Config file to load scan settings from. If not set, tries to load one from %s"
56
+ % USER_CONFIG_FILE_PATH
57
+ ),
58
+ )
59
+
60
+ parser.add_argument(
61
+ "-V",
62
+ "--version",
63
+ action=VersionAction,
64
+ version=VERSION_STRING,
65
+ )
66
+
67
+ parser.add_argument(
68
+ "-v",
69
+ "--verbosity",
70
+ metavar="type",
71
+ type=string_type_check(CHOICE_MAP["verbosity"], False, "type"),
72
+ help=(
73
+ "Verbosity type to use for log output. Must be one of: %s."
74
+ % (", ".join(CHOICE_MAP["verbosity"]),)
75
+ ),
76
+ )
77
+
78
+ parser.add_argument(
79
+ "--logfile",
80
+ metavar="file",
81
+ type=str,
82
+ help=(
83
+ "Appends application output to file. If file does not exist it will be created. "
84
+ f"Log path: {LOGFILE_PATH.parent}"
85
+ ),
86
+ )
87
+
88
+ parser.add_argument(
89
+ "-L",
90
+ "--license",
91
+ action=LicenseAction,
92
+ version=get_license_info(),
93
+ )
94
+
95
+ parser.add_argument(
96
+ "--debug",
97
+ action="store_true",
98
+ help=argparse.SUPPRESS,
99
+ default=False,
100
+ )
101
+
102
+ parser.add_argument(
103
+ "input",
104
+ nargs="*",
105
+ type=str,
106
+ )
107
+
108
+ return parser
109
+
110
+
111
+ # TODO: There's a lot of duplicated code here between the CLI and GUI. See if we can combine some
112
+ # of the handling of config file loading and exceptions to be consistent between the two.
113
+ #
114
+ # It would also be nice if both commands took the same set of arguments. Can probably re-use the
115
+ # existing CLI parser.
116
+ def main():
117
+ """Parse command line options and load config file settings."""
118
+ # We defer printing the debug log until we know where to put it.
119
+ init_log = []
120
+ failed_to_load_config = True
121
+ config = ConfigRegistry()
122
+ config_load_error = None
123
+ # Create debug log handler and try to load the user config file.
124
+ try:
125
+ user_config = ConfigRegistry()
126
+ user_config.load()
127
+ config = user_config
128
+ except ConfigLoadFailure as ex:
129
+ config_load_error = ex
130
+ # Parse CLI args, override config if an override was specified on the command line.
131
+ try:
132
+ args = get_cli_parser().parse_args()
133
+ init_logging(args, config)
134
+ init_log += [(logging.INFO, "DVR-Scan Application %s" % dvr_scan.__version__)]
135
+ if config_load_error and not hasattr(args, "config"):
136
+ raise config_load_error
137
+ if hasattr(args, "config"):
138
+ config_setting = ConfigRegistry()
139
+ config_setting.load(args.config)
140
+ init_logging(args, config_setting)
141
+ config = config_setting
142
+ init_log += config.consume_init_log()
143
+ if config.config_dict:
144
+ logger.debug("Loaded configuration:\n%s", str(config.config_dict))
145
+ logger.debug("Program arguments:\n%s", str(args))
146
+ settings = ScanSettings(args=args, config=config)
147
+ if settings.get("save-log"):
148
+ setup_logger(
149
+ logfile_path=LOGFILE_PATH,
150
+ max_log_files=settings.get("max-log-files"),
151
+ name_prefix="dvr-scan-app",
152
+ )
153
+ failed_to_load_config = False
154
+ except ConfigLoadFailure as ex:
155
+ init_log += ex.init_log
156
+ if ex.reason is not None:
157
+ init_log += [(logging.ERROR, "Error: %s" % str(ex.reason).replace("\t", " "))]
158
+ failed_to_load_config = True
159
+ config_load_error = ex
160
+ finally:
161
+ for log_level, log_str in init_log:
162
+ logger.log(log_level, log_str)
163
+
164
+ if failed_to_load_config:
165
+ logger.critical("Failed to load config file.")
166
+ logger.debug("Error loading config file:", exc_info=config_load_error)
167
+ # Intentionally suppress the exception in release mode since we've already logged the
168
+ # failure reason to the user above. We can now exit with an error code.
169
+ raise SystemExit(1)
170
+
171
+ show_traceback = getattr(logging, settings.get("verbosity").upper()) == logging.DEBUG
172
+ # TODO: Use Python __debug__ mode instead of hard-coding as config option.
173
+ debug_mode = settings.get("debug")
174
+ with logging_redirect_tqdm(loggers=[logger]):
175
+ try:
176
+ app = Application(
177
+ settings=settings, initial_videos=args.input if hasattr(args, "input") else []
178
+ )
179
+ app.run()
180
+
181
+ except ValueError as ex:
182
+ logger.critical("Setting Error: %s", str(ex), exc_info=show_traceback)
183
+ if debug_mode:
184
+ raise
185
+ except VideoOpenFailure as ex:
186
+ logger.critical("Failed to load input: %s", str(ex), exc_info=show_traceback)
187
+ if debug_mode:
188
+ raise
189
+ except KeyboardInterrupt:
190
+ # TODO: This doesn't always work when the GUI is running.
191
+ logger.info("Stopping (interrupt received)...", exc_info=show_traceback)
192
+ if debug_mode:
193
+ raise
194
+ except CalledProcessError as ex:
195
+ logger.error(
196
+ "Failed to run command:\n %s\nCommand returned %d, output:\n\n%s",
197
+ " ".join(ex.cmd),
198
+ ex.returncode,
199
+ ex.output,
200
+ exc_info=show_traceback,
201
+ )
202
+ if debug_mode:
203
+ raise
204
+ except NotImplementedError as ex:
205
+ logger.critical("Error (Not Implemented): %s", str(ex), exc_info=show_traceback)
206
+ if debug_mode:
207
+ raise
208
+ except Exception as ex:
209
+ logger.critical("Critical Error: %s", str(ex), exc_info=True)
210
+ if debug_mode:
211
+ raise
212
+ else:
213
+ sys.exit(EXIT_SUCCESS)
214
+ sys.exit(EXIT_ERROR)
215
+
216
+
217
+ if __name__ == "__main__":
218
+ main()
@@ -0,0 +1,137 @@
1
+ #
2
+ # DVR-Scan: Video Motion Event Detection & Extraction Tool
3
+ # --------------------------------------------------------------
4
+ # [ Site: https://www.dvr-scan.com/ ]
5
+ # [ Repo: https://github.com/Breakthrough/DVR-Scan ]
6
+ #
7
+ # Copyright (C) 2024 Brandon Castellano <http://www.bcastell.com>.
8
+ # DVR-Scan is licensed under the BSD 2-Clause License; see the included
9
+ # LICENSE file, or visit one of the above pages for details.
10
+ #
11
+
12
+ import importlib.resources as resources
13
+ import tkinter as tk
14
+ import tkinter.filedialog
15
+ import tkinter.messagebox
16
+ import tkinter.scrolledtext
17
+ import tkinter.ttk as ttk
18
+ import typing as ty
19
+ import webbrowser
20
+
21
+ import PIL
22
+ import PIL.Image
23
+ import PIL.ImageTk
24
+
25
+ import dvr_scan
26
+ from dvr_scan.platform import get_system_version_info
27
+
28
+ TITLE = "About DVR-Scan"
29
+ COPYRIGHT = (
30
+ f"DVR-Scan {dvr_scan.__version__}\n\nCopyright © Brandon Castellano.\nAll rights reserved."
31
+ )
32
+
33
+
34
+ class AboutWindow:
35
+ def __init__(self):
36
+ self._version_info: ty.Optional[str] = None
37
+ self._about_image: PIL.Image = None
38
+ self._about_image_tk: PIL.ImageTk.PhotoImage = None
39
+
40
+ def show(self, root: tk.Tk):
41
+ window = tk.Toplevel(master=root)
42
+ window.withdraw()
43
+ window.title(TITLE)
44
+ window.resizable(True, True)
45
+
46
+ app_logo = PIL.Image.open(resources.open_binary(dvr_scan, "dvr-scan-logo.png"))
47
+ self._about_image = app_logo.crop((8, 8, app_logo.width - 132, app_logo.height - 8))
48
+ self._about_image_tk = PIL.ImageTk.PhotoImage(self._about_image)
49
+ canvas = tk.Canvas(window, width=self._about_image.width, height=self._about_image.height)
50
+ canvas.grid()
51
+ canvas.create_image(0, 0, anchor=tk.NW, image=self._about_image_tk)
52
+
53
+ ttk.Separator(window, orient=tk.HORIZONTAL).grid(row=1, sticky="ew", padx=16.0)
54
+ ttk.Label(
55
+ window,
56
+ text=COPYRIGHT,
57
+ ).grid(row=2, sticky="nw", padx=24.0, pady=24.0)
58
+
59
+ # TODO: These should be buttons not labels.
60
+ website_link = ttk.Label(
61
+ window, text="www.dvr-scan.com", cursor="hand2", foreground="medium blue"
62
+ )
63
+ website_link.grid(row=2, sticky="ne", padx=24.0, pady=24.0)
64
+ website_link.bind("<Button-1>", lambda _: webbrowser.open_new_tab("www.dvr-scan.com"))
65
+
66
+ about_tabs = ttk.Notebook(window)
67
+ version_tab = ttk.Frame(about_tabs)
68
+ version_area = tkinter.scrolledtext.ScrolledText(
69
+ version_tab, wrap=tk.NONE, width=40, height=1
70
+ )
71
+ # TODO: See if we can add another button that will copy debug logs.
72
+ if not self._version_info:
73
+ self._version_info = get_system_version_info()
74
+ version_area.insert(tk.INSERT, self._version_info)
75
+ version_area.grid(sticky="nsew")
76
+ version_area.config(state="disabled")
77
+ version_tab.columnconfigure(0, weight=1)
78
+ version_tab.rowconfigure(0, weight=1)
79
+ tk.Button(
80
+ version_tab,
81
+ text="Copy to Clipboard",
82
+ command=lambda: root.clipboard_append(self._version_info),
83
+ ).grid(row=1, column=0)
84
+
85
+ license_tab = ttk.Frame(about_tabs)
86
+ scrollbar = tk.Scrollbar(license_tab, orient=tk.HORIZONTAL)
87
+ license_area = tkinter.scrolledtext.ScrolledText(
88
+ license_tab, wrap=tk.NONE, width=40, xscrollcommand=scrollbar.set, height=1
89
+ )
90
+ license_area.insert(tk.INSERT, dvr_scan.get_license_info())
91
+ license_area.grid(sticky="nsew")
92
+ scrollbar.config(command=license_area.xview)
93
+ scrollbar.grid(row=1, sticky="swe")
94
+ license_area.config(state="disabled")
95
+ license_tab.columnconfigure(0, weight=1)
96
+ license_tab.rowconfigure(0, weight=1)
97
+
98
+ # TODO: Add tab that has some useful links like submitting bug report, etc
99
+ about_tabs.add(version_tab, text="Version Info")
100
+ about_tabs.add(license_tab, text="License Info")
101
+
102
+ about_tabs.grid(
103
+ row=0, column=1, rowspan=4, padx=(0.0, 16.0), pady=(16.0, 16.0), sticky="nsew"
104
+ )
105
+ window.update()
106
+ if self._about_image is not None:
107
+ window.columnconfigure(0, minsize=self._about_image.width)
108
+ window.rowconfigure(0, minsize=self._about_image.height)
109
+ else:
110
+ window.columnconfigure(0, minsize=200)
111
+ window.rowconfigure(0, minsize=100)
112
+ # minsize includes padding
113
+ window.columnconfigure(1, weight=1, minsize=100)
114
+ window.rowconfigure(3, weight=1)
115
+
116
+ window.minsize(width=window.winfo_reqwidth(), height=window.winfo_reqheight())
117
+ # can we query widget height?
118
+
119
+ root.grab_release()
120
+
121
+ window.transient(root)
122
+ window.focus()
123
+ window.grab_set()
124
+
125
+ def dismiss():
126
+ window.grab_release()
127
+ window.destroy()
128
+ root.grab_set()
129
+ root.focus()
130
+
131
+ window.protocol("WM_DELETE_WINDOW", dismiss)
132
+ window.attributes("-topmost", True)
133
+ window.bind("<Escape>", lambda _: window.destroy())
134
+ window.bind("<Destroy>", lambda _: dismiss())
135
+
136
+ window.deiconify()
137
+ window.wait_window()