Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/sparrow/snapshots.py: 56%

532 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-10-06 15:01 +0000

1from subprocess import check_call, CalledProcessError 

2import logging 

3import tempfile 

4 

5from pyrocko import util 

6from pyrocko.guts import Object, String, Float, Bytes, clone, \ 

7 dump_all, load_all 

8 

9from pyrocko.gui.qt_compat import qw, qc, qg, get_em 

10from .state import ViewerState, Interpolator, interpolateables 

11from vtk.util.numpy_support import vtk_to_numpy 

12import vtk 

13from . import common 

14 

15guts_prefix = 'sparrow' 

16 

17logger = logging.getLogger('pyrocko.gui.sparrow.snapshots') 

18 

19thumb_size = 128, 72 

20 

21 

22def to_rect(r): 

23 return [float(x) for x in (r.left(), r.top(), r.width(), r.height())] 

24 

25 

26def fit_to_rect(frame, size, halign='center', valign='center'): 

27 fl, ft, fw, fh = to_rect(frame) 

28 rw, rh = size.width(), size.height() 

29 

30 ft += 1 

31 fh -= 1 

32 

33 fl += 1 

34 fw -= 1 

35 

36 fa = fh / fw 

37 ra = rh / rw 

38 

39 if fa <= ra: 

40 rh = fh 

41 rw = rh / ra 

42 if halign == 'left': 

43 rl = fl 

44 elif halign == 'center': 

45 rl = fl + 0.5 * (fw - rw) 

46 elif halign == 'right': 

47 rl = fl + fw - rw 

48 

49 rt = ft 

50 else: 

51 rw = fw 

52 rh = rw * ra 

53 rl = fl 

54 if valign == 'top': 

55 rt = ft 

56 elif valign == 'center': 

57 rt = ft + 0.5 * (fh - rh) 

58 elif valign == 'bottom': 

59 rt = ft + fh - rh 

60 

61 return qc.QRectF(rl, rt, rw, rh) 

62 

63 

64def getitem_or_none(items, i): 

65 try: 

66 return items[i] 

67 except IndexError: 

68 return None 

69 

70 

71def iround(f): 

72 return int(round(f)) 

73 

74 

75class SnapshotItemDelegate(qw.QStyledItemDelegate): 

76 def __init__(self, model, parent): 

77 qw.QStyledItemDelegate.__init__(self, parent=parent) 

78 self.model = model 

79 

80 def sizeHint(self, option, index): 

81 item = self.model.get_item_or_none(index) 

82 if isinstance(item, Snapshot): 

83 return qc.QSize(*thumb_size) 

84 else: 

85 return qw.QStyledItemDelegate.sizeHint(self, option, index) 

86 

87 def paint(self, painter, option, index): 

88 app = common.get_app() 

89 item = self.model.get_item_or_none(index) 

90 em = get_em(painter) 

91 frect = option.rect.adjusted(0, 0, 0, 0) 

92 nb = iround(em*0.5) 

93 trect = option.rect.adjusted(nb, nb, -nb, -nb) 

94 

95 if isinstance(item, Snapshot): 

96 

97 old_pen = painter.pen() 

98 if option.state & qw.QStyle.State_Selected: 

99 bg_brush = app.palette().brush( 

100 qg.QPalette.Active, qg.QPalette.Highlight) 

101 

102 fg_pen = qg.QPen(app.palette().color( 

103 qg.QPalette.Active, qg.QPalette.HighlightedText)) 

104 

105 painter.fillRect(frect, bg_brush) 

106 painter.setPen(fg_pen) 

107 

108 else: 

109 bg_brush = app.palette().brush( 

110 qg.QPalette.Active, qg.QPalette.AlternateBase) 

111 

112 painter.fillRect(frect, bg_brush) 

113 

114 # painter.drawRect(frect) 

115 img = item.get_image() 

116 if img is not None: 

117 prect = fit_to_rect(frect, img.size(), halign='right') 

118 painter.drawImage(prect, img) 

119 

120 painter.drawText( 

121 trect, 

122 qc.Qt.AlignLeft | qc.Qt.AlignTop, 

123 item.name) 

124 

