Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/snuffler/snuffler_app.py: 53%

506 statements  

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

1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6''' 

7Effective seismological trace viewer. 

8''' 

9 

10import sys 

11import logging 

12import time 

13import re 

14import zlib 

15import struct 

16import pickle 

17try: 

18 from urlparse import parse_qsl 

19except ImportError: 

20 from urllib.parse import parse_qsl 

21 

22from pyrocko.streaming import serial_hamster 

23from pyrocko.streaming import slink 

24from pyrocko.streaming import edl 

25from pyrocko.streaming import datacube 

26 

27from pyrocko import pile # noqa 

28from pyrocko import util # noqa 

29from pyrocko import model # noqa 

30from pyrocko import config # noqa 

31from pyrocko import io # noqa 

32 

33from . import pile_viewer # noqa 

34 

35from ..qt_compat import qc, qg, qw 

36 

37logger = logging.getLogger('pyrocko.gui.snuffler.snuffler_app') 

38 

39 

40class AcquisitionThread(qc.QThread): 

41 def __init__(self, post_process_sleep=0.0): 

42 qc.QThread.__init__(self) 

43 self.mutex = qc.QMutex() 

44 self.queue = [] 

45 self.post_process_sleep = post_process_sleep 

46 self._sun_is_shining = True 

47 

48 def get_wanted_poll_interval(self): 

49 return 1000. 

50 

51 def run(self): 

52 while True: 

53 try: 

54 self.acquisition_start() 

55 while self._sun_is_shining: 

56 t0 = time.time() 

57 self.process() 

58 t1 = time.time() 

59 if self.post_process_sleep != 0.0: 

60 time.sleep(max(0, self.post_process_sleep-(t1-t0))) 

61 

62 self.acquisition_stop() 

63 break 

64 

65 except ( 

66 edl.ReadError, 

67 serial_hamster.SerialHamsterError, 

68 slink.SlowSlinkError) as e: 

69 

70 logger.error(str(e)) 

71 logger.error('Acquistion terminated, restart in 5 s') 

72 self.acquisition_stop() 

73 time.sleep(5) 

74 if not self._sun_is_shining: 

75 break 

76 

77 def stop(self): 

78 self._sun_is_shining = False 

79 

80 logger.debug('Waiting for thread to terminate...') 

81 self.wait() 

82 logger.debug('Thread has terminated.') 

83 

84 def got_trace(self, tr): 

85 self.mutex.lock() 

86 self.queue.append(tr) 

87 self.mutex.unlock() 

88 

89 def poll(self): 

90 self.mutex.lock() 

91 items = self.queue[:] 

92 self.queue[:] = [] 

93 self.mutex.unlock() 

94 return items 

95 

96 

97class SlinkAcquisition( 

98 slink.SlowSlink, AcquisitionThread): 

99 

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

101 slink.SlowSlink.__init__(self, *args, **kwargs) 

102 AcquisitionThread.__init__(self) 

103 

104 def got_trace(self, tr): 

105 AcquisitionThread.got_trace(self, tr) 

106 

107 

108class CamAcquisition( 

109 serial_hamster.CamSerialHamster, AcquisitionThread): 

110 

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

112 serial_hamster.CamSerialHamster.__init__(self, *args, **kwargs) 

113 AcquisitionThread.__init__(self, post_process_sleep=0.1) 

114 

115 def got_trace(self, tr): 

116 AcquisitionThread.got_trace(self, tr) 

117 

118 

119class USBHB628Acquisition( 

120 serial_hamster.USBHB628Hamster, AcquisitionThread): 

121 

122 def __init__(self, deltat=0.02, *args, **kwargs): 

123 serial_hamster.USBHB628Hamster.__init__( 

124 self, deltat=deltat, *args, **kwargs) 

125 AcquisitionThread.__init__(self) 

126 

127 def got_trace(self, tr): 

128 AcquisitionThread.got_trace(self, tr) 

129 

130 

131class SchoolSeismometerAcquisition( 

132 serial_hamster.SerialHamster, AcquisitionThread): 

