Skip to content

Commit 3b3ff36

Browse files
committed
[app] Create new DVR-Scan application for GUI
1 parent 042ca1d commit 3b3ff36

File tree

10 files changed

+684
-411
lines changed

10 files changed

+684
-411
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
*.pyc
33
*.pyo
44
*.egg-info/
5+
__pycache__/
56

67
# Location of built documentation
78
site/

dvr_scan/app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# [ Site: https://www.dvr-scan.com/ ]
55
# [ Repo: https://github.com/Breakthrough/DVR-Scan ]
66
#
7-
# Copyright (C) 2014-2024 Brandon Castellano <http://www.bcastell.com>.
7+
# Copyright (C) 2016 Brandon Castellano <http://www.bcastell.com>.
88
# DVR-Scan is licensed under the BSD 2-Clause License; see the included
99
# LICENSE file, or visit one of the above pages for details.
1010
#

dvr_scan/app/__main__.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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) 2014 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+
import typing as ty
16+
17+
from dvr_scan import get_license_info
18+
from dvr_scan.app.application import Application
19+
from dvr_scan.config import CHOICE_MAP
20+
from dvr_scan.platform import init_logger
21+
from dvr_scan.shared import (
22+
VERSION_STRING,
23+
LicenseAction,
24+
VersionAction,
25+
string_type_check,
26+
)
27+
28+
EXIT_SUCCESS: int = 0
29+
EXIT_ERROR: int = 1
30+
31+
32+
def get_cli_parser():
33+
parser = argparse.ArgumentParser(
34+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
35+
argument_default=argparse.SUPPRESS,
36+
)
37+
if hasattr(parser, "_optionals"):
38+
parser._optionals.title = "arguments"
39+
40+
parser.add_argument(
41+
"-V",
42+
"--version",
43+
action=VersionAction,
44+
version=VERSION_STRING,
45+
)
46+
47+
parser.add_argument(
48+
"-v",
49+
"--verbosity",
50+
metavar="type",
51+
type=string_type_check(CHOICE_MAP["verbosity"], False, "type"),
52+
help=(
53+
"Amount of verbosity to use for log output. Must be one of: %s."
54+
% (", ".join(CHOICE_MAP["verbosity"]),)
55+
),
56+
)
57+
58+
parser.add_argument(
59+
"--logfile",
60+
metavar="file",
61+
type=str,
62+
help=(
63+
"Path to log file for writing application output. If FILE already exists, the program"
64+
" output will be appended to the existing contents."
65+
),
66+
)
67+
68+
parser.add_argument(
69+
"-L",
70+
"--license",
71+
action=LicenseAction,
72+
version=get_license_info(),
73+
)
74+
75+
return parser
76+
77+
78+
def _init_logging(args: ty.Optional[argparse.ArgumentParser]):
79+
verbosity = logging.INFO
80+
if args is not None and hasattr(args, "verbosity"):
81+
verbosity = getattr(logging, args.verbosity.upper())
82+
83+
quiet_mode = False
84+
if args is not None and hasattr(args, "quiet_mode"):
85+
quiet_mode = args.quiet_mode
86+
87+
init_logger(
88+
log_level=verbosity,
89+
show_stdout=not quiet_mode,
90+
log_file=args.logfile if hasattr(args, "logfile") else None,
91+
)
92+
93+
94+
def main():
95+
args = get_cli_parser().parse_args()
96+
_init_logging(args)
97+
app = Application()
98+
app.run()
99+
sys.exit(EXIT_SUCCESS)
100+
101+
102+
if __name__ == "__main__":
103+
main()

