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 AcquisitionThread(qc.QThread): 

40 def __init__(self, post_process_sleep=0.0): 

41 qc.QThread.__init__(self) 

42 self.mutex = qc.QMutex() 

43 self.queue = [] 

44 self.post_process_sleep = post_process_sleep 

45 self._sun_is_shining = True 

46 

47 def run(self): 

48 while True: 

49 try: 

50 self.acquisition_start() 

51 while self._sun_is_shining: 

52 t0 = time.time() 

53 self.process() 

54 t1 = time.time() 

55 if self.post_process_sleep != 0.0: 

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

57 

58 self.acquisition_stop() 

59 break 

60 

61 except ( 

62 edl.ReadError, 

63 serial_hamster.SerialHamsterError, 

64 slink.SlowSlinkError) as e: 

65 

66 logger.error(str(e)) 

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

68 self.acquisition_stop() 

69 time.sleep(5) 

70 if not self._sun_is_shining: 

71 break 

72 

73 def stop(self): 

74 self._sun_is_shining = False 

75 

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

77 self.wait() 

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

79 

80 def got_trace(self, tr): 

81 self.mutex.lock() 

82 self.queue.append(tr) 

83 self.mutex.unlock() 

84 

85 def poll(self): 

86 self.mutex.lock() 

87 items = self.queue[:] 

88 self.queue[:] = [] 

89 self.mutex.unlock() 

90 return items 

91 

92 

93class SlinkAcquisition( 

94 slink.SlowSlink, AcquisitionThread): 

95 

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

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

98 AcquisitionThread.__init__(self) 

99 

100 def got_trace(self, tr): 

101 AcquisitionThread.got_trace(self, tr) 

102 

103 

104class CamAcquisition( 

105 serial_hamster.CamSerialHamster, AcquisitionThread): 

106 

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

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

109 AcquisitionThread.__init__(self, post_process_sleep=0.1) 

110 

111 def got_trace(self, tr): 

112 AcquisitionThread.got_trace(self, tr) 

113 

114 

115class USBHB628Acquisition( 

116 serial_hamster.USBHB628Hamster, AcquisitionThread): 

117 

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

