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 os 

11import sys 

12import signal 

13import logging 

14import time 

15import re 

16import zlib 

17import struct 

18import pickle 

19 

20 

21from pyrocko.streaming import serial_hamster 

22from pyrocko.streaming import slink 

23from pyrocko.streaming import edl 

24from pyrocko.streaming import datacube 

25 

26from pyrocko import pile # noqa 

27from pyrocko import util # noqa 

28from pyrocko import model # noqa 

29from pyrocko import config # noqa 

30from pyrocko import io # noqa 

31 

32from . import pile_viewer # noqa 

33 

34from .qt_compat import qc, qg, qw, qn 

35 

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

37 

38 

39class _Getch: 

40 ''' 

41 Gets a single character from standard input. 

42 

43 Does not echo to the screen. 

44 

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

46 ''' 

47 def __init__(self): 

48 try: 

49 self.impl = _GetchWindows() 

50 except ImportError: 

51 self.impl = _GetchUnix() 

52 

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

54 

55 

56class _GetchUnix: 

57 def __init__(self): 

58 import tty, sys # noqa 

59 

60 def __call__(self): 

61 import sys 

62 import tty 

63 import termios 

64 

65 fd = sys.stdin.fileno() 

66 old_settings = termios.tcgetattr(fd) 

67 try: 

68 tty.setraw(fd) 

69 ch = sys.stdin.read(1) 

70 finally: 

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

72 

73 return ch 

74 

75 

76class _GetchWindows: 

77 def __init__(self): 

78 import msvcrt # noqa 

79 

80 def __call__(self): 

81 import msvcrt 

82 return msvcrt.getch() 

83 

84 

85getch = _Getch() 

86 

87 

88class AcquisitionThread(qc.QThread): 

89 def __init__(self, post_process_sleep=0.0): 

90 qc.QThread.__init__(self) 

91 self.mutex = qc.QMutex() 

92 self.queue = [] 

93 self.post_process_sleep = post_process_sleep 

94 self._sun_is_shining = True 

95 

96 def run(self): 

97 while True: 

98 try: 

99 self.acquisition_start() 

100 while self._sun_is_shining: 

101 t0 = time.time() 

102 self.process() 

103 t1 = time.time() 

104 if self.post_process_sleep != 0.0: 

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

106 

107 self.acquisition_stop() 

108 break 

109 

110 except ( 

111 edl.ReadError, 

112 serial_hamster.SerialHamsterError, 

113 slink.SlowSlinkError) as e: 

114 

115 logger.error(str(e)) 

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

117 self.acquisition_stop() 

118 time.sleep(5) 

119 if not self._sun_is_shining: 

120 break 

121 

122 def stop(self): 

123 self._sun_is_shining = False 

124 

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

126 self.wait() 

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

128 

129 def got_trace(self, tr): 

130 self.mutex.lock() 

131 self.queue.append(tr) 

132 self.mutex.unlock() 

133 

134 def poll(self): 

135 self.mutex.lock() 

136 items = self.queue[:] 

137 self.queue[:] = [] 

138 self.mutex.unlock() 

139 return items 

140 

141 

142class SlinkAcquisition( 

143 slink.SlowSlink, AcquisitionThread): 

144 

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

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

147 AcquisitionThread.__init__(self) 

148 

149 def got_trace(self, tr): 

150 AcquisitionThread.got_trace(self, tr) 

151 

152 

153class CamAcquisition( 

154 serial_hamster.CamSerialHamster, AcquisitionThread): 

155 

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

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

158 AcquisitionThread.__init__(self, post_process_sleep=0.1) 

159 

160 def got_trace(self, tr): 

161 AcquisitionThread.got_trace(self, tr) 

162 

163 

164class USBHB628Acquisition( 

165 serial_hamster.USBHB628Hamster, AcquisitionThread): 

166 

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

