NotYAFC 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- font_converter/__init__.py +4 -0
- font_converter/__main__.py +45 -0
- font_converter/assets/ico.png +0 -0
- font_converter/assets/notyafc-1024.png +0 -0
- font_converter/assets/notyafc.ico +0 -0
- font_converter/assets/notyafc.svg +15 -0
- font_converter/cli.py +543 -0
- font_converter/constants.py +27 -0
- font_converter/contours.py +336 -0
- font_converter/css_kit.py +77 -0
- font_converter/finder.py +213 -0
- font_converter/font.py +530 -0
- font_converter/gui/__init__.py +1 -0
- font_converter/gui/conversion_dialog.py +110 -0
- font_converter/gui/file_list.py +411 -0
- font_converter/gui/instance_tree.py +583 -0
- font_converter/gui/logs.py +51 -0
- font_converter/gui/organization_dialog.py +417 -0
- font_converter/gui/profile_options.py +316 -0
- font_converter/gui/profiles.py +17 -0
- font_converter/gui/scanner.py +42 -0
- font_converter/gui/styles.py +658 -0
- font_converter/gui/widgets.py +495 -0
- font_converter/gui/window.py +1768 -0
- font_converter/inspection.py +169 -0
- font_converter/merge_curves.py +499 -0
- font_converter/otf_builder.py +144 -0
- font_converter/qu2cu.py +117 -0
- font_converter/service.py +729 -0
- font_converter/ttf_builder.py +107 -0
- notyafc-1.0.0.dist-info/METADATA +256 -0
- notyafc-1.0.0.dist-info/RECORD +34 -0
- notyafc-1.0.0.dist-info/WHEEL +4 -0
- notyafc-1.0.0.dist-info/entry_points.txt +6 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Model-backed font table, filtering and scan coordination."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterator
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import (
|
|
8
|
+
QAbstractTableModel,
|
|
9
|
+
QModelIndex,
|
|
10
|
+
QObject,
|
|
11
|
+
QPersistentModelIndex,
|
|
12
|
+
QSortFilterProxyModel,
|
|
13
|
+
Qt,
|
|
14
|
+
Signal,
|
|
15
|
+
)
|
|
16
|
+
from PySide6.QtGui import QBrush, QColor, QPainter
|
|
17
|
+
from PySide6.QtWidgets import QStyle, QStyledItemDelegate, QStyleOptionViewItem
|
|
18
|
+
|
|
19
|
+
from font_converter.gui.scanner import ScanWorker
|
|
20
|
+
from font_converter.gui.styles import LIGHT
|
|
21
|
+
from font_converter.gui.widgets import FileTable
|
|
22
|
+
from font_converter.inspection import FontInfo, incompatibility
|
|
23
|
+
|
|
24
|
+
_PATH_ROLE = Qt.ItemDataRole.UserRole
|
|
25
|
+
_REASON_ROLE = Qt.ItemDataRole.UserRole + 1
|
|
26
|
+
_INFO_ROLE = Qt.ItemDataRole.UserRole + 2
|
|
27
|
+
_ROOT_INDEX = QModelIndex()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class FontRow:
|
|
32
|
+
path: Path
|
|
33
|
+
label: str
|
|
34
|
+
format: str
|
|
35
|
+
outlines: str
|
|
36
|
+
upm: str
|
|
37
|
+
info: FontInfo
|
|
38
|
+
checked: bool
|
|
39
|
+
reason: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FontTableModel(QAbstractTableModel):
|
|
43
|
+
HEADERS = ("Name", "Format", "Outlines", "UPM", "Family", "Style")
|
|
44
|
+
|
|
45
|
+
def __init__(self, parent=None) -> None:
|
|
46
|
+
super().__init__(parent)
|
|
47
|
+
self.rows: list[FontRow] = []
|
|
48
|
+
|
|
49
|
+
def rowCount(self, parent: QModelIndex | QPersistentModelIndex = _ROOT_INDEX) -> int:
|
|
50
|
+
return 0 if parent.isValid() else len(self.rows)
|
|
51
|
+
|
|
52
|
+
def columnCount(self, parent: QModelIndex | QPersistentModelIndex = _ROOT_INDEX) -> int:
|
|
53
|
+
return 0 if parent.isValid() else len(self.HEADERS)
|
|
54
|
+
|
|
55
|
+
def headerData(self, section: int, orientation: Qt.Orientation, role: int = 0):
|
|
56
|
+
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
|
|
57
|
+
return self.HEADERS[section]
|
|
58
|
+
if role == Qt.ItemDataRole.TextAlignmentRole and orientation == Qt.Orientation.Horizontal:
|
|
59
|
+
if section == 0:
|
|
60
|
+
return Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
|
61
|
+
if section == 3:
|
|
62
|
+
return Qt.AlignmentFlag.AlignCenter
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def data(self, index: QModelIndex | QPersistentModelIndex, role: int = 0):
|
|
66
|
+
if not index.isValid():
|
|
67
|
+
return None
|
|
68
|
+
row = self.rows[index.row()]
|
|
69
|
+
column = index.column()
|
|
70
|
+
if role == Qt.ItemDataRole.DisplayRole:
|
|
71
|
+
return (row.label, row.format, row.outlines, row.upm, row.info.family, row.info.style)[
|
|
72
|
+
column
|
|
73
|
+
]
|
|
74
|
+
if role == Qt.ItemDataRole.TextAlignmentRole and column == 3:
|
|
75
|
+
return Qt.AlignmentFlag.AlignCenter
|
|
76
|
+
if role == Qt.ItemDataRole.CheckStateRole and column == 0:
|
|
77
|
+
checked = row.checked and row.reason is None
|
|
78
|
+
return Qt.CheckState.Checked if checked else Qt.CheckState.Unchecked
|
|
79
|
+
if role == _PATH_ROLE:
|
|
80
|
+
return str(row.path)
|
|
81
|
+
if role == _REASON_ROLE:
|
|
82
|
+
return row.reason
|
|
83
|
+
if role == _INFO_ROLE:
|
|
84
|
+
return row.info
|
|
85
|
+
if role == Qt.ItemDataRole.ToolTipRole:
|
|
86
|
+
return f"{row.path}\n{row.reason}" if row.reason else str(row.path)
|
|
87
|
+
if role == Qt.ItemDataRole.ForegroundRole and row.reason:
|
|
88
|
+
view = self.parent()
|
|
89
|
+
muted = view.muted_color if isinstance(view, FileTable) else QColor(LIGHT.text_disabled)
|
|
90
|
+
return QBrush(muted)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def flags(self, index: QModelIndex | QPersistentModelIndex) -> Qt.ItemFlag:
|
|
94
|
+
if not index.isValid():
|
|
95
|
+
return Qt.ItemFlag.NoItemFlags
|
|
96
|
+
flags = Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
|
|
97
|
+
row = self.rows[index.row()]
|
|
98
|
+
if index.column() == 0 and row.reason is None:
|
|
99
|
+
flags |= Qt.ItemFlag.ItemIsUserCheckable
|
|
100
|
+
return flags
|
|
101
|
+
|
|
102
|
+
def setData(self, index: QModelIndex | QPersistentModelIndex, value, role: int = 0) -> bool:
|
|
103
|
+
if (
|
|
104
|
+
not index.isValid()
|
|
105
|
+
or index.column() != 0
|
|
106
|
+
or role != Qt.ItemDataRole.CheckStateRole
|
|
107
|
+
or self.rows[index.row()].reason is not None
|
|
108
|
+
):
|
|
109
|
+
return False
|
|
110
|
+
checked = value in (Qt.CheckState.Checked, Qt.CheckState.Checked.value)
|
|
111
|
+
row = self.rows[index.row()]
|
|
112
|
+
if row.checked == checked:
|
|
113
|
+
return True
|
|
114
|
+
row.checked = checked
|
|
115
|
+
self.dataChanged.emit(index, index, [Qt.ItemDataRole.CheckStateRole])
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
def add_font(
|
|
119
|
+
self,
|
|
120
|
+
path: Path,
|
|
121
|
+
label: str,
|
|
122
|
+
info: FontInfo,
|
|
123
|
+
checked: bool,
|
|
124
|
+
profile: tuple[str | None, bool],
|
|
125
|
+
) -> None:
|
|
126
|
+
conversion, _ = profile
|
|
127
|
+
reason = incompatibility(info, conversion) if conversion is not None else None
|
|
128
|
+
row = FontRow(
|
|
129
|
+
path,
|
|
130
|
+
label,
|
|
131
|
+
f"TTC ({info.num_fonts})" if info.collection else info.format,
|
|
132
|
+
info.outlines,
|
|
133
|
+
"Mixed" if info.upm is None else str(info.upm),
|
|
134
|
+
info,
|
|
135
|
+
checked,
|
|
136
|
+
reason,
|
|
137
|
+
)
|
|
138
|
+
position = len(self.rows)
|
|
139
|
+
self.beginInsertRows(QModelIndex(), position, position)
|
|
140
|
+
self.rows.append(row)
|
|
141
|
+
self.endInsertRows()
|
|
142
|
+
|
|
143
|
+
def clear(self) -> None:
|
|
144
|
+
self.beginResetModel()
|
|
145
|
+
self.rows.clear()
|
|
146
|
+
self.endResetModel()
|
|
147
|
+
|
|
148
|
+
def set_checks(self, source_rows: list[int], checked: bool) -> None:
|
|
149
|
+
changed_rows = []
|
|
150
|
+
for source_row in source_rows:
|
|
151
|
+
row = self.rows[source_row]
|
|
152
|
+
if row.reason is None and row.checked != checked:
|
|
153
|
+
row.checked = checked
|
|
154
|
+
changed_rows.append(source_row)
|
|
155
|
+
if changed_rows:
|
|
156
|
+
self.dataChanged.emit(
|
|
157
|
+
self.index(min(changed_rows), 0),
|
|
158
|
+
self.index(max(changed_rows), 0),
|
|
159
|
+
[Qt.ItemDataRole.CheckStateRole],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def update_compatibility(self, profile: tuple[str | None, bool]) -> None:
|
|
163
|
+
conversion, _ = profile
|
|
164
|
+
for row in self.rows:
|
|
165
|
+
row.reason = incompatibility(row.info, conversion) if conversion is not None else None
|
|
166
|
+
if self.rows:
|
|
167
|
+
self.dataChanged.emit(
|
|
168
|
+
self.index(0, 0),
|
|
169
|
+
self.index(len(self.rows) - 1, self.columnCount() - 1),
|
|
170
|
+
[
|
|
171
|
+
Qt.ItemDataRole.DisplayRole,
|
|
172
|
+
Qt.ItemDataRole.ToolTipRole,
|
|
173
|
+
Qt.ItemDataRole.ForegroundRole,
|
|
174
|
+
Qt.ItemDataRole.CheckStateRole,
|
|
175
|
+
],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class FontNameDelegate(QStyledItemDelegate):
|
|
180
|
+
def paint(
|
|
181
|
+
self,
|
|
182
|
+
painter: QPainter,
|
|
183
|
+
option: QStyleOptionViewItem,
|
|
184
|
+
index: QModelIndex | QPersistentModelIndex,
|
|
185
|
+
) -> None:
|
|
186
|
+
reason = index.data(_REASON_ROLE)
|
|
187
|
+
display_option = QStyleOptionViewItem(option)
|
|
188
|
+
if reason:
|
|
189
|
+
display_option.state &= ~QStyle.StateFlag.State_Enabled
|
|
190
|
+
super().paint(painter, display_option, index)
|
|
191
|
+
if not isinstance(reason, str) or not reason:
|
|
192
|
+
return
|
|
193
|
+
self.initStyleOption(display_option, index)
|
|
194
|
+
view = self.parent()
|
|
195
|
+
if not isinstance(view, FileTable):
|
|
196
|
+
return
|
|
197
|
+
rect = view.style().subElementRect(
|
|
198
|
+
QStyle.SubElement.SE_ItemViewItemText, display_option, view
|
|
199
|
+
)
|
|
200
|
+
metrics = display_option.fontMetrics
|
|
201
|
+
name_width = metrics.horizontalAdvance(str(index.data(Qt.ItemDataRole.DisplayRole) or ""))
|
|
202
|
+
rect.setLeft(rect.left() + name_width + 12)
|
|
203
|
+
rect.adjust(0, 0, -4, 0)
|
|
204
|
+
if rect.width() < 60:
|
|
205
|
+
return
|
|
206
|
+
text = metrics.elidedText(reason, Qt.TextElideMode.ElideRight, rect.width())
|
|
207
|
+
painter.save()
|
|
208
|
+
painter.setClipRect(option.rect)
|
|
209
|
+
painter.setFont(display_option.font)
|
|
210
|
+
painter.setPen(view.muted_color)
|
|
211
|
+
painter.drawText(rect, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, text)
|
|
212
|
+
painter.restore()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class FontFilterProxyModel(QSortFilterProxyModel):
|
|
216
|
+
def __init__(self, parent=None) -> None:
|
|
217
|
+
super().__init__(parent)
|
|
218
|
+
self.hide_incompatible = False
|
|
219
|
+
self.setFilterKeyColumn(-1)
|
|
220
|
+
self.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
|
221
|
+
|
|
222
|
+
def filterAcceptsRow(
|
|
223
|
+
self, source_row: int, source_parent: QModelIndex | QPersistentModelIndex
|
|
224
|
+
) -> bool:
|
|
225
|
+
if not super().filterAcceptsRow(source_row, source_parent):
|
|
226
|
+
return False
|
|
227
|
+
if not self.hide_incompatible:
|
|
228
|
+
return True
|
|
229
|
+
model = self.sourceModel()
|
|
230
|
+
index = model.index(source_row, 0, source_parent) if model is not None else QModelIndex()
|
|
231
|
+
return not index.isValid() or index.data(_REASON_ROLE) is None
|
|
232
|
+
|
|
233
|
+
def lessThan(
|
|
234
|
+
self,
|
|
235
|
+
left: QModelIndex | QPersistentModelIndex,
|
|
236
|
+
right: QModelIndex | QPersistentModelIndex,
|
|
237
|
+
) -> bool:
|
|
238
|
+
left_value = str(left.data(Qt.ItemDataRole.DisplayRole) or "")
|
|
239
|
+
right_value = str(right.data(Qt.ItemDataRole.DisplayRole) or "")
|
|
240
|
+
if left.column() == 3:
|
|
241
|
+
try:
|
|
242
|
+
return int(left_value) < int(right_value)
|
|
243
|
+
except ValueError:
|
|
244
|
+
pass
|
|
245
|
+
return left_value.casefold() < right_value.casefold()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class FontFileList(QObject):
|
|
249
|
+
"""Own the model-backed font table and its asynchronous scan."""
|
|
250
|
+
|
|
251
|
+
progress = Signal(int, int)
|
|
252
|
+
font_found = Signal(object, object)
|
|
253
|
+
scan_finished = Signal()
|
|
254
|
+
|
|
255
|
+
def __init__(
|
|
256
|
+
self,
|
|
257
|
+
view: FileTable,
|
|
258
|
+
profile: Callable[[], tuple[str | None, bool]],
|
|
259
|
+
) -> None:
|
|
260
|
+
super().__init__(view)
|
|
261
|
+
self.view = view
|
|
262
|
+
self.profile = profile
|
|
263
|
+
self.model = FontTableModel(view)
|
|
264
|
+
self.proxy = FontFilterProxyModel(view)
|
|
265
|
+
self.proxy.setSourceModel(self.model)
|
|
266
|
+
self.view.setModel(self.proxy)
|
|
267
|
+
self.view.setItemDelegateForColumn(0, FontNameDelegate(self.view))
|
|
268
|
+
self.view.configure_columns()
|
|
269
|
+
self.view.sortByColumn(0, Qt.SortOrder.AscendingOrder)
|
|
270
|
+
self._worker: ScanWorker | None = None
|
|
271
|
+
self._retired_workers: list[ScanWorker] = []
|
|
272
|
+
self._seen: set[Path] = set()
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def scanning(self) -> bool:
|
|
276
|
+
return self._worker is not None
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def row_count(self) -> int:
|
|
280
|
+
return len(self.model.rows)
|
|
281
|
+
|
|
282
|
+
def start(self, sources: list[Path], recursive: bool) -> None:
|
|
283
|
+
self.stop()
|
|
284
|
+
self._seen.clear()
|
|
285
|
+
worker = ScanWorker(sources, recursive)
|
|
286
|
+
self._worker = worker
|
|
287
|
+
# Lambdas have no receiver thread affinity: force queued delivery to the GUI thread.
|
|
288
|
+
queued = Qt.ConnectionType.QueuedConnection
|
|
289
|
+
worker.progress.connect(
|
|
290
|
+
lambda done, total, w=worker: self._on_progress(w, done, total), queued
|
|
291
|
+
)
|
|
292
|
+
worker.font_found.connect(
|
|
293
|
+
lambda path, info, w=worker: self._on_font_found(w, path, info), queued
|
|
294
|
+
)
|
|
295
|
+
worker.finished.connect(lambda w=worker: self._on_finished(w), queued)
|
|
296
|
+
worker.start()
|
|
297
|
+
|
|
298
|
+
def stop(self) -> None:
|
|
299
|
+
worker = self._worker
|
|
300
|
+
if worker is None:
|
|
301
|
+
return
|
|
302
|
+
self._worker = None
|
|
303
|
+
worker.requestInterruption()
|
|
304
|
+
if worker.isRunning():
|
|
305
|
+
self._retired_workers.append(worker)
|
|
306
|
+
|
|
307
|
+
def cancel(self) -> None:
|
|
308
|
+
if self._worker is not None:
|
|
309
|
+
self._worker.requestInterruption()
|
|
310
|
+
|
|
311
|
+
def shutdown(self) -> None:
|
|
312
|
+
self.stop()
|
|
313
|
+
for worker in self._retired_workers:
|
|
314
|
+
worker.wait()
|
|
315
|
+
self._retired_workers.clear()
|
|
316
|
+
|
|
317
|
+
def clear(self) -> None:
|
|
318
|
+
self.model.clear()
|
|
319
|
+
|
|
320
|
+
def add_font(self, path: Path, label: str, info: FontInfo, checked: bool) -> None:
|
|
321
|
+
self.model.add_font(path, label, info, checked, self.profile())
|
|
322
|
+
|
|
323
|
+
def apply_compatibility(self, *_: object) -> None:
|
|
324
|
+
profile = self.profile()
|
|
325
|
+
self.model.update_compatibility(profile)
|
|
326
|
+
self.proxy.hide_incompatible = profile[1]
|
|
327
|
+
if hasattr(self.proxy, "beginFilterChange"):
|
|
328
|
+
self.proxy.beginFilterChange()
|
|
329
|
+
self.proxy.endFilterChange(QSortFilterProxyModel.Direction.Rows)
|
|
330
|
+
else:
|
|
331
|
+
self.proxy.invalidateFilter()
|
|
332
|
+
|
|
333
|
+
def rows(self) -> Iterator[FontRow]:
|
|
334
|
+
return iter(self.model.rows)
|
|
335
|
+
|
|
336
|
+
def path_for_index(self, index: QModelIndex) -> Path:
|
|
337
|
+
source = self.proxy.mapToSource(index)
|
|
338
|
+
return self.model.rows[source.row()].path
|
|
339
|
+
|
|
340
|
+
def index_for_path(self, path: Path) -> QModelIndex:
|
|
341
|
+
for source_row, row in enumerate(self.model.rows):
|
|
342
|
+
if row.path == path:
|
|
343
|
+
return self.proxy.mapFromSource(self.model.index(source_row, 0))
|
|
344
|
+
return QModelIndex()
|
|
345
|
+
|
|
346
|
+
def is_visible(self, path: Path) -> bool:
|
|
347
|
+
return self.index_for_path(path).isValid()
|
|
348
|
+
|
|
349
|
+
def selected_paths(self) -> list[Path]:
|
|
350
|
+
return [
|
|
351
|
+
row.path
|
|
352
|
+
for proxy_row in range(self.proxy.rowCount())
|
|
353
|
+
if (
|
|
354
|
+
(source := self.proxy.mapToSource(self.proxy.index(proxy_row, 0))).isValid()
|
|
355
|
+
and (row := self.model.rows[source.row()]).checked
|
|
356
|
+
and row.reason is None
|
|
357
|
+
)
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
def remember_checks(self) -> dict[Path, bool]:
|
|
361
|
+
return {row.path: row.checked for row in self.model.rows}
|
|
362
|
+
|
|
363
|
+
def set_visible_checks(self, checked: bool) -> None:
|
|
364
|
+
source_rows = []
|
|
365
|
+
for proxy_row in range(self.proxy.rowCount()):
|
|
366
|
+
proxy_index = self.proxy.index(proxy_row, 0)
|
|
367
|
+
source_rows.append(self.proxy.mapToSource(proxy_index).row())
|
|
368
|
+
self.model.set_checks(source_rows, checked)
|
|
369
|
+
|
|
370
|
+
def compatible_count(self) -> int:
|
|
371
|
+
return sum(1 for row in self.model.rows if row.reason is None)
|
|
372
|
+
|
|
373
|
+
def visible_check_state(self) -> Qt.CheckState | None:
|
|
374
|
+
compatible = 0
|
|
375
|
+
checked = 0
|
|
376
|
+
for proxy_row in range(self.proxy.rowCount()):
|
|
377
|
+
source = self.proxy.mapToSource(self.proxy.index(proxy_row, 0))
|
|
378
|
+
if not source.isValid():
|
|
379
|
+
continue
|
|
380
|
+
row = self.model.rows[source.row()]
|
|
381
|
+
if row.reason is None:
|
|
382
|
+
compatible += 1
|
|
383
|
+
checked += row.checked
|
|
384
|
+
if compatible == 0:
|
|
385
|
+
return None
|
|
386
|
+
if checked == 0:
|
|
387
|
+
return Qt.CheckState.Unchecked
|
|
388
|
+
if checked == compatible:
|
|
389
|
+
return Qt.CheckState.Checked
|
|
390
|
+
return Qt.CheckState.PartiallyChecked
|
|
391
|
+
|
|
392
|
+
def _on_progress(self, worker: ScanWorker, done: int, total: int) -> None:
|
|
393
|
+
if worker is self._worker:
|
|
394
|
+
self.progress.emit(done, total)
|
|
395
|
+
|
|
396
|
+
def _on_font_found(self, worker: ScanWorker, path: Path, info: FontInfo) -> None:
|
|
397
|
+
if worker is self._worker and path not in self._seen:
|
|
398
|
+
self._seen.add(path)
|
|
399
|
+
self.font_found.emit(path, info)
|
|
400
|
+
|
|
401
|
+
def _on_finished(self, worker: ScanWorker) -> None:
|
|
402
|
+
worker.wait()
|
|
403
|
+
for signal in (worker.progress, worker.font_found, worker.finished):
|
|
404
|
+
signal.disconnect()
|
|
405
|
+
if worker in self._retired_workers:
|
|
406
|
+
self._retired_workers.remove(worker)
|
|
407
|
+
return
|
|
408
|
+
if worker is not self._worker:
|
|
409
|
+
return
|
|
410
|
+
self._worker = None
|
|
411
|
+
self.scan_finished.emit()
|