125 painter.setPen( 

126 app.palette().brush( 

127 qg.QPalette.Disabled 

128 if item.duration is None 

129 else qg.QPalette.Active, 

130 qg.QPalette.Text).color()) 

131 

132 ed = item.effective_duration 

133 painter.drawText( 

134 trect, 

135 qc.Qt.AlignLeft | qc.Qt.AlignBottom, 

136 '%.2f s' % ed if ed != 0.0 else '') 

137 

138 painter.setPen(old_pen) 

139 

140 else: 

141 qw.QStyledItemDelegate.paint(self, painter, option, index) 

142 

143 # painter.drawText( 

144 # trect, 

145 # qc.Qt.AlignRight | qc.Qt.AlignTop, 

146 # '%.2f' % item.effective_duration) 

147 

148 def editorEvent(self, event, model, option, index): 

149 

150 item = self.model.get_item_or_none(index) 

151 

152 if isinstance(event, qg.QMouseEvent) \ 

153 and event.button() == qc.Qt.RightButton: 

154 

155 menu = qw.QMenu() 

156 

157 for name, duration in [ 

158 ('Auto', None), 

159 ('0 s', 0.0), 

160 ('1/2 s', 0.5), 

161 ('1 s', 1.0), 

162 ('2 s', 2.0), 

163 ('3 s', 3.0), 

164 ('5 s', 5.0), 

165 ('10 s', 10.0), 

166 ('60 s', 60.0)]: 

167 

168 def make_triggered(duration): 

169 def triggered(): 

170 item.duration = duration 

171 

172 return triggered 

173 

174 action = qw.QAction(name, menu) 

175 action.triggered.connect(make_triggered(duration)) 

176 menu.addAction(action) 

177 

178 action = qw.QAction('Custom...', menu) 

179 

180 def triggered(): 

181 self.parent().edit(index) 

182 

183 action.triggered.connect(triggered) 

184 

185 menu.addAction(action) 

186 menu.exec_(event.globalPos()) 

187 

188 return True 

189 

190 else: 

191 return qw.QStyledItemDelegate.editorEvent( 

192 self, event, model, option, index) 

193 

194 def createEditor(self, parent, option, index): 

195 return qw.QLineEdit(parent=parent) 

196 

197 def setModelData(self, editor, model, index): 

198 item = self.model.get_item_or_none(index) 

199 if item: 

200 try: 

201 item.duration = max(float(editor.text()), 0.0) 

202 except ValueError: 

203 item.duration = None 

204 

205 def setEditorData(self, editor, index): 

206 item = self.model.get_item_or_none(index) 

207 if item: 

208 editor.setText( 

209 'Auto' if item.duration is None else '%g' % item.duration) 

210 

211 

212class SnapshotListView(qw.QListView): 

213 

214 def startDrag(self, supported): 

215 if supported & (qc.Qt.CopyAction | qc.Qt.MoveAction): 

216 drag = qg.QDrag(self) 

217 selected_indexes = self.selectedIndexes() 

218 mime_data = self.model().mimeData(selected_indexes) 

219 drag.setMimeData(mime_data) 

220 drag.exec(qc.Qt.MoveAction) 

221 

222 def dropEvent(self, *args): 

223 mod = self.model() 

224 selected_items = [ 

225 mod.get_item_or_none(index) for index in self.selectedIndexes()] 

226 

227 selected_items = [item for item in selected_items if item is not None] 

228 

229 result = qw.QListView.dropEvent(self, *args) 

230 

231 indexes = [mod.get_index_for_item(item) for item in selected_items] 

232 

233 smod = self.selectionModel() 

234 smod.clear() 

235 scroll_index = None 

236 for index in indexes: 

237 if index is not None: 

238 smod.select(index, qc.QItemSelectionModel.Select) 

239 if scroll_index is None: 

240 scroll_index = index 

241 

242 if scroll_index is not None: 

243 self.scrollTo(scroll_index) 

244 

245 return result 

246 

247 

248class SnapshotsPanel(qw.QFrame): 

249 

250 def __init__(self, viewer): 

251 qw.QFrame.__init__(self) 