168 serial_hamster.USBHB628Hamster.__init__( 

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

170 AcquisitionThread.__init__(self) 

171 

172 def got_trace(self, tr): 

173 AcquisitionThread.got_trace(self, tr) 

174 

175 

176class SchoolSeismometerAcquisition( 

177 serial_hamster.SerialHamster, AcquisitionThread): 

178 

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

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

181 AcquisitionThread.__init__(self, post_process_sleep=0.01) 

182 

183 def got_trace(self, tr): 

184 AcquisitionThread.got_trace(self, tr) 

185 

186 

187class EDLAcquisition( 

188 edl.EDLHamster, AcquisitionThread): 

189 

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

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

192 AcquisitionThread.__init__(self) 

193 

194 def got_trace(self, tr): 

195 AcquisitionThread.got_trace(self, tr) 

196 

197 

198class CubeAcquisition( 

199 datacube.SerialCube, AcquisitionThread): 

200 

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

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

203 AcquisitionThread.__init__(self) 

204 

205 def got_trace(self, tr): 

206 AcquisitionThread.got_trace(self, tr) 

207 

208 

209def setup_acquisition_sources(args): 

210 

211 sources = [] 

212 iarg = 0 

213 while iarg < len(args): 

214 arg = args[iarg] 

215 

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

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

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

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

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

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

222 

223 if msl: 

224 host = msl.group(1) 

225 port = msl.group(3) 

226 if not port: 

227 port = '18000' 

228 

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

230 if msl.group(5): 

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

232 

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

234 try: 

235 streams = sl.query_streams() 

236 except slink.SlowSlinkError as e: 

237 logger.fatal(str(e)) 

238 sys.exit(1) 

239 

240 streams = list(set( 

241 util.match_nslcs(stream_patterns, streams))) 

242 

243 for stream in streams: 

244 sl.add_stream(*stream) 

245 else: 

246 for stream in stream_patterns: 

247 sl.add_raw_stream_selector(stream) 

248 

249 sources.append(sl) 

250 elif mca: 

251 port = mca.group(1) 

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

253 sources.append(cam) 

254 elif mus: 

255 port = mus.group(1) 

256 try: 

257 d = {} 

258 if mus.group(3): 

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

260 

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

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

263 hb628 = USBHB628Acquisition( 

264 port=port, 

265 deltat=deltat, 

266 channels=channels, 

267 buffersize=16, 

268 lookback=50) 

269 

270 sources.append(hb628) 

271 except Exception: 

272 raise 

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

274 

275 elif msc: 

276 port = msc.group(1) 

277 sco = SchoolSeismometerAcquisition(port=port) 

278 sources.append(sco) 

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

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

301 self._sources = [] 

302 self.startTimer(1000) 

303 

304 def add_source(self, source): 

305 self._sources.append(source) 

306 

307 def remove_source(self, source): 

308 self._sources.remove(source) 

309 

310 def timerEvent(self, ev): 

311 for source in self._sources: 

312 trs = source.poll() 

313 for tr in trs: 

314 self._injector.inject(tr) 

315 

316 # following methods needed because mulitple inheritance does not seem 

317 # to work anymore with QObject in Python3 or PyQt5 

318 

319 def set_fixation_length(self, length): 

320 return self._injector.set_fixation_length(length) 

321 

322 def set_save_path( 

323 self, 

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

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

326 

327 return self._injector.set_save_path(path) 

328 

329 def fixate_all(self): 

330 return self._injector.fixate_all() 

331 

332 def free(self): 

333 return self._injector.free() 

334 

335 

336class Connection(qc.QObject): 

337 

338 received = qc.pyqtSignal(object, object) 

339 disconnected = qc.pyqtSignal(object) 

340 

341 def __init__(self, parent, sock): 

342 qc.QObject.__init__(self, parent) 

343 self.socket = sock 

344 self.readyRead.connect( 

345 self.handle_read) 

346 self.disconnected.connect( 

347 self.handle_disconnected) 

348 self.nwanted = 8 

349 self.reading_size = True 

350 self.handler = None 

351 self.nbytes_received = 0 

352 self.nbytes_sent = 0 

353 self.compressor = zlib.compressobj() 

354 self.decompressor = zlib.decompressobj() 

355 

356 def handle_read(self): 

357 while True: 

358 navail = self.socket.bytesAvailable() 

359 if navail < self.nwanted: 

360 return 

361 

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

363 self.nbytes_received += len(data) 

364 if self.reading_size: 

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

366 self.reading_size = False 

367 else: 

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

369 if obj is None: 

370 self.socket.disconnectFromHost() 

371 else: 

372 self.handle_received(obj) 

373 self.nwanted = 8 

374 self.reading_size = True 

375 

376 def handle_received(self, obj): 

377 self.received.emit(self, obj) 

378 

379 def ship(self, obj): 

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

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

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

383 self.socket.write(data) 

384 self.socket.write(data_end) 

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

386 

387 def handle_disconnected(self): 

388 self.disconnected.emit(self) 

389 

390 def close(self): 

391 self.socket.close() 

392 

393 

394class ConnectionHandler(qc.QObject): 

395 def __init__(self, parent): 

396 qc.QObject.__init__(self, parent) 

397 self.queue = [] 

398 self.connection = None 

399 

400 def connected(self): 

401 return self.connection is None 

402 

403 def set_connection(self, connection): 

404 self.connection = connection 

405 connection.received.connect( 

406 self._handle_received) 

407 

408 connection.connect( 

409 self.handle_disconnected) 

410 

411 for obj in self.queue: 

412 self.connection.ship(obj) 

413 

414 self.queue = [] 

415 

416 def _handle_received(self, conn, obj): 

417 self.handle_received(obj) 

418 

419 def handle_received(self, obj): 

420 pass 

421 

422 def handle_disconnected(self): 

423 self.connection = None 

424 

425 def ship(self, obj): 

426 if self.connection: 

427 self.connection.ship(obj) 

428 else: 

429 self.queue.append(obj) 

430 

431 

432class SimpleConnectionHandler(ConnectionHandler): 

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

434 ConnectionHandler.__init__(self, parent) 

435 self.mapping = mapping 

436 

437 def handle_received(self, obj): 

438 command = obj[0] 

439 args = obj[1:] 

440 self.mapping[command](*args) 

441 

442 

443class MyMainWindow(qw.QMainWindow): 

444 

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

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

447 self.app = app 

448 

449 def keyPressEvent(self, ev): 

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

451 

452 

453class SnufflerTabs(qw.QTabWidget): 

454 def __init__(self, parent): 

455 qw.QTabWidget.__init__(self, parent) 

456 if hasattr(self, 'setTabsClosable'): 

457 self.setTabsClosable(True) 

458 

459 self.tabCloseRequested.connect( 

460 self.removeTab) 

461 

462 if hasattr(self, 'setDocumentMode'): 

463 self.setDocumentMode(True) 

464 

465 def hide_close_button_on_first_tab(self): 

466 tbar = self.tabBar() 

467 if hasattr(tbar, 'setTabButton'): 

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

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

470 

471 def append_tab(self, widget, name): 

472 widget.setParent(self) 

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

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

475 

476 def remove_tab(self, widget): 

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

478 

479 def tabInserted(self, index): 

480 if index == 0: 

481 self.hide_close_button_on_first_tab() 

482 

483 self.tabbar_visibility() 

484 self.setFocus() 

485 

486 def removeTab(self, index): 

487 w = self.widget(index) 

488 w.close() 

489 qw.QTabWidget.removeTab(self, index) 

490 

491 def tabRemoved(self, index): 

492 self.tabbar_visibility() 

493 

494 def tabbar_visibility(self): 

495 if self.count() <= 1: 

496 self.tabBar().hide() 

497 elif self.count() > 1: 

498 self.tabBar().show() 

499 

500 def keyPressEvent(self, event): 

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

502 i = self.currentIndex() 

503 if i != 0: 

504 self.tabCloseRequested.emit(i) 

505 else: 

506 self.parent().keyPressEvent(event) 

507 

508 

509class SnufflerStartWizard(qw.QWizard): 

510 

511 def __init__(self, parent): 

512 qw.QWizard.__init__(self, parent) 

513 

514 self.setOption(self.NoBackButtonOnStartPage) 

515 self.setOption(self.NoBackButtonOnLastPage) 

516 self.setOption(self.NoCancelButton) 

517 self.addPageSurvey() 

518 self.addPageHelp() 

519 self.setWindowTitle('Welcome to Pyrocko') 

520 

521 def getSystemInfo(self): 

522 import numpy 

523 import scipy 

524 import pyrocko 

525 import platform 

526 import uuid 

527 data = { 

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

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

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

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

532 'python': platform.python_version(), 

533 'pyrocko': pyrocko.__version__, 

534 'numpy': numpy.__version__, 

535 'scipy': scipy.__version__, 

536 'qt': qc.PYQT_VERSION_STR, 

537 } 

538 return data 

539 

540 def addPageSurvey(self): 

541 import pprint 

542 webtk = 'DSFGK234ADF4ASDF' 

543 sys_info = self.getSystemInfo() 

544 

545 p = qw.QWizardPage() 

546 p.setCommitPage(True) 

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

548 

549 lyt = qw.QVBoxLayout() 

550 lyt.addWidget(qw.QLabel( 

551 '<p>Your feedback is important for' 

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

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

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

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

556 

557 text_data = qw.QLabel( 

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

559 pprint.pformat( 

560 sys_info, 

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

562 ) 

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

564 lyt.addWidget(text_data) 

565 

566 lyt.addWidget(qw.QLabel( 

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

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

569 )) 

570 

571 p.setLayout(lyt) 

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

573 

574 yes_btn = qw.QPushButton(p) 

575 yes_btn.setText('Yes') 

576 

577 @qc.pyqtSlot() 

578 def send_data(): 

579 import requests 

580 import json 

581 try: 

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

583 data=json.dumps(sys_info)) 

584 except Exception as e: 

585 print(e) 

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

587 

588 self.customButtonClicked.connect(send_data) 

589 

590 self.setButton(self.CustomButton1, yes_btn) 

591 self.setOption(self.HaveCustomButton1, True) 

592 

593 self.addPage(p) 

594 return p 

595 

596 def addPageHelp(self): 

597 p = qw.QWizardPage() 

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

599 

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

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

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

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

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

605<ul> 

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

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

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

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

610 <li> 

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

612 </li> 

613</ul> 

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

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

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

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

618</p> 

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

620 align="center"> 

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

622</p> 

623</html>''') 

624 

625 lyt = qw.QVBoxLayout() 

626 lyt.addWidget(text) 

627 

628 def remove_custom_button(): 

629 self.setOption(self.HaveCustomButton1, False) 

630 

631 p.initializePage = remove_custom_button 

632 

633 p.setLayout(lyt) 

634 self.addPage(p) 

635 return p 

636 

637 

638class SnufflerWindow(qw.QMainWindow): 

639 

640 def __init__( 

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

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

643 opengl=None, instant_close=False): 

644 

645 qw.QMainWindow.__init__(self) 

646 

647 self.instant_close = instant_close 

648 

649 self.dockwidget_to_toggler = {} 

650 self.dockwidgets = [] 

651 

652 self.setWindowTitle("Snuffler") 

653 

654 self.pile_viewer = pile_viewer.PileViewer( 

655 pile, ntracks_shown_max=ntracks, use_opengl=opengl, 

656 marker_editor_sortable=marker_editor_sortable, 

657 panel_parent=self) 

658 

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

660 self.add_panel( 

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

662 where=qc.Qt.RightDockWidgetArea) 

663 if stations: 

664 self.get_view().add_stations(stations) 

665 

666 if events: 

667 self.get_view().add_events(events) 

668 

669 if len(events) == 1: 

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

671 

672 if markers: 

673 self.get_view().add_markers(markers) 

674 self.get_view().associate_phases_to_events() 

675 

676 self.tabs = SnufflerTabs(self) 

677 self.setCentralWidget(self.tabs) 

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

679 

680 self.pile_viewer.setup_snufflings() 

681 self.setMenuBar(self.pile_viewer.menu) 

682 

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

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

685 self.show() 

686 

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

688 

689 sb = self.statusBar() 

690 sb.clearMessage() 

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

692 

693 snuffler_config = self.pile_viewer.viewer.config 

694 

695 if snuffler_config.first_start: 

696 wizard = SnufflerStartWizard(self) 

697 

698 @qc.pyqtSlot() 

699 def wizard_finished(result): 

700 if result == wizard.Accepted: 

701 snuffler_config.first_start = False 

702 config.write_config(snuffler_config, 'snuffler') 

703 

704 wizard.finished.connect(wizard_finished) 

705 

706 wizard.show() 

707 

708 if follow: 

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

710 

711 self.closing = False 

712 

713 def sizeHint(self): 

714 return qc.QSize(1024, 768) 

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

716 

717 def keyPressEvent(self, ev): 

718 self.get_view().keyPressEvent(ev) 

719 

720 def get_view(self): 

721 return self.pile_viewer.get_view() 

722 

723 def get_panel_parent_widget(self): 

724 return self 

725 

726 def add_tab(self, name, widget): 

727 self.tabs.append_tab(widget, name) 

728 

729 def remove_tab(self, widget): 

730 self.tabs.remove_tab(widget) 

731 

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

733 where=qc.Qt.BottomDockWidgetArea): 

734 

735 if not self.dockwidgets: 

736 self.dockwidgets = [] 

737 

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

739 

740 dockwidget = qw.QDockWidget(name, self) 

741 self.dockwidgets.append(dockwidget) 

742 dockwidget.setWidget(panel) 

743 panel.setParent(dockwidget) 

744 self.addDockWidget(where, dockwidget) 

745 

746 if dws: 

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

748 

749 self.toggle_panel(dockwidget, visible) 

750 

751 mitem = qw.QAction(name, None) 

752 

753 def toggle_panel(checked): 

754 self.toggle_panel(dockwidget, True) 

755 

756 mitem.triggered.connect(toggle_panel) 

757 

758 if volatile: 

759 def visibility(visible): 

760 if not visible: 

761 self.remove_panel(panel) 

762 

763 dockwidget.visibilityChanged.connect( 

764 visibility) 

765 

766 self.get_view().add_panel_toggler(mitem) 

767 self.dockwidget_to_toggler[dockwidget] = mitem 

768 

769 if pile_viewer.is_macos: 

770 tabbars = self.findChildren(qw.QTabBar) 

771 for tabbar in tabbars: 

772 tabbar.setShape(qw.QTabBar.TriangularNorth) 

773 tabbar.setDocumentMode(True) 

774 

775 def toggle_panel(self, dockwidget, visible): 

776 if visible is None: 

777 visible = not dockwidget.isVisible() 

778 

779 dockwidget.setVisible(visible) 

780 if visible: 

781 w = dockwidget.widget() 

782 minsize = w.minimumSize() 

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

784 

785 def reset_minimum_size(): 

786 import sip 

787 if not sip.isdeleted(w): 

788 w.setMinimumSize(minsize) 

789 

790 qc.QTimer.singleShot(200, reset_minimum_size) 

791 

792 dockwidget.setFocus() 

793 dockwidget.raise_() 

794 

795 def toggle_marker_editor(self): 

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

797 

798 def toggle_main_controls(self): 

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

800 

801 def remove_panel(self, panel): 

802 dockwidget = panel.parent() 

803 self.removeDockWidget(dockwidget) 

804 dockwidget.setParent(None) 

805 mitem = self.dockwidget_to_toggler[dockwidget] 

806 self.get_view().remove_panel_toggler(mitem) 

807 

808 def return_tag(self): 

809 return self.get_view().return_tag 

810 

811 def confirm_close(self): 

812 ret = qw.QMessageBox.question( 

813 self, 

814 'Snuffler', 

815 'Close Snuffler window?', 

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

817 qw.QMessageBox.Ok) 

818 

819 return ret == qw.QMessageBox.Ok 

820 

821 def closeEvent(self, event): 

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

823 self.closing = True 

824 self.pile_viewer.cleanup() 

825 event.accept() 

826 else: 

827 event.ignore() 

828 

829 def is_closing(self): 

830 return self.closing 

831 

832 

833class Snuffler(qw.QApplication): 

834 

835 def __init__(self): 

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

837 self.setApplicationName('Snuffler') 

838 self.setApplicationDisplayName('Snuffler') 

839 self.lastWindowClosed.connect(self.myQuit) 

840 self.server = None 

841 self.loader = None 

842 

843 def install_sigint_handler(self): 

844 self._old_signal_handler = signal.signal( 

845 signal.SIGINT, 

846 self.myCloseAllWindows) 

847 

848 def uninstall_sigint_handler(self): 

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

850 

851 def start_server(self): 

852 self.connections = [] 

853 s = qn.QTcpServer(self) 

854 s.listen(qn.QHostAddress.LocalHost) 

855 s.newConnection.connect( 

856 self.handle_accept) 

857 self.server = s 

858 

859 def start_loader(self): 

860 self.loader = SimpleConnectionHandler( 

861 self, 

862 add_files=self.add_files, 

863 update_progress=self.update_progress) 

864 ticket = os.urandom(32) 

865 self.forker.spawn('loader', self.server.serverPort(), ticket) 

866 self.connection_handlers[ticket] = self.loader 

867 

868 def handle_accept(self): 

869 sock = self.server.nextPendingConnection() 

870 con = Connection(self, sock) 

871 self.connections.append(con) 

872 

873 con.disconnected.connect( 

874 self.handle_disconnected) 

875 

876 con.received.connect( 

877 self.handle_received_ticket) 

878 

879 def handle_disconnected(self, connection): 

880 self.connections.remove(connection) 

881 connection.close() 

882 del connection 

883 

884 def handle_received_ticket(self, connection, object): 

885 if not isinstance(object, str): 

886 self.handle_disconnected(connection) 

887 

888 ticket = object 

889 if ticket in self.connection_handlers: 

890 h = self.connection_handlers[ticket] 

891 connection.received.disconnect( 

892 self.handle_received_ticket) 

893 

894 h.set_connection(connection) 

895 else: 

896 self.handle_disconnected(connection) 

897 

898 def snuffler_windows(self): 

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

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

901 

902 def event(self, e): 

903 if isinstance(e, qg.QFileOpenEvent): 

904 paths = [str(e.file())] 

905 wins = self.snuffler_windows() 

906 if wins: 

907 wins[0].get_view().load_soon(paths) 

908 

909 return True 

910 else: 

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

912 

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

914 if not self.loader: 

915 self.start_loader() 

916 

917 self.loader.ship( 

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

919 

920 def add_files(self, files): 

921 p = self.pile_viewer.get_pile() 

922 p.add_files(files) 

923 self.pile_viewer.update_contents() 

924 

925 def update_progress(self, task, percent): 

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

927 

928 def myCloseAllWindows(self, *args): 

929 

930 def confirm(): 

931 try: 

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

933 confirmed = getch() == 'y' 

934 if not confirmed: 

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

936 else: 

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

938 

939 return confirmed 

940 

941 except Exception: 

942 return False 

943 

944 if confirm(): 

945 for win in self.snuffler_windows(): 

946 win.instant_close = True 

947 

948 self.closeAllWindows() 

949 

950 def myQuit(self, *args): 

951 self.quit()