133 

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

135 serial_hamster.SerialHamster.__init__(self, *args, **kwargs) 

136 AcquisitionThread.__init__(self, post_process_sleep=0.0) 

137 

138 def got_trace(self, tr): 

139 AcquisitionThread.got_trace(self, tr) 

140 

141 def get_wanted_poll_interval(self): 

142 return 100. 

143 

144 

145class EDLAcquisition( 

146 edl.EDLHamster, AcquisitionThread): 

147 

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

149 edl.EDLHamster.__init__(self, *args, **kwargs) 

150 AcquisitionThread.__init__(self) 

151 

152 def got_trace(self, tr): 

153 AcquisitionThread.got_trace(self, tr) 

154 

155 

156class CubeAcquisition( 

157 datacube.SerialCube, AcquisitionThread): 

158 

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

160 datacube.SerialCube.__init__(self, *args, **kwargs) 

161 AcquisitionThread.__init__(self) 

162 

163 def got_trace(self, tr): 

164 AcquisitionThread.got_trace(self, tr) 

165 

166 

167def setup_acquisition_sources(args): 

168 

169 sources = [] 

170 iarg = 0 

171 while iarg < len(args): 

172 arg = args[iarg] 

173 

174 msl = re.match(r'seedlink://([a-zA-Z0-9.-]+)(:(\d+))?(/(.*))?', arg) 

175 mca = re.match(r'cam://([^:]+)', arg) 

176 mus = re.match(r'hb628://([^:?]+)(\?([^?]+))?', arg) 

177 msc = re.match(r'school://([^:?]+)(\?([^?]+))?', arg) 

178 med = re.match(r'edl://([^:]+)', arg) 

179 mcu = re.match(r'cube://([^:]+)', arg) 

180 

181 if msl: 

182 host = msl.group(1) 

183 port = msl.group(3) 

184 if not port: 

185 port = '18000' 

186 

187 sl = SlinkAcquisition(host=host, port=port) 

188 if msl.group(5): 

189 stream_patterns = msl.group(5).split(',') 

190 

191 if '_' not in msl.group(5): 

192 try: 

193 streams = sl.query_streams() 

194 except slink.SlowSlinkError as e: 

195 logger.fatal(str(e)) 

196 sys.exit(1) 

197 

198 streams = list(set( 

199 util.match_nslcs(stream_patterns, streams))) 

200 

201 for stream in streams: 

202 sl.add_stream(*stream) 

203 else: 

204 for stream in stream_patterns: 

205 sl.add_raw_stream_selector(stream) 

206 

207 sources.append(sl) 

208 

209 elif mca: 

210 port = mca.group(1) 

211 cam = CamAcquisition(port=port, deltat=0.0314504) 

212 sources.append(cam) 

213 

214 elif mus: 

215 port = mus.group(1) 

216 try: 

217 d = {} 

218 if mus.group(3): 

219 d = dict(parse_qsl(mus.group(3))) 

220 

221 deltat = 1.0/float(d.get('rate', '50')) 

222 channels = [(int(c), c) for c in d.get('channels', '01234567')] 

223 hb628 = USBHB628Acquisition( 

224 port=port, 

225 deltat=deltat, 

226 channels=channels, 

227 buffersize=16, 

228 lookback=50) 

229 

230 sources.append(hb628) 

231 except Exception: 

232 sys.exit('invalid acquisition source: %s' % arg) 

233 

234 elif msc: 

235 port = msc.group(1) 

236 

237 d = {} 

238 if msc.group(3): 

239 d = dict(parse_qsl(msc.group(3))) 

240 

241 d_rate = { 

242 '20': ('a', 20.032), 

243 '40': ('b', 39.860), 

244 '80': ('c', 79.719)} 

245 

246 s_rate = d.get('rate', '80') 

247 station = d.get('station', 'TEST') 

248 

249 if s_rate not in d_rate: 