252 layout = qw.QGridLayout() 

253 self.setLayout(layout) 

254 

255 self.model = SnapshotsModel() 

256 

257 self.viewer = viewer 

258 

259 lv = SnapshotListView() 

260 lv.sizePolicy().setVerticalPolicy(qw.QSizePolicy.Expanding) 

261 lv.setModel(self.model) 

262 lv.doubleClicked.connect(self.goto_snapshot) 

263 lv.setSelectionMode(qw.QAbstractItemView.ExtendedSelection) 

264 lv.setDragDropMode(qw.QAbstractItemView.InternalMove) 

265 lv.setEditTriggers(qw.QAbstractItemView.NoEditTriggers) 

266 lv.viewport().setAcceptDrops(True) 

267 self.item_delegate = SnapshotItemDelegate(self.model, lv) 

268 lv.setItemDelegate(self.item_delegate) 

269 self.list_view = lv 

270 layout.addWidget(lv, 0, 0, 1, 3) 

271 

272 pb = qw.QPushButton('New') 

273 pb.clicked.connect(self.take_snapshot) 

274 layout.addWidget(pb, 1, 0, 1, 1) 

275 

276 pb = qw.QPushButton('Replace') 

277 pb.clicked.connect(self.replace_snapshot) 

278 layout.addWidget(pb, 1, 1, 1, 1) 

279 

280 pb = qw.QPushButton('Delete') 

281 pb.clicked.connect(self.delete_snapshots) 

282 layout.addWidget(pb, 1, 2, 1, 1) 

283 

284 self.window_to_image_filter = None 

285 

286 def setup_menu(self, menu): 

287 menu.addAction( 

288 'New', 

289 self.take_snapshot, 

290 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_N)).setShortcutContext( 

291 qc.Qt.ApplicationShortcut) 

292 

293 menu.addSeparator() 

294 

295 menu.addAction( 

296 'Next', 

297 self.transition_to_next_snapshot, 

298 qg.QKeySequence(qc.Qt.Key_PageDown)).setShortcutContext( 

299 qc.Qt.ApplicationShortcut) 

300 

301 menu.addAction( 

302 'Previous', 

303 self.transition_to_previous_snapshot, 

304 qg.QKeySequence(qc.Qt.Key_PageUp)).setShortcutContext( 

305 qc.Qt.ApplicationShortcut) 

306 

307 menu.addSeparator() 

308 

309 menu.addAction( 

310 'Import...', 

311 self.import_snapshots) 

312 

313 menu.addAction( 

314 'Export...', 

315 self.export_snapshots) 

316 

317 menu.addAction( 

318 'Animate', 

319 self.animate_snapshots) 

320 

321 menu.addAction( 

322 'Export Movie...', 

323 self.render_movie) 

324 

325 menu.addSeparator() 

326 

327 menu.addAction( 

328 'Show Panel', 

329 self.show_and_raise) 

330 

331 def show_and_raise(self): 

332 self.viewer.raise_panel(self) 

333 

334 def get_snapshot_image(self): 

335 if not self.window_to_image_filter: 

336 wif = vtk.vtkWindowToImageFilter() 

337 wif.SetInput(self.viewer.renwin) 

338 wif.SetInputBufferTypeToRGBA() 

339 wif.ReadFrontBufferOff() 

340 self.window_to_image_filter = wif 

341 

342 writer = vtk.vtkPNGWriter() 

343 writer.SetInputConnection(wif.GetOutputPort()) 

344 writer.SetWriteToMemory(True) 

345 self.png_writer = writer 

346 

347 self.viewer.renwin.Render() 

348 self.window_to_image_filter.Modified() 

349 self.png_writer.Write() 

350 data = vtk_to_numpy(self.png_writer.GetResult()).tobytes() 

351 img = qg.QImage() 

352 img.loadFromData(data) 

353 return img 

354 

355 def get_snapshot_thumbnail(self): 

356 return self.get_snapshot_image().scaled( 

357 thumb_size[0], thumb_size[1], 

358 qc.Qt.KeepAspectRatio, qc.Qt.SmoothTransformation) 

359 

360 def get_snapshot_thumbnail_png(self): 

