1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5from __future__ import absolute_import 

6 

7import sys 

8 

9from .qt_compat import qc, qg, qw, QSortFilterProxyModel, \ 

10 QItemSelectionModel, QItemSelection, QPixmapCache, use_pyqt5 

11 

12from .util import EventMarker, PhaseMarker, make_QPolygonF 

13from pyrocko.plot.beachball import mt2beachball, BeachballError 

14from pyrocko.moment_tensor import kagan_angle 

15from pyrocko.plot import tango_colors 

16from pyrocko import orthodrome 

17from pyrocko.util import time_to_str 

18 

19import logging 

20 

21from .marker import g_color_b 

22 

23 

24def faint(c): 

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

26 

27 

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

29 

30 

31def noop(x=None): 

32 return x 

33 

34 

35if sys.version_info[0] >= 3 or use_pyqt5: 

36 qc.QString = str 

37 qc.QVariant = noop 

38 

39 def toFloat(val): 

40 try: 

41 return float(val), True 

42 except (ValueError, TypeError): 

43 return 9e99, False 

44 

45else: 

46 def toFloat(val): 

47 return val.toFloat() 

48 

49 

50logger = logging.getLogger('pyrocko.gui.marker_editor') 

51 

52_header_data = [ 

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

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

55 

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

57 

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

59 

60_header_sizes = [70] * len(_header_data) 

61_header_sizes[0] = 40 

62_header_sizes[1] = 190 

63_header_sizes[-1] = 20 

64 

65 

66class BeachballWidget(qw.QWidget): 

67 

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

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

70 self.color = color 

71 self.moment_tensor = moment_tensor 

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

73 self.setAttribute(qc.Qt.WA_TranslucentBackground) 

74 

75 self.flipy = qg.QTransform() 

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

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

78 

79 def paintEvent(self, e): 

80 center = e.rect().center() 

81 painter = qg.QPainter(self) 

82 painter.save() 

83 painter.setWorldTransform(self.flipy) 

84 try: 

85 data = mt2beachball( 

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

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

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

89 for pdata in data: 

90 paths, fill, edges, thickness = pdata 

91 

92 pen = qg.QPen(edges) 

93 pen.setWidthF(3) 

94 if fill != 'none': 

95 brush = qg.QBrush(fill) 

96 painter.setBrush(brush) 

97 

98 polygon = qg.QPolygonF() 

99 polygon = make_QPolygonF(*paths.T) 

100 painter.setRenderHint(qg.QPainter.Antialiasing) 

101 painter.setPen(pen) 

102 painter.drawPolygon(polygon) 

103 except BeachballError as e: 

104 logger.exception(e) 

105 finally: 

106 painter.restore() 

107 

108 def to_qpixmap(self): 

109 try: 

110 return self.grab(self.rect()) 

111 except AttributeError: 

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

113 

114 

115class MarkerItemDelegate(qw.QStyledItemDelegate): 

116 

117 # Takes care of how table entries are displayed 

118 

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

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

121 self.c_alignment = qc.Qt.AlignHCenter 

122 self.bbcache = QPixmapCache() 

123 

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

125 mcolor = self.color_from_index(index) 

126 

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

128 mt = self.bb_data_from_index(index) 

129 if mt: 

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

131 pixmap = self.bbcache.cached(key) 

132 if pixmap: 

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

134 else: 

135 pixmap = BeachballWidget( 

136 moment_tensor=mt, 

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

138 ).to_qpixmap() 

139 self.bbcache.insert(key, pixmap) 

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

141 painter.save() 

142 painter.setRenderHint(qg.QPainter.Antialiasing) 

143 painter.drawPixmap(a+d/2., b, d, d, pixmap) 

144 painter.restore() 

145 

146 else: 

147 if index.column() == 0: 

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

149 

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

151 

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

153 

154 if marker.active: 

155 

156 painter.save() 

157 

158 rect = option.rect 

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

160 y1 += 1 

161 pen = painter.pen() 

162 pen.setWidth(2) 

163 pen.setColor(mcolor) 

164 painter.setPen(pen) 

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

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

167 painter.restore() 

168 

169 def marker_from_index(self, index): 

170 tv = self.parent() 

171 pv = tv.pile_viewer 

172 tvm = tv.model() 

173 if isinstance(tvm, QSortFilterProxyModel): 

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

175 else: 

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

177 

178 def bb_data_from_index(self, index): 

179 marker = self.marker_from_index(index) 

180 if isinstance(marker, EventMarker): 

181 return marker.get_event().moment_tensor 

182 else: 

183 return None 

184 

185 def color_from_index(self, index): 

186 marker = self.marker_from_index(index) 

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

188 

189 

190class MarkerSortFilterProxyModel(QSortFilterProxyModel): 

191 

192 # Proxy object between view and model to handle sorting 

193 

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

195 QSortFilterProxyModel.__init__(self, *args, **kwargs) 

196 self.setSortRole(qc.Qt.UserRole) 

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

198 

199 def get_marker(self, index): 

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

201 

202 

203class MarkerTableView(qw.QTableView): 

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

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

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

207 self.setSelectionBehavior(qw.QAbstractItemView.SelectRows) 

208 self.setHorizontalScrollMode(qw.QAbstractItemView.ScrollPerPixel) 

209 self.setEditTriggers(qw.QAbstractItemView.DoubleClicked) 

210 self.setSortingEnabled(sortable) 

211 self.setStyleSheet( 

212 'QTableView{selection-background-color: \ 

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

214 

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

216 self.setAlternatingRowColors(True) 

217 

218 self.setShowGrid(False) 

219 self.verticalHeader().hide() 

220 self.pile_viewer = None 

221 

222 self.clicked.connect(self.table_clicked) 

223 self.doubleClicked.connect(self.table_double_clicked) 

224 

225 self.header_menu = qw.QMenu(self) 

226 

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

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

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

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

231 'Event Hash', 'MT'] 

232 

233 self.menu_items = dict(zip( 

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

235 

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

237 

238 self.column_actions = {} 

239 for hd in self.menu_labels: 

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

241 a.triggered.connect( 

242 self.toggle_columns) 

243 a.setCheckable(True) 

244 a.setChecked(hd in show_initially) 

245 self.header_menu.addAction(a) 

246 self.column_actions[hd] = a 

247 

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

249 a.setCheckable(True) 

250 a.setChecked(False) 

251 a.triggered.connect( 

252 self.toggle_numbering) 

253 self.header_menu.addAction(a) 

254 

255 header = self.horizontalHeader() 

256 header.setContextMenuPolicy(qc.Qt.CustomContextMenu) 

257 header.customContextMenuRequested.connect( 

258 self.show_context_menu) 

259 

260 self.right_click_menu = qw.QMenu(self) 

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

262 print_action.triggered.connect(self.print_menu) 

263 self.right_click_menu.addAction(print_action) 

264 

265 def wheelEvent(self, wheel_event): 

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

267 height = self.rowAt(self.height()) 

268 ci = self.indexAt( 

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

270 v = self.verticalHeader() 

271 

272 if use_pyqt5: 

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

274 else: 

275 wheel_delta = wheel_event.delta() 

276 

277 v.setDefaultSectionSize( 

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

279 self.scrollTo(ci) 

280 if v.isVisible(): 

281 self.toggle_numbering(False) 

282 self.toggle_numbering(True) 

283 

284 else: 

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

286 

287 def set_viewer(self, viewer): 

288 ''' 

289 Set connected pile viewer and hook up signals. 

290 ''' 

291 

292 self.pile_viewer = viewer 

293 

294 def keyPressEvent(self, key_event): 

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

296 

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

298 qw.QTableView.keyPressEvent(self, key_event) 

299 self.pile_viewer.go_to_selection() 

300 else: 

301 self.pile_viewer.keyPressEvent(key_event) 

302 

303 def table_clicked(self, model_index): 

304 # Ignore mouse clicks 

305 pass 

306 

307 def contextMenuEvent(self, event): 

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

309 

310 def toggle_numbering(self, want): 

311 if want: 

312 self.verticalHeader().show() 

313 else: 

314 self.verticalHeader().hide() 

315 

316 def print_menu(self): 

317 from .qt_compat import qprint 

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

319 printer.setOutputFormat(qprint.QPrinter.NativeFormat) 

320 printer_dialog = qprint.QPrintDialog(printer, self) 

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

322 

323 scrollbarpolicy = self.verticalScrollBarPolicy() 

324 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

325 rect = printer.pageRect() 

326 painter = qg.QPainter() 

327 painter.begin(printer) 

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

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

330 scale = min(xscale, yscale) 

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

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

333 painter.scale(scale, scale) 

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

335 painter.setRenderHints(qg.QPainter.HighQualityAntialiasing | 

336 qg.QPainter.TextAntialiasing) 

337 self.render(painter) 

338 painter.end() 

339 self.setVerticalScrollBarPolicy(scrollbarpolicy) 

340 

341 def table_double_clicked(self, model_index): 

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

343 return 

344 else: 

345 self.pile_viewer.go_to_selection() 

346 

347 def show_context_menu(self, point): 

348 ''' 

349 Pop-up menu to toggle column visibility. 

350 ''' 

351 

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

353 

354 def toggle_columns(self): 

355 ''' 

356 Toggle columns depending in checked state. 

357 ''' 

358 

359 width = 0 

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

361 hide = not ca.isChecked() 

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

363 if header == 'Latitude/Longitude': 

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

365 if not hide: 

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

367 

368 self.parent().setMinimumWidth(width) 

369 

370 def update_viewport(self): 

371 self.viewport().update() 

372 

373 

374class MarkerTableModel(qc.QAbstractTableModel): 

375 

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

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

378 self.pile_viewer = None 

379 self.distances = {} 

380 self.kagan_angles = {} 

381 self.row_count = 0 

382 self.proxy_filter = None 

383 

384 def sourceModel(self): 

385 return self 

386 

387 def set_viewer(self, viewer): 

388 ''' 

389 Set connected pile viewer and hook up signals. 

390 ''' 

391 

392 self.pile_viewer = viewer 

393 self.pile_viewer.begin_markers_add.connect( 

394 self.begin_markers_add) 

395 self.pile_viewer.end_markers_add.connect( 

396 self.end_markers_add) 

397 self.pile_viewer.begin_markers_remove.connect( 

398 self.begin_markers_remove) 

399 self.pile_viewer.end_markers_remove.connect( 

400 self.end_markers_remove) 

401 

402 def rowCount(self, parent=None): 

403 if not self.pile_viewer: 

404 return 0 

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

406 

407 def columnCount(self, parent=None): 

408 return len(_column_mapping) 

409 

410 def begin_markers_add(self, istart, istop): 

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

412 

413 def end_markers_add(self): 

414 self.endInsertRows() 

415 

416 def begin_markers_remove(self, istart, istop): 

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

418 

419 def end_markers_remove(self): 

420 self.endRemoveRows() 

421 self.marker_table_view.updateGeometries() 

422 

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

424 ''' 

425 Get header data entry. 

426 ''' 

427 

428 if orientation == qc.Qt.Horizontal: 

429 if role == qc.Qt.DisplayRole: 

430 return qc.QVariant(_header_data[col]) 

431 elif role == qc.Qt.SizeHintRole: 

432 return qc.QSize(10, 20) 

433 

434 elif orientation == qc.Qt.Vertical: 

435 if role == qc.Qt.DisplayRole: 

436 return qc.QVariant(str(col)) 

437 

438 else: 

439 return qc.QVariant() 

440 

441 def get_marker(self, index): 

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

443 

444 def data(self, index, role): 

445 ''' 

446 Get model data entry. 

447 ''' 

448 

449 if not self.pile_viewer: 

450 return qc.QVariant() 

451 

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

453 column = index.column() 

454 

455 if role == qc.Qt.BackgroundRole: 

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

457 return qg.QBrush( 

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

459 

460 if role == qc.Qt.ForegroundRole: 

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

462 return qg.QBrush( 

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

464 

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

466 

467 v = None 

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

469 if role == qc.Qt.UserRole: 

470 v = marker.tmin 

471 else: 

472 v = time_to_str(marker.tmin) 

473 

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

475 if isinstance(marker, EventMarker): 

476 v = u'\u25ce' 

477 elif isinstance(marker, PhaseMarker): 

478 v = marker.get_label() 

479 

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

481 if isinstance(marker, EventMarker): 

482 e = marker.get_event() 

483 if e.moment_tensor is not None: 

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

485 elif e.magnitude is not None: 

486 v = round(e.magnitude, 1) 

487 

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

489 if isinstance(marker, EventMarker): 

490 v = marker.label() 

491 elif isinstance(marker, PhaseMarker): 

492 v = marker.get_label() 

493 

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

495 if isinstance(marker, EventMarker): 

496 d = marker.get_event().depth 

497 if d is not None: 

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

499 

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

501 if isinstance(marker, EventMarker): 

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

503 

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

505 if isinstance(marker, EventMarker): 

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

507 

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

509 v = marker.kind 

510 

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

512 active_event = self.pile_viewer.get_active_event() 

513 if isinstance(marker, EventMarker) \ 

514 and active_event is not None: 

515 

516 dist = orthodrome.distance_accurate50m( 

517 marker.get_event(), 

518 active_event) 

519 

520 v = dist 

521 if role == qc.Qt.DisplayRole: 

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

523 

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

525 strs = [] 

526 for nslc_id in marker.get_nslc_ids(): 

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

528 v = '|'.join(strs) 

529 

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

531 active_event = self.pile_viewer.get_active_event() 

532 if isinstance(marker, EventMarker) \ 

533 and active_event is not None \ 

534 and active_event.moment_tensor is not None \ 

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

536 

537 v = kagan_angle( 

538 active_event.moment_tensor, 

539 marker.get_event().moment_tensor) 

540 

541 if role == qc.Qt.DisplayRole: 

542 v = '%.1f' % v 

543 

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

545 return qc.QVariant() 

546 

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

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

549 v = marker.get_event_hash() 

550 else: 

551 return qc.QVariant() 

552 

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

554 if isinstance(marker, (PhaseMarker)): 

555 v = marker.get_polarity_symbol() 

556 else: 

557 return qc.QVariant() 

558 

559 return qc.QVariant(v) 

560 

561 return qc.QVariant() 

562 

563 def handle_active_event_changed(self): 

564 nmarkers = self.rowCount() 

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

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

567 self.dataChanged.emit(istart, istop) 

568 

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

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

571 self.dataChanged.emit(istart, istop) 

572 

573 def done(self, index): 

574 self.dataChanged.emit(index, index) 

575 return True 

576 

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

578 ''' 

579 Set model data entry. 

580 ''' 

581 

582 if role == qc.Qt.EditRole: 

583 imarker = index.row() 

584 marker = self.pile_viewer.markers[imarker] 

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

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

587 

588 if not isinstance(marker, EventMarker): 

589 return False 

590 else: 

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

592 valuef, valid = toFloat(value) 

593 if valid: 

594 e = marker.get_event() 

595 if e.moment_tensor is None: 

596 e.magnitude = valuef 

597 else: 

598 e.moment_tensor.magnitude = valuef 

599 return self.done(index) 

600 

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

602 _column_mapping['Lat'], 

603 _column_mapping['Depth [km]']]: 

604 if isinstance(marker, EventMarker): 

605 valuef, valid = toFloat(value) 

606 if valid: 

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

608 marker.get_event().lat = valuef 

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

610 marker.get_event().lon = valuef 

611 elif index.column() == _column_mapping[ 

612 'Depth [km]']: 

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

614 return self.done(index) 

615 

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

617 values = str(value) 

618 if values != '': 

619 if isinstance(marker, EventMarker): 

620 marker.get_event().set_name(values) 

621 return self.done(index) 

622 

623 if isinstance(marker, PhaseMarker): 

624 marker.set_phasename(values) 

625 return self.done(index) 

626 

627 return False 

628 

629 def flags(self, index): 

630 ''' 

631 Set flags for cells which the user can edit. 

632 ''' 

633 

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

635 return qc.Qt.ItemFlags(33) 

636 else: 

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

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

639 return qc.Qt.ItemFlags(35) 

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

641 return qc.Qt.ItemFlags(35) 

642 return qc.Qt.ItemFlags(33) 

643 

644 

645class MarkerEditor(qw.QFrame): 

646 

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

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

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

650 layout = qw.QGridLayout() 

651 layout.setContentsMargins(0, 0, 0, 0) 

652 self.setLayout(layout) 

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

654 

655 self.delegate = MarkerItemDelegate(self.marker_table_view) 

656 self.marker_table_view.setItemDelegate(self.delegate) 

657 

658 self.marker_model = MarkerTableModel() 

659 self.marker_model.marker_table_view = self.marker_table_view 

660 

661 if sortable: 

662 self.proxy_filter = MarkerSortFilterProxyModel() 

663 self.proxy_filter.setDynamicSortFilter(True) 

664 self.proxy_filter.setSourceModel(self.marker_model) 

665 self.marker_model.proxy_filter = self.proxy_filter 

666 

667 self.marker_table_view.setModel(self.proxy_filter) 

668 else: 

669 self.proxy_filter = None 

670 self.marker_table_view.setModel(self.marker_model) 

671 

672 header = self.marker_table_view.horizontalHeader() 

673 for i_s, s in enumerate(_header_sizes): 

674 if use_pyqt5: 

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

676 else: 

677 header.setResizeMode(i_s, qw.QHeaderView.Interactive) 

678 

679 header.resizeSection(i_s, s) 

680 

681 header.setStretchLastSection(True) 

682 

683 if self.proxy_filter: 

684 self.selection_model = QItemSelectionModel(self.proxy_filter) 

685 else: 

686 self.selection_model = QItemSelectionModel(self.marker_model) 

687 

688 self.marker_table_view.setSelectionModel(self.selection_model) 

689 self.selection_model.selectionChanged.connect( 

690 self.set_selected_markers) 

691 

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

693 

694 self.pile_viewer = None 

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

696 

697 def set_viewer(self, viewer): 

698 ''' 

699 Set the pile viewer and connect signals. 

700 ''' 

701 

702 self.pile_viewer = viewer 

703 self.marker_model.set_viewer(viewer) 

704 self.marker_table_view.set_viewer(viewer) 

705 self.pile_viewer.marker_selection_changed.connect( 

706 self.update_selection_model) 

707 

708 self.pile_viewer.active_event_marker_changed.connect( 

709 self.marker_model.handle_active_event_changed) 

710 

711 # self.pile_viewer.active_event_marker_changed.connect( 

712 # self.marker_table_view.update_viewport) 

713 

714 self.marker_table_view.toggle_columns() 

715 

716 def set_selected_markers(self, selected, deselected): 

717 ''' 

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

719 ''' 

720 

721 if self.proxy_filter: 

722 def to_source(x): 

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

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

725 else: 

726 def to_source(x): 

727 return x 

728 

729 markers = self.pile_viewer.markers 

730 

731 for rsel in selected: 

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

733 marker = markers[to_source(i)] 

734 if not marker.selected: 

735 marker.selected = True 

736 self.pile_viewer.n_selected_markers += 1 

737 

738 for rsel in deselected: 

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

740 marker = markers[to_source(i)] 

741 if marker.selected: 

742 marker.selected = False 

743 self.pile_viewer.n_selected_markers -= 1 

744 

745 self.pile_viewer.update() 

746 

747 def get_marker_model(self): 

748 ''' 

749 Get the attached Qt table data model. 

750 

751 :returns: :py:class:`MarkerTableModel` object 

752 ''' 

753 

754 return self.marker_model 

755 

756 def update_selection_model(self, indices): 

757 ''' 

758 Set currently selected table rows. 

759 

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

761 :type indices: list of tuples 

762 ''' 

763 self.selection_model.clearSelection() 

764 selections = QItemSelection() 

765 selection_flags = QItemSelectionModel.SelectionFlags( 

766 (QItemSelectionModel.Select | 

767 QItemSelectionModel.Rows | 

768 QItemSelectionModel.Current)) 

769 

770 for chunk in indices: 

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

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

773 if self.proxy_filter: 

774 row_selection = self.proxy_filter.mapSelectionFromSource( 

775 QItemSelection(mi_start, mi_stop)) 

776 else: 

777 row_selection = QItemSelection(mi_start, mi_stop) 

778 selections.merge(row_selection, selection_flags) 

779 

780 if len(indices) != 0: 

781 if self.proxy_filter: 

782 self.marker_table_view.scrollTo( 

783 self.proxy_filter.mapFromSource(mi_start)) 

784 else: 

785 self.marker_table_view.scrollTo(mi_start) 

786 

787 self.marker_table_view.setCurrentIndex(mi_start) 

788 self.selection_model.setCurrentIndex( 

789 mi_start, selection_flags) 

790 

791 self.selection_model.select(selections, selection_flags)