From 086fc1e522b2889f242b6bcef7a13c3f91a120b0 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Fri, 19 Jul 2024 23:43:50 -0700 Subject: [PATCH] feat(ui): add resizable thumbnail options --- .../resources/qt/images/thumb_border_512.png | Bin 5649 -> 0 bytes .../resources/qt/images/thumb_mask_128.png | Bin 2245 -> 0 bytes .../resources/qt/images/thumb_mask_512.png | Bin 4902 -> 0 bytes .../resources/qt/images/thumb_mask_hl_512.png | Bin 5392 -> 0 bytes tagstudio/src/qt/main_window.py | 20 ++-- tagstudio/src/qt/ts_qt.py | 50 ++++++++- tagstudio/src/qt/widgets/thumb_renderer.py | 95 +++++++++++++++--- 7 files changed, 136 insertions(+), 29 deletions(-) delete mode 100644 tagstudio/resources/qt/images/thumb_border_512.png delete mode 100644 tagstudio/resources/qt/images/thumb_mask_128.png delete mode 100644 tagstudio/resources/qt/images/thumb_mask_512.png delete mode 100644 tagstudio/resources/qt/images/thumb_mask_hl_512.png 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 605717e3de726590cfadfeac82e232272c9c4638..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5649 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 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 52a0a1353c955e7a7215bb1378fb3f9ad98e3a09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2245 zcmeHJ`%@EF7+pbL0mdrUA_KBr>fnoP5+;GHNkCqbYA7HKi4Vpl*(3`|c1<=Az)?h{ zLw$@$r7aIpr&>l3wU$;bQzBGl%2>pf+A30U5NBE~l{#WUv3C;+QndXC`onJa-gCaQ z-|K#x(&VI-zMKFK1VO&?#JE)OTf|`g!#f~o<{2_g$tV@8F*Rl6;uxk-;xp5SY|Rkqn`Nr=VjAJ(*ZwCe{=rrKt;Y)M5=1CF4kJ z7!WWL3=Z3j29pJ|NfC!G2An)Vlm1SpaMY{V1!xnO$=@eGg(4d38zLJVNsh&njt9@ z%xdCUl$DVp2*BZSal%GUs+%n1;e$k=EQ0d6JoH66Luy|^XOU@iT2C>QMNiQa0Ze(t z1U$_Ur0?Y>Hk^JLS)urMy3sf#3=0#V3lcG&)G0pw{Q^r`K24yhgoU!2)kJ(QVPZlY znPBsRC6hM7po}ApgvkQ3D@FK%$bX=!SD~?#fikZFZAAbxhY_1QOm4(=L>ZQ;`Eii|F3Q7aq3wKyeLh{(y zWHY5D4FI#Gu8e`@@v$PlSR@J)aGjxm!ePLc)aJ+G3}6X(g77e&IE)vO#us722rOI# zo-v-&9|s$p2Mx~P|HWs!8kPVHg#t??EevJOcWgOZeGRe6xpFQIq@%}S*imT=SF=fy zBD9&(Sk;8a5f+ehq83Wad??} z$58gr{q30qo#wm5L3rVm0z2m(IQ?LZpc8WhEG8xqVFKgF4CYL7d#FDI&DchQs!Xc} z1T8AFmbI*kNtt^&io~OK7w-rNcljni7;eso!#yRbdG>)y%e2M-UGaoc!+oApG6^ zdoS(m+_9l@ee#oUzgI8ZA{3|Jp~{=+tFtO{aozFv)aeZ$H#MAUuI@Ex(-;4Id2^|> ztjb${fA(NPXd)Dxbj<&9r|0(F-$;t9-gMW*@7df*jV)WczrMZU)4i7ji?8)>Dm}lX z+iy~rNQ_>`B>}Fyp~4^zuqhT zB&lfF9$@nhjQV4B=H4*c%}~^7zf!xPy2!H8qbK6|-TOsno~fb)Gd_53FTR2gWNjS? zJRRvFdu>;Seb()pUk_Dn^=&9=d_Q*7tNEacyE*8qPEKTv$~3xZ?K}3ok$PQIjncHU zta2r~?=&a65$gZ{_F@jS8KiNjkV9LKHVIUfd2Tj=A5f+==SfmX?AmHq}vap q;w=LoH3zP%*fr+<#HB~_bm$HHd^ewzL*icc--kRtDXuYQWAR@VJTlw> 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 ce641abc48961a476b30cd87ea346c215ad834db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4902 zcmeHLX;f3!7Cr$P1T0IjMHC^%K%7WI5RjMnKF}WmxBsa|f2~dnu<@qXD zZxkp)X|)cupcF+#sen`?l|gN(78R#d>Qfo5G$r7>2@dtyzW3+-WUXZ7ob!Eq-#wk3 zz3%pysIW9N6~FNCojlORwiq;v`;Jw2VA?n6doe2RBKKmdhGqtIwx7{g1hmMAzXFNu7%NyV6s zU|7zT38e}lBEg$2^LpLFB z(WL@Jfyf1j^pyi|JmMAnmLL|szZOw(q<=+bv;R9?EPlfcxgsPD3&e|{zG2hL2gu{p zQkW78%MqoF3x}k^62)wDBusIk#|TxhC?QxVh9z<=x=ezvH|;;r#J@v>5D_9*Vl|+d7GiHKWDB(56B>2;?!ZIb@ z3?=?0^ePv0vwduYhneSNmU<0AK|wJxBv~lJFnMg)GCUL#5RWq{+-4z`mAA41z?@ymUG)*mR!r5ggdEn6Egwyv zPCN`X*_|t}-oN0GweQ80E8RzzJn8>z@|Eew?W|Tr)NlL4K4nsI&gXNs2r6q2?bj>U z+b!4EmSzSHW%dT#%p@nAbMg54S|+~aQfi~7J-|It+PPkEE|j$1bwx-#f0+{}rL&u< zOs-Rw)TERTUwi-F#C4lbhu-xL=@>YtJ@-SZxP<5Z`Zuu|bvweZmOvNdO9_d|uFn~%&pC1@->F>g-F1z6;qa%{nb<;w;w&gUJI22XM3 zdq$E6Gvpgk!-s)VXL&EPJMc{R;_lM!()}%6GE@#lc_zAK&bvM?pY#G0kMQsI(iuHL!XKj}F`(bO% z>LG2`ZvD4Jgk)%6m`2ZoGIr~0h%9KpIQ5wg5cP(l?j%D9nz^%jU)UtgAe3FK|8aJY z4Y*#P3Ux&er38c-HRLKUnHtN(6000~_T!iR?k-wOhoa#M+Ec z@(~4%Fz=9N+Ct07>K{9sy-u5th20pzb|P%GK{6 zn#F%KW6e(B${r9ht~!Hv_RWCSU#z0v`C>LezSBE`+>Y%I@(IjK^cV9L8^hxo?a&gYjlC-VFW^ z%|Ni5C%%#cz=IU=ZvC^pVK~szQMm$L^Rb&1pvGgbE$k*@Lp>3I&o5)gkH(I#6`~7B z214^LD@SWk^zCoNc1f2_lCm)e2OivC?$=}a3DKP_BZ7vTSw+N%8Jk?A2Mh)EI89S` zHoAwH?za=%j2h^_cfk^yeZrs~q9e=U*IVazSb-v~jeO*%pwTdGRu3XtYRuz` z9X?uYxU&fw-J}1-%E1m4Rl0i`h99`!cOJ<@jkBA-Qs>)(qOVe+C{pc7!v(=(`ysS0 zpnWn9_zdk6eQMN}6^^WfWN8nYa3ERi+g#i4pKhmdaw)KKEBWOgK=bKoY~CXJ>sZrR zR#gu}QEcq&b+EAlEo~WSe08h7oKa_&bh9p=?&TF?cS$%CAd$B+MLDEbYKvh11&KAxBJ-z%K$nE)XoM<%)~W+iyv(ONeg zU?rgGR)IV`>HZrRd z*;m^=)N`A+I^>dGPVVh}NoO5VBe}Uu_`HEJ8nL0RkJqS$R-aINwR0Rm->Jk1S!&6( i-yEEWDq9D1&mHN|OK7-%wu6+%TvGuSW= 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 ON%Hdb%w4^E|Gxp|9T9&3 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", )