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

1"""Timeline cell/frame/key operations; owned as ``window.timeline_ops``. 

2 

3Drives timeline navigation, exposure and drawing-number editing, blank-key 

4creation, frame deletion, and cell move/copy against the timeline widget. 

5 

6A collaborator rather than a mixin -- see ``main_window_export.py`` for why. 

7""" 

8from typing import TYPE_CHECKING 

9 

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 

17 

18if TYPE_CHECKING: 

19 from .main_window import MainWindow 

20 

21log = get_logger(__name__) 

22 

23 

24class TimelineOpsController: 

25 """Owned by ``MainWindow`` as ``window.timeline_ops``. 

26 

27 A collaborator rather than a mixin -- see main_window_export.py for why. 

28 """ 

29 

30 def __init__(self, window: "MainWindow"): 

31 self.window = window 

32 

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

81 

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

114 

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

123 

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

147 

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

153 

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

159 

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

165 

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

171 

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

206 

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 ) 

222 

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 ) 

242 

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

268 

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 ) 

284 

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 

318 

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 

325 

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 ) 

351 

352 if not blocks: 

353 return 

354 

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 ) 

396 

397 self.window.canvas.push_doc_undo() 

398 

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) 

413 

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) 

427 

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 ) 

453 

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 ) 

472 

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

518 

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 ] 

530 

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 ) 

552 

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

603 

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 

713 

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

718 

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) 

740 

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

778 

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

815 

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

844 

845 # ------------------------------------------------------------------ 

846 # セルの右クリックメニューから呼ぶ操作(Issue #48) 

847 # ------------------------------------------------------------------ 

848 def _cell_target(self, visual_row, column): 

849 """``(レイヤー番号, 列)``。範囲外なら ``None``。 

850 

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 

863 

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) 

875 

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

884 

885 def clear_cell(self, visual_row, column): 

886 """セルを未使用に戻す(トラックラベルを削除/削除)。 

887 

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 

902 

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 

918 

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 

925 

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 

952 

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 

970 

971 def split_clip(self, visual_row, column): 

972 """露出の途中で切り、そこから新しいセルにする(クリップを分割)。 

973 

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 

1000 

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 

1024 

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 

1037 

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