Coverage for paintmaskanimator/onion.py: 62%

557 statements  

« 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 

23 

24log = get_logger(__name__) 

25 

26 

27class OnionOpacityGraph(QWidget): 

28 """表示枚数と同数の縦スライダーで、コマ別の濃度を設定する。""" 

29 

30 levelsChanged = Signal() 

31 

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) 

59 

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 ) 

74 

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() 

81 

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 

89 

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) 

106 

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 ) 

116 

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 ) 

136 

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 ) 

143 

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 ) 

153 

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 

167 

168 # 余分な伸縮領域を置かず、表示枚数分だけの幅にする。 

169 self.setFixedWidth(max(22, count * 22)) 

170 self._values = self.levels() 

171 

172 def levels(self): 

173 if self._sliders: 

174 return [int(slider.value()) for slider in self._sliders] 

175 return list(self._values) 

176 

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) 

192 

193 

194class OnionScaleSlider(QSlider): 

195 """100%を中央に置く、低速・1%刻みのTU/TB用スライダー。""" 

196 

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() 

213 

214 def _step_multiplier(self, modifiers): 

215 return ( 

216 3 

217 if modifiers & Qt.KeyboardModifier.ShiftModifier 

218 else 1 

219 ) 

220 

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) 

233 

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) 

260 

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) 

274 

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) 

296 

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) 

309 

310 

311class OnionRotationSlider(QSlider): 

312 """0.5度刻みでゆっくり動くオニオン回転スライダー。""" 

313 

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()) 

338 

339 @staticmethod 

340 def _step_units(modifiers): 

341 return ( 

342 30 

343 if modifiers & Qt.KeyboardModifier.ShiftModifier 

344 else 5 

345 ) 

346 

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) 

359 

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) 

384 

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) 

398 

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) 

419 

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) 

431 

432 

433class OnionSkinSettingsBrowser(QWidget): 

434 """オニオンスキンを即時調整する一時表示ブラウザ。""" 

435 

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) 

443 

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) 

473 

474 self.previous_color = QColor(previous_color) 

475 self.next_color = QColor(next_color) 

476 

477 layout = QVBoxLayout(self) 

478 layout.setContentsMargins(6, 6, 6, 6) 

479 layout.setSpacing(3) 

480 

481 explanation = QLabel( 

482 tr("X・Yシフトボタンを押すと、キャンバス上のドラッグで" 

483 "上下左右へ移動できます。回転とTU/TB拡大率は" 

484 "スライダーで調整します。") 

485 ) 

486 explanation.setWordWrap(True) 

487 layout.addWidget(explanation) 

488 

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) 

500 

501 self.tabs = QTabWidget() 

502 self.tabs.setSizePolicy( 

503 QSizePolicy.Policy.Expanding, 

504 QSizePolicy.Policy.Maximum, 

505 ) 

506 layout.addWidget(self.tabs, 0) 

507 

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("前のコマ")) 

539 

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("後のコマ")) 

571 

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) 

577 

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 ) 

594 

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 ) 

625 

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 ) 

646 

647 self.center_canvas_button = QPushButton( 

648 tr("キャンバスを中央位置へ") 

649 ) 

650 self.center_canvas_button.setToolTip( 

651 tr("中央%で指定した前後間の位置・回転・TU/TB拡大率を" 

652 "キャンバス基準へ取り込みます。" 

653 "キャンバス回転は0°へ戻し、表示用デジタルズームは" 

654 "変更しません。") 

655 ) 

656 

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) 

677 

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 ) 

696 

697 layout.addWidget(position_panel) 

698 

699 outline_note = QLabel( 

700 tr("オニオンスキン外周は濃度設定に関係なく、" 

701 "常に100%の1px線で表示されます。") 

702 ) 

703 outline_note.setWordWrap(True) 

704 layout.addWidget(outline_note) 

705 

706 close_button = QPushButton(tr("設定ブラウザを閉じる")) 

707 # 設定は別ウィンドウに入っているので、中身ではなくウィンドウごと閉じる。 

708 close_button.clicked.connect(lambda: self.window().close()) 

709 layout.addWidget(close_button) 

710 

711 

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 ) 

736 

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 ) 

751 

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 ) 

760 

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() 

774 

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) 

799 

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 ) 

826 

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) 

832 

833 opacity_graph = OnionOpacityGraph( 

834 count_box.value(), 

835 levels, 

836 reverse_order=(int(direction) < 0), 

837 ) 

838 form.addRow(tr("コマ別濃度"), opacity_graph) 

839 

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) 

853 

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) 

863 

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 ) 

870 

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) 

878 

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) 

886 

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) 

898 

899 # 既存の属性名との互換性を保ちつつ、同一ボタンを共有する。 

900 shift_x_button = shift_xy_button 

901 shift_y_button = shift_xy_button 

902 

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) 

912 

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) 

931 

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) 

943 

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) 

952 

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) 

966 

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 ) 

995 

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 ) 

1015 

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) 

1021 

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") 

1032 

1033 def _request_canvas_position_edit(self): 

1034 self.clear_interaction_buttons() 

1035 self.canvas_position_button.setChecked(True) 

1036 self.canvasPositionEditRequested.emit() 

1037 

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) 

1047 

1048 def previous_levels(self): 

1049 return self.previous_opacity_graph.levels() 

1050 

1051 def next_levels(self): 

1052 return self.next_opacity_graph.levels() 

1053 

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) 

1078 

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) 

1100 

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) 

1120 

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) 

1133 

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() 

1147 

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) 

1157 

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 ) 

1169 

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 

1191 

1192 for control in shift_controls: 

1193 control.blockSignals(True) 

1194 control.setValue(0) 

1195 control.blockSignals(False) 

1196 

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) 

1203 

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) 

1210 

1211 self.settingsChanged.emit() 

1212 

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 )