Coverage for paintmaskanimator/toolpanel.py: 88%

1179 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 04:50 +0000

1from PySide6.QtCore import QPoint, QSize, QTimer, Qt, Signal 

2from PySide6.QtGui import QColor, QIcon, QPainter, QPen, QPixmap 

3from PySide6.QtWidgets import ( 

4 QAbstractItemView, 

5 QMenu, 

6 QToolButton, 

7 QAbstractSpinBox, 

8 QApplication, 

9 QCheckBox, 

10 QComboBox, 

11 QFormLayout, 

12 QGridLayout, 

13 QHBoxLayout, 

14 QLabel, 

15 QLineEdit, 

16 QListView, 

17 QListWidget, 

18 QListWidgetItem, 

19 QPushButton, 

20 QSizePolicy, 

21 QSlider, 

22 QSpinBox, 

23 QVBoxLayout, 

24 QWidget, 

25) 

26from . import bucket_fill, config, icons, theme 

27from .i18n import tr 

28from .utils import on_double_click 

29from .widgets import (BrushSizeSpinBox, ClickableValueLabel, HSVColorWheel, LineTaperCurvePopup, SliderValueSpinBox, SwatchEyedropButton) 

30 

31 

32#: (tool id, source label). The id is the stable value everything branches on. 

33#: The label is *not* translated here: this runs at import time, before the 

34#: translator is installed, so a ``tr()`` at module level would freeze the source 

35#: language in. Use :func:`tool_label` wherever the name is displayed. 

36TOOL_DEFINITIONS = [ 

37 ("brush", "ブラシ"), 

38 ("shape", "図形"), ("bucket", "バケツ"), ("closed_fill", "閉領域フィル"), 

39 ("lasso_fill", "投げ縄塗り"), ("lasso", "投げ縄選択"), 

40 ("rect_select", "長方形選択"), ("auto_select", "自動選択"), 

41 ("eyedropper", "スポイト"), ("dust", "ゴミ取り"), 

42] 

43 

44 

45def tool_label(tool_id: str) -> str: 

46 """The display name for a tool, translated at call time. 

47 

48 ``pyside6-lupdate`` only sees a literal inside ``tr()``, so the names are 

49 written out here rather than beside ``TOOL_DEFINITIONS``. 

50 """ 

51 labels = { 

52 "brush": tr("ブラシ"), "line": tr("ライン"), 

53 "shape": tr("図形"), "bucket": tr("バケツ"), 

54 "closed_fill": tr("閉領域フィル"), 

55 "lasso_fill": tr("投げ縄塗り"), "lasso": tr("投げ縄選択"), 

56 "rect_select": tr("長方形選択"), "auto_select": tr("自動選択"), 

57 "eyedropper": tr("スポイト"), "dust": tr("ゴミ取り"), 

58 } 

59 return labels.get(tool_id, tool_id) 

60 

61 

62def _tool_icon(tool_id): 

63 """Return the drawing tool's line icon in the active theme's colours.""" 

64 return icons.icon(tool_id) 

65 

66 

67class SwatchStack(QWidget): 

68 """Compact drawing-colour control: stacked main/sub swatches plus a 

69 background swatch below, sized to fit the tool bar's current width. 

70 

71 Emits ``modeRequested`` with ``"main"`` / ``"sub"`` / ``"transparent"``. 

72 ``backgroundColorRequested`` fires on right-clicking the background swatch 

73 (to change the background display colour). 

74 """ 

75 modeRequested = Signal(str) 

76 backgroundColorRequested = Signal() 

77 colorEditRequested = Signal(str) 

78 

79 def __init__(self, width=52, parent=None): 

80 super().__init__(parent) 

81 self._main = QColor("black") 

82 self._sub = QColor(255, 0, 0) 

83 self._bg = QColor("white") 

84 self._mode = "main" 

85 self._columns = 2 

86 self._sub_btn = QPushButton(self) 

87 self._main_btn = QPushButton(self) 

88 self._bg_btn = QPushButton(self) 

89 for btn in (self._sub_btn, self._main_btn, self._bg_btn): 

90 btn.setCursor(Qt.CursorShape.PointingHandCursor) 

91 self._main_btn.setToolTip(tr("クリック:メイン色に切替")) 

92 self._sub_btn.setToolTip(tr("クリック:サブ色に切替")) 

93 self._bg_btn.setToolTip(tr( 

94 "クリック:背景色で描画/右クリック:背景色を変更" 

95 )) 

96 self._main_btn.clicked.connect(lambda: self.modeRequested.emit("main")) 

97 self._sub_btn.clicked.connect(lambda: self.modeRequested.emit("sub")) 

98 self._bg_btn.clicked.connect( 

99 lambda: self.modeRequested.emit("transparent") 

100 ) 

101 self._bg_btn.setContextMenuPolicy( 

102 Qt.ContextMenuPolicy.CustomContextMenu 

103 ) 

104 self._bg_btn.customContextMenuRequested.connect( 

105 lambda _p: self.backgroundColorRequested.emit() 

106 ) 

107 on_double_click(self._main_btn, lambda: self.colorEditRequested.emit("main")) 

108 on_double_click(self._sub_btn, lambda: self.colorEditRequested.emit("sub")) 

109 on_double_click(self._bg_btn, self.backgroundColorRequested.emit) 

110 self.relayout(width) 

111 

112 def relayout(self, width, columns=None): 

113 """Resize the swatch to ``width`` px (shrinks to one icon column).""" 

114 w = max(16, int(width)) 

115 if columns is not None: 

116 self._columns = max(1, int(columns)) 

117 if self._columns == 1: 

118 bg_h = max(8, round(w * 0.5)) 

119 self.setFixedSize(w, w * 2 + bg_h + 6) 

120 self._layout_buttons() 

121 self._restyle() 

122 return 

123 box = max(10, round(w * 0.64)) 

124 off = w - box 

125 self._main_btn.setGeometry(0, 0, box, box) 

126 self._sub_btn.setGeometry(off, off, box, box) 

127 gap = 3 

128 bg_h = max(8, round(w * 0.26)) 

129 self._bg_btn.setGeometry(0, w + gap, w, bg_h) 

130 self.setFixedSize(w, w + gap + bg_h) 

131 self._restyle() 

132 

133 def _layout_buttons(self): 

134 if self._columns != 1: 

135 return 

136 w = self.width() 

137 gap = 3 

138 bg_h = max(8, round(w * 0.5)) 

139 # メインは常に上、サブは常に下。どちらを選んでも並びは変えず、 

140 # 選択中の色は枠で示す(入れ替わるとどちらがメインか分からなくなる)。 

141 self._main_btn.setGeometry(0, 0, w, w) 

142 self._sub_btn.setGeometry(0, w + gap, w, w) 

143 # The background colour is a fixed, shorter strip below the two 

144 # drawing colours. Selecting it must not reorder the stack. 

145 self._bg_btn.setGeometry(0, 2 * (w + gap), w, bg_h) 

146 

147 def set_colors(self, main, sub, mode, background=None): 

148 self._main = QColor(main) 

149 self._sub = QColor(sub) 

150 if background is not None: 150 ↛ 152line 150 didn't jump to line 152 because the condition on line 150 was always true

151 self._bg = QColor(background) 

152 self._mode = mode 

153 self._restyle() 

154 

155 def _restyle(self): 

156 self._layout_buttons() 

157 accent = theme.accent() 

158 def style(c, selected): 

159 border = ( 

160 f"2px solid {accent}" if selected else "1px solid #202020" 

161 ) 

162 return ( 

163 f"QPushButton{{background:{c.name()};border:{border};" 

164 "border-radius:4px;}" 

165 ) 

166 self._sub_btn.setStyleSheet(style(self._sub, self._mode == "sub")) 

167 self._main_btn.setStyleSheet(style(self._main, self._mode == "main")) 

168 self._bg_btn.setStyleSheet( 

169 style(self._bg, self._mode == "transparent") 

170 ) 

171 # 重ね表示でも前後関係は固定(メインが手前)。選択は枠で示す。 

172 self._main_btn.raise_() 

173 

174 

175class ToolSelectorPanel(QWidget): 

176 """Narrow icon-only tool selector; options live in ``ToolPanel``.""" 

177 toolChanged = Signal(str) 

178 snapWidthRequested = Signal(int) 

179 colorModeRequested = Signal(str) 

180 backgroundColorRequested = Signal() 

181 colorEditRequested = Signal(str) 

182 TOOLS = TOOL_DEFINITIONS 

183 

184 CELL_SIZE = 30 

185 ICON_SIZE = 20 

186 PANEL_MARGIN = 2 

187 # QListView's wrapping check is strict at an exact N * gridSize boundary. 

188 # One shared pixel (not one per cell) keeps the final cell on the row. 

189 LAYOUT_SLACK = 1 

190 

191 def __init__(self): 

192 super().__init__() 

193 self.items = {} 

194 self.active_tool = "brush" 

195 self._column_count = 1 

196 self._swatch_column_count = 1 

197 self._swatch_sync_timer = QTimer(self) 

198 self._swatch_sync_timer.setSingleShot(True) 

199 self._swatch_sync_timer.setInterval(0) 

200 self._swatch_sync_timer.timeout.connect( 

201 self.sync_swatch_to_displayed_columns 

202 ) 

203 self.setMinimumWidth(self.width_for_columns(1)) 

204 self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) 

205 layout = QVBoxLayout(self) 

206 layout.setContentsMargins( 

207 self.PANEL_MARGIN, self.PANEL_MARGIN, 

208 self.PANEL_MARGIN, self.PANEL_MARGIN, 

209 ) 

210 layout.setSpacing(0) 

211 self.list = QListWidget() 

212 self.list.setViewMode(QListView.ViewMode.IconMode) 

213 self.list.setFlow(QListView.Flow.LeftToRight) 

214 self.list.setWrapping(True) 

215 self.list.setResizeMode(QListView.ResizeMode.Adjust) 

216 # Free movement + internal move so tool icons can be reordered by drag. 

217 self.list.setMovement(QListView.Movement.Snap) 

218 self.list.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) 

219 self.list.setDefaultDropAction(Qt.DropAction.MoveAction) 

220 self.list.setSelectionMode(QListView.SelectionMode.SingleSelection) 

221 self.list.setSizePolicy( 

222 QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Expanding 

223 ) 

224 self.list.setIconSize(QSize(self.ICON_SIZE, self.ICON_SIZE)) 

225 self.list.setGridSize(QSize(self.CELL_SIZE, self.CELL_SIZE)) 

226 self.list.setSpacing(0) 

227 self.list.setFrameStyle(0) 

228 self.list.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 

229 self.list.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 

230 self._apply_list_theme() 

231 layout.addWidget(self.list, 1) 

232 # Drawing-colour swatch (main/sub/background) at the BOTTOM of the bar. 

233 # Its width tracks the icon column count (shrinks to one column). 

234 self.color_swatch = SwatchStack(self.CELL_SIZE * 2 - 8) 

235 self.color_swatch.modeRequested.connect(self.colorModeRequested) 

236 self.color_swatch.backgroundColorRequested.connect( 

237 self.backgroundColorRequested 

238 ) 

239 self.color_swatch.colorEditRequested.connect(self.colorEditRequested) 

240 self._swatch_holder = QWidget() 

241 swatch_row = QHBoxLayout(self._swatch_holder) 

242 swatch_row.setContentsMargins(0, 4, 0, 2) 

243 swatch_row.addStretch(1) 

244 swatch_row.addWidget(self.color_swatch) 

245 swatch_row.addStretch(1) 

246 layout.addWidget(self._swatch_holder) 

247 for tool_id, _source_label in self.TOOLS: 

248 label = tool_label(tool_id) 

249 item = QListWidgetItem(_tool_icon(tool_id), "") 

250 item.setData(Qt.ItemDataRole.UserRole, tool_id) 

251 item.setData(Qt.ItemDataRole.ToolTipRole, label) 

252 item.setData(Qt.ItemDataRole.AccessibleTextRole, label) 

253 item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) 

254 self.list.addItem(item) 

255 self.items[tool_id] = item 

256 self.list.currentItemChanged.connect(self._current_item_changed) 

257 # InternalMove drag-reorder recreates QListWidgetItems on drop, so the 

258 # references in self.items go stale. Rebuild the map when rows change. 

259 self.list.model().rowsInserted.connect(self._rebuild_item_map) 

260 self.set_active_tool("brush") 

261 self._resize_swatch_to_columns() 

262 

263 def _rebuild_item_map(self, *args): 

264 rebuilt = {} 

265 for index in range(self.list.count()): 

266 item = self.list.item(index) 

267 tool_id = item.data(Qt.ItemDataRole.UserRole) 

268 if tool_id: 

269 rebuilt[tool_id] = item 

270 if rebuilt: 

271 self.items = rebuilt 

272 # Restore the active highlight on the (possibly new) item. 

273 active = self.items.get(self.active_tool) 

274 if active is not None: 

275 self.list.blockSignals(True) 

276 self.list.setCurrentItem(active) 

277 self.list.blockSignals(False) 

278 

279 def _apply_list_theme(self): 

280 c = theme.palette() 

