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 signal 

12import logging 

13import time 

14import re 

15import zlib 

16import struct 

17import pickle 

18try: 

19 from urlparse import parse_qsl 

20except ImportError: 

21 from urllib.parse import parse_qsl 

22 

23from pyrocko.streaming import serial_hamster 

24from pyrocko.streaming import slink 

25from pyrocko.streaming import edl 

26from pyrocko.streaming import datacube 

27 

28from pyrocko import pile # noqa 

29from pyrocko import util # noqa 

30from pyrocko import model # noqa 

31from pyrocko import config # noqa 

32from pyrocko import io # noqa 

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 _Getch: 

42 ''' 

43 Gets a single character from standard input. 

44 

45 Does not echo to the screen. 

46 

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

48 ''' 

49 def __init__(self): 

50 try: 

51 self.impl = _GetchWindows() 

52 except ImportError: 

53 self.impl = _GetchUnix() 

54 

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

56 

57 

58class _GetchUnix: 

59 def __init__(self): 

60 import tty, sys # noqa 

61 

62 def __call__(self): 

63 import sys 

64 import tty 

65 import termios 

66 

67 fd = sys.stdin.fileno() 

68 old_settings = termios.tcgetattr(fd) 

69 try: 

70 tty.setraw(fd) 

71 ch = sys.stdin.read(1) 

72 finally: 

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

74 

75 return ch 

76 

77 

78class _GetchWindows: 

79 def __init__(self): 

80 import msvcrt # noqa 

81 

82 def __call__(self): 

83 import msvcrt 

84 return msvcrt.getch() 

85 

86 

87getch = _Getch() 

88 

89 

90class AcquisitionThread(qc.QThread): 

91 def __init__(self, post_process_sleep=0.0): 

92 qc.QThread.__init__(self) 

93 self.mutex = qc.QMutex() 

94 self.queue = [] 

95 self.post_process_sleep = post_process_sleep 

96 self._sun_is_shining = True 

97 

98 def get_wanted_poll_interval(self): 

99 return 1000. 

100 

101 def run(self): 

102 while True: 

103 try: 

104 self.acquisition_start() 

105 while self._sun_is_shining: 

106 t0 = time.time() 

107 self.process() 

108 t1 = time.time() 

109 if self.post_process_sleep != 0.0: 

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

111 

112 self.acquisition_stop() 

113 break 

114 

115 except ( 

116 edl.ReadError, 

117 serial_hamster.SerialHamsterError, 

118 slink.SlowSlinkError) as e: 

119 

120 logger.error(str(e)) 

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

122 self.acquisition_stop() 

123 time.sleep(5) 

124 if not self._sun_is_shining: 

125 break 

126 

127 def stop(self): 

128 self._sun_is_shining = False 

129 

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

131 self.wait() 

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

133 

134 def got_trace(self, tr): 

135 self.mutex.lock() 

136 self.queue.append(tr) 

137 self.mutex.unlock() 

138 

139 def poll(self): 

140 self.mutex.lock() 

141 items = self.queue[:] 

142 self.queue[:] = [] 

143 self.mutex.unlock() 

144 return items 

145 

146 

147class SlinkAcquisition( 

148 slink.SlowSlink, AcquisitionThread): 

149 

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

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

152 AcquisitionThread.__init__(self) 

153 

154 def got_trace(self, tr): 

155 AcquisitionThread.got_trace(self, tr) 

156 

157 

158class CamAcquisition( 

159 serial_hamster.CamSerialHamster, AcquisitionThread): 

160 

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

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

163 AcquisitionThread.__init__(self, post_process_sleep=0.1) 

164 

165 def got_trace(self, tr): 

166 AcquisitionThread.got_trace(self, tr) 

167 

168 

169class USBHB628Acquisition( 

170 serial_hamster.USBHB628Hamster, AcquisitionThread): 

171 

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