250 raise Exception( 

251 'Unsupported rate: %s (expected "20", "40" or "80")' 

252 % s_rate) 

253 

254 s_gain = d.get('gain', '4') 

255 

256 if s_gain not in ('1', '2', '4'): 

257 raise Exception( 

258 'Unsupported gain: %s (expected "1", "2" or "4")' 

259 % s_gain) 

260 

261 start_string = s_gain + d_rate[s_rate][0] 

262 deltat = 1.0 / d_rate[s_rate][1] 

263 

264 logger.info( 

265 'School seismometer: trying to use device %s with gain=%s and ' 

266 'rate=%g.' % (port, s_gain, 1.0/deltat)) 

267 

268 sco = SchoolSeismometerAcquisition( 

269 port=port, 

270 deltat=deltat, 

271 start_string=start_string, 

272 min_detection_size=50, 

273 disallow_uneven_sampling_rates=False, 

274 station=station) 

275 

276 sources.append(sco) 

277 

278 elif med: 

279 port = med.group(1) 

280 edl = EDLAcquisition(port=port) 

281 sources.append(edl) 

282 elif mcu: 

283 device = mcu.group(1) 

284 cube = CubeAcquisition(device=device) 

285 sources.append(cube) 

286 

287 if msl or mca or mus or msc or med or mcu: 

288 args.pop(iarg) 

289 else: 

290 iarg += 1 

291 

292 return sources 

293 

294 

295class PollInjector(qc.QObject): 

296 

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

298 interval = kwargs.pop('interval', 1000.) 

299 qc.QObject.__init__(self) 

300 self._injector = pile.Injector(*args, **kwargs) 

301 self._sources = [] 

302 self.startTimer(int(interval)) 

303 

304 def add_source(self, source): 

305 self._sources.append(source) 

306 

307 def remove_source(self, source): 

308 self._sources.remove(source) 

309 

310 def timerEvent(self, ev): 

311 for source in self._sources: 

312 trs = source.poll() 

313 for tr in trs: 

314 self._injector.inject(tr) 

315 

316 # following methods needed because mulitple inheritance does not seem 

317 # to work anymore with QObject in Python3 or PyQt5 

318 

319 def set_fixation_length(self, length): 

320 return self._injector.set_fixation_length(length) 

321 

322 def set_save_path( 

323 self, 

324 path='dump_%(network)s.%(station)s.%(location)s.%(channel)s_' 

325 '%(tmin)s_%(tmax)s.mseed'): 

326 

327 return self._injector.set_save_path(path) 

328 

329 def fixate_all(self): 

330 return self._injector.fixate_all() 

331 

332 def free(self): 

333 return self._injector.free() 

334 

335 

336class Connection(qc.QObject): 

337 

338 received = qc.pyqtSignal(object, object) 

339 disconnected = qc.pyqtSignal(object) 

340 

341 def __init__(self, parent, sock): 

342 qc.QObject.__init__(self, parent) 

343 self.socket = sock 

344 self.readyRead.connect( 

345 self.handle_read) 

346 self.disconnected.connect( 

347 self.handle_disconnected) 

348 self.nwanted = 8 

349 self.reading_size = True 

350 self.handler = None 

351 self.nbytes_received = 0 

352 self.nbytes_sent = 0 

353 self.compressor = zlib.compressobj() 

354 self.decompressor = zlib.decompressobj() 

355 

356 def handle_read(self): 

357 while True: 

358 navail = self.socket.bytesAvailable() 

359 if navail < self.nwanted: 

360 return 

361 

362 data = self.socket.read(self.nwanted) 

363 self.nbytes_received += len(data) 

364 if self.reading_size: 

365 self.nwanted = struct.unpack('>Q', data)[0] 

366 self.reading_size = False 

367 else: 

368 obj = pickle.loads(self.decompressor.decompress(data)) 

369 if obj is None: 

370 self.socket.disconnectFromHost() 

371 else: 

372 self.handle_received(obj) 

373 self.nwanted = 8 

374 self.reading_size = True 

375 

