Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/gui/util.py: 54%
1319 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import sys
7import math
8import time
9import numpy as num
10import logging
11import enum
12import calendar
13import signal
14import weakref
16from matplotlib.colors import Normalize
18from .qt_compat import qc, qg, qw
20from .snuffler.marker import Marker, PhaseMarker, EventMarker # noqa
21from .snuffler.marker import MarkerParseError, MarkerOneNSLCRequired # noqa
22from .snuffler.marker import load_markers, save_markers # noqa
23from pyrocko import plot, util
26logger = logging.getLogger('pyrocko.gui.util')
29class _Getch:
30 '''
31 Gets a single character from standard input.
33 Does not echo to the screen.
35 https://stackoverflow.com/questions/510357/how-to-read-a-single-character-from-the-user
36 '''
37 def __init__(self):
38 try:
39 self.impl = _GetchWindows()
40 except ImportError:
41 self.impl = _GetchUnix()
43 def __call__(self): return self.impl()
46class _GetchUnix:
47 def __init__(self):
48 import tty, sys # noqa
50 def __call__(self):
51 import sys
52 import tty
53 import termios
55 fd = sys.stdin.fileno()
56 old_settings = termios.tcgetattr(fd)
57 try:
58 tty.setraw(fd)
59 ch = sys.stdin.read(1)
60 finally:
61 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
63 return ch
66class _GetchWindows:
67 def __init__(self):
68 import msvcrt # noqa
70 def __call__(self):
71 import msvcrt
72 return msvcrt.getch()
75getch = _Getch()
78class PyrockoQApplication(qw.QApplication):
80 def __init__(self):
81 qw.QApplication.__init__(self, [])
82 self._main_window = None
84 def install_sigint_handler(self):
85 self._old_signal_handler = signal.signal(
86 signal.SIGINT,
87 self.request_close_all_windows)
89 def uninstall_sigint_handler(self):
90 signal.signal(signal.SIGINT, self._old_signal_handler)
92 def set_main_window(self, win):
93 self._main_window = win
94 name = win.windowTitle() if win is not None else ''
95 self.setApplicationName(name)
96 self.setApplicationDisplayName(name)
97 self.setDesktopFileName(name)
99 def unset_main_window(self):
100 self.set_main_window(None)
102 def get_main_window(self):
103 return self._main_window
105 def get_main_windows(self):
106 return [self.get_main_window()]
108 def status(self, message, duration=None):
109 win = self.get_main_window()
110 if not win:
111 print(' - %s' % message, file=sys.stderr)
112 else:
113 win.statusBar().showMessage(
114 message, int((duration or 0) * 1000))
116 def event(self, e):
117 if isinstance(e, qg.QFileOpenEvent):
118 path = str(e.file())
119 if path != sys.argv[0]:
120 wins = self.get_main_windows()
121 if wins:
122 wins[0].get_view().load_soon([path])
124 return True
125 else:
126 return qw.QApplication.event(self, e)
128 def request_close_all_windows(self, *args):
130 def confirm():
131 try:
132 print(
133 '\nQuit %s? [y/n]' % self.applicationName(),
134 file=sys.stderr)
136 confirmed = getch() == 'y'
137 if not confirmed:
138 print(
139 'Continuing.',
140 file=sys.stderr)
141 else:
142 print(
143 'Quitting %s.' % self.applicationName(),
144 file=sys.stderr)
146 return confirmed
148 except Exception:
149 return False
151 windows = self.get_main_windows()
152 instant_close = all(win.instant_close for win in windows)
154 if instant_close or confirm():
155 for win in windows:
156 win.instant_close = True
158 self.closeAllWindows()
161app = None
164def get_app():
165 from .qt_compat import qg
166 try:
167 global app
168 if app is None:
169 qg.QSurfaceFormat.setDefaultFormat(qg.QSurfaceFormat())
170 app = PyrockoQApplication()
171 return app
172 except NameError: # can happen during shutdown
173 return None
176def rint(x):
177 return int(round(x))
180def make_QPolygonF(xdata, ydata):
181 assert len(xdata) == len(ydata)
182 qpoints = qg.QPolygonF(len(ydata))
183 vptr = qpoints.data()
184 vptr.setsize(len(ydata)*8*2)
185 aa = num.ndarray(
186 shape=(len(ydata), 2),
187 dtype=num.float64,
188 buffer=memoryview(vptr))
189 aa.setflags(write=True)
190 aa[:, 0] = xdata
191 aa[:, 1] = ydata
192 return qpoints
195def get_colormap_qimage(cmap_name, vmin=None, vmax=None):
196 NCOLORS = 512
197 norm = Normalize()
198 norm.vmin = vmin
199 norm.vmax = vmax
201 return qg.QImage(
202 plot.mpl_get_cmap(cmap_name)(
203 norm(num.linspace(0., 1., NCOLORS)),
204 alpha=None, bytes=True),
205 NCOLORS, 1, qg.QImage.Format_RGBX8888)
208class Label(object):
209 def __init__(
210 self, p, x, y, label_str,
211 label_bg=None,
212 anchor='BL',
213 outline=False,
214 font=None,
215 color=None):
217 text = qg.QTextDocument()
218 if font:
219 text.setDefaultFont(font)
220 text.setDefaultStyleSheet('span { color: %s; }' % color.name())
221 text.setHtml('<span>%s</span>' % label_str)
222 s = text.size()
223 rect = qc.QRect(0, 0, int(s.width()), int(s.height()))
224 tx, ty = x, y
226 if 'B' in anchor:
227 ty -= rect.height()
228 if 'R' in anchor:
229 tx -= rect.width()
230 if 'M' in anchor:
231 ty -= rect.height() // 2
232 if 'C' in anchor:
233 tx -= rect.width() // 2
235 rect.translate(int(tx), int(ty))
236 self.rect = rect
237 self.text = text
238 self.outline = outline
239 self.label_bg = label_bg
240 self.color = color
241 self.p = p
243 def draw(self):
244 p = self.p
245 rect = self.rect
246 tx = rect.left()
247 ty = rect.top()
249 if self.outline:
250 oldpen = p.pen()
251 oldbrush = p.brush()
252 p.setBrush(self.label_bg)
253 rect.adjust(-2, 0, 2, 0)
254 p.drawRect(rect)
255 p.setPen(oldpen)
256 p.setBrush(oldbrush)
258 else:
259 if self.label_bg:
260 p.fillRect(rect, self.label_bg)
262 p.translate(int(tx), int(ty))
263 self.text.drawContents(p)
264 p.translate(-int(tx), -int(ty))
267def draw_label(p, x, y, label_str, label_bg, anchor='BL', outline=False):
268 fm = p.fontMetrics()
270 label = label_str
271 rect = fm.boundingRect(label)
273 tx, ty = x, y
274 if 'T' in anchor:
275 ty += rect.height()
276 if 'R' in anchor:
277 tx -= rect.width()
278 if 'M' in anchor:
279 ty += rect.height() // 2
280 if 'C' in anchor:
281 tx -= rect.width() // 2
283 rect.translate(int(tx), int(ty))
284 if outline:
285 oldpen = p.pen()
286 oldbrush = p.brush()
287 p.setBrush(label_bg)
288 rect.adjust(-2, 0, 2, 0)
289 p.drawRect(rect)
290 p.setPen(oldpen)
291 p.setBrush(oldbrush)
293 else:
294 p.fillRect(rect, label_bg)
296 p.drawText(int(tx), int(ty), label)
299def get_err_palette():
300 err_palette = qg.QPalette()
301 err_palette.setColor(qg.QPalette.Base, qg.QColor(255, 200, 200))
302 return err_palette
305class QSliderNoWheel(qw.QSlider):
307 def wheelEvent(self, ev):
308 ''
309 ev.ignore()
311 def keyPressEvent(self, ev):
312 ''
313 ev.ignore()
316class QSliderFloat(qw.QSlider):
318 sliderMovedFloat = qc.pyqtSignal(float)
319 valueChangedFloat = qc.pyqtSignal(float)
320 rangeChangedFloat = qc.pyqtSignal(float, float)
322 def __init__(self, *args, **kwargs):
323 qw.QSlider.__init__(self, *args, **kwargs)
324 self.setMinimum(0)
325 self.setMaximum(1000)
326 self.setSingleStep(10)
327 self.setPageStep(100)
328 self._fmin = 0.
329 self._fmax = 1.
330 self.valueChanged.connect(self._handleValueChanged)
331 self.sliderMoved.connect(self._handleSliderMoved)
333 def _f_to_i(self, fval):
334 fval = float(fval)
335 imin = self.minimum()
336 imax = self.maximum()
337 return max(
338 imin,
339 imin + min(
340 int(round(
341 (fval-self._fmin) * (imax - imin)
342 / (self._fmax-self._fmin))),
343 imax))
345 def _i_to_f(self, ival):
346 imin = self.minimum()
347 imax = self.maximum()
348 return self._fmin + (ival - imin) * (self._fmax - self._fmin) \
349 / (imax - imin)
351 def minimumFloat(self):
352 return self._fmin
354 def setMinimumFloat(self, fval):
355 self._fmin = float(fval)
356 self.rangeChangedFloat.emit(self._fmin, self._fmax)
358 def maximumFloat(self):
359 return self._fmax
361 def setMaximumFloat(self, fval):
362 self._fmax = float(fval)
363 self.rangeChangedFloat.emit(self._fmin, self._fmax)
365 def setRangeFloat(self, fmin, fmax):
366 self._fmin = float(fmin)
367 self._fmax = float(fmax)
368 self.rangeChangedFloat.emit(self._fmin, self._fmax)
370 def valueFloat(self):
371 return self._i_to_f(self.value())
373 def setValueFloat(self, fval):
374 qw.QSlider.setValue(self, self._f_to_i(fval))
376 def _handleValueChanged(self, ival):
377 self.valueChangedFloat.emit(self._i_to_f(ival))
379 def _handleSliderMoved(self, ival):
380 self.sliderMovedFloat.emit(self._i_to_f(ival))
383class MyValueEdit(qw.QLineEdit):
385 edited = qc.pyqtSignal(float)
387 def __init__(
388 self,
389 low_is_none=False,
390 high_is_none=False,
391 low_is_zero=False,
392 *args, **kwargs):
394 qw.QLineEdit.__init__(self, *args, **kwargs)
395 self.value = 0.
396 self.mi = 0.
397 self.ma = 1.
398 self.low_is_none = low_is_none
399 self.high_is_none = high_is_none
400 self.low_is_zero = low_is_zero
401 self.editingFinished.connect(
402 self.myEditingFinished)
403 self.lock = False
405 def setRange(self, mi, ma):
406 self.mi = mi
407 self.ma = ma
409 def setValue(self, value):
410 if not self.lock:
411 self.value = value
412 self.setPalette(qw.QApplication.palette())
413 self.adjust_text()
415 def myEditingFinished(self):
416 try:
417 t = str(self.text()).strip()
418 if self.low_is_none and t in ('off', 'below'):
419 value = self.mi
420 elif self.high_is_none and t in ('off', 'above'):
421 value = self.ma
422 elif self.low_is_zero and float(t) == 0.0:
423 value = self.mi
424 else:
425 value = float(t)
427 if not (self.mi <= value <= self.ma):
428 raise Exception('out of range')
430 if value != self.value:
431 self.value = value
432 self.lock = True
433 self.edited.emit(value)
434 self.setPalette(qw.QApplication.palette())
435 except Exception:
436 self.setPalette(get_err_palette())
438 self.lock = False
440 def adjust_text(self):
441 t = ('%8.5g' % self.value).strip()
443 if self.low_is_zero and self.value == self.mi:
444 t = '0'
446 if self.low_is_none and self.value == self.mi:
447 if self.high_is_none:
448 t = 'below'
449 else:
450 t = 'off'
452 if self.high_is_none and self.value == self.ma:
453 if self.low_is_none:
454 t = 'above'
455 else:
456 t = 'off'
458 if t in ('off', 'below', 'above'):
459 self.setStyleSheet('font-style: italic;')
460 else:
461 self.setStyleSheet(None)
463 self.setText(t)
466class ValControl(qw.QWidget):
468 valchange = qc.pyqtSignal(object, int)
470 def __init__(
471 self,
472 low_is_none=False,
473 high_is_none=False,
474 low_is_zero=False,
475 type=float,
476 *args):
478 qc.QObject.__init__(self, *args)
480 self.lname = qw.QLabel('name')
481 self.lname.setSizePolicy(
482 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum))
483 self.lvalue = MyValueEdit(
484 low_is_none=low_is_none,
485 high_is_none=high_is_none,
486 low_is_zero=low_is_zero)
487 self.lvalue.setFixedWidth(100)
488 self.slider = QSliderNoWheel(qc.Qt.Horizontal)
489 self.slider.setSizePolicy(
490 qw.QSizePolicy(qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
491 self.slider.setMaximum(10000)
492 self.slider.setSingleStep(100)
493 self.slider.setPageStep(1000)
494 self.slider.setTickPosition(qw.QSlider.NoTicks)
495 self.slider.setFocusPolicy(qc.Qt.ClickFocus)
497 self.low_is_none = low_is_none
498 self.high_is_none = high_is_none
499 self.low_is_zero = low_is_zero
501 self.slider.valueChanged.connect(
502 self.slided)
503 self.lvalue.edited.connect(
504 self.edited)
506 self.type = type
507 self.mute = False
509 def widgets(self):
510 return self.lname, self.lvalue, self.slider
512 def s2v(self, svalue):
513 if self.ma == 0 or self.mi == 0:
514 return 0
516 a = math.log(self.ma/self.mi) / 10000.
517 value = self.mi*math.exp(a*svalue)
518 value = self.type(value)
519 return value
521 def v2s(self, value):
522 value = self.type(value)
524 if value == 0 or self.mi == 0:
525 return 0
527 a = math.log(self.ma/self.mi) / 10000.
528 return int(round(math.log(value/self.mi) / a))
530 def setup(self, name, mi, ma, cur, ind):
531 self.lname.setText(name)
532 self.mi = mi
533 self.ma = ma
534 self.ind = ind
535 self.lvalue.setRange(self.s2v(0), self.s2v(10000))
536 self.set_value(cur)
538 def set_range(self, mi, ma):
539 if self.mi == mi and self.ma == ma:
540 return
542 vput = None
543 if self.cursl == 0:
544 vput = mi
545 if self.cursl == 10000:
546 vput = ma
548 self.mi = mi
549 self.ma = ma
550 self.lvalue.setRange(self.s2v(0), self.s2v(10000))
552 if vput is not None:
553 self.set_value(vput)
554 else:
555 if self.cur < mi:
556 self.set_value(mi)
557 if self.cur > ma:
558 self.set_value(ma)
560 def set_value(self, cur):
561 if cur is None:
562 if self.low_is_none:
563 cur = self.mi
564 elif self.high_is_none:
565 cur = self.ma
567 if cur == 0.0:
568 if self.low_is_zero:
569 cur = self.mi
571 self.mute = True
572 self.cur = cur
573 self.cursl = self.v2s(cur)
574 self.slider.blockSignals(True)
575 self.slider.setValue(self.cursl)
576 self.slider.blockSignals(False)
577 self.lvalue.blockSignals(True)
578 if self.cursl in (0, 10000):
579 self.lvalue.setValue(self.s2v(self.cursl))
580 else:
581 self.lvalue.setValue(self.cur)
582 self.lvalue.blockSignals(False)
583 self.mute = False
585 def set_tracking(self, tracking):
586 self.slider.setTracking(tracking)
588 def get_value(self):
589 return self.cur
591 def slided(self, val):
592 if self.cursl != val:
593 self.cursl = val
594 cur = self.s2v(self.cursl)
596 if cur != self.cur:
597 self.cur = cur
598 self.lvalue.blockSignals(True)
599 self.lvalue.setValue(self.cur)
600 self.lvalue.blockSignals(False)
601 self.fire_valchange()
603 def edited(self, val):
604 if self.cur != val:
605 self.cur = val
606 cursl = self.v2s(val)
607 if (cursl != self.cursl):
608 self.slider.blockSignals(True)
609 self.slider.setValue(cursl)
610 self.slider.blockSignals(False)
611 self.cursl = cursl
613 self.fire_valchange()
615 def fire_valchange(self):
617 if self.mute:
618 return
620 cur = self.cur
622 if self.cursl == 0:
623 if self.low_is_none:
624 cur = None
626 elif self.low_is_zero:
627 cur = 0.0
629 if self.cursl == 10000 and self.high_is_none:
630 cur = None
632 self.valchange.emit(cur, int(self.ind))
635class LinValControl(ValControl):
637 def s2v(self, svalue):
638 value = svalue/10000. * (self.ma-self.mi) + self.mi
639 value = self.type(value)
640 return value
642 def v2s(self, value):
643 value = self.type(value)
644 if self.ma == self.mi:
645 return 0
646 return int(round((value-self.mi)/(self.ma-self.mi) * 10000.))
649class ColorbarControl(qw.QWidget):
651 AVAILABLE_CMAPS = (
652 'viridis',
653 'plasma',
654 'magma',
655 'binary',
656 'Reds',
657 'copper',
658 'seismic',
659 'RdBu',
660 'YlGn',
661 )
663 DEFAULT_CMAP = 'viridis'
665 cmap_changed = qc.pyqtSignal(str)
666 show_absolute_toggled = qc.pyqtSignal(bool)
667 show_integrate_toggled = qc.pyqtSignal(bool)
669 def __init__(self, *args, **kwargs):
670 super().__init__(*args, **kwargs)
672 self.lname = qw.QLabel('Colormap')
673 self.lname.setSizePolicy(
674 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum))
676 self.cmap_options = qw.QComboBox()
677 self.cmap_options.setIconSize(qc.QSize(64, 12))
678 for ic, cmap in enumerate(self.AVAILABLE_CMAPS):
679 pixmap = qg.QPixmap.fromImage(
680 get_colormap_qimage(cmap))
681 icon = qg.QIcon(pixmap.scaled(64, 12))
683 self.cmap_options.addItem(icon, '', cmap)
684 self.cmap_options.setItemData(ic, cmap, qc.Qt.ToolTipRole)
686 # self.cmap_options.setCurrentIndex(self.cmap_name)
687 self.cmap_options.currentIndexChanged.connect(self.set_cmap)
688 self.cmap_options.setSizePolicy(
689 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum))
691 self.colorslider = ColorbarSlider(self)
692 self.colorslider.setSizePolicy(
693 qw.QSizePolicy.MinimumExpanding | qw.QSizePolicy.ExpandFlag,
694 qw.QSizePolicy.MinimumExpanding | qw.QSizePolicy.ExpandFlag
695 )
696 self.clip_changed = self.colorslider.clip_changed
698 btn_size = qw.QSizePolicy(
699 qw.QSizePolicy.Maximum | qw.QSizePolicy.ShrinkFlag,
700 qw.QSizePolicy.Maximum | qw.QSizePolicy.ShrinkFlag)
702 self.symetry_toggle = qw.QPushButton()
703 self.symetry_toggle.setIcon(
704 qg.QIcon.fromTheme('object-flip-horizontal'))
705 self.symetry_toggle.setToolTip('Symetric clip values')
706 self.symetry_toggle.setSizePolicy(btn_size)
707 self.symetry_toggle.setCheckable(True)
708 self.symetry_toggle.toggled.connect(self.toggle_symetry)
709 self.symetry_toggle.setChecked(True)
711 self.reverse_toggle = qw.QPushButton()
712 self.reverse_toggle.setIcon(
713 qg.QIcon.fromTheme('object-rotate-right'))
714 self.reverse_toggle.setToolTip('Reverse the colormap')
715 self.reverse_toggle.setSizePolicy(btn_size)
716 self.reverse_toggle.setCheckable(True)
717 self.reverse_toggle.toggled.connect(self.toggle_reverse_cmap)
719 self.abs_toggle = qw.QPushButton()
720 self.abs_toggle.setIcon(
721 qg.QIcon.fromTheme('go-bottom'))
722 self.abs_toggle.setToolTip('Show absolute values')
723 self.abs_toggle.setSizePolicy(btn_size)
724 self.abs_toggle.setCheckable(True)
725 self.abs_toggle.toggled.connect(self.toggle_absolute)
727 self.int_toggle = qw.QPushButton()
728 self.int_toggle.setText('∫')
729 self.int_toggle.setToolTip(
730 u'Integrate traces (e.g. strain rate → strain)')
731 self.int_toggle.setSizePolicy(btn_size)
732 self.int_toggle.setCheckable(True)
733 self.int_toggle.setMaximumSize(
734 24,
735 self.int_toggle.maximumSize().height())
736 self.int_toggle.toggled.connect(self.show_integrate_toggled.emit)
738 v_splitter = qw.QFrame()
739 v_splitter.setFrameShape(qw.QFrame.VLine)
740 v_splitter.setFrameShadow(qw.QFrame.Sunken)
742 self.controls = qw.QWidget()
743 layout = qw.QHBoxLayout()
744 layout.addWidget(self.colorslider)
745 layout.addWidget(self.symetry_toggle)
746 layout.addWidget(self.reverse_toggle)
747 layout.addWidget(v_splitter)
748 layout.addWidget(self.abs_toggle)
749 layout.addWidget(self.int_toggle)
750 self.controls.setLayout(layout)
752 self.set_cmap_name(self.DEFAULT_CMAP)
754 def set_cmap(self, idx):
755 self.set_cmap_name(self.cmap_options.itemData(idx))
757 def set_cmap_name(self, cmap_name):
758 self.cmap_name = cmap_name
759 self.colorslider.set_cmap_name(cmap_name)
760 self.cmap_changed.emit(cmap_name)
762 def get_cmap(self):
763 return self.cmap_name
765 def toggle_symetry(self, toggled):
766 self.colorslider.set_symetry(toggled)
768 def toggle_reverse_cmap(self):
769 cmap = self.get_cmap()
770 if cmap.endswith('_r'):
771 r_cmap = cmap.rstrip('_r')
772 else:
773 r_cmap = cmap + '_r'
774 self.set_cmap_name(r_cmap)
776 def toggle_absolute(self, toggled):
777 self.symetry_toggle.setChecked(not toggled)
778 self.show_absolute_toggled.emit(toggled)
780 def widgets(self):
781 return (self.lname, self.cmap_options, self.controls)
784class ColorbarSlider(qw.QWidget):
785 DEFAULT_CMAP = 'viridis'
786 CORNER_THRESHOLD = 10
787 MIN_WIDTH = .05
789 clip_changed = qc.pyqtSignal(float, float)
791 class COMPONENTS(enum.Enum):
792 LeftLine = 1
793 RightLine = 2
794 Center = 3
796 def __init__(self, *args, cmap_name=None):
797 super().__init__()
798 self.cmap_name = cmap_name or self.DEFAULT_CMAP
799 self.clip_min = 0.
800 self.clip_max = 1.
802 self._sym_locked = True
803 self._mouse_inside = False
804 self._window = None
805 self._old_pos = None
806 self._component_grabbed = None
808 self.setMouseTracking(True)
810 def set_cmap_name(self, cmap_name):
811 self.cmap_name = cmap_name
812 self.repaint()
814 def get_cmap_name(self):
815 return self.cmap_name
817 def set_symetry(self, symetry):
818 self._sym_locked = symetry
819 if self._sym_locked:
820 clip_max = 1. - min(self.clip_min, 1.-self.clip_max)
821 clip_min = 1. - clip_max
822 self.set_clip(clip_min, clip_max)
824 def _set_window(self, window):
825 self._window = window
827 def _get_left_line(self):
828 rect = self._get_active_rect()
829 if not rect:
830 return
831 return qc.QLineF(rect.left(), 0, rect.left(), rect.height())
833 def _get_right_line(self):
834 rect = self._get_active_rect()
835 if not rect:
836 return
837 return qc.QLineF(rect.right(), 0, rect.right(), rect.height())
839 def _get_active_rect(self):
840 if not self._window:
841 return
842 rect = qc.QRect(self._window)
843 width = rect.width()
844 rect.setLeft(int(width * self.clip_min))
845 rect.setRight(int(width * self.clip_max))
846 return rect
848 def set_clip(self, clip_min, clip_max):
849 if clip_min < 0. or clip_max > 1.:
850 return
851 if clip_max - clip_min < self.MIN_WIDTH:
852 return
854 self.clip_min = clip_min
855 self.clip_max = clip_max
856 self.repaint()
857 self.clip_changed.emit(self.clip_min, self.clip_max)
859 def mousePressEvent(self, event):
860 ''
861 act_rect = self._get_active_rect()
862 if event.buttons() != qc.Qt.MouseButton.LeftButton:
863 self._component_grabbed = None
864 return
866 dist_left = abs(event.pos().x() - act_rect.left())
867 dist_right = abs(event.pos().x() - act_rect.right())
869 if 0 < dist_left < self.CORNER_THRESHOLD:
870 self._component_grabbed = self.COMPONENTS.LeftLine
871 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor))
872 elif 0 < dist_right < self.CORNER_THRESHOLD:
873 self._component_grabbed = self.COMPONENTS.RightLine
874 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor))
875 else:
876 self.setCursor(qg.QCursor())
878 def mouseReleaseEvent(self, event):
879 ''
880 self._component_grabbed = None
881 self.repaint()
883 def mouseDoubleClickEvent(self, event):
884 ''
885 self.set_clip(0., 1.)
887 def wheelEvent(self, event):
888 ''
889 event.accept()
890 if not self._sym_locked:
891 return
893 delta = event.angleDelta().y()
894 delta = -delta / 5e3
895 clip_min_new = max(self.clip_min + delta, 0.)
896 clip_max_new = min(self.clip_max - delta, 1.)
897 self._mouse_inside = True
898 self.set_clip(clip_min_new, clip_max_new)
900 def mouseMoveEvent(self, event):
901 ''
902 act_rect = self._get_active_rect()
904 if not self._component_grabbed:
905 dist_left = abs(event.pos().x() - act_rect.left())
906 dist_right = abs(event.pos().x() - act_rect.right())
908 if 0 <= dist_left < self.CORNER_THRESHOLD or \
909 0 <= dist_right < self.CORNER_THRESHOLD:
910 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor))
911 else:
912 self.setCursor(qg.QCursor())
914 if self._old_pos and self._component_grabbed:
915 shift = (event.pos() - self._old_pos).x() / self._window.width()
917 if self._component_grabbed is self.COMPONENTS.LeftLine:
918 clip_min_new = max(self.clip_min + shift, 0.)
919 clip_max_new = \
920 min(self.clip_max - shift, 1.) \
921 if self._sym_locked else self.clip_max
923 elif self._component_grabbed is self.COMPONENTS.RightLine:
924 clip_max_new = min(self.clip_max + shift, 1.)
925 clip_min_new = \
926 max(self.clip_min - shift, 0.) \
927 if self._sym_locked else self.clip_min
929 self.set_clip(clip_min_new, clip_max_new)
931 self._old_pos = event.pos()
933 def enterEvent(self, e):
934 ''
935 self._mouse_inside = True
936 self.repaint()
938 def leaveEvent(self, e):
939 ''
940 self._mouse_inside = False
941 self.repaint()
943 def paintEvent(self, e):
944 ''
945 p = qg.QPainter(self)
946 self._set_window(p.window())
948 p.drawImage(
949 p.window(),
950 get_colormap_qimage(self.cmap_name, self.clip_min, self.clip_max))
952 left_line = self._get_left_line()
953 right_line = self._get_right_line()
955 pen = qg.QPen()
956 pen.setWidth(2)
957 pen.setStyle(qc.Qt.DotLine)
958 pen.setBrush(qc.Qt.white)
959 p.setPen(pen)
960 p.setCompositionMode(
961 qg.QPainter.CompositionMode.CompositionMode_Difference)
963 p.drawLine(left_line)
964 p.drawLine(right_line)
966 label_rect = self._get_active_rect()
967 label_rect.setLeft(label_rect.left() + 5)
968 label_rect.setRight(label_rect.right() - 5)
969 label_left_rect = qc.QRectF(label_rect)
970 label_right_rect = qc.QRectF(label_rect)
971 label_left_align = qc.Qt.AlignLeft
972 label_right_align = qc.Qt.AlignRight
974 if label_rect.left() > 50:
975 label_left_rect.setRight(label_rect.left() - 10)
976 label_left_rect.setLeft(0)
977 label_left_align = qc.Qt.AlignRight
979 if self._window.right() - label_rect.right() > 50:
980 label_right_rect.setLeft(label_rect.right() + 10)
981 label_right_rect.setRight(self._window.right())
982 label_right_align = qc.Qt.AlignLeft
984 if self._mouse_inside or self._component_grabbed:
985 p.drawText(
986 label_left_rect,
987 label_left_align | qc.Qt.AlignVCenter,
988 '%d%%' % round(self.clip_min * 100))
989 p.drawText(
990 label_right_rect,
991 label_right_align | qc.Qt.AlignVCenter,
992 '%d%%' % round(self.clip_max * 100))
995class Progressbar(object):
996 def __init__(self, parent, name, can_abort=True):
997 self.parent = parent
998 self.name = name
999 self.label = qw.QLabel(name, parent)
1000 self.pbar = qw.QProgressBar(parent)
1001 self.aborted = False
1002 self.time_last_update = 0.
1003 if can_abort:
1004 self.abort_button = qw.QPushButton('Abort', parent)
1005 self.abort_button.clicked.connect(
1006 self.abort)
1007 else:
1008 self.abort_button = None
1010 def widgets(self):
1011 widgets = [self.label, self.bar()]
1012 if self.abort_button:
1013 widgets.append(self.abort_button)
1014 return widgets
1016 def bar(self):
1017 return self.pbar
1019 def abort(self):
1020 self.aborted = True
1023class Progressbars(qw.QFrame):
1024 def __init__(self, parent):
1025 qw.QFrame.__init__(self, parent)
1026 self.layout = qw.QGridLayout()
1027 self.setLayout(self.layout)
1028 self.bars = {}
1029 self.start_times = {}
1030 self.hide()
1032 def set_status(self, name, value, can_abort=True, force=False):
1033 value = int(round(value))
1034 now = time.time()
1035 if name not in self.start_times:
1036 self.start_times[name] = now
1037 if not force:
1038 return False
1039 else:
1040 if now < self.start_times[name] + 1.0:
1041 if value == 100:
1042 del self.start_times[name]
1043 if not force:
1044 return False
1046 self.start_times.get(name, 0.0)
1047 if name not in self.bars:
1048 if value == 100:
1049 return False
1050 self.bars[name] = Progressbar(self, name, can_abort=can_abort)
1051 self.make_layout()
1053 bar = self.bars[name]
1054 if bar.time_last_update < now - 0.1 or value == 100:
1055 bar.bar().setValue(value)
1056 bar.time_last_update = now
1058 if value == 100:
1059 del self.bars[name]
1060 if name in self.start_times:
1061 del self.start_times[name]
1062 self.make_layout()
1063 for w in bar.widgets():
1064 w.setParent(None)
1066 return bar.aborted
1068 def make_layout(self):
1069 while True:
1070 c = self.layout.takeAt(0)
1071 if c is None:
1072 break
1074 for ibar, bar in enumerate(self.bars.values()):
1075 for iw, w in enumerate(bar.widgets()):
1076 self.layout.addWidget(w, ibar, iw)
1078 if not self.bars:
1079 self.hide()
1080 else:
1081 self.show()
1084def tohex(c):
1085 return '%02x%02x%02x' % c
1088def to01(c):
1089 return c[0]/255., c[1]/255., c[2]/255.
1092def beautify_axes(axes):
1093 try:
1094 from cycler import cycler
1095 axes.set_prop_cycle(
1096 cycler('color', [to01(x) for x in plot.graph_colors]))
1098 except (ImportError, KeyError):
1099 axes.set_color_cycle(list(map(to01, plot.graph_colors)))
1101 xa = axes.get_xaxis()
1102 ya = axes.get_yaxis()
1103 for attr in ('labelpad', 'LABELPAD'):
1104 if hasattr(xa, attr):
1105 setattr(xa, attr, xa.get_label().get_fontsize())
1106 setattr(ya, attr, ya.get_label().get_fontsize())
1107 break
1110class FigureFrame(qw.QFrame):
1111 '''
1112 A widget to present a :py:mod:`matplotlib` figure.
1113 '''
1115 def __init__(self, parent=None, figure_cls=None):
1116 qw.QFrame.__init__(self, parent)
1117 fgcolor = plot.tango_colors['aluminium5']
1118 dpi = 0.5*(self.logicalDpiX() + self.logicalDpiY())
1120 font = qg.QFont()
1121 font.setBold(True)
1122 fontsize = font.pointSize()
1124 import matplotlib
1125 matplotlib.rcdefaults()
1126 matplotlib.rcParams['backend'] = 'Qt5Agg'
1128 matplotlib.rc('xtick', direction='out', labelsize=fontsize)
1129 matplotlib.rc('ytick', direction='out', labelsize=fontsize)
1130 matplotlib.rc('xtick.major', size=8, width=1)
1131 matplotlib.rc('xtick.minor', size=4, width=1)
1132 matplotlib.rc('ytick.major', size=8, width=1)
1133 matplotlib.rc('ytick.minor', size=4, width=1)
1134 matplotlib.rc('figure', facecolor='white', edgecolor=tohex(fgcolor))
1136 matplotlib.rc(
1137 'font',
1138 family='sans-serif',
1139 weight='bold',
1140 size=fontsize,
1141 **{'sans-serif': [
1142 font.family(),
1143 'DejaVu Sans', 'Bitstream Vera Sans', 'Lucida Grande',
1144 'Verdana', 'Geneva', 'Lucid', 'Arial', 'Helvetica']})
1146 matplotlib.rc('legend', fontsize=fontsize)
1148 matplotlib.rc('text', color=tohex(fgcolor))
1149 matplotlib.rc('xtick', color=tohex(fgcolor))
1150 matplotlib.rc('ytick', color=tohex(fgcolor))
1151 matplotlib.rc('figure.subplot', bottom=0.15)
1153 matplotlib.rc('axes', linewidth=1.0, unicode_minus=False)
1154 matplotlib.rc(
1155 'axes',
1156 facecolor='white',
1157 edgecolor=tohex(fgcolor),
1158 labelcolor=tohex(fgcolor))
1160 try:
1161 from cycler import cycler
1162 matplotlib.rc(
1163 'axes', prop_cycle=cycler(
1164 'color', [to01(x) for x in plot.graph_colors]))
1166 except (ImportError, KeyError):
1167 try:
1168 matplotlib.rc('axes', color_cycle=[
1169 to01(x) for x in plot.graph_colors])
1171 except KeyError:
1172 pass
1174 try:
1175 matplotlib.rc('axes', labelsize=fontsize)
1176 except KeyError:
1177 pass
1179 try:
1180 matplotlib.rc('axes', labelweight='bold')
1181 except KeyError:
1182 pass
1184 if figure_cls is None:
1185 from matplotlib.figure import Figure
1186 figure_cls = Figure
1188 from matplotlib.backends.backend_qt5agg import \
1189 NavigationToolbar2QT as NavigationToolbar
1191 from matplotlib.backends.backend_qt5agg \
1192 import FigureCanvasQTAgg as FigureCanvas
1194 layout = qw.QGridLayout()
1195 layout.setContentsMargins(0, 0, 0, 0)
1196 self.setLayout(layout)
1198 canvas_frame = qw.QFrame()
1199 canvas_frame_layout = qw.QHBoxLayout()
1200 canvas_frame_layout.setContentsMargins(0, 0, 0, 0)
1201 canvas_frame.setLayout(canvas_frame_layout)
1202 canvas_frame.setFrameShape(qw.QFrame.StyledPanel)
1204 self.figure = figure_cls(dpi=dpi)
1205 self.canvas = FigureCanvas(self.figure)
1206 canvas_frame_layout.addWidget(self.canvas)
1207 self.canvas.setSizePolicy(
1208 qw.QSizePolicy(
1209 qw.QSizePolicy.Expanding,
1210 qw.QSizePolicy.Expanding))
1211 toolbar_frame = qw.QFrame()
1212 toolbar_frame.setFrameShape(qw.QFrame.StyledPanel)
1213 toolbar_frame_layout = qw.QHBoxLayout()
1214 toolbar_frame_layout.setContentsMargins(0, 0, 0, 0)
1215 toolbar_frame.setLayout(toolbar_frame_layout)
1216 self.toolbar = NavigationToolbar(self.canvas, self)
1217 layout.addWidget(canvas_frame, 0, 0)
1218 toolbar_frame_layout.addWidget(self.toolbar)
1219 layout.addWidget(toolbar_frame, 1, 0)
1220 self.closed = False
1222 def gca(self):
1223 axes = self.figure.gca()
1224 beautify_axes(axes)
1225 return axes
1227 def gcf(self):
1228 return self.figure
1230 def draw(self):
1231 '''
1232 Draw with AGG, then queue for Qt update.
1233 '''
1234 self.canvas.draw()
1236 def closeEvent(self, ev):
1237 self.closed = True
1240class SmartplotFrame(FigureFrame):
1241 '''
1242 A widget to present a :py:mod:`pyrocko.plot.smartplot` figure.
1243 '''
1245 def __init__(
1246 self, parent=None, plot_args=[], plot_kwargs={}, plot_cls=None):
1248 from pyrocko.plot import smartplot
1250 FigureFrame.__init__(
1251 self,
1252 parent=parent,
1253 figure_cls=smartplot.SmartplotFigure)
1255 if plot_cls is None:
1256 plot_cls = smartplot.Plot
1258 self.plot = plot_cls(
1259 *plot_args,
1260 fig=self.figure,
1261 call_mpl_init=False,
1262 **plot_kwargs)
1265class WebKitFrame(qw.QFrame):
1266 '''
1267 A widget to present a html page using WebKit.
1268 '''
1270 def __init__(self, url=None, parent=None):
1271 from pyrocko.deps import require
1272 require('PyQt5.QtWebEngine')
1273 from PyQt5.QtWebEngineWidgets import QWebEngineView as WebView
1275 qw.QFrame.__init__(self, parent)
1276 layout = qw.QGridLayout()
1277 layout.setContentsMargins(0, 0, 0, 0)
1278 layout.setSpacing(0)
1279 self.setLayout(layout)
1280 self.web_widget = WebView()
1281 layout.addWidget(self.web_widget, 0, 0)
1282 if url:
1283 self.web_widget.load(qc.QUrl(url))
1286class VTKFrame(qw.QFrame):
1287 '''
1288 A widget to present a VTK visualization.
1289 '''
1291 def __init__(self, actors=None, parent=None):
1292 import vtk
1293 from vtk.qt.QVTKRenderWindowInteractor import \
1294 QVTKRenderWindowInteractor
1296 qw.QFrame.__init__(self, parent)
1297 layout = qw.QGridLayout()
1298 layout.setContentsMargins(0, 0, 0, 0)
1299 layout.setSpacing(0)
1301 self.setLayout(layout)
1303 self.vtk_widget = QVTKRenderWindowInteractor(self)
1304 layout.addWidget(self.vtk_widget, 0, 0)
1306 self.renderer = vtk.vtkRenderer()
1307 self.vtk_widget.GetRenderWindow().AddRenderer(self.renderer)
1308 self.iren = self.vtk_widget.GetRenderWindow().GetInteractor()
1310 if actors:
1311 for a in actors:
1312 self.renderer.AddActor(a)
1314 def init(self):
1315 self.iren.Initialize()
1317 def add_actor(self, actor):
1318 self.renderer.AddActor(actor)
1321class PixmapFrame(qw.QLabel):
1322 '''
1323 A widget to preset a pixmap image.
1324 '''
1326 def __init__(self, filename=None, parent=None):
1328 qw.QLabel.__init__(self, parent)
1329 self.setAlignment(qc.Qt.AlignCenter)
1330 self.setContentsMargins(0, 0, 0, 0)
1331 self.menu = qw.QMenu(self)
1332 action = qw.QAction('Save as', self.menu)
1333 action.triggered.connect(self.save_pixmap)
1334 self.menu.addAction(action)
1336 if filename:
1337 self.load_pixmap(filename)
1339 def contextMenuEvent(self, event):
1340 self.menu.popup(qg.QCursor.pos())
1342 def load_pixmap(self, filename):
1343 self.pixmap = qg.QPixmap(filename)
1344 self.setPixmap(self.pixmap)
1346 def save_pixmap(self, filename=None):
1347 if not filename:
1348 filename, _ = qw.QFileDialog.getSaveFileName(
1349 self.parent(), caption='save as')
1350 self.pixmap.save(filename)
1353class Projection(object):
1354 def __init__(self):
1355 self.xr = 0., 1.
1356 self.ur = 0., 1.
1358 def set_in_range(self, xmin, xmax):
1359 if xmax == xmin:
1360 xmax = xmin + 1.
1362 self.xr = xmin, xmax
1364 def get_in_range(self):
1365 return self.xr
1367 def set_out_range(self, umin, umax):
1368 if umax == umin:
1369 umax = umin + 1.
1371 self.ur = umin, umax
1373 def get_out_range(self):
1374 return self.ur
1376 def __call__(self, x):
1377 umin, umax = self.ur
1378 xmin, xmax = self.xr
1379 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
1381 def clipped(self, x):
1382 umin, umax = self.ur
1383 xmin, xmax = self.xr
1384 return min(umax, max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
1386 def rev(self, u):
1387 umin, umax = self.ur
1388 xmin, xmax = self.xr
1389 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
1392class NoData(Exception):
1393 pass
1396g_working_system_time_range = util.get_working_system_time_range()
1398g_initial_time_range = []
1400try:
1401 g_initial_time_range.append(
1402 calendar.timegm((1950, 1, 1, 0, 0, 0)))
1403except Exception:
1404 g_initial_time_range.append(g_working_system_time_range[0])
1406try:
1407 g_initial_time_range.append(
1408 calendar.timegm((time.gmtime().tm_year + 1, 1, 1, 0, 0, 0)))
1409except Exception:
1410 g_initial_time_range.append(g_working_system_time_range[1])
1413def four_way_arrow(position, size):
1414 r = 5.
1415 w = 1.
1417 points = [
1418 (position[0]+size*float(a), position[1]+size*float(b))
1419 for (a, b) in [
1420 (0, r),
1421 (1.5*w, r-2*w),
1422 (0.5*w, r-2*w),
1423 (0.5*w, 0.5*w),
1424 (r-2*w, 0.5*w),
1425 (r-2*w, 1.5*w),
1426 (r, 0),
1427 (r-2*w, -1.5*w),
1428 (r-2*w, -0.5*w),
1429 (0.5*w, -0.5*w),
1430 (0.5*w, -(r-2*w)),
1431 (1.5*w, -(r-2*w)),
1432 (0, -r),
1433 (-1.5*w, -(r-2*w)),
1434 (-0.5*w, -(r-2*w)),
1435 (-0.5*w, -0.5*w),
1436 (-(r-2*w), -0.5*w),
1437 (-(r-2*w), -1.5*w),
1438 (-r, 0),
1439 (-(r-2*w), 1.5*w),
1440 (-(r-2*w), 0.5*w),
1441 (-0.5*w, 0.5*w),
1442 (-0.5*w, r-2*w),
1443 (-1.5*w, r-2*w)]]
1445 poly = qg.QPolygon(len(points))
1446 for ipoint, point in enumerate(points):
1447 poly.setPoint(ipoint, *(int(round(v)) for v in point))
1449 return poly
1452def tmin_effective(tmin, tmax, tduration, tposition):
1453 if None in (tmin, tmax, tduration, tposition):
1454 return tmin
1455 else:
1456 return tmin + (tmax - tmin) * tposition
1459def tmax_effective(tmin, tmax, tduration, tposition):
1460 if None in (tmin, tmax, tduration, tposition):
1461 return tmax
1462 else:
1463 return tmin + (tmax - tmin) * tposition + tduration
1466class RangeEdit(qw.QFrame):
1468 rangeChanged = qc.pyqtSignal()
1469 focusChanged = qc.pyqtSignal()
1470 tcursorChanged = qc.pyqtSignal()
1471 rangeEditPressed = qc.pyqtSignal()
1472 rangeEditReleased = qc.pyqtSignal()
1474 def __init__(self, parent=None):
1475 qw.QFrame.__init__(self, parent)
1476 self.setFrameStyle(qw.QFrame.StyledPanel | qw.QFrame.Plain)
1477 # self.setBackgroundRole(qg.QPalette.Button)
1478 # self.setAutoFillBackground(True)
1479 self.setMouseTracking(True)
1480 poli = qw.QSizePolicy(
1481 qw.QSizePolicy.Expanding,
1482 qw.QSizePolicy.Fixed)
1484 self.setSizePolicy(poli)
1485 self.setMinimumSize(100, 3*24)
1487 self._size_hint = qw.QPushButton().sizeHint()
1489 self._track_start = None
1490 self._track_range = None
1491 self._track_focus = None
1492 self._track_what = None
1494 self._tcursor = None
1495 self._hover_point = None
1497 self._provider = None
1498 self.tmin, self.tmax = None, None
1499 self.tduration, self.tposition = None, 0.
1501 def set_data_provider(self, provider):
1502 self._provider = provider
1504 def set_data_name(self, name):
1505 self._data_name = name
1507 def sizeHint(self):
1508 ''
1509 return self._size_hint
1511 def get_data_range(self):
1512 if self._provider:
1513 vals = []
1514 for data in self._provider.iter_data(self._data_name):
1515 vals.append(data.min())
1516 vals.append(data.max())
1518 if vals:
1519 return min(vals), max(vals)
1521 return None, None
1523 def get_histogram(self, projection, h):
1524 h = int(h)
1525 umin_w, umax_w = projection.get_out_range()
1526 tmin_w, tmax_w = projection.get_in_range()
1527 nbins = int(umax_w - umin_w)
1528 counts = num.zeros(nbins, dtype=int)
1529 if self._provider:
1530 for data in self._provider.iter_data(self._data_name):
1531 ibins = ((data - tmin_w) * (nbins / (tmax_w - tmin_w))) \
1532 .astype(int)
1533 num.clip(ibins, 0, nbins-1, ibins)
1534 counts += num.bincount(ibins, minlength=nbins)
1536 histogram = counts * h // (num.max(counts[1:-1]) or 1)
1537 bitmap = num.zeros((h, nbins), dtype=bool)
1538 for i in range(h):
1539 bitmap[h-1-i, :] = histogram > i
1541 try:
1542 bitmap = num.packbits(bitmap, axis=1, bitorder='little')
1543 except TypeError:
1544 # numpy < 1.17.0 has no bitorder and default behaviour is 'big'
1545 bitmap = num.packbits(
1546 num.flip(bitmap.reshape((h*nbins//8, 8)), axis=1),
1547 axis=1)
1549 return qg.QBitmap.fromData(
1550 qc.QSize(nbins, h),
1551 bitmap.tobytes(),
1552 qg.QImage.Format_MonoLSB)
1554 def draw_time_ticks(self, painter, projection, rect):
1556 palette = self.palette()
1557 alpha_brush = palette.highlight()
1558 color = alpha_brush.color()
1559 # color.setAlpha(60)
1560 painter.setPen(qg.QPen(color))
1562 tmin, tmax = projection.get_in_range()
1563 tinc, tinc_unit = plot.nice_time_tick_inc((tmax - tmin) / 7.)
1564 tick_times, _ = plot.time_tick_labels(tmin, tmax, tinc, tinc_unit)
1566 for tick_time in tick_times:
1567 x = int(round(projection(tick_time)))
1568 painter.drawLine(
1569 x, rect.top(), x, rect.top() + rect.height() // 5)
1571 def drawit(self, painter):
1573 palette = self.palette()
1575 upper_projection = self.upper_projection()
1576 lower_projection = self.lower_projection()
1578 upper_rect = self.upper_rect()
1579 lower_rect = self.lower_rect()
1580 focus_rect = self.focus_rect(upper_projection)
1582 fill_brush = palette.brush(qg.QPalette.Button)
1583 painter.fillRect(upper_rect, fill_brush)
1585 if focus_rect:
1586 painter.setBrush(palette.light())
1587 poly = qg.QPolygon(8)
1588 poly.setPoint(
1589 0, lower_rect.x(), lower_rect.y())
1590 poly.setPoint(
1591 1, lower_rect.x(), lower_rect.y()+lower_rect.height())
1592 poly.setPoint(
1593 2, lower_rect.x() + lower_rect.width(),
1594 lower_rect.y() + lower_rect.height())
1595 poly.setPoint(
1596 3, lower_rect.x() + lower_rect.width(), lower_rect.y())
1597 poly.setPoint(
1598 4, focus_rect.x() + focus_rect.width(),
1599 upper_rect.y() + upper_rect.height())
1600 poly.setPoint(
1601 5, focus_rect.x() + focus_rect.width(), upper_rect.y())
1602 poly.setPoint(
1603 6, focus_rect.x(), upper_rect.y())
1604 poly.setPoint(
1605 7, focus_rect.x(), upper_rect.y() + upper_rect.height())
1606 painter.drawPolygon(poly)
1607 else:
1608 fill_brush = palette.light()
1609 painter.fillRect(upper_rect, fill_brush)
1611 # painter.setBrush(palette.text())
1612 # poly = four_way_arrow((self.width() / 2.0, self.height() / 2.0), 2.)
1613 # painter.drawPolygon(poly)
1615 self.draw_time_ticks(painter, upper_projection, upper_rect)
1616 if focus_rect and self.tduration:
1617 self.draw_time_ticks(painter, lower_projection, lower_rect)
1619 xpen = qg.QPen(palette.color(qg.QPalette.ButtonText))
1620 painter.setPen(xpen)
1621 painter.drawPixmap(
1622 0, upper_rect.x(),
1623 self.get_histogram(upper_projection, upper_rect.height()))
1625 if focus_rect and self.tduration:
1626 painter.drawPixmap(
1627 0, lower_rect.y(),
1628 self.get_histogram(lower_projection, lower_rect.height()))
1630 # frame_pen = qg.QPen(palette.color(qg.QPalette.ButtonText))
1631 # painter.setPen(frame_pen)
1632 # painter.drawRect(upper_rect)
1633 # if self.tduration:
1634 # painter.drawRect(lower_rect)
1636 if self._tcursor is not None:
1637 x = int(round(upper_projection(self._tcursor)))
1638 painter.drawLine(x, upper_rect.top(), x, upper_rect.bottom())
1639 if focus_rect and self.tduration and lower_projection:
1640 x = int(round(lower_projection(self._tcursor)))
1641 painter.drawLine(x, lower_rect.top(), x, lower_rect.bottom())
1643 if self._hover_point and lower_rect.contains(self._hover_point) \
1644 and not self.tduration and not self._track_start:
1646 alpha_brush = palette.highlight()
1647 color = alpha_brush.color()
1648 color.setAlpha(30)
1649 alpha_brush.setColor(color)
1650 painter.fillRect(lower_rect, alpha_brush)
1652 def upper_projection(self):
1653 p = Projection()
1654 if None in (self.tmin, self.tmax):
1655 p.set_in_range(*g_initial_time_range)
1656 else:
1657 p.set_in_range(self.tmin, self.tmax)
1659 p.set_out_range(0., self.width())
1660 return p
1662 def lower_projection(self):
1663 tmin_eff = self.tmin_effective()
1664 tmax_eff = self.tmax_effective()
1665 if None in (tmin_eff, tmax_eff):
1666 return None
1668 p = Projection()
1669 p.set_in_range(tmin_eff, tmax_eff)
1670 p.set_out_range(0., self.width())
1671 return p
1673 def tmin_effective(self):
1674 return tmin_effective(
1675 self.tmin, self.tmax, self.tduration, self.tposition)
1677 def tmax_effective(self):
1678 return tmax_effective(
1679 self.tmin, self.tmax, self.tduration, self.tposition)
1681 def upper_rect(self):
1682 vmin = 0
1683 vmax = self.height() // 3
1684 umin, umax = 0, self.width()
1685 return qc.QRect(umin, vmin, umax-umin, vmax-vmin)
1687 def lower_rect(self):
1688 vmin = 2 * self.height() // 3
1689 vmax = self.height()
1690 umin, umax = 0, self.width()
1691 return qc.QRect(umin, vmin, umax-umin, vmax-vmin)
1693 def focus_rect(self, projection):
1694 vmin = 0
1695 vmax = self.height() // 3
1697 tmin_eff = self.tmin_effective()
1698 tmax_eff = self.tmax_effective()
1699 if None in (tmin_eff, tmax_eff):
1700 return None
1702 umin = rint(projection(tmin_eff))
1703 umax = rint(projection(tmax_eff))
1705 return qc.QRect(umin, vmin, umax-umin+1, vmax-vmin)
1707 def set_range(self, tmin, tmax):
1708 if None in (tmin, tmax):
1709 tmin = None
1710 tmax = None
1711 elif tmin == tmax:
1712 tmin -= 0.5
1713 tmax += 0.5
1715 self.tmin = tmin
1716 self.tmax = tmax
1718 self.rangeChanged.emit()
1719 self.update()
1721 def get_range(self):
1722 return self.tmin, self.tmax
1724 def set_focus(self, tduration, tposition):
1725 self.tduration = tduration
1726 self.tposition = tposition
1727 self.focusChanged.emit()
1728 self.update()
1730 def get_focus(self):
1731 return (self.tduration, self.tposition)
1733 def get_tcursor(self):
1734 return self._tcursor
1736 def update_data_range(self):
1737 self.set_range(*self.get_data_range())
1739 def paintEvent(self, paint_ev):
1740 ''
1741 painter = qg.QPainter(self)
1742 painter.setRenderHint(qg.QPainter.Antialiasing)
1743 self.drawit(painter)
1744 qw.QFrame.paintEvent(self, paint_ev)
1746 def mousePressEvent(self, mouse_ev):
1747 ''
1748 if mouse_ev.button() == qc.Qt.LeftButton:
1749 self.rangeEditPressed.emit()
1751 if None in (self.tmin, self.tmax):
1752 self.set_range(*g_initial_time_range)
1754 self._track_start = mouse_ev.x(), mouse_ev.y()
1755 self._track_range = self.get_range()
1756 self._track_focus = self.get_focus()
1757 # upper_projection = self.upper_projection()
1758 # focus_rect = self.focus_rect(upper_projection)
1759 upper_rect = self.upper_rect()
1760 lower_rect = self.lower_rect()
1761 if upper_rect.contains(mouse_ev.pos()):
1762 self._track_what = 'global'
1763 elif lower_rect.contains(mouse_ev.pos()):
1764 self._track_what = 'focus'
1765 if self.tduration is None:
1766 frac = 0.02
1767 tduration = (self.tmax - self.tmin) * (1.0 - frac)
1768 tposition = 0.5*frac
1769 self.set_focus(tduration, tposition)
1771 else:
1772 if self.tduration is not None:
1773 self._track_what = 'focus_slide'
1774 else:
1775 self._track_what = 'global_slide'
1777 self.update()
1779 def enterEvent(self, ev):
1780 ''
1781 self._tcursor = None # is set later by mouseMoveEvent
1782 self._hover_point = None
1783 self.tcursorChanged.emit()
1785 def leaveEvent(self, ev):
1786 ''
1787 self._tcursor = None
1788 self._hover_point = None
1789 self.tcursorChanged.emit()
1790 self.update()
1792 def mouseReleaseEvent(self, mouse_ev):
1793 ''
1794 if self._track_start:
1795 self.rangeEditReleased.emit()
1796 self.update()
1798 self._track_start = None
1799 self._track_range = None
1800 self._track_focus = None
1801 self._track_what = None
1802 if self.tduration is not None:
1803 if self.tduration >= self.tmax - self.tmin:
1804 self.set_focus(None, 0.0)
1805 elif self.tposition < 0.:
1806 self.set_focus(self.tduration, 0.0)
1807 elif self.tposition > 1.0 - self.tduration \
1808 / (self.tmax - self.tmin):
1809 self.set_focus(
1810 self.tduration, 1.0 - self.tduration
1811 / (self.tmax - self.tmin))
1813 def mouseDoubleClickEvent(self, mouse_ev):
1814 ''
1815 if mouse_ev.button() == qc.Qt.LeftButton:
1816 lower_rect = self.lower_rect()
1817 if lower_rect.contains(mouse_ev.pos()) \
1818 and self.tduration is not None:
1820 etmin = self.tmin_effective()
1821 etmax = self.tmax_effective()
1822 self.set_range(etmin, etmax)
1823 self.set_focus(None, 0.0)
1825 upper_rect = self.upper_rect()
1826 if upper_rect.contains(mouse_ev.pos()) \
1827 and self.tduration is not None:
1829 self.set_focus(None, 0.0)
1831 def mouseMoveEvent(self, mouse_ev):
1832 ''
1833 point = self.mapFromGlobal(mouse_ev.globalPos())
1834 self._hover_point = point
1836 if self._track_start is not None:
1837 x0, y0 = self._track_start
1838 dx = (point.x() - x0)/float(self.width())
1839 dy = (point.y() - y0)/float(self.height())
1840 xfrac = x0/float(self.width())
1841 tmin0, tmax0 = self._track_range
1842 tduration0, tposition0 = self._track_focus
1844 if self._track_what in ('global', 'global_slide'):
1845 if self._track_what == 'global':
1846 scale = math.exp(-dy)
1847 else:
1848 scale = 1.0
1850 dtr = (tmax0-tmin0) * (scale - 1.0)
1851 dt = dx*(tmax0-tmin0)*scale
1853 tmin = tmin0 - dt - dtr*xfrac
1854 tmax = tmax0 - dt + dtr*(1.-xfrac)
1856 self.set_range(tmin, tmax)
1858 tduration, tposition = self._track_focus
1859 if tduration is not None:
1860 etmin0 = tmin_effective(
1861 tmin0, tmax0, tduration0, tposition0)
1863 tposition = (etmin0 - tmin) / (tmax - tmin)
1864 self.set_focus(tduration0, tposition)
1866 elif self._track_what == 'focus':
1867 if tduration0 is not None:
1868 scale = math.exp(-dy)
1870 dtr = tduration0 * (scale - 1.0)
1871 dt = dx * tduration0 * scale
1873 etmin0 = tmin_effective(
1874 tmin0, tmax0, tduration0, tposition0)
1875 etmax0 = tmax_effective(
1876 tmin0, tmax0, tduration0, tposition0)
1878 tmin = etmin0 - dt - dtr*xfrac
1879 tmax = etmax0 - dt + dtr*(1.-xfrac)
1881 tduration = tmax - tmin
1883 tposition = (tmin - tmin0) / (tmax0 - tmin0)
1884 tposition = min(
1885 max(0., tposition),
1886 1.0 - tduration / (tmax0 - tmin0))
1888 if tduration < (tmax0 - tmin0):
1889 self.set_focus(tduration, tposition)
1890 else:
1891 self.set_focus(None, tposition)
1893 else:
1894 tduration, tposition = tmax0 - tmin0, 0.0
1895 self.set_focus(tduration, tposition)
1896 self._track_focus = (tduration, tposition)
1898 elif self._track_what == 'focus_slide':
1899 if tduration0 is not None:
1900 self.set_focus(
1901 tduration0,
1902 min(
1903 max(0., tposition0 + dx),
1904 1.0 - tduration0 / (tmax0 - tmin0)))
1906 else:
1908 upper_rect = self.upper_rect()
1909 lower_rect = self.lower_rect()
1910 upper_projection = self.upper_projection()
1911 lower_projection = self.lower_projection()
1913 app = get_app()
1914 have_focus = lower_projection and self.tduration is not None
1916 if upper_rect.contains(point):
1917 self.setCursor(qg.QCursor(qc.Qt.CursorShape.CrossCursor))
1918 self._tcursor = upper_projection.rev(point.x())
1919 app.status(
1920 'Click and drag to change global time interval. '
1921 'Move up/down to zoom.' + (
1922 ' Double-click to clear focus time interval.'
1923 if have_focus else ''))
1925 elif lower_rect.contains(point):
1926 self.setCursor(qg.QCursor(qc.Qt.CursorShape.CrossCursor))
1927 if have_focus:
1928 self._tcursor = lower_projection.rev(point.x())
1929 app.status(
1930 'Click and drag to change local time interval. '
1931 'Double-click to set global time interval from focus.')
1932 else:
1933 app.status(
1934 'Click to activate focus time window.')
1935 else:
1936 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor))
1937 self._tcursor = None
1938 if have_focus:
1939 app.status('Move focus time interval with fixed length.')
1940 else:
1941 app.status('Move global time interval with fixed length.')
1943 self.update()
1944 self.tcursorChanged.emit()
1947class StatusMessages(qw.QLabel):
1948 def __init__(self):
1949 qw.QLabel.__init__(self)
1950 self._messages = {}
1951 self._timers = {}
1953 def set(self, key, text, timeout=5.0):
1954 self._messages[key] = text
1955 timer = qc.QTimer()
1956 timer.setSingleShot(True)
1957 timer.setInterval(int(timeout*1000))
1959 def clear():
1960 self.clear(key)
1962 timer.timeout.connect(clear)
1963 timer.start()
1964 if key in self._timers:
1965 self._timers[key].stop()
1967 self._timers[key] = timer
1968 self.update_label()
1970 def clear(self, key):
1971 try:
1972 del self._messages[key]
1973 except KeyError:
1974 pass
1975 try:
1976 del self._timers[key]
1977 except KeyError:
1978 pass
1980 self.update_label()
1982 def update_label(self):
1983 messages = []
1984 for key, text in self._messages.items():
1985 messages.append(text)
1987 if messages:
1988 message = '\u29BF ' + ' - '.join(messages)
1989 else:
1990 message = ''
1992 self.setText(message)
1995def errorize(widget):
1996 widget.setStyleSheet('''
1997 QLineEdit {
1998 background: rgb(200, 150, 150);
1999 }''')
2002def de_errorize(widget):
2003 if isinstance(widget, qw.QWidget):
2004 widget.setStyleSheet('')
2007_call_later_timers = {}
2010def _remove_call_later(ref):
2011 del _call_later_timers[ref]
2014def call_later(method, delay=0):
2015 ref = weakref.WeakMethod(method, _remove_call_later)
2016 previous_timer = _call_later_timers.pop(ref, None)
2017 if previous_timer is not None:
2018 previous_timer.stop()
2020 def call():
2021 _remove_call_later(ref)
2022 method = ref()
2023 if method is not None:
2024 method()
2026 timer = qc.QTimer()
2027 timer.setSingleShot(True)
2028 timer.setInterval(delay)
2029 timer.timeout.connect(call)
2030 timer.start()
2032 _call_later_timers[ref] = timer
2035def time_or_none_to_str(t):
2036 if t is None:
2037 return ''
2038 else:
2039 return util.time_to_str(t)
2042def time_to_lineedit(state, attribute, widget):
2043 widget.setText(time_or_none_to_str(getattr(state, attribute)))
2046def lineedit_to_time(widget, state, attribute):
2047 s = str(widget.text())
2048 if not s.strip():
2049 setattr(state, attribute, None)
2050 else:
2051 try:
2052 setattr(state, attribute, util.str_to_time_fillup(s))
2053 except Exception:
2054 raise ValueError(
2055 'Use time format: YYYY-MM-DD HH:MM:SS.FFF')