-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathviewer.py
More file actions
293 lines (269 loc) · 11.3 KB
/
viewer.py
File metadata and controls
293 lines (269 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
from PySide6.QtCore import QMimeData, Qt
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import (
QApplication,
QFrame,
QGridLayout,
QLabel,
QWidget,
)
class PDFViewer(QWidget):
"""
PDF 页面缩略图预览与交互组件。
支持页面多选、拖拽排序、右键菜单等功能。
"""
def __init__(self, workspace):
"""
:param workspace: Workspace 实例,管理所有 PDF 文件与页面
"""
super().__init__()
self.workspace = workspace
self.grid_layout = QGridLayout()
self.grid_layout.setSpacing(10)
self.setLayout(self.grid_layout)
self.selected_pages = []
self.drag_label = None
self._all_labels = []
def clear_thumbnails(self):
"""清空所有页面缩略图控件。"""
while self.grid_layout.count():
widget = self.grid_layout.takeAt(0).widget()
if widget:
widget.deleteLater()
def update_view(self):
"""根据 workspace 的页面数据刷新缩略图显示。"""
self.clear_thumbnails()
self.selected_pages.clear()
self._all_labels.clear()
row, col = 0, 0
for pdf_file in self.workspace.pdf_files:
for page in pdf_file.pages:
label = DraggableLabel(page, self)
label.setPixmap(page.thumbnail)
label.setAlignment(Qt.AlignCenter)
label.setFrameShape(QFrame.Box)
label.setStyleSheet("background-color: white;")
self.grid_layout.addWidget(label, row, col)
self._all_labels.append(label)
col += 1
if col >= 4:
col = 0
row += 1
def select_page(self, label, multi_select=False, range_select=False):
"""
处理页面的选中逻辑。
:param label: DraggableLabel 被选中的页面标签
:param multi_select: 是否为Ctrl多选
:param range_select: 是否为Shift连续选中
"""
if not multi_select and not range_select:
# 单选,清空其他选中页面
for selected_label in self.selected_pages:
selected_label.setStyleSheet("background-color: white;")
self.selected_pages.clear()
if range_select and self.selected_pages:
# Shift连续选中
all_labels = self._all_labels
start_index = all_labels.index(self.selected_pages[-1])
end_index = all_labels.index(label)
for i in range(
min(start_index, end_index), max(start_index, end_index) + 1
):
if all_labels[i] not in self.selected_pages:
self.selected_pages.append(all_labels[i])
all_labels[i].setStyleSheet("background-color: lightblue;")
else:
# 单选或Ctrl多选
if label not in self.selected_pages:
self.selected_pages.append(label)
label.setStyleSheet("background-color: lightblue;")
elif multi_select:
self.selected_pages.remove(label)
label.setStyleSheet("background-color: white;")
def get_label_index(self, label):
"""获取 label 在所有页面中的索引。"""
return self._all_labels.index(label)
def swap_labels(self, from_idx, to_idx, before=True):
"""
将 from_idx 的 label 移动到 to_idx 前面(before=True)或后面(before=False)。
"""
if from_idx == to_idx or from_idx < 0 or to_idx < 0:
return
label = self._all_labels.pop(from_idx)
insert_pos = to_idx if before else to_idx + 1
if from_idx < insert_pos:
insert_pos -= 1
self._all_labels.insert(insert_pos, label)
self._relayout()
def swap_labels_multi(self, from_indices, to_idx, before=True):
"""
将 from_indices 对应的 labels(按原顺序)整体移动到 to_idx 前面(before=True)或后面(before=False)。
"""
moving_labels = [self._all_labels[i] for i in from_indices]
for idx in sorted(from_indices, reverse=True):
self._all_labels.pop(idx)
insert_pos = to_idx if before else to_idx + 1
for idx in from_indices:
if idx < insert_pos:
insert_pos -= 1
for i, label in enumerate(moving_labels):
self._all_labels.insert(insert_pos + i, label)
self._relayout()
self.clear_highlight()
def _relayout(self):
"""根据 _all_labels 顺序重新布局页面缩略图。"""
for i, lbl in enumerate(self._all_labels):
row, col = divmod(i, 4)
self.grid_layout.addWidget(lbl, row, col)
def highlight_label_at(self, index):
"""高亮显示插入目标页面的背景。"""
for lbl in self._all_labels:
if lbl not in self.selected_pages:
lbl.setStyleSheet("background-color: white;")
else:
lbl.setStyleSheet("background-color: lightblue;")
if 0 <= index < len(self._all_labels):
self._all_labels[index].setStyleSheet("background-color: #ffe082;")
def clear_highlight(self):
"""清除所有页面的插入高亮,仅保留选中高亮。"""
for lbl in self._all_labels:
if lbl in self.selected_pages:
lbl.setStyleSheet("background-color: lightblue;")
else:
lbl.setStyleSheet("background-color: white;")
def reorder_pages(self, insert_index):
"""
处理页面的重新排序。
:param insert_index: 插入位置的索引
"""
if not self.selected_pages or insert_index < 0:
return
selected_labels = self.selected_pages[:]
for label in selected_labels:
self._all_labels.remove(label)
for label in reversed(selected_labels):
self._all_labels.insert(insert_index, label)
self._relayout()
mw = self.window()
if hasattr(mw, "notify_pages_reordered"):
mw.notify_pages_reordered()
class DraggableLabel(QLabel):
"""
可拖拽的页面缩略图控件。
"""
def __init__(self, page, viewer):
"""
:param page: PDFPage 实例
:param viewer: PDFViewer 实例
"""
super().__init__()
self.page = page
self.viewer = viewer
self.setAcceptDrops(True)
self._mouse_pressed = False
self._drag_start_pos = None
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self._mouse_pressed = True
self._drag_start_pos = event.pos()
modifiers = QApplication.keyboardModifiers()
multi_select = modifiers == Qt.ControlModifier
range_select = modifiers == Qt.ShiftModifier
# 修改逻辑:如果是多选/连续选中且已经选中,则不改变选中状态
if (multi_select or range_select) and self in self.viewer.selected_pages:
pass # 保持原有选中状态,不做任何操作
else:
if multi_select:
if self in self.viewer.selected_pages:
self.viewer.selected_pages.remove(self)
self.setStyleSheet("background-color: white;")
else:
self.viewer.selected_pages.append(self)
self.setStyleSheet("background-color: lightblue;")
else:
self.viewer.select_page(self, multi_select, range_select)
def mouseMoveEvent(self, event):
if not self._mouse_pressed:
return
if (
event.buttons() == Qt.LeftButton
and (event.pos() - self._drag_start_pos).manhattanLength()
> QApplication.startDragDistance()
):
drag = QDrag(self)
mime_data = QMimeData()
drag.setMimeData(mime_data)
drag.setPixmap(self.pixmap())
self.viewer.drag_label = self
# 记录拖拽时的多选状态
modifiers = QApplication.keyboardModifiers()
multi_select = modifiers == Qt.ControlModifier
range_select = modifiers == Qt.ShiftModifier
# 计算拖拽涉及的所有页面
if (multi_select or range_select) and self in self.viewer.selected_pages:
self.viewer._drag_indices = [
self.viewer.get_label_index(lbl)
for lbl in self.viewer.selected_pages
]
else:
self.viewer._drag_indices = [self.viewer.get_label_index(self)]
drag.exec(Qt.MoveAction)
self.viewer._drag_indices = None
def mouseReleaseEvent(self, event):
self._mouse_pressed = False
def dragEnterEvent(self, event):
if self.viewer.drag_label and self.viewer.drag_label != self:
event.acceptProposedAction()
else:
event.ignore()
def dragMoveEvent(self, event):
if self.viewer.drag_label and self.viewer.drag_label != self:
event.acceptProposedAction()
idx = self.viewer.get_label_index(self)
rect = self.rect()
mouse_x = (
event.position().x() if hasattr(event, "position") else event.pos().x()
)
# 仅第一页支持左右判断插入位置,其余页面默认插入到该页面后面
if idx == 0:
before = mouse_x < rect.width() / 2
highlight_idx = idx if before else idx
else:
before = False
highlight_idx = idx
self.viewer.highlight_label_at(highlight_idx)
else:
event.ignore()
self.viewer.clear_highlight()
def dropEvent(self, event):
if self.viewer.drag_label and self.viewer.drag_label != self:
rect = self.rect()
mouse_x = (
event.position().x() if hasattr(event, "position") else event.pos().x()
)
to_idx = self.viewer.get_label_index(self)
# 仅第一页支持左右判断插入位置,其余页面默认插入到该页面后面
if to_idx == 0:
before = mouse_x < rect.width() / 2
else:
before = False
# 多选拖拽
drag_indices = getattr(self.viewer, "_drag_indices", None)
if drag_indices is not None and len(drag_indices) > 1:
# 保证顺序
drag_indices = sorted(drag_indices)
self.viewer.swap_labels_multi(drag_indices, to_idx, before=before)
else:
from_idx = self.viewer.get_label_index(self.viewer.drag_label)
self.viewer.swap_labels(from_idx, to_idx, before=before)
self.viewer.drag_label = None
self.viewer.clear_highlight()
event.acceptProposedAction()
else:
event.ignore()
self.viewer.clear_highlight()
def contextMenuEvent(self, event):
"""
右键菜单由 MainWindow 统一处理,这里只触发父控件的 customContextMenuRequested 信号。
"""
self.viewer.customContextMenuRequested.emit(self.mapToParent(event.pos()))