forked from Boria138/PortProtonQt
Move repo from git to gitea
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
503
portprotonqt/image_utils.py
Normal file
503
portprotonqt/image_utils.py
Normal file
@ -0,0 +1,503 @@
|
||||
import os
|
||||
from PySide6.QtGui import QPen, QColor, QPixmap, QPainter, QPainterPath
|
||||
from PySide6.QtCore import Qt, QFile, QEvent, QByteArray, QEasingCurve, QPropertyAnimation
|
||||
from PySide6.QtWidgets import QGraphicsItem, QToolButton, QFrame, QLabel, QGraphicsScene, QHBoxLayout, QWidget, QGraphicsView, QVBoxLayout, QSizePolicy
|
||||
from PySide6.QtWidgets import QSpacerItem, QGraphicsPixmapItem, QDialog, QApplication
|
||||
import portprotonqt.themes.standart.styles as default_styles
|
||||
from portprotonqt.config_utils import read_theme_from_config
|
||||
from portprotonqt.theme_manager import ThemeManager
|
||||
from portprotonqt.downloader import Downloader
|
||||
from portprotonqt.logger import get_logger
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from queue import Queue
|
||||
import threading
|
||||
|
||||
downloader = Downloader()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Глобальная очередь и пул потоков для загрузки изображений
|
||||
image_load_queue = Queue()
|
||||
image_executor = ThreadPoolExecutor(max_workers=4)
|
||||
queue_lock = threading.Lock()
|
||||
|
||||
def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[QPixmap], None], app_name: str = ""):
|
||||
"""
|
||||
Асинхронно загружает обложку через очередь задач.
|
||||
"""
|
||||
def process_image():
|
||||
theme_manager = ThemeManager()
|
||||
current_theme_name = read_theme_from_config()
|
||||
|
||||
def finish_with(pixmap: QPixmap):
|
||||
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
||||
x = (scaled.width() - width) // 2
|
||||
y = (scaled.height() - height) // 2
|
||||
cropped = scaled.copy(x, y, width, height)
|
||||
callback(cropped)
|
||||
# Removed: pixmap = None (unnecessary, causes type error)
|
||||
|
||||
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
||||
image_folder = os.path.join(xdg_cache_home, "PortProtonQT", "images")
|
||||
os.makedirs(image_folder, exist_ok=True)
|
||||
|
||||
if cover and cover.startswith("https://steamcdn-a.akamaihd.net/steam/apps/"):
|
||||
try:
|
||||
parts = cover.split("/")
|
||||
appid = None
|
||||
if "apps" in parts:
|
||||
idx = parts.index("apps")
|
||||
if idx + 1 < len(parts):
|
||||
appid = parts[idx + 1]
|
||||
if appid:
|
||||
local_path = os.path.join(image_folder, f"{appid}.jpg")
|
||||
if os.path.exists(local_path):
|
||||
pixmap = QPixmap(local_path)
|
||||
finish_with(pixmap)
|
||||
return
|
||||
|
||||
def on_downloaded(result: str | None):
|
||||
pixmap = QPixmap()
|
||||
if result and os.path.exists(result):
|
||||
pixmap.load(result)
|
||||
if pixmap.isNull():
|
||||
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||
if placeholder_path and QFile.exists(placeholder_path):
|
||||
pixmap.load(placeholder_path)
|
||||
else:
|
||||
pixmap = QPixmap(width, height)
|
||||
pixmap.fill(QColor("#333333"))
|
||||
painter = QPainter(pixmap)
|
||||
painter.setPen(QPen(QColor("white")))
|
||||
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
|
||||
painter.end()
|
||||
finish_with(pixmap)
|
||||
|
||||
downloader.download_async(cover, local_path, timeout=5, callback=on_downloaded)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки URL {cover}: {e}")
|
||||
|
||||
if cover and cover.startswith(("http://", "https://")):
|
||||
try:
|
||||
local_path = os.path.join(image_folder, f"{app_name}.jpg")
|
||||
if os.path.exists(local_path):
|
||||
pixmap = QPixmap(local_path)
|
||||
finish_with(pixmap)
|
||||
return
|
||||
|
||||
def on_downloaded(result: str | None):
|
||||
pixmap = QPixmap()
|
||||
if result and os.path.exists(result):
|
||||
pixmap.load(result)
|
||||
if pixmap.isNull():
|
||||
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||
if placeholder_path and QFile.exists(placeholder_path):
|
||||
pixmap.load(placeholder_path)
|
||||
else:
|
||||
pixmap = QPixmap(width, height)
|
||||
pixmap.fill(QColor("#333333"))
|
||||
painter = QPainter(pixmap)
|
||||
painter.setPen(QPen(QColor("white")))
|
||||
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
|
||||
painter.end()
|
||||
finish_with(pixmap)
|
||||
|
||||
downloader.download_async(cover, local_path, timeout=5, callback=on_downloaded)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error("Error processing EGS URL %s: %s", cover, str(e))
|
||||
|
||||
if cover and QFile.exists(cover):
|
||||
pixmap = QPixmap(cover)
|
||||
finish_with(pixmap)
|
||||
return
|
||||
|
||||
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||
pixmap = QPixmap()
|
||||
if placeholder_path and QFile.exists(placeholder_path):
|
||||
pixmap.load(placeholder_path)
|
||||
else:
|
||||
pixmap = QPixmap(width, height)
|
||||
pixmap.fill(QColor("#333333"))
|
||||
painter = QPainter(pixmap)
|
||||
painter.setPen(QPen(QColor("white")))
|
||||
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
|
||||
painter.end()
|
||||
finish_with(pixmap)
|
||||
|
||||
with queue_lock:
|
||||
image_load_queue.put(process_image)
|
||||
image_executor.submit(lambda: image_load_queue.get()())
|
||||
|
||||
def round_corners(pixmap, radius):
|
||||
"""
|
||||
Возвращает QPixmap с закруглёнными углами.
|
||||
"""
|
||||
if pixmap.isNull():
|
||||
return pixmap
|
||||
size = pixmap.size()
|
||||
rounded = QPixmap(size)
|
||||
rounded.fill(QColor(0, 0, 0, 0))
|
||||
painter = QPainter(rounded)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(0, 0, size.width(), size.height(), radius, radius)
|
||||
painter.setClipPath(path)
|
||||
painter.drawPixmap(0, 0, pixmap)
|
||||
painter.end()
|
||||
return rounded
|
||||
|
||||
class FullscreenDialog(QDialog):
|
||||
"""
|
||||
Диалог для просмотра изображений без стандартных элементов управления.
|
||||
Изображение отображается в области фиксированного размера, а подпись располагается чуть выше нижней границы.
|
||||
В окне есть кнопки-стрелки для перелистывания изображений.
|
||||
Диалог закрывается при клике по изображению или подписи.
|
||||
"""
|
||||
FIXED_WIDTH = 800
|
||||
FIXED_HEIGHT = 400
|
||||
|
||||
def __init__(self, images, current_index=0, parent=None, theme=None):
|
||||
"""
|
||||
:param images: Список кортежей (QPixmap, caption)
|
||||
:param current_index: Индекс текущего изображения
|
||||
:param theme: Объект темы для стилизации (если None, используется default_styles)
|
||||
"""
|
||||
super().__init__(parent)
|
||||
# Удаление диалога после закрытия
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.setFocus()
|
||||
|
||||
self.images = images
|
||||
self.current_index = current_index
|
||||
self.theme = theme if theme else default_styles
|
||||
|
||||
# Убираем стандартные элементы управления окна
|
||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
self.init_ui()
|
||||
self.update_display()
|
||||
|
||||
# Фильтруем события для закрытия диалога по клику
|
||||
self.imageLabel.installEventFilter(self)
|
||||
self.captionLabel.installEventFilter(self)
|
||||
|
||||
def init_ui(self):
|
||||
self.mainLayout = QVBoxLayout(self)
|
||||
self.setLayout(self.mainLayout)
|
||||
self.mainLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.mainLayout.setSpacing(0)
|
||||
|
||||
# Контейнер для изображения и стрелок
|
||||
self.imageContainer = QWidget()
|
||||
self.imageContainer.setFixedSize(self.FIXED_WIDTH, self.FIXED_HEIGHT)
|
||||
self.imageContainerLayout = QHBoxLayout(self.imageContainer)
|
||||
self.imageContainerLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.imageContainerLayout.setSpacing(0)
|
||||
|
||||
# Левая стрелка
|
||||
self.prevButton = QToolButton()
|
||||
self.prevButton.setArrowType(Qt.ArrowType.LeftArrow)
|
||||
self.prevButton.setStyleSheet(self.theme.PREV_BUTTON_STYLE)
|
||||
self.prevButton.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.prevButton.setFixedSize(40, 40)
|
||||
self.prevButton.clicked.connect(self.show_prev)
|
||||
self.imageContainerLayout.addWidget(self.prevButton)
|
||||
|
||||
# Метка для изображения
|
||||
self.imageLabel = QLabel()
|
||||
self.imageLabel.setFixedSize(self.FIXED_WIDTH - 80, self.FIXED_HEIGHT)
|
||||
self.imageLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.imageContainerLayout.addWidget(self.imageLabel, stretch=1)
|
||||
|
||||
# Правая стрелка
|
||||
self.nextButton = QToolButton()
|
||||
self.nextButton.setArrowType(Qt.ArrowType.RightArrow)
|
||||
self.nextButton.setStyleSheet(self.theme.NEXT_BUTTON_STYLE)
|
||||
self.nextButton.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.nextButton.setFixedSize(40, 40)
|
||||
self.nextButton.clicked.connect(self.show_next)
|
||||
self.imageContainerLayout.addWidget(self.nextButton)
|
||||
|
||||
self.mainLayout.addWidget(self.imageContainer)
|
||||
|
||||
# Небольшой отступ между изображением и подписью
|
||||
spacer = QSpacerItem(20, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
||||
self.mainLayout.addItem(spacer)
|
||||
|
||||
# Подпись
|
||||
self.captionLabel = QLabel()
|
||||
self.captionLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.captionLabel.setFixedHeight(40)
|
||||
self.captionLabel.setWordWrap(True)
|
||||
self.captionLabel.setStyleSheet(self.theme.CAPTION_LABEL_STYLE)
|
||||
self.captionLabel.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.mainLayout.addWidget(self.captionLabel)
|
||||
|
||||
def update_display(self):
|
||||
"""Обновляет изображение и подпись согласно текущему индексу."""
|
||||
if not self.images:
|
||||
return
|
||||
|
||||
# Очищаем старое содержимое
|
||||
self.imageLabel.clear()
|
||||
self.captionLabel.clear()
|
||||
QApplication.processEvents()
|
||||
|
||||
pixmap, caption = self.images[self.current_index]
|
||||
# Масштабируем изображение так, чтобы оно поместилось в область фиксированного размера
|
||||
scaled_pixmap = pixmap.scaled(
|
||||
self.FIXED_WIDTH - 80, # учитываем ширину стрелок
|
||||
self.FIXED_HEIGHT,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
self.imageLabel.setPixmap(scaled_pixmap)
|
||||
self.captionLabel.setText(caption)
|
||||
self.setWindowTitle(caption)
|
||||
|
||||
# Принудительная перерисовка виджетов
|
||||
self.imageLabel.repaint()
|
||||
self.captionLabel.repaint()
|
||||
self.repaint()
|
||||
|
||||
def show_prev(self):
|
||||
"""Показывает предыдущее изображение."""
|
||||
if self.images:
|
||||
self.current_index = (self.current_index - 1) % len(self.images)
|
||||
self.update_display()
|
||||
|
||||
def show_next(self):
|
||||
"""Показывает следующее изображение."""
|
||||
if self.images:
|
||||
self.current_index = (self.current_index + 1) % len(self.images)
|
||||
self.update_display()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Закрывает диалог при клике по изображению или подписи."""
|
||||
if event.type() == QEvent.Type.MouseButtonPress and obj in [self.imageLabel, self.captionLabel]:
|
||||
self.close()
|
||||
return True
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def changeEvent(self, event):
|
||||
"""Закрывает диалог при потере фокуса."""
|
||||
if event.type() == QEvent.Type.ActivationChange:
|
||||
if not self.isActiveWindow():
|
||||
self.close()
|
||||
super().changeEvent(event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Закрывает диалог при клике на пустую область."""
|
||||
pos = event.pos()
|
||||
# Проверяем, находится ли клик вне imageContainer и captionLabel
|
||||
if not (self.imageContainer.geometry().contains(pos) or
|
||||
self.captionLabel.geometry().contains(pos)):
|
||||
self.close()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
class ClickablePixmapItem(QGraphicsPixmapItem):
|
||||
"""
|
||||
Элемент карусели, реагирующий на клик.
|
||||
При клике открывается FullscreenDialog с возможностью перелистывания изображений.
|
||||
"""
|
||||
def __init__(self, pixmap, caption="Просмотр изображения", images_list=None, index=0, carousel=None):
|
||||
"""
|
||||
:param pixmap: QPixmap для отображения в карусели
|
||||
:param caption: Подпись к изображению
|
||||
:param images_list: Список всех изображений (кортежей (QPixmap, caption)),
|
||||
чтобы в диалоге можно было перелистывать.
|
||||
Если не передан, будет использован только текущее изображение.
|
||||
:param index: Индекс текущего изображения в images_list.
|
||||
:param carousel: Ссылка на родительскую карусель (ImageCarousel) для управления стрелками.
|
||||
"""
|
||||
super().__init__(pixmap)
|
||||
self.caption = caption
|
||||
self.images_list = images_list if images_list is not None else [(pixmap, caption)]
|
||||
self.index = index
|
||||
self.carousel = carousel
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.setToolTip(caption)
|
||||
self._click_start_position = None
|
||||
self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._click_start_position = event.scenePos()
|
||||
event.accept()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton and self._click_start_position is not None:
|
||||
distance = (event.scenePos() - self._click_start_position).manhattanLength()
|
||||
if distance < 2:
|
||||
self.show_fullscreen()
|
||||
event.accept()
|
||||
return
|
||||
event.accept()
|
||||
|
||||
def show_fullscreen(self):
|
||||
# Скрываем стрелки карусели перед открытием FullscreenDialog
|
||||
if self.carousel:
|
||||
self.carousel.prevArrow.hide()
|
||||
self.carousel.nextArrow.hide()
|
||||
dialog = FullscreenDialog(self.images_list, current_index=self.index)
|
||||
dialog.exec()
|
||||
# После закрытия диалога обновляем видимость стрелок
|
||||
if self.carousel:
|
||||
self.carousel.update_arrows_visibility()
|
||||
|
||||
|
||||
class ImageCarousel(QGraphicsView):
|
||||
"""
|
||||
Карусель изображений с адаптивностью, возможностью увеличения по клику
|
||||
и перетаскиванием мыши.
|
||||
"""
|
||||
def __init__(self, images: list[tuple], parent: QWidget | None = None, theme: object | None = None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Аннотируем тип scene как QGraphicsScene
|
||||
self.carousel_scene: QGraphicsScene = QGraphicsScene(self)
|
||||
self.setScene(self.carousel_scene)
|
||||
|
||||
self.images = images # Список кортежей: (QPixmap, caption)
|
||||
self.image_items = []
|
||||
self._animation = None
|
||||
self.theme = theme if theme else default_styles
|
||||
self.init_ui()
|
||||
self.create_arrows()
|
||||
|
||||
# Переменные для поддержки перетаскивания
|
||||
self._drag_active = False
|
||||
self._drag_start_position = None
|
||||
self._scroll_start_value = None
|
||||
|
||||
def init_ui(self):
|
||||
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.setFrameShape(QFrame.Shape.NoFrame)
|
||||
|
||||
x_offset = 10 # Отступ между изображениями
|
||||
max_height = 300 # Фиксированная высота изображений
|
||||
x = 0
|
||||
|
||||
for i, (pixmap, caption) in enumerate(self.images):
|
||||
item = ClickablePixmapItem(
|
||||
pixmap.scaledToHeight(max_height, Qt.TransformationMode.SmoothTransformation),
|
||||
caption,
|
||||
images_list=self.images,
|
||||
index=i,
|
||||
carousel=self # Передаем ссылку на карусель
|
||||
)
|
||||
item.setPos(x, 0)
|
||||
self.carousel_scene.addItem(item)
|
||||
self.image_items.append(item)
|
||||
x += item.pixmap().width() + x_offset
|
||||
|
||||
self.setSceneRect(0, 0, x, max_height)
|
||||
|
||||
def create_arrows(self):
|
||||
"""Создаёт кнопки-стрелки и привязывает их к функциям прокрутки."""
|
||||
self.prevArrow = QToolButton(self)
|
||||
self.prevArrow.setArrowType(Qt.ArrowType.LeftArrow)
|
||||
self.prevArrow.setStyleSheet(self.theme.PREV_BUTTON_STYLE) # type: ignore
|
||||
self.prevArrow.setFixedSize(40, 40)
|
||||
self.prevArrow.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.prevArrow.setAutoRepeat(True)
|
||||
self.prevArrow.setAutoRepeatDelay(300)
|
||||
self.prevArrow.setAutoRepeatInterval(100)
|
||||
self.prevArrow.clicked.connect(self.scroll_left)
|
||||
self.prevArrow.raise_()
|
||||
|
||||
self.nextArrow = QToolButton(self)
|
||||
self.nextArrow.setArrowType(Qt.ArrowType.RightArrow)
|
||||
self.nextArrow.setStyleSheet(self.theme.NEXT_BUTTON_STYLE) # type: ignore
|
||||
self.nextArrow.setFixedSize(40, 40)
|
||||
self.nextArrow.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.nextArrow.setAutoRepeat(True)
|
||||
self.nextArrow.setAutoRepeatDelay(300)
|
||||
self.nextArrow.setAutoRepeatInterval(100)
|
||||
self.nextArrow.clicked.connect(self.scroll_right)
|
||||
self.nextArrow.raise_()
|
||||
|
||||
# Проверяем видимость стрелок при создании
|
||||
self.update_arrows_visibility()
|
||||
|
||||
def update_arrows_visibility(self):
|
||||
"""
|
||||
Показывает стрелки, если контент шире видимой области.
|
||||
Иначе скрывает их.
|
||||
"""
|
||||
if hasattr(self, "prevArrow") and hasattr(self, "nextArrow"):
|
||||
if self.horizontalScrollBar().maximum() == 0:
|
||||
self.prevArrow.hide()
|
||||
self.nextArrow.hide()
|
||||
else:
|
||||
self.prevArrow.show()
|
||||
self.nextArrow.show()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
margin = 10
|
||||
self.prevArrow.move(margin, (self.height() - self.prevArrow.height()) // 2)
|
||||
self.nextArrow.move(self.width() - self.nextArrow.width() - margin,
|
||||
(self.height() - self.nextArrow.height()) // 2)
|
||||
self.update_arrows_visibility()
|
||||
|
||||
def animate_scroll(self, end_value):
|
||||
scrollbar = self.horizontalScrollBar()
|
||||
start_value = scrollbar.value()
|
||||
animation = QPropertyAnimation(scrollbar, QByteArray(b"value"), self)
|
||||
animation.setDuration(300)
|
||||
animation.setStartValue(start_value)
|
||||
animation.setEndValue(end_value)
|
||||
animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
||||
self._animation = animation
|
||||
animation.start()
|
||||
|
||||
def scroll_left(self):
|
||||
scrollbar = self.horizontalScrollBar()
|
||||
new_value = scrollbar.value() - 100
|
||||
self.animate_scroll(new_value)
|
||||
|
||||
def scroll_right(self):
|
||||
scrollbar = self.horizontalScrollBar()
|
||||
new_value = scrollbar.value() + 100
|
||||
self.animate_scroll(new_value)
|
||||
|
||||
def update_images(self, new_images):
|
||||
self.carousel_scene.clear()
|
||||
self.images = new_images
|
||||
self.image_items.clear()
|
||||
self.init_ui()
|
||||
self.update_arrows_visibility()
|
||||
|
||||
# Обработка событий мыши для перетаскивания
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._drag_active = True
|
||||
self._drag_start_position = event.pos()
|
||||
self._scroll_start_value = self.horizontalScrollBar().value()
|
||||
# Скрываем стрелки при начале перетаскивания
|
||||
if hasattr(self, "prevArrow"):
|
||||
self.prevArrow.hide()
|
||||
if hasattr(self, "nextArrow"):
|
||||
self.nextArrow.hide()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self._drag_active and self._drag_start_position is not None:
|
||||
delta = event.pos().x() - self._drag_start_position.x()
|
||||
new_value = self._scroll_start_value - delta
|
||||
self.horizontalScrollBar().setValue(new_value)
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self._drag_active = False
|
||||
# Показываем стрелки после завершения перетаскивания (с проверкой видимости)
|
||||
self.update_arrows_visibility()
|
||||
super().mouseReleaseEvent(event)
|
Reference in New Issue
Block a user