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

507 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2024-02-27 10:58 +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 

32from pyrocko.gui import util as gui_util 

33 

34from . import pile_viewer # noqa 

35 

36from ..qt_compat import qc, qg, qw 

37 

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

39 

40 

41class AcquisitionThread(qc.QThread): 

42 def __init__(self, post_process_sleep=0.0): 

43 qc.QThread.__init__(self) 

44 self.mutex = qc.QMutex() 

45 self.queue = [] 

46 self.post_process_sleep = post_process_sleep 

47 self._sun_is_shining = True 

48 

49 def get_wanted_poll_interval(self): 

50 return 1000. 

51 

52 def run(self): 

53 while True: 

54 try: 

55 self.acquisition_start() 

56 while self._sun_is_shining: 

57 t0 = time.time() 

58 self.process() 

59 t1 = time.time() 

60 if self.post_process_sleep != 0.0: 

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

62 

63 self.acquisition_stop() 

64 break 

65 

66 except ( 

67 edl.ReadError, 

68 serial_hamster.SerialHamsterError, 

69 slink.SlowSlinkError) as e: 

70 

71 logger.error(str(e)) 

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

73 self.acquisition_stop() 

74 time.sleep(5) 

75 if not self._sun_is_shining: 

76 break 

77 

78 def stop(self): 

79 self._sun_is_shining = False 

80 

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

82 self.wait() 

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

84 

85 def got_trace(self, tr): 

86 self.mutex.lock() 

87 self.queue.append(tr) 

88 self.mutex.unlock() 

89 

90 def poll(self): 

91 self.mutex.lock() 

92 items = self.queue[:] 

93 self.queue[:] = [] 

94 self.mutex.unlock() 

95 return items 

96 

97 

98class SlinkAcquisition( 

99 slink.SlowSlink, AcquisitionThread): 

100 

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

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

103 AcquisitionThread.__init__(self) 

104 

105 def got_trace(self, tr): 

106 AcquisitionThread.got_trace(self, tr) 

107 

108 

109class CamAcquisition( 

110 serial_hamster.CamSerialHamster, AcquisitionThread): 

111 

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

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

114 AcquisitionThread.__init__(self, post_process_sleep=0.1) 

115 

116 def got_trace(self, tr): 

117 AcquisitionThread.got_trace(self, tr) 

118 

119 

120class USBHB628Acquisition( 

121 serial_hamster.USBHB628Hamster, AcquisitionThread): 

122 

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

