Coverage for paintmaskanimator/onion.py: 62%
557 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
1from typing import Any
2from PySide6.QtCore import Qt, Signal
3from PySide6.QtGui import QColor
4from PySide6.QtWidgets import (
5 QAbstractSpinBox,
6 QCheckBox,
7 QDoubleSpinBox,
8 QFormLayout,
9 QHBoxLayout,
10 QLabel,
11 QPushButton,
12 QSizePolicy,
13 QSlider,
14 QSpinBox,
15 QTabWidget,
16 QVBoxLayout,
17 QWidget,
18)
19from . import theme
20from .i18n import tr
21from .color_picker_dialog import ColorPickerDialog
22from .logging_setup import get_logger
24log = get_logger(__name__)
27class OnionOpacityGraph(QWidget):
28 """表示枚数と同数の縦スライダーで、コマ別の濃度を設定する。"""
30 levelsChanged = Signal()
32 def __init__(
33 self,
34 count=1,
35 levels=None,
36 reverse_order=False,
37 parent=None,
38 ):
39 super().__init__(parent)
40 self._reverse_order = bool(reverse_order)
41 self._sliders = []
42 self._values = [
43 max(0, min(100, int(value)))
44 for value in (levels or [])
45 ]
46 self._layout = QHBoxLayout(self)
47 self._layout.setContentsMargins(0, 0, 0, 0)
48 self._layout.setSpacing(0)
49 self.setFixedHeight(102)
50 self.setSizePolicy(
51 QSizePolicy.Policy.Fixed,
52 QSizePolicy.Policy.Fixed,
53 )
54 self.setToolTip(
55 tr("各縦スライダーが、近いコマから順に1枚ずつ対応します。"
56 "上ほど濃く、下ほど薄く表示します。")
57 )
58 self.set_count(count)
60 @staticmethod
61 def _default_level(index, count):
62 if count <= 1:
63 return 100
64 return max(
65 10,
66 min(
67 100,
68 int(round(
69 100.0
70 - 55.0 * index / float(max(1, count - 1))
71 )),
72 ),
73 )
75 def _clear_layout(self):
76 while self._layout.count(): 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 item = self._layout.takeAt(0)
78 widget = item.widget() if item is not None else None
79 if widget is not None:
80 widget.deleteLater()
82 def set_count(self, count):
83 count = max(0, min(12, int(count)))
84 current = self.levels()
85 if not current: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true
86 current = list(self._values)
87 self._clear_layout()
88 self._sliders: list[Any] = [None] * count
90 visual_indices = (
91 range(count - 1, -1, -1)
92 if self._reverse_order
93 else range(count)
94 )
95 for index in visual_indices:
96 value = (
97 current[index]
98 if index < len(current)
99 else self._default_level(index, count)
100 )
101 column = QWidget()
102 column.setFixedWidth(22)
103 column_layout = QVBoxLayout(column)
104 column_layout.setContentsMargins(0, 0, 0, 0)
105 column_layout.setSpacing(0)
107 number = QLabel(str(index + 1))
108 number.setFixedSize(22, 14)
109 number.setAlignment(Qt.AlignmentFlag.AlignCenter)
110 number.setStyleSheet(
111 "font-size:9px;padding:0px;margin:0px;"
112 )
113 number.setToolTip(
114 tr("現在コマから{value}枚離れたオニオンスキン").format(value=index + 1)
115 )
117 slider = QSlider(Qt.Orientation.Vertical)
118 slider.setRange(0, 100)
119 slider.setSingleStep(1)
120 slider.setPageStep(10)
121 slider.setValue(max(0, min(100, int(value))))
122 slider.setFixedSize(16, 70)
123 slider.setTickPosition(QSlider.TickPosition.NoTicks)
124 _c = theme.palette()
125 slider.setStyleSheet(
126 "QSlider::groove:vertical{width:4px;"
127 "background:%s;border-radius:2px;}"
128 "QSlider::handle:vertical{height:12px;width:12px;"
129 "margin:0 -6px;background:%s;"
130 "border:2px solid %s;border-radius:8px;}"
131 % (_c["border"], _c["surface"], _c["accent"])
132 )
133 slider.setToolTip(
134 tr("{value}枚目の濃度:{value2}%").format(value=index + 1, value2=slider.value())
135 )
137 value_label = QLabel(f"{slider.value()}")
138 value_label.setFixedSize(22, 14)
139 value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
140 value_label.setStyleSheet(
141 "font-size:9px;padding:0px;margin:0px;"
142 )
144 slider.valueChanged.connect(
145 lambda value, target=slider, label=value_label: (
146 target.setToolTip(
147 tr("コマ別濃度:{int}%").format(int=int(value))
148 ),
149 label.setText(str(int(value))),
150 self.levelsChanged.emit(),
151 )
152 )
154 column_layout.addWidget(number)
155 column_layout.addWidget(
156 slider,
157 1,
158 Qt.AlignmentFlag.AlignHCenter,
159 )
160 column_layout.addWidget(value_label)
161 self._layout.addWidget(
162 column,
163 0,
164 Qt.AlignmentFlag.AlignLeft,
165 )
166 self._sliders[index] = slider
168 # 余分な伸縮領域を置かず、表示枚数分だけの幅にする。
169 self.setFixedWidth(max(22, count * 22))
170 self._values = self.levels()
172 def levels(self):
173 if self._sliders:
174 return [int(slider.value()) for slider in self._sliders]
175 return list(self._values)
177 def set_levels(self, levels):
178 values = [
179 max(0, min(100, int(value)))
180 for value in (levels or [])
181 ]
182 self._values = values
183 for index, slider in enumerate(self._sliders):
184 value = (
185 values[index]
186 if index < len(values)
187 else self._default_level(index, len(self._sliders))
188 )
189 slider.blockSignals(True)
190 slider.setValue(value)
191 slider.blockSignals(False)
194class OnionScaleSlider(QSlider):
195 """100%を中央に置く、低速・1%刻みのTU/TB用スライダー。"""
197 def __init__(self, value=100, parent=None):
198 super().__init__(Qt.Orientation.Horizontal, parent)
199 # 100%が物理的にも中央になる対称範囲。
200 self.setRange(1, 199)
201 self.setSingleStep(1)
202 self.setPageStep(1)
203 self.setValue(max(1, min(199, int(round(float(value))))))
204 self.setTickPosition(QSlider.TickPosition.TicksBelow)
205 self.setTickInterval(10)
206 self.setToolTip(
207 tr("中央が100%。左で縮小、右で拡大します。"
208 "通常は1%ずつ、Shiftを押しながら操作すると3%ずつ動きます。")
209 )
210 self._slow_dragging = False
211 self._slow_drag_start_x = 0.0
212 self._slow_drag_start_value = self.value()
214 def _step_multiplier(self, modifiers):
215 return (
216 3
217 if modifiers & Qt.KeyboardModifier.ShiftModifier
218 else 1
219 )
221 def mousePressEvent(self, event):
222 if event.button() == Qt.MouseButton.LeftButton:
223 self._slow_dragging = True
224 self._slow_drag_start_x = event.globalPosition().x()
225 self._slow_drag_start_value = int(self.value())
226 try:
227 self.grabMouse()
228 except RuntimeError as exc:
229 log.debug("grabMouse() failed: %s", exc)
230 event.accept()
231 return
232 super().mousePressEvent(event)
234 def mouseMoveEvent(self, event):
235 if (
236 self._slow_dragging
237 and event.buttons() & Qt.MouseButton.LeftButton
238 ):
239 # 2pxで1%。Shift中は同じ移動量で3倍速。
240 delta_x = (
241 event.globalPosition().x()
242 - self._slow_drag_start_x
243 )
244 raw_steps = int(round(delta_x / 2.0))
245 steps = raw_steps * self._step_multiplier(
246 event.modifiers()
247 )
248 self.setValue(
249 max(
250 self.minimum(),
251 min(
252 self.maximum(),
253 self._slow_drag_start_value + steps,
254 ),
255 )
256 )
257 event.accept()
258 return
259 super().mouseMoveEvent(event)
261 def mouseReleaseEvent(self, event):
262 if (
263 self._slow_dragging
264 and event.button() == Qt.MouseButton.LeftButton
265 ):
266 self._slow_dragging = False
267 try:
268 self.releaseMouse()
269 except RuntimeError as exc:
270 log.debug("releaseMouse() failed: %s", exc)
271 event.accept()
272 return
273 super().mouseReleaseEvent(event)
275 def keyPressEvent(self, event):
276 key = event.key()
277 if key in (
278 Qt.Key.Key_Left,
279 Qt.Key.Key_Down,
280 Qt.Key.Key_Right,
281 Qt.Key.Key_Up,
282 ):
283 direction = (
284 -1
285 if key in (Qt.Key.Key_Left, Qt.Key.Key_Down)
286 else 1
287 )
288 self.setValue(
289 self.value()
290 + direction
291 * self._step_multiplier(event.modifiers())
292 )
293 event.accept()
294 return
295 super().keyPressEvent(event)
297 def wheelEvent(self, event):
298 delta = event.angleDelta().y()
299 if delta:
300 direction = 1 if delta > 0 else -1
301 self.setValue(
302 self.value()
303 + direction
304 * self._step_multiplier(event.modifiers())
305 )
306 event.accept()
307 return
308 super().wheelEvent(event)
311class OnionRotationSlider(QSlider):
312 """0.5度刻みでゆっくり動くオニオン回転スライダー。"""
314 def __init__(self, value=0.0, parent=None):
315 super().__init__(Qt.Orientation.Horizontal, parent)
316 # 内部値は0.1度単位。通常5=0.5度、Shift時30=3度。
317 self.setRange(-1800, 1800)
318 self.setSingleStep(5)
319 self.setPageStep(5)
320 self.setValue(
321 max(
322 self.minimum(),
323 min(
324 self.maximum(),
325 int(round(float(value) * 10.0)),
326 ),
327 )
328 )
329 self.setTickPosition(QSlider.TickPosition.TicksBelow)
330 self.setTickInterval(300)
331 self.setToolTip(
332 tr("通常は0.5°ずつゆっくり調整します。"
333 "Shiftを押しながら操作すると3°ずつ動きます。")
334 )
335 self._slow_dragging = False
336 self._slow_drag_start_x = 0.0
337 self._slow_drag_start_value = int(self.value())
339 @staticmethod
340 def _step_units(modifiers):
341 return (
342 30
343 if modifiers & Qt.KeyboardModifier.ShiftModifier
344 else 5
345 )
347 def mousePressEvent(self, event):
348 if event.button() == Qt.MouseButton.LeftButton:
349 self._slow_dragging = True
350 self._slow_drag_start_x = event.globalPosition().x()
351 self._slow_drag_start_value = int(self.value())
352 try:
353 self.grabMouse()
354 except RuntimeError as exc:
355 log.debug("grabMouse() failed: %s", exc)
356 event.accept()
357 return
358 super().mousePressEvent(event)
360 def mouseMoveEvent(self, event):
361 if (
362 self._slow_dragging
363 and event.buttons() & Qt.MouseButton.LeftButton
364 ):
365 # 3pxの移動につき、通常0.5°/Shift時3°。
366 delta_x = (
367 event.globalPosition().x()
368 - self._slow_drag_start_x
369 )
370 steps = int(round(delta_x / 3.0))
371 self.setValue(
372 max(
373 self.minimum(),
374 min(
375 self.maximum(),
376 self._slow_drag_start_value
377 + steps * self._step_units(event.modifiers()),
378 ),
379 )
380 )
381 event.accept()
382 return
383 super().mouseMoveEvent(event)
385 def mouseReleaseEvent(self, event):
386 if (
387 self._slow_dragging
388 and event.button() == Qt.MouseButton.LeftButton
389 ):
390 self._slow_dragging = False
391 try:
392 self.releaseMouse()
393 except RuntimeError as exc:
394 log.debug("releaseMouse() failed: %s", exc)
395 event.accept()
396 return
397 super().mouseReleaseEvent(event)
399 def keyPressEvent(self, event):
400 key = event.key()
401 if key in (
402 Qt.Key.Key_Left,
403 Qt.Key.Key_Down,
404 Qt.Key.Key_Right,
405 Qt.Key.Key_Up,
406 ):
407 direction = (
408 -1
409 if key in (Qt.Key.Key_Left, Qt.Key.Key_Down)
410 else 1
411 )
412 self.setValue(
413 self.value()
414 + direction * self._step_units(event.modifiers())
415 )
416 event.accept()
417 return
418 super().keyPressEvent(event)
420 def wheelEvent(self, event):
421 delta = event.angleDelta().y()
422 if delta:
423 direction = 1 if delta > 0 else -1
424 self.setValue(
425 self.value()
426 + direction * self._step_units(event.modifiers())
427 )
428 event.accept()
429 return
430 super().wheelEvent(event)
433class OnionSkinSettingsBrowser(QWidget):
434 """オニオンスキンを即時調整する一時表示ブラウザ。"""
436 settingsChanged = Signal()
437 shiftEditRequested = Signal(int, str)
438 # 旧バージョンとの互換用。新UIでは使用しない。
439 transformEditRequested = Signal(int)
440 canvasPositionEditRequested = Signal()
441 canvasRotationRequested = Signal(float)
442 centerCanvasRequested = Signal(float)
444 def __init__(
445 self,
446 previous_count,
447 next_count,
448 previous_opacity,
449 next_opacity,
450 previous_color,
451 next_color,
452 previous_color_enabled=True,
453 next_color_enabled=True,
454 selected_colors_only=False,
455 previous_shift_x=0.0,
456 previous_shift_y=0.0,
457 previous_rotation=0.0,
458 next_shift_x=0.0,
459 next_shift_y=0.0,
460 next_rotation=0.0,
461 previous_scale=100.0,
462 next_scale=100.0,
463 previous_levels=None,
464 next_levels=None,
465 center_percent=50.0,
466 canvas_rotation=0.0,
467 parent=None,
468 ):
469 super().__init__(parent)
470 self.setObjectName("temporaryOnionSkinSettingsBrowser")
471 self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
472 self.setMinimumWidth(320)
474 self.previous_color = QColor(previous_color)
475 self.next_color = QColor(next_color)
477 layout = QVBoxLayout(self)
478 layout.setContentsMargins(6, 6, 6, 6)
479 layout.setSpacing(3)
481 explanation = QLabel(
482 tr("X・Yシフトボタンを押すと、キャンバス上のドラッグで"
483 "上下左右へ移動できます。回転とTU/TB拡大率は"
484 "スライダーで調整します。")
485 )
486 explanation.setWordWrap(True)
487 layout.addWidget(explanation)
489 self.selected_colors_only = QCheckBox(
490 tr("選択した使用色のみ表示")
491 )
492 self.selected_colors_only.setChecked(
493 bool(selected_colors_only)
494 )
495 self.selected_colors_only.setToolTip(
496 tr("使用色パネルで親・子として選択している色だけを"
497 "オニオンスキンに表示します。")
498 )
499 layout.addWidget(self.selected_colors_only)
501 self.tabs = QTabWidget()
502 self.tabs.setSizePolicy(
503 QSizePolicy.Policy.Expanding,
504 QSizePolicy.Policy.Maximum,
505 )
506 layout.addWidget(self.tabs, 0)
508 previous_values = self._build_direction_page(
509 -1,
510 previous_count,
511 previous_opacity,
512 previous_color_enabled,
513 previous_shift_x,
514 previous_shift_y,
515 previous_rotation,
516 previous_scale,
517 previous_levels,
518 )
519 (
520 previous_page,
521 self.previous_count,
522 self.previous_opacity,
523 self.previous_opacity_label,
524 self.previous_opacity_graph,
525 self.previous_color_enabled,
526 self.previous_color_button,
527 self.previous_shift_x_button,
528 self.previous_shift_x,
529 self.previous_shift_y_button,
530 self.previous_shift_y,
531 self.previous_rotation_slider,
532 self.previous_rotation,
533 self.previous_scale_slider,
534 self.previous_scale,
535 self.previous_reset_button,
536 self.previous_reset_both,
537 ) = previous_values
538 self.tabs.addTab(previous_page, tr("前のコマ"))
540 next_values = self._build_direction_page(
541 1,
542 next_count,
543 next_opacity,
544 next_color_enabled,
545 next_shift_x,
546 next_shift_y,
547 next_rotation,
548 next_scale,
549 next_levels,
550 )
551 (
552 next_page,
553 self.next_count,
554 self.next_opacity,
555 self.next_opacity_label,
556 self.next_opacity_graph,
557 self.next_color_enabled,
558 self.next_color_button,
559 self.next_shift_x_button,
560 self.next_shift_x,
561 self.next_shift_y_button,
562 self.next_shift_y,
563 self.next_rotation_slider,
564 self.next_rotation,
565 self.next_scale_slider,
566 self.next_scale,
567 self.next_reset_button,
568 self.next_reset_both,
569 ) = next_values
570 self.tabs.addTab(next_page, tr("後のコマ"))
572 position_panel = QWidget()
573 position_form = QFormLayout(position_panel)
574 position_form.setContentsMargins(0, 2, 0, 0)
575 position_form.setHorizontalSpacing(4)
576 position_form.setVerticalSpacing(2)
578 self.canvas_position_button = QPushButton(tr("表示位置"))
579 self.canvas_position_button.setCheckable(True)
580 self.canvas_position_button.setToolTip(
581 tr("左ドラッグでキャンバスを移動します。"
582 "Shift+左ドラッグまたは右ドラッグで回転します。"
583 "前後のオニオンスキンは画面上の位置・回転・拡大率を"
584 "保つよう相対値へ反映されます。")
585 )
586 position_help = QLabel(
587 tr("左:移動 Shift/右:回転")
588 )
589 position_help.setWordWrap(True)
590 position_form.addRow(
591 self.canvas_position_button,
592 position_help,
593 )
595 # 表示位置にも、前後タブと同じ回転UIを用意する。
596 self.canvas_rotation_slider = OnionRotationSlider(
597 canvas_rotation
598 )
599 self.canvas_rotation = QDoubleSpinBox()
600 self.canvas_rotation.setRange(-180.0, 180.0)
601 self.canvas_rotation.setDecimals(1)
602 self.canvas_rotation.setSingleStep(0.5)
603 self.canvas_rotation.setValue(float(canvas_rotation))
604 self._compact_spinbox(
605 self.canvas_rotation,
606 width=62,
607 )
608 canvas_degree_label = QLabel("°")
609 canvas_degree_label.setFixedWidth(9)
610 self.canvas_rotation_slider.valueChanged.connect(
611 lambda value: self.canvas_rotation.setValue(
612 float(value) / 10.0
613 )
614 )
615 self.canvas_rotation.valueChanged.connect(
616 lambda value: self.canvas_rotation_slider.setValue(
617 int(round(float(value) * 10.0))
618 )
619 )
620 self.canvas_rotation.valueChanged.connect(
621 lambda value: self.canvasRotationRequested.emit(
622 float(value)
623 )
624 )
626 canvas_rotation_row = QWidget()
627 canvas_rotation_layout = QHBoxLayout(
628 canvas_rotation_row
629 )
630 canvas_rotation_layout.setContentsMargins(0, 0, 0, 0)
631 canvas_rotation_layout.setSpacing(2)
632 canvas_rotation_layout.addWidget(
633 self.canvas_rotation_slider,
634 1,
635 )
636 canvas_rotation_layout.addWidget(
637 self.canvas_rotation
638 )
639 canvas_rotation_layout.addWidget(
640 canvas_degree_label
641 )
642 position_form.addRow(
643 tr("回転"),
644 canvas_rotation_row,
645 )
647 self.center_canvas_button = QPushButton(
648 tr("キャンバスを中央位置へ")
649 )
650 self.center_canvas_button.setToolTip(
651 tr("中央%で指定した前後間の位置・回転・TU/TB拡大率を"
652 "キャンバス基準へ取り込みます。"
653 "キャンバス回転は0°へ戻し、表示用デジタルズームは"
654 "変更しません。")
655 )
657 # 中央%はボタンと同じ行へ寄せる。
658 self.center_percent_slider = QSlider(
659 Qt.Orientation.Horizontal
660 )
661 self.center_percent_slider.setRange(0, 100)
662 self.center_percent_slider.setSingleStep(1)
663 self.center_percent_slider.setValue(
664 max(0, min(100, int(round(float(center_percent)))))
665 )
666 self.center_percent_value = QSpinBox()
667 self.center_percent_value.setRange(0, 100)
668 self.center_percent_value.setValue(
669 self.center_percent_slider.value()
670 )
671 self._compact_spinbox(
672 self.center_percent_value,
673 width=54,
674 )
675 center_percent_mark = QLabel("%")
676 center_percent_mark.setFixedWidth(10)
678 self.center_percent_slider.valueChanged.connect(
679 self.center_percent_value.setValue
680 )
681 self.center_percent_value.valueChanged.connect(
682 self.center_percent_slider.setValue
683 )
684 center_row = QWidget()
685 center_layout = QHBoxLayout(center_row)
686 center_layout.setContentsMargins(0, 0, 0, 0)
687 center_layout.setSpacing(2)
688 center_layout.addWidget(QLabel(tr("中央%")))
689 center_layout.addWidget(self.center_percent_slider, 1)
690 center_layout.addWidget(self.center_percent_value)
691 center_layout.addWidget(center_percent_mark)
692 position_form.addRow(
693 self.center_canvas_button,
694 center_row,
695 )
697 layout.addWidget(position_panel)
699 outline_note = QLabel(
700 tr("オニオンスキン外周は濃度設定に関係なく、"
701 "常に100%の1px線で表示されます。")
702 )
703 outline_note.setWordWrap(True)
704 layout.addWidget(outline_note)
706 close_button = QPushButton(tr("設定ブラウザを閉じる"))
707 # 設定は別ウィンドウに入っているので、中身ではなくウィンドウごと閉じる。
708 close_button.clicked.connect(lambda: self.window().close())
709 layout.addWidget(close_button)
712 self.previous_color_button.clicked.connect(
713 lambda: self._choose_color("previous")
714 )
715 self.next_color_button.clicked.connect(
716 lambda: self._choose_color("next")
717 )
718 self.previous_reset_button.clicked.connect(
719 lambda: self._reset_shift("previous")
720 )
721 self.next_reset_button.clicked.connect(
722 lambda: self._reset_shift("next")
723 )
724 self.previous_reset_both.toggled.connect(
725 lambda checked: self._sync_reset_both(
726 self.next_reset_both,
727 checked,
728 )
729 )
730 self.next_reset_both.toggled.connect(
731 lambda checked: self._sync_reset_both(
732 self.previous_reset_both,
733 checked,
734 )
735 )
737 self.previous_shift_x_button.clicked.connect(
738 lambda: self._request_shift_edit(-1, "xy")
739 )
740 self.next_shift_x_button.clicked.connect(
741 lambda: self._request_shift_edit(1, "xy")
742 )
743 self.canvas_position_button.clicked.connect(
744 self._request_canvas_position_edit
745 )
746 self.center_canvas_button.clicked.connect(
747 lambda: self.centerCanvasRequested.emit(
748 float(self.center_percent_value.value())
749 )
750 )
752 for checkbox in (
753 self.previous_color_enabled,
754 self.next_color_enabled,
755 self.selected_colors_only,
756 ):
757 checkbox.toggled.connect(
758 lambda _checked=False: self.settingsChanged.emit()
759 )
761 self.previous_color_enabled.toggled.connect(
762 self.previous_color_button.setEnabled
763 )
764 self.next_color_enabled.toggled.connect(
765 self.next_color_button.setEnabled
766 )
767 self.previous_color_button.setEnabled(
768 self.previous_color_enabled.isChecked()
769 )
770 self.next_color_button.setEnabled(
771 self.next_color_enabled.isChecked()
772 )
773 self._refresh_color_buttons()
775 @staticmethod
776 def _compact_spinbox(spinbox, width=72):
777 """数字を隠さず、標準▲▼だけを横方向へ圧縮する。"""
778 spinbox.setFixedWidth(int(width))
779 spinbox.setButtonSymbols(
780 QAbstractSpinBox.ButtonSymbols.UpDownArrows
781 )
782 spinbox.setStyleSheet(
783 "QSpinBox,QDoubleSpinBox{"
784 "padding-left:2px;padding-right:9px;}"
785 "QSpinBox::up-button,QDoubleSpinBox::up-button{"
786 "subcontrol-origin:border;subcontrol-position:top right;"
787 "width:8px;padding:0px;margin:0px;}"
788 "QSpinBox::down-button,QDoubleSpinBox::down-button{"
789 "subcontrol-origin:border;subcontrol-position:bottom right;"
790 "width:8px;padding:0px;margin:0px;}"
791 "QSpinBox::up-arrow,QDoubleSpinBox::up-arrow,"
792 "QSpinBox::down-arrow,QDoubleSpinBox::down-arrow{"
793 "width:5px;height:4px;}"
794 )
795 editor = spinbox.lineEdit()
796 if editor is not None: 796 ↛ exitline 796 didn't return from function '_compact_spinbox' because the condition on line 796 was always true
797 editor.setTextMargins(0, 0, 7, 0)
798 editor.setAlignment(Qt.AlignmentFlag.AlignRight)
800 def _build_direction_page(
801 self,
802 direction,
803 count,
804 opacity,
805 color_enabled,
806 shift_x,
807 shift_y,
808 rotation,
809 scale,
810 levels,
811 ):
812 page = QWidget()
813 outer = QVBoxLayout(page)
814 outer.setContentsMargins(3, 3, 3, 0)
815 outer.setSpacing(2)
816 outer.setAlignment(Qt.AlignmentFlag.AlignTop)
817 form = QFormLayout()
818 form.setContentsMargins(0, 0, 0, 0)
819 form.setHorizontalSpacing(4)
820 form.setVerticalSpacing(2)
821 outer.addLayout(form)
822 page.setSizePolicy(
823 QSizePolicy.Policy.Expanding,
824 QSizePolicy.Policy.Maximum,
825 )
827 count_box = QSpinBox()
828 count_box.setRange(0, 12)
829 count_box.setValue(int(count))
830 count_box.setSuffix(tr(" 枚"))
831 form.addRow(tr("表示枚数"), count_box)
833 opacity_graph = OnionOpacityGraph(
834 count_box.value(),
835 levels,
836 reverse_order=(int(direction) < 0),
837 )
838 form.addRow(tr("コマ別濃度"), opacity_graph)
840 opacity_slider = QSlider(Qt.Orientation.Horizontal)
841 opacity_slider.setRange(1, 100)
842 opacity_slider.setValue(
843 max(1, min(100, int(round(float(opacity) * 100))))
844 )
845 opacity_label = QLabel(f"{opacity_slider.value()}%")
846 opacity_label.setFixedWidth(38)
847 opacity_row = QWidget()
848 opacity_layout = QHBoxLayout(opacity_row)
849 opacity_layout.setContentsMargins(0, 0, 0, 0)
850 opacity_layout.addWidget(opacity_slider, 1)
851 opacity_layout.addWidget(opacity_label)
852 form.addRow(tr("全体濃度"), opacity_row)
854 color_enabled_box = QCheckBox(tr("表示色を適用"))
855 color_enabled_box.setChecked(bool(color_enabled))
856 color_button = QPushButton()
857 color_row = QWidget()
858 color_layout = QHBoxLayout(color_row)
859 color_layout.setContentsMargins(0, 0, 0, 0)
860 color_layout.addWidget(color_enabled_box)
861 color_layout.addWidget(color_button, 1)
862 form.addRow(tr("表示色"), color_row)
864 shift_xy_button = QPushButton(tr("X・Yシフト"))
865 shift_xy_button.setCheckable(True)
866 shift_xy_button.setToolTip(
867 tr("押した後、キャンバス上を上下左右へドラッグして"
868 "X・Yシフトを同時に調整します。")
869 )
871 shift_x_box = QSpinBox()
872 shift_x_box.setRange(-10000, 10000)
873 shift_x_box.setSingleStep(1)
874 shift_x_box.setValue(
875 max(-10000, min(10000, int(round(float(shift_x)))))
876 )
877 self._compact_spinbox(shift_x_box, width=66)
879 shift_y_box = QSpinBox()
880 shift_y_box.setRange(-10000, 10000)
881 shift_y_box.setSingleStep(1)
882 shift_y_box.setValue(
883 max(-10000, min(10000, int(round(float(shift_y)))))
884 )
885 self._compact_spinbox(shift_y_box, width=66)
887 shift_row = QWidget()
888 shift_layout = QHBoxLayout(shift_row)
889 shift_layout.setContentsMargins(0, 0, 0, 0)
890 shift_layout.setSpacing(2)
891 shift_layout.addWidget(QLabel("X"))
892 shift_layout.addWidget(shift_x_box)
893 shift_layout.addWidget(QLabel("Y"))
894 shift_layout.addWidget(shift_y_box)
895 shift_layout.addWidget(QLabel("px"))
896 shift_layout.addStretch(1)
897 form.addRow(shift_xy_button, shift_row)
899 # 既存の属性名との互換性を保ちつつ、同一ボタンを共有する。
900 shift_x_button = shift_xy_button
901 shift_y_button = shift_xy_button
903 rotation_slider = OnionRotationSlider(rotation)
904 rotation_box = QDoubleSpinBox()
905 rotation_box.setRange(-180.0, 180.0)
906 rotation_box.setDecimals(1)
907 rotation_box.setSingleStep(0.5)
908 rotation_box.setValue(float(rotation))
909 self._compact_spinbox(rotation_box, width=62)
910 degree_label = QLabel("°")
911 degree_label.setFixedWidth(9)
913 rotation_slider.valueChanged.connect(
914 lambda value, box=rotation_box: box.setValue(
915 float(value) / 10.0
916 )
917 )
918 rotation_box.valueChanged.connect(
919 lambda value, slider=rotation_slider: slider.setValue(
920 int(round(float(value) * 10.0))
921 )
922 )
923 rotation_row = QWidget()
924 rotation_layout = QHBoxLayout(rotation_row)
925 rotation_layout.setContentsMargins(0, 0, 0, 0)
926 rotation_layout.setSpacing(2)
927 rotation_layout.addWidget(rotation_slider, 1)
928 rotation_layout.addWidget(rotation_box)
929 rotation_layout.addWidget(degree_label)
930 form.addRow(tr("回転"), rotation_row)
932 # TU/TB拡大率。100%が中央、1%刻み、Shiftで3倍速。
933 scale_slider = OnionScaleSlider(scale)
934 scale_box = QSpinBox()
935 scale_box.setRange(1, 199)
936 scale_box.setSingleStep(1)
937 scale_box.setValue(scale_slider.value())
938 self._compact_spinbox(scale_box, width=54)
939 percent_label = QLabel("%")
940 percent_label.setFixedWidth(10)
941 scale_slider.valueChanged.connect(scale_box.setValue)
942 scale_box.valueChanged.connect(scale_slider.setValue)
944 scale_row = QWidget()
945 scale_layout = QHBoxLayout(scale_row)
946 scale_layout.setContentsMargins(0, 0, 0, 0)
947 scale_layout.setSpacing(2)
948 scale_layout.addWidget(scale_slider, 1)
949 scale_layout.addWidget(scale_box)
950 scale_layout.addWidget(percent_label)
951 form.addRow(tr("拡大率"), scale_row)
953 reset_button = QPushButton(tr("表示をリセット"))
954 reset_both_box = QCheckBox(tr("前後"))
955 reset_both_box.setToolTip(
956 tr("チェックすると、前後両方の位置・回転・"
957 "拡大率を一度に初期値へ戻します。")
958 )
959 reset_row = QWidget()
960 reset_layout = QHBoxLayout(reset_row)
961 reset_layout.setContentsMargins(0, 0, 0, 0)
962 reset_layout.setSpacing(3)
963 reset_layout.addWidget(reset_button, 1)
964 reset_layout.addWidget(reset_both_box)
965 form.addRow(reset_row)
967 count_box.valueChanged.connect(opacity_graph.set_count)
968 count_box.valueChanged.connect(
969 lambda _value: self.settingsChanged.emit()
970 )
971 opacity_graph.levelsChanged.connect(
972 self.settingsChanged
973 )
974 opacity_slider.valueChanged.connect(
975 lambda value, label=opacity_label: (
976 label.setText(f"{int(value)}%"),
977 self.settingsChanged.emit(),
978 )
979 )
980 color_enabled_box.toggled.connect(
981 lambda _checked=False: self.settingsChanged.emit()
982 )
983 shift_x_box.valueChanged.connect(
984 lambda _value: self.settingsChanged.emit()
985 )
986 shift_y_box.valueChanged.connect(
987 lambda _value: self.settingsChanged.emit()
988 )
989 rotation_box.valueChanged.connect(
990 lambda _value: self.settingsChanged.emit()
991 )
992 scale_box.valueChanged.connect(
993 lambda _value: self.settingsChanged.emit()
994 )
996 return (
997 page,
998 count_box,
999 opacity_slider,
1000 opacity_label,
1001 opacity_graph,
1002 color_enabled_box,
1003 color_button,
1004 shift_x_button,
1005 shift_x_box,
1006 shift_y_button,
1007 shift_y_box,
1008 rotation_slider,
1009 rotation_box,
1010 scale_slider,
1011 scale_box,
1012 reset_button,
1013 reset_both_box,
1014 )
1016 @staticmethod
1017 def _sync_reset_both(other_checkbox, checked):
1018 other_checkbox.blockSignals(True)
1019 other_checkbox.setChecked(bool(checked))
1020 other_checkbox.blockSignals(False)
1022 def _request_shift_edit(self, direction, axis="xy"):
1023 self.clear_interaction_buttons()
1024 direction = -1 if int(direction) < 0 else 1
1025 button = (
1026 self.previous_shift_x_button
1027 if direction < 0
1028 else self.next_shift_x_button
1029 )
1030 button.setChecked(True)
1031 self.shiftEditRequested.emit(direction, "xy")
1033 def _request_canvas_position_edit(self):
1034 self.clear_interaction_buttons()
1035 self.canvas_position_button.setChecked(True)
1036 self.canvasPositionEditRequested.emit()
1038 def clear_interaction_buttons(self):
1039 for button in (
1040 self.previous_shift_x_button,
1041 self.next_shift_x_button,
1042 self.canvas_position_button,
1043 ):
1044 button.blockSignals(True)
1045 button.setChecked(False)
1046 button.blockSignals(False)
1048 def previous_levels(self):
1049 return self.previous_opacity_graph.levels()
1051 def next_levels(self):
1052 return self.next_opacity_graph.levels()
1054 def set_transform_values(
1055 self,
1056 previous_x,
1057 previous_y,
1058 previous_rotation,
1059 previous_scale,
1060 next_x,
1061 next_y,
1062 next_rotation,
1063 next_scale,
1064 ):
1065 for spinbox, value in (
1066 (self.previous_shift_x, previous_x),
1067 (self.previous_shift_y, previous_y),
1068 (self.next_shift_x, next_x),
1069 (self.next_shift_y, next_y),
1070 ):
1071 integer_value = max(
1072 -10000,
1073 min(10000, int(round(float(value)))),
1074 )
1075 spinbox.blockSignals(True)
1076 spinbox.setValue(integer_value)
1077 spinbox.blockSignals(False)
1079 for spinbox, slider, value in (
1080 (
1081 self.previous_rotation,
1082 self.previous_rotation_slider,
1083 previous_rotation,
1084 ),
1085 (
1086 self.next_rotation,
1087 self.next_rotation_slider,
1088 next_rotation,
1089 ),
1090 ):
1091 angle = (
1092 (float(value) + 180.0) % 360.0
1093 ) - 180.0
1094 spinbox.blockSignals(True)
1095 slider.blockSignals(True)
1096 spinbox.setValue(angle)
1097 slider.setValue(int(round(angle * 10.0)))
1098 slider.blockSignals(False)
1099 spinbox.blockSignals(False)
1101 for spinbox, slider, value in (
1102 (
1103 self.previous_scale,
1104 self.previous_scale_slider,
1105 previous_scale,
1106 ),
1107 (
1108 self.next_scale,
1109 self.next_scale_slider,
1110 next_scale,
1111 ),
1112 ):
1113 scale_value = max(1, min(199, int(round(float(value)))))
1114 spinbox.blockSignals(True)
1115 slider.blockSignals(True)
1116 spinbox.setValue(scale_value)
1117 slider.setValue(scale_value)
1118 slider.blockSignals(False)
1119 spinbox.blockSignals(False)
1121 def set_canvas_rotation_value(self, value):
1122 angle = (
1123 (float(value) + 180.0) % 360.0
1124 ) - 180.0
1125 self.canvas_rotation.blockSignals(True)
1126 self.canvas_rotation_slider.blockSignals(True)
1127 self.canvas_rotation.setValue(angle)
1128 self.canvas_rotation_slider.setValue(
1129 int(round(angle * 10.0))
1130 )
1131 self.canvas_rotation_slider.blockSignals(False)
1132 self.canvas_rotation.blockSignals(False)
1134 def _choose_color(self, which):
1135 current = (
1136 self.previous_color
1137 if which == "previous"
1138 else self.next_color
1139 )
1140 def apply(color):
1141 if which == "previous":
1142 self.previous_color = QColor(color)
1143 else:
1144 self.next_color = QColor(color)
1145 self._refresh_color_buttons()
1146 self.settingsChanged.emit()
1148 color = ColorPickerDialog.get_color(
1149 current,
1150 self,
1151 tr("オニオンスキンの表示色"),
1152 preview=apply,
1153 )
1154 if not color.isValid():
1155 return
1156 apply(color)
1158 def _reset_shift(self, which):
1159 reset_both = (
1160 self.previous_reset_both.isChecked()
1161 if which == "previous"
1162 else self.next_reset_both.isChecked()
1163 )
1164 targets = (
1165 ("previous", "next")
1166 if reset_both
1167 else (which,)
1168 )
1170 for target in targets:
1171 if target == "previous":
1172 shift_controls = (
1173 self.previous_shift_x,
1174 self.previous_shift_y,
1175 )
1176 rotation_box = self.previous_rotation
1177 rotation_slider = (
1178 self.previous_rotation_slider
1179 )
1180 scale_box = self.previous_scale
1181 scale_slider = self.previous_scale_slider
1182 else:
1183 shift_controls = (
1184 self.next_shift_x,
1185 self.next_shift_y,
1186 )
1187 rotation_box = self.next_rotation
1188 rotation_slider = self.next_rotation_slider
1189 scale_box = self.next_scale
1190 scale_slider = self.next_scale_slider
1192 for control in shift_controls:
1193 control.blockSignals(True)
1194 control.setValue(0)
1195 control.blockSignals(False)
1197 rotation_box.blockSignals(True)
1198 rotation_slider.blockSignals(True)
1199 rotation_box.setValue(0.0)
1200 rotation_slider.setValue(0)
1201 rotation_slider.blockSignals(False)
1202 rotation_box.blockSignals(False)
1204 scale_box.blockSignals(True)
1205 scale_slider.blockSignals(True)
1206 scale_box.setValue(100)
1207 scale_slider.setValue(100)
1208 scale_slider.blockSignals(False)
1209 scale_box.blockSignals(False)
1211 self.settingsChanged.emit()
1213 def _refresh_color_buttons(self):
1214 for button, color in (
1215 (self.previous_color_button, self.previous_color),
1216 (self.next_color_button, self.next_color),
1217 ):
1218 foreground = (
1219 "#000" if color.lightness() > 145 else "#fff"
1220 )
1221 button.setText(
1222 color.name(QColor.NameFormat.HexRgb).upper()
1223 )
1224 button.setStyleSheet(
1225 f"background:{color.name()};color:{foreground};"
1226 "padding:5px;"
1227 )