119 serial_hamster.USBHB628Hamster.__init__( 

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

121 AcquisitionThread.__init__(self) 

122 

123 def got_trace(self, tr): 

124 AcquisitionThread.got_trace(self, tr) 

125 

126 

127class SchoolSeismometerAcquisition( 

128 serial_hamster.SerialHamster, AcquisitionThread): 

129 

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

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

132 AcquisitionThread.__init__(self, post_process_sleep=0.01) 

133 

134 def got_trace(self, tr): 

135 AcquisitionThread.got_trace(self, tr) 

136 

137 

138class EDLAcquisition( 

139 edl.EDLHamster, AcquisitionThread): 

140 

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

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

143 AcquisitionThread.__init__(self) 

144 

145 def got_trace(self, tr): 

146 AcquisitionThread.got_trace(self, tr) 

147 

148 

149class CubeAcquisition( 

150 datacube.SerialCube, AcquisitionThread): 

151 

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

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

154 AcquisitionThread.__init__(self) 

155 

156 def got_trace(self, tr): 

157 AcquisitionThread.got_trace(self, tr) 

158 

159 

160def setup_acquisition_sources(args): 

161 

162 sources = [] 

163 iarg = 0 

164 while iarg < len(args): 

165 arg = args[iarg] 

166 

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

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

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

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

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

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

173 

174 if msl: 

175 host = msl.group(1) 

176 port = msl.group(3) 

177 if not port: 

178 port = '18000' 

179 

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

181 if msl.group(5): 

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

183 

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

185 try: 

186 streams = sl.query_streams() 

187 except slink.SlowSlinkError as e: 

188 logger.fatal(str(e)) 

189 sys.exit(1) 

190 

191 streams = list(set( 

192 util.match_nslcs(stream_patterns, streams))) 

193 

194 for stream in streams: 

195 sl.add_stream(*stream) 

196 else: 

197 for stream in stream_patterns: 

198 sl.add_raw_stream_selector(stream) 

199 

200 sources.append(sl) 

201 elif mca: 

202 port = mca.group(1) 

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

204 sources.append(cam) 

205 elif mus: 

206 port = mus.group(1) 

207 try: 

208 d = {} 

209 if mus.group(3): 

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

211 

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

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

214 hb628 = USBHB628Acquisition( 

215 port=port, 

216 deltat=deltat, 

217 channels=channels, 

218 buffersize=16, 

219 lookback=50) 

220 

221 sources.append(hb628) 

222 except Exception: 

223 raise 

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

225 

226 elif msc: 

227 port = msc.group(1) 

228 sco = SchoolSeismometerAcquisition(port=port) 

229 sources.append(sco) 

230 elif med: 

231 port = med.group(1) 

232 edl = EDLAcquisition(port=port) 

233 sources.append(edl) 

234 elif mcu: 

235 device = mcu.group(1) 

236 cube = CubeAcquisition(device=device) 

237 sources.append(cube) 

238 

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

240 args.pop(iarg) 

241 else: 

242 iarg += 1 

243 

244 return sources 

245 

246 

247class PollInjector(qc.QObject): 

248 

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

250 qc.QObject.__init__(self) 

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

252 self._sources = [] 

253 self.startTimer(1000) 

254 

255 def add_source(self, source): 

256 self._sources.append(source) 

257 

258 def remove_source(self, source): 

259 self._sources.remove(source) 

260 

261 def timerEvent(self, ev): 

262 for source in self._sources: 

263 trs = source.poll() 

264 for tr in trs: 

265 self._injector.inject(tr) 

266 

267 # following methods needed because mulitple inheritance does not seem 

268 # to work anymore with QObject in Python3 or PyQt5 

269 

270 def set_fixation_length(self, length): 

271 return self._injector.set_fixation_length(length) 

272 

273 def set_save_path( 

274 self, 

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

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

277 

278 return self._injector.set_save_path(path) 

279 

280 def fixate_all(self): 

281 return self._injector.fixate_all() 

282 

283 def free(self): 

284 return self._injector.free() 

285 

286 

287class Connection(qc.QObject): 

288 

289 received = qc.pyqtSignal(object, object) 

290 disconnected = qc.pyqtSignal(object) 

291 

292 def __init__(self, parent, sock): 

293 qc.QObject.__init__(self, parent) 

294 self.socket = sock 

295 self.readyRead.connect( 

296 self.handle_read) 

297 self.disconnected.connect( 

298 self.handle_disconnected) 

299 self.nwanted = 8 

300 self.reading_size = True 

301 self.handler = None 

302 self.nbytes_received = 0 

303 self.nbytes_sent = 0 

304 self.compressor = zlib.compressobj() 

305 self.decompressor = zlib.decompressobj() 

306 

307 def handle_read(self): 

308 while True: 

309 navail = self.socket.bytesAvailable() 

310 if navail < self.nwanted: 

311 return 

312 

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

314 self.nbytes_received += len(data) 

315 if self.reading_size: 

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

317 self.reading_size = False 

318 else: 

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

320 if obj is None: 

321 self.socket.disconnectFromHost() 

322 else: 

323 self.handle_received(obj) 

324 self.nwanted = 8 

325 self.reading_size = True 

326 

327 def handle_received(self, obj): 

328 self.received.emit(self, obj) 

329 

330 def ship(self, obj): 

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

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

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

334 self.socket.write(data) 

335 self.socket.write(data_end) 

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

337 

338 def handle_disconnected(self): 

339 self.disconnected.emit(self) 

340 

341 def close(self): 

342 self.socket.close() 

343 

344 

345class ConnectionHandler(qc.QObject): 

346 def __init__(self, parent): 

347 qc.QObject.__init__(self, parent) 

348 self.queue = [] 

349 self.connection = None 

350 

351 def connected(self): 

352 return self.connection is None 

353 

354 def set_connection(self, connection): 

355 self.connection = connection 

356 connection.received.connect( 

357 self._handle_received) 

358 

359 connection.connect( 

360 self.handle_disconnected) 

361 

362 for obj in self.queue: 

363 self.connection.ship(obj) 

364 

365 self.queue = [] 

366 

367 def _handle_received(self, conn, obj): 

368 self.handle_received(obj) 

369 

370 def handle_received(self, obj): 

371 pass 

372 

373 def handle_disconnected(self): 

374 self.connection = None 

375 

376 def ship(self, obj): 

377 if self.connection: 

378 self.connection.ship(obj) 

379 else: 

380 self.queue.append(obj) 

381 

382 

383class SimpleConnectionHandler(ConnectionHandler): 

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

385 ConnectionHandler.__init__(self, parent) 

386 self.mapping = mapping 

387 

388 def handle_received(self, obj): 

389 command = obj[0] 

390 args = obj[1:] 

391 self.mapping[command](*args) 

392 

393 

394class MyMainWindow(qw.QMainWindow): 

395 

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

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

398 self.app = app 

399 

400 def keyPressEvent(self, ev): 

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

402 

403 

404class SnufflerTabs(qw.QTabWidget): 

405 def __init__(self, parent): 

406 qw.QTabWidget.__init__(self, parent) 

407 if hasattr(self, 'setTabsClosable'): 

408 self.setTabsClosable(True) 

409 

410 self.tabCloseRequested.connect( 

411 self.removeTab) 

412 

413 if hasattr(self, 'setDocumentMode'): 

414 self.setDocumentMode(True) 

415 

416 def hide_close_button_on_first_tab(self): 

417 tbar = self.tabBar() 

418 if hasattr(tbar, 'setTabButton'): 

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

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

421 

422 def append_tab(self, widget, name): 

423 widget.setParent(self) 

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

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

426 

427 def remove_tab(self, widget): 

428 self.removeTab(self.indexOf(widget)) 

429 

430 def tabInserted(self, index): 

431 if index == 0: 

432 self.hide_close_button_on_first_tab() 

433 

434 self.tabbar_visibility() 

435 self.setFocus() 

436 

437 def removeTab(self, index): 

438 w = self.widget(index) 

439 w.close() 

440 qw.QTabWidget.removeTab(self, index) 

441 

442 def tabRemoved(self, index): 

443 self.tabbar_visibility() 

444 

445 def tabbar_visibility(self): 

446 if self.count() <= 1: 

447 self.tabBar().hide() 

448 elif self.count() > 1: 

449 self.tabBar().show() 

450 

451 def keyPressEvent(self, event): 

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

453 i = self.currentIndex() 

454 if i != 0: 

455 self.tabCloseRequested.emit(i) 

456 else: 

457 self.parent().keyPressEvent(event) 

458 

459 

460class SnufflerStartWizard(qw.QWizard): 

461 

462 def __init__(self, parent): 

463 qw.QWizard.__init__(self, parent) 

464 

465 self.setOption(self.NoBackButtonOnStartPage) 

466 self.setOption(self.NoBackButtonOnLastPage) 

467 self.setOption(self.NoCancelButton) 

468 self.addPageSurvey() 

469 self.addPageHelp() 

470 self.setWindowTitle('Welcome to Pyrocko') 

471 

472 def getSystemInfo(self): 

473 import numpy 

474 import scipy 

475 import pyrocko 

476 import platform 

477 import uuid 

478 data = { 

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

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

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

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

483 'python': platform.python_version(), 

484 'pyrocko': pyrocko.__version__, 

485 'numpy': numpy.__version__, 

486 'scipy': scipy.__version__, 

487 'qt': qc.PYQT_VERSION_STR, 

488 } 

489 return data 

490 

491 def addPageSurvey(self): 

492 import pprint 

493 webtk = 'DSFGK234ADF4ASDF' 

494 sys_info = self.getSystemInfo() 

495 

496 p = qw.QWizardPage() 

497 p.setCommitPage(True) 

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

499 

500 lyt = qw.QVBoxLayout() 

501 lyt.addWidget(qw.QLabel( 

502 '<p>Your feedback is important for' 

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

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

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

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

507 

508 text_data = qw.QLabel( 

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

510 pprint.pformat( 

511 sys_info, 

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

513 ) 

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

515 lyt.addWidget(text_data) 

516 

517 lyt.addWidget(qw.QLabel( 

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

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

520 )) 

521 

522 p.setLayout(lyt) 

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

524 

525 yes_btn = qw.QPushButton(p) 

526 yes_btn.setText('Yes') 

527 

528 @qc.pyqtSlot() 

529 def send_data(): 

530 import requests 

531 import json 

532 try: 

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

534 data=json.dumps(sys_info)) 

535 except Exception as e: 

536 print(e) 

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

538 

539 self.customButtonClicked.connect(send_data) 

540 

541 self.setButton(self.CustomButton1, yes_btn) 

542 self.setOption(self.HaveCustomButton1, True) 

543 

544 self.addPage(p) 

545 return p 

546 

547 def addPageHelp(self): 

548 p = qw.QWizardPage() 

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

550 

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

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

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

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

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

556<ul> 

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

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

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

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

561 <li> 

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

563 </li> 

564</ul> 

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

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

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

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

569</p> 

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

571 align="center"> 

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

573</p> 

574</html>''') 

575 

576 lyt = qw.QVBoxLayout() 

577 lyt.addWidget(text) 

578 

579 def remove_custom_button(): 

580 self.setOption(self.HaveCustomButton1, False) 

581 

582 p.initializePage = remove_custom_button 

583 

584 p.setLayout(lyt) 

585 self.addPage(p) 

586 return p 

587 

588 

589class SnufflerWindow(qw.QMainWindow): 

590 

591 def __init__( 

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

593 marker_editor_sortable=True, follow=None, controls=True, 

594 opengl=None, instant_close=False): 

595 

596 qw.QMainWindow.__init__(self) 

597 

598 self.instant_close = instant_close 

599 

600 self.dockwidget_to_toggler = {} 

601 self.dockwidgets = [] 

602 

603 self.setWindowTitle("Snuffler") 

604 

605 self.pile_viewer = pile_viewer.PileViewer( 

606 pile, ntracks_shown_max=ntracks, use_opengl=opengl, 

607 marker_editor_sortable=marker_editor_sortable, 

608 panel_parent=self) 

609 

610 self.marker_editor = self.pile_viewer.marker_editor() 

611 self.add_panel( 

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

613 where=qc.Qt.RightDockWidgetArea) 

614 if stations: 

615 self.get_view().add_stations(stations) 

616 

617 if events: 

618 self.get_view().add_events(events) 

619 

620 if len(events) == 1: 

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

622 

623 if markers: 

624 self.get_view().add_markers(markers) 

625 self.get_view().associate_phases_to_events() 

626 

627 self.tabs = SnufflerTabs(self) 

628 self.setCentralWidget(self.tabs) 

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

630 

631 self.pile_viewer.setup_snufflings() 

632 self.setMenuBar(self.pile_viewer.menu) 

633 

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

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

636 self.show() 

637 

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

639 

640 sb = self.statusBar() 

641 sb.clearMessage() 

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

643 

644 snuffler_config = self.pile_viewer.viewer.config 

645 

646 if snuffler_config.first_start: 

647 wizard = SnufflerStartWizard(self) 

648 

649 @qc.pyqtSlot() 

650 def wizard_finished(result): 

651 if result == wizard.Accepted: 

652 snuffler_config.first_start = False 

653 config.write_config(snuffler_config, 'snuffler') 

654 

655 wizard.finished.connect(wizard_finished) 

656 

657 wizard.show() 

658 

659 if follow: 

660 self.get_view().follow(float(follow)) 

661 

662 self.closing = False 

663 

664 def sizeHint(self): 

665 return qc.QSize(1024, 768) 

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

667 

668 def keyPressEvent(self, ev): 

669 self.get_view().keyPressEvent(ev) 

670 

671 def get_view(self): 

672 return self.pile_viewer.get_view() 

673 

674 def get_panel_parent_widget(self): 

675 return self 

676 

677 def add_tab(self, name, widget): 

678 self.tabs.append_tab(widget, name) 

679 

680 def remove_tab(self, widget): 

681 self.tabs.remove_tab(widget) 

682 

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

684 where=qc.Qt.BottomDockWidgetArea): 

685 

686 if not self.dockwidgets: 

687 self.dockwidgets = [] 

688 

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

690 

691 dockwidget = qw.QDockWidget(name, self) 

692 self.dockwidgets.append(dockwidget) 

693 dockwidget.setWidget(panel) 

694 panel.setParent(dockwidget) 

695 self.addDockWidget(where, dockwidget) 

696 

697 if dws: 

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

699 

700 self.toggle_panel(dockwidget, visible) 

701 

702 mitem = qw.QAction(name, None) 

703 

704 def toggle_panel(checked): 

705 self.toggle_panel(dockwidget, True) 

706 

707 mitem.triggered.connect(toggle_panel) 

708 

709 if volatile: 

710 def visibility(visible): 

711 if not visible: 

712 self.remove_panel(panel) 

713 

714 dockwidget.visibilityChanged.connect( 

715 visibility) 

716 

717 self.get_view().add_panel_toggler(mitem) 

718 self.dockwidget_to_toggler[dockwidget] = mitem 

719 

720 def toggle_panel(self, dockwidget, visible): 

721 if visible is None: 

722 visible = not dockwidget.isVisible() 

723 

724 dockwidget.setVisible(visible) 

725 if visible: 

726 w = dockwidget.widget() 

727 minsize = w.minimumSize() 

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

729 

730 def reset_minimum_size(): 

731 import sip 

732 if not sip.isdeleted(w): 

733 w.setMinimumSize(minsize) 

734 

735 qc.QTimer.singleShot(200, reset_minimum_size) 

736 

737 dockwidget.setFocus() 

738 dockwidget.raise_() 

739 

740 def toggle_marker_editor(self): 

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

742 

743 def toggle_main_controls(self): 

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

745 

746 def remove_panel(self, panel): 

747 dockwidget = panel.parent() 

748 self.removeDockWidget(dockwidget) 

749 dockwidget.setParent(None) 

750 mitem = self.dockwidget_to_toggler[dockwidget] 

751 self.get_view().remove_panel_toggler(mitem) 

752 

753 def return_tag(self): 

754 return self.get_view().return_tag 

755 

756 def confirm_close(self): 

757 ret = qw.QMessageBox.question( 

758 self, 

759 'Snuffler', 

760 'Close Snuffler window?', 

761 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

762 qw.QMessageBox.Ok) 

763 

764 return ret == qw.QMessageBox.Ok 

765 

766 def closeEvent(self, event): 

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

768 self.closing = True 

769 self.pile_viewer.cleanup() 

770 event.accept() 

771 else: 

772 event.ignore() 

773 

774 def is_closing(self): 

775 return self.closing 

776 

777 

778class Snuffler(qw.QApplication): 

779 

780 def __init__(self): 

781 qw.QApplication.__init__(self, sys.argv) 

782 self.lastWindowClosed.connect(self.myQuit) 

783 self.server = None 

784 self.loader = None 

785 

786 def install_sigint_handler(self): 

787 self._old_signal_handler = signal.signal( 

788 signal.SIGINT, 

789 self.myCloseAllWindows) 

790 

791 def uninstall_sigint_handler(self): 

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

793 

794 def start_server(self): 

795 self.connections = [] 

796 s = qn.QTcpServer(self) 

797 s.listen(qn.QHostAddress.LocalHost) 

798 s.newConnection.connect( 

799 self.handle_accept) 

800 self.server = s 

801 

802 def start_loader(self): 

803 self.loader = SimpleConnectionHandler( 

804 self, 

805 add_files=self.add_files, 

806 update_progress=self.update_progress) 

807 ticket = os.urandom(32) 

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

809 self.connection_handlers[ticket] = self.loader 

810 

811 def handle_accept(self): 

812 sock = self.server.nextPendingConnection() 

813 con = Connection(self, sock) 

814 self.connections.append(con) 

815 

816 con.disconnected.connect( 

817 self.handle_disconnected) 

818 

819 con.received.connect( 

820 self.handle_received_ticket) 

821 

822 def handle_disconnected(self, connection): 

823 self.connections.remove(connection) 

824 connection.close() 

825 del connection 

826 

827 def handle_received_ticket(self, connection, object): 

828 if not isinstance(object, str): 

829 self.handle_disconnected(connection) 

830 

831 ticket = object 

832 if ticket in self.connection_handlers: 

833 h = self.connection_handlers[ticket] 

834 connection.received.disconnect( 

835 self.handle_received_ticket) 

836 

837 h.set_connection(connection) 

838 else: 

839 self.handle_disconnected(connection) 

840 

841 def snuffler_windows(self): 

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

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

844 

845 def event(self, e): 

846 if isinstance(e, qg.QFileOpenEvent): 

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

848 wins = self.snuffler_windows() 

849 if wins: 

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

851 

852 return True 

853 else: 

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

855 

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

857 if not self.loader: 

858 self.start_loader() 

859 

860 self.loader.ship( 

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

862 

863 def add_files(self, files): 

864 p = self.pile_viewer.get_pile() 

865 p.add_files(files) 

866 self.pile_viewer.update_contents() 

867 

868 def update_progress(self, task, percent): 

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

870 

871 def myCloseAllWindows(self, *args): 

872 self.closeAllWindows() 

873 

874 def myQuit(self, *args): 

875 self.quit()