mirror of
https://github.com/hyugogirubato/cbz.git
synced 2026-09-26 21:41:03 +00:00
Added zoom and scroll
This commit is contained in:
+129
-22
@@ -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,17 +301,77 @@ 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()
|
||||||
|
|
||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user