Coverage for paintmaskanimator/main_window_timeline_ops.py: 49%
603 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
1"""Timeline cell/frame/key operations; owned as ``window.timeline_ops``.
3Drives timeline navigation, exposure and drawing-number editing, blank-key
4creation, frame deletion, and cell move/copy against the timeline widget.
6A collaborator rather than a mixin -- see ``main_window_export.py`` for why.
7"""
8from typing import TYPE_CHECKING
10from PySide6.QtCore import QItemSelectionModel, QTimer
11from .i18n import tr
12from .utils import blank_image
13from .undo_entries import ExposureUndo
14from .normalize_dialog import NormalizeNumbersDialog
15from .timeline import TimelineWidget
16from .logging_setup import get_logger
18if TYPE_CHECKING:
19 from .main_window import MainWindow
21log = get_logger(__name__)
24class TimelineOpsController:
25 """Owned by ``MainWindow`` as ``window.timeline_ops``.
27 A collaborator rather than a mixin -- see main_window_export.py for why.
28 """
30 def __init__(self, window: "MainWindow"):
31 self.window = window
33 def set_mode(self, mode):
34 mode = "sheet" if str(mode) == "sheet" else "sequence"
35 layer_index = int(self.window.canvas.active_layer_index)
36 selected_number = None
37 if self.window.canvas.frames and 0 <= layer_index < len(self.window.canvas.layers): 37 ↛ 41line 37 didn't jump to line 41 because the condition on line 37 was always true
38 selected_number = self.window.canvas.frames[
39 self.window.canvas.current_frame
40 ].layers[layer_index].sequence_number
41 if self.window.canvas.timeline_mode == "sequence" and mode == "sheet": 41 ↛ 44line 41 didn't jump to line 44 because the condition on line 41 was never true
42 # 連番専用セルのシート配置ではフレーム数・位置が変わるため、
43 # 移動前の状態を文書単位で保存してUndo参照切れを防ぐ。
44 if any(
45 layer.sequence_only
46 for frame in self.window.canvas.frames
47 for layer in frame.layers
48 ):
49 self.window.canvas.push_doc_undo()
50 self.window.canvas.apply_sequence_only_entries()
51 if selected_number is not None:
52 matching = [
53 column
54 for column, frame in enumerate(self.window.canvas.frames)
55 if (
56 frame.layers[layer_index].sequence_number == selected_number
57 and (frame.layers[layer_index].has_content or frame.layers[layer_index].is_blank_key)
58 and (mode == "sequence" or not frame.layers[layer_index].sequence_only)
59 )
60 ]
61 if matching: 61 ↛ 63line 61 didn't jump to line 63 because the condition on line 61 was always true
62 self.window.canvas.current_frame = matching[0]
63 self.window.canvas.timeline_mode = mode
64 self.window.timeline.set_timeline_mode(mode)
65 self.window.timeline.sequence_archive = self.window.canvas._sequence_archive
66 self.window.timeline.add_exposure.setVisible(mode == "sheet")
67 # 切り替え前の選択セルを保持すると、キャンバスだけ先に切り替わり
68 # 赤枠が旧タブの列へ残る。再構築前に選択を明示的に解除する。
69 self.window.timeline.table.clearSelection()
70 self.window.timeline.refresh(
71 self.window.canvas.frames,
72 self.window.canvas.current_frame,
73 self.window.canvas.active_layer_index,
74 getattr(self.window.canvas, "tween_pending", None),
75 )
76 self.window.timeline.select_current(
77 self.window.canvas.current_frame,
78 self.window.canvas.active_layer_index,
79 )
80 self.window.timeline.table.viewport().update()
82 def _navigate_sequence_number(self, step, wrap=False):
83 """シート配置ではなく絵番号順に連番セルを移動する。"""
84 layer_index = int(self.window.canvas.active_layer_index)
85 columns = self.window.canvas.sequence_entry_columns(layer_index)
86 entries = sorted(
87 (
88 int(self.window.canvas.frames[column].layers[layer_index].sequence_number),
89 int(column),
90 )
91 for column in columns
92 )
93 if not entries:
94 return
95 current_layer = self.window.canvas.frames[
96 self.window.canvas.current_frame
97 ].layers[layer_index]
98 current_number = current_layer.sequence_number
99 current_index = next(
100 (
101 index for index, (number, _column) in enumerate(entries)
102 if number == current_number
103 ),
104 -1 if int(step) > 0 else len(entries),
105 )
106 target_index = current_index + (1 if int(step) > 0 else -1)
107 if wrap:
108 target_index %= len(entries)
109 else:
110 target_index = max(0, min(len(entries) - 1, target_index))
111 self.window.canvas.current_frame = entries[target_index][1]
112 self.window.canvas.selectionChanged.emit()
113 self.window.canvas.update()
115 def jump_boundary(self, end=False):
116 canvas = self.window.canvas
117 if canvas.timeline_mode == "sequence": 117 ↛ 118line 117 didn't jump to line 118 because the condition on line 117 was never true
118 self._navigate_sequence_number(1000000 if end else -1000000, wrap=False)
119 else:
120 canvas.current_frame = len(canvas.frames) - 1 if end else 0
121 canvas.selectionChanged.emit()
122 canvas.update()
124 def resize_range(self, edge, column):
125 canvas = self.window.canvas
126 if canvas.timeline_mode != "sheet": 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true
127 return
128 start = max(0, int(canvas.playback_range_start))
129 end = (
130 len(canvas.frames)
131 if canvas.playback_range_end is None
132 else max(start + 1, int(canvas.playback_range_end))
133 )
134 if edge == "end":
135 target = max(start + 1, int(column))
136 if target == end: 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true
137 return
138 canvas.playback_range_end = target
139 else:
140 target = max(0, min(int(column), end - 1))
141 if target == start: 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true
142 return
143 canvas.playback_range_start = target
144 canvas.playback_range_end = end
145 canvas.changed.emit()
146 self.window.refresh_ui()
148 def previous_frame(self):
149 if self.window.canvas.timeline_mode == "sequence":
150 self._navigate_sequence_number(-1, wrap=False)
151 else:
152 self.window.canvas.previous_frame()
154 def next_frame(self):
155 if self.window.canvas.timeline_mode == "sequence":
156 self._navigate_sequence_number(1, wrap=False)
157 else:
158 self.window.canvas.next_frame()
160 def previous_key(self):
161 if self.window.canvas.timeline_mode == "sequence":
162 self._navigate_sequence_number(-1, wrap=True)
163 else:
164 self.window.canvas.previous_key_frame()
166 def next_key(self):
167 if self.window.canvas.timeline_mode == "sequence":
168 self._navigate_sequence_number(1, wrap=True)
169 else:
170 self.window.canvas.next_key_frame()
172 def normalize_numbers(self, visual_rows):
173 """確認画面で振り直す番号を見せてから、絵番号をシート順へ振り直す。"""
174 if self.window.canvas.timeline_mode != "sheet": 174 ↛ 175line 174 didn't jump to line 175 because the condition on line 174 was never true
175 return
176 canvas = self.window.canvas
177 layer_count = len(canvas.layers)
178 selected = {
179 layer_count - 1 - int(row)
180 for row in visual_rows
181 if 0 <= layer_count - 1 - int(row) < layer_count
182 }
183 if not selected: 183 ↛ 184line 183 didn't jump to line 184 because the condition on line 183 was never true
184 selected = {int(canvas.active_layer_index)}
185 layers = [
186 (
187 layer_index,
188 canvas.layers[layer_index].name,
189 canvas.sequence_number_normalization(layer_index),
190 )
191 for layer_index in reversed(range(layer_count))
192 ]
193 if not any( 193 ↛ 198line 193 didn't jump to line 198 because the condition on line 193 was never true
194 old != new
195 for _index, _name, mapping in layers
196 for old, new in mapping.items()
197 ):
198 self.window.statusBar().showMessage(
199 tr("番号はすでにタイムラインの順番どおりです。"), 2500
200 )
201 return
202 dialog = NormalizeNumbersDialog(layers, selected, self.window)
203 if dialog.exec() != NormalizeNumbersDialog.DialogCode.Accepted:
204 return
205 self.apply_normalize_numbers(dialog.target_layer_indices())
207 def apply_normalize_numbers(self, layer_indices):
208 """指定レイヤーの絵番号をシート順へ振り直す(確認なし)。"""
209 layer_indices = sorted({int(index) for index in layer_indices})
210 if not layer_indices: 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true
211 return
212 self.window.canvas.push_doc_undo()
213 for layer_index in layer_indices:
214 self.window.canvas.normalize_sequence_numbers(layer_index)
215 self.window.canvas._cell_structure_dirty = True
216 self.window.canvas.changed.emit()
217 self.window.canvas.selectionChanged.emit()
218 self.window.canvas.update()
219 self.window.statusBar().showMessage(
220 tr("番号をタイムラインの順番に正規化しました。"), 2500
221 )
223 def create_blank_key(
224 self,
225 visual_row,
226 column,
227 ):
228 layer_count = len(self.window.canvas.layers)
229 layer_index = layer_count - 1 - int(visual_row)
230 if not (0 <= layer_index < layer_count): 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true
231 return
232 if self.window.canvas.timeline_mode == "sequence": 232 ↛ 233line 232 didn't jump to line 233 because the condition on line 232 was never true
233 self.window.canvas.insert_sequence_blank(
234 layer_index,
235 int(column) + 1,
236 )
237 return
238 self.window.canvas.create_blank_key(
239 int(column),
240 layer_index,
241 )
243 def delete_frame(self):
244 if self.window.canvas.timeline_mode == "sequence":
245 self.window.canvas.delete_sequence_entry(
246 self.window.canvas.active_layer_index,
247 self.window.timeline.table.currentColumn() + 1,
248 )
249 return
250 layer_index = int(self.window.canvas.active_layer_index)
251 block = self.window.canvas.timeline_block_at(
252 self.window.canvas.current_frame, layer_index
253 )
254 if block is None:
255 return
256 _kind, start, _exposure = block
257 layer = self.window.canvas.frames[int(start)].layers[layer_index]
258 self.window.canvas.push_doc_undo()
259 if layer.has_content and layer.sequence_number is not None:
260 self.window.canvas._sequence_archive[
261 (layer_index, int(layer.sequence_number))
262 ] = layer.clone()
263 self.window.canvas._clear_timeline_layer_cell(layer)
264 self.window.canvas._cell_structure_dirty = True
265 self.window.canvas.changed.emit()
266 self.window.canvas.selectionChanged.emit()
267 self.window.canvas.update()
269 def _restore_selection(self, cells):
270 table = self.window.timeline.table
271 table.clearSelection()
272 valid = []
273 for row, column in cells:
274 item = table.item(int(row), int(column))
275 if item is not None:
276 item.setSelected(True)
277 valid.append((int(row), int(column)))
278 if valid:
279 table.setCurrentCell(
280 valid[0][0],
281 valid[0][1],
282 QItemSelectionModel.SelectionFlag.NoUpdate,
283 )
285 def move_selection(
286 self,
287 cells,
288 anchor_row,
289 anchor_column,
290 destination_row,
291 destination_column,
292 ):
293 """複数選択に含まれるコマ塊を相対配置のまま移動する。"""
294 if self.window.canvas.timeline_mode == "sequence":
295 if int(anchor_row) != int(destination_row):
296 self.window.statusBar().showMessage(
297 tr("連番画像は同じレイヤー内で入れ替えてください。"), 2500
298 )
299 return
300 layer_count = len(self.window.canvas.layers)
301 layer_index = layer_count - 1 - int(anchor_row)
302 self.window.canvas.move_sequence_image(
303 layer_index,
304 int(anchor_column) + 1,
305 int(destination_column) + 1,
306 )
307 return
308 try:
309 selected_cells = {
310 (int(row), int(column))
311 for row, column in cells
312 }
313 except (TypeError, ValueError) as exc:
314 log.debug("could not normalize selected cells: %s", exc)
315 return
316 if not selected_cells:
317 return
319 row_delta = int(destination_row) - int(anchor_row)
320 column_delta = (
321 int(destination_column) - int(anchor_column)
322 )
323 if row_delta == 0 and column_delta == 0:
324 return
326 layer_count = len(self.window.canvas.layers)
327 blocks = {}
328 for visual_row, column in selected_cells:
329 layer_index = layer_count - 1 - visual_row
330 if not (0 <= layer_index < layer_count):
331 continue
332 kind, start, exposure = TimelineWidget.timeline_span_at(
333 self.window.canvas.frames,
334 layer_index,
335 column,
336 )
337 if (
338 kind in ("content", "blank")
339 and start is not None
340 ):
341 key = (layer_index, int(start))
342 if key not in blocks:
343 layer = self.window.canvas.frames[
344 int(start)
345 ].layers[layer_index]
346 blocks[key] = (
347 kind,
348 max(1, int(exposure)),
349 layer.clone(),
350 )
352 if not blocks:
353 return
355 moves = []
356 for (
357 source_layer,
358 source_start,
359 ), (
360 kind,
361 exposure,
362 copied,
363 ) in blocks.items():
364 source_visual_row = (
365 layer_count - 1 - source_layer
366 )
367 target_visual_row = (
368 source_visual_row + row_delta
369 )
370 target_layer = (
371 layer_count - 1 - target_visual_row
372 )
373 target_start = (
374 source_start + column_delta
375 )
376 if (
377 target_start < 0
378 or not (0 <= target_layer < layer_count)
379 ):
380 self.window.statusBar().showMessage(
381 tr("移動先がタイムライン範囲外です。"),
382 2500,
383 )
384 return
385 moves.append(
386 (
387 source_layer,
388 source_start,
389 target_layer,
390 target_start,
391 kind,
392 exposure,
393 copied,
394 )
395 )
397 self.window.canvas.push_doc_undo()
399 # 元位置を未使用セルへ戻す。
400 for (
401 source_layer,
402 source_start,
403 _target_layer,
404 _target_start,
405 _kind,
406 _exposure,
407 _copied,
408 ) in moves:
409 source = self.window.canvas.frames[
410 source_start
411 ].layers[source_layer]
412 self.window.canvas._clear_timeline_layer_cell(source)
414 maximum_end = max(
415 target_start + exposure
416 for (
417 _source_layer,
418 _source_start,
419 _target_layer,
420 target_start,
421 _kind,
422 exposure,
423 _copied,
424 ) in moves
425 )
426 self.window.canvas._ensure_frame_count(maximum_end)
428 # 移動先と重なる既存露出を切り、既存の明示コマを消す。
429 for (
430 _source_layer,
431 _source_start,
432 target_layer,
433 target_start,
434 _kind,
435 exposure,
436 _copied,
437 ) in moves:
438 target_end = target_start + exposure - 1
439 covering = self.window.canvas.timeline_block_at(
440 target_start,
441 target_layer,
442 )
443 if covering is not None:
444 _cover_kind, cover_start, _cover_exposure = covering
445 if cover_start < target_start:
446 cover_layer = self.window.canvas.frames[
447 cover_start
448 ].layers[target_layer]
449 cover_layer.exposure = max(
450 1,
451 target_start - cover_start,
452 )
454 for column in range(
455 target_start,
456 target_end + 1,
457 ):
458 target = self.window.canvas.frames[
459 column
460 ].layers[target_layer]
461 if (
462 target.has_content
463 or getattr(
464 target,
465 "is_blank_key",
466 False,
467 )
468 ):
469 self.window.canvas._clear_timeline_layer_cell(
470 target
471 )
473 # 相対位置を保って配置。
474 for (
475 _source_layer,
476 _source_start,
477 target_layer,
478 target_start,
479 kind,
480 exposure,
481 copied,
482 ) in moves:
483 target = self.window.canvas.frames[
484 target_start
485 ].layers[target_layer]
486 target.image = (
487 copied.image.copy()
488 if kind == "content"
489 else blank_image()
490 )
491 target.visible = bool(copied.visible)
492 target.opacity = float(copied.opacity)
493 target.alpha_locked = bool(
494 copied.alpha_locked
495 )
496 target.color_filter_enabled = bool(
497 copied.color_filter_enabled
498 )
499 target.color_filter_rgb = (
500 tuple(copied.color_filter_rgb)
501 if copied.color_filter_rgb is not None
502 else None
503 )
504 target.has_content = kind == "content"
505 target.is_blank_key = kind == "blank"
506 target.sequence_number = (
507 copied.sequence_number
508 if kind == "content"
509 else None
510 )
511 target.cell_name = (
512 getattr(copied, "cell_name", None)
513 if kind == "content"
514 else None
515 )
516 target.sequence_only = bool(copied.sequence_only)
517 target.exposure = max(1, int(exposure))
519 moved_cells = [
520 (
521 row + row_delta,
522 column + column_delta,
523 )
524 for row, column in selected_cells
525 if (
526 row + row_delta >= 0
527 and column + column_delta >= 0
528 )
529 ]
531 self.window.canvas.current_frame = max(
532 0,
533 int(destination_column),
534 )
535 self.window.canvas.active_layer_index = max(
536 0,
537 min(
538 layer_count - 1,
539 layer_count - 1 - int(destination_row),
540 ),
541 )
542 self.window.canvas._cell_structure_dirty = True
543 self.window.canvas._onion_cache.clear()
544 self.window.canvas.changed.emit()
545 self.window.canvas.selectionChanged.emit()
546 self.window.canvas.update()
547 QTimer.singleShot(
548 0,
549 lambda cells=tuple(moved_cells):
550 self._restore_selection(cells)
551 )
553 def resize_exposure(
554 self, visual_row, key_column, boundary_column, edge="right"
555 ):
556 layer_count = len(self.window.canvas.layers)
557 layer_index = layer_count - 1 - int(visual_row)
558 key_column = int(key_column)
559 boundary_column = int(boundary_column)
560 if not ( 560 ↛ 564line 560 didn't jump to line 564 because the condition on line 560 was never true
561 0 <= layer_index < layer_count
562 and 0 <= key_column < len(self.window.canvas.frames)
563 ):
564 return
565 layer = self.window.canvas.frames[key_column].layers[layer_index]
566 old_exposure = max(1, int(layer.exposure))
567 old_end = key_column + old_exposure - 1
568 # A simple right-edge edit changes no pixels. Saving every frame's
569 # image here exhausted the undo memory budget and evicted history.
570 if edge != "left": 570 ↛ 602line 570 didn't jump to line 602 because the condition on line 570 was always true
571 new_end = max(key_column, boundary_column)
572 next_key = next((
573 col for col in range(key_column + 1, len(self.window.canvas.frames))
574 if self.window.canvas.frames[col].layers[layer_index].has_content
575 or self.window.canvas.frames[col].layers[layer_index].is_blank_key
576 ), None)
577 if next_key is None or new_end < next_key:
578 if new_end == old_end: 578 ↛ 579line 578 didn't jump to line 579 because the condition on line 578 was never true
579 return
580 self.window.canvas.push_undo(ExposureUndo.capture(
581 self.window.canvas, layer_index, key_column
582 ))
583 self.window.canvas._ensure_frame_count(new_end + 1)
584 layer.exposure = new_end - key_column + 1
585 self.window.canvas.current_frame = key_column
586 self.window.canvas.active_layer_index = layer_index
587 self.window.canvas.changed.emit()
588 self.window.canvas.selectionChanged.emit()
589 self.window.canvas.update()
590 return
591 shift = new_end + 1 - next_key
592 moves = tuple(
593 (col + shift, col)
594 for col in range(key_column + 1, len(self.window.canvas.frames))
595 if self.window.canvas.frames[col].layers[layer_index].has_content
596 or self.window.canvas.frames[col].layers[layer_index].is_blank_key
597 )
598 self.window.canvas.push_undo(ExposureUndo.capture(
599 self.window.canvas, layer_index, key_column, moves
600 ))
601 else:
602 self.window.canvas.push_doc_undo()
604 if edge == "left": 604 ↛ 605line 604 didn't jump to line 605 because the condition on line 604 was never true
605 previous_keys = [
606 col for col in range(0, key_column)
607 if (
608 self.window.canvas.frames[col].layers[layer_index].has_content
609 or getattr(
610 self.window.canvas.frames[col].layers[layer_index],
611 "is_blank_key",
612 False,
613 )
614 )
615 ]
616 minimum_start = 0
617 if previous_keys:
618 previous_key = previous_keys[-1]
619 previous_layer = self.window.canvas.frames[previous_key].layers[layer_index]
620 minimum_start = previous_key + max(1, previous_layer.exposure)
621 new_start = max(minimum_start, min(boundary_column, old_end))
622 if new_start == key_column:
623 if self.window.canvas.undo_stack:
624 self.window.canvas.undo_stack.pop()
625 return
626 self.window.canvas._ensure_frame_count(old_end + 1)
627 destination = self.window.canvas.frames[new_start].layers[layer_index]
628 if (
629 (
630 destination.has_content
631 or getattr(destination, "is_blank_key", False)
632 )
633 and new_start != key_column
634 ):
635 if self.window.canvas.undo_stack:
636 self.window.canvas.undo_stack.pop()
637 self.window.statusBar().showMessage(
638 tr("左端の移動先に別のコマがあるため伸縮できません。"), 2500
639 )
640 return
641 source = self.window.canvas.frames[key_column].layers[layer_index]
642 copied = source.clone()
643 destination.image = copied.image.copy()
644 destination.has_content = bool(copied.has_content)
645 destination.is_blank_key = bool(
646 getattr(copied, "is_blank_key", False)
647 )
648 destination.sequence_number = copied.sequence_number
649 destination.cell_name = getattr(copied, "cell_name", None)
650 destination.sequence_only = bool(copied.sequence_only)
651 destination.exposure = max(1, old_end - new_start + 1)
652 destination.visible = copied.visible
653 destination.opacity = copied.opacity
654 destination.alpha_locked = copied.alpha_locked
655 destination.color_filter_enabled = copied.color_filter_enabled
656 destination.color_filter_rgb = (
657 tuple(copied.color_filter_rgb)
658 if copied.color_filter_rgb is not None else None
659 )
660 if new_start != key_column:
661 self.window.canvas._clear_timeline_layer_cell(source)
662 self.window.canvas.current_frame = new_start
663 else:
664 new_end = max(key_column, boundary_column)
665 requested_exposure = max(1, new_end - key_column + 1)
666 next_keys = [
667 col for col in range(key_column + 1, len(self.window.canvas.frames))
668 if (
669 self.window.canvas.frames[col].layers[layer_index].has_content
670 or getattr(
671 self.window.canvas.frames[col].layers[layer_index],
672 "is_blank_key",
673 False,
674 )
675 )
676 ]
677 shift = 0
678 if next_keys and key_column + requested_exposure > next_keys[0]: 678 ↛ 680line 678 didn't jump to line 680 because the condition on line 678 was always true
679 shift = key_column + requested_exposure - next_keys[0]
680 if shift > 0: 680 ↛ 710line 680 didn't jump to line 710 because the condition on line 680 was always true
681 old_count = len(self.window.canvas.frames)
682 self.window.canvas._ensure_frame_count(old_count + shift)
683 for col in range(old_count - 1, key_column, -1):
684 source = self.window.canvas.frames[col].layers[layer_index]
685 if not (
686 source.has_content
687 or getattr(source, "is_blank_key", False)
688 ):
689 continue
690 destination = self.window.canvas.frames[col + shift].layers[layer_index]
691 destination.image = source.image.copy()
692 destination.has_content = bool(source.has_content)
693 destination.is_blank_key = bool(
694 getattr(source, "is_blank_key", False)
695 )
696 destination.sequence_number = source.sequence_number
697 destination.cell_name = getattr(source, "cell_name", None)
698 destination.sequence_only = bool(source.sequence_only)
699 destination.exposure = source.exposure
700 destination.visible = source.visible
701 destination.opacity = source.opacity
702 destination.alpha_locked = source.alpha_locked
703 destination.color_filter_enabled = source.color_filter_enabled
704 destination.color_filter_rgb = (
705 tuple(source.color_filter_rgb)
706 if source.color_filter_rgb is not None else None
707 )
708 self.window.canvas._clear_timeline_layer_cell(source)
709 else:
710 self.window.canvas._ensure_frame_count(key_column + requested_exposure)
711 self.window.canvas.frames[key_column].layers[layer_index].exposure = requested_exposure
712 self.window.canvas.current_frame = key_column
714 self.window.canvas.active_layer_index = layer_index
715 self.window.canvas.changed.emit()
716 self.window.canvas.selectionChanged.emit()
717 self.window.canvas.update()
719 def move_cell(self, source_row, source_column, destination_row, destination_column):
720 layer_count = len(self.window.canvas.layers)
721 source_layer = layer_count - 1 - source_row
722 destination_layer = layer_count - 1 - destination_row
723 if self.window.canvas.timeline_mode == "sequence":
724 if source_layer != destination_layer:
725 self.window.statusBar().showMessage(
726 tr("連番画像は同じレイヤー内で入れ替えてください。"), 2500
727 )
728 return
729 self.window.canvas.move_sequence_image(
730 source_layer,
731 int(source_column) + 1,
732 int(destination_column) + 1,
733 )
734 return
735 self.window._suppress_used_color_refresh_once = True
736 moved = self.window.canvas.move_timeline_cell(source_column, source_layer, destination_column, destination_layer)
737 if not moved:
738 self.window._suppress_used_color_refresh_once = False
739 self.window.statusBar().showMessage(tr("コマを移動できませんでした。"), 2500)
741 def copy_cell(self, source_row, source_column, destination_row, destination_column):
742 """Altドラッグで、同じ絵番号を参照するシートキーを複製する。"""
743 if self.window.canvas.timeline_mode != "sheet":
744 return
745 layer_count = len(self.window.canvas.layers)
746 source_layer = layer_count - 1 - int(source_row)
747 destination_layer = layer_count - 1 - int(destination_row)
748 if source_layer != destination_layer:
749 self.window.statusBar().showMessage(tr("複製は同じレイヤー内で行ってください。"), 2500)
750 return
751 block = self.window.canvas.timeline_block_at(int(source_column), source_layer)
752 if block is None or block[0] != "content":
753 return
754 source = self.window.canvas.frames[int(block[1])].layers[source_layer]
755 self.window.canvas.push_doc_undo()
756 self.window.canvas._ensure_frame_count(int(destination_column) + 1)
757 target = self.window.canvas.frames[int(destination_column)].layers[destination_layer]
758 copied = source.clone()
759 target.image = source.image
760 target.initial_image = copied.initial_image
761 target.has_content = True
762 target.is_blank_key = False
763 target.sequence_number = copied.sequence_number
764 target.cell_name = getattr(copied, "cell_name", None)
765 target.sequence_only = False
766 target.exposure = max(1, int(copied.exposure))
767 target.visible = copied.visible
768 target.opacity = copied.opacity
769 target.alpha_locked = copied.alpha_locked
770 target.color_filter_enabled = copied.color_filter_enabled
771 target.color_filter_rgb = copied.color_filter_rgb
772 self.window.canvas.current_frame = int(destination_column)
773 self.window.canvas.active_layer_index = destination_layer
774 self.window.canvas._cell_structure_dirty = True
775 self.window.canvas.changed.emit()
776 self.window.canvas.selectionChanged.emit()
777 self.window.canvas.update()
779 def recall_sequence_number(self, visual_row, column, number):
780 """既存の絵番号をシートの指定位置へ再配置する。"""
781 if self.window.canvas.timeline_mode != "sheet": 781 ↛ 782line 781 didn't jump to line 782 because the condition on line 781 was never true
782 return
783 layer_count = len(self.window.canvas.layers)
784 layer_index = layer_count - 1 - int(visual_row)
785 candidates = self.window.canvas.sequence_entry_columns(layer_index)
786 source = next((
787 self.window.canvas.frames[index].layers[layer_index]
788 for index in candidates
789 if self.window.canvas.frames[index].layers[layer_index].sequence_number == int(number)
790 ), None)
791 if source is None: 791 ↛ 795line 791 didn't jump to line 795 because the condition on line 791 was always true
792 source = self.window.canvas._sequence_archive.get(
793 (layer_index, int(number))
794 )
795 if source is None: 795 ↛ 796line 795 didn't jump to line 796 because the condition on line 795 was never true
796 return
797 self.window.canvas.push_doc_undo()
798 self.window.canvas._ensure_frame_count(int(column) + 1)
799 target = self.window.canvas.frames[int(column)].layers[layer_index]
800 copied = source.clone()
801 target.image = source.image
802 target.initial_image = copied.initial_image
803 target.has_content = bool(copied.has_content)
804 target.is_blank_key = not bool(copied.has_content)
805 target.sequence_number = int(number)
806 target.cell_name = getattr(copied, "cell_name", None)
807 target.sequence_only = False
808 target.exposure = 1
809 self.window.canvas.current_frame = int(column)
810 self.window.canvas.active_layer_index = layer_index
811 self.window.canvas._cell_structure_dirty = True
812 self.window.canvas.changed.emit()
813 self.window.canvas.selectionChanged.emit()
814 self.window.canvas.update()
816 def select_exposure(self, column, visual_row):
817 previous_layer = self.window.canvas.active_layer_index
818 if self.window.canvas.timeline_mode == "sequence": 818 ↛ 819line 818 didn't jump to line 819 because the condition on line 818 was never true
819 layer_count = len(self.window.canvas.layers)
820 layer_index = layer_count - 1 - int(visual_row)
821 number = int(column) + 1
822 entries = self.window.canvas.sequence_entry_columns(layer_index)
823 frame_by_number = {
824 int(self.window.canvas.frames[index].layers[layer_index].sequence_number): index
825 for index in entries
826 }
827 if number in frame_by_number:
828 self.window.canvas.current_frame = frame_by_number[number]
829 self.window.canvas.active_layer_index = layer_index
830 self.window.canvas.selectionChanged.emit()
831 self.window.canvas.update()
832 return
833 column = max(0, int(column))
834 if column >= len(self.window.canvas.frames):
835 self.window.canvas._ensure_frame_count(column + 1)
836 self.window.canvas._cell_structure_dirty = True
837 self.window.canvas.select_exposure(column, visual_row)
838 # タイムラインの別レイヤーのコマを選んだ場合も、そのレイヤーの使用色へ即時更新。
839 if self.window.canvas.active_layer_index != previous_layer: 839 ↛ 840line 839 didn't jump to line 840 because the condition on line 839 was never true
840 self.window.used_color._refresh_without_delay()
841 else:
842 # 同じレイヤーでは全コマ共通の使用色一覧なので、既存表示を維持する。
843 self.window.canvas.update()
845 # ------------------------------------------------------------------
846 # セルの右クリックメニューから呼ぶ操作(Issue #48)
847 # ------------------------------------------------------------------
848 def _cell_target(self, visual_row, column):
849 """``(レイヤー番号, 列)``。範囲外なら ``None``。
851 タイムラインの行は下のレイヤーが下に並ぶので、表示行とレイヤー番号は
852 逆順になる。
853 """
854 canvas = self.window.canvas
855 layer_count = len(canvas.layers)
856 layer_index = layer_count - 1 - int(visual_row)
857 column = int(column)
858 if not (0 <= layer_index < layer_count): 858 ↛ 859line 858 didn't jump to line 859 because the condition on line 858 was never true
859 return None
860 if not (0 <= column < len(canvas.frames)): 860 ↛ 861line 860 didn't jump to line 861 because the condition on line 860 was never true
861 return None
862 return layer_index, column
864 def _cell_block(self, visual_row, column):
865 """``(レイヤー番号, 露出の開始列, 露出の長さ)``。空セルなら ``None``。"""
866 target = self._cell_target(visual_row, column)
867 if target is None: 867 ↛ 868line 867 didn't jump to line 868 because the condition on line 867 was never true
868 return None
869 layer_index, column = target
870 block = self.window.canvas.timeline_block_at(column, layer_index)
871 if block is None: 871 ↛ 872line 871 didn't jump to line 872 because the condition on line 871 was never true
872 return None
873 _kind, start, exposure = block
874 return layer_index, int(start), int(exposure)
876 def _after_cell_edit(self):
877 canvas = self.window.canvas
878 canvas._cell_structure_dirty = True
879 canvas._onion_cache.clear()
880 canvas.changed.emit()
881 canvas.selectionChanged.emit()
882 canvas.update()
883 self.window.refresh_ui()
885 def clear_cell(self, visual_row, column):
886 """セルを未使用に戻す(トラックラベルを削除/削除)。
888 露出は詰めない。前のセルの表示が伸びるのではなく、そこが空くだけに
889 するため。コマごと詰めたいときは「フレームを削除」を使う。
890 """
891 block = self._cell_block(visual_row, column)
892 if block is None: 892 ↛ 893line 892 didn't jump to line 893 because the condition on line 892 was never true
893 return False
894 layer_index, start, _exposure = block
895 canvas = self.window.canvas
896 canvas.push_doc_undo()
897 canvas._clear_timeline_layer_cell(
898 canvas.frames[start].layers[layer_index]
899 )
900 self._after_cell_edit()
901 return True
903 def copy_cell_to_clipboard(self, visual_row, column):
904 """セルを控える。絵・露出・番号ごと複製して持つ。"""
905 block = self._cell_block(visual_row, column)
906 if block is None: 906 ↛ 907line 906 didn't jump to line 907 because the condition on line 906 was never true
907 self.window.statusBar().showMessage(
908 tr("コピーできるセルがありません。"), 2400
909 )
910 return False
911 layer_index, start, _exposure = block
912 self.window._timeline_cell_clipboard = (
913 self.window.canvas.frames[start].layers[layer_index].clone()
914 )
915 self.window.timeline.set_cell_clipboard_filled(True)
916 self.window.statusBar().showMessage(tr("セルをコピーしました。"), 2000)
917 return True
919 def cut_cell_to_clipboard(self, visual_row, column):
920 if not self.copy_cell_to_clipboard(visual_row, column): 920 ↛ 921line 920 didn't jump to line 921 because the condition on line 920 was never true
921 return False
922 self.clear_cell(visual_row, column)
923 self.window.statusBar().showMessage(tr("セルを切り取りました。"), 2000)
924 return True
926 def paste_cell_from_clipboard(self, visual_row, column):
927 """控えたセルを貼り付ける。名前はその場のレイヤー名のままにする。"""
928 source = getattr(self.window, "_timeline_cell_clipboard", None)
929 if source is None:
930 self.window.statusBar().showMessage(
931 tr("貼り付けられるセルがありません。"), 2400
932 )
933 return False
934 target = self._cell_target(visual_row, column)
935 if target is None: 935 ↛ 936line 935 didn't jump to line 936 because the condition on line 935 was never true
936 return False
937 layer_index, column = target
938 canvas = self.window.canvas
939 canvas.push_doc_undo()
940 existing = canvas.frames[column].layers[layer_index]
941 pasted = source.clone()
942 pasted.name = existing.name
943 pasted.visible = existing.visible
944 pasted.opacity = existing.opacity
945 # トゥイーンの帯は貼り付け先では組み直せないので持ち込まない。
946 pasted.tween = None
947 pasted.tween_member = None
948 canvas.frames[column].layers[layer_index] = pasted
949 self._after_cell_edit()
950 self.window.statusBar().showMessage(tr("セルを貼り付けました。"), 2000)
951 return True
953 def new_animation_cell(self, visual_row, column):
954 """そのセルに、絵を持つ新しいセルを作る。"""
955 target = self._cell_target(visual_row, column)
956 if target is None: 956 ↛ 957line 956 didn't jump to line 957 because the condition on line 956 was never true
957 return False
958 layer_index, column = target
959 canvas = self.window.canvas
960 canvas.push_doc_undo()
961 layer = canvas.frames[column].layers[layer_index]
962 canvas._clear_timeline_layer_cell(layer)
963 layer.image = blank_image()
964 layer.has_content = True
965 layer.exposure = 1
966 canvas.current_frame = column
967 canvas.active_layer_index = layer_index
968 self._after_cell_edit()
969 return True
971 def split_clip(self, visual_row, column):
972 """露出の途中で切り、そこから新しいセルにする(クリップを分割)。
974 絵は複製するので、分割したあとは前半・後半を別々に直せる。
975 """
976 block = self._cell_block(visual_row, column)
977 if block is None: 977 ↛ 978line 977 didn't jump to line 978 because the condition on line 977 was never true
978 return False
979 layer_index, start, exposure = block
980 target = self._cell_target(visual_row, column)
981 if target is None: 981 ↛ 982line 981 didn't jump to line 982 because the condition on line 981 was never true
982 return False
983 _layer_index, column = target
984 if column <= start or column > start + exposure - 1:
985 self.window.statusBar().showMessage(
986 tr("露出の途中を選んでください。先頭では分割できません。"), 2400
987 )
988 return False
989 canvas = self.window.canvas
990 canvas.push_doc_undo()
991 head = canvas.frames[start].layers[layer_index]
992 tail = head.clone()
993 tail.exposure = start + exposure - column
994 tail.tween = None
995 tail.tween_member = None
996 head.exposure = column - start
997 canvas.frames[column].layers[layer_index] = tail
998 self._after_cell_edit()
999 return True
1001 def merge_clip(self, visual_row, column):
1002 """前のセルの露出を伸ばして、このセルを呑み込む(クリップを結合)。"""
1003 block = self._cell_block(visual_row, column)
1004 if block is None: 1004 ↛ 1005line 1004 didn't jump to line 1005 because the condition on line 1004 was never true
1005 return False
1006 layer_index, start, exposure = block
1007 canvas = self.window.canvas
1008 previous = canvas.timeline_block_at(start - 1, layer_index) if start else None
1009 if previous is None:
1010 self.window.statusBar().showMessage(
1011 tr("前に結合できるセルがありません。"), 2400
1012 )
1013 return False
1014 _kind, previous_start, previous_exposure = previous
1015 canvas.push_doc_undo()
1016 canvas._clear_timeline_layer_cell(
1017 canvas.frames[start].layers[layer_index]
1018 )
1019 canvas.frames[previous_start].layers[layer_index].exposure = (
1020 int(previous_exposure) + int(exposure)
1021 )
1022 self._after_cell_edit()
1023 return True
1025 def insert_frame_at(self, visual_row, column):
1026 """そのレイヤーの、その位置へ1コマ分の空きを差し込む。"""
1027 target = self._cell_target(visual_row, column)
1028 if target is None: 1028 ↛ 1029line 1028 didn't jump to line 1029 because the condition on line 1028 was never true
1029 return False
1030 layer_index, column = target
1031 canvas = self.window.canvas
1032 canvas.push_doc_undo()
1033 canvas._ensure_frame_count(len(canvas.frames) + 1)
1034 canvas._shift_timeline_layer_right(layer_index, column, 1)
1035 self._after_cell_edit()
1036 return True
1038 def delete_frame_at(self, visual_row, column):
1039 """そのコマを削除して、以降を1コマ詰める。"""
1040 target = self._cell_target(visual_row, column)
1041 if target is None: 1041 ↛ 1042line 1041 didn't jump to line 1042 because the condition on line 1041 was never true
1042 return False
1043 layer_index, column = target
1044 canvas = self.window.canvas
1045 canvas.push_doc_undo()
1046 # そのコマを空にしてから、以降を1コマ詰める。露出の途中なら、
1047 # 詰めた分だけ短くなる。
1048 block = canvas.timeline_block_at(column, layer_index)
1049 if block is not None: 1049 ↛ 1050line 1049 didn't jump to line 1050 because the condition on line 1049 was never true
1050 _kind, start, exposure = block
1051 layer = canvas.frames[start].layers[layer_index]
1052 if exposure > 1:
1053 layer.exposure = exposure - 1
1054 else:
1055 canvas._clear_timeline_layer_cell(layer)
1056 canvas._shift_timeline_layer_left(layer_index, column + 1)
1057 canvas.current_frame = min(column, len(canvas.frames) - 1)
1058 self._after_cell_edit()
1059 return True