Coverage for paintmaskanimator/toolpanel.py: 88%
1179 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
1from 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)
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]
45def tool_label(tool_id: str) -> str:
46 """The display name for a tool, translated at call time.
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)
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)
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.
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)
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)
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()
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)
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()
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_()
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
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
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()
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)
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 )
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()
300 def set_swatch_colors(self, main, sub, mode, background=None):
301 self.color_swatch.set_colors(main, sub, mode, background)
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)
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)
324 def minimumSizeHint(self):
325 return QSize(self.width_for_columns(1), 0)
327 def sizeHint(self):
328 return QSize(self.width_for_columns(1), self.CELL_SIZE * len(self.items))
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 )
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()
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)
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)
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)
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)
404class IncludeColorSlots(QWidget):
405 """バケツの含み塗りの対象色を登録しておく、数色分の枠。
407 枠をクリックすると現在の描画色を登録し、右クリックで消せる。登録した色は
408 設定に保存して次回も使う。1色も登録していなければサブカラーを含む
409 (以前の「サブカラーを含み塗り」と同じ動作)。
411 登録した色は枠ごとに ON/OFF できる(Issue #43)。色トレス線が重なっている
412 ところを塗るとき、含む色を消さずに切り換えられるようにするため。OFF の枠は
413 斜線を重ねて見分ける。
414 """
416 SLOT_COUNT = 3
417 CONFIG_KEY = "bucket_include_colors"
418 ENABLED_CONFIG_KEY = "bucket_include_colors_enabled"
419 SLOT_SIZE = 22
421 # 枠へ色を登録した(含み塗りを自動でONにするため)。
422 colorRegistered = Signal()
423 changed = Signal()
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()
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 )
461 def registered_colors(self):
462 """ON/OFF に関わらず登録してある RGB タプル。"""
463 return tuple(color for color in self._colors if color is not None)
465 def is_enabled(self, index):
466 return bool(0 <= index < self.SLOT_COUNT and self._enabled[index])
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()
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()
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()
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))
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)
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 )
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()))
581class ToolPanel(QWidget):
582 #: カラーサークル上部のメイン/サブ重ねスウォッチの一辺(px)。
583 COLOR_WELL_SIZE = 64
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
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)
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)
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)
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()
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)
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)
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 ))
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)
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)
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)
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)
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)
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)
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 )
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 ))
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)
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()
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)
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)
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)
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 )
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)
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)
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)
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)
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 )
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 )
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 )
1448 v.addStretch(); self.select_tool("brush"); self.refresh_swatches()
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()
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_()
1494 def _sync_transform_quality_options(self, enabled):
1495 self.selection_all_frames.setToolTip(
1496 tr("選択範囲の変形をすべてのコマへ適用します。")
1497 + (tr("クオリティ変形はコマごとに時間がかかります。")
1498 if bool(enabled) else "")
1499 )
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 )
1514 def set_tween_active(self, active):
1515 self._tween_active = bool(active)
1516 self._refresh_transform_line_width_visibility()
1518 def set_transform_line_colors_available(self, available):
1519 self._has_transform_line_colors = bool(available)
1520 self._refresh_transform_line_width_visibility()
1522 def toggle_selection_all_frames(self):
1523 if self.selection_all_frames.isEnabled():
1524 self.selection_all_frames.toggle()
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()
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)
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
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)
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 )
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)
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)
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)
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")
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 )
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)))
1695 def set_color_mode(self, mode):
1696 self.color_mode=mode; self.refresh_swatches(); self.sync_sliders(); self.colorModeChanged.emit(mode)
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()
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 )
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)
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())
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)
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()
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=[]
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 }
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)
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)
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)
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()
1871 def active_color(self): return self.main_color if self.color_mode=="main" else self.sub_color
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]
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 )
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()
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 )
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)
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)
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)