361 img = self.get_snapshot_thumbnail() 

362 

363 ba = qc.QByteArray() 

364 buf = qc.QBuffer(ba) 

365 buf.open(qc.QIODevice.WriteOnly) 

366 img.save(buf, format='PNG') 

367 return ba.data() 

368 

369 def take_snapshot(self): 

370 self.model.add_snapshot( 

371 Snapshot( 

372 state=clone(self.viewer.state), 

373 thumb=self.get_snapshot_thumbnail_png())) 

374 self.viewer.raise_panel(self) 

375 

376 def replace_snapshot(self): 

377 state = clone(self.viewer.state) 

378 selected_indexes = self.list_view.selectedIndexes() 

379 

380 if len(selected_indexes) == 1: 

381 self.model.replace_snapshot( 

382 selected_indexes[0], 

383 Snapshot( 

384 state, 

385 thumb=self.get_snapshot_thumbnail_png())) 

386 

387 self.list_view.update() 

388 

389 def goto_snapshot(self, index): 

390 item = self.model.get_item_or_none(index) 

391 if isinstance(item, Snapshot): 

392 self.viewer.set_state(item.state) 

393 elif isinstance(item, Transition): 

394 snap1 = self.model.get_item_or_none(index.row()-1) 

395 snap2 = self.model.get_item_or_none(index.row()+1) 

396 if isinstance(snap1, Snapshot) and isinstance(snap2, Snapshot): 

397 ip = Interpolator( 

398 [0.0, item.effective_duration], 

399 [snap1.state, snap2.state]) 

400 

401 self.viewer.start_animation(ip) 

402 

403 def transition_to_next_snapshot(self, direction=1): 

404 index = self.list_view.currentIndex() 

405 if index.row() == -1: 

406 if direction == 1: 

407 index = self.model.createIndex(0, 0) 

408 

409 item = self.model.get_item_or_none(index) 

410 if item is None: 

411 return 

412 

413 if isinstance(item, Snapshot): 

414 snap1 = item 

415 transition = self.model.get_item_or_none(index.row()+1*direction) 

416 snap2 = self.model.get_item_or_none(index.row()+2*direction) 

417 elif isinstance(item, Transition): 

418 snap1 = self.model.get_item_or_none(index.row()-1*direction) 

419 transition = item 

420 snap2 = self.model.get_item_or_none(index.row()+1*direction) 

421 

422 if None not in (snap1, transition, snap2): 

423 ip = Interpolator( 

424 [0.0, transition.effective_duration], 

425 [snap1.state, snap2.state]) 

426 

427 index = self.model.get_index_for_item(snap2) 

428 self.list_view.setCurrentIndex(index) 

429 

430 self.viewer.start_animation(ip) 

431 

432 elif snap2 is not None: 

433 index = self.model.get_index_for_item(snap2) 

434 self.list_view.setCurrentIndex(index) 

435 self.viewer.set_state(snap2.state) 

436 

437 def transition_to_previous_snapshot(self): 

438 self.transition_to_next_snapshot(-1) 

439 

440 def delete_snapshots(self): 

441 selected_indexes = self.list_view.selectedIndexes() 

442 self.model.remove_snapshots(selected_indexes) 

443 

444 def animate_snapshots(self, **kwargs): 

445 selected_indexes = self.list_view.selectedIndexes() 

446 items = self.model.get_series(selected_indexes) 

447 

448 time_state = [] 

449 item_previous = None 

450 t = 0.0 

451 for i, item in enumerate(items): 

452 item_next = getitem_or_none(items, i+1) 

453 item_previous = getitem_or_none(items, i-1) 

454 

455 if isinstance(item, Snapshot): 

456 time_state.append((t, item.state)) 

457 if item.effective_duration > 0: 

458 time_state.append((t+item.effective_duration, item.state)) 

459 

460 t += item.effective_duration 

461 

462 elif isinstance(item, Transition): 

463 if None not in (item_previous, item_next) \ 

464 and item.effective_duration != 0.0: 

465 

466 t += item.effective_duration 

467 

468 item_previous = item 

469 

470 if len(time_state) < 2: 

471 return 

