1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5''' 

6Effective seismological trace viewer. 

7''' 

8from __future__ import absolute_import 

9 

10import sys 

11import signal 

12import logging 

13import time 

14import re 

15import zlib 

16import struct 

17import pickle 

18 

19 

20from pyrocko.streaming import serial_hamster 

21from pyrocko.streaming import slink 

22from pyrocko.streaming import edl 

23from pyrocko.streaming import datacube 

24 

25from pyrocko import pile # noqa 

26from pyrocko import util # noqa 

27from pyrocko import model # noqa 

28from pyrocko import config # noqa 

29from pyrocko import io # noqa 

30 

31from . import pile_viewer # noqa 

32 

33from .qt_compat import qc, qg, qw 

34 

35logger = logging.getLogger('pyrocko.gui.snuffler_app') 

36 

37 

38class _Getch: 

39 ''' 

40 Gets a single character from standard input. 

41 

42 Does not echo to the screen. 

43 

44 https://stackoverflow.com/questions/510357/how-to-read-a-single-character-from-the-user 

45 ''' 

46 def __init__(self): 

47 try: 

48 self.impl = _GetchWindows() 

49 except ImportError: 

50 self.impl = _GetchUnix() 

51 

52 def __call__(self): return self.impl() 

53 

54 

55class _GetchUnix: 

56 def __init__(self): 

57 import tty, sys # noqa 

58 

59 def __call__(self): 

60 import sys 

61 import tty 

62 import termios 

63 

64 fd = sys.stdin.fileno() 

65 old_settings = termios.tcgetattr(fd) 

66 try: 

67 tty.setraw(fd) 

68 ch = sys.stdin.read(1) 

69 finally: 

70 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 

71 

72 return ch 

73 

74 

75class _GetchWindows: 

76 def __init__(self): 

77 import msvcrt # noqa 

78 

79 def __call__(self): 

80 import msvcrt 

81 return msvcrt.getch() 

82 

83 

84getch = _Getch() 

85 

86 

87class AcquisitionThread(qc.QThread): 

88 def __init__(self, post_process_sleep=0.0): 

89 qc.QThread.__init__(self) 

90 self.mutex = qc.QMutex() 

91 self.queue = [] 

92 self.post_process_sleep = post_process_sleep 

93 self._sun_is_shining = True 

94 

95 def run(self): 

96 while True: 

97 try: 

98 self.acquisition_start() 

99 while self._sun_is_shining: 

100 t0 = time.time() 

101 self.process() 

102 t1 = time.time() 

103 if self.post_process_sleep != 0.0: 

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

105 

106 self.acquisition_stop() 

107 break 

108 

109 except ( 

110 edl.ReadError, 

111 serial_hamster.SerialHamsterError, 

112 slink.SlowSlinkError) as e: 

113 

114 logger.error(str(e)) 

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

116 self.acquisition_stop() 

117 time.sleep(5) 

118 if not self._sun_is_shining: 

119 break 

120 

121 def stop(self): 

122 self._sun_is_shining = False 

123 

124 logger.debug("Waiting for thread to terminate...") 

125 self.wait() 

126 logger.debug("Thread has terminated.") 

127 

128 def got_trace(self, tr): 

129 self.mutex.lock() 

130 self.queue.append(tr) 

131 self.mutex.unlock() 

132 

133 def poll(self): 

134 self.mutex.lock() 

135 items = self.queue[:] 

136 self.queue[:] = [] 

137 self.mutex.unlock() 

138 return items 

139 

140 

141class SlinkAcquisition( 

142 slink.SlowSlink, AcquisitionThread): 

143 

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

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

146 AcquisitionThread.__init__(self) 

147 

148 def got_trace(self, tr): 

149 AcquisitionThread.got_trace(self, tr) 

150 

151 

152class CamAcquisition( 

153 serial_hamster.CamSerialHamster, AcquisitionThread): 

154 

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

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