376 def handle_received(self, obj): 

377 self.received.emit(self, obj) 

378 

379 def ship(self, obj): 

380 data = self.compressor.compress(pickle.dumps(obj)) 

381 data_end = self.compressor.flush(zlib.Z_FULL_FLUSH) 

382 self.socket.write(struct.pack('>Q', len(data)+len(data_end))) 

383 self.socket.write(data) 

384 self.socket.write(data_end) 

385 self.nbytes_sent += len(data)+len(data_end) + 8 

386 

387 def handle_disconnected(self): 

388 self.disconnected.emit(self) 

389 

390 def close(self): 

391 self.socket.close() 

392 

393 

394class ConnectionHandler(qc.QObject): 

395 def __init__(self, parent): 

396 qc.QObject.__init__(self, parent) 

397 self.queue = [] 

398 self.connection = None 

399 

400 def connected(self): 

401 return self.connection is None 

402 

403 def set_connection(self, connection): 

404 self.connection = connection 

405 connection.received.connect( 

406 self._handle_received) 

407 

408 connection.connect( 

409 self.handle_disconnected) 

410 

411 for obj in self.queue: 

412 self.connection.ship(obj) 

413 

414 self.queue = [] 

415 

416 def _handle_received(self, conn, obj): 

417 self.handle_received(obj) 

418 

419 def handle_received(self, obj): 

420 pass 

421 

422 def handle_disconnected(self): 

423 self.connection = None 

424 

425 def ship(self, obj): 

426 if self.connection: 

427 self.connection.ship(obj) 

428 else: 

429 self.queue.append(obj) 

430 

431 

432class SimpleConnectionHandler(ConnectionHandler): 

433 def __init__(self, parent, **mapping): 

434 ConnectionHandler.__init__(self, parent) 

435 self.mapping = mapping 

436 

437 def handle_received(self, obj): 

438 command = obj[0] 

439 args = obj[1:] 

440 self.mapping[command](*args) 

441 

442 

443class MyMainWindow(qw.QMainWindow): 

444 

445 def __init__(self, app, *args): 

446 qg.QMainWindow.__init__(self, *args) 

447 self.app = app 

448 

449 def keyPressEvent(self, ev): 

450 self.app.pile_viewer.get_view().keyPressEvent(ev) 

451 

452 

453class SnufflerTabs(qw.QTabWidget): 

454 def __init__(self, parent): 

455 qw.QTabWidget.__init__(self, parent) 

456 if hasattr(self, 'setTabsClosable'): 

457 self.setTabsClosable(True) 

458 

459 self.tabCloseRequested.connect( 

460 self.removeTab) 

461 

462 if hasattr(self, 'setDocumentMode'): 

463 self.setDocumentMode(True) 

464 

465 def hide_close_button_on_first_tab(self): 

466 tbar = self.tabBar() 

467 if hasattr(tbar, 'setTabButton'): 

468 tbar.setTabButton(0, qw.QTabBar.LeftSide, None) 

469 tbar.setTabButton(0, qw.QTabBar.RightSide, None) 

470 

471 def append_tab(self, widget, name): 

472 widget.setParent(self) 

473 self.insertTab(self.count(), widget, name) 

474 self.setCurrentIndex(self.count()-1) 

475 

476 def remove_tab(self, widget): 

477 self.removeTab(self.indexOf(widget)) 

478 

479 def tabInserted(self, index): 

480 if index == 0: 

481 self.hide_close_button_on_first_tab() 

482 

483 self.tabbar_visibility() 

484 self.setFocus() 

485 

486 def removeTab(self, index): 

487 w = self.widget(index) 

488 w.close() 

489 qw.QTabWidget.removeTab(self, index) 

490 

491 def tabRemoved(self, index): 

492 self.tabbar_visibility() 

493 

494 def tabbar_visibility(self): 

495 if self.count() <= 1: 

496 self.tabBar().hide() 

497 elif self.count() > 1: 

498 self.tabBar().show() 

499 

500 def keyPressEvent(self, event): 