472 

473 ip = Interpolator(*zip(*time_state)) 

474 

475 self.viewer.start_animation( 

476 ip, output_path=kwargs.get('output_path', None)) 

477 

478 def render_movie(self): 

479 try: 

480 check_call(['ffmpeg', '-loglevel', 'panic']) 

481 except CalledProcessError: 

482 pass 

483 except (TypeError, FileNotFoundError): 

484 logger.warn( 

485 'Package ffmpeg needed for movie rendering. Please install it ' 

486 '(e.g. on linux distr. via sudo apt-get ffmpeg.) and retry.') 

487 return 

488 

489 caption = 'Export Movie' 

490 fn_out, _ = qw.QFileDialog.getSaveFileName( 

491 self, caption, 'movie.mp4', 

492 options=common.qfiledialog_options) 

493 

494 if fn_out: 

495 self.animate_snapshots(output_path=fn_out) 

496 

497 def export_snapshots(self): 

498 caption = 'Export Snapshots' 

499 fn, _ = qw.QFileDialog.getSaveFileName( 

500 self, caption, options=common.qfiledialog_options) 

501 

502 selected_indexes = self.list_view.selectedIndexes() 

503 items = self.model.get_series(selected_indexes) 

504 

505 if fn: 

506 dump_all(items, filename=fn) 

507 

508 def add_snapshots(self, snapshots): 

509 self.model.append_series(snapshots) 

510 

511 def load_snapshots(self, path): 

512 items = load_snapshots(path) 

513 self.add_snapshots(items) 

514 

515 def import_snapshots(self): 

516 caption = 'Import Snapshots' 

517 path, _ = qw.QFileDialog.getOpenFileName( 

518 self, caption, options=common.qfiledialog_options) 

519 

520 if path: 

521 self.load_snapshots(path) 

522 

523 

524class Item(Object): 

525 duration = Float.T(optional=True) 

526 

527 def __init__(self, **kwargs): 

528 Object.__init__(self, **kwargs) 

529 self.auto_duration = 0.0 

530 

531 @property 

532 def effective_duration(self): 

533 if self.duration is not None: 

534 return self.duration 

535 else: 

536 return self.auto_duration 

537 

538 

539class Snapshot(Item): 

540 name = String.T() 

541 state = ViewerState.T() 

542 thumb = Bytes.T(optional=True) 

543 

544 isnapshot = 0 

545 

546 def __init__(self, state, name=None, thumb=None, **kwargs): 

547 

548 if name is None: 

549 Snapshot.isnapshot += 1 

550 name = '%i' % Snapshot.isnapshot 

551 

552 Item.__init__(self, state=state, name=name, thumb=thumb, **kwargs) 

553 self._img = None 

554 

555 def get_name(self): 

556 return self.name 

557 

558 def get_image(self): 

559 if self.thumb is not None and not self._img: 

560 img = qg.QImage() 

561 img.loadFromData(self.thumb) 

562 self._img = img 

563 

564 return self._img 

565 

566 

567class Transition(Item): 

568 

569 def __init__(self, **kwargs): 

570 Item.__init__(self, **kwargs) 

571 self.animate = [] 

572 

573 def get_name(self): 

574 ed = self.effective_duration 

575 return '%s %s' % ( 

576 'T' if self.animate and self.effective_duration > 0.0 else '', 

577 '%.2f s' % ed if ed != 0.0 else '') 

578 

579 @property 

580 def name(self): 

581 return self.get_name() 

582 

583 

584class SnapshotsModel(qc.QAbstractListModel): 

585 

586 def __init__(self): 

587 qc.QAbstractListModel.__init__(self) 

588 self._items = [] 

589 

590 def supportedDropActions(self): 

591 return qc.Qt.MoveAction 

592 

593 def rowCount(self, parent=None): 

594 return len(self._items) 

595 

596 def insertRows(self, index): 

597 pass 

598 

599 def mimeTypes(self): 

600 return ['text/plain'] 

601 

602 def mimeData(self, indices): 

603 objects = [self._items[i.row()] for i in indices] 

604 serialized = dump_all(objects) 

605 md = qc.QMimeData() 