dvr_scan/app/about_window.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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) 2014-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+
"""DVR-Scan Region Editor handles detection region input and processing.
12+
13+
Regions are represented as a set of closed polygons defined by lists of points.
14+
15+
*NOTE*: The region editor is being transitioned to an offical GUI for DVR-Scan.
16+
During this period, there may still be some keyboard/CLI interaction required to
17+
run the program. Usability and accessibility bugs will be prioritized over feature
18+
development.
19+
20+
The code in this module covers *all* the current UI logic, and consequently is not
21+
well organized. This should be resolved as we develop the UI further and start to
22+
better separate the CLI from the GUI. To facilitate this, a separate entry-point
23+
for the GUI will be developed, and the region editor functionality will be deprecated.
24+
"""
25+
26+
import os
27+
import os.path
28+
import tkinter as tk
29+
import tkinter.filedialog
30+
import tkinter.messagebox
31+
import tkinter.scrolledtext
32+
import tkinter.ttk as ttk
33+
import typing as ty
34+
import webbrowser
35+
36+
import PIL
37+
import PIL.Image
38+
import PIL.ImageTk
39+
40+
import dvr_scan
41+
from dvr_scan.app.common import SUPPORTS_RESOURCES
42+
from dvr_scan.platform import get_system_version_info
43+
44+
if SUPPORTS_RESOURCES:
45+
import importlib.resources as resources
46+
47+
TITLE = "About DVR-Scan"
48+
COPYRIGHT = (
49+
f"DVR-Scan {dvr_scan.__version__}\n\nCopyright © Brandon Castellano.\nAll rights reserved."
50+
)
51+
52+
53+
class AboutWindow:
54+
def __init__(self):
55+
self._version_info: ty.Optional[str] = None
56+
self._about_image: PIL.Image = None
57+
self._about_image_tk: PIL.ImageTk.PhotoImage = None
58+
59+
def show(self, root: tk.Tk):
60+
window = tk.Toplevel(master=root)
61+
window.withdraw()
62+
window.title(TITLE)
63+
window.resizable(True, True)
64+
65+
if SUPPORTS_RESOURCES:
66+
app_logo = PIL.Image.open(resources.open_binary(dvr_scan, "dvr-scan-logo.png"))
67+
self._about_image = app_logo.crop((8, 8, app_logo.width - 132, app_logo.height - 8))
68+
self._about_image_tk = PIL.ImageTk.PhotoImage(self._about_image)
69+
canvas = tk.Canvas(
70+
window, width=self._about_image.width, height=self._about_image.height
71+
)
72+
canvas.grid()
73+
canvas.create_image(0, 0, anchor=tk.NW, image=self._about_image_tk)
74+
75+
ttk.Separator(window, orient=tk.HORIZONTAL).grid(row=1, sticky="ew", padx=16.0)
76+
ttk.Label(
77+
window,
78+
text=COPYRIGHT,
79+
).grid(row=2, sticky="nw", padx=24.0, pady=24.0)
80+
81+
# TODO: These should be buttons not labels.
82+
website_link = ttk.Label(
83+
window, text="www.dvr-scan.com", cursor="hand2", foreground="medium blue"
84+
)
85+
website_link.grid(row=2, sticky="ne", padx=24.0, pady=24.0)
86+
website_link.bind("<Button-1>", lambda _: webbrowser.open_new_tab("www.dvr-scan.com"))
87+
88+
about_tabs = ttk.Notebook(window)
89+
version_tab = ttk.Frame(about_tabs)
90+
version_area = tkinter.scrolledtext.ScrolledText(
91+
version_tab, wrap=tk.NONE, width=40, height=1
92+
)
93+
# TODO: See if we can add another button that will copy debug logs.
94+
if not self._version_info:
95+
self._version_info = get_system_version_info()
96+
version_area.insert(tk.INSERT, self._version_info)
97+
version_area.grid(sticky="nsew")
98+
version_area.config(state="disabled")
99+
version_tab.columnconfigure(0, weight=1)
100+
version_tab.rowconfigure(0, weight=1)
101+
tk.Button(
102+
version_tab,
103+
text="Copy to Clipboard",
104+
command=lambda: root.clipboard_append(self._version_info),
105+
).grid(row=1, column=0)
106+
107+
license_tab = ttk.Frame(about_tabs)
108+
scrollbar = tk.Scrollbar(license_tab, orient=tk.HORIZONTAL)
109+
license_area = tkinter.scrolledtext.ScrolledText(
110+
license_tab, wrap=tk.NONE, width=40, xscrollcommand=scrollbar.set, height=1
111+
)
112+
license_area.insert(tk.INSERT, dvr_scan.get_license_info())
113+
license_area.grid(sticky="nsew")
114+
scrollbar.config(command=license_area.xview)
115+
scrollbar.grid(row=1, sticky="swe")
116+
license_area.config(state="disabled")
117+
license_tab.columnconfigure(0, weight=1)
118+
license_tab.rowconfigure(0, weight=1)
119+
120+
# TODO: Add tab that has some useful links like submitting bug report, etc
121+
about_tabs.add(version_tab, text="Version Info")
122+
about_tabs.add(license_tab, text="License Info")
123+
124+
about_tabs.grid(
125+
row=0, column=1, rowspan=4, padx=(0.0, 16.0), pady=(16.0, 16.0), sticky="nsew"
126+
)
127+
window.update()
128+
if self._about_image is not None:
129+
window.columnconfigure(0, minsize=self._about_image.width)
130+
window.rowconfigure(0, minsize=self._about_image.height)
131+
else:
132+
window.columnconfigure(0, minsize=200)
133+
window.rowconfigure(0, minsize=100)
134+
# minsize includes padding
135+
window.columnconfigure(1, weight=1, minsize=100)
136+
window.rowconfigure(3, weight=1)
137+
138+
window.minsize(width=window.winfo_reqwidth(), height=window.winfo_reqheight())
139+
# can we query widget height?
140+
141+
root.grab_release()
142+
if os == "nt":
143+
root.attributes("-disabled", True)
144+
145+
window.transient(root)
146+
window.focus()
147+
window.grab_set()
148+
149+
def dismiss():
150+
window.grab_release()
151+
window.destroy()
152+
if os == "nt":
153+
root.attributes("-disabled", False)
154+
root.grab_set()
155+
root.focus()
156+
157+
window.protocol("WM_DELETE_WINDOW", dismiss)
158+
window.attributes("-topmost", True)
159+
window.bind("<Escape>", lambda _: window.destroy())
160+
window.bind("<Destroy>", lambda _: dismiss())
161+
162+
window.deiconify()
163+
window.wait_window()

