Added zoom and scroll

This commit is contained in:
hyugogirubato
2024-07-17 23:55:33 +02:00
parent baee10d922
commit 963ecf9e6f
+131 -24
View File
@@ -1,5 +1,7 @@
import hashlib import hashlib
import tkinter as tk import tkinter as tk
import os
from io import BytesIO from io import BytesIO
from tkinter import ttk from tkinter import ttk
from tkinter.constants import DISABLED, NORMAL from tkinter.constants import DISABLED, NORMAL
@@ -11,9 +13,9 @@ from pathlib import Path
from cbz.comic import ComicInfo from cbz.comic import ComicInfo
from cbz.page import PageInfo from cbz.page import PageInfo
from cbz.utils import readable_size, ico_to_png from cbz.utils import readable_size, ico_to_png
import os
PARENT = Path(__file__).parent PARENT = Path(__file__).parent
CTRL_KEY = 0x4 # Constant for Ctrl key
class Player: class Player:
@@ -37,6 +39,10 @@ class Player:
page_index_label (ttk.Label): Label showing the current page index. page_index_label (ttk.Label): Label showing the current page index.
previous_width (int): Previous width of the main window. previous_width (int): Previous width of the main window.
previous_height (int): Previous height of the main window. previous_height (int): Previous height of the main window.
resize_timer (int or None): Timer ID for handling window resize delay.
max_zoom_factor (float or None): Maximum allowable zoom factor based on canvas size and image dimensions.
zoom_factor (float or None): Current zoom factor for image display.
img_original (PIL.Image.Image or None): Placeholder for the original image to be displayed.
""" """
def __init__(self, comic_info: ComicInfo): def __init__(self, comic_info: ComicInfo):
@@ -48,6 +54,9 @@ class Player:
""" """
self.comic_info = comic_info self.comic_info = comic_info
self.current_page = -1 # Track the current page index self.current_page = -1 # Track the current page index
self.max_zoom_factor = None # Maximum allowable zoom factor based on canvas size and image dimensions
self.zoom_factor = None # Initial zoom factor (None means no zoom initially)
self.img_original = None # Placeholder for the original image
# Initialize tkinter window # Initialize tkinter window
self.root = tk.Tk() self.root = tk.Tk()
@@ -74,7 +83,6 @@ class Player:
self._init_summary_text() self._init_summary_text()
# Display initial page content # Display initial page content
# TODO: Support scrollable image and/or zoom when it is too large for the screen (eg: webtoon)
self.show_page() self.show_page()
# Bind keys for navigation and window resize # Bind keys for navigation and window resize
@@ -127,6 +135,10 @@ class Player:
available_width = self.root.winfo_screenwidth() - 2 * margin_width available_width = self.root.winfo_screenwidth() - 2 * margin_width
available_height = self.root.winfo_screenheight() - 2 * margin_height available_height = self.root.winfo_screenheight() - 2 * margin_height
# Default screen size for initialization
screen_width = int(available_height * 0.63)
screen_height = int(available_height)
# Calculate scaling factors for width and height # Calculate scaling factors for width and height
if self.comic_info.pages: if self.comic_info.pages:
# Get the minimum dimensions from all pages # Get the minimum dimensions from all pages
@@ -147,8 +159,13 @@ class Player:
initial_width = int(image_width * scale_factor * 0.94) initial_width = int(image_width * scale_factor * 0.94)
initial_height = int(image_height * scale_factor) initial_height = int(image_height * scale_factor)
else: else:
initial_width = int(available_height * 0.63) # If no pages are available, default to screen size
initial_height = int(available_height) initial_width = screen_width
initial_height = screen_height
# Ensure that the window's initial width is at least 50% of the screen width
if 100 / screen_width * initial_width < 50.0:
initial_width = screen_width
# Calculate initial position to center the window on the screen # Calculate initial position to center the window on the screen
x = self.root.winfo_screenwidth() // 2 - initial_width // 2 x = self.root.winfo_screenwidth() // 2 - initial_width // 2
@@ -189,7 +206,7 @@ class Player:
self.summary_text.tag_configure('normal', font=('Arial', 12), lmargin1=10, lmargin2=10) self.summary_text.tag_configure('normal', font=('Arial', 12), lmargin1=10, lmargin2=10)
self.summary_text.config(state=DISABLED) self.summary_text.config(state=DISABLED)
def show_page(self): def show_page(self) -> None:
""" """
Display the current page content (either summary or image). Display the current page content (either summary or image).
""" """
@@ -242,26 +259,40 @@ class Player:
# Display image centered on canvas # Display image centered on canvas
page: PageInfo = self.comic_info.pages[self.current_page] page: PageInfo = self.comic_info.pages[self.current_page]
img = Image.open(BytesIO(page.content)) self.img_original = Image.open(BytesIO(page.content))
self._display_image()
# Calculate scaling factor def _display_image(self) -> None:
scale_width = self.canvas.winfo_width() / img.width """
scale_height = self.canvas.winfo_height() / img.height Display the image on the canvas with zooming capabilities.
scale_factor = min(scale_width, scale_height) """
# Check if zoom factor is not set; initialize zoom factors
if not self.zoom_factor:
# Determine maximum zoom factor based on canvas size and original image size
self.max_zoom_factor = min(
self.canvas.winfo_width() / self.img_original.width,
self.canvas.winfo_height() / self.img_original.height
)
self.zoom_factor = self.max_zoom_factor
# Resize image # Calculate new dimensions of the image based on current zoom factor
new_width = int(img.width * scale_factor) new_width = int(self.img_original.width * self.zoom_factor)
new_height = int(img.height * scale_factor) new_height = int(self.img_original.height * self.zoom_factor)
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) img = self.img_original.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Set image on canvas # Convert the resized image to PhotoImage format compatible with Tkinter canvas
self.canvas.image = ImageTk.PhotoImage(img) self.canvas.image = ImageTk.PhotoImage(img)
# Calculate coordinates to center the image on the canvas # Calculate the position to center the image on the canvas
x = (self.canvas.winfo_width() - new_width) // 2 x = max(0, (self.canvas.winfo_width() - new_width) // 2)
y = (self.canvas.winfo_height() - new_height) // 2 y = max(0, (self.canvas.winfo_height() - new_height) // 2)
self.canvas.create_image(x, y, anchor=tk.NW, image=self.canvas.image) self.canvas.create_image(x, y, anchor=tk.NW, image=self.canvas.image)
# Configure the scroll region of the canvas to allow scrolling if image is larger than canvas
canvas_width = max(new_width, self.canvas.winfo_width())
canvas_height = max(new_height, self.canvas.winfo_height())
self.canvas.config(scrollregion=(0, 0, canvas_width, canvas_height))
def _bind_keys(self) -> None: def _bind_keys(self) -> None:
""" """
Bind keys for navigation (left/right arrow keys) and window events. Bind keys for navigation (left/right arrow keys) and window events.
@@ -270,19 +301,79 @@ class Player:
self.root.bind('<Left>', lambda event: self.on_previous()) self.root.bind('<Left>', lambda event: self.on_previous())
self.root.bind('<Right>', lambda event: self.one_next()) self.root.bind('<Right>', lambda event: self.one_next())
# Bind Ctrl+C to exit application # Bind Ctrl+Q to exit application
self.root.bind('<Control-q>', lambda event: self.root.quit()) self.root.bind('<Control-q>', lambda event: self.root.quit())
# Bind the configure event to handle window resize # Bind the configure event to handle window resize
self.root.bind('<Configure>', lambda event: self.on_window(event)) self.root.bind('<Configure>', lambda event: self.on_window(event))
# Bind mouse wheel events for zooming
self.root.bind('<Control-MouseWheel>', self.on_zoom)
self.canvas.bind_all("<MouseWheel>", self._on_mouse_wheel)
self.canvas.bind_all("<Shift-MouseWheel>", self._on_shift_mouse_wheel)
# Bind zoom controls to keyboard keys
self.root.bind('<KeyPress-plus>', lambda event: self.on_zoom_key(True))
self.root.bind('<KeyPress-minus>', lambda event: self.on_zoom_key(False))
def on_zoom_key(self, positive: bool) -> None:
"""
Handle zooming triggered by keyboard keys ('+' for zoom in, '-' for zoom out).
Args:
positive (bool): True for zoom in, False for zoom out.
"""
# Create a synthetic event object to simulate mouse wheel scrolling
event = tk.Event()
event.delta = 120 if positive else -120
self.on_zoom(event)
def on_zoom(self, event) -> None:
"""
Handle zooming using Ctrl + Mouse Wheel.
"""
if self.current_page == -1: return
# Adjust zoom factor based on mouse wheel direction
if event.delta > 0:
# Increase zoom factor by 10% for zoom in
self.zoom_factor *= 1.1
elif event.delta < 0:
# Decrease zoom factor by 10% for zoom out
self.zoom_factor /= 1.1
# Ensure the zoom-out factor doesn't go below the original image size
min_zoom_factor = min(
self.canvas.winfo_width() / self.img_original.width,
self.canvas.winfo_height() / self.img_original.height
)
if self.zoom_factor < min_zoom_factor:
self.zoom_factor = min_zoom_factor
# Update the displayed image with the new zoom factor
self._display_image()
def _on_mouse_wheel(self, event) -> None:
"""
Handle vertical scroll using Mouse Wheel.
"""
if self.current_page == -1 or (event.state & CTRL_KEY): return
self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
def _on_shift_mouse_wheel(self, event) -> None:
"""
Handle horizontal scroll using Shift + Mouse Wheel.
"""
if self.current_page == -1 or (event.state & CTRL_KEY): return
self.canvas.xview_scroll(int(-1 * (event.delta / 120)), "units")
def on_previous(self) -> None: def on_previous(self) -> None:
""" """
Navigate to the previous page. Navigate to the previous page.
""" """
if self.current_page > -1: if self.current_page == -1: return
self.current_page -= 1 self.current_page -= 1
self.show_page() self.show_page()
def one_next(self) -> None: def one_next(self) -> None:
""" """
@@ -306,7 +397,23 @@ class Player:
self.root.after_cancel(self.resize_timer) self.root.after_cancel(self.resize_timer)
# Set a new timer to call show_page after a delay # Set a new timer to call show_page after a delay
self.resize_timer = self.root.after(100, self.show_page) self.resize_timer = self.root.after(200, self._on_resize)
def _on_resize(self) -> None:
"""
Handle resizing of the window and adjust image display accordingly.
"""
if self.current_page == -1: return
# Reset zoom factor to None if it's equal to the maximum zoom factor
if self.zoom_factor == self.max_zoom_factor:
self.zoom_factor = None
# Update the displayed image with the current zoom factor
self._display_image()
# Reset the resize timer
self.resize_timer = None
def run(self) -> None: def run(self) -> None:
""" """