281 self.list.setStyleSheet( 

282 "QListWidget{background:transparent;outline:0;border:0;}" 

283 "QListWidget::item{margin:1px;border:1px solid transparent;" 

284 "border-radius:7px;}" 

285 f"QListWidget::item:hover{{background:{c['hover']};" 

286 "border-color:transparent;}" 

287 f"QListWidget::item:selected{{background:{c['selection']};" 

288 f"border:1px solid {c['accent']};}}" 

289 f"QListWidget::item:selected:hover{{background:{c['selection']};" 

290 f"border-color:{c['accent']};}}" 

291 ) 

292 

293 def apply_theme(self): 

294 """Re-apply palette-derived styling after a theme/accent change.""" 

295 self._apply_list_theme() 

296 for tool_id, item in self.items.items(): 

297 item.setIcon(_tool_icon(tool_id)) 

298 self.color_swatch._restyle() 

299 

300 def set_swatch_colors(self, main, sub, mode, background=None): 

301 self.color_swatch.set_colors(main, sub, mode, background) 

302 

303 def _resize_swatch_to_columns(self, columns=None): 

304 """Size the drawing-colour swatch to the current icon column count.""" 

305 if columns is None: 

306 columns = self._column_count 

307 columns = max(1, int(columns)) 

308 self._swatch_column_count = columns 

309 # Fit within the column band, capped so a wide bar stays reasonable. 

310 swatch_w = min( 

311 columns * self.CELL_SIZE - 6, 

312 self.CELL_SIZE * 3, 

313 ) 

314 swatch_w = max(18, swatch_w) 

315 self.color_swatch.relayout(swatch_w, columns) 

316 

317 def sync_swatch_to_displayed_columns(self): 

318 """Match the swatch to the columns the icon view actually rendered.""" 

319 columns = self.displayed_column_count() 

320 if columns == self._swatch_column_count: 

321 return 

322 self._resize_swatch_to_columns(columns) 

323 

324 def minimumSizeHint(self): 

325 return QSize(self.width_for_columns(1), 0) 

326 

327 def sizeHint(self): 

328 return QSize(self.width_for_columns(1), self.CELL_SIZE * len(self.items)) 

329 

330 @classmethod 

331 def width_for_columns(cls, columns): 

332 columns = max(1, int(columns)) 

333 return ( 

334 cls.PANEL_MARGIN * 2 

335 + columns * cls.CELL_SIZE 

336 + cls.LAYOUT_SLACK 

337 ) 

338 

339 def resizeEvent(self, event): 

340 super().resizeEvent(event) 

341 available_width = max( 

342 1, 

343 self.width() - self.PANEL_MARGIN * 2 - self.LAYOUT_SLACK, 

344 ) 