dvr_scan/app/application.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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) 2014-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 tkinter as tk
13+
import tkinter.filedialog
14+
import tkinter.messagebox
15+
import tkinter.scrolledtext
16+
from logging import getLogger
17+
18+
from dvr_scan.app.common import register_icon
19+
20+
WINDOW_TITLE = "DVR-Scan"
21+
22+
logger = getLogger("dvr_scan")
23+
24+
25+
class Application:
26+
def __init__(self):
27+
self._root = tk.Tk()
28+
29+
def run(self):
30+
# Withdraw root window until we're done adding everything to avoid visual flicker.
31+
self._root.withdraw()
32+
self._root.option_add("*tearOff", False)
33+
self._root.title(WINDOW_TITLE)
34+
register_icon(self._root)
35+
self._root.resizable(True, True)
36+
self._root.minsize(width=320, height=240)
37+
tk.Label(self._root, text="testing").grid(row=0, column=0)
38+
tk.Label(self._root, text="testing").grid(row=1, column=0)
39+
tk.Label(self._root, text="testing").grid(row=0, column=1)
40+
tk.Label(self._root, text="testing").grid(row=1, column=1)
41+
42+
logger.debug("starting main loop")
43+
self._root.deiconify()
44+
self._root.focus()
45+
self._root.grab_release()
46+
self._root.mainloop()

dvr_scan/app/common.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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) 2014-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+
"""DVR-Scan Region Editor handles detection region input and processing.
12+
13+
Regions are represented as a set of closed polygons defined by lists of points.
14+
15+
*NOTE*: The region editor is being transitioned to an offical GUI for DVR-Scan.
16+
During this period, there may still be some keyboard/CLI interaction required to
17+
run the program. Usability and accessibility bugs will be prioritized over feature
18+
development.
19+
20+
The code in this module covers *all* the current UI logic, and consequently is not
21+
well organized. This should be resolved as we develop the UI further and start to
22+
better separate the CLI from the GUI. To facilitate this, a separate entry-point
23+
for the GUI will be developed, and the region editor functionality will be deprecated.
24+
"""
25+
26+
import os
27+
import sys
28+
import tkinter as tk
29+
30+
import PIL
31+
import PIL.Image
32+
import PIL.ImageTk
33+
34+
import dvr_scan
35+
36+
SUPPORTS_RESOURCES = sys.version_info.minor >= 9
37+
if SUPPORTS_RESOURCES:
38+
import importlib.resources as resources
39+
40+
41+
def register_icon(root: tk.Tk):
42+
if SUPPORTS_RESOURCES:
43+
# On Windows we always want a path so we can load the .ICO with `iconbitmap`.
44+
# On other systems, we can just use the PNG logo directly with `iconphoto`.
45+
if os.name == "nt":
46+
icon_path = resources.files(dvr_scan).joinpath("dvr-scan.ico")
47+
with resources.as_file(icon_path) as icon_path:
48+
root.iconbitmap(default=icon_path)
49+
return
50+
icon = PIL.Image.open(resources.open_binary(dvr_scan, "dvr-scan.png"))
51+
root.iconphoto(True, PIL.ImageTk.PhotoImage(icon))

0 commit comments

Comments
 (0)