501 if event.text() == 'd': 

502 i = self.currentIndex() 

503 if i != 0: 

504 self.tabCloseRequested.emit(i) 

505 else: 

506 self.parent().keyPressEvent(event) 

507 

508 

509class SnufflerStartWizard(qw.QWizard): 

510 

511 def __init__(self, parent): 

512 qw.QWizard.__init__(self, parent) 

513 

514 self.setOption(self.NoBackButtonOnStartPage) 

515 self.setOption(self.NoBackButtonOnLastPage) 

516 self.setOption(self.NoCancelButton) 

517 self.addPageSurvey() 

518 self.addPageHelp() 

519 self.setWindowTitle('Welcome to Pyrocko') 

520 

521 def getSystemInfo(self): 

522 import numpy 

523 import scipy 

524 import pyrocko 

525 import platform 

526 import uuid 

527 data = { 

528 'node-uuid': uuid.getnode(), 

529 'platform.architecture': platform.architecture(), 

530 'platform.system': platform.system(), 

531 'platform.release': platform.release(), 

532 'python': platform.python_version(), 

533 'pyrocko': pyrocko.__version__, 

534 'numpy': numpy.__version__, 

535 'scipy': scipy.__version__, 

536 'qt': qc.PYQT_VERSION_STR, 

537 } 

538 return data 

539 

540 def addPageSurvey(self): 

541 import pprint 

542 webtk = 'DSFGK234ADF4ASDF' 

543 sys_info = self.getSystemInfo() 

544 

545 p = qw.QWizardPage() 

546 p.setCommitPage(True) 

547 p.setTitle('Thank you for installing Pyrocko!') 

548 

549 lyt = qw.QVBoxLayout() 

550 lyt.addWidget(qw.QLabel( 

551 '<p>Your feedback is important for' 

552 ' the development and improvement of Pyrocko.</p>' 

553 '<p>Do you want to send this system information anon' 

554 'ymously to <a href="https://pyrocko.org">' 

555 'https://pyrocko.org</a>?</p>')) 

556 

557 text_data = qw.QLabel( 

558 '<code style="font-size: small;">%s</code>' % 

559 pprint.pformat( 

560 sys_info, 

561 indent=1).replace('\n', '<br>') 

562 ) 

563 text_data.setStyleSheet('padding: 10px;') 

564 lyt.addWidget(text_data) 

565 

566 lyt.addWidget(qw.QLabel( 

567 "This message won't be shown again.\n\n" 

568 'We appreciate your contribution!\n- The Pyrocko Developers' 

569 )) 

570 

571 p.setLayout(lyt) 

572 p.setButtonText(self.CommitButton, 'No') 

573 

574 yes_btn = qw.QPushButton(p) 

575 yes_btn.setText('Yes') 

576 

577 @qc.pyqtSlot() 

578 def send_data(): 

579 import requests 

580 import json 

581 try: 

582 requests.post('https://pyrocko.org/%s' % webtk, 

583 data=json.dumps(sys_info)) 

584 except Exception as e: 

585 print(e) 

586 self.button(self.NextButton).clicked.emit(True) 

587 

588 self.customButtonClicked.connect(send_data) 

589 

590 self.setButton(self.CustomButton1, yes_btn) 

591 self.setOption(self.HaveCustomButton1, True) 

592 

593 self.addPage(p) 

594 return p 

595 

596 def addPageHelp(self): 

597 p = qw.QWizardPage() 

598 p.setTitle('Welcome to Snuffler!') 

599 