345 columns = max( 

346 1, 

347 min( 

348 len(self.items), 

349 (available_width + self.CELL_SIZE // 2) // self.CELL_SIZE, 

350 ), 

351 ) 

352 self._column_count = columns 

353 target_width = self.width_for_columns(columns) 

354 if self.width() != target_width: 

355 self.snapWidthRequested.emit(target_width) 

356 # QListWidget lays its items out after the parent resize. Recheck on 

357 # the next event-loop turn so the colour control changes at exactly 

358 # the same threshold as the visible icon columns. 

359 self._swatch_sync_timer.start() 

360 

361 def displayed_column_count(self): 

362 """Return the number of icons Qt actually placed on the first row.""" 

363 if not self.items: 363 ↛ 364line 363 didn't jump to line 364 because the condition on line 363 was never true

364 return 1 

365 self.list.doItemsLayout() 

366 first_rect = self.list.visualItemRect(self.list.item(0)) 

367 first_y = first_rect.y() 

368 columns = 0 

369 for index in range(self.list.count()): 369 ↛ 374line 369 didn't jump to line 374 because the loop on line 369 didn't complete

370 rect = self.list.visualItemRect(self.list.item(index)) 

371 if rect.y() != first_y: 

372 break 

373 columns += 1 

374 return max(1, columns) 

375 

376 def set_active_tool(self, tool_id): 

377 item = self.items.get(tool_id) 

378 if item is None: 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true

379 return 

380 self.active_tool = tool_id 

381 self.list.blockSignals(True) 

382 self.list.setCurrentItem(item) 

383 self.list.blockSignals(False) 

384 

385 def _current_item_changed(self, current, _previous): 

386 if current is None: 

387 return 

388 tool_id = current.data(Qt.ItemDataRole.UserRole) 

389 if tool_id == self.active_tool: 

390 return 

391 self.active_tool = tool_id 

392 self.toolChanged.emit(tool_id) 

393 

394 def select_tool(self, tool_id): 

395 item = self.items.get(tool_id) 

396 if item is None: 

397 return 

398 if tool_id == self.active_tool: 

399 self.list.setCurrentItem(item) 

400 return 

401 self.list.setCurrentItem(item) 

402 

403 

404class IncludeColorSlots(QWidget): 

405 """バケツの含み塗りの対象色を登録しておく、数色分の枠。 

406 

407 枠をクリックすると現在の描画色を登録し、右クリックで消せる。登録した色は 

408 設定に保存して次回も使う。1色も登録していなければサブカラーを含む 

409 (以前の「サブカラーを含み塗り」と同じ動作)。 

410 

411 登録した色は枠ごとに ON/OFF できる(Issue #43)。色トレス線が重なっている 

412 ところを塗るとき、含む色を消さずに切り換えられるようにするため。OFF の枠は 

413 斜線を重ねて見分ける。 

414 """ 

415 

416 SLOT_COUNT = 3 

417 CONFIG_KEY = "bucket_include_colors" 

418 ENABLED_CONFIG_KEY = "bucket_include_colors_enabled" 

419 SLOT_SIZE = 22 

420 

421 # 枠へ色を登録した(含み塗りを自動でONにするため)。 

422 colorRegistered = Signal() 

423 changed = Signal() 

424 

425 def __init__(self, current_color, parent=None): 

426 """``current_color`` は登録する描画色を返す関数(``None`` なら登録しない)。""" 

427 super().__init__(parent) 

428 self._current_color = current_color 

429 self._colors: list = [None] * self.SLOT_COUNT 

430 self._enabled: list = [True] * self.SLOT_COUNT 

431 layout = QHBoxLayout(self) 

432 layout.setContentsMargins(18, 0, 0, 0) 

433 layout.setSpacing(4) 

434 self.title = QLabel(tr("含む色")) 

435 layout.addWidget(self.title) 

436 self.buttons = [] 

437 for index in range(self.SLOT_COUNT): 

438 button = QToolButton() 

439 button.setFixedSize(self.SLOT_SIZE, self.SLOT_SIZE) 

440 button.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 

441 button.clicked.connect( 

442 lambda _checked=False, i=index: self.register_current(i) 

443 ) 

444 button.customContextMenuRequested.connect( 

445 lambda pos, i=index: self._show_menu(i, pos) 

446 ) 

447 layout.addWidget(button) 

448 self.buttons.append(button) 

449 layout.addStretch(1) 

450 self._load() 

451 self._refresh() 

452 

453 def colors(self): 

454 """含み塗りに使う RGB タプル(空きと OFF の枠は除く)。""" 

455 return tuple( 

456 color 

457 for color, enabled in zip(self._colors, self._enabled) 

458 if color is not None and enabled 

459 ) 

460 

461 def registered_colors(self): 

462 """ON/OFF に関わらず登録してある RGB タプル。""" 

463 return tuple(color for color in self._colors if color is not None) 

464 

465 def is_enabled(self, index): 

466 return bool(0 <= index < self.SLOT_COUNT and self._enabled[index]) 

467 

468 def set_enabled(self, index, enabled): 

469 """枠ごとの ON/OFF。色は消さずに含み塗りの対象から外す。""" 

470 if not 0 <= index < self.SLOT_COUNT: 470 ↛ 471line 470 didn't jump to line 471 because the condition on line 470 was never true

471 return 

472 self._enabled[index] = bool(enabled) 

473 self._save() 

474 self._refresh() 

475 self.changed.emit() 

476 

477 def set_color(self, index, rgb): 

478 if not 0 <= index < self.SLOT_COUNT: 478 ↛ 479line 478 didn't jump to line 479 because the condition on line 478 was never true

479 return 

480 self._colors[index] = ( 

481 None if rgb is None 

482 else (int(rgb[0]), int(rgb[1]), int(rgb[2])) 

483 ) 

484 # 色を入れ替えたら、使う状態で始める。 

485 self._enabled[index] = True 

486 self._save() 

487 self._refresh() 

488 self.changed.emit() 

489 

490 def register_current(self, index): 

491 color = self._current_color() 

492 if color is None: 492 ↛ 493line 492 didn't jump to line 493 because the condition on line 492 was never true

493 return 

494 color = QColor(color) 

495 self.set_color(index, (color.red(), color.green(), color.blue())) 

496 self.colorRegistered.emit() 

497 

498 def _show_menu(self, index, pos): 

499 menu = QMenu(self) 

500 use = menu.addAction(tr("この色を含み塗りに使う")) 

501 use.setCheckable(True) 

502 use.setChecked(self._enabled[index]) 

503 use.setEnabled(self._colors[index] is not None) 

504 use.toggled.connect( 

505 lambda checked, i=index: self.set_enabled(i, checked) 

506 ) 

507 menu.addSeparator() 

508 menu.addAction( 

509 tr("現在の描画色を登録"), lambda: self.register_current(index) 

510 ) 

511 clear = menu.addAction(tr("消去"), lambda: self.set_color(index, None)) 

512 clear.setEnabled(self._colors[index] is not None) 

513 menu.exec(self.buttons[index].mapToGlobal(pos)) 

514 

515 def _load(self): 

516 stored = config.get_value(self.CONFIG_KEY, []) 

517 if isinstance(stored, list): 517 ↛ 526line 517 didn't jump to line 526 because the condition on line 517 was always true

518 for index, value in enumerate(stored[: self.SLOT_COUNT]): 

519 if ( 

520 isinstance(value, (list, tuple)) 

521 and len(value) == 3 

522 and all(isinstance(v, int) and 0 <= v <= 255 for v in value) 

523 ): 

524 self._colors[index] = tuple(value) 

525 # ON/OFF を持たない古い設定では、登録済みの色をすべて使う。 

526 stored_enabled = config.get_value(self.ENABLED_CONFIG_KEY, None) 

527 if isinstance(stored_enabled, list): 

528 for index, value in enumerate(stored_enabled[: self.SLOT_COUNT]): 

529 self._enabled[index] = bool(value) 

530 

531 def _save(self): 

532 config.set_value( 

533 self.CONFIG_KEY, 

534 [list(color) if color is not None else None for color in self._colors], 

535 ) 

536 config.set_value( 

537 self.ENABLED_CONFIG_KEY, [bool(value) for value in self._enabled] 

538 ) 

539 

540 def _refresh(self): 

541 c = theme.palette() 

542 for button, color in zip(self.buttons, self._colors): 

543 if color is None: 

544 button.setStyleSheet( 

545 "QToolButton{background:transparent;" 

546 f"border:1px dashed {c['text_muted']};border-radius:4px;}}" 

547 f"QToolButton:hover{{border-color:{c['accent']};}}" 

548 ) 

549 button.setToolTip(tr( 

550 "クリックで現在の描画色を含み塗りの色に登録します。" 

551 )) 

552 else: 

553 name = QColor(*color).name() 

554 index = self.buttons.index(button) 

555 if self._enabled[index]: 

556 button.setStyleSheet( 

557 f"QToolButton{{background:{name};" 

558 f"border:1px solid {c['border']};border-radius:4px;}}" 

559 ) 

560 button.setToolTip(tr( 

561 "{code}\nクリックで現在の描画色に置き換え、" 

562 "右クリックで ON/OFF と消去ができます。" 

563 ).format(code=name.upper())) 

564 else: 

565 # OFF:色は残したまま、斜線を重ねて使っていないことを示す。 

566 button.setStyleSheet( 

567 "QToolButton{background:qlineargradient(" 

568 "x1:0,y1:0,x2:1,y2:1," 

569 f"stop:0 {name},stop:0.42 {name}," 

570 f"stop:0.43 {c['text_muted']}," 

571 f"stop:0.57 {c['text_muted']}," 

572 f"stop:0.58 {name},stop:1 {name});" 

573 f"border:1px dashed {c['text_muted']};border-radius:4px;}}" 

574 ) 

575 button.setToolTip(tr( 

576 "{code}(含み塗りに使っていません)\n" 

577 "右クリックで ON に戻せます。" 

578 ).format(code=name.upper())) 

579 

580 

581class ToolPanel(QWidget): 

582 #: カラーサークル上部のメイン/サブ重ねスウォッチの一辺(px)。 

583 COLOR_WELL_SIZE = 64 

584 

585 toolChanged = Signal(str) 

586 colorModeChanged = Signal(str) 

587 colorChanged = Signal(str, QColor) 

588 meshCommitRequested = Signal() 

589 meshCancelRequested = Signal() 

590 selectionTransformRequested = Signal() 

591 selectionScaleRequested = Signal() 

592 selectionMeshRequested = Signal() 

593 selectionClearRequested = Signal() 

594 selectionRotateRequested = Signal(float) 

595 transformMeshGridChanged = Signal(int, int) 

596 selectionCommitRequested = Signal() 

597 selectionCancelRequested = Signal() 

598 flipLayerRequested = Signal(bool) 

599 swapMainSubRequested = Signal() 

600 resetMainSubRequested = Signal() 

601 isolateColorRequested = Signal() 

602 removeDustRequested = Signal() 

603 backgroundColorRequested = Signal() 

604 #: 色見本・カラーサークルのダブルクリックで、その色の編集を求める(main / sub)。 

605 colorEditRequested = Signal(str) 

606 clearColorFilterRequested = Signal() 

607 TOOLS = TOOL_DEFINITIONS 

608 

609 def _sync_shape_corner_controls(self, visible=True): 

610 """Corner count applies to polygons only; keyed on the item data, not its label.""" 

611 enabled = visible and self.shape_type.currentData() == "polygon" 

612 self.shape_corners_label.setEnabled(enabled) 

613 self.shape_corners.setEnabled(enabled) 

614 self.shape_corners_slider.setEnabled(enabled) 

615 

616 def __init__(self): 

617 super().__init__(); self.setFixedWidth(190); self.active_tool="brush" 

618 self._line_curve_popup = None 

619 self.main_color=QColor("black"); self.sub_color=QColor(255,0,0); self.color_mode="main"; self.transparent_display_color=QColor("white") 

620 v=QVBoxLayout(self) 

621 v.setContentsMargins(4, 4, 4, 4) 

622 v.setSpacing(3) 

623 # パネル名はドックのタブに出ているので、ここでは使用中のツール名だけを 

624 # 見出しとして出す(以前はアクセント色の帯で、画面の中で一番目立っていた)。 

625 self.active=QLabel() 

626 self.active.setAlignment( 

627 Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter 

628 ) 

629 v.addWidget(self.active) 

630 

631 # Context commands: only commands required by the selected tool are shown. 

632 self.command_box=QWidget(); self.command_layout=QVBoxLayout(self.command_box) 

633 self.command_layout.setContentsMargins(0,2,0,2) 

634 self.command_layout.setSpacing(3) 

635 self.command_title=QLabel(tr("<b>ツールコマンド</b>")); self.command_layout.addWidget(self.command_title) 

636 self.mesh_commit=QPushButton(tr("変形を確定")) 

637 self.mesh_cancel=QPushButton(tr("変形をキャンセル")) 

638 self.flip_h=QPushButton(tr("左右反転")) 

639 self.flip_v=QPushButton(tr("上下反転")) 

640 self.selection_transform=QPushButton(tr("自由変形")) 

641 self.selection_scale=QPushButton(tr("拡大縮小")) 

642 self.selection_mesh=QPushButton(tr("メッシュ変形")) 

643 self.selection_clear=QPushButton(tr("選択範囲を解除")) 

644 self.selection_clear.setToolTip(tr( 

645 "選択ツール使用時に、現在の選択範囲を解除します。" 

646 "どのツールからでも右上の「選択解除」を使用できます。" 

647 )) 

648 self.selection_rotate_left=QPushButton(tr("左へ90°回転")) 

649 self.selection_rotate_right=QPushButton(tr("右へ90°回転")) 

650 self.transform_mesh_grid_label=QLabel(tr("メッシュ格子数")) 

651 self.transform_mesh_grid_x=QSpinBox() 

652 self.transform_mesh_grid_y=QSpinBox() 

653 for spin in (self.transform_mesh_grid_x, self.transform_mesh_grid_y): 

654 spin.setRange(2, 12) 

655 spin.setValue(4) 

656 spin.setToolTip(tr("メッシュ変形中でも格子数を変更できます。")) 

657 self.transform_mesh_grid_x.setPrefix(tr("横 ")) 

658 self.transform_mesh_grid_y.setPrefix(tr("縦 ")) 

659 self.transform_mesh_grid_x_slider = QSlider(Qt.Orientation.Horizontal) 

660 self.transform_mesh_grid_x_slider.setRange(2, 12) 

661 self.transform_mesh_grid_x_slider.setValue(4) 

662 self.transform_mesh_grid_y_slider = QSlider(Qt.Orientation.Horizontal) 

663 self.transform_mesh_grid_y_slider.setRange(2, 12) 

664 self.transform_mesh_grid_y_slider.setValue(4) 

665 self.transform_mesh_grid_x_slider.valueChanged.connect( 

666 self.transform_mesh_grid_x.setValue 

667 ) 

668 self.transform_mesh_grid_y_slider.valueChanged.connect( 

669 self.transform_mesh_grid_y.setValue 

670 ) 

671 self.transform_mesh_grid_x.valueChanged.connect( 

672 self.transform_mesh_grid_x_slider.setValue 

673 ) 

674 self.transform_mesh_grid_y.valueChanged.connect( 

675 self.transform_mesh_grid_y_slider.setValue 

676 ) 

677 self.selection_scale.clicked.connect(self.selectionScaleRequested) 

678 self.selection_mesh.clicked.connect(self.selectionMeshRequested) 

679 self.selection_transform.clicked.connect(self.selectionTransformRequested) 

680 self.selection_clear.clicked.connect(self.selectionClearRequested) 

681 self.selection_rotate_left.clicked.connect(lambda: self.selectionRotateRequested.emit(-90.0)) 

682 self.selection_rotate_right.clicked.connect(lambda: self.selectionRotateRequested.emit(90.0)) 

683 self.transform_mesh_grid_x.valueChanged.connect( 

684 lambda _value: self.transformMeshGridChanged.emit( 

685 self.transform_mesh_grid_x.value(), 

686 self.transform_mesh_grid_y.value(), 

687 ) 

688 ) 

689 self.transform_mesh_grid_y.valueChanged.connect( 

690 lambda _value: self.transformMeshGridChanged.emit( 

691 self.transform_mesh_grid_x.value(), 

692 self.transform_mesh_grid_y.value(), 

693 ) 

694 ) 

695 self.selection_all_frames=QCheckBox(tr("すべてのコマに適用")) 

696 self.selection_all_frames.setChecked(False) 

697 

698 self.transform_quality=QCheckBox(tr("クオリティ(Tp_mask v0.7方式)")) 

699 self.transform_quality.setChecked(True) 

700 self.transform_quality.setToolTip(tr( 

701 "色ごとに分離して変形し、中間色を作らずに再合成します。" 

702 "拡大・縮小・回転で線や塗りが崩れにくくなります。" 

703 )) 

704 self.transform_line_width_note=QLabel(tr( 

705 "色選択があるときは実線の太さを調整できます。" 

706 )) 

707 self.transform_line_width_note.setWordWrap(True) 

708 self.transform_line_width_note.setStyleSheet( 

709 "color:palette(placeholder-text);padding-left:4px;padding-right:4px;" 

710 ) 

711 self.transform_line_width_label=QLabel( 

712 tr("実線の太さ:{value}").format(value=159) 

713 ) 

714 self.transform_line_width=QSlider(Qt.Orientation.Horizontal) 

715 self.transform_line_width.setRange(1, 254) 

716 self.transform_line_width.setValue(96) 

717 self.transform_line_width.setInvertedAppearance(True) 

718 self.transform_line_width.setToolTip(tr( 

719 "クオリティ変形で、使用色パネルの選択色を実線として残す太さを調整します。" 

720 "右へ動かすほど太くなります。" 

721 )) 

722 self.transform_line_width.valueChanged.connect( 

723 lambda value: self.transform_line_width_label.setText( 

724 tr("実線の太さ:{value}").format(value=255 - value) 

725 ) 

726 ) 

727 self._has_transform_line_colors=False 

728 self._tween_active=False 

729 self.transform_quality.toggled.connect(self._sync_transform_quality_options) 

730 self._sync_transform_quality_options(True) 

731 self._refresh_transform_line_width_visibility() 

732 

733 # 参照:どの絵を見て塗る範囲を決めるか。塗る先はアクティブレイヤーのまま。 

734 self.bucket_reference = QComboBox() 

735 for label, value in ( 

736 (tr("選択したレイヤーのみ"), bucket_fill.REFERENCE_LAYER), 

737 (tr("キャンバス全体"), bucket_fill.REFERENCE_CANVAS), 

738 (tr("色塗り前の初期状態"), bucket_fill.REFERENCE_INITIAL), 

739 ): 

740 self.bucket_reference.addItem(label, value) 

741 self.bucket_reference.setToolTip(tr( 

742 "どの絵を見て塗る範囲を決めるかを選びます。塗る先はいつも選択中の" 

743 "レイヤーです。\n" 

744 "「選択したレイヤーのみ」はそのレイヤーの絵だけで判定します。\n" 

745 "「キャンバス全体」は表示しているレイヤーを合成した絵で判定するので、" 

746 "線が別のレイヤーにあっても、その線で区切られた部分を塗れます。\n" 

747 "「色塗り前の初期状態」は保持した塗る前の絵で判定するので、" 

748 "塗りかけの色に邪魔されません(保持していなければ今の絵で判定します)。" 

749 )) 

750 self.bucket_reference_row = QWidget() 

751 bucket_reference_layout = QHBoxLayout(self.bucket_reference_row) 

752 bucket_reference_layout.setContentsMargins(0, 0, 0, 0) 

753 bucket_reference_layout.setSpacing(4) 

754 bucket_reference_layout.addWidget(QLabel(tr("参照"))) 

755 bucket_reference_layout.addWidget(self.bucket_reference, 1) 

756 

757 # 対象色:どの画素をたどって塗るか(CLIP STUDIO の「対象色」と同じ選択肢)。 

758 self.bucket_target_color = QComboBox() 

759 for label, value in ( 

760 (tr("すべての色を対象"), bucket_fill.TARGET_ALL), 

761 (tr("透明部分のみ"), bucket_fill.TARGET_TRANSPARENT), 

762 (tr("透明に囲まれた部分"), bucket_fill.TARGET_SURROUNDED_BY_TRANSPARENT), 

763 (tr("黒部分のみ"), bucket_fill.TARGET_BLACK), 

764 (tr("黒に囲まれた部分"), bucket_fill.TARGET_SURROUNDED_BY_BLACK), 

765 (tr("白と透明部分のみ"), bucket_fill.TARGET_WHITE_TRANSPARENT), 

766 (tr("白と透明に囲まれた部分"), bucket_fill.TARGET_SURROUNDED_BY_WHITE_TRANSPARENT), 

767 (tr("薄い半透明を透明扱い"), bucket_fill.TARGET_FAINT_AS_TRANSPARENT), 

768 (tr("透明以外と内部の透明"), bucket_fill.TARGET_OPAQUE_AND_INNER_TRANSPARENT), 

769 (tr("透明以外は閉領域にも"), bucket_fill.TARGET_TRANSPARENT_AND_CLOSED_OPAQUE), 

770 ): 

771 self.bucket_target_color.addItem(label, value) 

772 self.bucket_target_color.setToolTip(tr( 

773 "どの画素をたどって塗るかを選びます。" 

774 "「すべての色を対象」はクリックした色の領域を塗ります。" 

775 "「〜のみ」はその画素だけ、「〜に囲まれた部分」はそれ以外の画素を、" 

776 "クリック位置からつながる範囲で塗ります。" 

777 )) 

778 self.bucket_target_row = QWidget() 

779 bucket_target_layout = QHBoxLayout(self.bucket_target_row) 

780 bucket_target_layout.setContentsMargins(0, 0, 0, 0) 

781 bucket_target_layout.setSpacing(4) 

782 bucket_target_layout.addWidget(QLabel(tr("対象色"))) 

783 bucket_target_layout.addWidget(self.bucket_target_color, 1) 

784 

785 self.bucket_adjacent=QCheckBox(tr("隣接")) 

786 self.bucket_adjacent.setChecked(True) 

787 self.bucket_adjacent.setToolTip(tr( 

788 "ON:クリック位置につながる同色領域だけを塗ります。" 

789 "OFF:レイヤー内の同じ色を一括で塗ります。" 

790 )) 

791 self.bucket_include_sub=QCheckBox(tr("含み塗り")) 

792 self.bucket_include_sub.setChecked(False) 

793 self.bucket_include_sub.setToolTip(tr( 

794 "ON:塗る領域に接している「含む色」の部分も一緒に塗ります。" 

795 "色トレス線の色を登録しておくと、線ごと塗りつぶせます。" 

796 "色を登録していないときはサブカラーを含みます。" 

797 )) 

798 self.bucket_include_colors=IncludeColorSlots(self._include_slot_color) 

799 # 色を登録したらすぐ使えるよう、含み塗りをONにする。 

800 self.bucket_include_colors.colorRegistered.connect( 

801 lambda: self.bucket_include_sub.setChecked(True) 

802 ) 

803 self.bucket_island_replace=QCheckBox(tr("囲まれた色を置換色で塗る")) 

804 self.bucket_island_replace.setChecked(False) 

805 self.bucket_island_replace.setToolTip(tr( 

806 "塗った範囲に囲まれた線や点のうち、使用色パネルで置換色を登録してある色を、" 

807 "その置換色で塗ります。頬の範囲線の中を塗るときに、中の斜線も" 

808 "決まった色にできます。" 

809 )) 

810 

811 # 塗り残し拾いと、拾う隙間の幅の上限を同じ横一列へ配置する。 

812 self.bucket_island_fill=QCheckBox(tr("塗り残し拾い")) 

813 self.bucket_island_fill.setChecked(True) 

814 self.bucket_island_fill.setToolTip(tr("塗った領域と角で直接つながる、同じ対象色の空白を指定した幅以下なら先端まで拾います。長さや面積は制限せず、線は飛び越えません。自動塗りにも適用します。")) 

815 self.bucket_island_fill_width=QSlider(Qt.Orientation.Horizontal) 

816 self.bucket_island_fill_width.setRange(1,20) 

817 self.bucket_island_fill_width.setValue(1) 

818 self.bucket_island_fill_width.setMinimumWidth(58) 

819 self.bucket_island_fill_width.setToolTip(self.bucket_island_fill.toolTip()) 

820 self.bucket_island_fill_width_label=QLabel(tr("幅 {width} px 以下").format(width=1)) 

821 self.bucket_island_fill_width_label.setMinimumWidth(90) 

822 self.bucket_island_fill_width.valueChanged.connect( 

823 lambda value: self.bucket_island_fill_width_label.setText( 

824 tr("幅 {width} px 以下").format(width=value) 

825 ) 

826 ) 

827 self.bucket_island_fill_row=QWidget() 

828 bucket_island_fill_layout=QHBoxLayout(self.bucket_island_fill_row) 

829 bucket_island_fill_layout.setContentsMargins(0,0,0,0) 

830 bucket_island_fill_layout.setSpacing(4) 

831 bucket_island_fill_layout.addWidget(self.bucket_island_fill) 

832 bucket_island_fill_layout.addWidget(self.bucket_island_fill_width,1) 

833 bucket_island_fill_layout.addWidget(self.bucket_island_fill_width_label) 

834 

835 self.bucket_close_gap=QCheckBox(tr("隙間閉じ")) 

836 self.bucket_close_gap.setChecked(False) 

837 self.bucket_gap_width=QSlider(Qt.Orientation.Horizontal) 

838 self.bucket_gap_width.setRange(1,20) 

839 self.bucket_gap_width.setValue(4) 

840 self.bucket_gap_width.setMinimumWidth(58) 

841 self.bucket_gap_width_label=QLabel("4 px") 

842 self.bucket_gap_width_label.setFixedWidth(34) 

843 self.bucket_gap_width.valueChanged.connect( 

844 lambda value: self.bucket_gap_width_label.setText( 

845 f"{value} px" 

846 ) 

847 ) 

848 self.bucket_gap_row=QWidget() 

849 bucket_gap_layout=QHBoxLayout(self.bucket_gap_row) 

850 bucket_gap_layout.setContentsMargins(0,0,0,0) 

851 bucket_gap_layout.setSpacing(4) 

852 # 閉領域フィルでは隙間閉じが常に効くので、チェックボックスの代わりに 

853 # ただの見出しを出す。 

854 self.bucket_gap_width_title=QLabel(tr("閉じる隙間")) 

855 self.bucket_gap_width_title.setToolTip(tr( 

856 "これより狭い線の隙間は閉じているものとみなして塗ります。" 

857 )) 

858 bucket_gap_layout.addWidget(self.bucket_close_gap) 

859 bucket_gap_layout.addWidget(self.bucket_gap_width_title) 

860 bucket_gap_layout.addWidget(self.bucket_gap_width,1) 

861 bucket_gap_layout.addWidget(self.bucket_gap_width_label) 

862 

863 # 線の延長と長さスライダーを同じ横一列へ配置する。 

864 self.bucket_extend_lines=QCheckBox(tr("線の延長")) 

865 # 端まで引かれていない線の素材が多いので、既定で入れておく。 

866 self.bucket_extend_lines.setChecked(True) 

867 self.bucket_extend_lines.setToolTip(tr( 

868 "キャンバス端の手前で途切れた線を、線の向きのまま端まで延ばしたものとして塗ります。" 

869 "スタンダードフレームの外側で線を端まで引いていない素材でも、" 

870 "線の切れ目から隣へ漏れずに塗れます。" 

871 "端からスライダーの長さより遠い線の端は延長しません。" 

872 )) 

873 self.bucket_extend_length=QSlider(Qt.Orientation.Horizontal) 

874 self.bucket_extend_length.setRange(10,1000) 

875 self.bucket_extend_length.setValue(bucket_fill.BucketOptions.extend_length) 

876 self.bucket_extend_length.setMinimumWidth(58) 

877 self.bucket_extend_length_label=QLabel( 

878 f"{self.bucket_extend_length.value()} px" 

879 ) 

880 self.bucket_extend_length_label.setFixedWidth(46) 

881 self.bucket_extend_length.valueChanged.connect( 

882 lambda value: self.bucket_extend_length_label.setText( 

883 f"{value} px" 

884 ) 

885 ) 

886 self.bucket_extend_row=QWidget() 

887 bucket_extend_layout=QHBoxLayout(self.bucket_extend_row) 

888 bucket_extend_layout.setContentsMargins(0,0,0,0) 

889 bucket_extend_layout.setSpacing(4) 

890 bucket_extend_layout.addWidget(self.bucket_extend_lines) 

891 bucket_extend_layout.addWidget(self.bucket_extend_length,1) 

892 bucket_extend_layout.addWidget(self.bucket_extend_length_label) 

893 

894 # 塗りつぶし・投げ縄塗りの不透明度。100%は選択RGBをそのまま書き込み、 

895 # 100%未満だけ下地RGBと混色する(アルファは常に255、白は消しゴム)。 

896 self.fill_opacity_row=QWidget() 

897 fill_opacity_layout=QHBoxLayout(self.fill_opacity_row) 

898 fill_opacity_layout.setContentsMargins(0,0,0,0) 

899 fill_opacity_layout.setSpacing(4) 

900 self.fill_opacity_title=QLabel(tr("不透明度")) 

901 self.fill_opacity=QSlider(Qt.Orientation.Horizontal) 

902 self.fill_opacity.setRange(1,100) 

903 self.fill_opacity.setValue(100) 

904 self.fill_opacity.setMinimumWidth(58) 

905 self.fill_opacity_label=QLabel("100%") 

906 self.fill_opacity_label.setFixedWidth(38) 

907 self.fill_opacity.valueChanged.connect( 

908 lambda value: self.fill_opacity_label.setText(f"{value}%") 

909 ) 

910 fill_opacity_tooltip = tr( 

911 "塗りつぶし・投げ縄塗りの不透明度。" 

912 "100%は選択した色をそのまま塗ります(近似色は増えません)。" 

913 "100%未満は下地の色と混ぜた色で塗ります。" 

914 ) 

915 for widget in ( 

916 self.fill_opacity_title, self.fill_opacity, self.fill_opacity_label 

917 ): 

918 widget.setToolTip(fill_opacity_tooltip) 

919 fill_opacity_layout.addWidget(self.fill_opacity_title) 

920 fill_opacity_layout.addWidget(self.fill_opacity,1) 

921 fill_opacity_layout.addWidget(self.fill_opacity_label) 

922 

923 self.lasso_inside_boundary=QCheckBox(tr("境界線の内側だけを塗る")) 

924 self.lasso_inside_boundary.setChecked(False) 

925 self.lasso_main_outline_sub_fill=QCheckBox(tr("サブ色を実線、メイン色を内面にする")) 

926 self.lasso_main_outline_sub_fill.setChecked(False) 

927 self.lasso_outline_width_label=QLabel(tr("外線の太さ:1.0 px")) 

928 self.lasso_outline_width=QSlider(Qt.Orientation.Horizontal) 

929 # 0.5 px単位。値2=1.0 px、3=1.5 px、5=2.5 px。 

930 self.lasso_outline_width.setRange(1,40) 

931 self.lasso_outline_width.setValue(2) 

932 self.lasso_outline_width.valueChanged.connect( 

933 lambda value: self.lasso_outline_width_label.setText( 

934 tr("外線の太さ:{value:.1f} px").format(value=value / 2) 

935 ) 

936 ) 

937 self.lasso_main_outline_sub_fill.toggled.connect( 

938 lambda enabled: ( 

939 self.lasso_outline_width_label.setEnabled(enabled), 

940 self.lasso_outline_width.setEnabled(enabled), 

941 ) 

942 ) 

943 self.lasso_outline_width_label.setEnabled(False) 

944 self.lasso_outline_width.setEnabled(False) 

945 

946 # ラインツール 

947 self.line_type_label = QLabel(tr("ライン種類")) 

948 self.line_type = QComboBox() 

949 # The item *data* is the value the rest of the app branches on; the item 

950 # text is display only. Comparing against the displayed text would break 

951 # the moment the UI is translated. 

952 self.line_type.addItem(tr("直線"), "straight") 

953 self.line_type.addItem(tr("曲線"), "curve") 

954 self.line_type.setToolTip( 

955 tr("曲線は、1回目のドラッグで始点と終点を決め、" 

956 "次のクリックで弓なりのカーブを確定します。") 

957 ) 

958 self.line_taper_in = QCheckBox(tr("入り")) 

959 self.line_taper_in.setChecked(False) 

960 self.line_taper_in_size_label = ClickableValueLabel(tr("入りサイズ:0.5 px")) 

961 self.line_taper_in_size_label.setToolTip( 

962 tr("クリックすると入りカーブ設定がポップアップします。") 

963 ) 

964 self.line_taper_in_size_label.clicked.connect( 

965 lambda global_pos: self._show_line_curve_popup("in", global_pos) 

966 ) 

967 self.line_taper_in_size = QSlider(Qt.Orientation.Horizontal) 

968 self.line_taper_in_size.setRange(1, 800) 

969 self.line_taper_in_size.setValue(1) 

970 self.line_taper_in_size.valueChanged.connect( 

971 lambda value: self.line_taper_in_size_label.setText( 

972 tr("入りサイズ:{value:.1f} px").format(value=value / 2) 

973 ) 

974 ) 

975 self.line_taper_in_curve_label = QLabel(tr("入りカーブ:1.00")) 

976 self.line_taper_in_curve = QSlider(Qt.Orientation.Horizontal) 

977 self.line_taper_in_curve.setRange(20, 400) 

978 self.line_taper_in_curve.setValue(100) 

979 self.line_taper_in_curve.setToolTip( 

980 tr("小さいほど緩やかに、値を大きくすると先端付近で急に太くなります。") 

981 ) 

982 self.line_taper_in_curve.valueChanged.connect( 

983 lambda value: self.line_taper_in_curve_label.setText( 

984 tr("入りカーブ:{value:.2f}").format(value=value / 100) 

985 ) 

986 ) 

987 self.line_taper_out = QCheckBox(tr("抜き")) 

988 self.line_taper_out.setChecked(False) 

989 self.line_taper_out_size_label = ClickableValueLabel(tr("抜きサイズ:0.5 px")) 

990 self.line_taper_out_size_label.setToolTip( 

991 tr("クリックすると抜きカーブ設定がポップアップします。") 

992 ) 

993 self.line_taper_out_size_label.clicked.connect( 

994 lambda global_pos: self._show_line_curve_popup("out", global_pos) 

995 ) 

996 self.line_taper_out_size = QSlider(Qt.Orientation.Horizontal) 

997 self.line_taper_out_size.setRange(1, 800) 

998 self.line_taper_out_size.setValue(1) 

999 self.line_taper_out_size.valueChanged.connect( 

1000 lambda value: self.line_taper_out_size_label.setText( 

1001 tr("抜きサイズ:{value:.1f} px").format(value=value / 2) 

1002 ) 

1003 ) 

1004 self.line_taper_out_curve_label = QLabel(tr("抜きカーブ:1.00")) 

1005 self.line_taper_out_curve = QSlider(Qt.Orientation.Horizontal) 

1006 self.line_taper_out_curve.setRange(20, 400) 

1007 self.line_taper_out_curve.setValue(100) 

1008 self.line_taper_out_curve.setToolTip( 

1009 tr("小さいほど緩やかに、値を大きくすると終端付近で急に細くなります。") 

1010 ) 

1011 self.line_taper_out_curve.valueChanged.connect( 

1012 lambda value: self.line_taper_out_curve_label.setText( 

1013 tr("抜きカーブ:{value:.2f}").format(value=value / 100) 

1014 ) 

1015 ) 

1016 self.line_taper_in.toggled.connect( 

1017 lambda enabled: ( 

1018 self.line_taper_in_size_label.setEnabled(enabled), 

1019 self.line_taper_in_size.setEnabled(enabled), 

1020 self.line_taper_in_curve_label.setEnabled(enabled), 

1021 self.line_taper_in_curve.setEnabled(enabled), 

1022 ) 

1023 ) 

1024 self.line_taper_out.toggled.connect( 

1025 lambda enabled: ( 

1026 self.line_taper_out_size_label.setEnabled(enabled), 

1027 self.line_taper_out_size.setEnabled(enabled), 

1028 self.line_taper_out_curve_label.setEnabled(enabled), 

1029 self.line_taper_out_curve.setEnabled(enabled), 

1030 ) 

1031 ) 

1032 self.line_taper_in_size_label.setEnabled(False) 

1033 self.line_taper_in_size.setEnabled(False) 

1034 self.line_taper_in_curve_label.setEnabled(False) 

1035 self.line_taper_in_curve.setEnabled(False) 

1036 self.line_taper_out_size_label.setEnabled(False) 

1037 self.line_taper_out_size.setEnabled(False) 

1038 self.line_taper_out_curve_label.setEnabled(False) 

1039 self.line_taper_out_curve.setEnabled(False) 

1040 

1041 # 図形ツール 

1042 self.shape_type_label = QLabel(tr("図形種類")) 

1043 self.shape_type = QComboBox() 

1044 self.shape_type.addItem(tr("直線"), "straight") 

1045 self.shape_type.addItem(tr("曲線"), "curve") 

1046 self.shape_type.addItem(tr("多角形"), "polygon") 

1047 self.shape_type.addItem(tr("楕円"), "ellipse") 

1048 self.shape_type.setToolTip(self.line_type.toolTip()) 

1049 self.shape_type.setCurrentIndex(self.shape_type.findData("polygon")) 

1050 self.shape_corners_label = QLabel(tr("角の数")) 

1051 self.shape_corners = QSpinBox() 

1052 self.shape_corners.setRange(3, 32) 

1053 self.shape_corners.setValue(4) 

1054 self.shape_corners_slider = QSlider(Qt.Orientation.Horizontal) 

1055 self.shape_corners_slider.setRange(3, 32) 

1056 self.shape_corners_slider.setValue(4) 

1057 self.shape_corners_slider.valueChanged.connect(self.shape_corners.setValue) 

1058 self.shape_corners.valueChanged.connect(self.shape_corners_slider.setValue) 

1059 self.shape_type.currentIndexChanged.connect( 

1060 self._shape_kind_changed 

1061 ) 

1062 self.shape_lock_ratio = QCheckBox(tr("比率固定")) 

1063 self.shape_fill_inside = QCheckBox(tr("内側を塗る")) 

1064 self.shape_sub_outline_main_fill = QCheckBox( 

1065 tr("サブ色を実線、メイン色を内面にする") 

1066 ) 

1067 self.shape_outline_width_label = QLabel(tr("線の太さ:1.0 px")) 

1068 self.shape_outline_width = QSlider(Qt.Orientation.Horizontal) 

1069 self.shape_outline_width.setRange(1, 80) 

1070 self.shape_outline_width.setValue(2) 

1071 self.shape_outline_width.valueChanged.connect( 

1072 lambda value: self.shape_outline_width_label.setText( 

1073 tr("線の太さ:{value:.1f} px").format(value=value / 2) 

1074 ) 

1075 ) 

1076 

1077 self.bucket_require_closed=QCheckBox( 

1078 tr("領域が開いている場合は塗りを開始しない") 

1079 ) 

1080 self.bucket_require_closed.setChecked(False) 

1081 self.bucket_all_frames=QCheckBox( 

1082 tr("串刺し塗り(選択レイヤーのすべてのコマ)") 

1083 ) 

1084 self.bucket_all_frames.setChecked(False) 

1085 self.bucket_all_frames.setToolTip(tr( 

1086 "クリックした図形を、選択レイヤーの各コマで追いかけて一度に塗ります。" 

1087 "追いかけきれないコマでは止まるので、塗る場所をクリックしてください" 

1088 "(Esc でそのコマを、Shift+Esc でその向きの残りのコマを飛ばします)。" 

1089 )) 

1090 

1091 def sync_bucket_options(_checked=None): 

1092 self.bucket_island_fill_width.setEnabled(self.bucket_island_fill.isChecked()) 

1093 self.bucket_island_fill_width_label.setEnabled(self.bucket_island_fill.isChecked()) 

1094 adjacent = self.bucket_adjacent.isChecked() 

1095 close_gap = ( 

1096 adjacent 

1097 and self.bucket_close_gap.isChecked() 

1098 ) 

1099 self.bucket_close_gap.setEnabled(adjacent) 

1100 self.bucket_require_closed.setEnabled(adjacent) 

1101 self.bucket_gap_width.setEnabled(close_gap) 

1102 self.bucket_gap_width_label.setEnabled(close_gap) 

1103 extend_lines = adjacent and self.bucket_extend_lines.isChecked() 

1104 self.bucket_extend_lines.setEnabled(adjacent) 

1105 self.bucket_extend_length.setEnabled(extend_lines) 

1106 self.bucket_extend_length_label.setEnabled(extend_lines) 

1107 

1108 self.bucket_adjacent.toggled.connect(sync_bucket_options) 

1109 self.bucket_island_fill.toggled.connect(sync_bucket_options) 

1110 self.bucket_close_gap.toggled.connect(sync_bucket_options) 

1111 self.bucket_extend_lines.toggled.connect(sync_bucket_options) 

1112 sync_bucket_options() 

1113 

1114 self.dust_mode_label=QLabel(tr("処理モード")) 

1115 self.dust_mode=QComboBox() 

1116 self.dust_mode.addItem(tr("ゴミ取り"), "despeckle") 

1117 self.dust_mode.addItem(tr("塗り抜け"), "fill_holes") 

1118 self.dust_mode.addItem(tr("線周りのノイズ"), "line_noise") 

1119 self.dust_mode.setToolTip( 

1120 tr("ゴミ取り:小さな色点を白(#FFFFFF)へ変更します。" 

1121 "塗り抜け:小さな白い穴を周囲色で埋めます。" 

1122 "線周りのノイズ:線の近くで、まわりに同じ色が1つも無い孤立した" 

1123 "1ドットを、8近傍の多数決でまわりの色へ寄せます。" 

1124 "色面のふちは同じ色の隣を必ず持つため、塗り分けは壊れません。") 

1125 ) 

1126 self.dust_size_label=QLabel(tr("適用サイズ:3 px")) 

1127 self.dust_size=QSlider(Qt.Orientation.Horizontal) 

1128 self.dust_size.setRange(1,100) 

1129 self.dust_size.setValue(3) 

1130 self.dust_size.valueChanged.connect( 

1131 lambda value: self.dust_size_label.setText( 

1132 tr("適用サイズ:{value} px").format(value=value) 

1133 ) 

1134 ) 

1135 self.dust_selected_only=QCheckBox(tr("選択色を対象")) 

1136 self.dust_selected_only.setChecked(False) 

1137 self.dust_selected_only.setToolTip( 

1138 tr("使用色パネルで選択している色だけを対象にします。" 

1139 "色ごとに独立判定するため、別色と隣接していても" 

1140 "小さな選択色を削除できます。") 

1141 ) 

1142 self.dust_all_frames=QCheckBox( 

1143 tr("選択レイヤーのすべてのコマに適用") 

1144 ) 

1145 self.dust_all_frames.setChecked(False) 

1146 self.dust_apply=QPushButton(tr("ゴミ取りを適用")) 

1147 self.dust_mode.currentTextChanged.connect( 

1148 lambda mode: self.dust_apply.setText( 

1149 tr("{mode}を適用").format(mode=mode) 

1150 ) 

1151 ) 

1152 self.mesh_commit.clicked.connect(self.selectionCommitRequested) 

1153 self.mesh_cancel.clicked.connect(self.selectionCancelRequested) 

1154 self.flip_h.clicked.connect(lambda:self.flipLayerRequested.emit(True)) 

1155 self.flip_v.clicked.connect(lambda:self.flipLayerRequested.emit(False)) 

1156 self.dust_apply.clicked.connect(self.removeDustRequested) 

1157 for command in ( 

1158 self.selection_transform,self.selection_scale,self.selection_mesh, 

1159 self.selection_clear,self.selection_rotate_left,self.selection_rotate_right, 

1160 self.transform_mesh_grid_label, 

1161 self.transform_mesh_grid_x,self.transform_mesh_grid_x_slider, 

1162 self.transform_mesh_grid_y,self.transform_mesh_grid_y_slider, 

1163 self.transform_quality,self.transform_line_width_note, 

1164 self.transform_line_width_label,self.transform_line_width, 

1165 self.selection_all_frames, 

1166 self.mesh_commit,self.mesh_cancel, 

1167 self.flip_h,self.flip_v, 

1168 self.shape_type_label,self.shape_type, 

1169 self.line_type_label,self.line_type, 

1170 self.line_taper_in,self.line_taper_in_size_label,self.line_taper_in_size, 

1171 self.line_taper_in_curve_label,self.line_taper_in_curve, 

1172 self.line_taper_out,self.line_taper_out_size_label,self.line_taper_out_size, 

1173 self.line_taper_out_curve_label,self.line_taper_out_curve, 

1174 self.shape_corners_label,self.shape_corners,self.shape_corners_slider, 

1175 self.shape_lock_ratio,self.shape_fill_inside, 

1176 self.shape_sub_outline_main_fill, 

1177 self.shape_outline_width_label,self.shape_outline_width, 

1178 self.lasso_inside_boundary,self.lasso_main_outline_sub_fill, 

1179 self.lasso_outline_width_label,self.lasso_outline_width, 

1180 self.bucket_reference_row, 

1181 self.bucket_target_row, 

1182 self.bucket_adjacent,self.bucket_include_sub, 

1183 self.bucket_include_colors, 

1184 self.bucket_island_replace, 

1185 self.bucket_gap_row, 

1186 self.bucket_island_fill_row, 

1187 self.bucket_extend_row, 

1188 self.bucket_require_closed, 

1189 self.fill_opacity_row, 

1190 self.bucket_all_frames, 

1191 self.dust_mode_label,self.dust_mode, 

1192 self.dust_size_label,self.dust_size, 

1193 self.dust_selected_only, 

1194 self.dust_all_frames,self.dust_apply 

1195 ): 

1196 self.command_layout.addWidget(command) 

1197 v.addWidget(self.command_box) 

1198 

1199 self.size_title=QLabel(tr("<b>ブラシサイズ</b>")) 

1200 size_title_row = QHBoxLayout() 

1201 size_title_row.setContentsMargins(0, 0, 0, 0) 

1202 size_title_row.addWidget(self.size_title) 

1203 size_title_row.addStretch() 

1204 self.pressure_settings_button = QPushButton(tr("筆圧…")) 

1205 self.pressure_settings_button.setFixedHeight(24) 

1206 self.pressure_settings_button.clicked.connect( 

1207 lambda: self.brush_size_spinbox.pressureRequested.emit() 

1208 ) 

1209 size_title_row.addWidget(self.pressure_settings_button) 

1210 v.addLayout(size_title_row) 

1211 self.brush_size_spinbox = BrushSizeSpinBox() 

1212 self.brush_size_spinbox.setRange(0.5, 400.0) 

1213 self.brush_size_spinbox.setSingleStep(0.5) 

1214 self.brush_size_spinbox.setDecimals(1) 

1215 self.brush_size_spinbox.setValue(8.0) 

1216 self.size_slider=QSlider(Qt.Orientation.Horizontal) 

1217 self.size_slider.setRange(1, 800) 

1218 self.size_slider.setValue(16) 

1219 self.brush_size_spinbox.valueChanged.connect( 

1220 lambda value: self.size_slider.setValue(int(round(value * 2))) 

1221 ) 

1222 self.size_slider.valueChanged.connect( 

1223 lambda value: self.brush_size_spinbox.setValue(value / 2.0) 

1224 ) 

1225 # スライダーと数値欄を1行にまとめ、縦の場所を空ける。 

1226 self.brush_size_spinbox.setFixedWidth(64) 

1227 size_row = QHBoxLayout() 

1228 size_row.setContentsMargins(0, 0, 0, 0) 

1229 size_row.setSpacing(6) 

1230 size_row.addWidget(self.size_slider, 1) 

1231 size_row.addWidget(self.brush_size_spinbox) 

1232 v.addLayout(size_row) 

1233 

1234 self.brush_stabilizer_label = QLabel(tr("手振れ補正:0")) 

1235 self.brush_stabilizer = QSlider(Qt.Orientation.Horizontal) 

1236 self.brush_stabilizer.setRange(0, 300) 

1237 self.brush_stabilizer.setValue(0) 

1238 self.brush_stabilizer.setToolTip( 

1239 tr("ブラシ軌跡を移動平均と遅延半径で滑らかにします。" 

1240 "0~300。値が大きいほど補正を強くします。") 

1241 ) 

1242 self.brush_stabilizer.valueChanged.connect( 

1243 lambda value: self.brush_stabilizer_label.setText( 

1244 tr("手振れ補正:{value}").format(value=value) 

1245 ) 

1246 ) 

1247 v.addWidget(self.brush_stabilizer_label) 

1248 v.addWidget(self.brush_stabilizer) 

1249 

1250 # 描画色(メイン/サブ/背景)。カラーサークルの下に、 

1251 # ツールバー(2列表示)と同じ重ね表示で置く:メインが左上・サブが右下。 

1252 # どちらを選んでも位置は入れ替えず、選択中の色は枠と見出しで示す 

1253 # (輪郭あり投げ縄塗りなど、両方を使う操作で迷わないようにする)。 

1254 # 切り替え(⇄)と初期色(◩)は Photoshop と同じくスウォッチの角に置く。 

1255 self.drawing_color_box = QWidget() 

1256 color_layout = QHBoxLayout(self.drawing_color_box) 

1257 color_layout.setContentsMargins(6, 6, 6, 4) 

1258 color_layout.setSpacing(10) 

1259 self.color_swatch_stack = QWidget() 

1260 self.color_swatch_stack.setFixedSize( 

1261 self.COLOR_WELL_SIZE, self.COLOR_WELL_SIZE 

1262 ) 

1263 self.main_btn = SwatchEyedropButton(self.color_swatch_stack) 

1264 self.sub_btn = SwatchEyedropButton(self.color_swatch_stack) 

1265 self.swap_colors_button = QPushButton("⇄", self.color_swatch_stack) 

1266 self.reset_colors_button = QPushButton(self.color_swatch_stack) 

1267 box = round(self.COLOR_WELL_SIZE * 0.62) 

1268 offset = self.COLOR_WELL_SIZE - box 

1269 self.main_btn.setGeometry(0, 0, box, box) 

1270 self.sub_btn.setGeometry(offset, offset, box, box) 

1271 corner = offset - 2 

1272 self.swap_colors_button.setGeometry(box + 2, 0, corner, corner) 

1273 self.reset_colors_button.setGeometry(0, box + 2, corner, corner) 

1274 self.reset_colors_button.setIcon(self._reset_colors_icon(corner)) 

1275 self.reset_colors_button.setIconSize(QSize(corner - 4, corner - 4)) 

1276 for button in (self.main_btn, self.sub_btn): 

1277 button.setText("") 

1278 button.setCursor(Qt.CursorShape.PointingHandCursor) 

1279 for button in (self.swap_colors_button, self.reset_colors_button): 

1280 button.setFocusPolicy(Qt.FocusPolicy.NoFocus) 

1281 button.setCursor(Qt.CursorShape.PointingHandCursor) 

1282 self.main_btn.setToolTip(tr("クリック:メイン色を選択/ドラッグ:スポイト")) 

1283 self.sub_btn.setToolTip(tr("クリック:サブ色を選択/ドラッグ:スポイト")) 

1284 self.swap_colors_button.setToolTip(tr("メイン色とサブ色を切り替え")) 

1285 self.reset_colors_button.setToolTip(tr("初期色(黒/白)に戻す")) 

1286 # 重ね表示でも前後関係は固定(メインが手前)。 

1287 self.main_btn.raise_() 

1288 color_layout.addWidget( 

1289 self.color_swatch_stack, 0, Qt.AlignmentFlag.AlignTop 

1290 ) 

1291 

1292 # 右側:メイン/サブのカラーコード(入力して Enter で変更)とコピー、背景色。 

1293 info = QGridLayout() 

1294 info.setContentsMargins(0, 0, 0, 0) 

1295 info.setHorizontalSpacing(6) 

1296 info.setVerticalSpacing(3) 

1297 self.main_caption = QLabel(tr("メイン")) 

1298 self.sub_caption = QLabel(tr("サブ")) 

1299 self.main_code_edit = QLineEdit() 

1300 self.sub_code_edit = QLineEdit() 

1301 self.main_copy_button = QPushButton(tr("コピー")) 

1302 self.sub_copy_button = QPushButton(tr("コピー")) 

1303 for row, (mode, caption, edit, copy_button) in enumerate(( 

1304 ("main", self.main_caption, self.main_code_edit, 

1305 self.main_copy_button), 

1306 ("sub", self.sub_caption, self.sub_code_edit, 

1307 self.sub_copy_button), 

1308 )): 

1309 edit.setFixedSize(68, 20) 

1310 edit.setMaxLength(7) 

1311 edit.setAlignment(Qt.AlignmentFlag.AlignCenter) 

1312 edit.setToolTip(tr("カラーコードを入力して Enter で色を変更")) 

1313 edit.editingFinished.connect( 

1314 lambda m=mode: self._apply_color_code(m) 

1315 ) 

1316 copy_button.setFixedSize(40, 20) 

1317 copy_button.setFocusPolicy(Qt.FocusPolicy.NoFocus) 

1318 copy_button.setCursor(Qt.CursorShape.PointingHandCursor) 

1319 copy_button.setToolTip(tr("現在のカラーコードをコピー")) 

1320 copy_button.setStyleSheet( 

1321 "QPushButton{font-size:9px;padding:1px 2px;}" 

1322 ) 

1323 copy_button.clicked.connect( 

1324 lambda _checked=False, m=mode: self._copy_color_code(m) 

1325 ) 

1326 info.addWidget(caption, row, 0) 

1327 info.addWidget(edit, row, 1) 

1328 info.addWidget(copy_button, row, 2) 

1329 

1330 # 背景色スウォッチ:クリックで背景色(透明表示色)を描画色として選択、 

1331 # 右クリックで表示色そのものを変更する。 

1332 self.background_label = QLabel(tr("背景色")) 

1333 self.background_btn = SwatchEyedropButton() 

1334 self.background_btn.setFixedSize(40, 16) 

1335 self.background_btn.setCursor(Qt.CursorShape.PointingHandCursor) 

1336 self.background_btn.setToolTip( 

1337 tr("クリック:背景色で描画/右クリック:背景色の表示色を変更") 

1338 ) 

1339 self.background_btn.clicked.connect( 

1340 lambda: self.set_color_mode("transparent") 

1341 ) 

1342 self.background_btn.setContextMenuPolicy( 

1343 Qt.ContextMenuPolicy.CustomContextMenu 

1344 ) 

1345 self.background_btn.customContextMenuRequested.connect( 

1346 lambda _p: self.backgroundColorRequested.emit() 

1347 ) 

1348 info.addWidget(self.background_label, 2, 0) 

1349 info.addWidget(self.background_btn, 2, 1, Qt.AlignmentFlag.AlignLeft) 

1350 info.setColumnStretch(3, 1) 

1351 info_box = QVBoxLayout() 

1352 info_box.setContentsMargins(0, 2, 0, 0) 

1353 info_box.addLayout(info) 

1354 info_box.addStretch(1) 

1355 color_layout.addLayout(info_box, 1) 

1356 

1357 self.main_btn.clicked.connect(lambda: self.set_color_mode("main")) 

1358 self.sub_btn.clicked.connect(lambda: self.set_color_mode("sub")) 

1359 # ダブルクリックでその色をカラーピッカーで設定する。 

1360 on_double_click(self.main_btn, lambda: self.colorEditRequested.emit("main")) 

1361 on_double_click(self.sub_btn, lambda: self.colorEditRequested.emit("sub")) 

1362 on_double_click(self.background_btn, self.backgroundColorRequested.emit) 

1363 self.swap_colors_button.clicked.connect(self.swapMainSubRequested) 

1364 self.reset_colors_button.clicked.connect(self.resetMainSubRequested) 

1365 

1366 # カラーサークルドック:色相リング/HSV(四角)/HLS(三角)を切替可能。 

1367 self.wheel_mode = HSVColorWheel.DEFAULT_MODE 

1368 self.wheel_hue_mode = HSVColorWheel.DEFAULT_HUE_MODE 

1369 self.color_wheel_box = QWidget() 

1370 self.color_wheel_box.setObjectName("colorPickerSurface") 

1371 _wheel_c = theme.palette() 

1372 self.color_wheel_box.setStyleSheet( 

1373 "QWidget#colorPickerSurface{background:%s;" 

1374 "border:1px solid %s;border-radius:6px;}" 

1375 % (_wheel_c["surface_alt"], _wheel_c["border"]) 

1376 ) 

1377 wheel_layout = QVBoxLayout(self.color_wheel_box) 

1378 wheel_layout.setContentsMargins(3, 3, 3, 3) 

1379 wheel_layout.setSpacing(2) 

1380 self.hsv_wheel = HSVColorWheel() 

1381 self.hsv_wheel.colorChanged.connect(self.wheel_color_changed) 

1382 # カラーサークルのダブルクリックは、いま編集している色(メイン/サブ)を設定する。 

1383 on_double_click( 

1384 self.hsv_wheel, 

1385 lambda: self.colorEditRequested.emit( 

1386 "sub" if self.color_mode == "sub" else "main" 

1387 ), 

1388 ) 

1389 wheel_layout.addWidget(self.hsv_wheel) 

1390 wheel_layout.addWidget(self.drawing_color_box) 

1391 wheel_layout.addStretch(1) 

1392 

1393 # カラースライダードック:RGB/HLS/CMYKを切替可能。 

1394 self.slider_mode = "RGB" 

1395 self.color_slider_box = QWidget() 

1396 slider_box_layout = QVBoxLayout(self.color_slider_box) 

1397 slider_box_layout.setContentsMargins(3, 3, 3, 3) 

1398 slider_box_layout.setSpacing(2) 

1399 self.slider_box=QWidget(); self.slider_layout=QFormLayout(self.slider_box); self.slider_layout.setContentsMargins(4,4,4,4); self.slider_layout.setVerticalSpacing(3) 

1400 slider_box_layout.addWidget(self.slider_box) 

1401 slider_box_layout.addStretch(1) 

1402 self.color_sliders=[]; self.color_value_labels=[]; self.rebuild_color_sliders(self.slider_mode) 

1403 # サイズ欄は▲▼のみ、RGB/HSV数値欄は▲▼と半角数値入力に対応。 

1404 for numeric in self.findChildren(QAbstractSpinBox): 

1405 is_size_numeric = numeric is self.brush_size_spinbox 

1406 is_color_numeric = ( 

1407 isinstance(numeric, SliderValueSpinBox) 

1408 ) 

1409 is_interactive_numeric = ( 

1410 is_size_numeric or is_color_numeric 

1411 ) 

1412 

1413 numeric.setReadOnly(not is_interactive_numeric) 

1414 numeric.setAttribute( 

1415 Qt.WidgetAttribute.WA_TransparentForMouseEvents, 

1416 not is_interactive_numeric, 

1417 ) 

1418 if not is_interactive_numeric: 

1419 numeric.setButtonSymbols( 

1420 QAbstractSpinBox.ButtonSymbols.NoButtons 

1421 ) 

1422 

1423 editor = numeric.lineEdit() 

1424 if is_color_numeric: 1424 ↛ 1425line 1424 didn't jump to line 1425 because the condition on line 1424 was never true

1425 numeric.setFocusPolicy( 

1426 Qt.FocusPolicy.StrongFocus 

1427 ) 

1428 if editor is not None: 

1429 editor.setReadOnly(False) 

1430 editor.setFocusPolicy( 

1431 Qt.FocusPolicy.StrongFocus 

1432 ) 

1433 elif is_size_numeric: 

1434 numeric.setFocusPolicy(Qt.FocusPolicy.NoFocus) 

1435 if editor is not None: 1435 ↛ 1404line 1435 didn't jump to line 1404 because the condition on line 1435 was always true

1436 editor.setReadOnly(True) 

1437 editor.setFocusPolicy( 

1438 Qt.FocusPolicy.NoFocus 

1439 ) 

1440 else: 

1441 numeric.setFocusPolicy(Qt.FocusPolicy.NoFocus) 

1442 if editor is not None: 1442 ↛ 1404line 1442 didn't jump to line 1404 because the condition on line 1442 was always true

1443 editor.setReadOnly(True) 

1444 editor.setFocusPolicy( 

1445 Qt.FocusPolicy.NoFocus 

1446 ) 

1447 

1448 v.addStretch(); self.select_tool("brush"); self.refresh_swatches() 

1449 

1450 def _close_line_curve_popup(self): 

1451 popup = self._line_curve_popup 

1452 self._line_curve_popup = None 

1453 if popup is not None: 

1454 popup.close() 

1455 popup.deleteLater() 

1456 

1457 def _show_line_curve_popup(self, which, global_position): 

1458 self._close_line_curve_popup() 

1459 if which == "in": 

1460 title = tr("入りカーブ") 

1461 target = self.line_taper_in_curve 

1462 else: 

1463 title = tr("抜きカーブ") 

1464 target = self.line_taper_out_curve 

1465 popup = LineTaperCurvePopup(title, target.value(), self) 

1466 self._line_curve_popup = popup 

1467 popup.curveChanged.connect(target.setValue) 

1468 popup.destroyed.connect( 

1469 lambda _obj=None, current=popup: 

1470 setattr( 

1471 self, 

1472 "_line_curve_popup", 

1473 None if self._line_curve_popup is current 

1474 else self._line_curve_popup, 

1475 ) 

1476 ) 

1477 popup.adjustSize() 

1478 screen = QApplication.screenAt(global_position) or QApplication.primaryScreen() 

1479 position = QPoint(global_position) 

1480 if screen is not None: 

1481 available = screen.availableGeometry() 

1482 width = popup.sizeHint().width() 

1483 height = popup.sizeHint().height() 

1484 position.setX( 

1485 max(available.left(), min(position.x(), available.right() - width + 1)) 

1486 ) 

1487 position.setY( 

1488 max(available.top(), min(position.y(), available.bottom() - height + 1)) 

1489 ) 

1490 popup.move(position) 

1491 popup.show() 

1492 popup.raise_() 

1493 

1494 def _sync_transform_quality_options(self, enabled): 

1495 self.selection_all_frames.setToolTip( 

1496 tr("選択範囲の変形をすべてのコマへ適用します。") 

1497 + (tr("クオリティ変形はコマごとに時間がかかります。") 

1498 if bool(enabled) else "") 

1499 ) 

1500 

1501 def _refresh_transform_line_width_visibility(self): 

1502 visible = ( 

1503 self.active_tool in ("lasso", "rect_select", "auto_select") 

1504 and bool(getattr(self, "_has_transform_line_colors", False)) 

1505 and not bool(getattr(self, "_tween_active", False)) 

1506 ) 

1507 self.transform_line_width_label.setVisible(visible) 

1508 self.transform_line_width.setVisible(visible) 

1509 self.transform_line_width_note.setVisible( 

1510 self.active_tool in ("lasso", "rect_select", "auto_select") 

1511 and not bool(getattr(self, "_tween_active", False)) 

1512 ) 

1513 

1514 def set_tween_active(self, active): 

1515 self._tween_active = bool(active) 

1516 self._refresh_transform_line_width_visibility() 

1517 

1518 def set_transform_line_colors_available(self, available): 

1519 self._has_transform_line_colors = bool(available) 

1520 self._refresh_transform_line_width_visibility() 

1521 

1522 def toggle_selection_all_frames(self): 

1523 if self.selection_all_frames.isEnabled(): 

1524 self.selection_all_frames.toggle() 

1525 

1526 def _shape_kind_changed(self, _index): 

1527 kind = self.shape_type.currentData() 

1528 if kind in ("straight", "curve"): 1528 ↛ 1530line 1528 didn't jump to line 1530 because the condition on line 1528 was always true

1529 self.line_type.setCurrentIndex(self.line_type.findData(kind)) 

1530 if self.active_tool == "shape": 1530 ↛ 1531line 1530 didn't jump to line 1531 because the condition on line 1530 was never true

1531 self.select_tool("shape") 

1532 self._sync_shape_corner_controls() 

1533 

1534 def select_tool(self, tid): 

1535 self.active_tool=tid 

1536 self.active.setText(tool_label(tid)) 

1537 if tid == "shape" and self.shape_type.currentData() in ("straight", "curve"): 

1538 self.line_type.setCurrentIndex(self.line_type.findData(self.shape_type.currentData())) 

1539 self.toolChanged.emit(tid) 

1540 

1541 is_selection = tid in ("lasso", "rect_select", "auto_select") 

1542 is_shape_tool = tid == "shape" 

1543 is_line = tid == "line" or ( 

1544 is_shape_tool and self.shape_type.currentData() in ("straight", "curve") 

1545 ) 

1546 is_shape = is_shape_tool and not is_line 

1547 # 閉領域フィルはバケツと同じ設定を使い、隙間閉じだけを常に効かせる。 

1548 is_closed_fill = tid == "closed_fill" 

1549 is_bucket = tid in ("bucket", "closed_fill") 

1550 is_dust = tid == "dust" 

1551 is_lasso_fill = tid == "lasso_fill" 

1552 uses_size = tid == "brush" or is_line 

1553 

1554 self.command_box.setVisible( 

1555 is_selection or is_line or is_shape_tool 

1556 or is_bucket or is_dust or is_lasso_fill 

1557 ) 

1558 for widget in ( 

1559 self.selection_transform,self.selection_scale,self.selection_mesh, 

1560 self.selection_clear,self.selection_rotate_left,self.selection_rotate_right, 

1561 self.transform_mesh_grid_label, 

1562 self.transform_mesh_grid_x,self.transform_mesh_grid_x_slider, 

1563 self.transform_mesh_grid_y,self.transform_mesh_grid_y_slider, 

1564 self.transform_quality,self.transform_line_width_note, 

1565 self.selection_all_frames 

1566 ): 

1567 widget.setVisible(is_selection) 

1568 self._refresh_transform_line_width_visibility() 

1569 self.mesh_commit.setVisible(is_selection) 

1570 self.mesh_cancel.setVisible(is_selection) 

1571 self.flip_h.setVisible(False) 

1572 self.flip_v.setVisible(False) 

1573 

1574 for widget in ( 

1575 self.line_type_label,self.line_type, 

1576 self.line_taper_in,self.line_taper_in_size_label,self.line_taper_in_size, 

1577 self.line_taper_out,self.line_taper_out_size_label,self.line_taper_out_size, 

1578 ): 

1579 widget.setVisible(is_line) 

1580 self.line_type_label.setVisible(tid == "line") 

1581 self.line_type.setVisible(tid == "line") 

1582 # 入り/抜きカーブはサイズ数値クリック時のポップアップだけで調整する。 

1583 self.line_taper_in_curve_label.setVisible(False) 

1584 self.line_taper_in_curve.setVisible(False) 

1585 self.line_taper_out_curve_label.setVisible(False) 

1586 self.line_taper_out_curve.setVisible(False) 

1587 self.line_taper_in_size_label.setEnabled( 

1588 is_line and self.line_taper_in.isChecked() 

1589 ) 

1590 self.line_taper_in_size.setEnabled( 

1591 is_line and self.line_taper_in.isChecked() 

1592 ) 

1593 self.line_taper_in_curve_label.setEnabled( 

1594 is_line and self.line_taper_in.isChecked() 

1595 ) 

1596 self.line_taper_in_curve.setEnabled( 

1597 is_line and self.line_taper_in.isChecked() 

1598 ) 

1599 self.line_taper_out_size_label.setEnabled( 

1600 is_line and self.line_taper_out.isChecked() 

1601 ) 

1602 self.line_taper_out_size.setEnabled( 

1603 is_line and self.line_taper_out.isChecked() 

1604 ) 

1605 self.line_taper_out_curve_label.setEnabled( 

1606 is_line and self.line_taper_out.isChecked() 

1607 ) 

1608 self.line_taper_out_curve.setEnabled( 

1609 is_line and self.line_taper_out.isChecked() 

1610 ) 

1611 

1612 for widget in ( 

1613 self.shape_type_label,self.shape_type, 

1614 self.shape_corners_label,self.shape_corners,self.shape_corners_slider, 

1615 self.shape_lock_ratio,self.shape_fill_inside, 

1616 self.shape_sub_outline_main_fill, 

1617 self.shape_outline_width_label,self.shape_outline_width, 

1618 ): 

1619 widget.setVisible(is_shape) 

1620 self.shape_type_label.setVisible(is_shape_tool) 

1621 self.shape_type.setVisible(is_shape_tool) 

1622 self._sync_shape_corner_controls(visible=is_shape) 

1623 

1624 self.lasso_inside_boundary.setVisible(is_lasso_fill) 

1625 self.lasso_main_outline_sub_fill.setVisible(is_lasso_fill) 

1626 self.lasso_outline_width_label.setVisible(is_lasso_fill) 

1627 self.lasso_outline_width.setVisible(is_lasso_fill) 

1628 self.lasso_outline_width_label.setEnabled( 

1629 is_lasso_fill and self.lasso_main_outline_sub_fill.isChecked() 

1630 ) 

1631 self.lasso_outline_width.setEnabled( 

1632 is_lasso_fill and self.lasso_main_outline_sub_fill.isChecked() 

1633 ) 

1634 uses_bucket_region = is_bucket or tid == "auto_select" 

1635 self.bucket_reference_row.setVisible(is_bucket or is_lasso_fill) 

1636 self.bucket_target_row.setVisible(is_bucket or is_lasso_fill) 

1637 self.bucket_adjacent.setVisible(uses_bucket_region) 

1638 self.bucket_include_sub.setVisible(is_bucket) 

1639 self.bucket_include_colors.setVisible(is_bucket) 

1640 self.bucket_island_replace.setVisible(is_bucket) 

1641 self.bucket_gap_row.setVisible(uses_bucket_region) 

1642 self.bucket_island_fill_row.setVisible(uses_bucket_region) 

1643 self.bucket_extend_row.setVisible(is_bucket) 

1644 self.bucket_require_closed.setVisible(uses_bucket_region) 

1645 self.fill_opacity_row.setVisible(is_bucket or is_lasso_fill) 

1646 self.bucket_all_frames.setVisible(is_bucket) 

1647 # 隙間閉じと塗り残し拾いは閉領域フィルでは常にこちらが決める。 

1648 self.bucket_close_gap.setVisible(is_bucket and not is_closed_fill) 

1649 self.bucket_island_fill_row.setVisible( 

1650 uses_bucket_region and not is_closed_fill 

1651 ) 

1652 self.bucket_gap_width_title.setVisible(is_closed_fill) 

1653 

1654 self.dust_mode_label.setVisible(is_dust) 

1655 self.dust_mode.setVisible(is_dust) 

1656 self.dust_size_label.setVisible(is_dust) 

1657 self.dust_size.setVisible(is_dust) 

1658 self.dust_selected_only.setVisible(is_dust) 

1659 self.dust_all_frames.setVisible(is_dust) 

1660 self.dust_apply.setVisible(is_dust) 

1661 

1662 self.size_title.setText( 

1663 tr("<b>ラインサイズ</b>") if is_line else tr("<b>ブラシサイズ</b>") 

1664 ) 

1665 self.size_title.setVisible(uses_size) 

1666 self.brush_size_spinbox.setPressurePopupEnabled(tid == "brush") 

1667 # ラインは筆圧を検出しないため、筆圧設定はブラシ時だけ表示する。 

1668 self.pressure_settings_button.setVisible(tid == "brush") 

1669 self.size_slider.setVisible(uses_size) 

1670 self.brush_size_spinbox.setVisible(uses_size) 

1671 self.brush_stabilizer_label.setVisible(tid == "brush") 

1672 self.brush_stabilizer.setVisible(tid == "brush") 

1673 

1674 

1675 def _include_slot_color(self): 

1676 """含み塗りの枠へ登録する色。背景色モードでは背景(白)を登録する。""" 

1677 if self.color_mode == "transparent": 1677 ↛ 1678line 1677 didn't jump to line 1678 because the condition on line 1677 was never true

1678 return QColor("white") 

1679 return QColor( 

1680 self.main_color if self.color_mode == "main" else self.sub_color 

1681 ) 

1682 

1683 def step_brush_size(self, direction): 

1684 """[ / ] でブラシサイズを1段(約15%、最低0.5px)変える。""" 

1685 spin = self.brush_size_spinbox 

1686 current = float(spin.value()) 

1687 if direction > 0: 

1688 target = max(current + 0.5, current * 1.15) 

1689 else: 

1690 target = min(current - 0.5, current / 1.15) 

1691 # 数値欄の刻み(0.5px)にそろえる。 

1692 target = round(target * 2) / 2 

1693 spin.setValue(max(spin.minimum(), min(spin.maximum(), target))) 

1694 

1695 def set_color_mode(self, mode): 

1696 self.color_mode=mode; self.refresh_swatches(); self.sync_sliders(); self.colorModeChanged.emit(mode) 

1697 

1698 def set_colors(self, main, sub, mode, transparent_display=None): 

1699 self.main_color=QColor(main); self.sub_color=QColor(sub); self.color_mode=mode 

1700 if transparent_display is not None: self.transparent_display_color=QColor(transparent_display) 

1701 self.refresh_swatches(); self.sync_sliders() 

1702 

1703 def refresh_swatches(self): 

1704 palette = theme.palette() 

1705 accent = palette["accent"] 

1706 hover = palette["accent_hover"] 

1707 def style(c, selected): 

1708 border = ( 

1709 f"2px solid {accent}" if selected else f"1px solid {palette['border']}" 

1710 ) 

1711 return ( 

1712 f"QPushButton{{background:{c.name()};border:{border};" 

1713 "padding:0;border-radius:4px;}" 

1714 f"QPushButton:hover{{border-color:{hover};}}" 

1715 ) 

1716 self.main_btn.setStyleSheet(style(self.main_color,self.color_mode=="main")) 

1717 self.sub_btn.setStyleSheet(style(self.sub_color,self.color_mode=="sub")) 

1718 self.background_btn.setStyleSheet( 

1719 style(self.transparent_display_color, self.color_mode == "transparent") 

1720 ) 

1721 corner_style = ( 

1722 "QPushButton{font-size:12px;padding:0;border:0;" 

1723 f"background:transparent;color:{palette['text']};border-radius:3px;}}" 

1724 f"QPushButton:hover{{background:{palette['hover']};}}" 

1725 ) 

1726 self.swap_colors_button.setStyleSheet(corner_style) 

1727 self.reset_colors_button.setStyleSheet(corner_style) 

1728 muted = palette["text_muted"] 

1729 for caption, hex_label, color, selected in ( 

1730 (self.main_caption, self.main_code_edit, self.main_color, 

1731 self.color_mode == "main"), 

1732 (self.sub_caption, self.sub_code_edit, self.sub_color, 

1733 self.color_mode == "sub"), 

1734 (self.background_label, None, self.transparent_display_color, 

1735 self.color_mode == "transparent"), 

1736 ): 

1737 caption.setStyleSheet( 

1738 f"font-size:11px;font-weight:700;color:{accent};" 

1739 if selected else f"font-size:11px;color:{muted};" 

1740 ) 

1741 if hex_label is not None: 

1742 if not hex_label.hasFocus(): 1742 ↛ 1744line 1742 didn't jump to line 1744 because the condition on line 1742 was always true

1743 hex_label.setText(color.name().upper()) 

1744 hex_label.setStyleSheet( 

1745 "QLineEdit{font-size:11px;" 

1746 "font-family:Menlo,Consolas,monospace;padding:0 2px;" 

1747 + ("font-weight:700;" if selected else f"color:{muted};") 

1748 + "}" 

1749 ) 

1750 

1751 def _apply_color_code(self, mode): 

1752 """メイン/サブ横のカラーコード欄の入力をその色に反映する。""" 

1753 edit = self.main_code_edit if mode == "main" else self.sub_code_edit 

1754 current = self.main_color if mode == "main" else self.sub_color 

1755 text = edit.text().strip() 

1756 if text and not text.startswith("#"): 1756 ↛ 1758line 1756 didn't jump to line 1758 because the condition on line 1756 was always true

1757 text = "#" + text 

1758 color = QColor(text) 

1759 if not color.isValid() or len(text) not in (4, 7): 

1760 edit.setText(current.name().upper()) 

1761 return 

1762 if color == current and self.color_mode == mode: 1762 ↛ 1763line 1762 didn't jump to line 1763 because the condition on line 1762 was never true

1763 edit.setText(current.name().upper()) 

1764 return 

1765 self.color_mode = mode 

1766 if mode == "main": 1766 ↛ 1769line 1766 didn't jump to line 1769 because the condition on line 1766 was always true

1767 self.main_color = color 

1768 else: 

1769 self.sub_color = color 

1770 self.refresh_swatches() 

1771 self.sync_sliders() 

1772 self.colorChanged.emit(mode, color) 

1773 

1774 def _copy_color_code(self, mode): 

1775 clipboard = QApplication.clipboard() 

1776 if clipboard is not None: 1776 ↛ exitline 1776 didn't return from function '_copy_color_code' because the condition on line 1776 was always true

1777 color = self.main_color if mode == "main" else self.sub_color 

1778 clipboard.setText(color.name().upper()) 

1779 

1780 @staticmethod 

1781 def _reset_colors_icon(size): 

1782 """初期色アイコン:小さな黒と白の重ね四角。""" 

1783 pixmap = QPixmap(size * 2, size * 2) 

1784 pixmap.setDevicePixelRatio(2.0) 

1785 pixmap.fill(Qt.GlobalColor.transparent) 

1786 painter = QPainter(pixmap) 

1787 side = max(4, round(size * 0.55)) 

1788 off = size - side - 1 

1789 painter.setPen(QPen(QColor("#808080"), 1)) 

1790 painter.setBrush(QColor("white")) 

1791 painter.drawRect(off, off, side, side) 

1792 painter.setBrush(QColor("black")) 

1793 painter.drawRect(1, 1, side, side) 

1794 painter.end() 

1795 return QIcon(pixmap) 

1796 

1797 def apply_theme(self): 

1798 """Re-apply palette-derived styling after a theme/accent change.""" 

1799 c = theme.palette() 

1800 self.color_wheel_box.setStyleSheet( 

1801 "QWidget#colorPickerSurface{background:%s;" 

1802 "border:1px solid %s;border-radius:6px;}" % (c["surface_alt"], c["border"]) 

1803 ) 

1804 self.active.setStyleSheet( 

1805 "color:%s;font-size:14px;font-weight:600;padding:2px 2px 6px 2px;" 

1806 "border-bottom:1px solid %s;margin-bottom:2px;" % (c["text"], c["border"]) 

1807 ) 

1808 self.refresh_swatches() 

1809 self.update_slider_gradients() 

1810 

1811 def clear_slider_layout(self): 

1812 while self.slider_layout.rowCount(): self.slider_layout.removeRow(0) 

1813 self.color_sliders=[] 

1814 self.color_value_labels=[] 

1815 

1816 #: スライダーモードごとのチャンネル定義(表示名, 最小, 最大)。 

1817 SLIDER_SPECS = { 

1818 "RGB": [("R", 0, 255), ("G", 0, 255), ("B", 0, 255)], 

1819 "HLS": [("H", 0, 359), ("L", 0, 255), ("S", 0, 255)], 

1820 "CMYK": [("C", 0, 255), ("M", 0, 255), ("Y", 0, 255), ("K", 0, 255)], 

1821 } 

1822 

1823 def set_wheel_mode(self, mode): 

1824 mode = str(mode).upper() 

1825 if mode not in HSVColorWheel.MODES: 

1826 return 

1827 self.wheel_mode = mode 

1828 self.hsv_wheel.setMode(mode) 

1829 

1830 def set_wheel_hue_mode(self, mode): 

1831 mode = str(mode).upper() 

1832 if mode not in HSVColorWheel.HUE_MODES: 

1833 return 

1834 self.wheel_hue_mode = mode 

1835 self.hsv_wheel.setHueMode(mode) 

1836 

1837 def set_slider_mode(self, mode): 

1838 mode = str(mode).upper() 

1839 if mode not in self.SLIDER_SPECS or mode == self.slider_mode: 1839 ↛ 1840line 1839 didn't jump to line 1840 because the condition on line 1839 was never true

1840 return 

1841 self.slider_mode = mode 

1842 self.rebuild_color_sliders(mode) 

1843 

1844 def rebuild_color_sliders(self, mode): 

1845 mode = str(mode).upper() 

1846 if mode not in self.SLIDER_SPECS: 1846 ↛ 1847line 1846 didn't jump to line 1847 because the condition on line 1846 was never true

1847 mode = "RGB" 

1848 self.slider_mode = mode 

1849 self.clear_slider_layout() 

1850 for name, lo, hi in self.SLIDER_SPECS[mode]: 

1851 slider = QSlider(Qt.Orientation.Horizontal) 

1852 slider.setRange(lo, hi) 

1853 slider.setSingleStep(1) 

1854 slider.setProperty("valueScale", 1.0) 

1855 slider.setProperty("channel", name) 

1856 value_control = SliderValueSpinBox(slider, scale=1.0, step=1.0) 

1857 if mode == "HLS" and name == "H": 

1858 value_control.setWrapping(True) 

1859 row = QWidget() 

1860 row_layout = QHBoxLayout(row) 

1861 row_layout.setContentsMargins(0, 0, 0, 0) 

1862 row_layout.addWidget(slider, 1) 

1863 row_layout.addWidget(value_control) 

1864 slider.valueChanged.connect(self.slider_color_changed) 

1865 self.slider_layout.addRow(name, row) 

1866 self.color_sliders.append(slider) 

1867 self.color_value_labels.append(value_control) 

1868 self.sync_sliders() 

1869 self.update_slider_gradients() 

1870 

1871 def active_color(self): return self.main_color if self.color_mode=="main" else self.sub_color 

1872 

1873 def _channel_values_from_color(self, color): 

1874 """現在のスライダーモードにおける各チャンネル値を返す。""" 

1875 if self.slider_mode == "RGB": 

1876 return [color.red(), color.green(), color.blue()] 

1877 if self.slider_mode == "HLS": 1877 ↛ 1884line 1877 didn't jump to line 1884 because the condition on line 1877 was always true

1878 return [ 

1879 max(0, color.hslHue()), 

1880 color.lightness(), 

1881 color.hslSaturation(), 

1882 ] 

1883 # CMYK 

1884 cyan, magenta, yellow, black, _alpha = color.getCmyk() 

1885 return [cyan, magenta, yellow, black] 

1886 

1887 def _color_from_channel_values(self, values): 

1888 """スライダー値から現在のモードに応じた色を組み立てる。""" 

1889 clamp = lambda v: max(0, min(255, int(v))) 

1890 if self.slider_mode == "RGB": 1890 ↛ 1891line 1890 didn't jump to line 1891 because the condition on line 1890 was never true

1891 return QColor(clamp(values[0]), clamp(values[1]), clamp(values[2])) 

1892 if self.slider_mode == "HLS": 1892 ↛ 1895line 1892 didn't jump to line 1895 because the condition on line 1892 was always true

1893 hue = max(0, min(359, int(values[0]))) 

1894 return QColor.fromHsl(hue, clamp(values[2]), clamp(values[1])) 

1895 return QColor.fromCmyk( 

1896 clamp(values[0]), clamp(values[1]), clamp(values[2]), clamp(values[3]) 

1897 ) 

1898 

1899 def sync_sliders(self): 

1900 self.hsv_wheel.setEnabled(True) 

1901 wheel_color = ( 

1902 self.main_color 

1903 if self.color_mode == "transparent" 

1904 else self.active_color() 

1905 ) 

1906 self.hsv_wheel.setColor(wheel_color) 

1907 if not self.color_sliders or self.color_mode == "transparent": 

1908 return 

1909 values = self._channel_values_from_color(self.active_color()) 

1910 for index, slider in enumerate(self.color_sliders): 

1911 if index >= len(values): 1911 ↛ 1912line 1911 didn't jump to line 1912 because the condition on line 1911 was never true

1912 break 

1913 scale = float(slider.property("valueScale") or 1.0) 

1914 slider_value = int(round(float(values[index]) * scale)) 

1915 slider_value = max( 

1916 slider.minimum(), 

1917 min(slider.maximum(), slider_value), 

1918 ) 

1919 slider.blockSignals(True) 

1920 slider.setValue(slider_value) 

1921 slider.blockSignals(False) 

1922 if index < len(self.color_value_labels): 1922 ↛ 1910line 1922 didn't jump to line 1910 because the condition on line 1922 was always true

1923 self.color_value_labels[index].setDisplayValue( 

1924 slider_value / scale 

1925 ) 

1926 self.update_slider_gradients() 

1927 

1928 def update_slider_gradients(self): 

1929 specs = self.SLIDER_SPECS.get(self.slider_mode) 

1930 if not specs or len(self.color_sliders) != len(specs): 1930 ↛ 1931line 1930 didn't jump to line 1931 because the condition on line 1930 was never true

1931 return 

1932 active = self.active_color() if self.color_mode != "transparent" else QColor("black") 

1933 if self.slider_mode == "RGB": 

1934 gradients = [ 

1935 "stop:0 rgb(0,%d,%d), stop:1 rgb(255,%d,%d)" % (active.green(), active.blue(), active.green(), active.blue()), 

1936 "stop:0 rgb(%d,0,%d), stop:1 rgb(%d,255,%d)" % (active.red(), active.blue(), active.red(), active.blue()), 

1937 "stop:0 rgb(%d,%d,0), stop:1 rgb(%d,%d,255)" % (active.red(), active.green(), active.red(), active.green()), 

1938 ] 

1939 elif self.slider_mode == "HLS": 1939 ↛ 1951line 1939 didn't jump to line 1951 because the condition on line 1939 was always true

1940 hue = max(0, active.hslHue()) 

1941 sat = max(1, active.hslSaturation()) 

1942 mid = QColor.fromHsl(hue, sat, 128).name() 

1943 gray = QColor.fromHsl(hue, 0, 128).name() 

1944 pure = QColor.fromHsl(hue, sat, 128).name() 

1945 gradients = [ 

1946 "stop:0 #ff0000, stop:0.17 #ffff00, stop:0.33 #00ff00, stop:0.50 #00ffff, stop:0.67 #0000ff, stop:0.83 #ff00ff, stop:1 #ff0000", 

1947 f"stop:0 #000000, stop:0.5 {mid}, stop:1 #ffffff", 

1948 f"stop:0 {gray}, stop:1 {pure}", 

1949 ] 

1950 else: # CMYK 

1951 gradients = [ 

1952 "stop:0 #ffffff, stop:1 #00ffff", 

1953 "stop:0 #ffffff, stop:1 #ff00ff", 

1954 "stop:0 #ffffff, stop:1 #ffff00", 

1955 "stop:0 #ffffff, stop:1 #000000", 

1956 ] 

1957 for slider, gradient in zip(self.color_sliders, gradients): 

1958 slider.setStyleSheet( 

1959 "QSlider::groove:horizontal{height:14px;border:none;" 

1960 "border-radius:7px;" 

1961 "background:qlineargradient(x1:0,y1:0,x2:1,y2:0,%s);}" 

1962 # Keep the gradient fully visible: the filled/empty halves must 

1963 # not paint over it. The global theme gives them a background 

1964 # *and* a border; Qt merges declarations per property, so both 

1965 # must be cleared or a frame is drawn around the colour bar. 

1966 "QSlider::sub-page:horizontal{background:transparent;border:none;}" 

1967 "QSlider::add-page:horizontal{background:transparent;border:none;}" 

1968 # Round, ring-style handle that reveals the colour beneath it. 

1969 "QSlider::handle:horizontal{width:14px;height:14px;margin:-3px 0;" 

1970 "border:2px solid white;background:transparent;border-radius:9px;}" 

1971 % gradient 

1972 ) 

1973 

1974 def slider_color_changed(self): 

1975 specs = self.SLIDER_SPECS.get(self.slider_mode) 

1976 if ( 1976 ↛ 1981line 1976 didn't jump to line 1981 because the condition on line 1976 was never true

1977 self.color_mode == "transparent" 

1978 or not specs 

1979 or len(self.color_sliders) != len(specs) 

1980 ): 

1981 return 

1982 values = [slider.value() for slider in self.color_sliders] 

1983 color = self._color_from_channel_values(values) 

1984 

1985 if self.color_mode == "main": 1985 ↛ 1988line 1985 didn't jump to line 1988 because the condition on line 1985 was always true

1986 self.main_color = color 

1987 else: 

1988 self.sub_color = color 

1989 self.refresh_swatches() 

1990 self.hsv_wheel.setColor(color) 

1991 self.update_slider_gradients() 

1992 self.colorChanged.emit(self.color_mode, color) 

1993 

1994 def wheel_color_changed(self, color): 

1995 if self.color_mode == "transparent": 1995 ↛ 1999line 1995 didn't jump to line 1999 because the condition on line 1995 was always true

1996 # The background colour is display-only. Starting an edit on the 

1997 # colour wheel therefore returns to the main drawing colour. 

1998 self.set_color_mode("main") 

1999 color = QColor(color) 

2000 if self.color_mode == "main": 2000 ↛ 2003line 2000 didn't jump to line 2003 because the condition on line 2000 was always true

2001 self.main_color = color 

2002 else: 

2003 self.sub_color = color 

2004 self.refresh_swatches() 

2005 self.sync_sliders() 

2006 self.colorChanged.emit(self.color_mode, color)