606 md.setText(serialized) 

607 md._item_objects = objects 

608 return md 

609 

610 def dropMimeData(self, md, action, row, col, index): 

611 i = index.row() 

612 items = getattr(md, '_item_objects', []) 

613 self.beginInsertRows(qc.QModelIndex(), i, i) 

614 self._items[i:i] = items 

615 self.endInsertRows() 

616 n = len(items) 

617 joff = 0 

618 for j in range(len(self._items)): 

619 if (j < i or j >= i+n) and self._items[j+joff] in items: 

620 self.beginRemoveRows(qc.QModelIndex(), j+joff, j+joff) 

621 self._items[j+joff:j+joff+1] = [] 

622 self.endRemoveRows() 

623 joff -= 1 

624 

625 self.repair_transitions() 

626 return True 

627 

628 def removeRows(self, i, n, parent): 

629 return True 

630 

631 def flags(self, index): 

632 if index.isValid(): 

633 i = index.row() 

634 if isinstance(self._items[i], Snapshot): 

635 return qc.Qt.ItemFlags( 

636 qc.Qt.ItemIsSelectable 

637 | qc.Qt.ItemIsEnabled 

638 | qc.Qt.ItemIsDragEnabled 

639 | qc.Qt.ItemIsEditable) 

640 

641 else: 

642 return qc.Qt.ItemFlags( 

643 qc.Qt.ItemIsEnabled 

644 | qc.Qt.ItemIsEnabled 

645 | qc.Qt.ItemIsDropEnabled 

646 | qc.Qt.ItemIsEditable) 

647 else: 

648 return qc.QAbstractListModel.flags(self, index) 

649 

650 def data(self, index, role): 

651 app = common.get_app() 

652 i = index.row() 

653 item = self._items[i] 

654 is_snap = isinstance(item, Snapshot) 

655 if role == qc.Qt.DisplayRole: 

656 if is_snap: 

657 return qc.QVariant(str(item.get_name())) 

658 else: 

659 return qc.QVariant(str(item.get_name())) 

660 

661 elif role == qc.Qt.ToolTipRole: 

662 if is_snap: 

663 # return qc.QVariant(str(item.state)) 

664 return qc.QVariant() 

665 else: 

666 if item.animate: 

667 label = 'Interpolation: %s' % \ 

668 ', '.join(x[0] for x in item.animate) 

669 else: 

670 label = 'Not interpolable.' 

671 

672 return qc.QVariant(label) 

673 

674 elif role == qc.Qt.TextAlignmentRole and not is_snap: 

675 return qc.QVariant(qc.Qt.AlignRight) 

676 

677 elif role == qc.Qt.ForegroundRole and not is_snap: 

678 if item.duration is None: 

679 return qc.QVariant(app.palette().brush( 

680 qg.QPalette.Disabled, qg.QPalette.Text)) 

681 else: 

682 return qc.QVariant(app.palette().brush( 

683 qg.QPalette.Active, qg.QPalette.Text)) 

684 

685 else: 

686 qc.QVariant() 

687 

688 def headerData(self): 

689 pass 

690 

691 def add_snapshot(self, snapshot): 

692 self.beginInsertRows( 

693 qc.QModelIndex(), self.rowCount(), self.rowCount()) 

694 self._items.append(snapshot) 

695 self.endInsertRows() 

696 self.repair_transitions() 

697 

698 def replace_snapshot(self, index, snapshot): 

699 self._items[index.row()] = snapshot 

700 self.dataChanged.emit(index, index) 

701 self.repair_transitions() 

702 

703 def remove_snapshots(self, indexes): 

704 indexes = sorted(indexes, key=lambda index: index.row()) 

705 ioff = 0 

706 for index in indexes: 

707 i = index.row() 

708 self.beginRemoveRows(qc.QModelIndex(), i+ioff, i+ioff) 

709 self._items[i+ioff:i+ioff+1] = [] 

710 self.endRemoveRows() 

711 ioff -= 1 

712 

713 self.repair_transitions() 

714 

715 def repair_transitions(self): 

716 items = self._items 

717 i = 0 

718 need = 0 

