pic-select 1.0.1__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.
- pic_select-1.0.1/LICENSE +9 -0
- pic_select-1.0.1/MANIFEST.in +0 -0
- pic_select-1.0.1/PKG-INFO +42 -0
- pic_select-1.0.1/README.md +20 -0
- pic_select-1.0.1/pic_select/__init__.py +0 -0
- pic_select-1.0.1/pic_select/__main__.py +9 -0
- pic_select-1.0.1/pic_select/gui/app.py +115 -0
- pic_select-1.0.1/pic_select/gui/frames/components/borderedImageFrame.py +35 -0
- pic_select-1.0.1/pic_select/gui/frames/components/imageFrame.py +61 -0
- pic_select-1.0.1/pic_select/gui/frames/components/paddedImageFrame.py +24 -0
- pic_select-1.0.1/pic_select/gui/frames/displayFrame.py +90 -0
- pic_select-1.0.1/pic_select/gui/frames/displays/imageViewFrame.py +108 -0
- pic_select-1.0.1/pic_select/gui/frames/displays/loadFrame.py +20 -0
- pic_select-1.0.1/pic_select/gui/frames/displays/selectImageFrame.py +16 -0
- pic_select-1.0.1/pic_select/gui/selectFile.py +9 -0
- pic_select-1.0.1/pic_select/log/__init__.py +4 -0
- pic_select-1.0.1/pic_select/log/logger.py +29 -0
- pic_select-1.0.1/pic_select/logic/clipboard.py +13 -0
- pic_select-1.0.1/pic_select/logic/input/UserInput.py +12 -0
- pic_select-1.0.1/pic_select/logic/input/userInputHandler.py +94 -0
- pic_select-1.0.1/pic_select/logic/load/loader.py +105 -0
- pic_select-1.0.1/pic_select/logic/load/loaderThread.py +124 -0
- pic_select-1.0.1/pic_select/logic/load/pictureDirectory.py +53 -0
- pic_select-1.0.1/pic_select/logic/selector.py +78 -0
- pic_select-1.0.1/pic_select/logic/sizeManager.py +19 -0
- pic_select-1.0.1/pic_select/pic_select.py +36 -0
- pic_select-1.0.1/pic_select.egg-info/PKG-INFO +42 -0
- pic_select-1.0.1/pic_select.egg-info/SOURCES.txt +31 -0
- pic_select-1.0.1/pic_select.egg-info/dependency_links.txt +1 -0
- pic_select-1.0.1/pic_select.egg-info/top_level.txt +1 -0
- pic_select-1.0.1/setup.cfg +4 -0
- pic_select-1.0.1/setup.py +26 -0
- pic_select-1.0.1/tests/test.py +141 -0
pic_select-1.0.1/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Nils Urbach
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pic_select
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Author: Nils Urbach
|
|
5
|
+
Author-email: ndu01u@gmail.com
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: license
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# pic_select
|
|
24
|
+
|
|
25
|
+
This is a small module that easily lets you view; select and copy images in a directory.
|
|
26
|
+
|
|
27
|
+
## Installation and Start
|
|
28
|
+
|
|
29
|
+
- install using `python -m pip install pic_select`
|
|
30
|
+
- run using `python -m pic_select`
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
- Ctrl+O: open a image file
|
|
35
|
+
- Left arrow: show next picture in directory (alphabetical order)
|
|
36
|
+
- Right arrow: show next picture in directory (alphabetical order)
|
|
37
|
+
- Space bar: (un)select an image
|
|
38
|
+
- Ctrl+C: copy all selected images to clipboard
|
|
39
|
+
- Ctrl+A: select all images in the directory
|
|
40
|
+
- Esc: unselect all images
|
|
41
|
+
- Ctrl+T: exit the program
|
|
42
|
+
- F11: toggle fullscreen
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# pic_select
|
|
2
|
+
|
|
3
|
+
This is a small module that easily lets you view; select and copy images in a directory.
|
|
4
|
+
|
|
5
|
+
## Installation and Start
|
|
6
|
+
|
|
7
|
+
- install using `python -m pip install pic_select`
|
|
8
|
+
- run using `python -m pic_select`
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
- Ctrl+O: open a image file
|
|
13
|
+
- Left arrow: show next picture in directory (alphabetical order)
|
|
14
|
+
- Right arrow: show next picture in directory (alphabetical order)
|
|
15
|
+
- Space bar: (un)select an image
|
|
16
|
+
- Ctrl+C: copy all selected images to clipboard
|
|
17
|
+
- Ctrl+A: select all images in the directory
|
|
18
|
+
- Esc: unselect all images
|
|
19
|
+
- Ctrl+T: exit the program
|
|
20
|
+
- F11: toggle fullscreen
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
|
|
3
|
+
from ..logic.input.UserInput import UserInput
|
|
4
|
+
from ..logic.input.userInputHandler import UserInputHandler
|
|
5
|
+
from ..logic.load.loader import Loader
|
|
6
|
+
from ..logic.load.loaderThread import LoaderThread
|
|
7
|
+
from ..logic.selector import Selector
|
|
8
|
+
|
|
9
|
+
from .frames.displayFrame import DisplayFrame
|
|
10
|
+
from .frames.displays.imageViewFrame import ImageViewFrame
|
|
11
|
+
|
|
12
|
+
from .selectFile import ask_file_name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_PADDING_PERCENT = 0.15
|
|
17
|
+
|
|
18
|
+
_MIN_WINDOW_DIM = int(25 / _PADDING_PERCENT) + 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class App(tk.Tk):
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
title: str,
|
|
27
|
+
input_handler: UserInputHandler,
|
|
28
|
+
loader: Loader,
|
|
29
|
+
loader_thread: LoaderThread,
|
|
30
|
+
selector: Selector,
|
|
31
|
+
fullscreen = True):
|
|
32
|
+
super().__init__()
|
|
33
|
+
|
|
34
|
+
self.title(title)
|
|
35
|
+
|
|
36
|
+
self.minsize(_MIN_WINDOW_DIM, _MIN_WINDOW_DIM)
|
|
37
|
+
self.state("zoomed")
|
|
38
|
+
self.focus_force()
|
|
39
|
+
self.lift()
|
|
40
|
+
|
|
41
|
+
self.bind("<Left>", self._on_left_arrow)
|
|
42
|
+
self.bind("<Right>", self._on_right_arrow)
|
|
43
|
+
self.bind("<space>", self._on_space)
|
|
44
|
+
self.bind("<Control-c>", self._on_strg_c)
|
|
45
|
+
self.bind("<Control-C>", self._on_strg_c)
|
|
46
|
+
self.bind("<Control-a>", self._on_strg_a)
|
|
47
|
+
self.bind("<Control-A>", self._on_strg_a)
|
|
48
|
+
self.bind("<Control-t>", self._on_strg_t)
|
|
49
|
+
self.bind("<Control-T>", self._on_strg_t)
|
|
50
|
+
self.bind("<Control-o>", self._on_strg_o)
|
|
51
|
+
self.bind("<Control-O>", self._on_strg_o)
|
|
52
|
+
self.bind("<Escape>", self._on_escape)
|
|
53
|
+
self.bind("<F11>", self._on_f_11)
|
|
54
|
+
|
|
55
|
+
self._is_fullscreen = fullscreen
|
|
56
|
+
if self._is_fullscreen:
|
|
57
|
+
self.enter_fullscreen()
|
|
58
|
+
|
|
59
|
+
self._input_handler = input_handler
|
|
60
|
+
|
|
61
|
+
self._display_frame = DisplayFrame(master=self, loader_controller=loader_thread, selector=selector)
|
|
62
|
+
self._display_frame.pack(fill="both", expand=True)
|
|
63
|
+
self._display_frame.setup_displays(selector=selector, loader=loader)
|
|
64
|
+
self._display_frame.show_select_image()
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def display_frame(self) -> DisplayFrame:
|
|
68
|
+
return self._display_frame
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def image_view_frame(self) -> ImageViewFrame:
|
|
72
|
+
return self._display_frame.image_view_frame
|
|
73
|
+
|
|
74
|
+
def _on_right_arrow(self, event) -> None:
|
|
75
|
+
self._input_handler.handle_input(input=UserInput.FORWARD)
|
|
76
|
+
|
|
77
|
+
def _on_left_arrow(self, event) -> None:
|
|
78
|
+
self._input_handler.handle_input(input=UserInput.BACKWARD)
|
|
79
|
+
|
|
80
|
+
def _on_space(self, event) -> None:
|
|
81
|
+
self._input_handler.handle_input(input=UserInput.SELECT)
|
|
82
|
+
|
|
83
|
+
def _on_strg_c(self, evetn) -> None:
|
|
84
|
+
self._input_handler.handle_input(input=UserInput.COPY)
|
|
85
|
+
|
|
86
|
+
def _on_strg_a(self, event) -> None:
|
|
87
|
+
self._input_handler.handle_input(input=UserInput.SELECT_ALL)
|
|
88
|
+
|
|
89
|
+
def _on_escape(self, event) -> None:
|
|
90
|
+
self._input_handler.handle_input(input=UserInput.UNSELECT_ALL)
|
|
91
|
+
|
|
92
|
+
def _on_strg_t(self, event) -> None:
|
|
93
|
+
self.quit()
|
|
94
|
+
|
|
95
|
+
def _on_f_11(self, event) -> None:
|
|
96
|
+
self.toggle_fullscreen()
|
|
97
|
+
|
|
98
|
+
def _on_strg_o(self, event) -> None:
|
|
99
|
+
file = ask_file_name(parent=self)
|
|
100
|
+
self._input_handler.handle_input(input=UserInput.OPEN, file=file)
|
|
101
|
+
|
|
102
|
+
def toggle_fullscreen(self) -> None:
|
|
103
|
+
self._is_fullscreen = not self._is_fullscreen
|
|
104
|
+
self.attributes("-fullscreen", self._is_fullscreen)
|
|
105
|
+
|
|
106
|
+
def enter_fullscreen(self) -> None:
|
|
107
|
+
self.attributes("-fullscreen", True)
|
|
108
|
+
self._is_fullscreen = True
|
|
109
|
+
|
|
110
|
+
def exit_fullscreen(self) -> None:
|
|
111
|
+
self.attributes("-fullscreen", False)
|
|
112
|
+
self._is_fullscreen = False
|
|
113
|
+
|
|
114
|
+
def quit(self) -> None:
|
|
115
|
+
self.destroy()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from PIL import ImageTk
|
|
3
|
+
|
|
4
|
+
from .paddedImageFrame import PaddedImageFrame
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BorderedImageFrame(tk.Frame):
|
|
9
|
+
|
|
10
|
+
def __init__(self, master: tk.Widget, image: ImageTk.PhotoImage, padding: int, borderwidth: int):
|
|
11
|
+
full_width = image.width() + (padding * 2) + (borderwidth * 2)
|
|
12
|
+
full_height = image.height() + (padding * 2) + (borderwidth * 2)
|
|
13
|
+
|
|
14
|
+
super().__init__(master, width = full_width, height = full_height, borderwidth=0, highlightthickness=0)
|
|
15
|
+
|
|
16
|
+
self._border_frame = tk.Frame(master=self,
|
|
17
|
+
background="blue",
|
|
18
|
+
width=full_width,
|
|
19
|
+
height=full_height,
|
|
20
|
+
borderwidth=0,
|
|
21
|
+
highlightthickness=0)
|
|
22
|
+
|
|
23
|
+
self._padded_frame = PaddedImageFrame(master=self._border_frame, image=image, padding=padding)
|
|
24
|
+
|
|
25
|
+
self._padded_frame.place(x=borderwidth, y=borderwidth)
|
|
26
|
+
self._border_frame.place(x=0, y=0)
|
|
27
|
+
|
|
28
|
+
self.pack_propagate(False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from PIL import Image, ImageTk
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
from .borderedImageFrame import BorderedImageFrame
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_GAP_SIZE = 5
|
|
11
|
+
_BORDER_SIZE = 7
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ImageFrame(tk.Frame):
|
|
16
|
+
|
|
17
|
+
def __init__(self, master: tk.Widget, display_size: Tuple[int, int], image: Image.Image, selected: bool = False):
|
|
18
|
+
super().__init__(master=master, width=display_size[0], height=display_size[1])
|
|
19
|
+
|
|
20
|
+
image_bounds = display_size[0] - (_GAP_SIZE * 2) - (_BORDER_SIZE * 2), display_size[1] - (_GAP_SIZE * 2) - (_BORDER_SIZE * 2)
|
|
21
|
+
image_size = ImageFrame.calculate_max_inbound_size(image_bounds=image_bounds, full_size=(image.width, image.height))
|
|
22
|
+
resized_image = image.resize(image_size, resample=Image.Resampling.LANCZOS)
|
|
23
|
+
self._image = ImageTk.PhotoImage(image=resized_image)
|
|
24
|
+
resized_image.close()
|
|
25
|
+
|
|
26
|
+
self._borderedFrame = BorderedImageFrame(master=self, image=self._image, padding=_GAP_SIZE, borderwidth=_BORDER_SIZE)
|
|
27
|
+
self._borderedFrame.place(relx=0.5, rely=0.5, anchor="center")
|
|
28
|
+
|
|
29
|
+
self._selected = selected
|
|
30
|
+
|
|
31
|
+
if self._selected:
|
|
32
|
+
self.select()
|
|
33
|
+
else:
|
|
34
|
+
self.unselect()
|
|
35
|
+
|
|
36
|
+
self.pack_propagate(False)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def calculate_max_inbound_size(image_bounds: Tuple[int, int], full_size: Tuple[int, int]) -> Tuple[int, int]:
|
|
40
|
+
width_factor = image_bounds[0] / full_size[0]
|
|
41
|
+
height_factor = image_bounds[1] / full_size[1]
|
|
42
|
+
factor = min(width_factor, height_factor)
|
|
43
|
+
return (int(math.floor(full_size[0] * factor)), int(math.floor(full_size[1] * factor)))
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def selected(self) -> bool:
|
|
47
|
+
return self._selected
|
|
48
|
+
|
|
49
|
+
def select(self) -> None:
|
|
50
|
+
self._selected = True
|
|
51
|
+
self._borderedFrame._border_frame.configure(background="blue")
|
|
52
|
+
|
|
53
|
+
def unselect(self) -> None:
|
|
54
|
+
self._selected = False
|
|
55
|
+
self._borderedFrame._border_frame.configure(background="SystemButtonFace")
|
|
56
|
+
|
|
57
|
+
def toggle(self) -> None:
|
|
58
|
+
if self.selected:
|
|
59
|
+
self.unselect()
|
|
60
|
+
else:
|
|
61
|
+
self.select()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from PIL import ImageTk
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PaddedImageFrame(tk.Frame):
|
|
7
|
+
|
|
8
|
+
def __init__(self, master: tk.Widget, image: ImageTk.PhotoImage, padding: int):
|
|
9
|
+
full_width = image.width() + (padding * 2)
|
|
10
|
+
full_height =image.height() + (padding * 2)
|
|
11
|
+
|
|
12
|
+
super().__init__(master, width = full_width, height = full_height, borderwidth=0, highlightthickness=0)
|
|
13
|
+
|
|
14
|
+
self._pad_Frame = tk.Frame(master=self, width = full_width, height=full_height, borderwidth=0, highlightthickness=0)
|
|
15
|
+
self._image_label = tk.Label(master=self._pad_Frame, image=image, borderwidth=0, highlightthickness=0, padx=0, pady=0)
|
|
16
|
+
|
|
17
|
+
self._image_label.place(x=padding, y=padding)
|
|
18
|
+
self._pad_Frame.place(x=0, y=0)
|
|
19
|
+
|
|
20
|
+
self.pack_propagate(False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import ttk
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ...logic.load.loaderThread import LoaderThread
|
|
7
|
+
from ...logic.load.loader import Loader
|
|
8
|
+
from ...logic.selector import Selector
|
|
9
|
+
|
|
10
|
+
from .displays.selectImageFrame import SelectImageFrame
|
|
11
|
+
from .displays.imageViewFrame import ImageViewFrame
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_PADDING_PIXELS = 70
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DisplayFrame(ttk.Frame):
|
|
20
|
+
|
|
21
|
+
def __init__(self, master: tk.Widget, loader_controller: LoaderThread, selector: Selector):
|
|
22
|
+
super().__init__(master=master)
|
|
23
|
+
|
|
24
|
+
loader_controller.subscribe_on_busy(callback=self._on_loader_busy)
|
|
25
|
+
loader_controller.subscribe_on_idle(callback=self._on_loader_idle)
|
|
26
|
+
|
|
27
|
+
selector.subscribe_on_select(callback=self._on_select)
|
|
28
|
+
|
|
29
|
+
self._selection_count: tk.StringVar = tk.StringVar(self, f"{0:05}")
|
|
30
|
+
self._selection_count_label: tk.Label = tk.Label(master=self,
|
|
31
|
+
textvariable=self._selection_count,
|
|
32
|
+
font=("Arial", -20))
|
|
33
|
+
self._selection_count_label.place(relx=0, rely=0, anchor="nw")
|
|
34
|
+
|
|
35
|
+
self._loader_busy: tk.StringVar = tk.StringVar(self, "")
|
|
36
|
+
self._busy_indicator_label: tk.Label = tk.Label(master=self,
|
|
37
|
+
textvariable=self._loader_busy,
|
|
38
|
+
font=("Arial", -20))
|
|
39
|
+
self._busy_indicator_label.place(relx=0, rely=1, anchor="sw")
|
|
40
|
+
|
|
41
|
+
self._image_name: tk.StringVar = tk.StringVar(self, "")
|
|
42
|
+
self._image_name_label: tk.Label = tk.Label(master=self,
|
|
43
|
+
textvariable=self._image_name,
|
|
44
|
+
font=("Arial", -20))
|
|
45
|
+
self._image_name_label.place(relx=0.5, rely=1, anchor="s")
|
|
46
|
+
|
|
47
|
+
self._select_image_frame: SelectImageFrame = None
|
|
48
|
+
self._image_view_frame: ImageViewFrame = None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def image_view_frame(self) -> ImageViewFrame:
|
|
52
|
+
return self._image_view_frame
|
|
53
|
+
|
|
54
|
+
def _on_loader_busy(self) -> None:
|
|
55
|
+
self._loader_busy.set("Loading ...")
|
|
56
|
+
|
|
57
|
+
def _on_loader_idle(self) -> None:
|
|
58
|
+
self._loader_busy.set("")
|
|
59
|
+
|
|
60
|
+
def _on_new_image(self, path: Path) -> None:
|
|
61
|
+
self.show_image(path=path)
|
|
62
|
+
|
|
63
|
+
def _on_select(self, path: Path, selected: bool, count: int) -> None:
|
|
64
|
+
self._selection_count.set(f"{count:05}")
|
|
65
|
+
|
|
66
|
+
def setup_displays(self, selector: Selector, loader: Loader) -> None:
|
|
67
|
+
self.master.update_idletasks()
|
|
68
|
+
|
|
69
|
+
self._image_view_frame = ImageViewFrame(master=self, display_size=self.display_size(), selector=selector, loader=loader)
|
|
70
|
+
self._image_view_frame.place(relx=0.5, rely=0.5, anchor="center")
|
|
71
|
+
|
|
72
|
+
self._select_image_frame = SelectImageFrame(master=self, display_size=self.display_size())
|
|
73
|
+
self._select_image_frame.place(relx=0.5, rely=0.5, anchor="center")
|
|
74
|
+
|
|
75
|
+
def display_size(self) -> Tuple[int, int]:
|
|
76
|
+
width, height = self.winfo_width(), self.winfo_height()
|
|
77
|
+
return width - _PADDING_PIXELS, height -_PADDING_PIXELS
|
|
78
|
+
|
|
79
|
+
def show_image(self, path: Path) -> None:
|
|
80
|
+
self._image_name.set(path.name)
|
|
81
|
+
self._image_view_frame.display_image_frame(path=path)
|
|
82
|
+
self._image_view_frame.tkraise()
|
|
83
|
+
self._image_view_frame.focus()
|
|
84
|
+
|
|
85
|
+
def show_select_image(self) -> None:
|
|
86
|
+
self._select_image_frame.tkraise()
|
|
87
|
+
self._select_image_frame.focus()
|
|
88
|
+
|
|
89
|
+
def set_image_name(self, path: Path) -> None:
|
|
90
|
+
self._image_name.set(path.name)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from typing import Tuple, Dict, Self
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from PIL import Image
|
|
6
|
+
|
|
7
|
+
from ....logic.selector import Selector
|
|
8
|
+
from ....logic.load.loader import Loader
|
|
9
|
+
|
|
10
|
+
from ..components.imageFrame import ImageFrame
|
|
11
|
+
from ..displays.loadFrame import LoadFrame
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ImageViewFrame(tk.Frame, Mapping):
|
|
16
|
+
|
|
17
|
+
def __init__(self, master: tk.Widget, display_size: Tuple[int, int], selector: Selector, loader: Loader):
|
|
18
|
+
super().__init__(master=master, width=display_size[0], height=display_size[1])
|
|
19
|
+
|
|
20
|
+
self.pack_propagate(False)
|
|
21
|
+
|
|
22
|
+
loader.subscribe_on_load(callback=self._on_load)
|
|
23
|
+
loader.subscribe_on_unload(callback=self._on_unload)
|
|
24
|
+
|
|
25
|
+
selector.subscribe_on_select(callback=self._on_select)
|
|
26
|
+
|
|
27
|
+
self._selector = selector
|
|
28
|
+
|
|
29
|
+
self._image_frames: Dict[Path, ImageFrame] = {}
|
|
30
|
+
|
|
31
|
+
self._current_image_path: Path = None
|
|
32
|
+
|
|
33
|
+
self._load_frame: LoadFrame = LoadFrame(master=self, display_size=display_size)
|
|
34
|
+
self._load_frame.place(relx=0.5, rely=0.5, anchor="center", )
|
|
35
|
+
|
|
36
|
+
def __getitem__(self, path: Path):
|
|
37
|
+
if path not in self._images:
|
|
38
|
+
raise KeyError(f"{path} is not framed")
|
|
39
|
+
return self._image_frames[path]
|
|
40
|
+
|
|
41
|
+
def __setitem__(self, path: Path, image: Image.Image):
|
|
42
|
+
raise RuntimeError("use ImageViewFrame.create_image_frame()")
|
|
43
|
+
|
|
44
|
+
def __delitem__(self, path: Path):
|
|
45
|
+
raise RuntimeError("use ImageViewFrame.remove_image_frame()")
|
|
46
|
+
|
|
47
|
+
def __contains__(self, path: Path):
|
|
48
|
+
return path in self._image_frames
|
|
49
|
+
|
|
50
|
+
def __iter__(self):
|
|
51
|
+
return iter(self._image_frames)
|
|
52
|
+
|
|
53
|
+
def __len__(self):
|
|
54
|
+
return len(self._image_frames)
|
|
55
|
+
|
|
56
|
+
def _on_load(self, path: Path, image: Image.Image) -> None:
|
|
57
|
+
self._create_image_frame(path=path, image=image)
|
|
58
|
+
if self._current_image_path == path:
|
|
59
|
+
self.display_image_frame(path=path)
|
|
60
|
+
|
|
61
|
+
def _on_unload(self, unloaded: Path) -> None:
|
|
62
|
+
if unloaded is not None:
|
|
63
|
+
self.remove_image_frame(path=unloaded)
|
|
64
|
+
|
|
65
|
+
def _on_select(self, path: Path, selected: bool, count: int) -> None:
|
|
66
|
+
if path not in self._image_frames:
|
|
67
|
+
return None
|
|
68
|
+
if selected:
|
|
69
|
+
self._image_frames[path].select()
|
|
70
|
+
else:
|
|
71
|
+
self._image_frames[path].unselect()
|
|
72
|
+
|
|
73
|
+
def _create_image_frame(self, path: Path, image: Image.Image) -> Self:
|
|
74
|
+
self._image_frames[path] = ImageFrame(master=self,
|
|
75
|
+
display_size=(self.winfo_width(), self.winfo_height()),
|
|
76
|
+
image=image,
|
|
77
|
+
selected=self._selector.is_selected(path=path))
|
|
78
|
+
self._image_frames[path].place(relx=0.5, rely=0.5, anchor="center")
|
|
79
|
+
|
|
80
|
+
if self._current_image_path in self:
|
|
81
|
+
self._image_frames[self._current_image_path].tkraise()
|
|
82
|
+
else:
|
|
83
|
+
self._load_frame.tkraise()
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def current_image_path(self) -> Path:
|
|
87
|
+
return self._current_image_path
|
|
88
|
+
|
|
89
|
+
def remove_image_frame(self, path: Path) -> None:
|
|
90
|
+
self._image_frames.pop(path)
|
|
91
|
+
|
|
92
|
+
def display_image_frame(self, path: Path) -> None:
|
|
93
|
+
if path not in self:
|
|
94
|
+
self._load_frame.set_text(name=path.name)
|
|
95
|
+
self._load_frame.tkraise()
|
|
96
|
+
else:
|
|
97
|
+
self._image_frames[path].tkraise()
|
|
98
|
+
self._current_image_path = path
|
|
99
|
+
|
|
100
|
+
def select_all(self, unselect = False):
|
|
101
|
+
for frame in self._image_frames.values():
|
|
102
|
+
if unselect:
|
|
103
|
+
frame.unselect()
|
|
104
|
+
else:
|
|
105
|
+
frame.select()
|
|
106
|
+
|
|
107
|
+
def is_waiting_on_loader(self) -> bool:
|
|
108
|
+
return self._current_image_path not in self
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LoadFrame(tk.Frame):
|
|
7
|
+
|
|
8
|
+
def __init__(self, master: tk.Widget, display_size: Tuple[int, int]):
|
|
9
|
+
super().__init__(master=master, width=display_size[0], height=display_size[1])
|
|
10
|
+
|
|
11
|
+
self.pack_propagate(False)
|
|
12
|
+
|
|
13
|
+
self._text: tk.StringVar = tk.StringVar(self)
|
|
14
|
+
self._label: tk.Label = tk.Label(master=self,
|
|
15
|
+
textvariable=self._text,
|
|
16
|
+
font=("Arial, 18"))
|
|
17
|
+
self._label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
|
|
18
|
+
|
|
19
|
+
def set_text(self, name: str) -> None:
|
|
20
|
+
self._text.set(f"loading image '{name}'")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SelectImageFrame(tk.Frame):
|
|
7
|
+
|
|
8
|
+
def __init__(self, master: tk.Widget, display_size: Tuple[int, int]):
|
|
9
|
+
super().__init__(master=master, width=display_size[0], height=display_size[1])
|
|
10
|
+
|
|
11
|
+
self.pack_propagate(False)
|
|
12
|
+
|
|
13
|
+
self._label: tk.Label = tk.Label(master=self,
|
|
14
|
+
text="Select a file (Strg + O)",
|
|
15
|
+
font=("Arial, 18"))
|
|
16
|
+
self._label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Logger():
|
|
7
|
+
def __init__(self, log_file: Path = None):
|
|
8
|
+
self._log_file = log_file
|
|
9
|
+
|
|
10
|
+
def log_file_action(self, thread: str, file: Path, action: str) -> None:
|
|
11
|
+
message = f"{thread} - {file.name} - {action}"
|
|
12
|
+
self._log(message=message)
|
|
13
|
+
|
|
14
|
+
def log_user_input(self, input: str) -> None:
|
|
15
|
+
self._log(message=input, new_line=True)
|
|
16
|
+
|
|
17
|
+
def log_benchmark(self, title: str, time_ms: int) -> None:
|
|
18
|
+
message = f"{title} - {time_ms}ms"
|
|
19
|
+
self._log(message=message, new_line=True)
|
|
20
|
+
|
|
21
|
+
def _log(self, message: str, new_line: bool = False) -> None:
|
|
22
|
+
current_time = datetime.now().strftime("%H:%M:%S")
|
|
23
|
+
good_message = f"{current_time}: {message}"
|
|
24
|
+
if self._log_file is None:
|
|
25
|
+
if new_line:
|
|
26
|
+
print()
|
|
27
|
+
print(good_message)
|
|
28
|
+
else:
|
|
29
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import List
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def copy_files_to_clipboard(files: List[Path]) -> None:
|
|
8
|
+
if files is None or len(files) == 0:
|
|
9
|
+
return None
|
|
10
|
+
seperator = "', '"
|
|
11
|
+
cmd = f"Set-Clipboard -Path '{seperator.join([str(p) for p in files])}'"
|
|
12
|
+
subprocess.run(["powershell", cmd], shell=True)
|
|
13
|
+
|