Coverage for paintmaskanimator/timeline.py: 66%
1889 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
1import math
2from pathlib import Path
3from .i18n import tr
4from .constants import CTRL_KEY_LABEL, HOLD_ZOOM_KEY_LABEL
5from PySide6.QtCore import QEvent, QItemSelectionModel, QPointF, QRectF, QSize, Qt, Signal
6from PySide6.QtGui import (
7 QBrush,
8 QColor,
9 QFont,
10 QIcon,
11 QPainter,
12 QPainterPath,
13 QPen,
14 QPixmap,
15 QPolygonF,
16)
17from PySide6.QtWidgets import (
18 QAbstractItemView,
19 QApplication,
20 QCheckBox,
21 QFrame,
22 QHBoxLayout,
23 QHeaderView,
24 QLabel,
25 QListWidget,
26 QListWidgetItem,
27 QMenu,
28 QPushButton,
29 QSizePolicy,
30 QSlider,
31 QSpinBox,
32 QStyle,
33 QStyledItemDelegate,
34 QTabBar,
35 QTableWidget,
36 QTableWidgetItem,
37 QVBoxLayout,
38 QWidget,
39)
40from . import icons, theme, time_display, tooltip
41from .cell_numbering import changed_count, normalization_mapping
42from .tween_groups import tween_groups
45def _titled_tip(title, detail):
46 """アイコンだけのボタン用に、1行目へ操作名を太字で置いたツールチップ。"""
47 return tooltip.titled(title, detail)
50class LayerListDelegate(QStyledItemDelegate):
51 """レイヤー行を描画する。表示切替の目アイコンを固定幅に置き、名前の位置を変えない。
53 下書きレイヤーは、行の左端の破線・鉛筆アイコン・「下書き」バッジで示し、
54 ひと目で通常のレイヤーと区別できるようにする。
55 """
57 NAME_ROLE = Qt.ItemDataRole.UserRole + 3
58 VISIBLE_ROLE = Qt.ItemDataRole.UserRole + 4
59 DRAFT_ROLE = Qt.ItemDataRole.UserRole + 6
60 EXPANDED_ROLE = Qt.ItemDataRole.UserRole + 7
61 BAND_HEIGHT_ROLE = Qt.ItemDataRole.UserRole + 8
62 # 左端から「>(サムネイルの開閉)」「目(表示切替)」「名前」の順に並べる。
63 CHEVRON_WIDTH = 18
64 MARKER_WIDTH = 50
65 ICON_SIZE = 16
67 def paint(self, painter, option, index):
68 painter.save()
69 painter.setRenderHint(QPainter.RenderHint.Antialiasing)
70 c = theme.palette()
71 selected = bool(option.state & QStyle.StateFlag.State_Selected)
72 visible = bool(index.data(self.VISIBLE_ROLE))
73 draft = bool(index.data(self.DRAFT_ROLE))
74 rect = option.rect
75 painter.fillRect(rect, QColor(c["selection"] if selected else c["surface_alt"]))
76 if selected:
77 # 選択行は薄いアクセント色の面に、左端のアクセント線で示す。
78 painter.fillRect(rect.left(), rect.top(), 3, rect.height(), QColor(c["accent"]))
79 elif draft: 79 ↛ 80line 79 didn't jump to line 80 because the condition on line 79 was never true
80 painter.setPen(QPen(QColor(c["warning"]), 3, Qt.PenStyle.DashLine))
81 painter.drawLine(rect.left() + 1, rect.top() + 2, rect.left() + 1, rect.bottom() - 2)
83 dpr = painter.device().devicePixelRatioF() if painter.device() else 1.0
84 # サムネイルを開いた行でも、アイコンと名前は上の段にそろえる。
85 band_height = int(index.data(self.BAND_HEIGHT_ROLE) or rect.height())
86 band_height = min(rect.height(), max(1, band_height))
87 rect = rect.adjusted(0, 0, 0, band_height - rect.height())
88 expanded = bool(index.data(self.EXPANDED_ROLE))
89 chevron_size = 12
90 chevron = icons.pixmap(
91 "chevron_down" if expanded else "chevron_right",
92 chevron_size,
93 c["text"] if expanded else c["text_muted"],
94 device_pixel_ratio=dpr,
95 )
96 painter.drawPixmap(
97 int(rect.left() + (self.CHEVRON_WIDTH - chevron_size) / 2 + 2),
98 int(rect.top() + (rect.height() - chevron_size) / 2),
99 chevron,
100 )
101 eye = icons.pixmap(
102 "eye" if visible else "eye_off",
103 self.ICON_SIZE,
104 c["text"] if visible else c["text_muted"],
105 device_pixel_ratio=dpr,
106 )
107 painter.drawPixmap(
108 int(
109 rect.left() + self.CHEVRON_WIDTH
110 + (self.MARKER_WIDTH - self.CHEVRON_WIDTH - self.ICON_SIZE) / 2
111 ),
112 int(rect.top() + (rect.height() - self.ICON_SIZE) / 2),
113 eye,
114 )
116 name = index.data(self.NAME_ROLE)
117 if name is None:
118 name = index.data(Qt.ItemDataRole.DisplayRole) or ""
119 name_left = rect.left() + self.MARKER_WIDTH
120 right = rect.right() - 6
121 font = QFont(option.font)
122 if draft: 122 ↛ 124line 122 didn't jump to line 124 because the condition on line 122 was never true
123 # 右端に鉛筆アイコン付きの「下書き」バッジを置く。
124 badge_font = QFont(option.font)
125 badge_font.setPointSizeF(max(7.0, option.font.pointSizeF() * 0.78))
126 badge_font.setWeight(QFont.Weight.DemiBold)
127 painter.setFont(badge_font)
128 label = tr("下書き")
129 metrics = painter.fontMetrics()
130 badge_h = min(rect.height() - 6, metrics.height() + 4)
131 badge_w = metrics.horizontalAdvance(label) + 12 + 12
132 badge = QRectF(right - badge_w, rect.top() + (rect.height() - badge_h) / 2, badge_w, badge_h)
133 warning = QColor(c["warning"])
134 fill = QColor(warning)
135 fill.setAlpha(46)
136 painter.setPen(QPen(warning, 1))
137 painter.setBrush(fill)
138 painter.drawRoundedRect(badge, badge_h / 2, badge_h / 2)
139 pencil = icons.pixmap("draft", 10, c["warning"], c["warning"], dpr)
140 painter.drawPixmap(
141 int(badge.left() + 6), int(badge.center().y() - 5), pencil,
142 )
143 painter.setPen(warning)
144 painter.drawText(
145 badge.adjusted(18, 0, -5, 0),
146 Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft,
147 label,
148 )
149 painter.setBrush(Qt.BrushStyle.NoBrush)
150 right = badge.left() - 4
151 font.setItalic(True)
152 painter.setFont(font)
153 painter.setPen(QColor(c["text"] if visible else c["text_muted"]))
154 name_rect = QRectF(name_left, rect.top(), max(0, right - name_left), rect.height())
155 painter.drawText(
156 name_rect,
157 Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft,
158 painter.fontMetrics().elidedText(
159 str(name), Qt.TextElideMode.ElideRight, int(name_rect.width())
160 ),
161 )
163 painter.setPen(QPen(QColor(c["border"]), 1))
164 bottom = option.rect.bottom()
165 painter.drawLine(rect.left(), bottom, rect.right(), bottom)
166 painter.restore()
168 def sizeHint(self, option, index):
169 # 行の高さはタイムラインの行に合わせて項目ごとに設定される。
170 hint = index.data(Qt.ItemDataRole.SizeHintRole)
171 height = hint.height() if isinstance(hint, QSize) and hint.height() > 0 else 28
172 return QSize(190, height)
174 def updateEditorGeometry(self, editor, option, index):
175 """表示切替記号を避け、レイヤー名の領域だけを編集欄にする。"""
176 band_height = int(index.data(self.BAND_HEIGHT_ROLE) or option.rect.height())
177 rect = option.rect.adjusted(
178 0, 0, 0, min(0, band_height - option.rect.height())
179 )
180 editor.setGeometry(rect.adjusted(self.MARKER_WIDTH, 1, -4, -1))
182 def eventFilter(self, editor, event):
183 # MainWindow の「Enter=変形確定」より名前編集の確定を優先する。
184 if ( 184 ↛ 188line 184 didn't jump to line 188 because the condition on line 184 was never true
185 event.type() == QEvent.Type.ShortcutOverride
186 and event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter)
187 ):
188 event.accept()
189 return True
190 return super().eventFilter(editor, event)
193class TimelineCellDelegate(QStyledItemDelegate):
194 """タイムラインのセルを、クリップ状の帯として描く。
196 キーの先頭は番号の箱、続くコマは色の面と横線、空セルは✗と波線、
197 右端は伸縮の取っ手で示す。トゥイーン中の区間は紫の面と矢印にし、
198 中割りのコマに点を置く。セルの文字(番号・ー・→・○・♦・◆)は
199 操作の判定に使うため、描画とは別にそのまま持たせておく。
200 """
201 STATE_ROLE = Qt.ItemDataRole.UserRole + 20
202 TWEEN_SPAN_ROLE = Qt.ItemDataRole.UserRole + 22
203 LABEL_ROLE = Qt.ItemDataRole.UserRole + 23
204 # セルとセルの間の空き(データ上は未使用)を、空セルとして見せるための
205 # (区間の先頭, 長さ, ✗を描くか)。操作の判定には使わない。
206 GAP_ROLE = Qt.ItemDataRole.UserRole + 24
207 # 確定済みのトゥイーンの帯に含まれるセル(移動・伸縮はさせない)。
208 TWEEN_GROUP_ROLE = Qt.ItemDataRole.UserRole + 25
209 BAR_INSET = 3
210 GRIP_WIDTH = 3
212 @staticmethod
213 def _state_colors(state_name):
214 c = theme.palette()
215 key = state_name if state_name in (
216 "key", "hold", "sheet_key", "sheet_hold", "blank", "uncreated"
217 ) else "uncreated"
218 return QColor(c[f"timeline_{key}"]), QColor(c[f"timeline_{key}_text"])
220 @staticmethod
221 def _span_colors(state_name, tween):
222 """(面の色, 箱の色, 線と文字の色) を返す。"""
223 c = theme.palette()
224 if tween:
225 ink = QColor(c["timeline_tween_text"])
226 return QColor(c["timeline_tween"]), QColor(c["timeline_tween"]), ink
227 prefix = "timeline_sheet_" if str(state_name).startswith("sheet_") else "timeline_"
228 return (
229 QColor(c[prefix + "hold"]),
230 QColor(c[prefix + "key"]),
231 QColor(c[prefix + "key_text"]),
232 )
234 @staticmethod
235 def _with_alpha(color, alpha):
236 color = QColor(color)
237 color.setAlpha(int(alpha))
238 return color
240 def _draw_grid(self, painter, rect, column):
241 c = theme.palette()
242 table = self.parent()
243 fps = max(1, int(getattr(table, "_fps", 24) or 24))
244 border = QColor(c["border"])
245 second = (column + 1) % fps == 0
246 painter.setPen(QPen(
247 self._with_alpha(c["text_muted"], 150) if second else border, 1
248 ))
249 painter.drawLine(
250 QPointF(rect.right() + 0.5, rect.top()),
251 QPointF(rect.right() + 0.5, rect.bottom() + 1),
252 )
253 painter.setPen(QPen(border, 1))
254 painter.drawLine(
255 QPointF(rect.left(), rect.bottom() + 0.5),
256 QPointF(rect.right() + 1, rect.bottom() + 0.5),
257 )
259 @staticmethod
260 def _wave_path(x0, x1, y, amplitude=2.2, period=8.0):
261 """左右のセルでつながるよう、ビューポートの x 座標から位相を決める。"""
262 path = QPainterPath()
263 x = float(x0)
264 path.moveTo(x, y + amplitude * math.sin(2 * math.pi * x / period))
265 while x < x1:
266 x = min(float(x1), x + 1.0)
267 path.lineTo(x, y + amplitude * math.sin(2 * math.pi * x / period))
268 return path
270 def _draw_cross(self, painter, center, size, color):
271 painter.setPen(QPen(color, 1.6, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))
272 half = size / 2
273 painter.drawLine(
274 QPointF(center.x() - half, center.y() - half),
275 QPointF(center.x() + half, center.y() + half),
276 )
277 painter.drawLine(
278 QPointF(center.x() - half, center.y() + half),
279 QPointF(center.x() + half, center.y() - half),
280 )
282 def _draw_arrow_head(self, painter, tip_x, y, direction, color):
283 size = 5.0
284 head = QPolygonF([
285 QPointF(tip_x, y),
286 QPointF(tip_x - direction * size * 1.2, y - size * 0.8),
287 QPointF(tip_x - direction * size * 1.2, y + size * 0.8),
288 ])
289 painter.setPen(Qt.PenStyle.NoPen)
290 painter.setBrush(color)
291 painter.drawPolygon(head)
292 painter.setBrush(Qt.BrushStyle.NoBrush)
294 def _paint_gap(self, painter, rect, column, gap):
295 """空きのコマを、空セルと同じ ✗ と波線で描く(取っ手は付けない)。"""
296 start, _length, cross = gap
297 muted = QColor(theme.palette()["text_muted"])
298 table = self.parent()
299 # サムネイルを開いた行では、上の段にそろえる。
300 band_height = min(
301 rect.height(),
302 float(getattr(table, "_collapsed_row_height", rect.height())),
303 )
304 mid_y = rect.top() + band_height / 2
305 line_left = rect.left()
306 if column == start and cross:
307 box = QRectF(
308 rect.left() + 1, rect.top() + self.BAR_INSET,
309 rect.width() - 2, band_height - 2 * self.BAR_INSET,
310 )
311 self._draw_cross(
312 painter, box.center(), min(box.width(), box.height()) * 0.36, muted
313 )
314 line_left = box.right() + 1
315 line_right = rect.right() + 1
316 if line_right - line_left >= 3:
317 painter.setPen(QPen(muted, 1.4))
318 painter.setBrush(Qt.BrushStyle.NoBrush)
319 painter.drawPath(self._wave_path(line_left, line_right, mid_y))
321 def paint(self, painter, option, index):
322 c = theme.palette()
323 table = self.parent()
324 rect = QRectF(option.rect)
325 column = index.column()
326 state_name = index.data(self.STATE_ROLE) or "uncreated"
327 key_col = index.data(Qt.ItemDataRole.UserRole)
328 exposure = index.data(Qt.ItemDataRole.UserRole + 1)
329 text = str(index.data(Qt.ItemDataRole.DisplayRole) or "")
331 painter.save()
332 painter.setRenderHint(QPainter.RenderHint.Antialiasing)
333 painter.fillRect(rect, QColor(c["timeline_uncreated"]))
334 self._draw_grid(painter, rect, column)
336 gap = index.data(self.GAP_ROLE)
337 if key_col is None and gap: 337 ↛ 338line 337 didn't jump to line 338 because the condition on line 337 was never true
338 self._paint_gap(painter, rect, column, gap)
339 elif key_col is not None and exposure is not None:
340 key_col = int(key_col)
341 end_col = key_col + max(1, int(exposure)) - 1
342 is_start = column == key_col
343 is_end = column == end_col
344 tween = index.data(self.TWEEN_SPAN_ROLE)
345 blank = state_name == "blank"
346 fill, box, ink = self._span_colors(state_name, tween)
347 muted = QColor(c["text_muted"])
348 # サムネイルを開いた行では、番号の箱と線を上の段に置き、
349 # 帯の面だけを行の下まで伸ばす。
350 band_height = rect.height()
351 if index.row() in getattr(table, "_expanded_rows", ()):
352 band_height = min(
353 rect.height(),
354 float(getattr(table, "_collapsed_row_height", rect.height())),
355 )
356 top = rect.top() + self.BAR_INSET
357 bottom = rect.top() + band_height - self.BAR_INSET
358 bar_bottom = rect.bottom() + 1 - self.BAR_INSET
359 mid_y = (top + bottom) / 2
361 # 帯の面。区間の途中では左右をセルの外まで伸ばし、角丸を両端だけにする。
362 if not blank:
363 bar = QRectF(
364 rect.left() + 1 if is_start else rect.left() - 8,
365 top,
366 0,
367 bar_bottom - top,
368 )
369 bar.setRight(rect.right() if is_end else rect.right() + 9)
370 painter.save()
371 painter.setClipRect(rect.adjusted(0, 0, 1, 0))
372 painter.setPen(QPen(self._with_alpha(ink, 70), 1))
373 painter.setBrush(fill)
374 painter.drawRoundedRect(bar.adjusted(0.5, 0.5, -0.5, -0.5), 4, 4)
375 painter.restore()
377 line_left = rect.left()
378 if is_start:
379 box_rect = QRectF(rect.left() + 1, top, rect.width() - 2, bottom - top)
380 line_left = box_rect.right() + 1
381 label = str(index.data(self.LABEL_ROLE) or "")
382 if blank and not label:
383 self._draw_cross(
384 painter, box_rect.center(),
385 min(box_rect.width(), box_rect.height()) * 0.36, muted,
386 )
387 elif blank: 387 ↛ 389line 387 didn't jump to line 389 because the condition on line 387 was never true
388 # 連番の空き番号:点線の箱に薄い番号。
389 painter.setPen(QPen(muted, 1, Qt.PenStyle.DashLine))
390 painter.setBrush(Qt.BrushStyle.NoBrush)
391 painter.drawRoundedRect(box_rect.adjusted(0.5, 0.5, -0.5, -0.5), 4, 4)
392 painter.setPen(muted)
393 painter.drawText(box_rect, Qt.AlignmentFlag.AlignCenter, label)
394 else:
395 painter.setPen(QPen(self._with_alpha(ink, 150), 1))
396 painter.setBrush(box)
397 painter.drawRoundedRect(box_rect.adjusted(0.5, 0.5, -0.5, -0.5), 4, 4)
398 font = QFont(index.data(Qt.ItemDataRole.FontRole) or option.font)
399 font.setBold(True)
400 painter.setFont(font)
401 painter.setPen(ink)
402 painter.drawText(
403 box_rect.adjusted(2, 0, -2, 0),
404 Qt.AlignmentFlag.AlignCenter,
405 painter.fontMetrics().elidedText(
406 label, Qt.TextElideMode.ElideRight,
407 int(box_rect.width() - 4),
408 ),
409 )
411 grip_right = rect.right() - 2
412 line_right = rect.right() + 1
413 if is_end and not is_start:
414 line_right = grip_right - self.GRIP_WIDTH - 3
415 if tween == "forward" and is_end and not is_start:
416 line_right -= 8
418 if line_right - line_left >= 3:
419 if blank:
420 painter.setPen(QPen(muted, 1.4))
421 painter.setBrush(Qt.BrushStyle.NoBrush)
422 painter.drawPath(self._wave_path(line_left, line_right, mid_y))
423 elif tween:
424 painter.setPen(QPen(ink, 1.6))
425 reverse_head = tween == "reverse" and column == key_col + 1
426 start_x = line_left + (7 if reverse_head else 0)
427 if line_right > start_x: 427 ↛ 433line 427 didn't jump to line 433 because the condition on line 427 was always true
428 painter.drawLine(QPointF(start_x, mid_y), QPointF(line_right, mid_y))
429 else:
430 painter.setPen(QPen(self._with_alpha(ink, 200), 2))
431 painter.drawLine(QPointF(line_left, mid_y), QPointF(line_right, mid_y))
433 if tween:
434 if tween == "forward" and is_end and not is_start:
435 self._draw_arrow_head(painter, line_right + 7, mid_y, 1, ink)
436 elif tween == "reverse" and column == key_col + 1: 436 ↛ 438line 436 didn't jump to line 438 because the condition on line 436 was never true
437 # 逆生成は、キーの箱の右隣から左向きの矢印にする。
438 self._draw_arrow_head(painter, rect.left() + 1, mid_y, -1, ink)
439 if not is_start and not is_end:
440 # 中割りのコマ。再生位置のコマは赤で塗る。
441 current = column == getattr(table, "_playhead_column", None)
442 painter.setPen(QPen(QColor(c["error"]) if current else ink, 1.3))
443 painter.setBrush(QColor(c["error"]) if current else QColor(c["surface"]))
444 radius = 3.2 if current else 2.6
445 painter.drawEllipse(QPointF(rect.center().x(), mid_y), radius, radius)
446 painter.setBrush(Qt.BrushStyle.NoBrush)
448 # 確定済みのトゥイーンの帯は伸縮できないので、取っ手を描かない。
449 if is_end and not is_start and not index.data(self.TWEEN_GROUP_ROLE):
450 grip = QRectF(
451 grip_right - self.GRIP_WIDTH, mid_y - 6, self.GRIP_WIDTH, 12
452 )
453 painter.setPen(Qt.PenStyle.NoPen)
454 painter.setBrush(muted if blank else self._with_alpha(ink, 210))
455 painter.drawRoundedRect(grip, 1.5, 1.5)
456 painter.setBrush(Qt.BrushStyle.NoBrush)
457 elif text: 457 ↛ 458line 457 didn't jump to line 458 because the condition on line 457 was never true
458 painter.setPen(QColor(c["timeline_uncreated_text"]))
459 painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, text)
461 if index.row() in getattr(table, "_draft_rows", ()): 461 ↛ 462line 461 didn't jump to line 462 because the condition on line 461 was never true
462 hatch = QColor(c["warning"])
463 hatch.setAlpha(70)
464 painter.fillRect(rect, QBrush(hatch, Qt.BrushStyle.BDiagPattern))
465 if option.state & QStyle.StateFlag.State_Selected:
466 painter.setPen(QPen(QColor(c["error"]), 2))
467 painter.setBrush(Qt.BrushStyle.NoBrush)
468 painter.drawRoundedRect(rect.adjusted(1, 1, -1, -1), 4, 4)
469 painter.restore()
472class TimelineRuler(QHeaderView):
473 """コマ番号の見出し。毎コマの目盛り、秒の区切り、現在コマの札を描く。
475 見出しの文字(6コマおきの番号)はモデルのものをそのまま使う。
476 クリック・ドラッグでそのコマへ移動する(列の選択はしない)。
477 """
479 frameScrubbed = Signal(int)
480 rangeResized = Signal(str, int)
481 _range_drag: str | None = None
483 def _edge_at(self, x):
484 table = self.parent()
485 if not isinstance(table, TimelineTable) or table._sequence_mode: 485 ↛ 486line 485 didn't jump to line 486 because the condition on line 485 was never true
486 return None
487 start, end = table.display_range()
488 right = self.sectionViewportPosition(end - 1) + self.sectionSize(end - 1)
489 if abs(x - right) <= 6:
490 return 'end'
491 if abs(x - self.sectionViewportPosition(start)) <= 6: 491 ↛ 492line 491 didn't jump to line 492 because the condition on line 491 was never true
492 return 'start'
493 return None
495 def paintEvent(self, event):
496 super().paintEvent(event)
497 table = self.parent()
498 if not isinstance(table, TimelineTable) or table._sequence_mode: 498 ↛ 499line 498 didn't jump to line 499 because the condition on line 498 was never true
499 return
500 start, end = table.display_range()
501 left = self.sectionViewportPosition(start) + 2
502 right = self.sectionViewportPosition(end - 1) + self.sectionSize(end - 1) - 2
503 painter = QPainter(self.viewport())
504 painter.setPen(Qt.PenStyle.NoPen)
505 painter.setBrush(QColor(96, 96, 96, 68))
506 if left > 2: 506 ↛ 507line 506 didn't jump to line 507 because the condition on line 506 was never true
507 painter.drawRect(0, 0, left - 2, self.height())
508 if right + 2 < self.viewport().width(): 508 ↛ 510line 508 didn't jump to line 510 because the condition on line 508 was always true
509 painter.drawRect(right + 2, 0, self.viewport().width() - right - 2, self.height())
510 painter.setPen(QPen(QColor(theme.palette()['text_muted']), 3))
511 painter.drawLine(left, 1, right, 1)
512 for x in (left, right):
513 painter.drawLine(x, 0, x, self.height() - 1)
514 painter.end()
516 def _scrub(self, event):
517 count = self.count()
518 if count <= 0: 518 ↛ 519line 518 didn't jump to line 519 because the condition on line 518 was never true
519 return
520 index = self.logicalIndexAt(int(event.position().x()))
521 if index < 0: 521 ↛ 522line 521 didn't jump to line 522 because the condition on line 521 was never true
522 index = 0 if event.position().x() < 0 else count - 1
523 self.frameScrubbed.emit(int(index))
525 def mousePressEvent(self, event):
526 if event.button() == Qt.MouseButton.LeftButton: 526 ↛ 532line 526 didn't jump to line 532 because the condition on line 526 was always true
527 self._range_drag = self._edge_at(event.position().x())
528 if self._range_drag is None:
529 self._scrub(event)
530 event.accept()
531 return
532 super().mousePressEvent(event)
534 def mouseMoveEvent(self, event):
535 cursor = (Qt.CursorShape.SizeHorCursor
536 if self._range_drag or self._edge_at(event.position().x())
537 else Qt.CursorShape.ArrowCursor)
538 self.viewport().setCursor(cursor)
539 if self._range_drag:
540 table = self.parent()
541 if isinstance(table, TimelineTable): 541 ↛ 554line 541 didn't jump to line 554 because the condition on line 541 was always true
542 column = max(
543 0,
544 round((event.position().x() + self.offset()) / max(1, self.sectionSize(0))),
545 )
546 start, end = table._range_start, table._range_end
547 if self._range_drag == "start": 547 ↛ 548line 547 didn't jump to line 548 because the condition on line 547 was never true
548 start = min(column, end - 1)
549 else:
550 end = max(start + 1, column)
551 table._range_preview = (start, end)
552 table.viewport().update()
553 self.viewport().update()
554 event.accept()
555 return
556 if event.buttons() & Qt.MouseButton.LeftButton:
557 self._scrub(event)
558 event.accept()
559 return
560 super().mouseMoveEvent(event)
562 def leaveEvent(self, event): # noqa: N802 - Qt override
563 if self._range_drag is None:
564 self.viewport().setCursor(Qt.CursorShape.ArrowCursor)
565 super().leaveEvent(event)
567 def mouseReleaseEvent(self, event):
568 if event.button() == Qt.MouseButton.LeftButton: 568 ↛ 580line 568 didn't jump to line 580 because the condition on line 568 was always true
569 if self._range_drag:
570 edge = self._range_drag
571 self._range_drag = None
572 column = max(0, round((event.position().x() + self.offset()) / max(1, self.sectionSize(0))))
573 table = self.parent()
574 if isinstance(table, TimelineTable): 574 ↛ 577line 574 didn't jump to line 577 because the condition on line 574 was always true
575 table._range_preview = None
576 table.viewport().update()
577 self.rangeResized.emit(edge, column)
578 event.accept()
579 return
580 super().mouseReleaseEvent(event)
582 def paintSection(self, painter, rect, logicalIndex):
583 c = theme.palette()
584 table = self.parent()
585 fps = max(1, int(getattr(table, "_fps", 24) or 24))
586 rect = QRectF(rect)
587 painter.save()
588 painter.setRenderHint(QPainter.RenderHint.Antialiasing)
589 painter.fillRect(rect, QColor(c["surface_alt"]))
590 painter.setPen(QPen(QColor(c["border"]), 1))
591 painter.drawLine(
592 QPointF(rect.left(), rect.bottom() + 0.5),
593 QPointF(rect.right() + 1, rect.bottom() + 0.5),
594 )
595 second = logicalIndex % fps == 0
596 tick = QColor(c["text_muted"] if second else c["border"])
597 tick_height = rect.height() * (0.5 if second else 0.22)
598 painter.setPen(QPen(tick, 1))
599 painter.drawLine(
600 QPointF(rect.left() + 0.5, rect.bottom() + 1 - tick_height),
601 QPointF(rect.left() + 0.5, rect.bottom() + 1),
602 )
603 font = QFont(self.font())
604 font.setPointSizeF(max(8.0, font.pointSizeF() * 0.85))
605 painter.setFont(font)
606 playhead = getattr(table, "_playhead_column", None)
607 if playhead is not None and int(playhead) == logicalIndex:
608 badge = QRectF(rect.left() + 1, rect.top() + 3, rect.width() - 2, rect.height() - 3)
609 painter.setPen(Qt.PenStyle.NoPen)
610 painter.setBrush(QColor(c["error"]))
611 painter.drawRoundedRect(badge, 4, 4)
612 painter.fillRect(
613 QRectF(badge.left(), badge.bottom() - 4, badge.width(), 4),
614 QColor(c["error"]),
615 )
616 painter.setPen(QColor("#ffffff"))
617 font.setBold(True)
618 painter.setFont(font)
619 painter.drawText(badge, Qt.AlignmentFlag.AlignCenter, str(logicalIndex + 1))
620 else:
621 model = self.model()
622 label = (
623 model.headerData(logicalIndex, Qt.Orientation.Horizontal)
624 if model is not None else None
625 )
626 if label:
627 font.setBold(second)
628 painter.setFont(font)
629 painter.setPen(QColor(c["text"] if second else c["text_muted"]))
630 painter.drawText(
631 rect.adjusted(3, 1, 0, -int(tick_height * 0.6)),
632 Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
633 str(label),
634 )
635 painter.restore()
638class TimelineTable(QTableWidget):
639 cellMoveRequested = Signal(int, int, int, int)
640 cellCopyRequested = Signal(int, int, int, int)
641 sequenceRecallRequested = Signal(int, int, int)
642 multiCellMoveRequested = Signal(object, int, int, int, int)
643 blankFrameRequested = Signal(int, int)
644 exposureResizeRequested = Signal(int, int, int, str)
645 headerFrameRequested = Signal(int)
646 tweenRequested = Signal(int, int)
647 tweenMeshRequested = Signal(int, int)
648 tweenCancelRequested = Signal()
649 tweenEditRequested = Signal(int, int)
650 tweenReleaseRequested = Signal(int, int)
651 addFrameRequested = Signal(bool)
652 extendExposureRequested = Signal(int, int, int)
653 # セルの右クリックメニュー(Issue #48)。いずれも (表示行, 列)。
654 newAnimationCellRequested = Signal(int, int)
655 clearCellRequested = Signal(int, int)
656 cutCellRequested = Signal(int, int)
657 copyCellToClipboardRequested = Signal(int, int)
658 pasteCellRequested = Signal(int, int)
659 mergeClipRequested = Signal(int, int)
660 splitClipRequested = Signal(int, int)
661 insertFrameAtRequested = Signal(int, int)
662 deleteFrameAtRequested = Signal(int, int)
663 #: ("start"|"end", 列)。
664 playbackRangeEdgeRequested = Signal(str, int)
665 # タイムリマップをシートから写す(Issue #23)。
666 timeRemapCopyRequested = Signal()
668 END_HANDLE_ROLE = Qt.ItemDataRole.UserRole + 2
669 START_HANDLE_ROLE = Qt.ItemDataRole.UserRole + 4
670 TWEEN_PENDING_ROLE = Qt.ItemDataRole.UserRole + 21
671 HANDLE_HIT_WIDTH = 6
673 def __init__(self, parent=None):
674 super().__init__(parent)
675 # Mirrors the owning TimelineWidget's frame count; the widget writes it
676 # (``self.table._real_frame_count = ...``) and cell-hit tests read it.
677 self._real_frame_count = 1
678 # 「貼り付け」を出してよいか。TimelineWidget が控えの有無を書き込む。
679 self.cell_clipboard_filled = False
680 self._sequence_mode = False
681 self._drag_source = None
682 self._drag_preview_dest = None
683 self._duplicate_drag = False
684 self._sequence_numbers_by_row = {}
685 self._group_drag_cells = None
686 self._group_drag_anchor = None
687 self._group_drag_dest = None
688 self._resize_source = None
689 self._resize_preview_column = None
690 # 秒の区切り線と再生ヘッドの描画に使う(TimelineWidget が書き込む)。
691 self._fps = 24
692 self._playhead_column: int | None = None
693 self._range_start = 0
694 self._range_end = 1
695 self._range_preview: tuple[int, int] | None = None
696 # サムネイルを開いた行と、その行のキーの絵(TimelineWidget が書き込む)。
697 self._collapsed_row_height = 28
698 self._expanded_rows = set()
699 self._thumb_sources = {}
700 # 下書きレイヤーの行(TimelineWidget が書き込む)。
701 self._draft_rows: set[int] = set()
702 self._thumb_cache = {}
703 self.setMouseTracking(True)
704 self.setSelectionBehavior(
705 QAbstractItemView.SelectionBehavior.SelectItems
706 )
707 self.setDragDropMode(QAbstractItemView.DragDropMode.NoDragDrop)
708 self.ruler = TimelineRuler(Qt.Orientation.Horizontal, self)
709 self.setHorizontalHeader(self.ruler)
710 self.ruler.frameScrubbed.connect(self._ruler_scrubbed)
711 self.horizontalHeader().setMouseTracking(True)
712 self.horizontalHeader().viewport().setMouseTracking(True)
713 self.horizontalHeader().installEventFilter(self)
715 def display_range(self):
716 start, end = self._range_preview or (self._range_start, self._range_end)
717 start = max(0, min(int(start), max(0, self.columnCount() - 1)))
718 end = max(start + 1, min(int(end), max(1, self.columnCount())))
719 return start, end
721 def eventFilter(self, watched, event):
722 if watched is self.horizontalHeader():
723 if event.type() in (QEvent.Type.MouseButtonPress, QEvent.Type.MouseMove): 723 ↛ 724line 723 didn't jump to line 724 because the condition on line 723 was never true
724 buttons = event.buttons() if hasattr(event, "buttons") else Qt.MouseButton.NoButton
725 pressed = (
726 event.type() == QEvent.Type.MouseButtonPress
727 and event.button() == Qt.MouseButton.LeftButton
728 )
729 dragging = (
730 event.type() == QEvent.Type.MouseMove
731 and bool(buttons & Qt.MouseButton.LeftButton)
732 )
733 if pressed or dragging:
734 column = self.horizontalHeader().logicalIndexAt(
735 int(event.position().x())
736 )
737 if 0 <= column < self._real_frame_count:
738 self.headerFrameRequested.emit(column)
739 return True
740 return super().eventFilter(watched, event)
742 def _ruler_scrubbed(self, column):
743 # シークでは尺を変更しない。
744 column = max(0, min(int(column), self._real_frame_count - 1))
745 self.headerFrameRequested.emit(column)
747 def _edge_source_at(self, point):
748 index = self.indexAt(point)
749 if not index.isValid():
750 return None
751 item = self.item(index.row(), index.column())
752 if item is None or item.data(TimelineCellDelegate.TWEEN_GROUP_ROLE):
753 return None
754 key_col = item.data(Qt.ItemDataRole.UserRole)
755 exposure = item.data(Qt.ItemDataRole.UserRole + 1)
756 if key_col is None or exposure is None:
757 return None
758 key_col = int(key_col)
759 exposure = max(1, int(exposure))
760 end_col = key_col + exposure - 1
761 rect = self.visualRect(index)
762 x = point.x()
763 if index.column() == key_col and x <= rect.left() + self.HANDLE_HIT_WIDTH:
764 return (index.row(), key_col, "left")
765 if index.column() == end_col and x >= rect.right() - self.HANDLE_HIT_WIDTH:
766 return (index.row(), key_col, "right")
767 return None
769 def _column_from_x(self, x):
770 column = self.columnAt(int(x))
771 if column >= 0:
772 return column
773 count = self.columnCount()
774 if count <= 0:
775 return 0
776 if x < 0:
777 return 0
778 last = count - 1
779 last_right = self.columnViewportPosition(last) + self.columnWidth(last)
780 if x >= last_right:
781 width = max(1, self.columnWidth(last))
782 return last + 1 + int((x - last_right) // width)
783 return last
785 def _set_cell_drag_cursor(self):
786 """ALT複製中だけ、右下に+の付いたコピーカーソルを表示する。"""
787 self.setCursor(
788 Qt.CursorShape.DragCopyCursor
789 if self._duplicate_drag
790 else Qt.CursorShape.ClosedHandCursor
791 )
793 def mousePressEvent(self, event):
794 index = self.indexAt(event.position().toPoint())
795 self._drag_source = None
796 self._drag_preview_dest = None
797 self._duplicate_drag = False
798 self._group_drag_cells = None
799 self._group_drag_anchor = None
800 self._group_drag_dest = None
801 self._resize_source = None
802 self._resize_preview_column = None
804 if event.button() == Qt.MouseButton.LeftButton and index.isValid():
805 modifiers = event.modifiers()
806 self._duplicate_drag = bool(
807 modifiers & Qt.KeyboardModifier.AltModifier
808 )
809 multi_select = bool(
810 modifiers
811 & (
812 Qt.KeyboardModifier.ShiftModifier
813 | Qt.KeyboardModifier.ControlModifier
814 )
815 )
817 selected = {
818 (selected_index.row(), selected_index.column())
819 for selected_index in self.selectedIndexes()
820 if selected_index.isValid()
821 }
822 clicked = (index.row(), index.column())
824 # 複数選択済みの範囲をそのままドラッグする。
825 if (
826 not multi_select
827 and clicked in selected
828 and len(selected) > 1
829 ):
830 self._group_drag_cells = tuple(sorted(selected))
831 self._group_drag_anchor = clicked
832 self._group_drag_dest = clicked
833 self.setCurrentCell(
834 index.row(),
835 index.column(),
836 QItemSelectionModel.SelectionFlag.NoUpdate,
837 )
838 self.setCursor(Qt.CursorShape.ClosedHandCursor)
839 event.accept()
840 return
842 edge_source = (
843 None
844 if multi_select
845 else self._edge_source_at(
846 event.position().toPoint()
847 )
848 )
849 if edge_source is not None:
850 self._resize_source = edge_source
851 self._resize_preview_column = edge_source[1]
852 self.setCurrentCell(edge_source[0], edge_source[1])
853 self.cellClicked.emit(edge_source[0], edge_source[1])
854 self.setCursor(Qt.CursorShape.SizeHorCursor)
855 event.accept()
856 return
858 item = self.item(index.row(), index.column())
859 if (
860 not multi_select
861 and item
862 and not item.data(TimelineCellDelegate.TWEEN_GROUP_ROLE)
863 and (
864 item.data(TimelineCellDelegate.STATE_ROLE)
865 in ("key", "sheet_key")
866 or item.text() == "○"
867 )
868 ):
869 self._drag_source = (index.row(), index.column())
870 self._drag_preview_dest = self._drag_source
871 self.setCurrentCell(index.row(), index.column())
872 self.cellClicked.emit(index.row(), index.column())
873 self._set_cell_drag_cursor()
874 event.accept()
875 return
877 super().mousePressEvent(event)
879 def mouseMoveEvent(self, event):
880 if self._group_drag_cells is not None:
881 index = self.indexAt(event.position().toPoint())
882 if index.isValid():
883 self._group_drag_dest = (
884 index.row(),
885 index.column(),
886 )
887 self.setCursor(Qt.CursorShape.ClosedHandCursor)
888 self.viewport().update()
889 event.accept()
890 return
891 if self._resize_source is not None:
892 self._resize_preview_column = self._column_from_x(event.position().x())
893 self.setCursor(Qt.CursorShape.SizeHorCursor)
894 self.viewport().update()
895 event.accept()
896 return
897 if self._drag_source is not None:
898 index = self.indexAt(event.position().toPoint())
899 if index.isValid():
900 self._drag_preview_dest = (index.row(), index.column())
901 self._set_cell_drag_cursor()
902 self.viewport().update()
903 event.accept()
904 return
905 edge = self._edge_source_at(event.position().toPoint())
906 if edge is not None:
907 self.setCursor(Qt.CursorShape.SizeHorCursor)
908 else:
909 index = self.indexAt(event.position().toPoint())
910 item = self.item(index.row(), index.column()) if index.isValid() else None
911 self.setCursor(
912 Qt.CursorShape.OpenHandCursor
913 if item and not item.data(TimelineCellDelegate.TWEEN_GROUP_ROLE) and (
914 item.data(TimelineCellDelegate.STATE_ROLE)
915 in ("key", "sheet_key")
916 or item.text() == "○"
917 )
918 else Qt.CursorShape.ArrowCursor
919 )
920 super().mouseMoveEvent(event)
922 def leaveEvent(self, event):
923 if (
924 self._resize_source is None
925 and self._drag_source is None
926 and self._group_drag_cells is None
927 ):
928 self.unsetCursor()
929 super().leaveEvent(event)
931 def paintEvent(self, event):
932 super().paintEvent(event)
933 painter = QPainter(self.viewport())
934 if not self._sequence_mode and self.columnCount(): 934 ↛ 953line 934 didn't jump to line 953 because the condition on line 934 was always true
935 height = self.viewport().height()
936 # 6コマごとの帯を交互に薄く暗くし、時間のまとまりを追いやすくする。
937 painter.setPen(Qt.PenStyle.NoPen)
938 painter.setBrush(QColor(0, 0, 0, 10))
939 for start in range(6, self.columnCount(), 12):
940 end = min(start + 6, self.columnCount())
941 left = self.columnViewportPosition(start)
942 right = self.columnViewportPosition(end - 1) + self.columnWidth(end - 1)
943 painter.drawRect(left, 0, right - left, height)
944 # 尺の外側は内容を残したままグレーアウトする。
945 start, end = self.display_range()
946 painter.setBrush(QColor(96, 96, 96, 68))
947 left = self.columnViewportPosition(start)
948 if left > 0: 948 ↛ 949line 948 didn't jump to line 949 because the condition on line 948 was never true
949 painter.drawRect(0, 0, left, height)
950 right = self.columnViewportPosition(end - 1) + self.columnWidth(end - 1)
951 if right < self.viewport().width(): 951 ↛ 953line 951 didn't jump to line 953 because the condition on line 951 was always true
952 painter.drawRect(right, 0, self.viewport().width() - right, height)
953 if self._resize_source is not None and self._resize_preview_column is not None: 953 ↛ 954line 953 didn't jump to line 954 because the condition on line 953 was never true
954 row, key_col, edge = self._resize_source
955 item = self.item(row, key_col)
956 exposure = max(
957 1,
958 int(item.data(Qt.ItemDataRole.UserRole + 1) or 1)
959 if item is not None else 1,
960 )
961 fixed_end = key_col + exposure - 1
962 if edge == "left":
963 start_col = max(0, min(self._resize_preview_column, fixed_end))
964 end_col = fixed_end
965 else:
966 start_col = key_col
967 end_col = max(key_col, self._resize_preview_column)
968 painter.setPen(Qt.PenStyle.NoPen)
969 painter.setBrush(QColor(255, 215, 0, 110))
970 for column in range(start_col, min(end_col + 1, self.columnCount())):
971 index = self.model().index(row, column)
972 if index.isValid():
973 painter.drawRect(self.visualRect(index).adjusted(1, 1, -1, -1))
974 if ( 974 ↛ 979line 974 didn't jump to line 979 because the condition on line 974 was never true
975 self._group_drag_cells is not None
976 and self._group_drag_anchor is not None
977 and self._group_drag_dest is not None
978 ):
979 row_delta = (
980 self._group_drag_dest[0]
981 - self._group_drag_anchor[0]
982 )
983 column_delta = (
984 self._group_drag_dest[1]
985 - self._group_drag_anchor[1]
986 )
987 painter.setPen(QPen(QColor(229, 57, 53), 2))
988 painter.setBrush(QColor(255, 215, 0, 65))
989 for source_row, source_column in self._group_drag_cells:
990 row = source_row + row_delta
991 column = source_column + column_delta
992 index = self.model().index(row, column)
993 if index.isValid():
994 painter.drawRect(
995 self.visualRect(index).adjusted(1, 1, -1, -1)
996 )
998 if self._drag_source is not None and self._drag_preview_dest is not None: 998 ↛ 999line 998 didn't jump to line 999 because the condition on line 998 was never true
999 row, column = self._drag_preview_dest
1000 index = self.model().index(row, column)
1001 if index.isValid():
1002 rect = self.visualRect(index)
1003 painter.setPen(QPen(QColor(229, 57, 53), 3))
1004 painter.drawLine(rect.left(), rect.top() + 1, rect.left(), rect.bottom() - 1)
1005 self._paint_thumbnails(painter)
1006 self._paint_playhead(painter)
1007 painter.end()
1009 def _thumbnail(self, image, width, height, ratio):
1010 """縮小した絵をキャッシュする。絵が描き変わると cacheKey も変わる。"""
1011 key = (image.cacheKey(), int(width), int(height), round(ratio, 2))
1012 pixmap = self._thumb_cache.get(key)
1013 if pixmap is None:
1014 if len(self._thumb_cache) > 512: 1014 ↛ 1015line 1014 didn't jump to line 1015 because the condition on line 1014 was never true
1015 self._thumb_cache.clear()
1016 scaled = image.scaled(
1017 max(1, int(width * ratio)), max(1, int(height * ratio)),
1018 Qt.AspectRatioMode.KeepAspectRatio,
1019 Qt.TransformationMode.SmoothTransformation,
1020 )
1021 pixmap = QPixmap.fromImage(scaled)
1022 pixmap.setDevicePixelRatio(ratio)
1023 self._thumb_cache[key] = pixmap
1024 return pixmap
1026 def _paint_thumbnails(self, painter):
1027 """開いた行のキーのセルに、区間をまたいでサムネイルを描く。"""
1028 if not self._thumb_sources:
1029 return
1030 c = theme.palette()
1031 ratio = self.devicePixelRatioF()
1032 viewport_width = self.viewport().width()
1033 painter.save()
1034 painter.setRenderHint(QPainter.RenderHint.Antialiasing)
1035 painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
1036 for (row, column), (image, exposure) in self._thumb_sources.items():
1037 if row not in self._expanded_rows or image is None or image.isNull():
1038 continue
1039 left = self.columnViewportPosition(column)
1040 span_width = self.columnWidth(column) * max(1, int(exposure))
1041 if left > viewport_width or left + span_width < 0: 1041 ↛ 1042line 1041 didn't jump to line 1042 because the condition on line 1041 was never true
1042 continue
1043 top = self.rowViewportPosition(row)
1044 if top < -self.rowHeight(row) or top > self.viewport().height(): 1044 ↛ 1045line 1044 didn't jump to line 1045 because the condition on line 1044 was never true
1045 continue
1046 area_top = top + self._collapsed_row_height - 2
1047 area_height = self.rowHeight(row) - self._collapsed_row_height - 3
1048 # 右端の取っ手の分をあける(1コマだけのセルには取っ手がない)。
1049 area_width = span_width - (14 if int(exposure) > 1 else 7)
1050 if area_height < 10 or area_width < 10: 1050 ↛ 1051line 1050 didn't jump to line 1051 because the condition on line 1050 was never true
1051 continue
1052 aspect = image.width() / float(max(1, image.height()))
1053 height = float(area_height)
1054 width = height * aspect
1055 if width > area_width: 1055 ↛ 1056line 1055 didn't jump to line 1056 because the condition on line 1055 was never true
1056 width = float(area_width)
1057 height = width / aspect
1058 frame = QRectF(left + 4, area_top, width, height)
1059 painter.setPen(QPen(QColor(c["border"]), 1))
1060 painter.setBrush(QColor("#ffffff"))
1061 painter.drawRoundedRect(frame.adjusted(-0.5, -0.5, 0.5, 0.5), 3, 3)
1062 pixmap = self._thumbnail(image, width, height, ratio)
1063 size = pixmap.deviceIndependentSize()
1064 painter.drawPixmap(
1065 QPointF(
1066 frame.left() + (frame.width() - size.width()) / 2,
1067 frame.top() + (frame.height() - size.height()) / 2,
1068 ),
1069 pixmap,
1070 )
1071 painter.restore()
1073 def _paint_playhead(self, painter):
1074 """現在のコマに、全レイヤーを貫く赤い縦線を引く。"""
1075 column = self._playhead_column
1076 if column is None or not (0 <= int(column) < self.columnCount()):
1077 return
1078 x = self.columnViewportPosition(int(column))
1079 if x < -self.columnWidth(int(column)) or x > self.viewport().width(): 1079 ↛ 1080line 1079 didn't jump to line 1080 because the condition on line 1079 was never true
1080 return
1081 # コマの中心に線を引くと番号や点に重なるので、現在のコマの列を
1082 # 薄く色付けし、左右の境目に細い線を引く。
1083 color = QColor(theme.palette()["error"])
1084 width = self.columnWidth(int(column))
1085 height = self.viewport().height()
1086 tint = QColor(color)
1087 tint.setAlpha(26)
1088 painter.fillRect(QRectF(x, 0, width, height), tint)
1089 edge = QColor(color)
1090 edge.setAlpha(170)
1091 painter.setPen(QPen(edge, 1))
1092 painter.drawLine(QPointF(x + 0.5, 0), QPointF(x + 0.5, height))
1093 painter.drawLine(
1094 QPointF(x + width - 0.5, 0), QPointF(x + width - 0.5, height)
1095 )
1097 def mouseReleaseEvent(self, event):
1098 group_cells = self._group_drag_cells
1099 group_anchor = self._group_drag_anchor
1100 group_destination = self._group_drag_dest
1101 self._group_drag_cells = None
1102 self._group_drag_anchor = None
1103 self._group_drag_dest = None
1105 if (
1106 group_cells
1107 and group_anchor
1108 and group_destination
1109 and event.button() == Qt.MouseButton.LeftButton
1110 ):
1111 if group_destination != group_anchor:
1112 self.multiCellMoveRequested.emit(
1113 group_cells,
1114 int(group_anchor[0]),
1115 int(group_anchor[1]),
1116 int(group_destination[0]),
1117 int(group_destination[1]),
1118 )
1119 self.viewport().update()
1120 self.unsetCursor()
1121 event.accept()
1122 return
1124 resize_source = self._resize_source
1125 resize_column = self._resize_preview_column
1126 self._resize_source = None
1127 self._resize_preview_column = None
1128 self.viewport().update()
1129 if resize_source and event.button() == Qt.MouseButton.LeftButton:
1130 row = self.rowAt(int(event.position().y()))
1131 if row < 0:
1132 row = resize_source[0]
1133 if row == resize_source[0]:
1134 boundary = (
1135 self._column_from_x(event.position().x())
1136 if resize_column is None else int(resize_column)
1137 )
1138 self.exposureResizeRequested.emit(
1139 resize_source[0], resize_source[1], boundary, resize_source[2]
1140 )
1141 self.unsetCursor()
1142 event.accept()
1143 return
1145 source = self._drag_source
1146 destination = self._drag_preview_dest
1147 duplicate_drag = self._duplicate_drag
1148 self._drag_source = None
1149 self._drag_preview_dest = None
1150 self._duplicate_drag = False
1151 self.viewport().update()
1152 if source and destination and event.button() == Qt.MouseButton.LeftButton:
1153 if destination != source:
1154 signal = (
1155 self.cellCopyRequested
1156 if duplicate_drag
1157 else self.cellMoveRequested
1158 )
1159 signal.emit(
1160 source[0], source[1], destination[0], destination[1]
1161 )
1162 self.unsetCursor()
1163 event.accept()
1164 return
1165 self.unsetCursor()
1166 super().mouseReleaseEvent(event)
1169 def _tween_group_item(self, index):
1170 if not index.isValid():
1171 return None
1172 item = self.item(index.row(), index.column())
1173 if item is None or not item.data(TimelineCellDelegate.TWEEN_GROUP_ROLE):
1174 return None
1175 return item
1177 def mouseDoubleClickEvent(self, event):
1178 index = self.indexAt(event.position().toPoint())
1179 item = self._tween_group_item(index)
1180 if item is not None and event.button() == Qt.MouseButton.LeftButton:
1181 self.tweenEditRequested.emit(
1182 int(index.row()), int(item.data(Qt.ItemDataRole.UserRole))
1183 )
1184 event.accept()
1185 return
1186 super().mouseDoubleClickEvent(event)
1188 def contextMenuEvent(self, event):
1189 index = self.indexAt(event.pos())
1190 item = (
1191 self.item(index.row(), index.column())
1192 if index.isValid() else None
1193 )
1194 row = int(index.row()) if index.isValid() else -1
1195 column = int(index.column()) if index.isValid() else -1
1196 on_cell = index.isValid()
1198 menu = QMenu(self)
1199 # セル操作ひと通り(Issue #48)。ペイントマンの並びに合わせる。
1200 # ●のトラックラベル(絵を持たない印)はこのアプリでは扱わない。
1201 action_label_blank = menu.addAction(tr("中割りトラックラベル○を作成"))
1202 action_label_blank.setEnabled(on_cell)
1203 action_clear_label = menu.addAction(tr("トラックラベルを削除"))
1204 action_clear_label.setEnabled(on_cell and item is not None)
1205 menu.addSeparator()
1206 action_cell_delete = menu.addAction(tr("削除"))
1207 action_cell_cut = menu.addAction(tr("切り取り"))
1208 action_cell_copy = menu.addAction(tr("コピー"))
1209 action_cell_paste = menu.addAction(tr("貼り付け"))
1210 for action in (action_cell_delete, action_cell_cut, action_cell_copy):
1211 action.setEnabled(on_cell and item is not None)
1212 action_cell_paste.setEnabled(on_cell and self.cell_clipboard_filled)
1213 menu.addSeparator()
1214 action_new_cell = menu.addAction(tr("新規アニメーションセル"))
1215 action_new_cell.setEnabled(on_cell)
1216 menu.addSeparator()
1217 action_clip_merge = menu.addAction(tr("クリップを結合"))
1218 action_clip_split = menu.addAction(tr("クリップを分割"))
1219 action_clip_merge.setEnabled(on_cell and item is not None)
1220 action_clip_split.setEnabled(on_cell and item is not None)
1221 action_range_start = menu.addAction(tr("表示開始フレームにする"))
1222 action_range_end = menu.addAction(tr("表示終了フレームにする"))
1223 action_range_start.setEnabled(on_cell)
1224 action_range_end.setEnabled(on_cell)
1225 menu.addSeparator()
1226 action_copy_time_remap = menu.addAction(tr("タイムリマップをコピー"))
1227 menu.addSeparator()
1228 action_insert_frame = menu.addAction(tr("フレームを挿入"))
1229 action_delete_frame = menu.addAction(tr("フレームを削除"))
1230 action_insert_frame.setEnabled(on_cell)
1231 action_delete_frame.setEnabled(on_cell)
1232 menu.addSeparator()
1234 add_menu = menu.addMenu(tr("コマを増やす"))
1235 action_add_blank = add_menu.addAction(
1236 tr("空フレームを追加")
1237 )
1238 action_extend = add_menu.addAction(
1239 tr("表示コマを1コマ伸ばす")
1240 )
1241 key_column = (
1242 item.data(Qt.ItemDataRole.UserRole)
1243 if item is not None else None
1244 )
1245 exposure = (
1246 item.data(Qt.ItemDataRole.UserRole + 1)
1247 if item is not None else None
1248 )
1249 action_extend.setEnabled(
1250 index.isValid()
1251 and key_column is not None
1252 and exposure is not None
1253 )
1255 action_cancel = None
1256 action_free = None
1257 action_mesh = None
1258 action_tween_edit = None
1259 action_tween_release = None
1260 recall_actions = {}
1261 if index.isValid():
1262 numbers = self._sequence_numbers_by_row.get(int(index.row()), ())
1263 if numbers:
1264 recall_menu = menu.addMenu(tr("連番の番号を呼び出す"))
1265 for number in numbers:
1266 recall_actions[recall_menu.addAction(str(number))] = int(number)
1268 item_tween_pending = bool(
1269 item is not None
1270 and item.data(
1271 TimelineTable.TWEEN_PENDING_ROLE
1272 )
1273 )
1274 if self._tween_group_item(index) is not None:
1275 menu.addSeparator()
1276 action_tween_edit = menu.addAction(tr("トゥイーンの形を直す"))
1277 action_tween_release = menu.addAction(
1278 tr("トゥイーンを解除(中割りを通常のセルにする)")
1279 )
1280 elif (
1281 item is not None
1282 and (
1283 item.text() in ("→", "♦", "◆")
1284 or item_tween_pending
1285 )
1286 ):
1287 menu.addSeparator()
1288 if (
1289 item_tween_pending
1290 or item.text() in ("♦", "◆")
1291 ):
1292 action_cancel = menu.addAction(
1293 tr("トゥイーンをキャンセル")
1294 )
1295 else:
1296 key_col = item.data(Qt.ItemDataRole.UserRole)
1297 exposure = item.data(
1298 Qt.ItemDataRole.UserRole + 1
1299 )
1300 tween_menu = menu.addMenu(
1301 tr("トゥイーンを有効にする")
1302 )
1303 action_free = tween_menu.addAction(tr("自由変形"))
1304 action_mesh = tween_menu.addAction(tr("メッシュ変形"))
1305 tween_enabled = (
1306 key_col is not None
1307 and exposure is not None
1308 and int(exposure) >= 2
1309 )
1310 action_free.setEnabled(tween_enabled)
1311 action_mesh.setEnabled(tween_enabled)
1313 chosen = menu.exec(event.globalPos())
1314 cell_actions = {
1315 action_label_blank: self.blankFrameRequested,
1316 action_new_cell: self.newAnimationCellRequested,
1317 action_clear_label: self.clearCellRequested,
1318 action_cell_delete: self.clearCellRequested,
1319 action_cell_cut: self.cutCellRequested,
1320 action_cell_copy: self.copyCellToClipboardRequested,
1321 action_cell_paste: self.pasteCellRequested,
1322 action_clip_merge: self.mergeClipRequested,
1323 action_clip_split: self.splitClipRequested,
1324 action_insert_frame: self.insertFrameAtRequested,
1325 action_delete_frame: self.deleteFrameAtRequested,
1326 }
1327 if chosen in cell_actions and on_cell:
1328 cell_actions[chosen].emit(row, column)
1329 event.accept()
1330 return
1331 if chosen is action_copy_time_remap:
1332 self.timeRemapCopyRequested.emit()
1333 event.accept()
1334 return
1335 if chosen is action_range_start and on_cell:
1336 self.playbackRangeEdgeRequested.emit("start", column)
1337 event.accept()
1338 return
1339 if chosen is action_range_end and on_cell:
1340 # 終端は「このコマまで」。範囲は開始以上・終了未満で持つ。
1341 self.playbackRangeEdgeRequested.emit("end", column + 1)
1342 event.accept()
1343 return
1344 if chosen is action_add_blank:
1345 if index.isValid():
1346 self.blankFrameRequested.emit(
1347 int(index.row()),
1348 int(index.column()),
1349 )
1350 elif chosen is action_extend and key_column is not None and exposure is not None:
1351 self.extendExposureRequested.emit(
1352 int(index.row()),
1353 int(key_column),
1354 int(exposure),
1355 )
1356 elif action_tween_edit is not None and chosen is action_tween_edit and item is not None:
1357 self.tweenEditRequested.emit(
1358 int(index.row()), int(item.data(Qt.ItemDataRole.UserRole))
1359 )
1360 elif (
1361 action_tween_release is not None
1362 and chosen is action_tween_release
1363 and item is not None
1364 ):
1365 self.tweenReleaseRequested.emit(
1366 int(index.row()), int(item.data(Qt.ItemDataRole.UserRole))
1367 )
1368 elif action_cancel is not None and chosen is action_cancel:
1369 self.tweenCancelRequested.emit()
1370 elif action_free is not None and chosen is action_free and item is not None:
1371 self.tweenRequested.emit(
1372 int(index.row()),
1373 int(item.data(Qt.ItemDataRole.UserRole)),
1374 )
1375 elif action_mesh is not None and chosen is action_mesh and item is not None:
1376 self.tweenMeshRequested.emit(
1377 int(index.row()),
1378 int(item.data(Qt.ItemDataRole.UserRole)),
1379 )
1380 elif chosen in recall_actions:
1381 self.sequenceRecallRequested.emit(
1382 int(index.row()), int(index.column()), recall_actions[chosen]
1383 )
1384 event.accept()
1387class TimelineWidget(QWidget):
1388 frameSelected=Signal(int,int); addFrameRequested=Signal(bool); deleteFrameRequested=Signal(); playRequested=Signal(bool)
1389 onionChanged=Signal(bool); onionAllLayersChanged=Signal(bool); onionPopupToggled=Signal(bool); durationChanged=Signal(int); previousRequested=Signal(); nextRequested=Signal(); previousKeyRequested=Signal(); nextKeyRequested=Signal()
1390 layerSelected=Signal(int); layerVisibilityChanged=Signal(int,bool); layerOpacityChanged=Signal(int,float); layerNameChanged=Signal(int,str); layerMoveRequested=Signal(object,int); addLayerRequested=Signal(); deleteLayerRequested=Signal()
1391 duplicateLayersRequested=Signal(object); mergeLayersRequested=Signal(object); deleteLayersRequested=Signal(object)
1392 cellMoveRequested=Signal(int,int,int,int)
1393 cellCopyRequested=Signal(int,int,int,int)
1394 sequenceRecallRequested=Signal(int,int,int)
1395 multiCellMoveRequested=Signal(object,int,int,int,int)
1396 blankFrameRequested=Signal(int,int)
1397 exposureResizeRequested=Signal(int,int,int,str)
1398 tweenRequested=Signal(int,int)
1399 tweenMeshRequested=Signal(int,int)
1400 tweenCancelRequested=Signal()
1401 tweenEditRequested=Signal(int,int)
1402 tweenReleaseRequested=Signal(int,int)
1403 timeRemapPasteRequested=Signal()
1404 timeRemapFileDropped=Signal(str)
1405 extendExposureRequested=Signal(int,int,int)
1406 newAnimationCellRequested=Signal(int,int)
1407 clearCellRequested=Signal(int,int)
1408 cutCellRequested=Signal(int,int)
1409 copyCellToClipboardRequested=Signal(int,int)
1410 pasteCellRequested=Signal(int,int)
1411 mergeClipRequested=Signal(int,int)
1412 splitClipRequested=Signal(int,int)
1413 insertFrameAtRequested=Signal(int,int)
1414 deleteFrameAtRequested=Signal(int,int)
1415 playbackRangeEdgeRequested=Signal(str,int)
1416 timeRemapCopyRequested=Signal()
1417 timelineModeChanged=Signal(str)
1418 normalizeNumbersRequested=Signal(object)
1419 toggleDraftLayersRequested=Signal(object)
1420 def __init__(self):
1421 super().__init__()
1422 self.setAcceptDrops(True)
1423 self.sequence_archive = {}
1424 self.playback_range_start = 0
1425 self.playback_range_end: int | None = None
1426 self._timeline_zoom_scale = 1.0
1427 self._timeline_base_cell_width = 36
1428 self._timeline_base_row_height = 28
1429 # サムネイルを開いているレイヤー(レイヤー番号)。
1430 self._expanded_layers = set()
1431 v=QVBoxLayout(self)
1432 v.setContentsMargins(2,2,2,2)
1433 v.setSpacing(2)
1434 c=QHBoxLayout()
1435 c.setContentsMargins(0,0,0,0)
1436 c.setSpacing(2)
1438 self.add_blank = QPushButton()
1439 self.add_exposure = QPushButton()
1440 self.add_exposure.setToolTip(_titled_tip(
1441 tr("コマを伸ばす"),
1442 tr("現在のキーフレーム/空フレームを1コマ伸ばします。"),
1443 ))
1444 self.delete=QPushButton()
1445 self.time_remap_paste=QPushButton()
1446 self.time_remap_paste.setToolTip(_titled_tip(
1447 tr("リマップを貼り付け"),
1448 tr("AEまたはToeiDigitalTimeSheetのコピー情報を"
1449 "タイムシートへ貼り付けます。XDTSはタイムラインへ"
1450 "ドラッグ&ドロップできます。"),
1451 ))
1452 # 番号の順番とタイムラインの順番がずれたら、ずれている数をバッジで出す。
1453 self.normalize_button = QPushButton(tr("順番で正規化"))
1454 self.normalize_button.setToolTip(_titled_tip(
1455 tr("タイムラインの順番で正規化"),
1456 tr("左から最初に出てくる順に番号を振り直します。"
1457 "実行前に、振り直す番号の一覧を確認できます。"),
1458 ))
1459 self.normalize_button.setFixedHeight(24)
1460 self.normalize_button.setIconSize(QSize(15, 15))
1461 self.normalize_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
1462 self.normalize_badge = QLabel()
1463 self.normalize_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
1464 self.normalize_badge.setMinimumWidth(18)
1465 self.normalize_badge.setFixedHeight(16)
1466 self.normalize_badge.hide()
1467 self._normalize_pending = 0
1468 self.first = QPushButton()
1469 self.first.setToolTip(tr("先頭へ(Home)"))
1470 self.last = QPushButton()
1471 self.last.setToolTip(tr("末尾へ(End)"))
1472 self.prev=QPushButton()
1473 self.prev.setToolTip(tr("前のフレーム(1)"))
1474 self.next=QPushButton()
1475 self.next.setToolTip(tr("次のフレーム(2)"))
1476 self.prev_key=QPushButton()
1477 self.prev_key.setToolTip(tr("前のコマ(A)"))
1478 self.next_key=QPushButton()
1479 self.next_key.setToolTip(tr("次のコマ(S)"))
1480 self.play=QPushButton()
1481 self.play.setToolTip(tr("再生/停止"))
1482 self.play.setCheckable(True)
1484 self.onion_all_layers=QCheckBox(tr("すべてのレイヤー"))
1485 self.onion_all_layers.setChecked(True)
1486 # オニオンスキンは設定ボタンと隣り合う切り替えチップにする。
1487 self.onion=QPushButton(tr("オニオンスキン"))
1488 self.onion.setCheckable(True)
1489 self.onion.setToolTip(tr("オニオンスキン表示のON/OFF"))
1490 self.onion.setProperty("toggleChip", True)
1491 self.onion_settings=QPushButton()
1492 self.onion_settings.setCheckable(True)
1493 self.onion_settings.setCursor(
1494 Qt.CursorShape.PointingHandCursor
1495 )
1496 self.onion_settings.setToolTip(
1497 tr("クリックでオニオンスキン設定を開き、"
1498 "再クリックで閉じます。")
1499 )
1500 self.duration=QSpinBox(); self.duration.setRange(1,240); self.duration.setSuffix(tr(" コマ"))
1501 self.duration.hide()
1502 self.fps=QSpinBox(); self.fps.setRange(1,60); self.fps.setValue(24); self.fps.setSuffix(" fps")
1503 self.fps.valueChanged.connect(self._fps_changed)
1504 self.fps.setFixedSize(72,24)
1506 # 文字のボタンをアイコンにし、編集・移動・再生・オニオンの
1507 # まとまりごとに区切る。
1508 self._icon_buttons = {
1509 self.add_blank: "frame_blank",
1510 self.add_exposure: "frame_extend",
1511 self.delete: "trash",
1512 self.time_remap_paste: "remap",
1513 self.first: "first_frame",
1514 self.last: "last_frame",
1515 self.prev: "prev_frame",
1516 self.prev_key: "prev_key",
1517 self.next_key: "next_key",
1518 self.next: "next_frame",
1519 self.onion_settings: "settings",
1520 }
1521 for button in self._icon_buttons:
1522 button.setFixedSize(26, 24)
1523 button.setIconSize(QSize(16, 16))
1524 button.setProperty("iconButton", True)
1525 button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
1526 self.play.setFixedSize(30, 24)
1527 self.play.setIconSize(QSize(14, 14))
1528 self.play.setProperty("accentButton", True)
1529 self.play.setFocusPolicy(Qt.FocusPolicy.NoFocus)
1530 self.onion.setFixedHeight(24)
1531 self.onion.setFocusPolicy(Qt.FocusPolicy.NoFocus)
1532 self.onion.setIconSize(QSize(15, 15))
1534 self.mode_tabs = QTabBar()
1535 self.mode_tabs.addTab(tr("シート"))
1536 self.mode_tabs.addTab(tr("連番"))
1537 self.mode_tabs.setCurrentIndex(0)
1538 self.mode_tabs.setExpanding(False)
1539 self.mode_tabs.setDrawBase(False)
1540 self.mode_tabs.setSizePolicy(
1541 QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed
1542 )
1543 self.mode_tabs.setToolTip(
1544 tr("連番:左から順番に自動採番/"
1545 "シート:タイムシートの絵番号を保持")
1546 )
1547 self.timeline_mode = "sheet"
1548 self.mode_tabs.currentChanged.connect(
1549 self._timeline_mode_tab_changed
1550 )
1551 self._update_timeline_mode_tab_style()
1553 self.onion_all_layers.hide()
1554 self.layer_opacity_text = QLabel(tr("不透明"))
1555 self.layer_opacity_slider = QSlider(Qt.Orientation.Horizontal)
1556 self.layer_opacity_slider.setRange(0, 100)
1557 self.layer_opacity_slider.setValue(100)
1558 self.layer_opacity_slider.setFixedWidth(84)
1559 self.layer_opacity_slider.setToolTip(
1560 tr("選択レイヤーの表示不透明度です。画像の色データ自体は変更しません。")
1561 )
1562 self.layer_opacity_value = QLabel("100%")
1563 self.layer_opacity_value.setAlignment(
1564 Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
1565 )
1566 self.layer_opacity_value.setFixedWidth(34)
1567 self.layer_opacity_value.setToolTip(
1568 tr("現在選択しているレイヤーの表示不透明度")
1569 )
1571 self._toolbar_separators = []
1573 def add_group(*widgets, spacing=1):
1574 if c.count(): 1574 ↛ 1580line 1574 didn't jump to line 1580 because the condition on line 1574 was always true
1575 separator = QFrame()
1576 separator.setFrameShape(QFrame.Shape.VLine)
1577 separator.setFixedSize(9, 16)
1578 self._toolbar_separators.append(separator)
1579 c.addWidget(separator)
1580 for widget in widgets:
1581 c.addWidget(widget)
1582 if spacing > 1 and widget is not widgets[-1]:
1583 c.addSpacing(spacing)
1585 c.setSpacing(1)
1586 c.addWidget(self.mode_tabs)
1587 add_group(
1588 self.layer_opacity_text, self.layer_opacity_slider,
1589 self.layer_opacity_value, spacing=4,
1590 )
1591 add_group(
1592 self.add_blank, self.add_exposure, self.delete,
1593 self.time_remap_paste,
1594 )
1595 add_group(self.normalize_button, self.normalize_badge, spacing=3)
1597 # 再生まわりは中央にまとめ、現在のコマを数字で常に見せる。
1598 self.frame_counter = QLabel()
1599 self.frame_counter.setToolTip(tr("現在位置 / 最後のコマ"))
1600 self.frame_counter.setMinimumWidth(66)
1601 self.frame_counter.setAlignment(
1602 Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
1603 )
1604 self._frame_counter_values = (1, 1)
1605 c.addStretch()
1606 for widget in (self.first, self.prev, self.prev_key, self.play, self.next_key, self.next, self.last):
1607 c.addWidget(widget)
1608 c.addSpacing(6)
1609 c.addWidget(self.frame_counter)
1610 c.addStretch()
1611 c.addWidget(self.onion)
1612 c.addSpacing(2)
1613 c.addWidget(self.onion_settings)
1614 c.addSpacing(8)
1615 compact_hint = QLabel(tr("Shift/{ctrl}:複数選択").format(ctrl=CTRL_KEY_LABEL))
1616 self.compact_hint = compact_hint
1617 compact_hint.setToolTip(
1618 tr("ドラッグ・Shift+クリック:複数選択/"
1619 "選択範囲をそのままドラッグ:まとめて移動/"
1620 "番号・✗の箱:移動/左右端:伸縮/"
1621 "Space:ハンド/{zoom}:拡大縮小").format(zoom=HOLD_ZOOM_KEY_LABEL)
1622 )
1623 c.addWidget(compact_hint)
1624 c.addSpacing(6)
1625 c.addWidget(self.fps)
1626 c.setContentsMargins(2, 1, 2, 3)
1627 v.addLayout(c)
1628 body=QHBoxLayout(); body.setContentsMargins(0,0,0,0); body.setSpacing(0)
1629 self.setMinimumHeight(82)
1630 self.layer_list=QListWidget()
1631 self.layer_list.setFixedWidth(190)
1632 self.layer_list.setMinimumHeight(70)
1633 self.layer_list.setFrameShape(QListWidget.Shape.NoFrame)
1634 self.layer_list.setItemDelegate(LayerListDelegate(self.layer_list))
1635 self.layer_list.setContentsMargins(0,0,0,0)
1636 self.layer_list.setSpacing(0)
1637 self.layer_list.setHorizontalScrollBarPolicy(
1638 Qt.ScrollBarPolicy.ScrollBarAlwaysOff
1639 )
1640 self.layer_list.setVerticalScrollBarPolicy(
1641 Qt.ScrollBarPolicy.ScrollBarAlwaysOff
1642 )
1643 self.layer_list.setSelectionMode(
1644 QAbstractItemView.SelectionMode.ContiguousSelection
1645 )
1646 self.layer_list.setContextMenuPolicy(
1647 Qt.ContextMenuPolicy.CustomContextMenu
1648 )
1649 self.layer_list.customContextMenuRequested.connect(
1650 self._show_layer_context_menu
1651 )
1652 self.layer_list.viewport().installEventFilter(self)
1653 self._active_layer_index = 0
1655 # レイヤー名とタイムラインを同一面として見せる。
1656 # タイムライン見出しと同じ高さのヘッダー内に追加・削除ボタンを置き、
1657 # その直下からレイヤー名とタイムライン行を隙間なく隣接させる。
1658 layer_box=QVBoxLayout()
1659 layer_box.setContentsMargins(0,0,0,0)
1660 layer_box.setSpacing(0)
1661 self.layer_header_spacer = QWidget()
1662 self.layer_header_spacer.setFixedHeight(25)
1663 self.layer_header_spacer.setSizePolicy(
1664 QSizePolicy.Policy.Preferred,
1665 QSizePolicy.Policy.Fixed,
1666 )
1667 layer_header_layout = QHBoxLayout(self.layer_header_spacer)
1668 layer_header_layout.setContentsMargins(2,0,2,0)
1669 layer_header_layout.setSpacing(2)
1670 self.expand_all_button = QPushButton()
1671 self.expand_all_button.setFixedSize(18, 22)
1672 self.expand_all_button.setIconSize(QSize(12, 12))
1673 self.expand_all_button.setProperty("iconButton", True)
1674 self.expand_all_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
1675 self.expand_all_button.setToolTip(tr("すべてのレイヤーのサムネイルを開く/閉じる"))
1676 self.expand_all_button.clicked.connect(self._toggle_all_expanded)
1677 layer_header_layout.addWidget(self.expand_all_button)
1678 layer_header_layout.addWidget(QLabel(tr("レイヤー")))
1679 layer_header_layout.addStretch()
1680 self.layer_add=QPushButton()
1681 self.layer_add.setToolTip(tr("レイヤーを追加"))
1682 self.layer_del=QPushButton()
1683 self.layer_del.setToolTip(tr("レイヤーを削除"))
1684 for button in (self.layer_add, self.layer_del):
1685 button.setFixedSize(24,22)
1686 button.setIconSize(QSize(14, 14))
1687 button.setProperty("iconButton", True)
1688 button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
1689 button.setContentsMargins(0,0,0,0)
1690 layer_header_layout.addWidget(self.layer_add)
1691 layer_header_layout.addWidget(self.layer_del)
1692 layer_box.addWidget(self.layer_header_spacer)
1693 layer_box.addWidget(self.layer_list,1)
1695 layer_widget=QWidget()
1696 layer_widget.setContentsMargins(0,0,0,0)
1697 layer_widget.setLayout(layer_box)
1698 self.layer_widget = layer_widget
1699 body.addWidget(layer_widget,0)
1701 self.table=TimelineTable()
1702 self.table.setAcceptDrops(False)
1703 self.table.viewport().setAcceptDrops(False)
1704 self.table.setItemDelegate(TimelineCellDelegate(self.table))
1705 self.table.setRowCount(1)
1706 self.table.setColumnCount(1)
1707 self.table.setMinimumHeight(70)
1708 self.table.setSelectionMode(
1709 QAbstractItemView.SelectionMode.ExtendedSelection
1710 )
1711 self.table.setSelectionBehavior(
1712 QAbstractItemView.SelectionBehavior.SelectItems
1713 )
1714 self.table.setFrameShape(QTableWidget.Shape.NoFrame)
1715 self.table.setContentsMargins(0,0,0,0)
1716 self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
1717 self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
1718 self.table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
1719 self.table.horizontalHeader().setSectionsMovable(False)
1720 self.table.verticalHeader().setSectionsMovable(False)
1721 self.table.verticalHeader().setVisible(False)
1722 # 格子線はセルの描画で引く(帯の面が格子で途切れないように)。
1723 self.table.setShowGrid(False)
1725 # どちらを縦スクロールしてもレイヤー名とタイムライン行を同時に動かす。
1726 self.table.verticalScrollBar().valueChanged.connect(
1727 self.layer_list.verticalScrollBar().setValue
1728 )
1729 self.layer_list.verticalScrollBar().valueChanged.connect(
1730 self.table.verticalScrollBar().setValue
1731 )
1733 self._sequence_frame_by_cell = {}
1734 self.table.cellClicked.connect(self._select_table_cell)
1735 self._real_frame_count = 1
1736 self.table._real_frame_count = 1
1737 self.table.headerFrameRequested.connect(self._scrub_header_frame)
1738 self.table.cellMoveRequested.connect(self.cellMoveRequested)
1739 self.table.cellCopyRequested.connect(self.cellCopyRequested)
1740 self.table.sequenceRecallRequested.connect(self.sequenceRecallRequested)
1741 self.table.multiCellMoveRequested.connect(
1742 self.multiCellMoveRequested
1743 )
1744 self.table.blankFrameRequested.connect(
1745 self.blankFrameRequested
1746 )
1747 self.table.exposureResizeRequested.connect(self.exposureResizeRequested)
1748 self.table.tweenRequested.connect(self.tweenRequested)
1749 self.table.tweenMeshRequested.connect(self.tweenMeshRequested)
1750 self.table.tweenCancelRequested.connect(self.tweenCancelRequested)
1751 self.table.tweenEditRequested.connect(self.tweenEditRequested)
1752 self.table.tweenReleaseRequested.connect(self.tweenReleaseRequested)
1753 self.table.addFrameRequested.connect(
1754 self.addFrameRequested
1755 )
1756 self.table.extendExposureRequested.connect(
1757 self.extendExposureRequested
1758 )
1759 self.table.newAnimationCellRequested.connect(self.newAnimationCellRequested)
1760 self.table.clearCellRequested.connect(self.clearCellRequested)
1761 self.table.cutCellRequested.connect(self.cutCellRequested)
1762 self.table.copyCellToClipboardRequested.connect(self.copyCellToClipboardRequested)
1763 self.table.pasteCellRequested.connect(self.pasteCellRequested)
1764 self.table.mergeClipRequested.connect(self.mergeClipRequested)
1765 self.table.splitClipRequested.connect(self.splitClipRequested)
1766 self.table.insertFrameAtRequested.connect(self.insertFrameAtRequested)
1767 self.table.deleteFrameAtRequested.connect(self.deleteFrameAtRequested)
1768 self.table.playbackRangeEdgeRequested.connect(self.playbackRangeEdgeRequested)
1769 self.table.timeRemapCopyRequested.connect(self.timeRemapCopyRequested)
1770 body.addWidget(self.table,1)
1771 v.addLayout(body)
1772 self.layer_list.currentRowChanged.connect(self._layer_selected)
1773 self.layer_list.model().rowsMoved.connect(self._layer_rows_moved)
1774 self.layer_list.itemChanged.connect(self._layer_item_changed)
1775 self.layer_list.setEditTriggers(
1776 QAbstractItemView.EditTrigger.DoubleClicked
1777 )
1778 self.layer_list.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
1779 self.layer_list.setDefaultDropAction(Qt.DropAction.MoveAction)
1780 self.layer_add.clicked.connect(self.addLayerRequested); self.layer_del.clicked.connect(self.deleteLayerRequested)
1781 self.add_blank.clicked.connect(
1782 self._create_blank_at_current
1783 )
1784 self.add_exposure.clicked.connect(
1785 self._extend_current_exposure
1786 )
1787 self.delete.clicked.connect(self.deleteFrameRequested)
1788 self.normalize_button.clicked.connect(self._request_normalize)
1789 self.time_remap_paste.clicked.connect(
1790 self.timeRemapPasteRequested
1791 )
1792 self.prev.clicked.connect(self.previousRequested); self.next.clicked.connect(self.nextRequested)
1793 self.prev_key.clicked.connect(self.previousKeyRequested); self.next_key.clicked.connect(self.nextKeyRequested)
1794 self.play.toggled.connect(self._play)
1795 self.onion_all_layers.toggled.connect(self.onionAllLayersChanged)
1796 self.onion.toggled.connect(self._on_onion_toggled)
1797 self.onion_settings.toggled.connect(
1798 self.onionPopupToggled
1799 )
1800 self.duration.valueChanged.connect(self.durationChanged)
1801 self.layer_opacity_slider.valueChanged.connect(
1802 self._layer_opacity_slider_changed
1803 )
1804 self._sync_timeline_mode_controls()
1805 self.apply_theme()
1807 def _apply_icons(self):
1808 for button, name in self._icon_buttons.items():
1809 button.setIcon(icons.icon(name))
1810 c = theme.palette()
1811 play_icon = QIcon()
1812 for size in (14, 16, 28, 32):
1813 play_icon.addPixmap(icons.pixmap("play", size, c["accent_text"]))
1814 play_icon.addPixmap(
1815 icons.pixmap("pause", size, c["accent_text"]),
1816 QIcon.Mode.Normal, QIcon.State.On,
1817 )
1818 self.play.setIcon(play_icon)
1819 self.onion.setIcon(icons.icon("onion"))
1820 self.normalize_button.setIcon(
1821 icons.icon(
1822 "sort_numbers",
1823 theme.palette()["warning"] if self._normalize_pending else None,
1824 )
1825 )
1826 self.layer_add.setIcon(icons.icon("plus"))
1827 self.expand_all_button.setIcon(icons.icon(
1828 "chevron_down" if self._all_expanded() else "chevron_right"
1829 ))
1830 self.layer_del.setIcon(icons.icon("minus"))
1832 def apply_theme(self):
1833 """Palette-aware styling for the timeline surfaces.
1835 Keeps the timeline's design (compact rows, red selected-cell frame,
1836 amber/blue mode tabs) but sources neutral colours from the active theme
1837 so it reads well in both light and dark, with a touch more polish.
1838 """
1839 c = theme.palette()
1840 self._apply_icons()
1841 for separator in self._toolbar_separators:
1842 separator.setStyleSheet(
1843 "QFrame{color:%s;margin:0 4px;}" % c["border"]
1844 )
1845 panel = "border:0px;margin:0px;padding:0px;background:%s;" % c["surface_alt"]
1846 self.layer_widget.setStyleSheet(panel)
1847 self.layer_header_spacer.setStyleSheet(panel)
1848 self.layer_list.setStyleSheet(
1849 "QListWidget{border:0px;margin:0px;padding:0px;background:%s;outline:0;}"
1850 "QListWidget::item{border:0px;padding:2px 0px;background:%s;color:%s;}"
1851 "QListWidget::item:selected{background:%s;color:%s;}"
1852 % (
1853 c["surface_alt"], c["surface_alt"], c["text"],
1854 c["selection"], c["text"],
1855 )
1856 )
1857 self.table.setStyleSheet(
1858 "QTableWidget{border:0px;margin:0px;padding:0px;"
1859 "gridline-color:%s;background:%s;}"
1860 "QHeaderView{border:0px;margin:0px;padding:0px;}"
1861 "QHeaderView::section{background:%s;border:0px;"
1862 "border-bottom:1px solid %s;border-right:1px solid %s;"
1863 "padding:2px;color:%s;}"
1864 "QTableWidget::item{border:0px;color:%s;}"
1865 "QTableWidget::item:selected{background:transparent;color:%s;"
1866 "border:2px solid %s;}"
1867 % (
1868 c["border"], c["surface"],
1869 c["surface_alt"], c["border"], c["border"], c["text"],
1870 c["text"], c["text"], c["error"],
1871 )
1872 )
1873 self.compact_hint.setStyleSheet(
1874 "font-size:9px;color:%s;" % c["text_muted"]
1875 )
1876 self.layer_opacity_value.setStyleSheet(
1877 "font-size:10px;color:%s;padding:0px;" % c["text_muted"]
1878 )
1879 self.layer_opacity_text.setStyleSheet(
1880 "font-size:11px;color:%s;padding:0px;" % c["text_muted"]
1881 )
1882 self._update_timeline_mode_tab_style()
1883 self._update_normalize_badge_style()
1884 self.frame_counter.setStyleSheet(
1885 "QLabel{font-family:Menlo,Consolas,monospace;font-size:11px;}"
1886 )
1887 self._set_frame_counter()
1888 self.table.viewport().update()
1889 self.layer_list.viewport().update()
1891 def _update_normalize_badge_style(self):
1892 c = theme.palette()
1893 warning = QColor(c["warning"])
1894 self.normalize_badge.setStyleSheet(
1895 "QLabel{background:%s;color:%s;border-radius:8px;"
1896 "padding:0 5px;font-size:10px;font-weight:600;}"
1897 % (warning.name(), theme._contrast_text(warning.name()))
1898 )
1900 def _set_frame_counter(self, current=None, total=None):
1901 old_current, old_total = self._frame_counter_values
1902 current = old_current if current is None else max(1, int(current))
1903 total = old_total if total is None else max(1, int(total))
1904 self._frame_counter_values = (current, total)
1905 fps = max(1, int(self.fps.value()))
1906 current_text = time_display.frame_position(current - 1, fps)
1907 total_text = time_display.frame_position(total - 1, fps)
1908 c = theme.palette()
1909 self.frame_counter.setText(
1910 "<span style='color:%s;font-weight:600'>%s</span>"
1911 "<span style='color:%s'> / %s</span>"
1912 % (
1913 c["error"], current_text,
1914 c["text_muted"], total_text,
1915 )
1916 )
1918 def set_normalize_pending(self, count):
1919 """正規化で番号が変わる絵の数を、ボタン横のバッジに出す。"""
1920 count = max(0, int(count))
1921 if count == self._normalize_pending:
1922 return
1923 self._normalize_pending = count
1924 self.normalize_badge.setText(str(count))
1925 self.normalize_badge.setVisible(
1926 bool(count) and self.timeline_mode == "sheet"
1927 )
1928 self.normalize_badge.setToolTip(
1929 tr("タイムラインの順番とずれている番号:{count}個").format(count=count)
1930 )
1931 self._apply_icons()
1933 def _request_normalize(self):
1934 rows = tuple(self.selected_layer_rows())
1935 if not rows and self.layer_list.currentRow() >= 0:
1936 rows = (self.layer_list.currentRow(),)
1937 self.normalizeNumbersRequested.emit(rows)
1939 def _timeline_mode_tab_changed(self, index):
1940 self.timeline_mode = (
1941 "sequence" if int(index) == 1 else "sheet"
1942 )
1943 self._update_timeline_mode_tab_style()
1944 self._sync_timeline_mode_controls()
1945 self.timelineModeChanged.emit(self.timeline_mode)
1947 def _sync_timeline_mode_controls(self):
1948 sequence_mode = self.timeline_mode == "sequence"
1949 self.add_exposure.setVisible(not sequence_mode)
1950 self.normalize_button.setVisible(not sequence_mode)
1951 self.normalize_badge.setVisible(
1952 not sequence_mode and bool(self._normalize_pending)
1953 )
1954 self.add_blank.setToolTip(_titled_tip(
1955 tr("空フレームを追加"),
1956 tr("選択番号の直後へ、新しい空の番号画像を追加します。")
1957 if sequence_mode
1958 else
1959 tr("番号・✗の先頭セルでは、直後へ同じ長さの空セル(✗)を挿入。"
1960 "続きの部分では、選択位置から後ろを空セルへ分割します。"),
1961 ))
1962 self.delete.setToolTip(_titled_tip(
1963 tr("コマを削除"),
1964 tr("選択番号を削除し、シート側の対応セルを未使用にします。")
1965 if sequence_mode
1966 else tr("現在の表示コマを1コマ削除します。"),
1967 ))
1969 def _update_timeline_mode_tab_style(self):
1970 """シート/連番を角丸の切り替えボタン(セグメント)として描く。
1972 選択中の色はセルと同じ系統(シート=黄、連番=青)にして、
1973 どちらのモードかをタイムラインの色と対応させる。
1974 """
1975 c = theme.palette()
1976 if self.timeline_mode == "sheet":
1977 selected_background = c["timeline_sheet_key"]
1978 selected_foreground = c["timeline_sheet_key_text"]
1979 else:
1980 selected_background = c["timeline_key"]
1981 selected_foreground = c["timeline_key_text"]
1982 self.mode_tabs.setStyleSheet(
1983 "QTabBar{background:%(surface_alt)s;border:1px solid %(border)s;"
1984 "border-radius:7px;}"
1985 "QTabBar::tab{background:transparent;color:%(text_muted)s;"
1986 "border:none;margin:2px;padding:2px 12px;min-width:34px;"
1987 "border-radius:5px;font-size:11px;}"
1988 "QTabBar::tab:hover:!selected{background:%(hover)s;color:%(text)s;}"
1989 % c
1990 + "QTabBar::tab:selected{"
1991 f"background:{selected_background};color:{selected_foreground};"
1992 "font-weight:600;}"
1993 )
1995 def set_timeline_mode(self, mode):
1996 mode = "sheet" if str(mode) == "sheet" else "sequence"
1997 self.timeline_mode = mode
1998 target_index = 1 if mode == "sequence" else 0
1999 if self.mode_tabs.currentIndex() != target_index:
2000 self.mode_tabs.blockSignals(True)
2001 self.mode_tabs.setCurrentIndex(target_index)
2002 self.mode_tabs.blockSignals(False)
2003 self._update_timeline_mode_tab_style()
2004 self._sync_timeline_mode_controls()
2006 def expanded_row_height(self, row_height=None):
2007 """サムネイルを開いた行の高さ(上の段+サムネイルの段)。"""
2008 if row_height is None:
2009 row_height = self.timeline_cell_metrics()[1]
2010 return int(row_height) + max(34, int(round(row_height * 1.4)))
2012 def _layer_index_for_row(self, visual_row):
2013 return self.layer_list.count() - 1 - int(visual_row)
2015 def _all_expanded(self):
2016 count = self.layer_list.count()
2017 return bool(count) and all(
2018 layer_index in self._expanded_layers for layer_index in range(count)
2019 )
2021 def _apply_row_heights(self):
2022 """開閉の状態に合わせて、レイヤー名と表の行の高さをそろえる。"""
2023 row_height = self.timeline_cell_metrics()[1]
2024 expanded_height = self.expanded_row_height(row_height)
2025 expanded_rows = set()
2026 for visual_row in range(self.layer_list.count()):
2027 expanded = self._layer_index_for_row(visual_row) in self._expanded_layers
2028 height = expanded_height if expanded else row_height
2029 if expanded:
2030 expanded_rows.add(visual_row)
2031 item = self.layer_list.item(visual_row)
2032 if item is not None: 2032 ↛ 2036line 2032 didn't jump to line 2036 because the condition on line 2032 was always true
2033 item.setData(LayerListDelegate.EXPANDED_ROLE, expanded)
2034 item.setData(LayerListDelegate.BAND_HEIGHT_ROLE, row_height)
2035 item.setSizeHint(QSize(0, height))
2036 if visual_row < self.table.rowCount(): 2036 ↛ 2026line 2036 didn't jump to line 2026 because the condition on line 2036 was always true
2037 self.table.setRowHeight(visual_row, height)
2038 self.table._collapsed_row_height = row_height
2039 self.table._expanded_rows = expanded_rows
2040 self.layer_list.doItemsLayout()
2041 self.layer_list.viewport().update()
2042 self.table.viewport().update()
2043 self._apply_icons()
2045 def toggle_layer_expanded(self, visual_row):
2046 layer_index = self._layer_index_for_row(visual_row)
2047 if not (0 <= layer_index < self.layer_list.count()): 2047 ↛ 2048line 2047 didn't jump to line 2048 because the condition on line 2047 was never true
2048 return
2049 if layer_index in self._expanded_layers:
2050 self._expanded_layers.discard(layer_index)
2051 else:
2052 self._expanded_layers.add(layer_index)
2053 self._apply_row_heights()
2055 def _toggle_all_expanded(self):
2056 if self._all_expanded():
2057 self._expanded_layers.clear()
2058 else:
2059 self._expanded_layers = set(range(self.layer_list.count()))
2060 self._apply_row_heights()
2062 def timeline_cell_metrics(self):
2063 scale = max(0.5, min(3.0, float(self._timeline_zoom_scale)))
2064 return (
2065 max(18, int(round(self._timeline_base_cell_width * scale))),
2066 max(20, int(round(self._timeline_base_row_height * scale))),
2067 )
2069 def set_timeline_zoom_scale(self, scale, anchor_x=None, anchor_y=None):
2070 old_width, old_height = self.timeline_cell_metrics()
2071 new_scale = max(0.5, min(3.0, float(scale)))
2072 if abs(new_scale - self._timeline_zoom_scale) < 1e-6: 2072 ↛ 2073line 2072 didn't jump to line 2073 because the condition on line 2072 was never true
2073 return
2074 anchor_x = float(
2075 self.table.viewport().width() / 2
2076 if anchor_x is None else anchor_x
2077 )
2078 anchor_y = float(
2079 self.table.viewport().height() / 2
2080 if anchor_y is None else anchor_y
2081 )
2082 horizontal = self.table.horizontalScrollBar()
2083 vertical = self.table.verticalScrollBar()
2084 horizontal_position = (
2085 horizontal.value() + anchor_x
2086 ) / float(max(1, old_width))
2087 vertical_position = (
2088 vertical.value() + anchor_y
2089 ) / float(max(1, old_height))
2091 self._timeline_zoom_scale = new_scale
2092 new_width, new_height = self.timeline_cell_metrics()
2093 self.table.horizontalHeader().setDefaultSectionSize(new_width)
2094 self.table.verticalHeader().setDefaultSectionSize(new_height)
2095 for column in range(self.table.columnCount()):
2096 self.table.setColumnWidth(column, new_width)
2097 self._apply_row_heights()
2099 horizontal.setValue(
2100 int(round(horizontal_position * new_width - anchor_x))
2101 )
2102 vertical.setValue(
2103 int(round(vertical_position * new_height - anchor_y))
2104 )
2105 self.table.viewport().update()
2106 self.layer_list.viewport().update()
2108 def adjust_timeline_zoom(self, factor, anchor_x=None, anchor_y=None):
2109 self.set_timeline_zoom_scale(
2110 self._timeline_zoom_scale * float(factor),
2111 anchor_x,
2112 anchor_y,
2113 )
2115 def _create_blank_at_current(self):
2116 """現在のタイムライン位置へ明示的な○を作る。"""
2117 row = self.table.currentRow()
2118 column = self.table.currentColumn()
2120 if row < 0:
2121 active = int(
2122 getattr(self, "_active_layer_index", 0)
2123 )
2124 row = max(
2125 0,
2126 self.table.rowCount() - 1 - active,
2127 )
2128 if column < 0:
2129 column = 0
2131 if (
2132 row >= self.table.rowCount()
2133 or column >= self.table.columnCount()
2134 ):
2135 return
2137 self.blankFrameRequested.emit(
2138 int(row),
2139 int(column),
2140 )
2142 def _extend_current_exposure(self):
2143 if self.timeline_mode == "sequence":
2144 return
2145 row = self.table.currentRow()
2146 column = self.table.currentColumn()
2147 item = (
2148 self.table.item(row, column)
2149 if row >= 0 and column >= 0
2150 else None
2151 )
2152 if item is None:
2153 return
2154 key_column = item.data(Qt.ItemDataRole.UserRole)
2155 exposure = item.data(Qt.ItemDataRole.UserRole + 1)
2156 if key_column is None or exposure is None:
2157 return
2158 self.extendExposureRequested.emit(
2159 int(row),
2160 int(key_column),
2161 max(1, int(exposure)),
2162 )
2164 def _fps_changed(self, value):
2165 self.table._fps = max(1, int(value))
2166 self._set_frame_counter()
2167 self.table.viewport().update()
2168 self.table.horizontalHeader().viewport().update()
2170 def _on_onion_toggled(self, checked):
2171 self.onionChanged.emit(bool(checked))
2173 @staticmethod
2174 def _time_remap_drop_path(mime_data):
2175 if mime_data is None or not mime_data.hasUrls():
2176 return None
2177 for url in mime_data.urls():
2178 path = str(url.toLocalFile() or "")
2179 if Path(path).suffix.lower() in (".xdts", ".xtds"):
2180 return path
2181 return None
2183 def dragEnterEvent(self, event):
2184 if self._time_remap_drop_path(event.mimeData()):
2185 event.acceptProposedAction()
2186 return
2187 super().dragEnterEvent(event)
2189 def dragMoveEvent(self, event):
2190 if self._time_remap_drop_path(event.mimeData()):
2191 event.acceptProposedAction()
2192 return
2193 super().dragMoveEvent(event)
2195 def dropEvent(self, event):
2196 path = self._time_remap_drop_path(event.mimeData())
2197 if path:
2198 self.timeRemapFileDropped.emit(path)
2199 event.acceptProposedAction()
2200 return
2201 super().dropEvent(event)
2203 def eventFilter(self, watched, event):
2204 if (
2205 watched is self.layer_list.viewport()
2206 and event.type() in (
2207 QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonDblClick,
2208 )
2209 and event.button() == Qt.MouseButton.LeftButton
2210 ):
2211 point = event.position().toPoint()
2212 item = self.layer_list.itemAt(point)
2213 if ( 2213 ↛ 2220line 2213 didn't jump to line 2220 because the condition on line 2213 was never true
2214 item is not None
2215 and point.x() <= LayerListDelegate.CHEVRON_WIDTH
2216 and event.type() == QEvent.Type.MouseButtonDblClick
2217 ):
2218 # Keep expansion a single action, but let the eye handle a
2219 # double-click as its second press during rapid toggling.
2220 event.accept()
2221 return True
2222 if item is not None and point.x() <= LayerListDelegate.CHEVRON_WIDTH:
2223 self.toggle_layer_expanded(self.layer_list.row(item))
2224 event.accept()
2225 return True
2226 if item is not None and point.x() <= LayerListDelegate.MARKER_WIDTH:
2227 row = self.layer_list.row(item)
2228 visible = bool(
2229 item.data(Qt.ItemDataRole.UserRole + 4)
2230 )
2231 visible = not visible
2232 item.setData(Qt.ItemDataRole.UserRole + 4, visible)
2233 self.layer_list.viewport().update()
2234 self.layerVisibilityChanged.emit(row, visible)
2235 event.accept()
2236 return True
2237 return super().eventFilter(watched, event)
2239 def _layer_opacity_slider_changed(self, value):
2240 value = max(0, min(100, int(value)))
2241 self.layer_opacity_value.setText(f"{value}%")
2242 row = self.layer_list.currentRow()
2243 if row >= 0:
2244 self.layerOpacityChanged.emit(row, value / 100.0)
2246 def _sync_layer_opacity_slider(self, opacity):
2247 value = max(0, min(100, int(round(float(opacity) * 100))))
2248 self.layer_opacity_slider.blockSignals(True)
2249 self.layer_opacity_slider.setValue(value)
2250 self.layer_opacity_slider.blockSignals(False)
2251 self.layer_opacity_value.setText(f"{value}%")
2253 def _scrub_header_frame(self, column):
2254 # シートでは範囲外のコマへも移動できる(連番は番号の範囲内だけ)。
2255 limit = (
2256 self._real_frame_count
2257 if self.timeline_mode == "sequence"
2258 else self.table.columnCount()
2259 )
2260 if not (0 <= int(column) < limit): 2260 ↛ 2261line 2260 didn't jump to line 2261 because the condition on line 2260 was never true
2261 return
2262 row = self.table.currentRow()
2263 if row < 0: 2263 ↛ 2264line 2263 didn't jump to line 2264 because the condition on line 2263 was never true
2264 row = 0
2265 if self.timeline_mode != "sequence" and self.table._playhead_column == int(column):
2266 # Mouse moves inside a single ruler cell need no selection work.
2267 return
2268 self.table.setCurrentCell(row, int(column))
2269 self.frameSelected.emit(int(column), int(row))
2271 def _select_table_cell(self, row, column):
2272 limit = self._real_frame_count if self.timeline_mode == "sequence" else self.table.columnCount()
2273 if not (0 <= int(column) < limit):
2274 return
2275 # 連番の表示列から内部フレームへの変換は受け手側で一度だけ行う。
2276 self.frameSelected.emit(int(column), int(row))
2278 def selected_layer_rows(self):
2279 return sorted({
2280 index.row() for index in self.layer_list.selectedIndexes()
2281 if index.isValid()
2282 })
2284 def _rows_all_draft(self, rows):
2285 """選択した全レイヤー行が下書きレイヤーなら True。"""
2286 checked = False
2287 for row in rows:
2288 item = self.layer_list.item(int(row))
2289 if item is None:
2290 return False
2291 checked = True
2292 if not bool(item.data(Qt.ItemDataRole.UserRole + 6)):
2293 return False
2294 return checked
2296 def _show_layer_context_menu(self, position):
2297 item = self.layer_list.itemAt(position)
2298 if item is None:
2299 return
2300 if not item.isSelected():
2301 self.layer_list.clearSelection()
2302 item.setSelected(True)
2303 self.layer_list.setCurrentItem(item)
2304 rows = tuple(self.selected_layer_rows())
2305 if not rows:
2306 return
2307 menu = QMenu(self)
2308 action_duplicate = menu.addAction(tr("複製"))
2309 action_merge = menu.addAction(tr("結合"))
2310 action_merge.setEnabled(len(rows) >= 2)
2311 action_delete = menu.addAction(tr("削除"))
2312 menu.addSeparator()
2313 action_draft = menu.addAction(tr("下書きレイヤー"))
2314 action_draft.setCheckable(True)
2315 action_draft.setChecked(bool(self._rows_all_draft(rows)))
2316 action_normalize = None
2317 if self.timeline_mode == "sheet":
2318 menu.addSeparator()
2319 action_normalize = menu.addAction(tr("番号の正規化"))
2320 chosen = menu.exec(self.layer_list.viewport().mapToGlobal(position))
2321 if chosen is action_duplicate:
2322 self.duplicateLayersRequested.emit(rows)
2323 elif chosen is action_merge:
2324 self.mergeLayersRequested.emit(rows)
2325 elif chosen is action_delete:
2326 self.deleteLayersRequested.emit(rows)
2327 elif chosen is action_draft:
2328 self.toggleDraftLayersRequested.emit(rows)
2329 elif action_normalize is not None and chosen is action_normalize:
2330 self.normalizeNumbersRequested.emit(rows)
2332 def rename_selected_layer(self):
2333 row = self.layer_list.currentRow()
2334 item = self.layer_list.item(row) if row >= 0 else None
2335 if item is None:
2336 return
2337 self.layer_list.setCurrentItem(item)
2338 self.layer_list.scrollToItem(item)
2339 self.layer_list.editItem(item)
2341 def _move_timeline_visual_rows(self, start, end, final_row):
2342 """レイヤー名のドラッグ中、対応するタイムライン行も同時に移動する。"""
2343 row_count = self.table.rowCount()
2344 if not (0 <= start <= end < row_count):
2345 return
2346 block_count = end - start + 1
2347 final_row = max(0, min(int(final_row), row_count - block_count))
2348 if final_row == start:
2349 return
2351 current_column = self.table.currentColumn()
2352 moved_rows = []
2353 self.table.blockSignals(True)
2354 try:
2355 for row in range(start, end + 1):
2356 moved_rows.append([
2357 self.table.takeItem(row, column)
2358 for column in range(self.table.columnCount())
2359 ])
2361 for row in range(end, start - 1, -1):
2362 self.table.removeRow(row)
2364 for offset, items in enumerate(moved_rows):
2365 target_row = final_row + offset
2366 self.table.insertRow(target_row)
2367 for column, item in enumerate(items):
2368 if item is not None:
2369 self.table.setItem(target_row, column, item)
2371 if current_column >= 0:
2372 self.table.setCurrentCell(final_row, current_column)
2373 finally:
2374 self.table.blockSignals(False)
2375 self._apply_row_heights()
2377 def _layer_rows_moved(self, parent, start, end, destination, destination_row):
2378 block_count = end - start + 1
2379 final_row = (
2380 destination_row - block_count
2381 if destination_row > start
2382 else destination_row
2383 )
2384 final_row = max(
2385 0,
2386 min(final_row, self.layer_list.count() - block_count),
2387 )
2388 if final_row == start:
2389 return
2391 # 名前だけ先に動いてタイムラインが残る状態を作らない。
2392 self._move_timeline_visual_rows(start, end, final_row)
2393 self.layerMoveRequested.emit(
2394 tuple(range(start, end + 1)),
2395 int(final_row),
2396 )
2398 def _layer_selected(self, row):
2399 item = self.layer_list.item(row) if row >= 0 else None
2400 if item is not None: 2400 ↛ 2404line 2400 didn't jump to line 2404 because the condition on line 2400 was always true
2401 opacity = item.data(Qt.ItemDataRole.UserRole + 5)
2402 if opacity is not None: 2402 ↛ 2403line 2402 didn't jump to line 2403 because the condition on line 2402 was never true
2403 self._sync_layer_opacity_slider(float(opacity))
2404 self.layerSelected.emit(row)
2406 def _layer_item_changed(self, item):
2407 row = self.layer_list.row(item)
2408 if row < 0: 2408 ↛ 2409line 2408 didn't jump to line 2409 because the condition on line 2408 was never true
2409 return
2410 previous_name = str(
2411 item.data(Qt.ItemDataRole.UserRole + 3) or ""
2412 ).strip()
2413 entered_name = item.text()
2414 current_name = entered_name.strip() or previous_name or "Layer"
2416 # 表示用の名前と比較用の保持値を揃える。ここで発生する itemChanged は
2417 # 再帰させず、実際に名前が変わった場合だけ外側へ通知する。
2418 was_blocked = self.layer_list.blockSignals(True)
2419 try:
2420 if entered_name != current_name: 2420 ↛ 2421line 2420 didn't jump to line 2421 because the condition on line 2420 was never true
2421 item.setText(current_name)
2422 item.setData(Qt.ItemDataRole.UserRole + 3, current_name)
2423 finally:
2424 self.layer_list.blockSignals(was_blocked)
2426 if current_name != previous_name:
2427 self.layerNameChanged.emit(row, current_name)
2428 def set_cell_clipboard_filled(self, filled):
2429 """「貼り付け」をメニューに出してよいかを伝える(Issue #48)。"""
2430 self.table.cell_clipboard_filled = bool(filled)
2432 def _play(self,on): self.playRequested.emit(on)
2433 @staticmethod
2434 def timeline_span_at(frames, layer_index, column):
2435 """表示中のコマ区間を返す。
2437 content は●、blank は○、None は未使用セル。
2438 """
2439 if (
2440 column < 0
2441 or column >= len(frames)
2442 or layer_index < 0
2443 ):
2444 return None, None, 0
2446 for start in range(column, -1, -1):
2447 if layer_index >= len(frames[start].layers): 2447 ↛ 2448line 2447 didn't jump to line 2448 because the condition on line 2447 was never true
2448 continue
2449 layer = frames[start].layers[layer_index]
2450 if getattr(layer, "sequence_only", False):
2451 continue
2452 is_content = bool(layer.has_content)
2453 is_blank = bool(
2454 getattr(layer, "is_blank_key", False)
2455 )
2456 if not (is_content or is_blank):
2457 continue
2459 exposure = max(1, int(layer.exposure))
2460 if column < start + exposure:
2461 return (
2462 "content" if is_content else "blank",
2463 start,
2464 exposure,
2465 )
2466 # 直近の明示コマが届いていない場合、それ以前の露出は
2467 # この明示コマを越えて表示しない。
2468 break
2470 return None, None, 0
2472 @classmethod
2473 def timeline_gaps(cls, frames, layer_index):
2474 """セルとセルの間(と先頭)の空きコマを {コマ: (先頭, 長さ, ✗を描くか)} で返す。
2476 データ上は未使用のコマだが、書き出しでは空セルと同じ × になるので、
2477 タイムラインでも空セルとして見せる。直前が空セルなら、その続きとして
2478 ✗ は描かない。最後のセルより後ろの空きは対象にしない。
2479 """
2480 kinds = [
2481 cls.timeline_span_at(frames, layer_index, column)[0]
2482 for column in range(len(frames))
2483 ]
2484 covered = [column for column, kind in enumerate(kinds) if kind is not None]
2485 if not covered:
2486 return {}
2487 last = covered[-1]
2488 gaps = {}
2489 column = 0
2490 while column < last:
2491 if kinds[column] is not None:
2492 column += 1
2493 continue
2494 start = column
2495 while column < last and kinds[column] is None:
2496 column += 1
2497 length = column - start
2498 cross = not (start > 0 and kinds[start - 1] == "blank")
2499 for gap_column in range(start, column):
2500 gaps[gap_column] = (start, length, cross)
2501 return gaps
2503 def refresh(self, frames, current, active, tween_pending=None):
2504 if not frames: 2504 ↛ 2505line 2504 didn't jump to line 2505 because the condition on line 2504 was never true
2505 return
2507 selected_cells = {
2508 (index.row(), index.column())
2509 for index in self.table.selectedIndexes()
2510 if index.isValid()
2511 }
2512 previous_current = (
2513 self.table.currentRow(),
2514 self.table.currentColumn(),
2515 )
2517 current = max(0, min(current, len(frames) - 1))
2518 pending_tween = tween_pending if isinstance(tween_pending, dict) else {}
2519 pending_layer = int(pending_tween.get("layer_index", -1))
2520 pending_key = int(pending_tween.get("key_col", -1))
2521 pending_end = int(pending_tween.get("end_col", -1))
2522 pending_reverse = bool(
2523 pending_tween.get(
2524 "reverse_generation",
2525 False,
2526 )
2527 )
2528 rows = len(frames[current].layers)
2529 if self.timeline_mode == "sequence":
2530 real_frame_count = max(
2531 (
2532 max({
2533 int(layer.sequence_number)
2534 for frame in frames
2535 for layer in [frame.layers[layer_index]]
2536 if layer.sequence_number is not None
2537 and (layer.has_content or layer.is_blank_key)
2538 } or {1})
2539 for layer_index in range(len(frames[current].layers))
2540 ),
2541 default=1,
2542 )
2543 real_frame_count = max(
2544 real_frame_count,
2545 max(
2546 (
2547 int(number)
2548 for _layer_index, number in self.sequence_archive
2549 ),
2550 default=1,
2551 ),
2552 )
2553 real_frame_count = max(1, real_frame_count)
2554 else:
2555 real_frame_count = len(frames)
2556 self._real_frame_count = real_frame_count
2557 self.table._real_frame_count = real_frame_count
2558 self.table._sequence_mode = self.timeline_mode == "sequence"
2559 range_start = max(0, int(self.playback_range_start))
2560 range_end = (
2561 real_frame_count
2562 if self.playback_range_end is None
2563 else max(range_start + 1, int(self.playback_range_end))
2564 )
2565 self.table._range_start = range_start
2566 self.table._range_end = range_end
2567 self.table._range_preview = None
2568 self.table._fps = max(1, int(self.fps.value()))
2569 self.table._playhead_column = (
2570 None if self.timeline_mode == "sequence" else int(current)
2571 )
2572 self._set_frame_counter(int(current) + 1, len(frames))
2573 cell_width, row_height = self.timeline_cell_metrics()
2574 visible_cols = max(
2575 1, self.table.viewport().width() // max(1, cell_width) + 4
2576 )
2577 cols = max(real_frame_count + 24, range_end + 24, visible_cols)
2578 self.table.blockSignals(True)
2579 self.table._sequence_numbers_by_row = {}
2580 self._sequence_frame_by_cell = {}
2581 self.table.setRowCount(rows)
2582 self.table.setColumnCount(cols)
2583 fps = max(1, int(self.fps.value()))
2584 self.table.setHorizontalHeaderLabels([
2585 time_display.frame_position(i, fps) if i % 6 == 0 else ""
2586 for i in range(cols)
2587 ])
2588 self.table.horizontalHeader().setVisible(True)
2589 self.table.horizontalHeader().setFixedHeight(25)
2590 self.layer_header_spacer.setFixedHeight(25)
2591 self.table.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter)
2592 self.table.setVerticalHeaderLabels(["" for _ in range(rows)])
2593 self.table.verticalHeader().setDefaultSectionSize(row_height)
2594 self.layer_list.blockSignals(True)
2595 self.layer_list.clear()
2596 self._expanded_layers = {
2597 layer_index for layer_index in self._expanded_layers
2598 if 0 <= layer_index < rows
2599 }
2600 self.table._thumb_sources = {}
2601 self._active_layer_index = int(active)
2602 # 下書きレイヤーの行は、タイムラインのセルにも斜線を重ねて示す。
2603 layers_top_first = list(reversed(frames[current].layers))
2604 self.table._draft_rows = {
2605 visual_row for visual_row, layer in enumerate(layers_top_first)
2606 if bool(getattr(layer, "is_draft", False))
2607 }
2608 for layer in reversed(frames[current].layers):
2609 is_draft = bool(getattr(layer, "is_draft", False))
2610 item = QListWidgetItem(layer.name)
2611 item.setFlags(
2612 (
2613 item.flags()
2614 | Qt.ItemFlag.ItemIsSelectable
2615 | Qt.ItemFlag.ItemIsDragEnabled
2616 | Qt.ItemFlag.ItemIsEditable
2617 )
2618 & ~Qt.ItemFlag.ItemIsUserCheckable
2619 )
2620 item.setData(Qt.ItemDataRole.UserRole + 2, False)
2621 item.setData(Qt.ItemDataRole.UserRole + 3, layer.name)
2622 item.setData(Qt.ItemDataRole.UserRole + 4, bool(layer.visible))
2623 item.setData(Qt.ItemDataRole.UserRole + 5, float(layer.opacity))
2624 item.setData(Qt.ItemDataRole.UserRole + 6, is_draft)
2625 if is_draft:
2626 font = item.font()
2627 font.setItalic(True)
2628 item.setFont(font)
2629 item.setToolTip(
2630 (tr("下書きレイヤー(色数削減の対象外)。\n") if is_draft else "")
2631 + tr("左端の>でサムネイルを開閉、目のアイコンで表示/非表示を切替、"
2632 "ダブルクリックでレイヤー名を変更。")
2633 )
2634 item.setSizeHint(QSize(0, row_height))
2635 self.layer_list.addItem(item)
2636 self.layer_list.setCurrentRow(rows - 1 - active)
2637 if 0 <= active < len(frames[current].layers): 2637 ↛ 2641line 2637 didn't jump to line 2641 because the condition on line 2637 was always true
2638 self._sync_layer_opacity_slider(
2639 frames[current].layers[active].opacity
2640 )
2641 self.layer_list.blockSignals(False)
2642 for col in range(cols):
2643 self.table.setColumnWidth(col, cell_width)
2644 for visual_row in range(rows):
2645 self.table.setRowHeight(visual_row, row_height)
2646 layer_index = rows - 1 - visual_row
2647 sequence_columns = [
2648 column
2649 for _number, column in sorted(
2650 {
2651 int(frame.layers[layer_index].sequence_number): column
2652 for column, frame in enumerate(frames)
2653 if (
2654 frame.layers[layer_index].sequence_number is not None
2655 and (frame.layers[layer_index].has_content or frame.layers[layer_index].is_blank_key)
2656 )
2657 }.items()
2658 )
2659 ]
2660 sequence_by_number = {
2661 int(frames[column].layers[layer_index].sequence_number): column
2662 for column in sequence_columns
2663 }
2664 archive_by_number = {
2665 int(number): archived
2666 for (archived_layer, number), archived
2667 in self.sequence_archive.items()
2668 if int(archived_layer) == layer_index
2669 }
2670 for number, frame_column in sequence_by_number.items():
2671 self._sequence_frame_by_cell[(visual_row, number - 1)] = frame_column
2672 self.table._sequence_numbers_by_row[visual_row] = tuple(
2673 sorted(
2674 set(sequence_by_number)
2675 | {
2676 int(number)
2677 for archived_layer, number in self.sequence_archive
2678 if int(archived_layer) == layer_index
2679 }
2680 )
2681 )
2682 content_columns = [
2683 column
2684 for column in range(len(frames))
2685 if frames[column].layers[layer_index].has_content
2686 ]
2687 if self.timeline_mode == "sequence":
2688 content_key_numbers = {
2689 number - 1: number
2690 for number in set(sequence_by_number) | set(archive_by_number)
2691 }
2692 elif self.timeline_mode == "sheet": 2692 ↛ 2712line 2692 didn't jump to line 2712 because the condition on line 2692 was always true
2693 used_numbers = [
2694 int(frames[column].layers[layer_index].sequence_number)
2695 for column in content_columns
2696 if frames[column].layers[layer_index].sequence_number
2697 is not None
2698 ]
2699 next_number = max(used_numbers, default=0) + 1
2700 for column in content_columns:
2701 layer = frames[column].layers[layer_index]
2702 if layer.sequence_number is None:
2703 layer.sequence_number = next_number
2704 next_number += 1
2705 content_key_numbers = {
2706 column: int(
2707 frames[column].layers[layer_index].sequence_number
2708 )
2709 for column in content_columns
2710 }
2711 else:
2712 content_key_numbers = {
2713 column: number
2714 for number, column in enumerate(
2715 content_columns,
2716 start=1,
2717 )
2718 }
2719 gap_cells = (
2720 {} if self.timeline_mode == "sequence"
2721 else self.timeline_gaps(frames, layer_index)
2722 )
2723 group_cells = {}
2724 if self.timeline_mode != "sequence":
2725 for group_start, (group_length, group_reverse) in tween_groups(
2726 frames, layer_index
2727 ).items():
2728 for group_column in range(group_start, group_start + group_length):
2729 group_cells[group_column] = (
2730 group_start, group_length, group_reverse,
2731 )
2732 for col in range(cols):
2733 text = ""
2734 label = ""
2735 sequence_source = None
2736 if self.timeline_mode == "sequence":
2737 number = col + 1
2738 if number in sequence_by_number:
2739 sequence_source = frames[
2740 sequence_by_number[number]
2741 ].layers[layer_index]
2742 elif number in archive_by_number:
2743 sequence_source = archive_by_number[number]
2744 if sequence_source is not None:
2745 span_kind = (
2746 "content"
2747 if sequence_source.has_content
2748 else "sequence_blank"
2749 )
2750 key_col, exposure = col, 1
2751 else:
2752 span_kind, key_col, exposure = None, None, 0
2753 else:
2754 span_kind, key_col, exposure = self.timeline_span_at(
2755 frames, layer_index, col
2756 )
2757 if col in group_cells:
2758 # 確定済みのトゥイーンは、区間全体を 1 つのセルとして見せる。
2759 span_kind = "content"
2760 key_col, exposure = group_cells[col][0], group_cells[col][1]
2761 is_handle = False
2762 is_start_handle = False
2763 is_pending_tween = False
2764 if key_col is not None:
2765 end_col = key_col + exposure - 1
2766 is_pending_tween = (
2767 layer_index == pending_layer
2768 and int(key_col) == pending_key
2769 and int(end_col) == pending_end
2770 )
2771 if (
2772 span_kind == "content"
2773 and col == key_col
2774 ):
2775 source_key_layer = (
2776 sequence_source
2777 if self.timeline_mode == "sequence"
2778 else frames[key_col].layers[layer_index]
2779 )
2780 source_cell_name = getattr(
2781 source_key_layer, "cell_name", None
2782 )
2783 label = str(
2784 source_cell_name
2785 or content_key_numbers.get(key_col, "")
2786 )
2787 # 開閉のたびに集め直さずに済むよう、全行の絵を覚えておく。
2788 if source_key_layer is not None: 2788 ↛ 2792line 2788 didn't jump to line 2792 because the condition on line 2788 was always true
2789 self.table._thumb_sources[(visual_row, col)] = (
2790 source_key_layer.image, int(exposure)
2791 )
2792 text = (
2793 "◆"
2794 if (
2795 is_pending_tween
2796 and pending_reverse
2797 )
2798 else str(
2799 source_cell_name
2800 or content_key_numbers.get(key_col, "")
2801 )
2802 )
2803 elif (
2804 span_kind == "blank"
2805 and col == key_col
2806 ):
2807 text = "○"
2808 elif col == end_col:
2809 text = (
2810 "♦"
2811 if (
2812 is_pending_tween
2813 and not pending_reverse
2814 )
2815 else "→"
2816 )
2817 elif col > key_col: 2817 ↛ 2819line 2817 didn't jump to line 2819 because the condition on line 2817 was always true
2818 text = "ー"
2819 is_handle = col == end_col
2820 is_start_handle = col == key_col
2821 if span_kind == "sequence_blank": 2821 ↛ 2822line 2821 didn't jump to line 2822 because the condition on line 2821 was never true
2822 text = str(col + 1)
2823 label = text
2824 item = QTableWidgetItem(text)
2825 item.setData(TimelineCellDelegate.LABEL_ROLE, label)
2826 if span_kind is None and col in gap_cells:
2827 item.setData(TimelineCellDelegate.GAP_ROLE, gap_cells[col])
2828 if is_pending_tween:
2829 item.setData(
2830 TimelineCellDelegate.TWEEN_SPAN_ROLE,
2831 "reverse" if pending_reverse else "forward",
2832 )
2833 elif col in group_cells:
2834 item.setData(
2835 TimelineCellDelegate.TWEEN_SPAN_ROLE,
2836 "reverse" if group_cells[col][2] else "forward",
2837 )
2838 item.setData(TimelineCellDelegate.TWEEN_GROUP_ROLE, True)
2839 if span_kind == "content":
2840 state_name = (
2841 "sheet_key"
2842 if self.timeline_mode == "sheet" and col == key_col
2843 else "sheet_hold"
2844 if self.timeline_mode == "sheet"
2845 else "key"
2846 if col == key_col
2847 else "hold"
2848 )
2849 else:
2850 state_name = (
2851 "blank" if span_kind in ("blank", "sequence_blank") else "uncreated"
2852 )
2853 item.setData(TimelineCellDelegate.STATE_ROLE, state_name)
2854 item.setFlags((item.flags() | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled) & ~Qt.ItemFlag.ItemIsEditable)
2855 item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
2856 # 参考UIに合わせたタイムライン専用配色。
2857 # キーフレームと保持区間は薄い水色、空フレームはさらに薄く、
2858 # 未作成フレームはテーブル既定色のままにする。
2859 cell_colors = theme.palette()
2860 if span_kind == "content" and col == key_col:
2861 cell_key = "sheet_key" if self.timeline_mode == "sheet" else "key"
2862 item.setBackground(QColor(cell_colors[f"timeline_{cell_key}"]))
2863 item.setForeground(QColor(cell_colors[f"timeline_{cell_key}_text"]))
2864 key_font = item.font()
2865 key_font.setBold(True)
2866 key_font.setPointSize(max(10, key_font.pointSize()))
2867 item.setFont(key_font)
2868 elif span_kind == "content":
2869 cell_key = "sheet_hold" if self.timeline_mode == "sheet" else "hold"
2870 item.setBackground(QColor(cell_colors[f"timeline_{cell_key}"]))
2871 item.setForeground(QColor(cell_colors[f"timeline_{cell_key}_text"]))
2872 elif span_kind == "blank":
2873 item.setBackground(QColor(cell_colors["timeline_blank"]))
2874 item.setForeground(QColor(cell_colors["timeline_blank_text"]))
2875 if text == "○":
2876 blank_font = item.font()
2877 blank_font.setBold(True)
2878 blank_font.setPointSize(
2879 max(10, blank_font.pointSize())
2880 )
2881 item.setFont(blank_font)
2882 else:
2883 item.setForeground(QColor(cell_colors["timeline_uncreated_text"]))
2884 if text in ("→", "♦", "◆"):
2885 item.setForeground(
2886 QColor("#A43A9E")
2887 if text in ("♦", "◆")
2888 else QColor("#145F80")
2889 )
2890 marker_font = item.font()
2891 marker_font.setBold(True)
2892 marker_font.setPointSize(
2893 max(10, marker_font.pointSize())
2894 )
2895 item.setFont(marker_font)
2896 if key_col is not None:
2897 item.setData(Qt.ItemDataRole.UserRole, int(key_col))
2898 item.setData(Qt.ItemDataRole.UserRole + 1, int(exposure))
2899 item.setData(TimelineTable.END_HANDLE_ROLE, bool(is_handle))
2900 item.setData(TimelineTable.START_HANDLE_ROLE, bool(is_start_handle))
2901 item.setData(
2902 TimelineTable.TWEEN_PENDING_ROLE,
2903 bool(
2904 is_pending_tween
2905 and col in (key_col, end_col)
2906 ),
2907 )
2908 if is_handle:
2909 if is_pending_tween:
2910 item.setToolTip(
2911 (
2912 tr("逆生成トゥイーン中。"
2913 "この右端は元の初期形状です。"
2914 "右クリックでキャンセルできます。")
2915 )
2916 if pending_reverse
2917 else
2918 tr("トゥイーン変形中。"
2919 "右クリックでキャンセルできます。")
2920 )
2921 else:
2922 item.setToolTip(
2923 (
2924 tr("右端をドラッグして後方向の表示コマ数を変更/"
2925 "右クリックでトゥイーンを有効化")
2926 )
2927 if span_kind == "content"
2928 else tr("空フレームの右端をドラッグして表示コマ数を変更")
2929 )
2930 elif text == "◆":
2931 item.setToolTip(
2932 tr("逆生成トゥイーン中。"
2933 "キーフレーム側が操作中の変形形状、"
2934 "右端側が元の初期形状になります。"
2935 "右クリックでキャンセルできます。")
2936 )
2937 elif (
2938 span_kind == "content"
2939 and col == key_col
2940 ):
2941 source_cell_name = getattr(
2942 (
2943 sequence_source
2944 if self.timeline_mode == "sequence"
2945 else frames[key_col].layers[layer_index]
2946 ),
2947 "cell_name",
2948 None,
2949 )
2950 item.setToolTip(
2951 (
2952 tr("CLIP STUDIOセル名:{name}\n").format(name=source_cell_name)
2953 if source_cell_name
2954 else ""
2955 )
2956 + tr("中央をドラッグして移動/左端をドラッグして前方向へ伸縮\nレイヤーセル {value} / {exposure}コマ").format(value=col + 1, exposure=exposure)
2957 )
2958 elif text == "○":
2959 item.setToolTip(
2960 tr("空フレームの先頭です。"
2961 "描画すると自動的にキーフレーム化します。")
2962 )
2963 if (
2964 (self.timeline_mode != "sequence" and col == current)
2965 or (
2966 self.timeline_mode == "sequence"
2967 and self._sequence_frame_by_cell.get(
2968 (visual_row, col)
2969 ) == current
2970 )
2971 ):
2972 item.setForeground(QColor(190, 40, 40))
2973 if col in group_cells:
2974 item.setToolTip(
2975 tr("確定したトゥイーン({exposure}コマ)。"
2976 "ダブルクリックで形を直す/右クリックで解除").format(
2977 exposure=group_cells[col][1]
2978 )
2979 )
2980 self.table.setItem(visual_row, col, item)
2981 self._apply_row_heights()
2982 selected_layer = frames[current].layers[active]
2983 self.duration.blockSignals(True)
2984 self.duration.setValue(max(1, selected_layer.exposure))
2985 self.duration.blockSignals(False)
2987 valid_selected = [
2988 (row, column)
2989 for row, column in selected_cells
2990 if (
2991 0 <= row < self.table.rowCount()
2992 and 0 <= column < self._real_frame_count
2993 )
2994 ]
2995 if valid_selected:
2996 self.table.clearSelection()
2997 for row, column in valid_selected:
2998 item = self.table.item(row, column)
2999 if item is not None: 2999 ↛ 2997line 2999 didn't jump to line 2997 because the condition on line 2999 was always true
3000 item.setSelected(True)
3002 current_row, current_column = previous_current
3003 if not ( 3003 ↛ 3007line 3003 didn't jump to line 3007 because the condition on line 3003 was never true
3004 0 <= current_row < self.table.rowCount()
3005 and 0 <= current_column < self._real_frame_count
3006 ):
3007 current_row, current_column = valid_selected[-1]
3008 current_index = self.table.model().index(
3009 current_row, current_column
3010 )
3011 self.table.selectionModel().setCurrentIndex(
3012 current_index,
3013 QItemSelectionModel.SelectionFlag.NoUpdate,
3014 )
3015 else:
3016 self.select_current(current, active)
3017 self.table.blockSignals(False)
3018 self.set_normalize_pending(
3019 sum(
3020 changed_count(
3021 normalization_mapping(frames, self.sequence_archive, layer_index)
3022 )
3023 for layer_index in range(rows)
3024 )
3025 if self.timeline_mode == "sheet"
3026 else 0
3027 )
3029 @staticmethod
3030 def key_at_or_before(frames, layer_index, column):
3031 for key_col in range(column, -1, -1):
3032 if layer_index >= len(frames[key_col].layers):
3033 continue
3034 layer = frames[key_col].layers[layer_index]
3035 if layer.has_content:
3036 if column < key_col + max(1, layer.exposure):
3037 return key_col
3038 return None
3039 return None
3041 def select_current(self, column, active):
3042 row = max(0, self.table.rowCount() - 1 - active)
3043 playhead = None if self.timeline_mode == "sequence" else int(column)
3044 if self.timeline_mode != "sequence":
3045 self._set_frame_counter(int(column) + 1)
3046 if playhead != self.table._playhead_column:
3047 previous_playhead = self.table._playhead_column
3048 self.table._playhead_column = playhead
3049 header = self.table.horizontalHeader()
3050 for changed_column in (previous_playhead, playhead):
3051 if changed_column is None or not 0 <= changed_column < self.table.columnCount(): 3051 ↛ 3052line 3051 didn't jump to line 3052 because the condition on line 3051 was never true
3052 continue
3053 x = self.table.columnViewportPosition(changed_column)
3054 width = self.table.columnWidth(changed_column)
3055 self.table.viewport().update(x - 1, 0, width + 2, self.table.viewport().height())
3056 header.viewport().update(x - 1, 0, width + 2, header.viewport().height())
3057 if self.timeline_mode == "sequence":
3058 column = next(
3059 (
3060 visual_column
3061 for (visual_row, visual_column), frame_column
3062 in self._sequence_frame_by_cell.items()
3063 if visual_row == row and frame_column == int(column)
3064 ),
3065 0,
3066 )
3067 column = max(
3068 0,
3069 min(column, max(0, self._real_frame_count - 1)),
3070 )
3072 # ドラッグ/Shift選択中のセルを消さず、
3073 # 再生ヘッドに相当する現在セルだけを更新する。
3074 selected_count = len(self.table.selectedIndexes())
3075 modifiers = QApplication.keyboardModifiers()
3076 preserve_selection = (
3077 selected_count > 1
3078 or bool(
3079 modifiers
3080 & (
3081 Qt.KeyboardModifier.ShiftModifier
3082 | Qt.KeyboardModifier.ControlModifier
3083 )
3084 )
3085 )
3086 if preserve_selection: 3086 ↛ 3087line 3086 didn't jump to line 3087 because the condition on line 3086 was never true
3087 index = self.table.model().index(row, column)
3088 self.table.selectionModel().setCurrentIndex(
3089 index,
3090 QItemSelectionModel.SelectionFlag.NoUpdate,
3091 )
3092 else:
3093 self.table.setCurrentCell(row, column)
3095 def update_cell(self, frames, frame_index, layer_index):
3096 if not frames or frame_index < 0 or frame_index >= len(frames): 3096 ↛ 3097line 3096 didn't jump to line 3097 because the condition on line 3096 was never true
3097 return
3098 if layer_index < 0 or layer_index >= len(frames[frame_index].layers): 3098 ↛ 3099line 3098 didn't jump to line 3099 because the condition on line 3098 was never true
3099 return
3100 self.refresh(frames, frame_index, layer_index)