diff --git a/tagstudio/resources/qt/images/thumb_border_512.png b/tagstudio/resources/qt/images/thumb_border_512.png deleted file mode 100644 index 605717e3..00000000 Binary files a/tagstudio/resources/qt/images/thumb_border_512.png and /dev/null differ diff --git a/tagstudio/resources/qt/images/thumb_mask_128.png b/tagstudio/resources/qt/images/thumb_mask_128.png deleted file mode 100644 index 52a0a135..00000000 Binary files a/tagstudio/resources/qt/images/thumb_mask_128.png and /dev/null differ diff --git a/tagstudio/resources/qt/images/thumb_mask_512.png b/tagstudio/resources/qt/images/thumb_mask_512.png deleted file mode 100644 index ce641abc..00000000 Binary files a/tagstudio/resources/qt/images/thumb_mask_512.png and /dev/null differ diff --git a/tagstudio/resources/qt/images/thumb_mask_hl_512.png b/tagstudio/resources/qt/images/thumb_mask_hl_512.png deleted file mode 100644 index 36c896b8..00000000 Binary files a/tagstudio/resources/qt/images/thumb_mask_hl_512.png and /dev/null differ diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index a77f8744..bd03b0b1 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -66,7 +66,7 @@ class Ui_MainWindow(QMainWindow): self.horizontalLayout = QHBoxLayout() self.horizontalLayout.setObjectName(u"horizontalLayout") - # ComboBox goup for search type and thumbnail size + # ComboBox group for search type and thumbnail size self.horizontalLayout_3 = QHBoxLayout() self.horizontalLayout_3.setObjectName("horizontalLayout_3") @@ -83,17 +83,17 @@ class Ui_MainWindow(QMainWindow): self.horizontalLayout_3.addWidget(self.comboBox_2) # Thumbnail Size placeholder - self.comboBox = QComboBox(self.centralwidget) - self.comboBox.setObjectName(u"comboBox") + self.thumb_size_combobox = QComboBox(self.centralwidget) + self.thumb_size_combobox.setObjectName(u"thumbSizeComboBox") 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.thumb_size_combobox.sizePolicy().hasHeightForWidth()) + self.thumb_size_combobox.setSizePolicy(sizePolicy) + self.thumb_size_combobox.setMinimumWidth(128) + self.thumb_size_combobox.setMaximumWidth(352) + self.horizontalLayout_3.addWidget(self.thumb_size_combobox) self.gridLayout.addLayout(self.horizontalLayout_3, 5, 0, 1, 1) self.splitter = QSplitter() @@ -212,10 +212,10 @@ class Ui_MainWindow(QMainWindow): # 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.thumb_size_combobox.setCurrentText("") # Thumbnail size selector - self.comboBox.setPlaceholderText( + self.thumb_size_combobox.setPlaceholderText( QCoreApplication.translate("MainWindow", u"Thumbnail Size", None)) # retranslateUi diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 73382b1d..c1169759 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -557,11 +557,17 @@ class QtDriver(QObject): str(Path(__file__).parents[2] / "resources/qt/fonts/Oxanium-Bold.ttf") ) + self.thumb_sizes: list[tuple[str, int]] = [ + ("Extra Large Thumbnails", 256), + ("Large Thumbnails", 192), + ("Medium Thumbnails", 128), + ("Small Thumbnails", 96), + ("Mini Thumbnails", 76), + ] self.thumb_size = 128 self.max_results = 500 self.item_thumbs: list[ItemThumb] = [] self.thumb_renderers: list[ThumbRenderer] = [] - self.collation_thumb_size = math.ceil(self.thumb_size * 2) self.init_library_window() @@ -596,23 +602,35 @@ 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 # so the resource isn't being used, then store the specific size variations # in a global dict for methods to access for different DPIs. # adj_font_size = math.floor(12 * self.main_window.devicePixelRatio()) # self.ext_font = ImageFont.truetype(os.path.normpath(f'{Path(__file__).parents[2]}/resources/qt/fonts/Oxanium-Bold.ttf'), adj_font_size) + # Search Button search_button: QPushButton = self.main_window.searchButton search_button.clicked.connect( lambda: self.filter_items(self.main_window.searchField.text()) ) + + # Search Field search_field: QLineEdit = self.main_window.searchField search_field.returnPressed.connect( lambda: self.filter_items(self.main_window.searchField.text()) ) + + # Thumbnail Size ComboBox + thumb_size_combobox: QComboBox = self.main_window.thumb_size_combobox + for size in self.thumb_sizes: + thumb_size_combobox.addItem(size[0]) + thumb_size_combobox.setCurrentIndex(2) # Default: Medium + thumb_size_combobox.currentIndexChanged.connect( + lambda: self.thumb_size_callback(thumb_size_combobox.currentIndex()) + ) + self._init_thumb_grid() + + # Search Type ComboBox search_type_selector: QComboBox = self.main_window.comboBox_2 search_type_selector.currentIndexChanged.connect( lambda: self.set_search_type( @@ -1099,6 +1117,30 @@ class QtDriver(QObject): else: self.paste_entry_fields_action.setText("&Paste Fields") + def thumb_size_callback(self, index: int): + """ + Performs actions needed when the thumbnail size selection is changed. + + Args: + index (int): The index of the item_thumbs/ComboBox list to use. + """ + # Index 2 is the default (Medium) + if index < len(self.thumb_sizes) and index >= 0: + self.thumb_size = self.thumb_sizes[index][1] + else: + logging.error( + f"ERROR: Invalid thumbnail size index ({index}). Defaulting to 128px." + ) + self.thumb_size = 128 + self.update_thumbs() + for it in self.item_thumbs: + it.resize(self.thumb_size, self.thumb_size) + it.thumb_size = (self.thumb_size, self.thumb_size) + it.setMinimumSize(self.thumb_size, self.thumb_size) + it.setMaximumSize(self.thumb_size, self.thumb_size) + it.thumb_button.thumb_size = (self.thumb_size, self.thumb_size) + self.flow_container.layout().setSpacing(min(self.thumb_size // 10, 12)) + def mouse_navigation(self, event: QMouseEvent): # print(event.button()) if event.button() == Qt.MouseButton.ForwardButton: diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index db9efdde..09b474f0 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -62,15 +62,10 @@ class ThumbRenderer(QObject): # updatedImage = Signal(QPixmap) # updatedSize = Signal(QSize) - thumb_mask_512: Image.Image = Image.open( - Path(__file__).parents[3] / "resources/qt/images/thumb_mask_512.png" - ) - thumb_mask_512.load() - - thumb_mask_hl_512: Image.Image = Image.open( - Path(__file__).parents[3] / "resources/qt/images/thumb_mask_hl_512.png" - ) - thumb_mask_hl_512.load() + # Cached thumbnail elements. + # Key: Size + Pixel Ratio Tuple (Ex. (512, 512, 1.25)) + thumb_masks: dict = {} + thumb_borders: dict = {} thumb_loading_512: Image.Image = Image.open( Path(__file__).parents[3] / "resources/qt/images/thumb_loading_512.png" @@ -98,6 +93,76 @@ class ThumbRenderer(QObject): math.floor(12 * font_pixel_ratio), ) + @staticmethod + def _get_mask(size: tuple[int, int], pixel_ratio: float) -> Image.Image: + """ + Returns a thumbnail mask given a size and pixel ratio. + If one is not already cached, then a new one will be rendered. + """ + item: Image.Image = ThumbRenderer.thumb_masks.get((*size, pixel_ratio)) + if not item: + item = ThumbRenderer._render_mask(size, pixel_ratio) + ThumbRenderer.thumb_masks[(*size, pixel_ratio)] = item + return item + + @staticmethod + def _get_border(size: tuple[int, int], pixel_ratio: float) -> Image.Image: + """ + Returns a thumbnail border given a size and pixel ratio. + If one is not already cached, then a new one will be rendered. + """ + item: Image.Image = ThumbRenderer.thumb_borders.get((*size, pixel_ratio)) + if not item: + item = ThumbRenderer._render_border(size, pixel_ratio) + ThumbRenderer.thumb_borders[(*size, pixel_ratio)] = item + return item + + @staticmethod + def _render_mask(size: tuple[int, int], pixel_ratio) -> Image.Image: + """Renders a thumbnail mask.""" + smooth_factor: int = math.ceil(2 * pixel_ratio) + radius_factor: int = 8 + im: Image.Image = Image.new( + mode="L", + size=tuple([d * smooth_factor for d in size]), # type: ignore + color="black", + ) + draw = ImageDraw.Draw(im) + draw.rounded_rectangle( + (0, 0) + tuple([d - 1 for d in im.size]), + radius=math.ceil(radius_factor * smooth_factor * pixel_ratio), + fill="white", + ) + im = im.resize( + size, + resample=Image.Resampling.BILINEAR, + ) + return im + + @staticmethod + def _render_border(size: tuple[int, int], pixel_ratio) -> Image.Image: + """Renders a thumbnail border.""" + smooth_factor: int = math.ceil(2 * pixel_ratio) + radius_factor: int = 8 + im: Image.Image = Image.new( + mode="RGBA", + size=tuple([d * smooth_factor for d in size]), # type: ignore + color="#00000000", + ) + draw = ImageDraw.Draw(im) + draw.rounded_rectangle( + (0, 0) + tuple([d - 1 for d in im.size]), + radius=math.ceil(radius_factor * smooth_factor * pixel_ratio), + fill=None, + outline="white", + width=math.floor(pixel_ratio * 2), + ) + im = im.resize( + size, + resample=Image.Resampling.BILINEAR, + ) + return im + def render( self, timestamp: float, @@ -324,11 +389,11 @@ class ThumbRenderer(QObject): ) image = image.resize((new_x, new_y), resample=resampling_method) if gradient: - mask: Image.Image = ThumbRenderer.thumb_mask_512.resize( - (adj_size, adj_size), resample=Image.Resampling.BILINEAR - ).getchannel(3) - hl: Image.Image = ThumbRenderer.thumb_mask_hl_512.resize( - (adj_size, adj_size), resample=Image.Resampling.BILINEAR + mask: Image.Image = ThumbRenderer._get_mask( + (adj_size, adj_size), pixel_ratio + ) + hl: Image.Image = ThumbRenderer._get_border( + (adj_size, adj_size), pixel_ratio ) final = four_corner_gradient_background(image, adj_size, mask, hl) else: @@ -340,7 +405,7 @@ class ThumbRenderer(QObject): ) draw = ImageDraw.Draw(rec) draw.rounded_rectangle( - (0, 0) + rec.size, + (0, 0) + tuple([d - 1 for d in rec.size]), (base_size[0] // 32) * scalar * pixel_ratio, fill="red", )