173 serial_hamster.USBHB628Hamster.__init__( 

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

175 AcquisitionThread.__init__(self) 

176 

177 def got_trace(self, tr): 

178 AcquisitionThread.got_trace(self, tr) 

179 

180 

181class SchoolSeismometerAcquisition( 

182 serial_hamster.SerialHamster, AcquisitionThread): 

183 

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

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

186 AcquisitionThread.__init__(self, post_process_sleep=0.0) 

187 

188 def got_trace(self, tr): 

189 AcquisitionThread.got_trace(self, tr) 

190 

191 def get_wanted_poll_interval(self): 

192 return 100. 

193 

194 

195class EDLAcquisition( 

196 edl.EDLHamster, AcquisitionThread): 

197 

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

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

200 AcquisitionThread.__init__(self) 

201 

202 def got_trace(self, tr): 

203 AcquisitionThread.got_trace(self, tr) 

204 

205 

206class CubeAcquisition( 

207 datacube.SerialCube, AcquisitionThread): 

208 

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

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

211 AcquisitionThread.__init__(self) 

212 

213 def got_trace(self, tr): 

214 AcquisitionThread.got_trace(self, tr) 

215 

216 

217def setup_acquisition_sources(args): 

218 

219 sources = [] 

220 iarg = 0 

221 while iarg < len(args): 

222 arg = args[iarg] 

223 

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

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

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

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

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

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

230 

231 if msl: 

232 host = msl.group(1) 

233 port = msl.group(3) 

234 if not port: 

235 port = '18000' 

236 

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

238 if msl.group(5): 

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

240 

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

242 try: 

243 streams = sl.query_streams() 

244 except slink.SlowSlinkError as e: 

245 logger.fatal(str(e)) 

246 sys.exit(1) 

247 

248 streams = list(set( 

249 util.match_nslcs(stream_patterns, streams))) 

250 

251 for stream in streams: 

252 sl.add_stream(*stream) 

253 else: 

254 for stream in stream_patterns: 

255 sl.add_raw_stream_selector(stream) 

256 

257 sources.append(sl) 

258 

259 elif mca: 

260 port = mca.group(1) 

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

262 sources.append(cam) 

263 

264 elif mus: 

265 port = mus.group(1) 

266 try: 

267 d = {} 

268 if mus.group(3): 

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

270 

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

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

273 hb628 = USBHB628Acquisition( 

274 port=port, 

275 deltat=deltat, 

276 channels=channels, 

277 buffersize=16, 

278 lookback=50) 

279 

280 sources.append(hb628) 

281 except Exception: 

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

283 

284 elif msc: 

285 port = msc.group(1) 

286 

287 d = {} 

288 if msc.group(3): 

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

290 

291 d_rate = { 

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

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

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

295 

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

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

298 

299 if s_rate not in d_rate: 

300 raise Exception( 

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

302 % s_rate) 

303 

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

305 

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

307 raise Exception( 

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

309 % s_gain) 

310 

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

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

313 

314 logger.info( 

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

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

317 

318 sco = SchoolSeismometerAcquisition( 

319 port=port, 

320 deltat=deltat, 

321 start_string=start_string, 

322 min_detection_size=50, 

323 disallow_uneven_sampling_rates=False, 

324 station=station) 

325 

326 sources.append(sco) 

327 

328 elif med: 

329 port = med.group(1) 

330 edl = EDLAcquisition(port=port) 

331 sources.append(edl) 

332 elif mcu: 

333 device = mcu.group(1) 

334 cube = CubeAcquisition(device=device) 

335 sources.append(cube) 

336 

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

338 args.pop(iarg) 

339 else: 

340 iarg += 1 

341 

342 return sources 

343 

344 

345class PollInjector(qc.QObject): 

346 

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

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

349 qc.QObject.__init__(self) 

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

351 self._sources = [] 

352 self.startTimer(int(interval)) 

353 

354 def add_source(self, source): 

355 self._sources.append(source) 

356 

357 def remove_source(self, source): 

358 self._sources.remove(source) 

359 

360 def timerEvent(self, ev): 

361 for source in self._sources: 

362 trs = source.poll() 

363 for tr in trs: 

364 self._injector.inject(tr) 

365 

366 # following methods needed because mulitple inheritance does not seem 

367 # to work anymore with QObject in Python3 or PyQt5 

368 

369 def set_fixation_length(self, length): 

370 return self._injector.set_fixation_length(length) 

371 

372 def set_save_path( 

373 self, 

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

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

376 

377 return self._injector.set_save_path(path) 

378 

379 def fixate_all(self): 

380 return self._injector.fixate_all() 

381 

382 def free(self): 

383 return self._injector.free() 

384 

385 

386class Connection(qc.QObject): 

387 

388 received = qc.pyqtSignal(object, object) 

389 disconnected = qc.pyqtSignal(object) 

390 

391 def __init__(self, parent, sock): 

392 qc.QObject.__init__(self, parent) 

393 self.socket = sock 

394 self.readyRead.connect( 

395 self.handle_read) 

396 self.disconnected.connect( 

397 self.handle_disconnected) 

398 self.nwanted = 8 

399 self.reading_size = True 

400 self.handler = None 

401 self.nbytes_received = 0 

402 self.nbytes_sent = 0 

403 self.compressor = zlib.compressobj() 

404 self.decompressor = zlib.decompressobj() 

405 

406 def handle_read(self): 

407 while True: 

408 navail = self.socket.bytesAvailable() 

409 if navail < self.nwanted: 

410 return 

411 

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

413 self.nbytes_received += len(data) 

414 if self.reading_size: 

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

416 self.reading_size = False 

417 else: 

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

419 if obj is None: 

420 self.socket.disconnectFromHost() 

421 else: 

422 self.handle_received(obj) 

423 self.nwanted = 8 

424 self.reading_size = True 

425 

426 def handle_received(self, obj): 

427 self.received.emit(self, obj) 

428 

429 def ship(self, obj): 

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

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

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

433 self.socket.write(data) 

434 self.socket.write(data_end) 

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

436 

437 def handle_disconnected(self): 

438 self.disconnected.emit(self) 

439 

440 def close(self): 

441 self.socket.close() 

442 

443 

444class ConnectionHandler(qc.QObject): 

445 def __init__(self, parent): 

446 qc.QObject.__init__(self, parent) 

447 self.queue = [] 

448 self.connection = None 

449 

450 def connected(self): 

451 return self.connection is None 

452 

453 def set_connection(self, connection): 

454 self.connection = connection 

455 connection.received.connect( 

456 self._handle_received) 

457 

458 connection.connect( 

459 self.handle_disconnected) 

460 

461 for obj in self.queue: 

462 self.connection.ship(obj) 

463 

464 self.queue = [] 

465 

466 def _handle_received(self, conn, obj): 

467 self.handle_received(obj) 

468 

469 def handle_received(self, obj): 

470 pass 

471 

472 def handle_disconnected(self): 

473 self.connection = None 

474 

475 def ship(self, obj): 

476 if self.connection: 

477 self.connection.ship(obj) 

478 else: 

479 self.queue.append(obj) 

480 

481 

482class SimpleConnectionHandler(ConnectionHandler): 

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

484 ConnectionHandler.__init__(self, parent) 

485 self.mapping = mapping 

486 

487 def handle_received(self, obj): 

488 command = obj[0] 

489 args = obj[1:] 

490 self.mapping[command](*args) 

491 

492 

493class MyMainWindow(qw.QMainWindow): 

494 

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

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

497 self.app = app 

498 

499 def keyPressEvent(self, ev): 

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

501 

502 

503class SnufflerTabs(qw.QTabWidget): 

504 def __init__(self, parent): 

505 qw.QTabWidget.__init__(self, parent) 

506 if hasattr(self, 'setTabsClosable'): 

507 self.setTabsClosable(True) 

508 

509 self.tabCloseRequested.connect( 

510 self.removeTab) 

511 

512 if hasattr(self, 'setDocumentMode'): 

513 self.setDocumentMode(True) 

514 

515 def hide_close_button_on_first_tab(self): 

516 tbar = self.tabBar() 

517 if hasattr(tbar, 'setTabButton'): 

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

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

520 

521 def append_tab(self, widget, name): 

522 widget.setParent(self) 

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

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

525 

526 def remove_tab(self, widget): 

527 self.removeTab(self.indexOf(widget)) 

528 

529 def tabInserted(self, index): 

530 if index == 0: 

531 self.hide_close_button_on_first_tab() 

532 

533 self.tabbar_visibility() 

534 self.setFocus() 

535 

536 def removeTab(self, index): 

537 w = self.widget(index) 

538 w.close() 

539 qw.QTabWidget.removeTab(self, index) 

540 

541 def tabRemoved(self, index): 

542 self.tabbar_visibility() 

543 

544 def tabbar_visibility(self): 

545 if self.count() <= 1: 

546 self.tabBar().hide() 

547 elif self.count() > 1: 

548 self.tabBar().show() 

549 

550 def keyPressEvent(self, event): 

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

552 i = self.currentIndex() 

553 if i != 0: 

554 self.tabCloseRequested.emit(i) 

555 else: 

556 self.parent().keyPressEvent(event) 

557 

558 

559class SnufflerStartWizard(qw.QWizard): 

560 

561 def __init__(self, parent): 

562 qw.QWizard.__init__(self, parent) 

563 

564 self.setOption(self.NoBackButtonOnStartPage) 

565 self.setOption(self.NoBackButtonOnLastPage) 

566 self.setOption(self.NoCancelButton) 

567 self.addPageSurvey() 

568 self.addPageHelp() 

569 self.setWindowTitle('Welcome to Pyrocko') 

570 

571 def getSystemInfo(self): 

572 import numpy 

573 import scipy 

574 import pyrocko 

575 import platform 

576 import uuid 

577 data = { 

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

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

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

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

582 'python': platform.python_version(), 

583 'pyrocko': pyrocko.__version__, 

584 'numpy': numpy.__version__, 

585 'scipy': scipy.__version__, 

586 'qt': qc.PYQT_VERSION_STR, 

587 } 

588 return data 

589 

590 def addPageSurvey(self): 

591 import pprint 

592 webtk = 'DSFGK234ADF4ASDF' 

593 sys_info = self.getSystemInfo() 

594 

595 p = qw.QWizardPage() 

596 p.setCommitPage(True) 

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

598 

599 lyt = qw.QVBoxLayout() 

600 lyt.addWidget(qw.QLabel( 

601 '<p>Your feedback is important for' 

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

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

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

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

606 

607 text_data = qw.QLabel( 

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

609 pprint.pformat( 

610 sys_info, 

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

612 ) 

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

614 lyt.addWidget(text_data) 

615 

616 lyt.addWidget(qw.QLabel( 

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

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

619 )) 

620 

621 p.setLayout(lyt) 

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

623 

624 yes_btn = qw.QPushButton(p) 

625 yes_btn.setText('Yes') 

626 

627 @qc.pyqtSlot() 

628 def send_data(): 

629 import requests 

630 import json 

631 try: 

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

633 data=json.dumps(sys_info)) 

634 except Exception as e: 

635 print(e) 

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

637 

638 self.customButtonClicked.connect(send_data) 

639 

640 self.setButton(self.CustomButton1, yes_btn) 

641 self.setOption(self.HaveCustomButton1, True) 

642 

643 self.addPage(p) 

644 return p 

645 

646 def addPageHelp(self): 

647 p = qw.QWizardPage() 

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

649 

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

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

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

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

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

655<ul> 

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

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

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

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

660 <li> 

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

662 </li> 

663</ul> 

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

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

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

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

668</p> 

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

670 align="center"> 

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

672</p> 

673</html>''') 

674 

675 lyt = qw.QVBoxLayout() 

676 lyt.addWidget(text) 

677 

678 def remove_custom_button(): 

679 self.setOption(self.HaveCustomButton1, False) 

680 

681 p.initializePage = remove_custom_button 

682 

683 p.setLayout(lyt) 

684 self.addPage(p) 

685 return p 

686 

687 

688class SnufflerWindow(qw.QMainWindow): 

689 

690 def __init__( 

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

692 marker_editor_sortable=True, follow=None, controls=True, 

693 opengl=None, instant_close=False): 

694 

695 qw.QMainWindow.__init__(self) 

696 

697 self.instant_close = instant_close 

698 

699 self.dockwidget_to_toggler = {} 

700 self.dockwidgets = [] 

701 

702 self.setWindowTitle('Snuffler') 

703 

704 self.pile_viewer = pile_viewer.PileViewer( 

705 pile, ntracks_shown_max=ntracks, use_opengl=opengl, 

706 marker_editor_sortable=marker_editor_sortable, 

707 panel_parent=self) 

708 

709 self.marker_editor = self.pile_viewer.marker_editor() 

710 self.add_panel( 

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

712 where=qc.Qt.RightDockWidgetArea) 

713 if stations: 

714 self.get_view().add_stations(stations) 

715 

716 if events: 

717 self.get_view().add_events(events) 

718 

719 if len(events) == 1: 

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

721 

722 if markers: 

723 self.get_view().add_markers(markers) 

724 self.get_view().associate_phases_to_events() 

725 

726 self.tabs = SnufflerTabs(self) 

727 self.setCentralWidget(self.tabs) 

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

729 

730 self.pile_viewer.setup_snufflings() 

731 self.setMenuBar(self.pile_viewer.menu) 

732 

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

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

735 self.show() 

736 

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

738 

739 sb = self.statusBar() 

740 sb.clearMessage() 

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

742 

743 snuffler_config = self.pile_viewer.viewer.config 

744 

745 if snuffler_config.first_start: 

746 wizard = SnufflerStartWizard(self) 

747 

748 @qc.pyqtSlot() 

749 def wizard_finished(result): 

750 if result == wizard.Accepted: 

751 snuffler_config.first_start = False 

752 config.write_config(snuffler_config, 'snuffler') 

753 

754 wizard.finished.connect(wizard_finished) 

755 

756 wizard.show() 

757 

758 if follow: 

759 self.get_view().follow(float(follow)) 

760 

761 self.closing = False 

762 

763 def sizeHint(self): 

764 return qc.QSize(1024, 768) 

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

766 

767 def keyPressEvent(self, ev): 

768 self.get_view().keyPressEvent(ev) 

769 

770 def get_view(self): 

771 return self.pile_viewer.get_view() 

772 

773 def get_panel_parent_widget(self): 

774 return self 

775 

776 def add_tab(self, name, widget): 

777 self.tabs.append_tab(widget, name) 

778 

779 def remove_tab(self, widget): 

780 self.tabs.remove_tab(widget) 

781 

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

783 where=qc.Qt.BottomDockWidgetArea): 

784 

785 if not self.dockwidgets: 

786 self.dockwidgets = [] 

787 

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

789 

790 dockwidget = qw.QDockWidget(name, self) 

791 self.dockwidgets.append(dockwidget) 

792 dockwidget.setWidget(panel) 

793 panel.setParent(dockwidget) 

794 self.addDockWidget(where, dockwidget) 

795 

796 if dws: 

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

798 

799 self.toggle_panel(dockwidget, visible) 

800 

801 mitem = qw.QAction(name, None) 

802 

803 def toggle_panel(checked): 

804 self.toggle_panel(dockwidget, True) 

805 

806 mitem.triggered.connect(toggle_panel) 

807 

808 if volatile: 

809 def visibility(visible): 

810 if not visible: 

811 self.remove_panel(panel) 

812 

813 dockwidget.visibilityChanged.connect( 

814 visibility) 

815 

816 self.get_view().add_panel_toggler(mitem) 

817 self.dockwidget_to_toggler[dockwidget] = mitem 

818 

819 if pile_viewer.is_macos: 

820 tabbars = self.findChildren(qw.QTabBar) 

821 for tabbar in tabbars: 

822 tabbar.setShape(qw.QTabBar.TriangularNorth) 

823 tabbar.setDocumentMode(True) 

824 

825 def toggle_panel(self, dockwidget, visible): 

826 if visible is None: 

827 visible = not dockwidget.isVisible() 

828 

829 dockwidget.setVisible(visible) 

830 if visible: 

831 w = dockwidget.widget() 

832 minsize = w.minimumSize() 

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

834 

835 def reset_minimum_size(): 

836 import sip 

837 if not sip.isdeleted(w): 

838 w.setMinimumSize(minsize) 

839 

840 qc.QTimer.singleShot(200, reset_minimum_size) 

841 

842 dockwidget.setFocus() 

843 dockwidget.raise_() 

844 

845 def toggle_marker_editor(self): 

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

847 

848 def toggle_main_controls(self): 

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

850 

851 def remove_panel(self, panel): 

852 dockwidget = panel.parent() 

853 self.removeDockWidget(dockwidget) 

854 dockwidget.setParent(None) 

855 mitem = self.dockwidget_to_toggler[dockwidget] 

856 self.get_view().remove_panel_toggler(mitem) 

857 

858 def return_tag(self): 

859 return self.get_view().return_tag 

860 

861 def confirm_close(self): 

862 ret = qw.QMessageBox.question( 

863 self, 

864 'Snuffler', 

865 'Close Snuffler window?', 

866 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

867 qw.QMessageBox.Ok) 

868 

869 return ret == qw.QMessageBox.Ok 

870 

871 def closeEvent(self, event): 

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

873 self.closing = True 

874 self.pile_viewer.cleanup() 

875 event.accept() 

876 else: 

877 event.ignore() 

878 

879 def is_closing(self): 

880 return self.closing 

881 

882 

883class Snuffler(qw.QApplication): 

884 

885 def __init__(self): 

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

887 self.setApplicationName('Snuffler') 

888 self.setApplicationDisplayName('Snuffler') 

889 self.lastWindowClosed.connect(self.myQuit) 

890 self.server = None 

891 self.loader = None 

892 

893 def install_sigint_handler(self): 

894 self._old_signal_handler = signal.signal( 

895 signal.SIGINT, 

896 self.myCloseAllWindows) 

897 

898 def uninstall_sigint_handler(self): 

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

900 

901 def snuffler_windows(self): 

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

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

904 

905 def event(self, e): 

906 if isinstance(e, qg.QFileOpenEvent): 

907 path = str(e.file()) 

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

909 wins = self.snuffler_windows() 

910 if wins: 

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

912 

913 return True 

914 else: 

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

916 

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

918 if not self.loader: 

919 self.start_loader() 

920 

921 self.loader.ship( 

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

923 

924 def update_progress(self, task, percent): 

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

926 

927 def myCloseAllWindows(self, *args): 

928 

929 def confirm(): 

930 try: 

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

932 confirmed = getch() == 'y' 

933 if not confirmed: 

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

935 else: 

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

937 

938 return confirmed 

939 

940 except Exception: 

941 return False 

942 

943 if confirm(): 

944 for win in self.snuffler_windows(): 

945 win.instant_close = True 

946 

947 self.closeAllWindows() 

948 

949 def myQuit(self, *args): 

950 self.quit()