From 08761d5f8a1003309aa782bc82e3048e572edc85 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sat, 8 Jun 2024 15:18:40 -0700 Subject: [PATCH] Add Landing Page When No Library Is Opened (#258) * Add landing page when no library is open - Add landing page when no library is open - Add linear_gradient method - Reformat main_window.py with spaces instead of tabs because apparently it wasn't formatted already? * Add color_overlay methods, ClickableLabel widget - Add color_overlay helper methods - Add clickable_label widget - Add docstrings to landing.py methods - Add logo easter egg - Refactor landing.py content * Fix redefinition * Fix macOS shortcut text --- .../qt/images/tagstudio_logo_text_mono.png | Bin 0 -> 43358 bytes tagstudio/src/qt/helpers/color_overlay.py | 59 +++ tagstudio/src/qt/helpers/gradient.py | 17 +- tagstudio/src/qt/main_window.py | 393 ++++++++---------- tagstudio/src/qt/ts_qt.py | 50 +-- tagstudio/src/qt/widgets/clickable_label.py | 18 + tagstudio/src/qt/widgets/landing.py | 181 ++++++++ 7 files changed, 481 insertions(+), 237 deletions(-) create mode 100644 tagstudio/resources/qt/images/tagstudio_logo_text_mono.png create mode 100644 tagstudio/src/qt/helpers/color_overlay.py create mode 100644 tagstudio/src/qt/widgets/clickable_label.py create mode 100644 tagstudio/src/qt/widgets/landing.py diff --git a/tagstudio/resources/qt/images/tagstudio_logo_text_mono.png b/tagstudio/resources/qt/images/tagstudio_logo_text_mono.png new file mode 100644 index 0000000000000000000000000000000000000000..a5d33347478ad9adbf6c23045240ead870bbd47b GIT binary patch literal 43358 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 literal 0 HcmV?d00001 diff --git a/tagstudio/src/qt/helpers/color_overlay.py b/tagstudio/src/qt/helpers/color_overlay.py new file mode 100644 index 00000000..c19ba73e --- /dev/null +++ b/tagstudio/src/qt/helpers/color_overlay.py @@ -0,0 +1,59 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from PIL import Image +from PySide6.QtCore import Qt +from PySide6.QtGui import QGuiApplication +from src.qt.helpers.gradient import linear_gradient + +# TODO: Consolidate the built-in QT theme values with the values +# here, in enums.py, and in palette.py. +_THEME_DARK_FG: str = "#FFFFFF55" +_THEME_LIGHT_FG: str = "#000000DD" + + +def theme_fg_overlay(image: Image.Image) -> Image.Image: + """ + Overlay the foreground theme color onto an image. + + Args: + image (Image): The PIL Image object to apply an overlay to. + """ + + overlay_color = ( + _THEME_DARK_FG + if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark + else _THEME_LIGHT_FG + ) + im = Image.new(mode="RGBA", size=image.size, color=overlay_color) + return _apply_overlay(image, im) + + +def gradient_overlay(image: Image.Image, gradient=list[str]) -> Image.Image: + """ + Overlay a color gradient onto an image. + + Args: + image (Image): The PIL Image object to apply an overlay to. + gradient (list[str): A list of string hex color codes for use as + the colors of the gradient. + """ + + im: Image.Image = _apply_overlay(image, linear_gradient(image.size, gradient)) + return im + + +def _apply_overlay(image: Image.Image, overlay: Image.Image) -> Image.Image: + """ + Internal method to apply an overlay on top of an image, using + the image's alpha channel as a mask. + + Args: + image (Image): The PIL Image object to apply an overlay to. + overlay (Image): The PIL Image object to act as the overlay contents. + """ + im: Image.Image = Image.new(mode="RGBA", size=image.size, color="#00000000") + im.paste(overlay, (0, 0), mask=image) + return im diff --git a/tagstudio/src/qt/helpers/gradient.py b/tagstudio/src/qt/helpers/gradient.py index b346d71a..dabe7639 100644 --- a/tagstudio/src/qt/helpers/gradient.py +++ b/tagstudio/src/qt/helpers/gradient.py @@ -5,7 +5,9 @@ from PIL import Image, ImageEnhance, ImageChops -def four_corner_gradient_background(image: Image.Image, adj_size, mask, hl): +def four_corner_gradient_background( + image: Image.Image, adj_size, mask, hl +) -> Image.Image: if image.size != (adj_size, adj_size): # Old 1 color method. # bg_col = image.copy().resize((1, 1)).getpixel((0,0)) @@ -48,3 +50,16 @@ def four_corner_gradient_background(image: Image.Image, adj_size, mask, hl): hl_soft.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(0.5)) final.paste(ImageChops.soft_light(final, hl_soft), mask=hl_soft.getchannel(3)) return final + + +def linear_gradient( + size=tuple[int, int], + colors=list[str], + interpolation: Image.Resampling = Image.Resampling.BICUBIC, +) -> Image.Image: + seed: Image.Image = Image.new(mode="RGBA", size=(len(colors), 1), color="#000000") + for i, color in enumerate(colors): + c_im: Image.Image = Image.new(mode="RGBA", size=(1, 1), color=color) + seed.paste(c_im, (i, 0)) + gradient: Image.Image = seed.resize(size, resample=interpolation) + return gradient diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index 1eb2053b..a77f8744 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -12,256 +12,227 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect, - QSize, Qt) -from PySide6.QtGui import (QFont, QAction) + +import logging +import typing +from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect,QSize, Qt) +from PySide6.QtGui import QFont from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout, - QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow, - QPushButton, QScrollArea, QSizePolicy, - QStatusBar, QWidget, QSplitter, QCheckBox, - QSpacerItem) + QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow, + QPushButton, QScrollArea, QSizePolicy, + QStatusBar, QWidget, QSplitter, QCheckBox, + QSpacerItem) from src.qt.pagination import Pagination +from src.qt.widgets.landing import LandingWidget + +# Only import for type checking/autocompletion, will not be imported at runtime. +if typing.TYPE_CHECKING: + from src.qt.ts_qt import QtDriver + +logging.basicConfig(format="%(message)s", level=logging.INFO) class Ui_MainWindow(QMainWindow): - def __init__(self, parent=None) -> None: - super().__init__(parent) - self.setupUi(self) + def __init__(self, driver: "QtDriver", parent=None) -> None: + super().__init__(parent) + self.driver: "QtDriver" = driver + self.setupUi(self) - # self.setWindowFlag(Qt.WindowType.NoDropShadowWindowHint, True) - # self.setWindowFlag(Qt.WindowType.WindowTransparentForInput, False) - # # self.setWindowFlag(Qt.WindowType.FramelessWindowHint, True) - # self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + # NOTE: These are old attempts to allow for a translucent/acrylic + # window effect. This may be attempted again in the future. + # self.setWindowFlag(Qt.WindowType.NoDropShadowWindowHint, True) + # self.setWindowFlag(Qt.WindowType.WindowTransparentForInput, False) + # # self.setWindowFlag(Qt.WindowType.FramelessWindowHint, True) + # self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - # self.windowFX = WindowEffect() - # self.windowFX.setAcrylicEffect(self.winId(), isEnableShadow=False) + # self.windowFX = WindowEffect() + # self.windowFX.setAcrylicEffect(self.winId(), isEnableShadow=False) - # # self.setStyleSheet( - # # 'background:#EE000000;' - # # ) - + # # self.setStyleSheet( + # # 'background:#EE000000;' + # # ) + - def setupUi(self, MainWindow): - if not MainWindow.objectName(): - MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(1300, 720) - - self.centralwidget = QWidget(MainWindow) - self.centralwidget.setObjectName(u"centralwidget") - self.gridLayout = QGridLayout(self.centralwidget) - self.gridLayout.setObjectName(u"gridLayout") - self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.setObjectName(u"horizontalLayout") + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(1300, 720) + + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.gridLayout = QGridLayout(self.centralwidget) + self.gridLayout.setObjectName(u"gridLayout") + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") - # ComboBox goup for search type and thumbnail size - self.horizontalLayout_3 = QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") + # ComboBox goup for search type and thumbnail size + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") - # left side spacer - spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) - self.horizontalLayout_3.addItem(spacerItem) + # left side spacer + spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalLayout_3.addItem(spacerItem) - # Search type selector - self.comboBox_2 = QComboBox(self.centralwidget) - self.comboBox_2.setMinimumSize(QSize(165, 0)) - self.comboBox_2.setObjectName("comboBox_2") - self.comboBox_2.addItem("") - self.comboBox_2.addItem("") - self.horizontalLayout_3.addWidget(self.comboBox_2) + # Search type selector + self.comboBox_2 = QComboBox(self.centralwidget) + self.comboBox_2.setMinimumSize(QSize(165, 0)) + self.comboBox_2.setObjectName("comboBox_2") + self.comboBox_2.addItem("") + self.comboBox_2.addItem("") + self.horizontalLayout_3.addWidget(self.comboBox_2) # Thumbnail Size placeholder - self.comboBox = QComboBox(self.centralwidget) - self.comboBox.setObjectName(u"comboBox") - sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth( - self.comboBox.sizePolicy().hasHeightForWidth()) - self.comboBox.setSizePolicy(sizePolicy) - self.comboBox.setMinimumWidth(128) - self.comboBox.setMaximumWidth(128) - self.horizontalLayout_3.addWidget(self.comboBox) - self.gridLayout.addLayout(self.horizontalLayout_3, 5, 0, 1, 1) + self.comboBox = QComboBox(self.centralwidget) + self.comboBox.setObjectName(u"comboBox") + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.comboBox.sizePolicy().hasHeightForWidth()) + self.comboBox.setSizePolicy(sizePolicy) + self.comboBox.setMinimumWidth(128) + self.comboBox.setMaximumWidth(128) + self.horizontalLayout_3.addWidget(self.comboBox) + self.gridLayout.addLayout(self.horizontalLayout_3, 5, 0, 1, 1) - self.splitter = QSplitter() - self.splitter.setObjectName(u"splitter") - self.splitter.setHandleWidth(12) + self.splitter = QSplitter() + self.splitter.setObjectName(u"splitter") + self.splitter.setHandleWidth(12) - self.frame_container = QWidget() - self.frame_layout = QVBoxLayout(self.frame_container) - self.frame_layout.setSpacing(0) + self.frame_container = QWidget() + self.frame_layout = QVBoxLayout(self.frame_container) + self.frame_layout.setSpacing(0) - self.scrollArea = QScrollArea() - self.scrollArea.setObjectName(u"scrollArea") - self.scrollArea.setFocusPolicy(Qt.WheelFocus) - self.scrollArea.setFrameShape(QFrame.NoFrame) - self.scrollArea.setFrameShadow(QFrame.Plain) - self.scrollArea.setWidgetResizable(True) - self.scrollAreaWidgetContents = QWidget() - self.scrollAreaWidgetContents.setObjectName( - u"scrollAreaWidgetContents") - self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 1260, 590)) - self.gridLayout_2 = QGridLayout(self.scrollAreaWidgetContents) - self.gridLayout_2.setSpacing(8) - self.gridLayout_2.setObjectName(u"gridLayout_2") - self.gridLayout_2.setContentsMargins(0, 0, 0, 8) - self.scrollArea.setWidget(self.scrollAreaWidgetContents) - self.frame_layout.addWidget(self.scrollArea) + self.scrollArea = QScrollArea() + self.scrollArea.setObjectName(u"scrollArea") + self.scrollArea.setFocusPolicy(Qt.WheelFocus) + self.scrollArea.setFrameShape(QFrame.NoFrame) + self.scrollArea.setFrameShadow(QFrame.Plain) + self.scrollArea.setWidgetResizable(True) + self.scrollAreaWidgetContents = QWidget() + self.scrollAreaWidgetContents.setObjectName( + u"scrollAreaWidgetContents") + self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 1260, 590)) + self.gridLayout_2 = QGridLayout(self.scrollAreaWidgetContents) + self.gridLayout_2.setSpacing(8) + self.gridLayout_2.setObjectName(u"gridLayout_2") + self.gridLayout_2.setContentsMargins(0, 0, 0, 8) + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + self.frame_layout.addWidget(self.scrollArea) + + self.landing_widget: LandingWidget = LandingWidget(self.driver, self.devicePixelRatio()) + self.frame_layout.addWidget(self.landing_widget) - # self.page_bar_controls = QWidget() - # self.page_bar_controls.setStyleSheet('background:blue;') - # self.page_bar_controls.setMinimumHeight(32) + self.pagination = Pagination() + self.frame_layout.addWidget(self.pagination) - self.pagination = Pagination() - self.frame_layout.addWidget(self.pagination) + self.horizontalLayout.addWidget(self.splitter) + self.splitter.addWidget(self.frame_container) + self.splitter.setStretchFactor(0, 1) - # self.frame_layout.addWidget(self.page_bar_controls) - # self.frame_layout.addWidget(self.page_bar_controls) + self.gridLayout.addLayout(self.horizontalLayout, 10, 0, 1, 1) - self.horizontalLayout.addWidget(self.splitter) - self.splitter.addWidget(self.frame_container) - self.splitter.setStretchFactor(0, 1) + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.horizontalLayout_2.setSizeConstraint(QLayout.SetMinimumSize) + self.backButton = QPushButton(self.centralwidget) + self.backButton.setObjectName(u"backButton") + self.backButton.setMinimumSize(QSize(0, 32)) + self.backButton.setMaximumSize(QSize(32, 16777215)) + font = QFont() + font.setPointSize(14) + font.setBold(True) + self.backButton.setFont(font) - self.gridLayout.addLayout(self.horizontalLayout, 10, 0, 1, 1) + self.horizontalLayout_2.addWidget(self.backButton) - self.horizontalLayout_2 = QHBoxLayout() - self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") - self.horizontalLayout_2.setSizeConstraint(QLayout.SetMinimumSize) - self.backButton = QPushButton(self.centralwidget) - self.backButton.setObjectName(u"backButton") - self.backButton.setMinimumSize(QSize(0, 32)) - self.backButton.setMaximumSize(QSize(32, 16777215)) - font = QFont() - font.setPointSize(14) - font.setBold(True) - self.backButton.setFont(font) + self.forwardButton = QPushButton(self.centralwidget) + self.forwardButton.setObjectName(u"forwardButton") + self.forwardButton.setMinimumSize(QSize(0, 32)) + self.forwardButton.setMaximumSize(QSize(32, 16777215)) + font1 = QFont() + font1.setPointSize(14) + font1.setBold(True) + font1.setKerning(True) + self.forwardButton.setFont(font1) - self.horizontalLayout_2.addWidget(self.backButton) + self.horizontalLayout_2.addWidget(self.forwardButton) - self.forwardButton = QPushButton(self.centralwidget) - self.forwardButton.setObjectName(u"forwardButton") - self.forwardButton.setMinimumSize(QSize(0, 32)) - self.forwardButton.setMaximumSize(QSize(32, 16777215)) - font1 = QFont() - font1.setPointSize(14) - font1.setBold(True) - font1.setKerning(True) - self.forwardButton.setFont(font1) + self.searchField = QLineEdit(self.centralwidget) + self.searchField.setObjectName(u"searchField") + self.searchField.setMinimumSize(QSize(0, 32)) + font2 = QFont() + font2.setPointSize(11) + font2.setBold(False) + self.searchField.setFont(font2) - self.horizontalLayout_2.addWidget(self.forwardButton) + self.horizontalLayout_2.addWidget(self.searchField) - self.searchField = QLineEdit(self.centralwidget) - self.searchField.setObjectName(u"searchField") - self.searchField.setMinimumSize(QSize(0, 32)) - font2 = QFont() - font2.setPointSize(11) - font2.setBold(False) - self.searchField.setFont(font2) + self.searchButton = QPushButton(self.centralwidget) + self.searchButton.setObjectName(u"searchButton") + self.searchButton.setMinimumSize(QSize(0, 32)) + self.searchButton.setFont(font2) - self.horizontalLayout_2.addWidget(self.searchField) + self.horizontalLayout_2.addWidget(self.searchButton) + self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1) + self.gridLayout_2.setContentsMargins(6, 6, 6, 6) - self.searchButton = QPushButton(self.centralwidget) - self.searchButton.setObjectName(u"searchButton") - self.searchButton.setMinimumSize(QSize(0, 32)) - self.searchButton.setFont(font2) + MainWindow.setCentralWidget(self.centralwidget) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName(u"statusbar") + sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth( + self.statusbar.sizePolicy().hasHeightForWidth()) + self.statusbar.setSizePolicy(sizePolicy1) + MainWindow.setStatusBar(self.statusbar) - self.horizontalLayout_2.addWidget(self.searchButton) - self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1) + self.retranslateUi(MainWindow) - # self.comboBox = QComboBox(self.centralwidget) - # self.comboBox.setObjectName(u"comboBox") - # sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) - # sizePolicy.setHorizontalStretch(0) - # sizePolicy.setVerticalStretch(0) - # sizePolicy.setHeightForWidth( - # self.comboBox.sizePolicy().hasHeightForWidth()) - # self.comboBox.setSizePolicy(sizePolicy) - # self.comboBox.setMinimumWidth(128) - # self.comboBox.setMaximumWidth(128) + QMetaObject.connectSlotsByName(MainWindow) + # setupUi - # self.gridLayout.addWidget(self.comboBox, 4, 0, 1, 1, Qt.AlignRight) - - self.gridLayout_2.setContentsMargins(6, 6, 6, 6) - - MainWindow.setCentralWidget(self.centralwidget) - # self.menubar = QMenuBar(MainWindow) - # self.menubar.setObjectName(u"menubar") - # self.menubar.setGeometry(QRect(0, 0, 1280, 22)) - # MainWindow.setMenuBar(self.menubar) - self.statusbar = QStatusBar(MainWindow) - self.statusbar.setObjectName(u"statusbar") - sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) - sizePolicy1.setHorizontalStretch(0) - sizePolicy1.setVerticalStretch(0) - sizePolicy1.setHeightForWidth( - self.statusbar.sizePolicy().hasHeightForWidth()) - self.statusbar.setSizePolicy(sizePolicy1) - MainWindow.setStatusBar(self.statusbar) - - # menu_bar = self.menuBar() - # self.setMenuBar(menu_bar) - # self.gridLayout.addWidget(menu_bar, 4, 0, 1, 1, Qt.AlignRight) - # self.frame_layout.addWidget(menu_bar) - - self.retranslateUi(MainWindow) - - QMetaObject.connectSlotsByName(MainWindow) - # setupUi - - def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QCoreApplication.translate( - "MainWindow", u"MainWindow", None)) + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate( + "MainWindow", u"MainWindow", None)) # Navigation buttons - self.backButton.setText( - QCoreApplication.translate("MainWindow", u"<", None)) - self.forwardButton.setText( - QCoreApplication.translate("MainWindow", u">", None)) + self.backButton.setText( + QCoreApplication.translate("MainWindow", u"<", None)) + self.forwardButton.setText( + QCoreApplication.translate("MainWindow", u">", None)) # Search field - self.searchField.setPlaceholderText( - QCoreApplication.translate("MainWindow", u"Search Entries", None)) - self.searchButton.setText( - QCoreApplication.translate("MainWindow", u"Search", None)) + self.searchField.setPlaceholderText( + QCoreApplication.translate("MainWindow", u"Search Entries", None)) + self.searchButton.setText( + QCoreApplication.translate("MainWindow", u"Search", None)) # Search type selector - self.comboBox_2.setItemText(0, QCoreApplication.translate("MainWindow", "And (Includes All Tags)")) - self.comboBox_2.setItemText(1, QCoreApplication.translate("MainWindow", "Or (Includes Any Tag)")) - self.comboBox.setCurrentText("") + self.comboBox_2.setItemText(0, QCoreApplication.translate("MainWindow", "And (Includes All Tags)")) + self.comboBox_2.setItemText(1, QCoreApplication.translate("MainWindow", "Or (Includes Any Tag)")) + self.comboBox.setCurrentText("") - # Tumbnail size selector - self.comboBox.setPlaceholderText( - QCoreApplication.translate("MainWindow", u"Thumbnail Size", None)) - # retranslateUi + # Thumbnail size selector + self.comboBox.setPlaceholderText( + QCoreApplication.translate("MainWindow", u"Thumbnail Size", None)) + # retranslateUi - def moveEvent(self, event) -> None: - # time.sleep(0.02) # sleep for 20ms - pass + def moveEvent(self, event) -> None: + # time.sleep(0.02) # sleep for 20ms + pass - def resizeEvent(self, event) -> None: - # time.sleep(0.02) # sleep for 20ms - pass + def resizeEvent(self, event) -> None: + # time.sleep(0.02) # sleep for 20ms + pass - # def _createMenuBar(self, main_window): - # menu_bar = QMenuBar(main_window) - # file_menu = QMenu('&File', main_window) - # edit_menu = QMenu('&Edit', main_window) - # tools_menu = QMenu('&Tools', main_window) - # macros_menu = QMenu('&Macros', main_window) - # help_menu = QMenu('&Help', main_window) - - # file_menu.addAction(QAction('&New Library', main_window)) - # file_menu.addAction(QAction('&Open Library', main_window)) - # file_menu.addAction(QAction('&Save Library', main_window)) - # file_menu.addAction(QAction('&Close Library', main_window)) - - # file_menu.addAction(QAction('&Refresh Directories', main_window)) - # file_menu.addAction(QAction('&Add New Files to Library', main_window)) - - # menu_bar.addMenu(file_menu) - # menu_bar.addMenu(edit_menu) - # menu_bar.addMenu(tools_menu) - # menu_bar.addMenu(macros_menu) - # menu_bar.addMenu(help_menu) - - # main_window.setMenuBar(menu_bar) + def toggle_landing_page(self, enabled: bool): + if enabled: + self.scrollArea.setHidden(True) + self.landing_widget.setHidden(False) + self.landing_widget.animate_logo_in() + else: + self.landing_widget.setHidden(True) + self.landing_widget.set_status_label("") + self.scrollArea.setHidden(False) \ No newline at end of file diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 5447182e..6da29000 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -21,7 +21,15 @@ from queue import Queue from typing import Optional from PIL import Image from PySide6 import QtCore -from PySide6.QtCore import QObject, QThread, Signal, Qt, QThreadPool, QTimer, QSettings +from PySide6.QtCore import ( + QObject, + QThread, + Signal, + Qt, + QThreadPool, + QTimer, + QSettings, +) from PySide6.QtGui import ( QGuiApplication, QPixmap, @@ -51,22 +59,6 @@ from src.core.enums import SettingItems, SearchMode from src.core.library import ItemType from src.core.ts_core import TagStudioCore from src.core.constants import ( - PLAINTEXT_TYPES, - TAG_COLORS, - DATE_FIELDS, - TEXT_FIELDS, - BOX_FIELDS, - ALL_FILE_TYPES, - SHORTCUT_TYPES, - PROGRAM_TYPES, - ARCHIVE_TYPES, - PRESENTATION_TYPES, - SPREADSHEET_TYPES, - DOC_TYPES, - AUDIO_TYPES, - VIDEO_TYPES, - IMAGE_TYPES, - LIBRARY_FILENAME, COLLAGE_FOLDER_NAME, BACKUP_FOLDER_NAME, TS_FOLDER_NAME, @@ -266,7 +258,7 @@ class QtDriver(QObject): timer.timeout.connect(lambda: None) # self.main_window = loader.load(home_path) - self.main_window = Ui_MainWindow() + self.main_window = Ui_MainWindow(self) self.main_window.setWindowTitle(self.base_title) self.main_window.mousePressEvent = self.mouse_navigation # type: ignore # self.main_window.setStyleSheet( @@ -557,6 +549,7 @@ class QtDriver(QObject): self.shutdown() def init_library_window(self): + # self._init_landing_page() # Taken care of inside the widget now self._init_thumb_grid() # TODO: Put this into its own method that copies the font file(s) into memory @@ -585,6 +578,13 @@ class QtDriver(QObject): forward_button: QPushButton = self.main_window.forwardButton forward_button.clicked.connect(self.nav_forward) + # NOTE: Putting this early will result in a white non-responsive + # window until everything is loaded. Consider adding a splash screen + # or implementing some clever loading tricks. + self.main_window.show() + self.main_window.activateWindow() + self.main_window.toggle_landing_page(True) + self.frame_dict = {} self.main_window.pagination.index.connect( lambda i: ( @@ -606,11 +606,6 @@ class QtDriver(QObject): # self.render_times: list = [] # self.main_window.setWindowFlag(Qt.FramelessWindowHint) - # NOTE: Putting this early will result in a white non-responsive - # window until everything is loaded. Consider adding a splash screen - # or implementing some clever loading tricks. - self.main_window.show() - self.main_window.activateWindow() # self.main_window.raise_() self.splash.finish(self.main_window) self.preview_panel.update_widgets() @@ -696,6 +691,7 @@ class QtDriver(QObject): self.selected.clear() self.preview_panel.update_widgets() self.filter_items() + self.main_window.toggle_landing_page(True) end_time = time.time() self.main_window.statusbar.showMessage( @@ -1439,15 +1435,18 @@ class QtDriver(QObject): def open_library(self, path: Path): """Opens a TagStudio library.""" + open_message: str = f'Opening Library "{str(path)}"...' + self.main_window.landing_widget.set_status_label(open_message) + self.main_window.statusbar.showMessage(open_message, 3) + self.main_window.repaint() + if self.lib.library_dir: self.save_library() self.lib.clear_internal_vars() - self.main_window.statusbar.showMessage(f"Opening Library {str(path)}", 3) return_code = self.lib.open_library(path) if return_code == 1: pass - else: logging.info( f"{ERROR} No existing TagStudio library found at '{path}'. Creating one." @@ -1465,6 +1464,7 @@ class QtDriver(QObject): self.selected.clear() self.preview_panel.update_widgets() self.filter_items() + self.main_window.toggle_landing_page(False) def create_collage(self) -> None: """Generates and saves an image collage based on Library Entries.""" diff --git a/tagstudio/src/qt/widgets/clickable_label.py b/tagstudio/src/qt/widgets/clickable_label.py new file mode 100644 index 00000000..ca812f95 --- /dev/null +++ b/tagstudio/src/qt/widgets/clickable_label.py @@ -0,0 +1,18 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +from PySide6.QtCore import Signal +from PySide6.QtWidgets import QLabel + + +class ClickableLabel(QLabel): + """A clickable Label widget.""" + + clicked = Signal() + + def __init__(self): + super().__init__() + + def mousePressEvent(self, event): + self.clicked.emit() diff --git a/tagstudio/src/qt/widgets/landing.py b/tagstudio/src/qt/widgets/landing.py new file mode 100644 index 00000000..9df3fa4d --- /dev/null +++ b/tagstudio/src/qt/widgets/landing.py @@ -0,0 +1,181 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +import logging +import sys +import typing +from pathlib import Path +from PIL import Image, ImageQt +from PySide6.QtCore import Qt, QPropertyAnimation, QPoint, QEasingCurve +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QPushButton +from src.qt.widgets.clickable_label import ClickableLabel +from src.qt.helpers.color_overlay import gradient_overlay, theme_fg_overlay + +# Only import for type checking/autocompletion, will not be imported at runtime. +if typing.TYPE_CHECKING: + from src.qt.ts_qt import QtDriver + +logging.basicConfig(format="%(message)s", level=logging.INFO) + + +class LandingWidget(QWidget): + def __init__(self, driver: "QtDriver", pixel_ratio: float): + super().__init__() + self.driver: "QtDriver" = driver + self.logo_label: ClickableLabel = ClickableLabel() + self._pixel_ratio: float = pixel_ratio + self._logo_width: int = int(480 * pixel_ratio) + self._special_click_count: int = 0 + + # Create layout -------------------------------------------------------- + self.landing_layout = QVBoxLayout() + self.landing_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.landing_layout.setSpacing(12) + self.setLayout(self.landing_layout) + + # Create landing logo -------------------------------------------------- + # self.landing_logo_pixmap = QPixmap(":/images/tagstudio_logo_text_mono.png") + self.logo_raw: Image.Image = Image.open( + Path(__file__).parents[3] + / "resources/qt/images/tagstudio_logo_text_mono.png" + ) + self.landing_pixmap: QPixmap = QPixmap() + self.update_logo_color() + self.logo_label.clicked.connect(self._update_special_click) + + # Initialize landing logo animation ------------------------------------ + self.logo_pos_anim = QPropertyAnimation(self.logo_label, b"pos") + self.logo_pos_anim.setEasingCurve(QEasingCurve.Type.OutCubic) + self.logo_pos_anim.setDuration(1000) + + self.logo_special_anim = QPropertyAnimation(self.logo_label, b"pos") + self.logo_special_anim.setEasingCurve(QEasingCurve.Type.OutCubic) + self.logo_special_anim.setDuration(500) + + # Create "Open/Create Library" button ---------------------------------- + open_shortcut_text: str = "" + if sys.platform == "darwin": + open_shortcut_text = "(⌘+O)" + else: + open_shortcut_text = "(Ctrl+O)" + self.open_button: QPushButton = QPushButton() + self.open_button.setMinimumWidth(200) + self.open_button.setText(f"Open/Create Library {open_shortcut_text}") + self.open_button.clicked.connect(self.driver.open_library_from_dialog) + + # Create status label -------------------------------------------------- + self.status_label = QLabel() + self.status_label.setMinimumWidth(200) + self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.status_label.setText("") + + # Initialize landing logo animation ------------------------------------ + self.status_pos_anim = QPropertyAnimation(self.status_label, b"pos") + self.status_pos_anim.setEasingCurve(QEasingCurve.Type.OutCubic) + self.status_pos_anim.setDuration(500) + + # Add widgets to layout ------------------------------------------------ + self.landing_layout.addWidget(self.logo_label) + self.landing_layout.addWidget( + self.open_button, alignment=Qt.AlignmentFlag.AlignCenter + ) + self.landing_layout.addWidget( + self.status_label, alignment=Qt.AlignmentFlag.AlignCenter + ) + + def update_logo_color(self, style: str = "mono"): + """ + Update the color of the TagStudio logo. + + Args: + style (str): = The style of the logo. Either "mono" or "gradient". + """ + + logo_im: Image.Image = None + if style == "mono": + logo_im = theme_fg_overlay(self.logo_raw) + elif style == "gradient": + gradient_colors: list[str] = ["#d27bf4", "#7992f5", "#63c6e3", "#63f5cf"] + logo_im = gradient_overlay(self.logo_raw, gradient_colors) + + logo_final: Image.Image = Image.new( + mode="RGBA", size=self.logo_raw.size, color="#00000000" + ) + + logo_final.paste(logo_im, (0, 0), mask=self.logo_raw) + + self.landing_pixmap = QPixmap.fromImage(ImageQt.ImageQt(logo_im)) + self.landing_pixmap.setDevicePixelRatio(self._pixel_ratio) + self.landing_pixmap = self.landing_pixmap.scaledToWidth( + self._logo_width, Qt.TransformationMode.SmoothTransformation + ) + self.logo_label.setMaximumHeight( + int(self.logo_raw.size[1] * (self.logo_raw.size[0] / self._logo_width)) + ) + self.logo_label.setMaximumWidth(self._logo_width) + self.logo_label.setPixmap(self.landing_pixmap) + + def _update_special_click(self): + """ + Increment the click count for the logo easter egg if it has not + been triggered. If it reaches the click threshold, this triggers it + and prevents it from triggering again. + """ + if self._special_click_count >= 0: + self._special_click_count += 1 + if self._special_click_count >= 10: + self.update_logo_color("gradient") + self.animate_logo_pop() + self._special_click_count = -1 + + def animate_logo_in(self): + """Animate in the TagStudio logo.""" + # NOTE: Sometimes, mostly on startup without a library open, the + # y position of logo_label is something like 10. I'm not sure what + # the cause of this is, so I've just done this workaround to disable + # the animation if the y position is too incorrect. + if self.logo_label.y() > 50: + self.logo_pos_anim.setStartValue( + QPoint(self.logo_label.x(), self.logo_label.y() - 100) + ) + self.logo_pos_anim.setEndValue(self.logo_label.pos()) + self.logo_pos_anim.start() + + def animate_logo_pop(self): + """Special pop animation for the TagStudio logo.""" + self.logo_special_anim.setStartValue(self.logo_label.pos()) + self.logo_special_anim.setKeyValueAt( + 0.25, QPoint(self.logo_label.x() - 5, self.logo_label.y()) + ) + self.logo_special_anim.setKeyValueAt( + 0.5, QPoint(self.logo_label.x() + 5, self.logo_label.y() - 10) + ) + self.logo_special_anim.setKeyValueAt( + 0.75, QPoint(self.logo_label.x() - 5, self.logo_label.y()) + ) + self.logo_special_anim.setEndValue(self.logo_label.pos()) + + self.logo_special_anim.start() + + # def animate_status(self): + # # if self.status_label.y() > 50: + # logging.info(f"{self.status_label.pos()}") + # self.status_pos_anim.setStartValue( + # QPoint(self.status_label.x(), self.status_label.y() + 50) + # ) + # self.status_pos_anim.setEndValue(self.status_label.pos()) + # self.status_pos_anim.start() + + def set_status_label(self, text=str): + """ + Set the text of the status label. + + Args: + text (str): Text of the status to set. + """ + # if text: + # self.animate_status() + self.status_label.setText(text)