Coverage for paintmaskanimator/subview.py: 69%
475 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
1"""Linked reference-image viewer used by the dockable sub-view panel."""
3import math
4from pathlib import Path
5from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, Qt, Signal
6from PySide6.QtGui import (
7 QColor,
8 QCursor,
9 QImage,
10 QImageReader,
11 QPainter,
12 QPainterPath,
13 QPen,
14 QPixmap,
15 QTransform,
16)
17from PySide6.QtWidgets import (
18 QApplication,
19 QFileDialog,
20 QHBoxLayout,
21 QLabel,
22 QPushButton,
23 QSizePolicy,
24 QSlider,
25 QToolButton,
26 QVBoxLayout,
27 QWidget,
28)
29from .i18n import tr
30from .optional_deps import PILImage, PSDImage
31from . import imaging
32from .utils import drag_zoom_factor, install_app_event_filter, magnifier_cursor
35SUBVIEW_EXTENSIONS = {".png", ".jpg", ".jpeg", ".tga", ".psd"}
38class SubViewWidget(QWidget):
39 """画像ファイルをコピーせず、元パスへのリンクとして表示する。"""
41 colorPicked = Signal(QColor)
42 pathChanged = Signal(str)
43 visibilityChanged = Signal(bool)
45 def __init__(self, parent=None):
46 super().__init__(parent)
47 self.setAcceptDrops(True)
48 self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
49 self.setMinimumSize(180, 140)
50 self.setWindowTitle(tr("サブビュー"))
51 self._image = QImage()
52 self._path = None
53 self._files = []
54 self._index = -1
55 self._zoom = 1.0
56 self._rotation = 0.0
57 self._pan = QPointF()
58 self._space_down = False
59 self._control_down = False
60 self._drag_mode = None
61 self._press_pos = QPointF()
62 self._zoom_anchor = QPointF()
63 self._last_pos = QPointF()
64 self._moved = False
65 self._color_provider = None
66 self._pick_position = None
67 self._pick_before_color = QColor()
68 self._pick_after_color = QColor()
70 self.previous_button = QToolButton(self)
71 self.previous_button.setText("◀")
72 self.previous_button.setToolTip(tr("前の画像"))
73 self.next_button = QToolButton(self)
74 self.next_button.setText("▶")
75 self.next_button.setToolTip(tr("次の画像"))
76 self.open_button = QToolButton(self)
77 self.open_button.setText(tr("開く"))
78 self.open_button.setToolTip(tr("画像を開く"))
79 self.fit_button = QToolButton(self)
80 self.fit_button.setText(tr("全体"))
81 self.fit_button.setToolTip(tr("全体を表示"))
82 self.name_label = QLabel(tr("画像またはフォルダをドロップ"), self)
83 self.name_label.setTextInteractionFlags(
84 Qt.TextInteractionFlag.TextSelectableByMouse
85 )
86 self.name_label.setSizePolicy(
87 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
88 )
89 toolbar = QHBoxLayout()
90 toolbar.setContentsMargins(4, 3, 4, 3)
91 toolbar.addWidget(self.previous_button)
92 toolbar.addWidget(self.next_button)
93 toolbar.addWidget(self.open_button)
94 toolbar.addWidget(self.fit_button)
95 toolbar.addWidget(self.name_label, 1)
96 self.viewer = _SubViewCanvas(self)
97 self._update_viewer_cursor()
98 layout = QVBoxLayout(self)
99 layout.setContentsMargins(0, 0, 0, 0)
100 layout.setSpacing(0)
101 layout.addLayout(toolbar)
102 layout.addWidget(self.viewer, 1)
104 controls = QHBoxLayout()
105 controls.setContentsMargins(5, 3, 5, 4)
106 self.zoom_slider = QSlider(Qt.Orientation.Horizontal)
107 self.zoom_slider.setRange(5, 1600)
108 self.zoom_slider.setValue(100)
109 self.zoom_label = QLabel("100%")
110 self.zoom_100_button = QPushButton(tr("100%表示"))
111 self.rotation_slider = QSlider(Qt.Orientation.Horizontal)
112 self.rotation_slider.setRange(-180, 180)
113 self.rotation_slider.setValue(0)
114 self.rotation_label = QLabel("0°")
115 self.rotation_0_button = QPushButton("0°")
116 for widget in (
117 QLabel(tr("拡大")), self.zoom_slider, self.zoom_label,
118 self.zoom_100_button, QLabel(tr("回転")), self.rotation_slider,
119 self.rotation_label, self.rotation_0_button,
120 ):
121 controls.addWidget(widget)
122 layout.addLayout(controls)
123 self.previous_button.clicked.connect(lambda: self.step(-1))
124 self.next_button.clicked.connect(lambda: self.step(1))
125 self.open_button.clicked.connect(self.choose_image)
126 self.fit_button.clicked.connect(self.reset_view)
127 self.zoom_slider.valueChanged.connect(self.set_zoom_percent)
128 self.zoom_100_button.clicked.connect(
129 lambda: self.zoom_slider.setValue(100)
130 )
131 self.rotation_slider.valueChanged.connect(self.set_rotation)
132 self.rotation_0_button.clicked.connect(
133 lambda: self.rotation_slider.setValue(0)
134 )
135 self._update_navigation()
136 install_app_event_filter(self)
138 def eventFilter(self, watched, event):
139 """サブビューが前面なら、子部品のフォーカスに関係なくSpaceを捕捉する。"""
140 if not isinstance(watched, QObject): 140 ↛ 146line 140 didn't jump to line 146 because the condition on line 140 was never true
141 # PySide6 が watched を QObject ではない型(QWidgetItem など)として
142 # 渡してくることがある。そのまま QObject.eventFilter に渡すと
143 # TypeError になり、例外がイベントループの外――たまたま実行中だった
144 # 任意の Python フレーム――へ抜けて落ちる。見分けられない相手なら、
145 # フィルターせずに降りる。
146 return False
147 active = (
148 QApplication.activeWindow()
149 if QApplication.instance() is not None
150 else None
151 )
152 belongs_to_subview = bool(
153 isinstance(watched, QWidget)
154 and (watched is self or self.isAncestorOf(watched))
155 )
156 if active is self or belongs_to_subview:
157 if event.type() == QEvent.Type.ShortcutOverride and event.key() in ( 157 ↛ 160line 157 didn't jump to line 160 because the condition on line 157 was never true
158 Qt.Key.Key_Space, Qt.Key.Key_Control
159 ):
160 event.accept()
161 return True
162 if ( 162 ↛ 167line 162 didn't jump to line 167 because the condition on line 162 was never true
163 event.type() in (QEvent.Type.KeyPress, QEvent.Type.KeyRelease)
164 and event.key() in (Qt.Key.Key_Space, Qt.Key.Key_Control)
165 and not event.isAutoRepeat()
166 ):
167 self.handle_hold_key_event(event)
168 event.accept()
169 return True
170 return super().eventFilter(watched, event)
172 def handle_hold_key_event(self, event):
173 pressed = event.type() == QEvent.Type.KeyPress
174 if event.key() == Qt.Key.Key_Space:
175 self._space_down = pressed
176 elif event.key() == Qt.Key.Key_Control:
177 self._control_down = pressed
178 self._update_viewer_cursor()
180 def _eyedropper_cursor(self):
181 cached = getattr(self, "_eyedropper_cursor_cache", None)
182 if cached is not None: 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true
183 return cached
184 pixmap = QPixmap(24, 24)
185 pixmap.fill(Qt.GlobalColor.transparent)
186 painter = QPainter(pixmap)
187 painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
188 painter.setPen(QPen(QColor("black"), 2))
189 painter.drawLine(5, 18, 16, 7)
190 painter.drawLine(8, 21, 19, 10)
191 painter.drawLine(15, 6, 20, 11)
192 painter.drawEllipse(QPointF(5, 19), 2, 2)
193 painter.end()
194 self._eyedropper_cursor_cache = QCursor(pixmap, 5, 19)
195 return self._eyedropper_cursor_cache
197 def _update_viewer_cursor(self):
198 if self._drag_mode == "pan":
199 self.viewer.setCursor(Qt.CursorShape.ClosedHandCursor)
200 elif self._drag_mode == "zoom" or ( 200 ↛ 203line 200 didn't jump to line 203 because the condition on line 200 was never true
201 self._space_down and self._control_down
202 ):
203 self.viewer.setCursor(magnifier_cursor())
204 elif self._space_down: 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true
205 self.viewer.setCursor(Qt.CursorShape.OpenHandCursor)
206 elif self._drag_mode == "pick":
207 self.viewer.setCursor(self._eyedropper_cursor())
208 else:
209 self.viewer.unsetCursor()
211 def set_color_provider(self, provider):
212 """スポイト確定前の描画色を取得するコールバックを設定する。"""
213 self._color_provider = provider
215 def current_paint_color(self):
216 if callable(self._color_provider): 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true
217 try:
218 return QColor(self._color_provider())
219 except (RuntimeError, TypeError, AttributeError):
220 pass
221 return QColor()
223 @staticmethod
224 def supported_extensions():
225 return set(SUBVIEW_EXTENSIONS)
227 def choose_image(self):
228 path, _selected = QFileDialog.getOpenFileName(
229 self, tr("サブビュー画像を開く"), "",
230 tr("対応画像 (*.png *.jpg *.jpeg *.tga *.psd);;"
231 "PNG (*.png);;JPEG (*.jpg *.jpeg);;TGA (*.tga);;PSD (*.psd);;"
232 "すべてのファイル (*)"),
233 )
234 if path:
235 self.load_path(path)
237 def load_path(self, path):
238 candidate = Path(path)
239 if candidate.is_dir():
240 files = self._images_in_folder(candidate)
241 if not files: 241 ↛ 242line 241 didn't jump to line 242 because the condition on line 241 was never true
242 self.name_label.setText(tr("画像が見つかりません"))
243 return False
244 target = files[0]
245 else:
246 target = candidate
247 files = self._images_in_folder(candidate.parent)
248 if candidate not in files:
249 files.append(candidate)
250 files.sort(key=lambda item: item.name.casefold())
251 image = self._read_image(target)
252 if image.isNull():
253 self.name_label.setText(tr("読み込めません: {name}").format(name=target.name))
254 return False
255 self._files = files
256 self._index = files.index(target)
257 self._set_image(target, image)
258 return True
260 @staticmethod
261 def _read_image(path):
262 """対応形式を読み込み、PSDは表示レイヤーの合成結果を返す。"""
263 path = Path(path)
264 if path.suffix.lower() != ".psd": 264 ↛ 277line 264 didn't jump to line 277 because the condition on line 264 was always true
265 reader = QImageReader(str(path))
266 reader.setAutoTransform(True)
267 image = reader.read()
268 if not image.isNull() or PILImage is None:
269 return image
270 # QtのTGAプラグインが対応しない圧縮・ピクセル形式はPillowで補完する。
271 try:
272 with PILImage.open(path) as source:
273 source.load()
274 return imaging.pil_rgba_to_qimage(source.convert("RGBA"))
275 except (ValueError, TypeError, OSError, RuntimeError, AttributeError):
276 return QImage()
277 if PSDImage is None or PILImage is None:
278 return QImage()
279 try:
280 psd = PSDImage.open(path)
281 # PSDImage.composite() は非表示レイヤーを除外した表示合成を返す。
282 rendered = psd.composite()
283 if rendered is None:
284 return QImage()
285 return imaging.pil_rgba_to_qimage(rendered)
286 except (
287 ValueError, KeyError, IndexError, TypeError, OSError,
288 RuntimeError, AttributeError,
289 ):
290 return QImage()
292 def _images_in_folder(self, folder):
293 extensions = self.supported_extensions()
294 try:
295 return sorted(
296 (item for item in folder.iterdir()
297 if item.is_file() and item.suffix.lower() in extensions),
298 key=lambda item: item.name.casefold(),
299 )
300 except OSError:
301 return []
303 def _set_image(self, path, image):
304 self._path = Path(path)
305 self._image = image.convertToFormat(QImage.Format.Format_ARGB32)
306 self.name_label.setText(self._path.name)
307 self.name_label.setToolTip(str(self._path))
308 self.reset_view()
309 self._update_navigation()
310 self.pathChanged.emit(str(self._path))
312 def step(self, offset):
313 if not self._files: 313 ↛ 314line 313 didn't jump to line 314 because the condition on line 313 was never true
314 return
315 index = self._index + int(offset)
316 if not 0 <= index < len(self._files): 316 ↛ 317line 316 didn't jump to line 317 because the condition on line 316 was never true
317 return
318 target = self._files[index]
319 image = self._read_image(target)
320 if image.isNull(): 320 ↛ 321line 320 didn't jump to line 321 because the condition on line 320 was never true
321 return
322 self._index = index
323 self._set_image(target, image)
325 def _update_navigation(self):
326 self.previous_button.setEnabled(self._index > 0)
327 self.next_button.setEnabled(0 <= self._index < len(self._files) - 1)
329 def reset_view(self):
330 self.zoom_slider.blockSignals(True)
331 self.zoom_slider.setValue(100)
332 self.zoom_slider.blockSignals(False)
333 self._zoom = 1.0
334 self.zoom_label.setText("100%")
335 self._pan = QPointF()
336 self.viewer.update()
338 def set_zoom_percent(self, value):
339 self._zoom = max(0.05, min(16.0, int(value) / 100.0))
340 self.zoom_label.setText(f"{int(value)}%")
341 self.viewer.update()
343 def set_rotation(self, value):
344 self._rotation = float(value)
345 self.rotation_label.setText(f"{int(value)}°")
346 self.viewer.update()
348 def showEvent(self, event):
349 super().showEvent(event)
350 self.visibilityChanged.emit(True)
352 def closeEvent(self, event):
353 self.visibilityChanged.emit(False)
354 super().closeEvent(event)
356 def dragEnterEvent(self, event):
357 if event.mimeData().hasUrls():
358 event.acceptProposedAction()
360 def dropEvent(self, event):
361 for url in event.mimeData().urls():
362 if url.isLocalFile() and self.load_path(url.toLocalFile()):
363 event.acceptProposedAction()
364 return
367_PAN_BUTTONS = (Qt.MouseButton.RightButton, Qt.MouseButton.MiddleButton)
368_HANDLED_BUTTONS = (Qt.MouseButton.LeftButton, *_PAN_BUTTONS)
371class _SubViewCanvas(QWidget):
372 def __init__(self, owner):
373 super().__init__(owner)
374 self.owner = owner
375 self._wheel_percent = None
376 self.setAcceptDrops(True)
377 self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
378 self.setMouseTracking(True)
380 def enterEvent(self, event):
381 self.owner._update_viewer_cursor()
382 super().enterEvent(event)
384 def _fit_scale(self):
385 image = self.owner._image
386 if image.isNull() or self.width() <= 0 or self.height() <= 0: 386 ↛ 387line 386 didn't jump to line 387 because the condition on line 386 was never true
387 return 0.0
388 angle = math.radians(self.owner._rotation)
389 cosine, sine = abs(math.cos(angle)), abs(math.sin(angle))
390 rotated_width = image.width() * cosine + image.height() * sine
391 rotated_height = image.width() * sine + image.height() * cosine
392 return min(self.width() / rotated_width, self.height() / rotated_height)
394 def _image_transform(self):
395 image = self.owner._image
396 transform = QTransform()
397 if image.isNull(): 397 ↛ 398line 397 didn't jump to line 398 because the condition on line 397 was never true
398 return transform
399 center = QPointF(self.rect().center()) + self.owner._pan
400 scale = max(0.01, self._fit_scale() * self.owner._zoom)
401 transform.translate(center.x(), center.y())
402 transform.rotate(self.owner._rotation)
403 transform.scale(scale, scale)
404 transform.translate(-image.width() / 2, -image.height() / 2)
405 return transform
407 def _image_rect(self):
408 image = self.owner._image
409 if image.isNull(): 409 ↛ 410line 409 didn't jump to line 410 because the condition on line 409 was never true
410 return QRectF()
411 return self._image_transform().mapRect(
412 QRectF(0, 0, image.width(), image.height())
413 )
415 def paintEvent(self, _event):
416 painter = QPainter(self)
417 painter.fillRect(self.rect(), QColor("#30343a"))
418 if self.owner._image.isNull():
419 painter.setPen(QColor("#c3c8cc"))
420 painter.drawText(
421 self.rect(), Qt.AlignmentFlag.AlignCenter,
422 tr("画像またはフォルダを\nここにドロップ"),
423 )
424 return
425 painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, False)
426 painter.setTransform(self._image_transform())
427 painter.drawImage(QPointF(0, 0), self.owner._image)
428 painter.resetTransform()
429 if self.owner._drag_mode == "pick" and self.owner._pick_position is not None: 429 ↛ 430line 429 didn't jump to line 430 because the condition on line 429 was never true
430 self._draw_color_loupe(painter)
432 def _draw_color_loupe(self, painter):
433 position = QPointF(self.owner._pick_position)
434 image_point = self._widget_to_image(position)
435 if image_point is None:
436 return
437 radius = 42.0
438 center = position + QPointF(58, -58)
439 center.setX(max(radius + 4, min(self.width() - radius - 4, center.x())))
440 center.setY(max(radius + 4, min(self.height() - radius - 30, center.y())))
441 circle = QRectF(
442 center.x() - radius, center.y() - radius,
443 radius * 2, radius * 2,
444 )
445 painter.save()
446 clip = QPainterPath()
447 clip.addEllipse(circle)
448 painter.setClipPath(clip)
449 painter.fillRect(circle, QColor("#20242a"))
450 magnification = 10.0
451 transform = QTransform()
452 transform.translate(center.x(), center.y())
453 transform.scale(magnification, magnification)
454 transform.translate(-image_point.x(), -image_point.y())
455 painter.setTransform(transform)
456 painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, False)
457 painter.drawImage(QPointF(0, 0), self.owner._image)
458 painter.restore()
459 painter.setPen(QPen(QColor("white"), 2))
460 painter.setBrush(Qt.BrushStyle.NoBrush)
461 painter.drawEllipse(circle)
462 painter.drawLine(
463 QPointF(center.x() - 7, center.y()),
464 QPointF(center.x() + 7, center.y()),
465 )
466 painter.drawLine(
467 QPointF(center.x(), center.y() - 7),
468 QPointF(center.x(), center.y() + 7),
469 )
471 swatch_y = circle.bottom() + 5
472 before_rect = QRectF(center.x() - 40, swatch_y, 38, 20)
473 after_rect = QRectF(center.x() + 2, swatch_y, 38, 20)
474 before = self.owner._pick_before_color
475 after = self.owner._pick_after_color
476 painter.fillRect(before_rect, before if before.isValid() else QColor("#000000"))
477 painter.fillRect(after_rect, after if after.isValid() else QColor("#000000"))
478 painter.setPen(QPen(QColor("white"), 1))
479 painter.drawRect(before_rect)
480 painter.drawRect(after_rect)
481 painter.drawText(before_rect, Qt.AlignmentFlag.AlignCenter, tr("前"))
482 painter.drawText(after_rect, Qt.AlignmentFlag.AlignCenter, tr("後"))
484 def dragEnterEvent(self, event):
485 self.owner.dragEnterEvent(event)
487 def dropEvent(self, event):
488 self.owner.dropEvent(event)
490 def keyPressEvent(self, event):
491 if event.key() in (Qt.Key.Key_Space, Qt.Key.Key_Control) and not event.isAutoRepeat():
492 self.owner.handle_hold_key_event(event)
493 event.accept()
494 return
495 super().keyPressEvent(event)
497 def keyReleaseEvent(self, event):
498 if event.key() in (Qt.Key.Key_Space, Qt.Key.Key_Control) and not event.isAutoRepeat():
499 self.owner.handle_hold_key_event(event)
500 event.accept()
501 return
502 super().keyReleaseEvent(event)
504 def mousePressEvent(self, event):
505 self.setFocus(Qt.FocusReason.MouseFocusReason)
506 if event.button() not in _HANDLED_BUTTONS: 506 ↛ 507line 506 didn't jump to line 507 because the condition on line 506 was never true
507 return
508 self.owner._press_pos = event.position()
509 self.owner._last_pos = event.position()
510 self.owner._moved = False
511 self.owner._zoom_anchor = QPointF(event.position())
512 if event.button() in _PAN_BUTTONS:
513 # 右ドラッグ(中ボタンも)で画像を動かす。左クリックはスポイト。
514 self.owner._drag_mode = "pan"
515 self.setCursor(Qt.CursorShape.ClosedHandCursor)
516 elif self.owner._space_down: 516 ↛ 517line 516 didn't jump to line 517 because the condition on line 516 was never true
517 modifiers = (
518 event.modifiers() | QApplication.keyboardModifiers()
519 )
520 self.owner._drag_mode = (
521 "zoom" if (
522 self.owner._control_down
523 or modifiers & Qt.KeyboardModifier.ControlModifier
524 )
525 else "pan"
526 )
527 else:
528 self.owner._drag_mode = "pick"
529 self.owner._pick_position = QPointF(event.position())
530 self.owner._pick_before_color = self.owner.current_paint_color()
531 sampled = self._color_at(event.position())
532 self.owner._pick_after_color = sampled or QColor()
533 self.owner._update_viewer_cursor()
534 self.update()
535 event.accept()
537 def mouseMoveEvent(self, event):
538 mode = self.owner._drag_mode
539 if mode is None: 539 ↛ 540line 539 didn't jump to line 540 because the condition on line 539 was never true
540 return
541 delta = event.position() - self.owner._last_pos
542 if (event.position() - self.owner._press_pos).manhattanLength() > 3: 542 ↛ 544line 542 didn't jump to line 544 because the condition on line 542 was always true
543 self.owner._moved = True
544 if mode == "pan": 544 ↛ 547line 544 didn't jump to line 547 because the condition on line 544 was always true
545 self.owner._pan += delta
546 self.update()
547 elif mode == "zoom":
548 factor = drag_zoom_factor(delta.x())
549 percent = int(round(self.owner._zoom * factor * 100))
550 # ドラッグを始めた位置を中心に拡大縮小する。
551 self.zoom_to_percent(
552 max(5, min(1600, percent)), self.owner._zoom_anchor
553 )
554 elif mode == "pick":
555 self.owner._pick_position = QPointF(event.position())
556 sampled = self._color_at(event.position())
557 self.owner._pick_after_color = sampled or QColor()
558 self.update()
559 self.owner._last_pos = event.position()
560 event.accept()
562 def mouseReleaseEvent(self, event):
563 if event.button() not in _HANDLED_BUTTONS: 563 ↛ 564line 563 didn't jump to line 564 because the condition on line 563 was never true
564 return
565 if self.owner._drag_mode == "pick":
566 self._pick_color(event.position())
567 self.owner._drag_mode = None
568 self.owner._pick_position = None
569 self.owner._update_viewer_cursor()
570 self.update()
571 event.accept()
573 def wheelEvent(self, event):
574 delta = event.angleDelta().y()
575 if not delta: 575 ↛ 576line 575 didn't jump to line 576 because the condition on line 575 was never true
576 event.ignore()
577 return
578 # トラックパッドの細かいイベントでも移動量に比例させる(1ノッチ=120)。
579 # スライダーは整数%なので、端数込みの倍率を保持して次のイベントへ持ち越す。
580 current = self.owner.zoom_slider.value()
581 exact = self._wheel_percent
582 if exact is None or round(exact) != current:
583 exact = float(current) # ボタンやドラッグなど他の経路で変わった
584 exact = max(5.0, min(1600.0, exact * 1.15 ** (delta / 120)))
585 self._wheel_percent = exact
586 if round(exact) != current:
587 self.zoom_to_percent(round(exact), event.position())
588 event.accept()
590 def zoom_to_percent(self, percent, anchor=None):
591 """倍率を変える。``anchor``(ウィジェット座標)の下の画素は動かさない。"""
592 image = self.owner._image
593 if anchor is None or image.isNull():
594 self.owner.zoom_slider.setValue(int(percent))
595 return
596 anchor = QPointF(anchor)
597 inverse, invertible = self._image_transform().inverted()
598 self.owner.zoom_slider.setValue(int(percent))
599 if not invertible: 599 ↛ 600line 599 didn't jump to line 600 because the condition on line 599 was never true
600 return
601 image_point = inverse.map(anchor)
602 moved_to = self._image_transform().map(image_point)
603 self.owner._pan += anchor - moved_to
604 self.update()
606 def _pick_color(self, position):
607 color = self._color_at(position)
608 if color is not None: 608 ↛ exitline 608 didn't return from function '_pick_color' because the condition on line 608 was always true
609 self.owner.colorPicked.emit(color)
611 def _widget_to_image(self, position):
612 image = self.owner._image
613 if image.isNull(): 613 ↛ 614line 613 didn't jump to line 614 because the condition on line 613 was never true
614 return None
615 inverse, invertible = self._image_transform().inverted()
616 if not invertible: 616 ↛ 617line 616 didn't jump to line 617 because the condition on line 616 was never true
617 return None
618 point = inverse.map(position)
619 x, y = int(point.x()), int(point.y())
620 if not (0 <= x < image.width() and 0 <= y < image.height()): 620 ↛ 621line 620 didn't jump to line 621 because the condition on line 620 was never true
621 return None
622 return QPointF(x + 0.5, y + 0.5)
624 def _color_at(self, position):
625 image = self.owner._image
626 point = self._widget_to_image(position)
627 if point is None: 627 ↛ 628line 627 didn't jump to line 628 because the condition on line 627 was never true
628 return None
629 x, y = int(point.x()), int(point.y())
630 x = max(0, min(image.width() - 1, x))
631 y = max(0, min(image.height() - 1, y))
632 return QColor.fromRgba(image.pixel(x, y))