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 

24 

25from pyrocko import pile # noqa 

26from pyrocko import util # noqa 

27from pyrocko import model # noqa 

28from pyrocko import config # noqa 

29from pyrocko import io # noqa 

30 

31from . import pile_viewer # noqa 

32 

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

34 

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

36 

37 

38class AcquisitionThread(qc.QThread): 

39 def __init__(self, post_process_sleep=0.0): 

40 qc.QThread.__init__(self) 

41 self.mutex = qc.QMutex() 

42 self.queue = [] 

43 self.post_process_sleep = post_process_sleep 

44 self._sun_is_shining = True 

45 

46 def run(self): 

47 while True: 

48 try: 

49 self.acquisition_start() 

50 while self._sun_is_shining: 

51 t0 = time.time() 

52 self.process() 

53 t1 = time.time() 

54 if self.post_process_sleep != 0.0: 

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

56 

57 self.acquisition_stop() 

58 break 

59 

60 except ( 

61 edl.ReadError, 

62 serial_hamster.SerialHamsterError, 

63 slink.SlowSlinkError) as e: 

64 

65 logger.error(str(e)) 

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

67 self.acquisition_stop() 

68 time.sleep(5) 

69 if not self._sun_is_shining: 

70 break 

71 

72 def stop(self): 

73 self._sun_is_shining = False 

74 

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

76 self.wait() 

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

78 

79 def got_trace(self, tr): 

80 self.mutex.lock() 

81 self.queue.append(tr) 

82 self.mutex.unlock() 

83 

84 def poll(self): 

85 self.mutex.lock() 

86 items = self.queue[:] 

87 self.queue[:] = [] 

88 self.mutex.unlock() 

89 return items 

90 

91 

92class SlinkAcquisition( 

93 slink.SlowSlink, AcquisitionThread): 

94 

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

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

97 AcquisitionThread.__init__(self) 

98 

99 def got_trace(self, tr): 

100 AcquisitionThread.got_trace(self, tr) 

101 

102 

103class CamAcquisition( 

104 serial_hamster.CamSerialHamster, AcquisitionThread): 

105 

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

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

108 AcquisitionThread.__init__(self, post_process_sleep=0.1) 

109 

110 def got_trace(self, tr): 

111 AcquisitionThread.got_trace(self, tr) 

112 

113 

114class USBHB628Acquisition( 

115 serial_hamster.USBHB628Hamster, AcquisitionThread): 

116 

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

