1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5''' 

6Effective seismological trace viewer. 

7''' 

8 

9import sys 

10import signal 

11import logging 

12import time 

13import re 

14import zlib 

15import struct 

16import pickle 

17 

18 

19from pyrocko.streaming import serial_hamster 

20from pyrocko.streaming import slink 

21from pyrocko.streaming import edl 

22from pyrocko.streaming import datacube 

23 

24from pyrocko import pile # noqa 

25from pyrocko import util # noqa 

26from pyrocko import model # noqa 

27from pyrocko import config # noqa 

28from pyrocko import io # noqa 

29 

30from . import pile_viewer # noqa 

31 

32from .qt_compat import qc, qg, qw 

33 

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

35 

36 

37class _Getch: 

38 ''' 

39 Gets a single character from standard input. 

40 

41 Does not echo to the screen. 

42 

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

44 ''' 

45 def __init__(self): 

46 try: 

47 self.impl = _GetchWindows() 

48 except ImportError: 

49 self.impl = _GetchUnix() 

50 

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

52 

53 

54class _GetchUnix: 

55 def __init__(self): 

56 import tty, sys # noqa 

57 

58 def __call__(self): 

59 import sys 

60 import tty 

61 import termios 

62 

63 fd = sys.stdin.fileno() 

64 old_settings = termios.tcgetattr(fd) 

65 try: 

66 tty.setraw(fd) 

67 ch = sys.stdin.read(1) 

68 finally: 

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

70 

71 return ch 

72 

73 

74class _GetchWindows: 

75 def __init__(self): 

76 import msvcrt # noqa 

77 

78 def __call__(self): 

79 import msvcrt 

80 return msvcrt.getch() 

81 

82 

83getch = _Getch() 

84 

85 

86class AcquisitionThread(qc.QThread): 

87 def __init__(self, post_process_sleep=0.0): 

88 qc.QThread.__init__(self) 

89 self.mutex = qc.QMutex() 

90 self.queue = [] 

91 self.post_process_sleep = post_process_sleep 

92 self._sun_is_shining = True 

93 

94 def run(self): 

95 while True: 

96 try: 

97 self.acquisition_start() 

98 while self._sun_is_shining: 

99 t0 = time.time() 

100 self.process() 

101 t1 = time.time() 

102 if self.post_process_sleep != 0.0: 

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

104 

105 self.acquisition_stop() 

106 break 

107 

108 except ( 

109 edl.ReadError, 

110 serial_hamster.SerialHamsterError, 

111 slink.SlowSlinkError) as e: 

112 

113 logger.error(str(e)) 

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

115 self.acquisition_stop() 

116 time.sleep(5) 

117 if not self._sun_is_shining: 

118 break 

119 

120 def stop(self): 

121 self._sun_is_shining = False 

122 

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

124 self.wait() 

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

126 

127 def got_trace(self, tr): 

128 self.mutex.lock() 

129 self.queue.append(tr) 

130 self.mutex.unlock() 

131 

132 def poll(self): 

133 self.mutex.lock() 

134 items = self.queue[:] 

135 self.queue[:] = [] 

136 self.mutex.unlock() 

137 return items 

138 

139 

140class SlinkAcquisition( 

141 slink.SlowSlink, AcquisitionThread): 

142 

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

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

145 AcquisitionThread.__init__(self) 

146 

147 def got_trace(self, tr): 

148 AcquisitionThread.got_trace(self, tr) 

149 

150 

151class CamAcquisition( 

152 serial_hamster.CamSerialHamster, AcquisitionThread): 

153 

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

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

156 AcquisitionThread.__init__(self, post_process_sleep=0.1) 

157 

158 def got_trace(self, tr): 

159 AcquisitionThread.got_trace(self, tr) 

160 

161 

162class USBHB628Acquisition( 

163 serial_hamster.USBHB628Hamster, AcquisitionThread): 

164 

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

