Skip to content

Commit f59b84b

Browse files
committed
refactor: use entry IDs instead of objects and indices
- fixes preview panel not updating after entry edits - fixes slow selection performance - fixes double render call
1 parent 8838a93 commit f59b84b

File tree

7 files changed

+188
-200
lines changed

7 files changed

+188
-200
lines changed

tagstudio/src/core/library/alchemy/library.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def get_entry(self, entry_id: int) -> Entry | None:
420420
return entry
421421

422422
def get_entry_full(self, entry_id: int) -> Entry | None:
423-
"""Load entry an join with all joins and all tags."""
423+
"""Load entry and join with all joins and all tags."""
424424
with Session(self.engine) as session:
425425
statement = select(Entry).where(Entry.id == entry_id)
426426
statement = (
@@ -454,7 +454,9 @@ def get_entries(self, with_joins: bool = False) -> Iterator[Entry]:
454454
if with_joins:
455455
# load Entry with all joins and all tags
456456
stmt = (
457-
stmt.outerjoin(Entry.text_fields).outerjoin(Entry.datetime_fields)
457+
stmt.outerjoin(Entry.text_fields)
458+
.outerjoin(Entry.datetime_fields)
459+
.outerjoin(Entry.tags)
458460
# .outerjoin(Entry.tag_box_fields)
459461
)
460462
stmt = stmt.options(
@@ -929,6 +931,7 @@ def add_tag(
929931
return None
930932

931933
def add_tags_to_entry(self, entry_id: int, tag_ids: int | list[int] | set[int]) -> bool:
934+
"""Add one or more tags to an entry."""
932935
tag_ids_ = [tag_ids] if isinstance(tag_ids, int) else tag_ids
933936
with Session(self.engine, expire_on_commit=False) as session:
934937
try:
@@ -937,6 +940,30 @@ def add_tags_to_entry(self, entry_id: int, tag_ids: int | list[int] | set[int])
937940
session.flush()
938941
session.commit()
939942
return True
943+
except IntegrityError as e:
944+
logger.warning("[add_tags_to_entry]", warning=e)
945+
session.rollback()
946+
return False
947+
948+
def remove_tags_from_entry(self, entry_id: int, tag_ids: int | list[int] | set[int]) -> bool:
949+
"""Remove one or more tags from an entry."""
950+
tag_ids_ = [tag_ids] if isinstance(tag_ids, int) else tag_ids
951+
with Session(self.engine, expire_on_commit=False) as session:
952+
try:
953+
for tag_id in tag_ids_:
954+
tag_entry = session.scalars(
955+
select(TagEntry).where(
956+
and_(
957+
TagEntry.tag_id == tag_id,
958+
TagEntry.entry_id == entry_id,
959+
)
960+
)
961+
).first()
962+
if tag_entry:
963+
session.delete(tag_entry)
964+
session.commit()
965+
session.commit()
966+
return True
940967
except IntegrityError as e:
941968
logger.exception(e)
942969
session.rollback()

tagstudio/src/core/library/alchemy/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def fields(self) -> list[BaseField]:
146146
return fields
147147

148148
@property
149-
def is_favorited(self) -> bool:
149+
def is_favorite(self) -> bool:
150150
return any(tag.id == TAG_FAVORITE for tag in self.tags)
151151

152152
@property

tagstudio/src/qt/ts_qt.py

Lines changed: 98 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import time
1717
import webbrowser
1818
from collections.abc import Sequence
19-
from itertools import zip_longest
2019
from pathlib import Path
2120
from queue import Queue
2221

@@ -53,8 +52,6 @@
5352
QWidget,
5453
)
5554
from src.core.constants import (
56-
TAG_ARCHIVED,
57-
TAG_FAVORITE,
5855
VERSION,
5956
VERSION_BRANCH,
6057
)
@@ -136,7 +133,7 @@ def __init__(self, backend, args):
136133
self.rm: ResourceManager = ResourceManager()
137134
self.args = args
138135
self.filter = FilterState.show_all()
139-
self.frame_content: list[Entry] = []
136+
self.frame_content: list[int] = [] # List of Entry IDs on the current page
140137
self.pages_count = 0
141138

142139
self.scrollbar_pos = 0
@@ -150,9 +147,7 @@ def __init__(self, backend, args):
150147
self.thumb_job_queue: Queue = Queue()
151148
self.thumb_threads: list[Consumer] = []
152149
self.thumb_cutoff: float = time.time()
153-
154-
# grid indexes of selected items
155-
self.selected: list[int] = []
150+
self.selected: list[int] = [] # Selected Entry IDs
156151

157152
self.SIGTERM.connect(self.handle_sigterm)
158153

@@ -546,11 +541,13 @@ def init_library_window(self):
546541
self.preview_panel.update_widgets()
547542

548543
def toggle_libs_list(self, value: bool):
549-
if value:
550-
self.preview_panel.libs_flow_container.show()
551-
else:
552-
self.preview_panel.libs_flow_container.hide()
553-
self.preview_panel.update()
544+
# TODO: Reimplement or remove
545+
# if value:
546+
# self.preview_panel.libs_flow_container.show()
547+
# else:
548+
# self.preview_panel.libs_flow_container.hide()
549+
# self.preview_panel.update()
550+
pass
554551

555552
def show_grid_filenames(self, value: bool):
556553
for thumb in self.item_thumbs:
@@ -643,10 +640,12 @@ def add_tag_action_callback(self):
643640
self.modal.show()
644641

645642
def select_all_action_callback(self):
646-
self.selected = list(range(0, len(self.frame_content)))
647-
648-
for grid_idx in self.selected:
649-
self.item_thumbs[grid_idx].thumb_button.set_selected(True)
643+
"""Set the selection to all visible items."""
644+
self.selected.clear()
645+
for item in self.item_thumbs:
646+
if item.mode and item.item_id not in self.selected:
647+
self.selected.append(item.item_id)
648+
item.thumb_button.set_selected(True)
650649

651650
self.set_macro_menu_viability()
652651
self.preview_panel.update_widgets()
@@ -766,14 +765,14 @@ def new_file_macros_runnable(self, new_ids):
766765
# sleep(5)
767766
# pb.deleteLater()
768767

769-
def run_macros(self, name: MacroID, grid_idx: list[int]):
768+
def run_macros(self, name: MacroID, entry_ids: list[int]):
770769
"""Run a specific Macro on a group of given entry_ids."""
771-
for gid in grid_idx:
772-
self.run_macro(name, gid)
770+
for entry_id in entry_ids:
771+
self.run_macro(name, entry_id)
773772

774-
def run_macro(self, name: MacroID, grid_idx: int):
773+
def run_macro(self, name: MacroID, entry_id: int):
775774
"""Run a specific Macro on an Entry given a Macro name."""
776-
entry: Entry = self.frame_content[grid_idx]
775+
entry: Entry = self.lib.get_entry(entry_id)
777776
full_path = self.lib.library_dir / entry.path
778777
source = "" if entry.path.parent == Path(".") else entry.path.parts[0].lower()
779778

@@ -782,14 +781,14 @@ def run_macro(self, name: MacroID, grid_idx: int):
782781
source=source,
783782
macro=name,
784783
entry_id=entry.id,
785-
grid_idx=grid_idx,
784+
grid_idx=entry_id,
786785
)
787786

788787
if name == MacroID.AUTOFILL:
789788
for macro_id in MacroID:
790789
if macro_id == MacroID.AUTOFILL:
791790
continue
792-
self.run_macro(macro_id, grid_idx)
791+
self.run_macro(macro_id, entry_id)
793792

794793
elif name == MacroID.SIDECAR:
795794
parsed_items = TagStudioCore.get_gdl_sidecar(full_path, source)
@@ -886,13 +885,12 @@ def _init_thumb_grid(self):
886885
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
887886

888887
# TODO - init after library is loaded, it can have different page_size
889-
for grid_idx in range(self.filter.page_size):
888+
for _ in range(self.filter.page_size):
890889
item_thumb = ItemThumb(
891890
None,
892891
self.lib,
893892
self,
894893
(self.thumb_size, self.thumb_size),
895-
grid_idx,
896894
bool(
897895
self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool)
898896
),
@@ -909,44 +907,55 @@ def _init_thumb_grid(self):
909907
sa.setWidgetResizable(True)
910908
sa.setWidget(self.flow_container)
911909

912-
def select_item(self, grid_index: int, append: bool, bridge: bool):
910+
def select_item(self, item_id: int, append: bool, bridge: bool):
913911
"""Select one or more items in the Thumbnail Grid."""
914-
logger.info("selecting item", grid_index=grid_index, append=append, bridge=bridge)
912+
logger.info("[QtDriver] Selecting Items:", item_id=item_id, append=append, bridge=bridge)
915913
if append:
916-
if grid_index not in self.selected:
917-
self.selected.append(grid_index)
918-
self.item_thumbs[grid_index].thumb_button.set_selected(True)
914+
if item_id not in self.selected:
915+
self.selected.append(item_id)
916+
for it in self.item_thumbs:
917+
if it.item_id == item_id:
918+
it.thumb_button.set_selected(True)
919919
else:
920-
self.selected.remove(grid_index)
921-
self.item_thumbs[grid_index].thumb_button.set_selected(False)
920+
self.selected.remove(item_id)
921+
for it in self.item_thumbs:
922+
if it.item_id == item_id:
923+
it.thumb_button.set_selected(False)
922924

923925
elif bridge and self.selected:
924-
select_from = min(self.selected)
925-
select_to = max(self.selected)
926-
927-
if select_to < grid_index:
928-
index_range = range(select_from, grid_index + 1)
929-
else:
930-
index_range = range(grid_index, select_to + 1)
931-
932-
self.selected = list(index_range)
926+
contents = self.frame_content
927+
last_index = self.frame_content.index(self.selected[-1])
928+
current_index = self.frame_content.index(item_id)
929+
index_range: list = contents[
930+
min(last_index, current_index) : max(last_index, current_index) + 1
931+
]
932+
# Preserve bridge direction for correct appending order.
933+
if last_index < current_index:
934+
index_range.reverse()
935+
for entry_id in index_range:
936+
for it in self.item_thumbs:
937+
if it.item_id == entry_id:
938+
it.thumb_button.set_selected(True)
939+
if entry_id not in self.selected:
940+
self.selected.append(entry_id)
933941

934-
for selected_idx in self.selected:
935-
self.item_thumbs[selected_idx].thumb_button.set_selected(True)
936942
else:
937-
self.selected = [grid_index]
938-
for thumb_idx, item_thumb in enumerate(self.item_thumbs):
939-
item_matched = thumb_idx == grid_index
940-
item_thumb.thumb_button.set_selected(item_matched)
941-
942-
# NOTE: By using the preview panel's "set_tags_updated_slot" method,
943-
# only the last of multiple identical item selections are connected.
944-
# If attaching the slot to multiple duplicate selections is needed,
945-
# just bypass the method and manually disconnect and connect the slots.
946-
if len(self.selected) == 1:
943+
self.selected.clear()
944+
self.selected.append(item_id)
947945
for it in self.item_thumbs:
948-
if it.item_id == id:
949-
self.preview_panel.set_tags_updated_slot(it.refresh_badge)
946+
if it.item_id == item_id:
947+
it.thumb_button.set_selected(True)
948+
else:
949+
it.thumb_button.set_selected(False)
950+
951+
# # NOTE: By using the preview panel's "set_tags_updated_slot" method,
952+
# # only the last of multiple identical item selections are connected.
953+
# # If attaching the slot to multiple duplicate selections is needed,
954+
# # just bypass the method and manually disconnect and connect the slots.
955+
# if len(self.selected) == 1:
956+
# for it in self.item_thumbs:
957+
# if it.item_id == item_id:
958+
# self.preview_panel.set_tags_updated_slot(it.refresh_badge)
950959

951960
self.set_macro_menu_viability()
952961
self.preview_panel.update_widgets()
@@ -1043,18 +1052,26 @@ def update_thumbs(self):
10431052
self.main_window.update()
10441053

10451054
is_grid_thumb = True
1046-
# Show loading placeholder icons
1047-
for entry, item_thumb in zip_longest(self.frame_content, self.item_thumbs):
1048-
if not entry:
1055+
logger.info("[QtDriver] Loading Entries...")
1056+
# TODO: Grab all entries at once
1057+
entries: list[Entry] = [self.lib.get_entry_full(e_id) for e_id in self.frame_content]
1058+
logger.info("[QtDriver] Building Filenames...")
1059+
filenames: list[Path] = [self.lib.library_dir / e.path for e in entries]
1060+
logger.info("[QtDriver] Done! Processing ItemThumbs...")
1061+
for index, item_thumb in enumerate(self.item_thumbs, start=0):
1062+
entry = None
1063+
try:
1064+
entry = entries[index]
1065+
except IndexError:
10491066
item_thumb.hide()
10501067
continue
1051-
1068+
if not entry:
1069+
continue
10521070
item_thumb.set_mode(ItemType.ENTRY)
1053-
item_thumb.set_item_id(entry)
1071+
item_thumb.set_item_id(entry.id)
10541072

10551073
# TODO - show after item is rendered
10561074
item_thumb.show()
1057-
10581075
is_loading = True
10591076
self.thumb_job_queue.put(
10601077
(
@@ -1064,29 +1081,29 @@ def update_thumbs(self):
10641081
)
10651082

10661083
# Show rendered thumbnails
1067-
for idx, (entry, item_thumb) in enumerate(
1068-
zip_longest(self.frame_content, self.item_thumbs)
1069-
):
1084+
for index, item_thumb in enumerate(self.item_thumbs, start=0):
1085+
entry = None
1086+
try:
1087+
entry = entries[index]
1088+
except IndexError:
1089+
item_thumb.hide()
1090+
continue
10701091
if not entry:
10711092
continue
10721093

1073-
filepath = self.lib.library_dir / entry.path
10741094
is_loading = False
1075-
10761095
self.thumb_job_queue.put(
10771096
(
10781097
item_thumb.renderer.render,
1079-
(time.time(), filepath, base_size, ratio, is_loading, is_grid_thumb),
1098+
(time.time(), filenames[index], base_size, ratio, is_loading, is_grid_thumb),
10801099
)
10811100
)
1082-
1083-
entry_tag_ids = {tag.id for tag in entry.tags}
1084-
item_thumb.assign_badge(BadgeType.ARCHIVED, TAG_ARCHIVED in entry_tag_ids)
1085-
item_thumb.assign_badge(BadgeType.FAVORITE, TAG_FAVORITE in entry_tag_ids)
1101+
item_thumb.assign_badge(BadgeType.ARCHIVED, entry.is_archived)
1102+
item_thumb.assign_badge(BadgeType.FAVORITE, entry.is_favorite)
10861103
item_thumb.update_clickable(
10871104
clickable=(
1088-
lambda checked=False, index=idx: self.select_item(
1089-
index,
1105+
lambda checked=False, item_id=entry.id: self.select_item(
1106+
item_id,
10901107
append=(
10911108
QGuiApplication.keyboardModifiers()
10921109
== Qt.KeyboardModifier.ControlModifier
@@ -1102,24 +1119,15 @@ def update_thumbs(self):
11021119
is_selected = (item_thumb.mode, item_thumb.item_id) in self.selected
11031120
item_thumb.thumb_button.set_selected(is_selected)
11041121

1105-
self.thumb_job_queue.put(
1106-
(
1107-
item_thumb.renderer.render,
1108-
(time.time(), filepath, base_size, ratio, False, True),
1109-
)
1110-
)
1111-
1112-
def update_badges(self, grid_item_ids: Sequence[int] = None):
1113-
if not grid_item_ids:
1122+
def update_badges(self, item_ids: Sequence[int] = None):
1123+
if not item_ids:
11141124
# no items passed, update all items in grid
1115-
grid_item_ids = range(min(len(self.item_thumbs), len(self.frame_content)))
1116-
1117-
logger.info("updating badges for items", grid_item_ids=grid_item_ids)
1125+
item_ids = range(min(len(self.item_thumbs), len(self.frame_content)))
11181126

1119-
for grid_idx in grid_item_ids:
1120-
# get the entry from grid to avoid loading from db again
1121-
entry = self.frame_content[grid_idx]
1122-
self.item_thumbs[grid_idx].refresh_badge(entry)
1127+
item_ids_ = set(item_ids)
1128+
for it in self.item_thumbs:
1129+
if it.item_id in item_ids_:
1130+
it.refresh_badge()
11231131

11241132
def filter_items(self, filter: FilterState | None = None) -> None:
11251133
if not self.lib.library_dir:
@@ -1150,7 +1158,7 @@ def filter_items(self, filter: FilterState | None = None) -> None:
11501158
)
11511159

11521160
# update page content
1153-
self.frame_content = results.items
1161+
self.frame_content = [item.id for item in results.items]
11541162
self.update_thumbs()
11551163

11561164
# update pagination

0 commit comments

Comments
 (0)