600 text = qw.QLabel('''<html> 

601<h3>- <i>The Seismogram browser and workbench.</i></h3> 

602<p>Looks like you are starting the Snuffler for the first time.<br> 

603It allows you to browse and process large archives of waveform data.</p> 

604<p>Basic processing is complemented by Snufflings (<i>Plugins</i>):</p> 

605<ul> 

606 <li><b>Download seismograms</b> from GEOFON IRIS and others</li> 

607 <li><b>Earthquake catalog</b> access to GEOFON, GlobalCMT, USGS...</li> 

608 <li><b>Cake</b>, Calculate synthetic arrival times</li> 

609 <li><b>Seismosizer</b>, generate synthetic seismograms on-the-fly</li> 

610 <li> 

611 <b>Map</b>, swiftly inspect stations and events on interactive maps 

612 </li> 

613</ul> 

614<p>And more, see <a href="https://pyrocko.org/">https://pyrocko.org/</a></p> 

615<p><b>NOTE:</b><br>If you installed snufflings from the 

616<a href="https://github.com/pyrocko/contrib-snufflings">user contributed 

617snufflings repository</a><br>you also have to pull an update from there. 

618</p> 

619<p style="width: 100%; background-color: #e9b96e; margin: 5px; padding: 50;" 

620 align="center"> 

621 <b>You can always press <code>?</code> for help!</b> 

622</p> 

623</html>''') 

624 

625 lyt = qw.QVBoxLayout() 

626 lyt.addWidget(text) 

627 

628 def remove_custom_button(): 

629 self.setOption(self.HaveCustomButton1, False) 

630 

631 p.initializePage = remove_custom_button 

632 

633 p.setLayout(lyt) 

634 self.addPage(p) 

635 return p 

636 

637 

638class SnufflerWindow(qw.QMainWindow): 

639 

640 def __init__( 

641 self, pile, stations=None, events=None, markers=None, ntracks=12, 

642 marker_editor_sortable=True, follow=None, controls=True, 

643 opengl=None, instant_close=False): 

644 

645 qw.QMainWindow.__init__(self) 

646 

647 self.instant_close = instant_close 

648 

649 self.dockwidget_to_toggler = {} 

650 self.dockwidgets = [] 

651 

652 self.setWindowTitle('Snuffler') 

653 

654 self.pile_viewer = pile_viewer.PileViewer( 

655 pile, ntracks_shown_max=ntracks, use_opengl=opengl, 

656 marker_editor_sortable=marker_editor_sortable, 

657 panel_parent=self) 

658 

659 self.marker_editor = self.pile_viewer.marker_editor() 

660 self.add_panel( 

661 'Markers', self.marker_editor, visible=False, 

662 where=qc.Qt.RightDockWidgetArea) 

663 if stations: 

664 self.get_view().add_stations(stations) 

665 

666 if events: 

667 self.get_view().add_events(events) 

668 

669 if len(events) == 1: 

670 self.get_view().set_active_event(events[0]) 

671 

672 if markers: 

673 self.get_view().add_markers(markers) 

674 self.get_view().associate_phases_to_events() 

675 

676 self.tabs = SnufflerTabs(self) 

677 self.setCentralWidget(self.tabs) 

678 self.add_tab('Main', self.pile_viewer) 

679 

680 self.pile_viewer.setup_snufflings() 

681 self.setMenuBar(self.pile_viewer.menu) 

682 

683 self.main_controls = self.pile_viewer.controls() 

684 self.add_panel('Main Controls', self.main_controls, visible=controls) 

685 self.show() 

686 

687 self.get_view().setFocus(qc.Qt.OtherFocusReason) 

688 

689 sb = self.statusBar() 

690 sb.clearMessage() 

691 sb.showMessage('Welcome to Snuffler! Press <?> for help.') 

692 

693 snuffler_config = self.pile_viewer.viewer.config 

694 

695 if snuffler_config.first_start: 

696 wizard = SnufflerStartWizard(self) 

697 

698 @qc.pyqtSlot() 

699 def wizard_finished(result): 

700 if result == wizard.Accepted: 

701 snuffler_config.first_start = False 

702 config.write_config(snuffler_config, 'snuffler') 

703 

704 wizard.finished.connect(wizard_finished) 

705 

706 wizard.show() 

707 

708 if follow: 

709 self.get_view().follow(float(follow)) 

710 

711 self.closing = False 

712 

713 def sizeHint(self): 

714 return qc.QSize(1024, 768) 

