Skip to content

Commit d54d46e

Browse files
authored
fix: persist entry selection across pages and save scroll positions (#1248)
* fix: persist entry selection across pages and save scroll positions * fix: add badges to all selected entries not just visible ones
1 parent 4c484bc commit d54d46e

File tree

5 files changed

+143
-131
lines changed

5 files changed

+143
-131
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import enum
22
import random
3-
from dataclasses import dataclass, replace
3+
from dataclasses import dataclass, field, replace
44
from pathlib import Path
55

66
import structlog
@@ -78,8 +78,9 @@ class BrowsingState:
7878
"""Represent a state of the Library grid view."""
7979

8080
page_index: int = 0
81+
page_positions: dict[int, int] = field(default_factory=dict)
8182
sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED
82-
ascending: bool = True
83+
ascending: bool = False
8384
random_seed: float = 0
8485

8586
show_hidden_entries: bool = False

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import structlog
66

7-
from tagstudio.core.library.alchemy.enums import BrowsingState
87
from tagstudio.core.library.alchemy.library import Library
98
from tagstudio.core.library.alchemy.models import Entry
109
from tagstudio.core.utils.types import unwrap
@@ -52,16 +51,12 @@ def refresh_dupe_files(self, results_filepath: str | Path):
5251
# The file is not in the library directory
5352
continue
5453

55-
results = self.library.search_library(
56-
BrowsingState.from_path(path_relative), 500
57-
)
58-
entries = self.library.get_entries(results.ids)
59-
60-
if not results:
54+
entry = self.library.get_entry_full_by_path(path_relative)
55+
if entry is None:
6156
# file not in library
6257
continue
6358

64-
files.append(entries[0])
59+
files.append(entry)
6560

6661
if not len(files) > 1:
6762
# only one file in the group, nothing to do

src/tagstudio/qt/mixed/item_thumb.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -496,13 +496,11 @@ def toggle_item_tag(
496496
toggle_value: bool,
497497
tag_id: int,
498498
):
499-
if entry_id in self.driver.selected:
500-
if len(self.driver.selected) == 1:
501-
self.driver.main_window.preview_panel.field_containers_widget.update_toggled_tag(
502-
tag_id, toggle_value
503-
)
504-
else:
505-
pass
499+
selected = self.driver._selected
500+
if len(selected) == 1 and entry_id in selected:
501+
self.driver.main_window.preview_panel.field_containers_widget.update_toggled_tag(
502+
tag_id, toggle_value
503+
)
506504

507505
@override
508506
def mouseMoveEvent(self, event: QMouseEvent) -> None: # type: ignore[misc]

src/tagstudio/qt/thumb_grid_layout.py

Lines changed: 30 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import math
22
import time
3+
from collections.abc import Iterable
34
from pathlib import Path
45
from typing import TYPE_CHECKING, Any, override
56

6-
from PySide6.QtCore import QPoint, QRect, QSize
7+
from PySide6.QtCore import QPoint, QRect, QSize, Signal
78
from PySide6.QtGui import QPixmap
89
from PySide6.QtWidgets import QLayout, QLayoutItem, QScrollArea
910

@@ -19,17 +20,16 @@
1920

2021

2122
class ThumbGridLayout(QLayout):
23+
# Id of first visible entry
24+
visible_changed = Signal(int)
25+
2226
def __init__(self, driver: "QtDriver", scroll_area: QScrollArea) -> None:
2327
super().__init__(None)
2428
self.driver: QtDriver = driver
2529
self.scroll_area: QScrollArea = scroll_area
2630

2731
self._item_thumbs: list[ItemThumb] = []
2832
self._items: list[QLayoutItem] = []
29-
# Entry.id -> _entry_ids[index]
30-
self._selected: dict[int, int] = {}
31-
# _entry_ids[index]
32-
self._last_selected: int | None = None
3333

3434
self._entry_ids: list[int] = []
3535
self._entries: dict[int, Entry] = {}
@@ -47,12 +47,14 @@ def __init__(self, driver: "QtDriver", scroll_area: QScrollArea) -> None:
4747
# _entry_ids[StartIndex:EndIndex]
4848
self._last_page_update: tuple[int, int] | None = None
4949

50+
self._scroll_to: int | None = None
51+
52+
def scroll_to(self, entry_id: int):
53+
self._scroll_to = entry_id
54+
5055
def set_entries(self, entry_ids: list[int]):
5156
self.scroll_area.verticalScrollBar().setValue(0)
5257

53-
self._selected.clear()
54-
self._last_selected = None
55-
5658
self._entry_ids = entry_ids
5759
self._entries.clear()
5860
self._tag_entries.clear()
@@ -83,90 +85,20 @@ def set_entries(self, entry_ids: list[int]):
8385

8486
self._last_page_update = None
8587

86-
def select_all(self):
87-
self._selected.clear()
88-
for index, id in enumerate(self._entry_ids):
89-
self._selected[id] = index
90-
self._last_selected = index
91-
92-
for entry_id in self._entry_items:
93-
self._set_selected(entry_id)
94-
95-
def select_inverse(self):
96-
selected = {}
97-
for index, id in enumerate(self._entry_ids):
98-
if id not in self._selected:
99-
selected[id] = index
100-
self._last_selected = index
101-
102-
for id in self._selected:
103-
if id not in selected:
104-
self._set_selected(id, value=False)
105-
for id in selected:
106-
self._set_selected(id)
107-
108-
self._selected = selected
109-
110-
def select_entry(self, entry_id: int):
111-
if entry_id in self._selected:
112-
index = self._selected.pop(entry_id)
113-
if index == self._last_selected:
114-
self._last_selected = None
115-
self._set_selected(entry_id, value=False)
116-
else:
117-
try:
118-
index = self._entry_ids.index(entry_id)
119-
except ValueError:
120-
index = -1
121-
122-
self._selected[entry_id] = index
123-
self._last_selected = index
124-
self._set_selected(entry_id)
125-
126-
def select_to_entry(self, entry_id: int):
127-
index = self._entry_ids.index(entry_id)
128-
if len(self._selected) == 0:
129-
self.select_entry(entry_id)
130-
return
131-
if self._last_selected is None:
132-
self._last_selected = min(self._selected.values(), key=lambda i: abs(index - i))
133-
134-
start = self._last_selected
135-
self._last_selected = index
88+
def update_selected(self):
89+
for item_thumb in self._item_thumbs:
90+
value = item_thumb.item_id in self.driver._selected
91+
item_thumb.thumb_button.set_selected(value)
13692

137-
if start > index:
138-
index, start = start, index
139-
else:
140-
index += 1
141-
142-
for i in range(start, index):
143-
entry_id = self._entry_ids[i]
144-
self._selected[entry_id] = i
145-
self._set_selected(entry_id)
146-
147-
def clear_selected(self):
148-
for entry_id in self._entry_items:
149-
self._set_selected(entry_id, value=False)
150-
151-
self._selected.clear()
152-
self._last_selected = None
153-
154-
def _set_selected(self, entry_id: int, value: bool = True):
155-
if entry_id not in self._entry_items:
156-
return
157-
index = self._entry_items[entry_id]
158-
if index < len(self._item_thumbs):
159-
self._item_thumbs[index].thumb_button.set_selected(value)
160-
161-
def add_tags(self, entry_ids: list[int], tag_ids: list[int]):
93+
def add_tags(self, entry_ids: Iterable[int], tag_ids: Iterable[int]):
16294
for tag_id in tag_ids:
16395
self._tag_entries.setdefault(tag_id, set()).update(entry_ids)
16496

165-
def remove_tags(self, entry_ids: list[int], tag_ids: list[int]):
97+
def remove_tags(self, entry_ids: Iterable[int], tag_ids: Iterable[int]):
16698
for tag_id in tag_ids:
16799
self._tag_entries.setdefault(tag_id, set()).difference_update(entry_ids)
168100

169-
def _fetch_entries(self, ids: list[int]):
101+
def _fetch_entries(self, ids: Iterable[int]):
170102
ids = [id for id in ids if id not in self._entries]
171103
entries = self.driver.lib.get_entries(ids)
172104
for entry in entries:
@@ -263,12 +195,24 @@ def setGeometry(self, arg__1: QRect) -> None:
263195
per_row, width_offset, height_offset = self._size(rect.right())
264196
view_height = self.parentWidget().parentWidget().height()
265197
offset = self.scroll_area.verticalScrollBar().value()
198+
if self._scroll_to is not None:
199+
try:
200+
index = self._entry_ids.index(self._scroll_to)
201+
value = (index // per_row) * height_offset
202+
self.scroll_area.verticalScrollBar().setMaximum(value)
203+
self.scroll_area.verticalScrollBar().setSliderPosition(value)
204+
offset = value
205+
except ValueError:
206+
pass
207+
self._scroll_to = None
266208

267209
visible_rows = math.ceil((view_height + (offset % height_offset)) / height_offset)
268210
offset = int(offset / height_offset)
269211
start = offset * per_row
270212
end = start + (visible_rows * per_row)
271213

214+
self.visible_changed.emit(self._entry_ids[start])
215+
272216
# Load closest off screen rows
273217
start -= per_row * 3
274218
end += per_row * 3
@@ -363,7 +307,7 @@ def setGeometry(self, arg__1: QRect) -> None:
363307
entry_id = self._entry_ids[i]
364308
item_index = self._entry_items[entry_id]
365309
item_thumb = self._item_thumbs[item_index]
366-
item_thumb.thumb_button.set_selected(entry_id in self._selected)
310+
item_thumb.thumb_button.set_selected(entry_id in self.driver._selected)
367311

368312
item_thumb.assign_badge(BadgeType.ARCHIVED, entry_id in self._tag_entries[TAG_ARCHIVED])
369313
item_thumb.assign_badge(BadgeType.FAVORITE, entry_id in self._tag_entries[TAG_FAVORITE])

0 commit comments

Comments
 (0)