118 serial_hamster.USBHB628Hamster.__init__( 

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

120 AcquisitionThread.__init__(self) 

121 

122 def got_trace(self, tr): 

123 AcquisitionThread.got_trace(self, tr) 

124 

125 

126class SchoolSeismometerAcquisition( 

127 serial_hamster.SerialHamster, AcquisitionThread): 

128 

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

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

131 AcquisitionThread.__init__(self, post_process_sleep=0.01) 

132 

133 def got_trace(self, tr): 

134 AcquisitionThread.got_trace(self, tr) 

135 

136 

137class EDLAcquisition( 

138 edl.EDLHamster, AcquisitionThread): 

139 

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

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

142 AcquisitionThread.__init__(self) 

143 

144 def got_trace(self, tr): 

145 AcquisitionThread.got_trace(self, tr) 

146 

147 

148def setup_acquisition_sources(args): 

149 

150 sources = [] 

151 iarg = 0 

152 while iarg < len(args): 

153 arg = args[iarg] 

154 

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

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

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

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

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

160 if msl: 

161 host = msl.group(1) 

162 port = msl.group(3) 

163 if not port: 

164 port = '18000' 

165 

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

167 if msl.group(5): 

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

169 

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

171 try: 

172 streams = sl.query_streams() 

173 except slink.SlowSlinkError as e: 

174 logger.fatal(str(e)) 

175 sys.exit(1) 

176 

177 streams = list(set( 

178 util.match_nslcs(stream_patterns, streams))) 

179 

180 for stream in streams: 

181 sl.add_stream(*stream) 

182 else: 

183 for stream in stream_patterns: 

184 sl.add_raw_stream_selector(stream) 

185 

186 sources.append(sl) 

187 elif mca: 

188 port = mca.group(1) 

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

190 sources.append(cam) 

191 elif mus: 

192 port = mus.group(1) 

193 try: 

194 d = {} 

195 if mus.group(3): 

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

197 

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

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

200 hb628 = USBHB628Acquisition( 

201 port=port, 

202 deltat=deltat, 

203 channels=channels, 

204 buffersize=16, 

205 lookback=50) 

206 

207 sources.append(hb628) 

208 except Exception: 

209 raise 

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

211 

212 elif msc: 

213 port = msc.group(1) 

214 sco = SchoolSeismometerAcquisition(port=port) 

215 sources.append(sco) 

216 elif med: 

217 port = med.group(1) 

218 edl = EDLAcquisition(port=port) 

219 sources.append(edl) 

220 

221 if msl or mca or mus or msc or med: 

222 args.pop(iarg) 

223 else: 

224 iarg += 1 

225 

226 return sources 

227 

228 

229class PollInjector(qc.QObject): 

230 

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

232 qc.QObject.__init__(self) 

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

234 self._sources = [] 

235 self.startTimer(1000.) 

236 

237 def add_source(self, source): 

238 self._sources.append(source) 

239 

240 def remove_source(self, source): 

241 self._sources.remove(source) 

242 

243 def timerEvent(self, ev): 

244 for source in self._sources: 

245 trs = source.poll() 

246 for tr in trs: 

247 self._injector.inject(tr) 

248 

249 # following methods needed because mulitple inheritance does not seem 

250 # to work anymore with QObject in Python3 or PyQt5 

251 

252 def set_fixation_length(self, length): 

253 return self._injector.set_fixation_length(length) 

254 

255 def set_save_path( 

256 self, 

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

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

259 

260 return self._injector.set_save_path(path) 

261 

262 def fixate_all(self): 

263 return self._injector.fixate_all() 

264 

265 def free(self): 

266 return self._injector.free() 

267 

268 

269class Connection(qc.QObject): 

270 

271 received = qc.pyqtSignal(object, object) 

272 disconnected = qc.pyqtSignal(object) 

273 

274 def __init__(self, parent, sock): 

275 qc.QObject.__init__(self, parent) 

276 self.socket = sock 

277 self.readyRead.connect( 

278 self.handle_read) 

279 self.disconnected.connect( 

280 self.handle_disconnected) 

281 self.nwanted = 8 

282 self.reading_size = True 

283 self.handler = None 

284 self.nbytes_received = 0 

285 self.nbytes_sent = 0 

286 self.compressor = zlib.compressobj() 

287 self.decompressor = zlib.decompressobj() 

288 

289 def handle_read(self): 

290 while True: 

291 navail = self.socket.bytesAvailable() 

292 if navail < self.nwanted: 

293 return 

294 

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

296 self.nbytes_received += len(data) 

297 if self.reading_size: 

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

299 self.reading_size = False 

300 else: 

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

302 if obj is None: 

303 self.socket.disconnectFromHost() 

304 else: 

305 self.handle_received(obj) 

306 self.nwanted = 8 

307 self.reading_size = True 

308 

309 def handle_received(self, obj): 

310 self.received.emit(self, obj) 

311 

312 def ship(self, obj): 

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

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

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

316 self.socket.write(data) 

317 self.socket.write(data_end) 

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

319 

320 def handle_disconnected(self): 

321 self.disconnected.emit(self) 

322 

323 def close(self): 

324 self.socket.close() 

325 

326 

327class ConnectionHandler(qc.QObject): 

328 def __init__(self, parent): 

329 qc.QObject.__init__(self, parent) 

330 self.queue = [] 

331 self.connection = None 

332 

333 def connected(self): 

334 return self.connection is None 

335 

336 def set_connection(self, connection): 

337 self.connection = connection 

338 connection.received.connect( 

339 self._handle_received) 

340 

341 connection.connect( 

342 self.handle_disconnected) 

343 

344 for obj in self.queue: 

345 self.connection.ship(obj) 

346 

347 self.queue = [] 

348 

349 def _handle_received(self, conn, obj): 

350 self.handle_received(obj) 

351 

352 def handle_received(self, obj): 

353 pass 

354 

355 def handle_disconnected(self): 

356 self.connection = None 

357 

358 def ship(self, obj): 

359 if self.connection: 

360 self.connection.ship(obj) 

361 else: 

362 self.queue.append(obj) 

363 

364 

365class SimpleConnectionHandler(ConnectionHandler): 

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

367 ConnectionHandler.__init__(self, parent) 

368 self.mapping = mapping 

369 

370 def handle_received(self, obj): 

371 command = obj[0] 

372 args = obj[1:] 

373 self.mapping[command](*args) 

374 

375 

376class MyMainWindow(qw.QMainWindow): 

377 

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

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

380 self.app = app 

381 

382 def keyPressEvent(self, ev): 

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

384 

385 

386class SnufflerTabs(qw.QTabWidget): 

387 def __init__(self, parent): 

388 qw.QTabWidget.__init__(self, parent) 

389 if hasattr(self, 'setTabsClosable'): 

390 self.setTabsClosable(True) 

391 

392 self.tabCloseRequested.connect( 

393 self.removeTab) 

394 

395 if hasattr(self, 'setDocumentMode'): 

396 self.setDocumentMode(True) 

397 

398 def hide_close_button_on_first_tab(self): 

399 tbar = self.tabBar() 

400 if hasattr(tbar, 'setTabButton'): 

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

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

403 

404 def append_tab(self, widget, name): 

405 widget.setParent(self) 

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

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

408 

409 def remove_tab(self, widget): 

410 self.removeTab(self.indexOf(widget)) 

411 

412 def tabInserted(self, index): 

413 if index == 0: 

414 self.hide_close_button_on_first_tab() 

415 

416 self.tabbar_visibility() 

417 self.setFocus() 

418 

419 def removeTab(self, index): 

420 w = self.widget(index) 

421 w.close() 

422 qw.QTabWidget.removeTab(self, index) 

423 

424 def tabRemoved(self, index): 

425 self.tabbar_visibility() 

426 

427 def tabbar_visibility(self): 

428 if self.count() <= 1: 

429 self.tabBar().hide() 

430 elif self.count() > 1: 

431 self.tabBar().show() 

432 

433 def keyPressEvent(self, event): 

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

435 i = self.currentIndex() 

436 if i != 0: 

437 self.tabCloseRequested.emit(i) 

438 else: 

439 self.parent().keyPressEvent(event) 

440 

441 

442class SnufflerStartWizard(qw.QWizard): 

443 

444 def __init__(self, parent): 

445 qw.QWizard.__init__(self, parent) 

446 

447 self.setOption(self.NoBackButtonOnStartPage) 

448 self.setOption(self.NoBackButtonOnLastPage) 

449 self.setOption(self.NoCancelButton) 

450 self.addPageSurvey() 

451 self.addPageHelp() 

452 self.setWindowTitle('Welcome to Pyrocko') 

453 

454 def getSystemInfo(self): 

455 import numpy 

456 import scipy 

457 import pyrocko 

458 import platform 

459 import uuid 

460 data = { 

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

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

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

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

465 'python': platform.python_version(), 

466 'pyrocko': pyrocko.__version__, 

467 'numpy': numpy.__version__, 

468 'scipy': scipy.__version__, 

469 'qt': qc.PYQT_VERSION_STR, 

470 } 

471 return data 

472 

473 def addPageSurvey(self): 

474 import pprint 

475 webtk = 'DSFGK234ADF4ASDF' 

476 sys_info = self.getSystemInfo() 

477 

478 p = qw.QWizardPage() 

479 p.setCommitPage(True) 

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

481 

482 lyt = qw.QVBoxLayout() 

483 lyt.addWidget(qw.QLabel( 

484 '<p>Your feedback is important for' 

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

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

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

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

489 

490 text_data = qw.QLabel( 

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

492 pprint.pformat( 

493 sys_info, 

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

495 ) 

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

497 lyt.addWidget(text_data) 

498 

499 lyt.addWidget(qw.QLabel( 

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

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

502 )) 

503 

504 p.setLayout(lyt) 

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

506 

507 yes_btn = qw.QPushButton(p) 

508 yes_btn.setText('Yes') 

509 

510 @qc.pyqtSlot() 

511 def send_data(): 

512 import requests 

513 import json 

514 try: 

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

516 data=json.dumps(sys_info)) 

517 except Exception as e: 

518 print(e) 

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

520 

521 self.customButtonClicked.connect(send_data) 

522 

523 self.setButton(self.CustomButton1, yes_btn) 

524 self.setOption(self.HaveCustomButton1, True) 

525 

526 self.addPage(p) 

527 return p 

528 

529 def addPageHelp(self): 

530 p = qw.QWizardPage() 

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

532 

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

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

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

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

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

538<ul> 

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

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

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

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

543 <li> 

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

545 </li> 

546</ul> 

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

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

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

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

551</p> 

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

553 align="center"> 

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

555</p> 

556</html>''') 

557 

558 lyt = qw.QVBoxLayout() 

559 lyt.addWidget(text) 

560 

561 def remove_custom_button(): 

562 self.setOption(self.HaveCustomButton1, False) 

563 

564 p.initializePage = remove_custom_button 

565 

566 p.setLayout(lyt) 

567 self.addPage(p) 

568 return p 

569 

570 

571class SnufflerWindow(qw.QMainWindow): 

572 

573 def __init__( 

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

575 marker_editor_sortable=True, follow=None, controls=True, 

576 opengl=None, instant_close=False): 

577 

578 qw.QMainWindow.__init__(self) 

579 

580 self.instant_close = instant_close 

581 

582 self.dockwidget_to_toggler = {} 

583 self.dockwidgets = [] 

584 

585 self.setWindowTitle("Snuffler") 

586 

587 self.pile_viewer = pile_viewer.PileViewer( 

588 pile, ntracks_shown_max=ntracks, use_opengl=opengl, 

589 marker_editor_sortable=marker_editor_sortable, 

590 panel_parent=self) 

591 

592 self.marker_editor = self.pile_viewer.marker_editor() 

593 self.add_panel( 

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

595 where=qc.Qt.RightDockWidgetArea) 

596 if stations: 

597 self.get_view().add_stations(stations) 

598 

599 if events: 

600 self.get_view().add_events(events) 

601 

602 if len(events) == 1: 

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

604 

605 if markers: 

606 self.get_view().add_markers(markers) 

607 self.get_view().associate_phases_to_events() 

608 

609 self.tabs = SnufflerTabs(self) 

610 self.setCentralWidget(self.tabs) 

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

612 

613 self.pile_viewer.setup_snufflings() 

614 self.setMenuBar(self.pile_viewer.menu) 

615 

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

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

618 self.show() 

619 

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

621 

622 sb = self.statusBar() 

623 sb.clearMessage() 

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

625 

626 snuffler_config = self.pile_viewer.viewer.config 

627 

628 if snuffler_config.first_start: 

629 wizard = SnufflerStartWizard(self) 

630 

631 @qc.pyqtSlot() 

632 def wizard_finished(result): 

633 if result == wizard.Accepted: 

634 snuffler_config.first_start = False 

635 config.write_config(snuffler_config, 'snuffler') 

636 

637 wizard.finished.connect(wizard_finished) 

638 

639 wizard.show() 

640 

641 if follow: 

642 self.get_view().follow(float(follow)) 

643 

644 self.closing = False 

645 

646 def sizeHint(self): 

647 return qc.QSize(1024, 768) 

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

649 

650 def keyPressEvent(self, ev): 

651 self.get_view().keyPressEvent(ev) 

652 

653 def get_view(self): 

654 return self.pile_viewer.get_view() 

655 

656 def get_panel_parent_widget(self): 

657 return self 

658 

659 def add_tab(self, name, widget): 

660 self.tabs.append_tab(widget, name) 

661 

662 def remove_tab(self, widget): 

663 self.tabs.remove_tab(widget) 

664 

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

666 where=qc.Qt.BottomDockWidgetArea): 

667 

668 if not self.dockwidgets: 

669 self.dockwidgets = [] 

670 

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

672 

673 dockwidget = qw.QDockWidget(name, self) 

674 self.dockwidgets.append(dockwidget) 

675 dockwidget.setWidget(panel) 

676 panel.setParent(dockwidget) 

677 self.addDockWidget(where, dockwidget) 

678 

679 if dws: 

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

681 

682 self.toggle_panel(dockwidget, visible) 

683 

684 mitem = qw.QAction(name, None) 

685 

686 def toggle_panel(checked): 

687 self.toggle_panel(dockwidget, True) 

688 

689 mitem.triggered.connect(toggle_panel) 

690 

691 if volatile: 

692 def visibility(visible): 

693 if not visible: 

694 self.remove_panel(panel) 

695 

696 dockwidget.visibilityChanged.connect( 

697 visibility) 

698 

699 self.get_view().add_panel_toggler(mitem) 

700 self.dockwidget_to_toggler[dockwidget] = mitem 

701 

702 def toggle_panel(self, dockwidget, visible): 

703 if visible is None: 

704 visible = not dockwidget.isVisible() 

705 

706 dockwidget.setVisible(visible) 

707 if visible: 

708 w = dockwidget.widget() 

709 minsize = w.minimumSize() 

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

711 

712 def reset_minimum_size(): 

713 import sip 

714 if not sip.isdeleted(w): 

715 w.setMinimumSize(minsize) 

716 

717 qc.QTimer.singleShot(200, reset_minimum_size) 

718 

719 dockwidget.setFocus() 

720 dockwidget.raise_() 

721 

722 def toggle_marker_editor(self): 

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

724 

725 def toggle_main_controls(self): 

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

727 

728 def remove_panel(self, panel): 

729 dockwidget = panel.parent() 

730 self.removeDockWidget(dockwidget) 

731 dockwidget.setParent(None) 

732 mitem = self.dockwidget_to_toggler[dockwidget] 

733 self.get_view().remove_panel_toggler(mitem) 

734 

735 def return_tag(self): 

736 return self.get_view().return_tag 

737 

738 def confirm_close(self): 

739 ret = qw.QMessageBox.question( 

740 self, 

741 'Snuffler', 

742 'Close Snuffler window?', 

743 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

744 qw.QMessageBox.Ok) 

745 

746 return ret == qw.QMessageBox.Ok 

747 

748 def closeEvent(self, event): 

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

750 self.closing = True 

751 self.pile_viewer.cleanup() 

752 event.accept() 

753 else: 

754 event.ignore() 

755 

756 def is_closing(self): 

757 return self.closing 

758 

759 

760class Snuffler(qw.QApplication): 

761 

762 def __init__(self): 

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

764 self.lastWindowClosed.connect(self.myQuit) 

765 self.server = None 

766 self.loader = None 

767 

768 def install_sigint_handler(self): 

769 self._old_signal_handler = signal.signal( 

770 signal.SIGINT, 

771 self.myCloseAllWindows) 

772 

773 def uninstall_sigint_handler(self): 

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

775 

776 def start_server(self): 

777 self.connections = [] 

778 s = qn.QTcpServer(self) 

779 s.listen(qn.QHostAddress.LocalHost) 

780 s.newConnection.connect( 

781 self.handle_accept) 

782 self.server = s 

783 

784 def start_loader(self): 

785 self.loader = SimpleConnectionHandler( 

786 self, 

787 add_files=self.add_files, 

788 update_progress=self.update_progress) 

789 ticket = os.urandom(32) 

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

791 self.connection_handlers[ticket] = self.loader 

792 

793 def handle_accept(self): 

794 sock = self.server.nextPendingConnection() 

795 con = Connection(self, sock) 

796 self.connections.append(con) 

797 

798 con.disconnected.connect( 

799 self.handle_disconnected) 

800 

801 con.received.connect( 

802 self.handle_received_ticket) 

803 

804 def handle_disconnected(self, connection): 

805 self.connections.remove(connection) 

806 connection.close() 

807 del connection 

808 

809 def handle_received_ticket(self, connection, object): 

810 if not isinstance(object, str): 

811 self.handle_disconnected(connection) 

812 

813 ticket = object 

814 if ticket in self.connection_handlers: 

815 h = self.connection_handlers[ticket] 

816 connection.received.disconnect( 

817 self.handle_received_ticket) 

818 

819 h.set_connection(connection) 

820 else: 

821 self.handle_disconnected(connection) 

822 

823 def snuffler_windows(self): 

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

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

826 

827 def event(self, e): 

828 if isinstance(e, qg.QFileOpenEvent): 

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

830 wins = self.snuffler_windows() 

831 if wins: 

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

833 

834 return True 

835 else: 

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

837 

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

839 if not self.loader: 

840 self.start_loader() 

841 

842 self.loader.ship( 

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

844 

845 def add_files(self, files): 

846 p = self.pile_viewer.get_pile() 

847 p.add_files(files) 

848 self.pile_viewer.update_contents() 

849 

850 def update_progress(self, task, percent): 

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

852 

853 def myCloseAllWindows(self, *args): 

854 self.closeAllWindows() 

855 

856 def myQuit(self, *args): 

857 self.quit()