715 # return qc.QSize(800, 600) # used for screen shots in tutorial 

716 

717 def keyPressEvent(self, ev): 

718 self.get_view().keyPressEvent(ev) 

719 

720 def get_view(self): 

721 return self.pile_viewer.get_view() 

722 

723 def get_panel_parent_widget(self): 

724 return self 

725 

726 def add_tab(self, name, widget): 

727 self.tabs.append_tab(widget, name) 

728 

729 def remove_tab(self, widget): 

730 self.tabs.remove_tab(widget) 

731 

732 def add_panel(self, name, panel, visible=False, volatile=False, 

733 where=qc.Qt.BottomDockWidgetArea): 

734 

735 if not self.dockwidgets: 

736 self.dockwidgets = [] 

737 

738 dws = [x for x in self.dockwidgets if self.dockWidgetArea(x) == where] 

739 

740 dockwidget = qw.QDockWidget(name, self) 

741 self.dockwidgets.append(dockwidget) 

742 dockwidget.setWidget(panel) 

743 panel.setParent(dockwidget) 

744 self.addDockWidget(where, dockwidget) 

745 

746 if dws: 

747 self.tabifyDockWidget(dws[-1], dockwidget) 

748 

749 self.toggle_panel(dockwidget, visible) 

750 

751 mitem = qw.QAction(name, None) 

752 

753 def toggle_panel(checked): 

754 self.toggle_panel(dockwidget, True) 

755 

756 mitem.triggered.connect(toggle_panel) 

757 

758 if volatile: 

759 def visibility(visible): 

760 if not visible: 

761 self.remove_panel(panel) 

762 

763 dockwidget.visibilityChanged.connect( 

764 visibility) 

765 

766 self.get_view().add_panel_toggler(mitem) 

767 self.dockwidget_to_toggler[dockwidget] = mitem 

768 

769 if pile_viewer.is_macos: 

770 tabbars = self.findChildren(qw.QTabBar) 

771 for tabbar in tabbars: 

772 tabbar.setShape(qw.QTabBar.TriangularNorth) 

773 tabbar.setDocumentMode(True) 

774 

775 def toggle_panel(self, dockwidget, visible): 

776 if visible is None: 

777 visible = not dockwidget.isVisible() 

778 

779 dockwidget.setVisible(visible) 

780 if visible: 

781 w = dockwidget.widget() 

782 minsize = w.minimumSize() 

783 w.setMinimumHeight(w.sizeHint().height() + 5) 

784 

785 def reset_minimum_size(): 

786 import sip 

787 if not sip.isdeleted(w): 

788 w.setMinimumSize(minsize) 

789 

790 qc.QTimer.singleShot(200, reset_minimum_size) 

791 

792 dockwidget.setFocus() 

793 dockwidget.raise_() 

794 

795 def toggle_marker_editor(self): 

796 self.toggle_panel(self.marker_editor.parent(), None) 

797 

798 def toggle_main_controls(self): 

799 self.toggle_panel(self.main_controls.parent(), None) 

800 

801 def remove_panel(self, panel): 

802 dockwidget = panel.parent() 

803 self.removeDockWidget(dockwidget) 

804 dockwidget.setParent(None) 

805 mitem = self.dockwidget_to_toggler[dockwidget] 

806 self.get_view().remove_panel_toggler(mitem) 

807 

808 def return_tag(self): 

809 return self.get_view().return_tag 

810 

811 def confirm_close(self): 

812 ret = qw.QMessageBox.question( 

813 self, 

814 'Snuffler', 

815 'Close Snuffler window?', 

816 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

817 qw.QMessageBox.Ok) 

818 

819 return ret == qw.QMessageBox.Ok 

820 

821 def closeEvent(self, event): 

822 if self.instant_close or self.confirm_close(): 

823 self.closing = True 

824 self.pile_viewer.cleanup() 

825 event.accept() 

826 else: 

827 event.ignore() 

828 

829 def is_closing(self): 

830 return self.closing