124 serial_hamster.USBHB628Hamster.__init__( 

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

126 AcquisitionThread.__init__(self) 

127 

128 def got_trace(self, tr): 

129 AcquisitionThread.got_trace(self, tr) 

130 

131 

132class SchoolSeismometerAcquisition( 

133 serial_hamster.SerialHamster, AcquisitionThread): 

134 

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

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

137 AcquisitionThread.__init__(self, post_process_sleep=0.0) 

138 

139 def got_trace(self, tr): 

140 AcquisitionThread.got_trace(self, tr) 

141 

142 def get_wanted_poll_interval(self): 

143 return 100. 

144 

145 

146class EDLAcquisition( 

147 edl.EDLHamster, AcquisitionThread): 

148 

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

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

151 AcquisitionThread.__init__(self) 

152 

153 def got_trace(self, tr): 

154 AcquisitionThread.got_trace(self, tr) 

155 

156 

157class CubeAcquisition( 

158 datacube.SerialCube, AcquisitionThread): 

159 

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

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

162 AcquisitionThread.__init__(self) 

163 

164 def got_trace(self, tr): 

165 AcquisitionThread.got_trace(self, tr) 

166 

167 

168def setup_acquisition_sources(args): 

169 

170 sources = [] 

171 iarg = 0 

172 while iarg < len(args): 

173 arg = args[iarg] 

174 

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

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

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

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

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

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

181 

182 if msl: 

183 host = msl.group(1) 

184 port = msl.group(3) 

185 if not port: 

186 port = '18000' 

187 

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

189 if msl.group(5): 

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

191 

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

193 try: 

194 streams = sl.query_streams() 

195 except slink.SlowSlinkError as e: 

196 logger.fatal(str(e)) 

197 sys.exit(1) 

198 

199 streams = list(set( 

200 util.match_nslcs(stream_patterns, streams))) 

201 

202 for stream in streams: 

203 sl.add_stream(*stream) 

204 else: 

205 for stream in stream_patterns: 

206 sl.add_raw_stream_selector(stream) 

207 

208 sources.append(sl) 

209 

210 elif mca: 

211 port = mca.group(1) 

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

213 sources.append(cam) 

214 

215 elif mus: 

216 port = mus.group(1) 

217 try: 

218 d = {} 

219 if mus.group(3): 

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

221 

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

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

224 hb628 = USBHB628Acquisition( 

225 port=port, 

226 deltat=deltat, 

227 channels=channels, 

228 buffersize=16, 

229 lookback=50) 

230 

231 sources.append(hb628) 

232 except Exception: 

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

234 

235 elif msc: 

236 port = msc.group(1) 

237 

238 d = {} 

239 if msc.group(3): 

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

241 

242 d_rate = { 

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

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

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

246 

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

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

249 

250 if s_rate not in d_rate: 

251 raise Exception( 

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

253 % s_rate) 

254 

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

256 

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

258 raise Exception( 

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

260 % s_gain) 

261 

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

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

264 

265 logger.info( 

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

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

268 

269 sco = SchoolSeismometerAcquisition( 

270 port=port, 

271 deltat=deltat, 

272 start_string=start_string, 

273 min_detection_size=50, 

274 disallow_uneven_sampling_rates=False, 

275 station=station) 

276 

277 sources.append(sco) 

278 

279 elif med: 

280 port = med.group(1) 

281 edl = EDLAcquisition(port=port) 

282 sources.append(edl) 

283 elif mcu: 

284 device = mcu.group(1) 

285 cube = CubeAcquisition(device=device) 

286 sources.append(cube) 

287 

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

289 args.pop(iarg) 

290 else: 

291 iarg += 1 

292 

293 return sources 

294 

295 

296class PollInjector(qc.QObject): 

297 

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

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

300 qc.QObject.__init__(self) 

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

302 self._sources = [] 

303 self.startTimer(int(interval)) 

304 

305 def add_source(self, source): 

306 self._sources.append(source) 

307 

308 def remove_source(self, source): 

309 self._sources.remove(source) 

310 

311 def timerEvent(self, ev): 

312 for source in self._sources: 

313 trs = source.poll() 

314 for tr in trs: 

315 self._injector.inject(tr) 

316 

317 # following methods needed because mulitple inheritance does not seem 

318 # to work anymore with QObject in Python3 or PyQt5 

319 

320 def set_fixation_length(self, length): 

321 return self._injector.set_fixation_length(length) 

322 

323 def set_save_path( 

324 self, 

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

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

327 

328 return self._injector.set_save_path(path) 

329 

330 def fixate_all(self): 

331 return self._injector.fixate_all() 

332 

333 def free(self): 

334 return self._injector.free() 

335 

336 

337class Connection(qc.QObject): 

338 

339 received = qc.pyqtSignal(object, object) 

340 disconnected = qc.pyqtSignal(object) 

341 

342 def __init__(self, parent, sock): 

343 qc.QObject.__init__(self, parent) 

344 self.socket = sock 

345 self.readyRead.connect( 

346 self.handle_read) 

347 self.disconnected.connect( 

348 self.handle_disconnected) 

349 self.nwanted = 8 

350 self.reading_size = True 

351 self.handler = None 

352 self.nbytes_received = 0 

353 self.nbytes_sent = 0 

354 self.compressor = zlib.compressobj() 

355 self.decompressor = zlib.decompressobj() 

356 

357 def handle_read(self): 

358 while True: 

359 navail = self.socket.bytesAvailable() 

360 if navail < self.nwanted: 

361 return 

362 

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

364 self.nbytes_received += len(data) 

365 if self.reading_size: 

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

367 self.reading_size = False 

368 else: 

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

370 if obj is None: 

371 self.socket.disconnectFromHost() 

372 else: 

373 self.handle_received(obj) 

374 self.nwanted = 8 

375 self.reading_size = True 

376 

377 def handle_received(self, obj): 

378 self.received.emit(self, obj) 

379 

380 def ship(self, obj): 

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

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

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

384 self.socket.write(data) 

385 self.socket.write(data_end) 

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

387 

388 def handle_disconnected(self): 

389 self.disconnected.emit(self) 

390 

391 def close(self): 

392 self.socket.close() 

393 

394 

395class ConnectionHandler(qc.QObject): 

396 def __init__(self, parent): 

397 qc.QObject.__init__(self, parent) 

398 self.queue = [] 

399 self.connection = None 

400 

401 def connected(self): 

402 return self.connection is None 

403 

404 def set_connection(self, connection): 

405 self.connection = connection 

406 connection.received.connect( 

407 self._handle_received) 

408 

409 connection.connect( 

410 self.handle_disconnected) 

411 

412 for obj in self.queue: 

413 self.connection.ship(obj) 

414 

415 self.queue = [] 

416 

417 def _handle_received(self, conn, obj): 

418 self.handle_received(obj) 

419 

420 def handle_received(self, obj): 

421 pass 

422 

423 def handle_disconnected(self): 

424 self.connection = None 

425 

426 def ship(self, obj): 

427 if self.connection: 

428 self.connection.ship(obj) 

429 else: 

430 self.queue.append(obj) 

431 

432 

433class SimpleConnectionHandler(ConnectionHandler): 

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

435 ConnectionHandler.__init__(self, parent) 

436 self.mapping = mapping 

437 

438 def handle_received(self, obj): 

439 command = obj[0] 

440 args = obj[1:] 

441 self.mapping[command](*args) 

442 

443 

444class MyMainWindow(qw.QMainWindow): 

445 

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

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

448 self.app = app 

449 

450 def keyPressEvent(self, ev): 

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

452 

453 

454class SnufflerTabs(qw.QTabWidget): 

455 def __init__(self, parent): 

456 qw.QTabWidget.__init__(self, parent) 

457 if hasattr(self, 'setTabsClosable'): 

458 self.setTabsClosable(True) 

459 

460 self.tabCloseRequested.connect( 

461 self.removeTab) 

462 

463 if hasattr(self, 'setDocumentMode'): 

464 self.setDocumentMode(True) 

465 

466 def hide_close_button_on_first_tab(self): 

467 tbar = self.tabBar() 

468 if hasattr(tbar, 'setTabButton'): 

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

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

471 

472 def append_tab(self, widget, name): 

473 widget.setParent(self) 

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

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

476 

477 def remove_tab(self, widget): 

478 self.removeTab(self.indexOf(widget)) 

479 

480 def tabInserted(self, index): 

481 if index == 0: 

482 self.hide_close_button_on_first_tab() 

483 

484 self.tabbar_visibility() 

485 self.setFocus() 

486 

487 def removeTab(self, index): 

488 w = self.widget(index) 

489 w.close() 

490 qw.QTabWidget.removeTab(self, index) 

491 

492 def tabRemoved(self, index): 

493 self.tabbar_visibility() 

494 

495 def tabbar_visibility(self): 

496 if self.count() <= 1: 

497 self.tabBar().hide() 

498 elif self.count() > 1: 

499 self.tabBar().show() 

500 

501 def keyPressEvent(self, event): 

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

503 i = self.currentIndex() 

504 if i != 0: 

505 self.tabCloseRequested.emit(i) 

506 else: 

507 self.parent().keyPressEvent(event) 

508 

509 

510class SnufflerStartWizard(qw.QWizard): 

511 

512 def __init__(self, parent): 

513 qw.QWizard.__init__(self, parent) 

514 

515 self.setOption(self.NoBackButtonOnStartPage) 

516 self.setOption(self.NoBackButtonOnLastPage) 

517 self.setOption(self.NoCancelButton) 

518 self.addPageSurvey() 

519 self.addPageHelp() 

520 self.setWindowTitle('Welcome to Pyrocko') 

521 

522 def getSystemInfo(self): 

523 import numpy 

524 import scipy 

525 import pyrocko 

526 import platform 

527 import uuid 

528 data = { 

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

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

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

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

533 'python': platform.python_version(), 

534 'pyrocko': pyrocko.__version__, 

535 'numpy': numpy.__version__, 

536 'scipy': scipy.__version__, 

537 'qt': qc.PYQT_VERSION_STR, 

538 } 

539 return data 

540 

541 def addPageSurvey(self): 

542 import pprint 

543 webtk = 'DSFGK234ADF4ASDF' 

544 sys_info = self.getSystemInfo() 

545 

546 p = qw.QWizardPage() 

547 p.setCommitPage(True) 

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

549 

550 lyt = qw.QVBoxLayout() 

551 lyt.addWidget(qw.QLabel( 

552 '<p>Your feedback is important for' 

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

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

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

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

557 

558 text_data = qw.QLabel( 

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

560 pprint.pformat( 

561 sys_info, 

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

563 ) 

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

565 lyt.addWidget(text_data) 

566 

567 lyt.addWidget(qw.QLabel( 

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

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

570 )) 

571 

572 p.setLayout(lyt) 

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

574 

575 yes_btn = qw.QPushButton(p) 

576 yes_btn.setText('Yes') 

577 

578 @qc.pyqtSlot() 

579 def send_data(): 

580 import requests 

581 import json 

582 try: 

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

584 data=json.dumps(sys_info)) 

585 except Exception as e: 

586 print(e) 

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

588 

589 self.customButtonClicked.connect(send_data) 

590 

591 self.setButton(self.CustomButton1, yes_btn) 

592 self.setOption(self.HaveCustomButton1, True) 

593 

594 self.addPage(p) 

595 return p 

596 

597 def addPageHelp(self): 

598 p = qw.QWizardPage() 

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

600 

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

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

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

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

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

606<ul> 

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

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

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

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

611 <li> 

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

613 </li> 

614</ul> 

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

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

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

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

619</p> 

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

621 align="center"> 

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

623</p> 

624</html>''') 

625 

626 lyt = qw.QVBoxLayout() 

627 lyt.addWidget(text) 

628 

629 def remove_custom_button(): 

630 self.setOption(self.HaveCustomButton1, False) 

631 

632 p.initializePage = remove_custom_button 

633 

634 p.setLayout(lyt) 

635 self.addPage(p) 

636 return p 

637 

638 

639class SnufflerWindow(qw.QMainWindow): 

640 

641 def __init__( 

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

643 marker_editor_sortable=True, follow=None, controls=True, 

644 opengl=None, instant_close=False): 

645 

646 qw.QMainWindow.__init__(self) 

647 

648 self.instant_close = instant_close 

649 

650 self.dockwidget_to_toggler = {} 

651 self.dockwidgets = [] 

652 

653 self.setWindowTitle('Snuffler') 

654 

655 self.pile_viewer = pile_viewer.PileViewer( 

656 pile, ntracks_shown_max=ntracks, use_opengl=opengl, 

657 marker_editor_sortable=marker_editor_sortable, 

658 panel_parent=self) 

659 

660 self.marker_editor = self.pile_viewer.marker_editor() 

661 self.add_panel( 

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

663 where=qc.Qt.RightDockWidgetArea) 

664 if stations: 

665 self.get_view().add_stations(stations) 

666 

667 if events: 

668 self.get_view().add_events(events) 

669 

670 if len(events) == 1: 

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

672 

673 if markers: 

674 self.get_view().add_markers(markers) 

675 self.get_view().associate_phases_to_events() 

676 

677 self.tabs = SnufflerTabs(self) 

678 self.setCentralWidget(self.tabs) 

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

680 

681 self.pile_viewer.setup_snufflings() 

682 self.setMenuBar(self.pile_viewer.menu) 

683 

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

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

686 self.show() 

687 

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

689 

690 self.status_messages = gui_util.StatusMessages() 

691 self.statusBar().addPermanentWidget(self.status_messages) 

692 

693 self.status_messages.set( 

694 'welcome', 'Welcome to Snuffler! Press ? for help.') 

695 

696 snuffler_config = self.pile_viewer.viewer.config 

697 

698 if snuffler_config.first_start: 

699 wizard = SnufflerStartWizard(self) 

700 

701 @qc.pyqtSlot() 

702 def wizard_finished(result): 

703 if result == wizard.Accepted: 

704 snuffler_config.first_start = False 

705 config.write_config(snuffler_config, 'snuffler') 

706 

707 wizard.finished.connect(wizard_finished) 

708 

709 wizard.show() 

710 

711 if follow: 

712 self.get_view().follow(float(follow)) 

713 

714 self.closing = False 

715 

716 def sizeHint(self): 

717 return qc.QSize(1024, 768) 

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

719 

720 def keyPressEvent(self, ev): 

721 self.get_view().keyPressEvent(ev) 

722 

723 def get_view(self): 

724 return self.pile_viewer.get_view() 

725 

726 def get_panel_parent_widget(self): 

727 return self 

728 

729 def add_tab(self, name, widget): 

730 self.tabs.append_tab(widget, name) 

731 

732 def remove_tab(self, widget): 

733 self.tabs.remove_tab(widget) 

734 

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

736 where=qc.Qt.BottomDockWidgetArea): 

737 

738 if not self.dockwidgets: 

739 self.dockwidgets = [] 

740 

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

742 

743 dockwidget = qw.QDockWidget(name, self) 

744 self.dockwidgets.append(dockwidget) 

745 dockwidget.setWidget(panel) 

746 panel.setParent(dockwidget) 

747 self.addDockWidget(where, dockwidget) 

748 

749 if dws: 

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

751 

752 self.toggle_panel(dockwidget, visible) 

753 

754 mitem = qw.QAction(name, None) 

755 

756 def toggle_panel(checked): 

757 self.toggle_panel(dockwidget, True) 

758 

759 mitem.triggered.connect(toggle_panel) 

760 

761 if volatile: 

762 def visibility(visible): 

763 if not visible: 

764 self.remove_panel(panel) 

765 

766 dockwidget.visibilityChanged.connect( 

767 visibility) 

768 

769 self.get_view().add_panel_toggler(mitem) 

770 self.dockwidget_to_toggler[dockwidget] = mitem 

771 

772 if pile_viewer.is_macos: 

773 tabbars = self.findChildren(qw.QTabBar) 

774 for tabbar in tabbars: 

775 tabbar.setShape(qw.QTabBar.TriangularNorth) 

776 tabbar.setDocumentMode(True) 

777 

778 def toggle_panel(self, dockwidget, visible): 

779 if visible is None: 

780 visible = not dockwidget.isVisible() 

781 

782 dockwidget.setVisible(visible) 

783 if visible: 

784 w = dockwidget.widget() 

785 minsize = w.minimumSize() 

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

787 

788 def reset_minimum_size(): 

789 import sip 

790 if not sip.isdeleted(w): 

791 w.setMinimumSize(minsize) 

792 

793 qc.QTimer.singleShot(200, reset_minimum_size) 

794 

795 dockwidget.setFocus() 

796 dockwidget.raise_() 

797 

798 def toggle_marker_editor(self): 

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

800 

801 def toggle_main_controls(self): 

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

803 

804 def remove_panel(self, panel): 

805 dockwidget = panel.parent() 

806 self.removeDockWidget(dockwidget) 

807 dockwidget.setParent(None) 

808 mitem = self.dockwidget_to_toggler[dockwidget] 

809 self.get_view().remove_panel_toggler(mitem) 

810 

811 def return_tag(self): 

812 return self.get_view().return_tag 

813 

814 def confirm_close(self): 

815 ret = qw.QMessageBox.question( 

816 self, 

817 'Snuffler', 

818 'Close Snuffler window?', 

819 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

820 qw.QMessageBox.Ok) 

821 

822 return ret == qw.QMessageBox.Ok 

823 

824 def closeEvent(self, event): 

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

826 self.closing = True 

827 self.pile_viewer.cleanup() 

828 event.accept() 

829 else: 

830 event.ignore() 

831 

832 def is_closing(self): 

833 return self.closing