719 while i < len(items): 

720 if need == 0: 

721 if not isinstance(items[i], Transition): 

722 self.beginInsertRows(qc.QModelIndex(), i, i) 

723 items[i:i] = [Transition()] 

724 self.endInsertRows() 

725 else: 

726 i += 1 

727 need = 1 

728 elif need == 1: 

729 if not isinstance(items[i], Snapshot): 

730 self.beginRemoveRows(qc.QModelIndex(), i, i) 

731 items[i:i+1] = [] 

732 self.endRemoveRows() 

733 else: 

734 i += 1 

735 need = 0 

736 

737 if len(items) == 1: 

738 self.beginRemoveRows(qc.QModelIndex(), 0, 0) 

739 items[:] = [] 

740 self.endRemoveRows() 

741 

742 elif len(items) > 1: 

743 if not isinstance(items[-1], Transition): 

744 self.beginInsertRows( 

745 qc.QModelIndex(), self.rowCount(), self.rowCount()) 

746 items.append(Transition()) 

747 self.endInsertRows() 

748 

749 self.update_auto_durations() 

750 

751 def update_auto_durations(self): 

752 items = self._items 

753 for i, item in enumerate(items): 

754 if isinstance(item, Transition): 

755 if 0 < i < len(items)-1: 

756 item.animate = interpolateables( 

757 items[i-1].state, items[i+1].state) 

758 

759 if item.animate: 

760 item.auto_duration = 1. 

761 else: 

762 item.auto_duration = 0. 

763 

764 for i, item in enumerate(items): 

765 if isinstance(item, Snapshot): 

766 if 0 < i < len(items)-1: 

767 if items[i-1].effective_duration == 0 \ 

768 and items[i+1].effective_duration == 0: 

769 item.auto_duration = 1. 

770 else: 

771 item.auto_duration = 0. 

772 

773 def get_index_for_item(self, item): 

774 for i, candidate in enumerate(self._items): 

775 if candidate is item: 

776 return self.createIndex(i, 0) 

777 

778 return None 

779 

780 def get_item_or_none(self, index): 

781 if not isinstance(index, int): 

782 i = index.row() 

783 else: 

784 i = index 

785 

786 if i < 0 or len(self._items) <= i: 

787 return None 

788 

789 try: 

790 return self._items[i] 

791 except IndexError: 

792 return None 

793 

794 def get_series(self, indexes): 

795 items = self._items 

796 

797 ilist = sorted([index.row() for index in indexes]) 

798 if len(ilist) <= 1: 

799 ilist = list(range(0, len(self._items))) 

800 

801 ilist = [i for i in ilist if isinstance(items[i], Snapshot)] 

802 if len(ilist) == 0: 

803 return [] 

804 

805 i = ilist[0] 

806 

807 series = [] 

808 while ilist: 

809 i = ilist.pop(0) 

810 series.append(items[i]) 

811 if ilist and ilist[0] == i+2: 

812 series.append(items[i+1]) 

813 

814 return series 

815 

816 def append_series(self, items): 

817 self.beginInsertRows( 

818 qc.QModelIndex(), 

819 self.rowCount(), self.rowCount() + len(items) - 1) 

820 

821 self._items.extend(items) 

822 self.endInsertRows() 

823 

824 self.repair_transitions() 

825 

826 

827def load_snapshots(path): 

828 

829 if path.startswith('http://') or path.startswith('https://'): 

830 with tempfile.NamedTemporaryFile() as fp: 

831 util.download_file(path, fp.name) 

832 return load_snapshots(fp.name) 

833 

834 items = load_all(filename=path) 

835 for i in range(len(items)): 

836 if not isinstance( 

837 items[i], (ViewerState, Snapshot, Transition)): 

838 

839 logger.warn( 

840 'Only Snapshot, Transition and ViewerState objects ' 

841 'are accepted. Object #%i from file %s ignored.' 

842 % (i, path)) 

843 

844 if isinstance(items[i], ViewerState): 

845 items[i] = Snapshot(items[i]) 

846 

847 for item in items: 

848 if isinstance(item, Snapshot): 

849 item.state.sort_elements() 

850 

851 return items