1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6from ..qt_compat import qc, qg, qw, QPixmapCache 

7 

8from .marker import EventMarker, PhaseMarker 

9from ..util import make_QPolygonF 

10from pyrocko.plot.beachball import mt2beachball, BeachballError 

11from pyrocko.moment_tensor import kagan_angle 

12from pyrocko.plot import tango_colors 

13from pyrocko import orthodrome 

14from pyrocko.util import time_to_str 

15 

16import logging 

17 

18from .marker import g_color_b 

19 

20 

21def faint(c): 

22 return tuple(255 - (255 - x) // 5 for x in c) 

23 

24 

25g_color_b_faint = [faint(c) for c in g_color_b] 

26 

27 

28def noop(x=None): 

29 return x 

30 

31 

32qc.QString = str 

33qc.QVariant = noop 

34 

35 

36def toFloat(val): 

37 try: 

38 return float(val), True 

39 except (ValueError, TypeError): 

40 return 9e99, False 

41 

42 

43logger = logging.getLogger('pyrocko.gui.snuffler.marker_editor') 

44 

45_header_data = [ 

46 'T', 'Time', 'M', 'Label', 'Depth [km]', 'Lat', 'Lon', 'Kind', 'Dist [km]', 

47 'NSLCs', 'Polarity', 'Kagan Angle [deg]', 'Event Hash', 'MT'] 

48 

49_column_mapping = dict(zip(_header_data, range(len(_header_data)))) 

50 

51_string_header = (_column_mapping['Time'], _column_mapping['Label']) 

52 

53_header_sizes = [70] * len(_header_data) 

54_header_sizes[0] = 40 

55_header_sizes[1] = 190 

56_header_sizes[-1] = 20 

57 

58 

59class BeachballWidget(qw.QWidget): 

60 

61 def __init__(self, moment_tensor, color, *args, **kwargs): 

62 qw.QWidget.__init__(self, *args, **kwargs) 

63 self.color = color 

64 self.moment_tensor = moment_tensor 

65 self.setGeometry(0, 0, 100, 100) 

66 self.setAttribute(qc.Qt.WA_TranslucentBackground) 

67 

68 self.flipy = qg.QTransform() 

69 self.flipy.translate(0, self.height()) 

70 self.flipy.scale(1, -1) 

71 

72 def paintEvent(self, e): 

73 center = e.rect().center() 

74 painter = qg.QPainter(self) 

75 painter.save() 

76 painter.setWorldTransform(self.flipy) 

77 try: 

78 data = mt2beachball( 

79 self.moment_tensor, size=self.height()/2.2, 

80 position=(center.x(), center.y()), 

81 color_t=self.color, color_p=qc.Qt.white, edgecolor=qc.Qt.black) 

82 for pdata in data: 

83 paths, fill, edges, thickness = pdata 

84 

85 pen = qg.QPen(edges) 

86 pen.setWidthF(3) 

87 if fill != 'none': 

88 brush = qg.QBrush(fill) 

89 painter.setBrush(brush) 

90 

91 polygon = qg.QPolygonF() 

92 polygon = make_QPolygonF(*paths.T) 

93 painter.setRenderHint(qg.QPainter.Antialiasing) 

94 painter.setPen(pen) 

95 painter.drawPolygon(polygon) 

96 except BeachballError as e: 

97 logger.exception(e) 

98 finally: 

99 painter.restore() 

100 

101 def to_qpixmap(self): 

102 try: 

103 return self.grab(self.rect()) 

104 except AttributeError: 

105 return qg.QPixmap().grabWidget(self, self.rect()) 

106 

107 

108class MarkerItemDelegate(qw.QStyledItemDelegate): 

109 

110 # Takes care of how table entries are displayed 

111 

112 def __init__(self, *args, **kwargs): 

113 qw.QStyledItemDelegate.__init__(self, *args, **kwargs) 

114 self.c_alignment = qc.Qt.AlignHCenter 

115 self.bbcache = QPixmapCache() 

116 

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

118 mcolor = self.color_from_index(index) 

119 

120 if index.column() == _column_mapping['MT']: 

121 mt = self.bb_data_from_index(index) 

122 if mt: 

123 key = ''.join([str(round(x, 1)) for x in mt.m6()]) 

124 pixmap = self.bbcache.cached(key) 

125 if pixmap: 

126 pixmap = pixmap.scaledToHeight(option.rect.height()) 

127 else: 

128 pixmap = BeachballWidget( 

129 moment_tensor=mt, 

130 color=qg.QColor(*tango_colors['scarletred3']) 

131 ).to_qpixmap() 

132 self.bbcache.insert(key, pixmap) 

133 a, b, c, d = option.rect.getRect() 

134 painter.save() 

135 painter.setRenderHint(qg.QPainter.Antialiasing) 

136 painter.drawPixmap(int(round(a+d/2.)), b, d, d, pixmap) 

137 painter.restore() 

138 

139 else: 

140 if index.column() == 0: 

141 option.state = option.state & ~qw.QStyle.State_Selected 

142 

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

144 

145 marker = self.parent().model().get_marker(index) 

146 

147 if marker.active: 

148 

149 painter.save() 

150 

151 rect = option.rect 

152 x1, y1, x2, y2 = rect.getCoords() 

153 y1 += 1 

154 pen = painter.pen() 

155 pen.setWidth(2) 

156 pen.setColor(mcolor) 

157 painter.setPen(pen) 

158 painter.drawLine(qc.QLineF(x1, y1, x2, y1)) 

159 painter.drawLine(qc.QLineF(x1, y2, x2, y2)) 

160 painter.restore() 

161 

162 def marker_from_index(self, index): 

163 tv = self.parent() 

164 pv = tv.pile_viewer 

165 tvm = tv.model() 

166 if isinstance(tvm, qc.QSortFilterProxyModel): 

167 return pv.markers[tvm.mapToSource(index).row()] 

168 else: 

169 return pv.markers[index.row()] 

170 

171 def bb_data_from_index(self, index): 

172 marker = self.marker_from_index(index) 

173 if isinstance(marker, EventMarker): 

174 return marker.get_event().moment_tensor 

175 else: 

176 return None 

177 

178 def color_from_index(self, index): 

179 marker = self.marker_from_index(index) 

180 return qg.QColor(*marker.select_color(g_color_b)) 

181 

182 

183class MarkerSortFilterProxyModel(qc.QSortFilterProxyModel): 

184 

185 # Proxy object between view and model to handle sorting 

186 

187 def __init__(self, *args, **kwargs): 

188 qc.QSortFilterProxyModel.__init__(self, *args, **kwargs) 

189 self.setSortRole(qc.Qt.UserRole) 

190 self.sort(1, qc.Qt.AscendingOrder) 

191 

192 def get_marker(self, index): 

193 return self.sourceModel().get_marker(self.mapToSource(index)) 

194 

195 

196class MarkerTableView(qw.QTableView): 

197 def __init__(self, *args, **kwargs): 

198 sortable = kwargs.pop('sortable', True) 

199 qw.QTableView.__init__(self, *args, **kwargs) 

200 self.setSelectionBehavior(qw.QAbstractItemView.SelectRows) 

201 self.setHorizontalScrollMode(qw.QAbstractItemView.ScrollPerPixel) 

202 self.setEditTriggers(qw.QAbstractItemView.DoubleClicked) 

203 self.setSortingEnabled(sortable) 

204 self.setStyleSheet( 

205 'QTableView{selection-background-color: \ 

206 rgba(130, 130, 130, 100% );}') 

207 

208 self.sortByColumn(1, qc.Qt.AscendingOrder) 

209 self.setAlternatingRowColors(True) 

210 

211 self.setShowGrid(False) 

212 self.verticalHeader().hide() 

213 self.pile_viewer = None 

214 

215 self.clicked.connect(self.table_clicked) 

216 self.doubleClicked.connect(self.table_double_clicked) 

217 

218 self.header_menu = qw.QMenu(self) 

219 

220 show_initially = ['Type', 'Time', 'Magnitude'] 

221 self.menu_labels = ['Type', 'Time', 'Magnitude', 'Label', 'Depth [km]', 

222 'Latitude/Longitude', 'Kind', 'Distance [km]', 

223 'NSLCs', 'Polarity', 'Kagan Angle [deg]', 

224 'Event Hash', 'MT'] 

225 

226 self.menu_items = dict(zip( 

227 self.menu_labels, [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13])) 

228 

229 self.editable_columns = [2, 3, 4, 5, 6, 7] 

230 

231 self.column_actions = {} 

232 for hd in self.menu_labels: 

233 a = qw.QAction(hd, self.header_menu) 

234 a.triggered.connect( 

235 self.toggle_columns) 

236 a.setCheckable(True) 

237 a.setChecked(hd in show_initially) 

238 self.header_menu.addAction(a) 

239 self.column_actions[hd] = a 

240 

241 a = qw.QAction('Numbering', self.header_menu) 

242 a.setCheckable(True) 

243 a.setChecked(False) 

244 a.triggered.connect( 

245 self.toggle_numbering) 

246 self.header_menu.addAction(a) 

247 

248 header = self.horizontalHeader() 

249 header.setContextMenuPolicy(qc.Qt.CustomContextMenu) 

250 header.customContextMenuRequested.connect( 

251 self.show_context_menu) 

252 

253 self.right_click_menu = qw.QMenu(self) 

254 print_action = qw.QAction('Print Table', self.right_click_menu) 

255 print_action.triggered.connect(self.print_menu) 

256 self.right_click_menu.addAction(print_action) 

257 

258 def wheelEvent(self, wheel_event): 

259 if wheel_event.modifiers() & qc.Qt.ControlModifier: 

260 height = self.rowAt(self.height()) 

261 ci = self.indexAt( 

262 qc.QPoint(self.viewport().rect().x(), height)) 

263 v = self.verticalHeader() 

264 

265 wheel_delta = wheel_event.angleDelta().y() 

266 

267 v.setDefaultSectionSize( 

268 max(12, v.defaultSectionSize()+wheel_delta//60)) 

269 self.scrollTo(ci) 

270 if v.isVisible(): 

271 self.toggle_numbering(False) 

272 self.toggle_numbering(True) 

273 

274 else: 

275 super(MarkerTableView, self).wheelEvent(wheel_event) 

276 

277 def set_viewer(self, viewer): 

278 ''' 

279 Set connected pile viewer and hook up signals. 

280 ''' 

281 

282 self.pile_viewer = viewer 

283 

284 def keyPressEvent(self, key_event): 

285 # Propagate key_event to pile_viewer, unless up/down pressed 

286 

287 if key_event.key() in [qc.Qt.Key_Up, qc.Qt.Key_Down]: 

288 qw.QTableView.keyPressEvent(self, key_event) 

289 self.pile_viewer.go_to_selection() 

290 else: 

291 self.pile_viewer.keyPressEvent(key_event) 

292 

293 def table_clicked(self, model_index): 

294 # Ignore mouse clicks 

295 pass 

296 

297 def contextMenuEvent(self, event): 

298 self.right_click_menu.popup(qg.QCursor.pos()) 

299 

300 def toggle_numbering(self, want): 

301 if want: 

302 self.verticalHeader().show() 

303 else: 

304 self.verticalHeader().hide() 

305 

306 def print_menu(self): 

307 from ..qt_compat import qprint 

308 printer = qprint.QPrinter(qprint.QPrinter.ScreenResolution) 

309 printer.setOutputFormat(qprint.QPrinter.NativeFormat) 

310 printer_dialog = qprint.QPrintDialog(printer, self) 

311 if printer_dialog.exec_() == qw.QDialog.Accepted: 

312 

313 scrollbarpolicy = self.verticalScrollBarPolicy() 

314 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

315 rect = printer.pageRect() 

316 painter = qg.QPainter() 

317 painter.begin(printer) 

318 xscale = rect.width() / (self.width()*1.1) 

319 yscale = rect.height() / (self.height() * 1.1) 

320 scale = min(xscale, yscale) 

321 painter.translate(rect.x() + rect.width()/2., 

322 rect.y() + rect.height()/2.) 

323 painter.scale(scale, scale) 

324 painter.translate(-self.width()/2., -self.height()/2.) 

325 painter.setRenderHints(qg.QPainter.HighQualityAntialiasing | 

326 qg.QPainter.TextAntialiasing) 

327 self.render(painter) 

328 painter.end() 

329 self.setVerticalScrollBarPolicy(scrollbarpolicy) 

330 

331 def table_double_clicked(self, model_index): 

332 if model_index.column() in self.editable_columns: 

333 return 

334 else: 

335 self.pile_viewer.go_to_selection() 

336 

337 def show_context_menu(self, point): 

338 ''' 

339 Pop-up menu to toggle column visibility. 

340 ''' 

341 

342 self.header_menu.popup(self.mapToGlobal(point)) 

343 

344 def toggle_columns(self): 

345 ''' 

346 Toggle columns depending in checked state. 

347 ''' 

348 

349 width = 0 

350 for header, ca in self.column_actions.items(): 

351 hide = not ca.isChecked() 

352 self.setColumnHidden(self.menu_items[header], hide) 

353 if header == 'Latitude/Longitude': 

354 self.setColumnHidden(self.menu_items[header]+1, hide) 

355 if not hide: 

356 width += _header_sizes[self.menu_labels.index(header)] 

357 

358 self.parent().setMinimumWidth(width) 

359 

360 def update_viewport(self): 

361 self.viewport().update() 

362 

363 

364class MarkerTableModel(qc.QAbstractTableModel): 

365 

366 def __init__(self, *args, **kwargs): 

367 qc.QAbstractTableModel.__init__(self, *args, **kwargs) 

368 self.pile_viewer = None 

369 self.distances = {} 

370 self.kagan_angles = {} 

371 self.row_count = 0 

372 self.proxy_filter = None 

373 

374 def sourceModel(self): 

375 return self 

376 

377 def set_viewer(self, viewer): 

378 ''' 

379 Set connected pile viewer and hook up signals. 

380 ''' 

381 

382 self.pile_viewer = viewer 

383 self.pile_viewer.begin_markers_add.connect( 

384 self.begin_markers_add) 

385 self.pile_viewer.end_markers_add.connect( 

386 self.end_markers_add) 

387 self.pile_viewer.begin_markers_remove.connect( 

388 self.begin_markers_remove) 

389 self.pile_viewer.end_markers_remove.connect( 

390 self.end_markers_remove) 

391 

392 def rowCount(self, parent=None): 

393 if not self.pile_viewer: 

394 return 0 

395 return len(self.pile_viewer.get_markers()) 

396 

397 def columnCount(self, parent=None): 

398 return len(_column_mapping) 

399 

400 def begin_markers_add(self, istart, istop): 

401 self.beginInsertRows(qc.QModelIndex(), istart, istop) 

402 

403 def end_markers_add(self): 

404 self.endInsertRows() 

405 

406 def begin_markers_remove(self, istart, istop): 

407 self.beginRemoveRows(qc.QModelIndex(), istart, istop) 

408 

409 def end_markers_remove(self): 

410 self.endRemoveRows() 

411 self.marker_table_view.updateGeometries() 

412 

413 def headerData(self, col, orientation, role): 

414 ''' 

415 Get header data entry. 

416 ''' 

417 

418 if orientation == qc.Qt.Horizontal: 

419 if role == qc.Qt.DisplayRole: 

420 return qc.QVariant(_header_data[col]) 

421 elif role == qc.Qt.SizeHintRole: 

422 return qc.QSize(10, 20) 

423 

424 elif orientation == qc.Qt.Vertical: 

425 if role == qc.Qt.DisplayRole: 

426 return qc.QVariant(str(col)) 

427 

428 else: 

429 return qc.QVariant() 

430 

431 def get_marker(self, index): 

432 return self.pile_viewer.markers[index.row()] 

433 

434 def data(self, index, role): 

435 ''' 

436 Get model data entry. 

437 ''' 

438 

439 if not self.pile_viewer: 

440 return qc.QVariant() 

441 

442 marker = self.pile_viewer.markers[index.row()] 

443 column = index.column() 

444 

445 if role == qc.Qt.BackgroundRole: 

446 if marker.active or column == _column_mapping['T']: 

447 return qg.QBrush( 

448 qg.QColor(*marker.select_color(g_color_b_faint))) 

449 

450 if role == qc.Qt.ForegroundRole: 

451 if marker.active or column == _column_mapping['T']: 

452 return qg.QBrush( 

453 qg.QColor(*marker.select_color(g_color_b))) 

454 

455 elif role in (qc.Qt.DisplayRole, qc.Qt.UserRole): 

456 

457 v = None 

458 if column == _column_mapping['Time']: 

459 if role == qc.Qt.UserRole: 

460 v = marker.tmin 

461 else: 

462 v = time_to_str(marker.tmin) 

463 

464 elif column == _column_mapping['T']: 

465 if isinstance(marker, EventMarker): 

466 v = u'\u25ce' 

467 elif isinstance(marker, PhaseMarker): 

468 v = marker.get_label() 

469 

470 elif column == _column_mapping['M']: 

471 if isinstance(marker, EventMarker): 

472 e = marker.get_event() 

473 if e.moment_tensor is not None: 

474 v = round(e.moment_tensor.magnitude, 1) 

475 elif e.magnitude is not None: 

476 v = round(e.magnitude, 1) 

477 

478 elif column == _column_mapping['Label']: 

479 if isinstance(marker, EventMarker): 

480 v = marker.label() 

481 elif isinstance(marker, PhaseMarker): 

482 v = marker.get_label() 

483 

484 elif column == _column_mapping['Depth [km]']: 

485 if isinstance(marker, EventMarker): 

486 d = marker.get_event().depth 

487 if d is not None: 

488 v = round(marker.get_event().depth/1000., 1) 

489 

490 elif column == _column_mapping['Lat']: 

491 if isinstance(marker, EventMarker): 

492 v = round(marker.get_event().effective_lat, 2) 

493 

494 elif column == _column_mapping['Lon']: 

495 if isinstance(marker, EventMarker): 

496 v = round(marker.get_event().effective_lon, 2) 

497 

498 elif column == _column_mapping['Kind']: 

499 v = marker.kind 

500 

501 elif column == _column_mapping['Dist [km]']: 

502 active_event = self.pile_viewer.get_active_event() 

503 if isinstance(marker, EventMarker) \ 

504 and active_event is not None: 

505 

506 dist = orthodrome.distance_accurate50m( 

507 marker.get_event(), 

508 active_event) 

509 

510 v = dist 

511 if role == qc.Qt.DisplayRole: 

512 v = '%.5g' % (v/1000.) 

513 

514 elif column == _column_mapping['NSLCs']: 

515 strs = [] 

516 for nslc_id in marker.get_nslc_ids(): 

517 strs.append('.'.join(nslc_id)) 

518 v = '|'.join(strs) 

519 

520 elif column == _column_mapping['Kagan Angle [deg]']: 

521 active_event = self.pile_viewer.get_active_event() 

522 if isinstance(marker, EventMarker) \ 

523 and active_event is not None \ 

524 and active_event.moment_tensor is not None \ 

525 and marker.get_event().moment_tensor is not None: 

526 

527 v = kagan_angle( 

528 active_event.moment_tensor, 

529 marker.get_event().moment_tensor) 

530 

531 if role == qc.Qt.DisplayRole: 

532 v = '%.1f' % v 

533 

534 elif column == _column_mapping['MT']: 

535 return qc.QVariant() 

536 

537 elif column == _column_mapping['Event Hash']: 

538 if isinstance(marker, (EventMarker, PhaseMarker)): 

539 v = marker.get_event_hash() 

540 else: 

541 return qc.QVariant() 

542 

543 elif column == _column_mapping['Polarity']: 

544 if isinstance(marker, (PhaseMarker)): 

545 v = marker.get_polarity_symbol() 

546 else: 

547 return qc.QVariant() 

548 

549 return qc.QVariant(v) 

550 

551 return qc.QVariant() 

552 

553 def handle_active_event_changed(self): 

554 nmarkers = self.rowCount() 

555 istart = self.index(0, _column_mapping['Dist [km]']) 

556 istop = self.index(nmarkers-1, _column_mapping['Dist [km]']) 

557 self.dataChanged.emit(istart, istop) 

558 

559 istart = self.index(0, _column_mapping['Kagan Angle [deg]']) 

560 istop = self.index(nmarkers-1, _column_mapping['Kagan Angle [deg]']) 

561 self.dataChanged.emit(istart, istop) 

562 

563 def done(self, index): 

564 self.dataChanged.emit(index, index) 

565 return True 

566 

567 def setData(self, index, value, role): 

568 ''' 

569 Set model data entry. 

570 ''' 

571 

572 if role == qc.Qt.EditRole: 

573 imarker = index.row() 

574 marker = self.pile_viewer.markers[imarker] 

575 if index.column() in [_column_mapping[c] for c in [ 

576 'M', 'Lat', 'Lon', 'Depth [km]']]: 

577 

578 if not isinstance(marker, EventMarker): 

579 return False 

580 else: 

581 if index.column() == _column_mapping['M']: 

582 valuef, valid = toFloat(value) 

583 if valid: 

584 e = marker.get_event() 

585 if e.moment_tensor is None: 

586 e.magnitude = valuef 

587 else: 

588 e.moment_tensor.magnitude = valuef 

589 return self.done(index) 

590 

591 if index.column() in [_column_mapping['Lon'], 

592 _column_mapping['Lat'], 

593 _column_mapping['Depth [km]']]: 

594 if isinstance(marker, EventMarker): 

595 valuef, valid = toFloat(value) 

596 if valid: 

597 if index.column() == _column_mapping['Lat']: 

598 marker.get_event().lat = valuef 

599 elif index.column() == _column_mapping['Lon']: 

600 marker.get_event().lon = valuef 

601 elif index.column() == _column_mapping[ 

602 'Depth [km]']: 

603 marker.get_event().depth = valuef*1000. 

604 return self.done(index) 

605 

606 if index.column() == _column_mapping['Label']: 

607 values = str(value) 

608 if values != '': 

609 if isinstance(marker, EventMarker): 

610 marker.get_event().set_name(values) 

611 return self.done(index) 

612 

613 if isinstance(marker, PhaseMarker): 

614 marker.set_phasename(values) 

615 return self.done(index) 

616 

617 return False 

618 

619 def flags(self, index): 

620 ''' 

621 Set flags for cells which the user can edit. 

622 ''' 

623 

624 if index.column() not in self.marker_table_view.editable_columns: 

625 return qc.Qt.ItemFlags(33) 

626 else: 

627 if isinstance(self.pile_viewer.markers[index.row()], EventMarker): 

628 if index.column() in self.marker_table_view.editable_columns: 

629 return qc.Qt.ItemFlags(35) 

630 if index.column() == _column_mapping['Label']: 

631 return qc.Qt.ItemFlags(35) 

632 return qc.Qt.ItemFlags(33) 

633 

634 

635class MarkerEditor(qw.QFrame): 

636 

637 def __init__(self, *args, **kwargs): 

638 sortable = kwargs.pop('sortable', True) 

639 qw.QFrame.__init__(self, *args, **kwargs) 

640 layout = qw.QGridLayout() 

641 layout.setContentsMargins(0, 0, 0, 0) 

642 self.setLayout(layout) 

643 self.marker_table_view = MarkerTableView(self, sortable=sortable) 

644 

645 self.delegate = MarkerItemDelegate(self.marker_table_view) 

646 self.marker_table_view.setItemDelegate(self.delegate) 

647 

648 self.marker_model = MarkerTableModel() 

649 self.marker_model.marker_table_view = self.marker_table_view 

650 

651 if sortable: 

652 self.proxy_filter = MarkerSortFilterProxyModel() 

653 self.proxy_filter.setDynamicSortFilter(True) 

654 self.proxy_filter.setSourceModel(self.marker_model) 

655 self.marker_model.proxy_filter = self.proxy_filter 

656 

657 self.marker_table_view.setModel(self.proxy_filter) 

658 else: 

659 self.proxy_filter = None 

660 self.marker_table_view.setModel(self.marker_model) 

661 

662 header = self.marker_table_view.horizontalHeader() 

663 for i_s, s in enumerate(_header_sizes): 

664 header.setSectionResizeMode(i_s, qw.QHeaderView.Interactive) 

665 header.resizeSection(i_s, s) 

666 

667 header.setStretchLastSection(True) 

668 

669 if self.proxy_filter: 

670 self.selection_model = qc.QItemSelectionModel(self.proxy_filter) 

671 else: 

672 self.selection_model = qc.QItemSelectionModel(self.marker_model) 

673 

674 self.marker_table_view.setSelectionModel(self.selection_model) 

675 self.selection_model.selectionChanged.connect( 

676 self.set_selected_markers) 

677 

678 layout.addWidget(self.marker_table_view, 0, 0) 

679 

680 self.pile_viewer = None 

681 self._size_hint = qc.QSize(1, 1) 

682 

683 def set_viewer(self, viewer): 

684 ''' 

685 Set the pile viewer and connect signals. 

686 ''' 

687 

688 self.pile_viewer = viewer 

689 self.marker_model.set_viewer(viewer) 

690 self.marker_table_view.set_viewer(viewer) 

691 self.pile_viewer.marker_selection_changed.connect( 

692 self.update_selection_model) 

693 

694 self.pile_viewer.active_event_marker_changed.connect( 

695 self.marker_model.handle_active_event_changed) 

696 

697 # self.pile_viewer.active_event_marker_changed.connect( 

698 # self.marker_table_view.update_viewport) 

699 

700 self.marker_table_view.toggle_columns() 

701 

702 def set_selected_markers(self, selected, deselected): 

703 ''' 

704 Update selection in viewer to reflect changes in table data. 

705 ''' 

706 

707 if self.proxy_filter: 

708 def to_source(x): 

709 ind = self.proxy_filter.index(x, 0) 

710 return self.proxy_filter.mapToSource(ind).row() 

711 else: 

712 def to_source(x): 

713 return x 

714 

715 markers = self.pile_viewer.markers 

716 

717 for rsel in selected: 

718 for i in range(rsel.top(), rsel.bottom()+1): 

719 marker = markers[to_source(i)] 

720 if not marker.selected: 

721 marker.selected = True 

722 self.pile_viewer.n_selected_markers += 1 

723 

724 for rsel in deselected: 

725 for i in range(rsel.top(), rsel.bottom()+1): 

726 marker = markers[to_source(i)] 

727 if marker.selected: 

728 marker.selected = False 

729 self.pile_viewer.n_selected_markers -= 1 

730 

731 self.pile_viewer.update() 

732 

733 def get_marker_model(self): 

734 ''' 

735 Get the attached Qt table data model. 

736 

737 :returns: :py:class:`MarkerTableModel` object 

738 ''' 

739 

740 return self.marker_model 

741 

742 def update_selection_model(self, indices): 

743 ''' 

744 Set currently selected table rows. 

745 

746 :param indices: begin and end+1 indices of contiguous selection chunks 

747 :type indices: list of tuples 

748 ''' 

749 self.selection_model.clearSelection() 

750 selections = qc.QItemSelection() 

751 selection_flags = qc.QItemSelectionModel.SelectionFlags(int( 

752 qc.QItemSelectionModel.Select | 

753 qc.QItemSelectionModel.Rows | 

754 qc.QItemSelectionModel.Current)) 

755 

756 for chunk in indices: 

757 mi_start = self.marker_model.index(chunk[0], 0) 

758 mi_stop = self.marker_model.index(chunk[1]-1, 0) 

759 if self.proxy_filter: 

760 row_selection = self.proxy_filter.mapSelectionFromSource( 

761 qc.QItemSelection(mi_start, mi_stop)) 

762 else: 

763 row_selection = qc.QItemSelection(mi_start, mi_stop) 

764 selections.merge(row_selection, selection_flags) 

765 

766 if len(indices) != 0: 

767 if self.proxy_filter: 

768 self.marker_table_view.scrollTo( 

769 self.proxy_filter.mapFromSource(mi_start)) 

770 else: 

771 self.marker_table_view.scrollTo(mi_start) 

772 

773 self.marker_table_view.setCurrentIndex(mi_start) 

774 self.selection_model.setCurrentIndex( 

775 mi_start, selection_flags) 

776 

777 self.selection_model.select(selections, selection_flags)