157 AcquisitionThread.__init__(self, post_process_sleep=0.1) 

158 

159 def got_trace(self, tr): 

160 AcquisitionThread.got_trace(self, tr) 

161 

162 

163class USBHB628Acquisition( 

164 serial_hamster.USBHB628Hamster, AcquisitionThread): 

165 

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

167 serial_hamster.USBHB628Hamster.__init__( 

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

169 AcquisitionThread.__init__(self) 

170 

171 def got_trace(self, tr): 

172 AcquisitionThread.got_trace(self, tr) 

173 

174 

175class SchoolSeismometerAcquisition( 

176 serial_hamster.SerialHamster, AcquisitionThread): 

177 

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

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

180 AcquisitionThread.__init__(self, post_process_sleep=0.01) 

181 

182 def got_trace(self, tr): 

183 AcquisitionThread.got_trace(self, tr) 

184 

185 

186class EDLAcquisition( 

187 edl.EDLHamster, AcquisitionThread): 

188 

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

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

191 AcquisitionThread.__init__(self) 

192 

193 def got_trace(self, tr): 

194 AcquisitionThread.got_trace(self, tr) 

195 

196 

197class CubeAcquisition( 

198 datacube.SerialCube, AcquisitionThread): 

199 

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

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

202 AcquisitionThread.__init__(self) 

203 

204 def got_trace(self, tr): 

205 AcquisitionThread.got_trace(self, tr) 

206 

207 

208def setup_acquisition_sources(args): 

209 

210 sources = [] 

211 iarg = 0 

212 while iarg < len(args): 

213 arg = args[iarg] 

214 

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

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

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

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

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

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

221 

222 if msl: 

223 host = msl.group(1) 

224 port = msl.group(3) 

225 if not port: 

226 port = '18000' 

227 

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

229 if msl.group(5): 

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

231 

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

233 try: 

234 streams = sl.query_streams() 

235 except slink.SlowSlinkError as e: 

236 logger.fatal(str(e)) 

237 sys.exit(1) 

238 

239 streams = list(set( 

240 util.match_nslcs(stream_patterns, streams))) 

241 

242 for stream in streams: 

243 sl.add_stream(*stream) 

244 else: 

245 for stream in stream_patterns: 

246 sl.add_raw_stream_selector(stream) 

247 

248 sources.append(sl) 

249 elif mca: 

250 port = mca.group(1) 

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

252 sources.append(cam) 

253 elif mus: 

254 port = mus.group(1) 

255 try: 

256 d = {} 

257 if mus.group(3): 

258 d = dict(urlparse.parse_qsl(mus.group(3))) # noqa 

259 

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

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

262 hb628 = USBHB628Acquisition( 

263 port=port, 

264 deltat=deltat, 

265 channels=channels, 

266 buffersize=16, 

267 lookback=50) 

268 

269 sources.append(hb628) 

270 except Exception: 

271 raise 

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

273 

274 elif msc: 

275 port = msc.group(1) 

276 sco = SchoolSeismometerAcquisition(port=port) 

277 sources.append(sco) 

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 qc.QObject.__init__(self) 

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

300 self._sources = [] 

301 self.startTimer(1000) 

302 

303 def add_source(self, source): 

304 self._sources.append(source) 

305 

306 def remove_source(self, source): 

307 self._sources.remove(source) 

308 

309 def timerEvent(self, ev): 

310 for source in self._sources: 

311 trs = source.poll() 

312 for tr in trs: 

313 self._injector.inject(tr) 

314 

315 # following methods needed because mulitple inheritance does not seem 

316 # to work anymore with QObject in Python3 or PyQt5 

317 

318 def set_fixation_length(self, length): 

319 return self._injector.set_fixation_length(length) 

320 

321 def set_save_path( 

322 self, 

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

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

325 

326 return self._injector.set_save_path(path) 

327 

328 def fixate_all(self): 

329 return self._injector.fixate_all() 

330 

331 def free(self): 

332 return self._injector.free() 

333 

334 

335class Connection(qc.QObject): 

336 

337 received = qc.pyqtSignal(object, object) 

338 disconnected = qc.pyqtSignal(object) 

339 

340 def __init__(self, parent, sock): 

341 qc.QObject.__init__(self, parent) 

342 self.socket = sock 

343 self.readyRead.connect( 

344 self.handle_read) 

345 self.disconnected.connect( 

346 self.handle_disconnected) 

347 self.nwanted = 8 

348 self.reading_size = True 

349 self.handler = None 

350 self.nbytes_received = 0 

351 self.nbytes_sent = 0 

352 self.compressor = zlib.compressobj() 

353 self.decompressor = zlib.decompressobj() 

354 

355 def handle_read(self): 

356 while True: 

357 navail = self.socket.bytesAvailable() 

358 if navail < self.nwanted: 

359 return 

360 

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

362 self.nbytes_received += len(data) 

363 if self.reading_size: 

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

365 self.reading_size = False 

366 else: 

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

368 if obj is None: 

369 self.socket.disconnectFromHost() 

370 else: 

371 self.handle_received(obj) 

372 self.nwanted = 8 

373 self.reading_size = True 

374 

375 def handle_received(self, obj): 

376 self.received.emit(self, obj) 

377 

378 def ship(self, obj): 

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

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

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

382 self.socket.write(data) 

383 self.socket.write(data_end) 

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

385 

386 def handle_disconnected(self): 

387 self.disconnected.emit(self) 

388 

389 def close(self): 

390 self.socket.close() 

391 

392 

393class ConnectionHandler(qc.QObject): 

394 def __init__(self, parent): 

395 qc.QObject.__init__(self, parent) 

396 self.queue = [] 

397 self.connection = None 

398 

399 def connected(self): 

400 return self.connection is None 

401 

402 def set_connection(self, connection): 

403 self.connection = connection 

404 connection.received.connect( 

405 self._handle_received) 

406 

407 connection.connect( 

408 self.handle_disconnected) 

409 

410 for obj in self.queue: 

411 self.connection.ship(obj) 

412 

413 self.queue = [] 

414 

415 def _handle_received(self, conn, obj): 

416 self.handle_received(obj) 

417 

418 def handle_received(self, obj): 

419 pass 

420 

421 def handle_disconnected(self): 

422 self.connection = None 

423 

424 def ship(self, obj): 

425 if self.connection: 

426 self.connection.ship(obj) 

427 else: 

428 self.queue.append(obj) 

429 

430 

431class SimpleConnectionHandler(ConnectionHandler): 

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

433 ConnectionHandler.__init__(self, parent) 

434 self.mapping = mapping 

435 

436 def handle_received(self, obj): 

437 command = obj[0] 

438 args = obj[1:] 

439 self.mapping[command](*args) 

440 

441 

442class MyMainWindow(qw.QMainWindow): 

443 

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

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

446 self.app = app 

447 

448 def keyPressEvent(self, ev): 

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

450 

451 

452class SnufflerTabs(qw.QTabWidget): 

453 def __init__(self, parent): 

454 qw.QTabWidget.__init__(self, parent) 

455 if hasattr(self, 'setTabsClosable'): 

456 self.setTabsClosable(True) 

457 

458 self.tabCloseRequested.connect( 

459 self.removeTab) 

460 

461 if hasattr(self, 'setDocumentMode'): 

462 self.setDocumentMode(True) 

463 

464 def hide_close_button_on_first_tab(self): 

465 tbar = self.tabBar() 

466 if hasattr(tbar, 'setTabButton'): 

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

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

469 

470 def append_tab(self, widget, name): 

471 widget.setParent(self) 

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

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

474 

475 def remove_tab(self, widget): 

476 self.removeTab(self.indexOf(widget)) 

477 

478 def tabInserted(self, index): 

479 if index == 0: 

480 self.hide_close_button_on_first_tab() 

481 

482 self.tabbar_visibility() 

483 self.setFocus() 

484 

485 def removeTab(self, index): 

486 w = self.widget(index) 

487 w.close() 

488 qw.QTabWidget.removeTab(self, index) 

489 

490 def tabRemoved(self, index): 

491 self.tabbar_visibility() 

492 

493 def tabbar_visibility(self): 

494 if self.count() <= 1: 

495 self.tabBar().hide() 

496 elif self.count() > 1: 

497 self.tabBar().show() 

498 

499 def keyPressEvent(self, event): 

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

501 i = self.currentIndex() 

502 if i != 0: 

503 self.tabCloseRequested.emit(i) 

504 else: 

505 self.parent().keyPressEvent(event) 

506 

507 

508class SnufflerStartWizard(qw.QWizard): 

509 

510 def __init__(self, parent): 

511 qw.QWizard.__init__(self, parent) 

512 

513 self.setOption(self.NoBackButtonOnStartPage) 

514 self.setOption(self.NoBackButtonOnLastPage) 

515 self.setOption(self.NoCancelButton) 

516 self.addPageSurvey() 

517 self.addPageHelp() 

518 self.setWindowTitle('Welcome to Pyrocko') 

519 

520 def getSystemInfo(self): 

521 import numpy 

522 import scipy 

523 import pyrocko 

524 import platform 

525 import uuid 

526 data = { 

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

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

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

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

531 'python': platform.python_version(), 

532 'pyrocko': pyrocko.__version__, 

533 'numpy': numpy.__version__, 

534 'scipy': scipy.__version__, 

535 'qt': qc.PYQT_VERSION_STR, 

536 } 

537 return data 

538 

539 def addPageSurvey(self): 

540 import pprint 

541 webtk = 'DSFGK234ADF4ASDF' 

542 sys_info = self.getSystemInfo() 

543 

544 p = qw.QWizardPage() 

545 p.setCommitPage(True) 

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

547 

548 lyt = qw.QVBoxLayout() 

549 lyt.addWidget(qw.QLabel( 

550 '<p>Your feedback is important for' 

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

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

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

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

555 

556 text_data = qw.QLabel( 

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

558 pprint.pformat( 

559 sys_info, 

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

561 ) 

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

563 lyt.addWidget(text_data) 

564 

565 lyt.addWidget(qw.QLabel( 

566 'This message won\'t be shown again.\n\n' 

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

568 )) 

569 

570 p.setLayout(lyt) 

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

572 

573 yes_btn = qw.QPushButton(p) 

574 yes_btn.setText('Yes') 

575 

576 @qc.pyqtSlot() 

577 def send_data(): 

578 import requests 

579 import json 

580 try: 

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

582 data=json.dumps(sys_info)) 

583 except Exception as e: 

584 print(e) 

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

586 

587 self.customButtonClicked.connect(send_data) 

588 

589 self.setButton(self.CustomButton1, yes_btn) 

590 self.setOption(self.HaveCustomButton1, True) 

591 

592 self.addPage(p) 

593 return p 

594 

595 def addPageHelp(self): 

596 p = qw.QWizardPage() 

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

598 

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

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

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

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

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

604<ul> 

605 <li><b>Download seismograms</b> from Geofon, IRIS and others</li> 

606 <li><b>Earthquake catalog</b> access to Geofon, GobalCMT, USGS...</li> 

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

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

609 <li> 

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

611 </li> 

612</ul> 

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

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

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

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

617</p> 

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

619 align="center"> 

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

621</p> 

622</html>''') 

623 

624 lyt = qw.QVBoxLayout() 

625 lyt.addWidget(text) 

626 

627 def remove_custom_button(): 

628 self.setOption(self.HaveCustomButton1, False) 

629 

630 p.initializePage = remove_custom_button 

631 

632 p.setLayout(lyt) 

633 self.addPage(p) 

634 return p 

635 

636 

637class SnufflerWindow(qw.QMainWindow): 

638 

639 def __init__( 

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

641 marker_editor_sortable=True, follow=None, controls=True, 

642 opengl=None, instant_close=False): 

643 

644 qw.QMainWindow.__init__(self) 

645 

646 self.instant_close = instant_close 

647 

648 self.dockwidget_to_toggler = {} 

649 self.dockwidgets = [] 

650 

651 self.setWindowTitle("Snuffler") 

652 

653 self.pile_viewer = pile_viewer.PileViewer( 

654 pile, ntracks_shown_max=ntracks, use_opengl=opengl, 

655 marker_editor_sortable=marker_editor_sortable, 

656 panel_parent=self) 

657 

658 self.marker_editor = self.pile_viewer.marker_editor() 

659 self.add_panel( 

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

661 where=qc.Qt.RightDockWidgetArea) 

662 if stations: 

663 self.get_view().add_stations(stations) 

664 

665 if events: 

666 self.get_view().add_events(events) 

667 

668 if len(events) == 1: 

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

670 

671 if markers: 

672 self.get_view().add_markers(markers) 

673 self.get_view().associate_phases_to_events() 

674 

675 self.tabs = SnufflerTabs(self) 

676 self.setCentralWidget(self.tabs) 

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

678 

679 self.pile_viewer.setup_snufflings() 

680 self.setMenuBar(self.pile_viewer.menu) 

681 

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

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

684 self.show() 

685 

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

687 

688 sb = self.statusBar() 

689 sb.clearMessage() 

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

691 

692 snuffler_config = self.pile_viewer.viewer.config 

693 

694 if snuffler_config.first_start: 

695 wizard = SnufflerStartWizard(self) 

696 

697 @qc.pyqtSlot() 

698 def wizard_finished(result): 

699 if result == wizard.Accepted: 

700 snuffler_config.first_start = False 

701 config.write_config(snuffler_config, 'snuffler') 

702 

703 wizard.finished.connect(wizard_finished) 

704 

705 wizard.show() 

706 

707 if follow: 

708 self.get_view().follow(float(follow)) 

709 

710 self.closing = False 

711 

712 def sizeHint(self): 

713 return qc.QSize(1024, 768) 

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

715 

716 def keyPressEvent(self, ev): 

717 self.get_view().keyPressEvent(ev) 

718 

719 def get_view(self): 

720 return self.pile_viewer.get_view() 

721 

722 def get_panel_parent_widget(self): 

723 return self 

724 

725 def add_tab(self, name, widget): 

726 self.tabs.append_tab(widget, name) 

727 

728 def remove_tab(self, widget): 

729 self.tabs.remove_tab(widget) 

730 

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

732 where=qc.Qt.BottomDockWidgetArea): 

733 

734 if not self.dockwidgets: 

735 self.dockwidgets = [] 

736 

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

738 

739 dockwidget = qw.QDockWidget(name, self) 

740 self.dockwidgets.append(dockwidget) 

741 dockwidget.setWidget(panel) 

742 panel.setParent(dockwidget) 

743 self.addDockWidget(where, dockwidget) 

744 

745 if dws: 

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

747 

748 self.toggle_panel(dockwidget, visible) 

749 

750 mitem = qw.QAction(name, None) 

751 

752 def toggle_panel(checked): 

753 self.toggle_panel(dockwidget, True) 

754 

755 mitem.triggered.connect(toggle_panel) 

756 

757 if volatile: 

758 def visibility(visible): 

759 if not visible: 

760 self.remove_panel(panel) 

761 

762 dockwidget.visibilityChanged.connect( 

763 visibility) 

764 

765 self.get_view().add_panel_toggler(mitem) 

766 self.dockwidget_to_toggler[dockwidget] = mitem 

767 

768 if pile_viewer.is_macos: 

769 tabbars = self.findChildren(qw.QTabBar) 

770 for tabbar in tabbars: 

771 tabbar.setShape(qw.QTabBar.TriangularNorth) 

772 tabbar.setDocumentMode(True) 

773 

774 def toggle_panel(self, dockwidget, visible): 

775 if visible is None: 

776 visible = not dockwidget.isVisible() 

777 

778 dockwidget.setVisible(visible) 

779 if visible: 

780 w = dockwidget.widget() 

781 minsize = w.minimumSize() 

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

783 

784 def reset_minimum_size(): 

785 import sip 

786 if not sip.isdeleted(w): 

787 w.setMinimumSize(minsize) 

788 

789 qc.QTimer.singleShot(200, reset_minimum_size) 

790 

791 dockwidget.setFocus() 

792 dockwidget.raise_() 

793 

794 def toggle_marker_editor(self): 

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

796 

797 def toggle_main_controls(self): 

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

799 

800 def remove_panel(self, panel): 

801 dockwidget = panel.parent() 

802 self.removeDockWidget(dockwidget) 

803 dockwidget.setParent(None) 

804 mitem = self.dockwidget_to_toggler[dockwidget] 

805 self.get_view().remove_panel_toggler(mitem) 

806 

807 def return_tag(self): 

808 return self.get_view().return_tag 

809 

810 def confirm_close(self): 

811 ret = qw.QMessageBox.question( 

812 self, 

813 'Snuffler', 

814 'Close Snuffler window?', 

815 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

816 qw.QMessageBox.Ok) 

817 

818 return ret == qw.QMessageBox.Ok 

819 

820 def closeEvent(self, event): 

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

822 self.closing = True 

823 self.pile_viewer.cleanup() 

824 event.accept() 

825 else: 

826 event.ignore() 

827 

828 def is_closing(self): 

829 return self.closing 

830 

831 

832class Snuffler(qw.QApplication): 

833 

834 def __init__(self): 

835 qw.QApplication.__init__(self, []) 

836 self.setApplicationName('Snuffler') 

837 self.setApplicationDisplayName('Snuffler') 

838 self.lastWindowClosed.connect(self.myQuit) 

839 self.server = None 

840 self.loader = None 

841 

842 def install_sigint_handler(self): 

843 self._old_signal_handler = signal.signal( 

844 signal.SIGINT, 

845 self.myCloseAllWindows) 

846 

847 def uninstall_sigint_handler(self): 

848 signal.signal(signal.SIGINT, self._old_signal_handler) 

849 

850 def snuffler_windows(self): 

851 return [w for w in self.topLevelWidgets() 

852 if isinstance(w, SnufflerWindow) and not w.is_closing()] 

853 

854 def event(self, e): 

855 if isinstance(e, qg.QFileOpenEvent): 

856 path = str(e.file()) 

857 if path != sys.argv[0]: 

858 wins = self.snuffler_windows() 

859 if wins: 

860 wins[0].get_view().load_soon([path]) 

861 

862 return True 

863 else: 

864 return qw.QApplication.event(self, e) 

865 

866 def load(self, pathes, cachedirname, pattern, format): 

867 if not self.loader: 

868 self.start_loader() 

869 

870 self.loader.ship( 

871 ('load', pathes, cachedirname, pattern, format)) 

872 

873 def update_progress(self, task, percent): 

874 self.pile_viewer.progressbars.set_status(task, percent) 

875 

876 def myCloseAllWindows(self, *args): 

877 

878 def confirm(): 

879 try: 

880 print('\nQuit Snuffler? [y/n]', file=sys.stderr) 

881 confirmed = getch() == 'y' 

882 if not confirmed: 

883 print('Continuing.', file=sys.stderr) 

884 else: 

885 print('Quitting Snuffler.', file=sys.stderr) 

886 

887 return confirmed 

888 

889 except Exception: 

890 return False 

891 

892 if confirm(): 

893 for win in self.snuffler_windows(): 

894 win.instant_close = True 

895 

896 self.closeAllWindows() 

897 

898 def myQuit(self, *args): 

899 self.quit()