166 serial_hamster.USBHB628Hamster.__init__( 

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

168 AcquisitionThread.__init__(self) 

169 

170 def got_trace(self, tr): 

171 AcquisitionThread.got_trace(self, tr) 

172 

173 

174class SchoolSeismometerAcquisition( 

175 serial_hamster.SerialHamster, AcquisitionThread): 

176 

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

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

179 AcquisitionThread.__init__(self, post_process_sleep=0.01) 

180 

181 def got_trace(self, tr): 

182 AcquisitionThread.got_trace(self, tr) 

183 

184 

185class EDLAcquisition( 

186 edl.EDLHamster, AcquisitionThread): 

187 

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

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

190 AcquisitionThread.__init__(self) 

191 

192 def got_trace(self, tr): 

193 AcquisitionThread.got_trace(self, tr) 

194 

195 

196class CubeAcquisition( 

197 datacube.SerialCube, AcquisitionThread): 

198 

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

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

201 AcquisitionThread.__init__(self) 

202 

203 def got_trace(self, tr): 

204 AcquisitionThread.got_trace(self, tr) 

205 

206 

207def setup_acquisition_sources(args): 

208 

209 sources = [] 

210 iarg = 0 

211 while iarg < len(args): 

212 arg = args[iarg] 

213 

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

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

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

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

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

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

220 

221 if msl: 

222 host = msl.group(1) 

223 port = msl.group(3) 

224 if not port: 

225 port = '18000' 

226 

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

228 if msl.group(5): 

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

230 

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

232 try: 

233 streams = sl.query_streams() 

234 except slink.SlowSlinkError as e: 

235 logger.fatal(str(e)) 

236 sys.exit(1) 

237 

238 streams = list(set( 

239 util.match_nslcs(stream_patterns, streams))) 

240 

241 for stream in streams: 

242 sl.add_stream(*stream) 

243 else: 

244 for stream in stream_patterns: 

245 sl.add_raw_stream_selector(stream) 

246 

247 sources.append(sl) 

248 elif mca: 

249 port = mca.group(1) 

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

251 sources.append(cam) 

252 elif mus: 

253 port = mus.group(1) 

254 try: 

255 d = {} 

256 if mus.group(3): 

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

258 

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

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

261 hb628 = USBHB628Acquisition( 

262 port=port, 

263 deltat=deltat, 

264 channels=channels, 

265 buffersize=16, 

266 lookback=50) 

267 

268 sources.append(hb628) 

269 except Exception: 

270 raise 

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

272 

273 elif msc: 

274 port = msc.group(1) 

275 sco = SchoolSeismometerAcquisition(port=port) 

276 sources.append(sco) 

277 elif med: 

278 port = med.group(1) 

279 edl = EDLAcquisition(port=port) 

280 sources.append(edl) 

281 elif mcu: 

282 device = mcu.group(1) 

283 cube = CubeAcquisition(device=device) 

284 sources.append(cube) 

285 

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

287 args.pop(iarg) 

288 else: 

289 iarg += 1 

290 

291 return sources 

292 

293 

294class PollInjector(qc.QObject): 

295 

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

297 qc.QObject.__init__(self) 

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

299 self._sources = [] 

300 self.startTimer(1000) 

301 

302 def add_source(self, source): 

303 self._sources.append(source) 

304 

305 def remove_source(self, source): 

306 self._sources.remove(source) 

307 

308 def timerEvent(self, ev): 

309 for source in self._sources: 

310 trs = source.poll() 

311 for tr in trs: 

312 self._injector.inject(tr) 

313 

314 # following methods needed because mulitple inheritance does not seem 

315 # to work anymore with QObject in Python3 or PyQt5 

316 

317 def set_fixation_length(self, length): 

318 return self._injector.set_fixation_length(length) 

319 

320 def set_save_path( 

321 self, 

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

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

324 

325 return self._injector.set_save_path(path) 

326 

327 def fixate_all(self): 

328 return self._injector.fixate_all() 

329 

330 def free(self): 

331 return self._injector.free() 

332 

333 

334class Connection(qc.QObject): 

335 

336 received = qc.pyqtSignal(object, object) 

337 disconnected = qc.pyqtSignal(object) 

338 

339 def __init__(self, parent, sock): 

340 qc.QObject.__init__(self, parent) 

341 self.socket = sock 

342 self.readyRead.connect( 

343 self.handle_read) 

344 self.disconnected.connect( 

345 self.handle_disconnected) 

346 self.nwanted = 8 

347 self.reading_size = True 

348 self.handler = None 

349 self.nbytes_received = 0 

350 self.nbytes_sent = 0 

351 self.compressor = zlib.compressobj() 

352 self.decompressor = zlib.decompressobj() 

353 

354 def handle_read(self): 

355 while True: 

356 navail = self.socket.bytesAvailable() 

357 if navail < self.nwanted: 

358 return 

359 

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

361 self.nbytes_received += len(data) 

362 if self.reading_size: 

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

364 self.reading_size = False 

365 else: 

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

367 if obj is None: 

368 self.socket.disconnectFromHost() 

369 else: 

370 self.handle_received(obj) 

371 self.nwanted = 8 

372 self.reading_size = True 

373 

374 def handle_received(self, obj): 

375 self.received.emit(self, obj) 

376 

377 def ship(self, obj): 

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

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

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

381 self.socket.write(data) 

382 self.socket.write(data_end) 

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

384 

385 def handle_disconnected(self): 

386 self.disconnected.emit(self) 

387 

388 def close(self): 

389 self.socket.close() 

390 

391 

392class ConnectionHandler(qc.QObject): 

393 def __init__(self, parent): 

394 qc.QObject.__init__(self, parent) 

395 self.queue = [] 

396 self.connection = None 

397 

398 def connected(self): 

399 return self.connection is None 

400 

401 def set_connection(self, connection): 

402 self.connection = connection 

403 connection.received.connect( 

404 self._handle_received) 

405 

406 connection.connect( 

407 self.handle_disconnected) 

408 

409 for obj in self.queue: 

410 self.connection.ship(obj) 

411 

412 self.queue = [] 

413 

414 def _handle_received(self, conn, obj): 

415 self.handle_received(obj) 

416 

417 def handle_received(self, obj): 

418 pass 

419 

420 def handle_disconnected(self): 

421 self.connection = None 

422 

423 def ship(self, obj): 

424 if self.connection: 

425 self.connection.ship(obj) 

426 else: 

427 self.queue.append(obj) 

428 

429 

430class SimpleConnectionHandler(ConnectionHandler): 

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

432 ConnectionHandler.__init__(self, parent) 

433 self.mapping = mapping 

434 

435 def handle_received(self, obj): 

436 command = obj[0] 

437 args = obj[1:] 

438 self.mapping[command](*args) 

439 

440 

441class MyMainWindow(qw.QMainWindow): 

442 

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

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

445 self.app = app 

446 

447 def keyPressEvent(self, ev): 

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

449 

450 

451class SnufflerTabs(qw.QTabWidget): 

452 def __init__(self, parent): 

453 qw.QTabWidget.__init__(self, parent) 

454 if hasattr(self, 'setTabsClosable'): 

455 self.setTabsClosable(True) 

456 

457 self.tabCloseRequested.connect( 

458 self.removeTab) 

459 

460 if hasattr(self, 'setDocumentMode'): 

461 self.setDocumentMode(True) 

462 

463 def hide_close_button_on_first_tab(self): 

464 tbar = self.tabBar() 

465 if hasattr(tbar, 'setTabButton'): 

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

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

468 

469 def append_tab(self, widget, name): 

470 widget.setParent(self) 

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

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

473 

474 def remove_tab(self, widget): 

475 self.removeTab(self.indexOf(widget)) 

476 

477 def tabInserted(self, index): 

478 if index == 0: 

479 self.hide_close_button_on_first_tab() 

480 

481 self.tabbar_visibility() 

482 self.setFocus() 

483 

484 def removeTab(self, index): 

485 w = self.widget(index) 

486 w.close() 

487 qw.QTabWidget.removeTab(self, index) 

488 

489 def tabRemoved(self, index): 

490 self.tabbar_visibility() 

491 

492 def tabbar_visibility(self): 

493 if self.count() <= 1: 

494 self.tabBar().hide() 

495 elif self.count() > 1: 

496 self.tabBar().show() 

497 

498 def keyPressEvent(self, event): 

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

500 i = self.currentIndex() 

501 if i != 0: 

502 self.tabCloseRequested.emit(i) 

503 else: 

504 self.parent().keyPressEvent(event) 

505 

506 

507class SnufflerStartWizard(qw.QWizard): 

508 

509 def __init__(self, parent): 

510 qw.QWizard.__init__(self, parent) 

511 

512 self.setOption(self.NoBackButtonOnStartPage) 

513 self.setOption(self.NoBackButtonOnLastPage) 

514 self.setOption(self.NoCancelButton) 

515 self.addPageSurvey() 

516 self.addPageHelp() 

517 self.setWindowTitle('Welcome to Pyrocko') 

518 

519 def getSystemInfo(self): 

520 import numpy 

521 import scipy 

522 import pyrocko 

523 import platform 

524 import uuid 

525 data = { 

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

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

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

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

530 'python': platform.python_version(), 

531 'pyrocko': pyrocko.__version__, 

532 'numpy': numpy.__version__, 

533 'scipy': scipy.__version__, 

534 'qt': qc.PYQT_VERSION_STR, 

535 } 

536 return data 

537 

538 def addPageSurvey(self): 

539 import pprint 

540 webtk = 'DSFGK234ADF4ASDF' 

541 sys_info = self.getSystemInfo() 

542 

543 p = qw.QWizardPage() 

544 p.setCommitPage(True) 

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

546 

547 lyt = qw.QVBoxLayout() 

548 lyt.addWidget(qw.QLabel( 

549 '<p>Your feedback is important for' 

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

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

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

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

554 

555 text_data = qw.QLabel( 

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

557 pprint.pformat( 

558 sys_info, 

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

560 ) 

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

562 lyt.addWidget(text_data) 

563 

564 lyt.addWidget(qw.QLabel( 

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

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

567 )) 

568 

569 p.setLayout(lyt) 

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

571 

572 yes_btn = qw.QPushButton(p) 

573 yes_btn.setText('Yes') 

574 

575 @qc.pyqtSlot() 

576 def send_data(): 

577 import requests 

578 import json 

579 try: 

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

581 data=json.dumps(sys_info)) 

582 except Exception as e: 

583 print(e) 

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

585 

586 self.customButtonClicked.connect(send_data) 

587 

588 self.setButton(self.CustomButton1, yes_btn) 

589 self.setOption(self.HaveCustomButton1, True) 

590 

591 self.addPage(p) 

592 return p 

593 

594 def addPageHelp(self): 

595 p = qw.QWizardPage() 

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

597 

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

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

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

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

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

603<ul> 

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

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

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

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

608 <li> 

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

610 </li> 

611</ul> 

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

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

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

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

616</p> 

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

618 align="center"> 

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

620</p> 

621</html>''') 

622 

623 lyt = qw.QVBoxLayout() 

624 lyt.addWidget(text) 

625 

626 def remove_custom_button(): 

627 self.setOption(self.HaveCustomButton1, False) 

628 

629 p.initializePage = remove_custom_button 

630 

631 p.setLayout(lyt) 

632 self.addPage(p) 

633 return p 

634 

635 

636class SnufflerWindow(qw.QMainWindow): 

637 

638 def __init__( 

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

640 marker_editor_sortable=True, follow=None, controls=True, 

641 opengl=None, instant_close=False): 

642 

643 qw.QMainWindow.__init__(self) 

644 

645 self.instant_close = instant_close 

646 

647 self.dockwidget_to_toggler = {} 

648 self.dockwidgets = [] 

649 

650 self.setWindowTitle('Snuffler') 

651 

652 self.pile_viewer = pile_viewer.PileViewer( 

653 pile, ntracks_shown_max=ntracks, use_opengl=opengl, 

654 marker_editor_sortable=marker_editor_sortable, 

655 panel_parent=self) 

656 

657 self.marker_editor = self.pile_viewer.marker_editor() 

658 self.add_panel( 

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

660 where=qc.Qt.RightDockWidgetArea) 

661 if stations: 

662 self.get_view().add_stations(stations) 

663 

664 if events: 

665 self.get_view().add_events(events) 

666 

667 if len(events) == 1: 

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

669 

670 if markers: 

671 self.get_view().add_markers(markers) 

672 self.get_view().associate_phases_to_events() 

673 

674 self.tabs = SnufflerTabs(self) 

675 self.setCentralWidget(self.tabs) 

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

677 

678 self.pile_viewer.setup_snufflings() 

679 self.setMenuBar(self.pile_viewer.menu) 

680 

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

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

683 self.show() 

684 

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

686 

687 sb = self.statusBar() 

688 sb.clearMessage() 

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

690 

691 snuffler_config = self.pile_viewer.viewer.config 

692 

693 if snuffler_config.first_start: 

694 wizard = SnufflerStartWizard(self) 

695 

696 @qc.pyqtSlot() 

697 def wizard_finished(result): 

698 if result == wizard.Accepted: 

699 snuffler_config.first_start = False 

700 config.write_config(snuffler_config, 'snuffler') 

701 

702 wizard.finished.connect(wizard_finished) 

703 

704 wizard.show() 

705 

706 if follow: 

707 self.get_view().follow(float(follow)) 

708 

709 self.closing = False 

710 

711 def sizeHint(self): 

712 return qc.QSize(1024, 768) 

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

714 

715 def keyPressEvent(self, ev): 

716 self.get_view().keyPressEvent(ev) 

717 

718 def get_view(self): 

719 return self.pile_viewer.get_view() 

720 

721 def get_panel_parent_widget(self): 

722 return self 

723 

724 def add_tab(self, name, widget): 

725 self.tabs.append_tab(widget, name) 

726 

727 def remove_tab(self, widget): 

728 self.tabs.remove_tab(widget) 

729 

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

731 where=qc.Qt.BottomDockWidgetArea): 

732 

733 if not self.dockwidgets: 

734 self.dockwidgets = [] 

735 

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

737 

738 dockwidget = qw.QDockWidget(name, self) 

739 self.dockwidgets.append(dockwidget) 

740 dockwidget.setWidget(panel) 

741 panel.setParent(dockwidget) 

742 self.addDockWidget(where, dockwidget) 

743 

744 if dws: 

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

746 

747 self.toggle_panel(dockwidget, visible) 

748 

749 mitem = qw.QAction(name, None) 

750 

751 def toggle_panel(checked): 

752 self.toggle_panel(dockwidget, True) 

753 

754 mitem.triggered.connect(toggle_panel) 

755 

756 if volatile: 

757 def visibility(visible): 

758 if not visible: 

759 self.remove_panel(panel) 

760 

761 dockwidget.visibilityChanged.connect( 

762 visibility) 

763 

764 self.get_view().add_panel_toggler(mitem) 

765 self.dockwidget_to_toggler[dockwidget] = mitem 

766 

767 if pile_viewer.is_macos: 

768 tabbars = self.findChildren(qw.QTabBar) 

769 for tabbar in tabbars: 

770 tabbar.setShape(qw.QTabBar.TriangularNorth) 

771 tabbar.setDocumentMode(True) 

772 

773 def toggle_panel(self, dockwidget, visible): 

774 if visible is None: 

775 visible = not dockwidget.isVisible() 

776 

777 dockwidget.setVisible(visible) 

778 if visible: 

779 w = dockwidget.widget() 

780 minsize = w.minimumSize() 

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

782 

783 def reset_minimum_size(): 

784 import sip 

785 if not sip.isdeleted(w): 

786 w.setMinimumSize(minsize) 

787 

788 qc.QTimer.singleShot(200, reset_minimum_size) 

789 

790 dockwidget.setFocus() 

791 dockwidget.raise_() 

792 

793 def toggle_marker_editor(self): 

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

795 

796 def toggle_main_controls(self): 

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

798 

799 def remove_panel(self, panel): 

800 dockwidget = panel.parent() 

801 self.removeDockWidget(dockwidget) 

802 dockwidget.setParent(None) 

803 mitem = self.dockwidget_to_toggler[dockwidget] 

804 self.get_view().remove_panel_toggler(mitem) 

805 

806 def return_tag(self): 

807 return self.get_view().return_tag 

808 

809 def confirm_close(self): 

810 ret = qw.QMessageBox.question( 

811 self, 

812 'Snuffler', 

813 'Close Snuffler window?', 

814 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

815 qw.QMessageBox.Ok) 

816 

817 return ret == qw.QMessageBox.Ok 

818 

819 def closeEvent(self, event): 

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

821 self.closing = True 

822 self.pile_viewer.cleanup() 

823 event.accept() 

824 else: 

825 event.ignore() 

826 

827 def is_closing(self): 

828 return self.closing 

829 

830 

831class Snuffler(qw.QApplication): 

832 

833 def __init__(self): 

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

835 self.setApplicationName('Snuffler') 

836 self.setApplicationDisplayName('Snuffler') 

837 self.lastWindowClosed.connect(self.myQuit) 

838 self.server = None 

839 self.loader = None 

840 

841 def install_sigint_handler(self): 

842 self._old_signal_handler = signal.signal( 

843 signal.SIGINT, 

844 self.myCloseAllWindows) 

845 

846 def uninstall_sigint_handler(self): 

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

848 

849 def snuffler_windows(self): 

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

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

852 

853 def event(self, e): 

854 if isinstance(e, qg.QFileOpenEvent): 

855 path = str(e.file()) 

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

857 wins = self.snuffler_windows() 

858 if wins: 

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

860 

861 return True 

862 else: 

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

864 

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

866 if not self.loader: 

867 self.start_loader() 

868 

869 self.loader.ship( 

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

871 

872 def update_progress(self, task, percent): 

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

874 

875 def myCloseAllWindows(self, *args): 

876 

877 def confirm(): 

878 try: 

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

880 confirmed = getch() == 'y' 

881 if not confirmed: 

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

883 else: 

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

885 

886 return confirmed 

887 

888 except Exception: 

889 return False 

890 

891 if confirm(): 

892 for win in self.snuffler_windows(): 

893 win.instant_close = True 

894 

895 self.closeAllWindows() 

896 

897 def myQuit(self, *args): 

898 self.quit()