Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/util.py: 54%
1237 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-06 06:59 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-06 06:59 +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
15from matplotlib.colors import Normalize
17from .qt_compat import qc, qg, qw
19from .snuffler.marker import Marker, PhaseMarker, EventMarker # noqa
20from .snuffler.marker import MarkerParseError, MarkerOneNSLCRequired # noqa
21from .snuffler.marker import load_markers, save_markers # noqa
22from pyrocko import plot, util
25logger = logging.getLogger('pyrocko.gui.util')
28class _Getch:
29 '''
30 Gets a single character from standard input.
32 Does not echo to the screen.
34 https://stackoverflow.com/questions/510357/how-to-read-a-single-character-from-the-user
35 '''
36 def __init__(self):
37 try:
38 self.impl = _GetchWindows()
39 except ImportError:
40 self.impl = _GetchUnix()
42 def __call__(self): return self.impl()
45class _GetchUnix:
46 def __init__(self):
47 import tty, sys # noqa
49 def __call__(self):
50 import sys
51 import tty
52 import termios
54 fd = sys.stdin.fileno()
55 old_settings = termios.tcgetattr(fd)
56 try:
57 tty.setraw(fd)
58 ch = sys.stdin.read(1)
59 finally:
60 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
62 return ch
65class _GetchWindows:
66 def __init__(self):
67 import msvcrt # noqa
69 def __call__(self):
70 import msvcrt
71 return msvcrt.getch()
74getch = _Getch()
77class PyrockoQApplication(qw.QApplication):
79 def __init__(self):
80 qw.QApplication.__init__(self, [])
81 self._main_window = None
83 def install_sigint_handler(self):
84 self._old_signal_handler = signal.signal(
85 signal.SIGINT,
86 self.request_close_all_windows)
88 def uninstall_sigint_handler(self):
89 signal.signal(signal.SIGINT, self._old_signal_handler)
91 def set_main_window(self, win):
92 self._main_window = win
93 name = win.windowTitle() if win is not None else ''
94 self.setApplicationName(name)
95 self.setApplicationDisplayName(name)
96 self.setDesktopFileName(name)
98 def unset_main_window(self):
99 self.set_main_window(None)
101 def get_main_window(self):
102 return self._main_window
104 def get_main_windows(self):
105 return [self.get_main_window()]
107 def status(self, message, duration=None):
108 win = self.get_main_window()
109 if not win:
110 print(' - %s' % message, file=sys.stderr)
111 else:
112 win.statusBar().showMessage(
113 message, int((duration or 0) * 1000))
115 def event(self, e):
116 if isinstance(e, qg.QFileOpenEvent):
117 path = str(e.file())
118 if path != sys.argv[0]:
119 wins = self.get_main_windows()
120 if wins:
121 wins[0].get_view().load_soon([path])
123 return True
124 else:
125 return qw.QApplication.event(self, e)
127 def request_close_all_windows(self, *args):
129 def confirm():
130 try:
131 print(
132 '\nQuit %s? [y/n]' % self.applicationName(),
133 file=sys.stderr)
135 confirmed = getch() == 'y'
136 if not confirmed:
137 print(
138 'Continuing.',
139 file=sys.stderr)
140 else:
141 print(
142 'Quitting %s.' % self.applicationName(),
143 file=sys.stderr)
145 return confirmed
147 except Exception:
148 return False
150 if confirm():
151 for win in self.get_main_windows():
152 win.instant_close = True
154 self.closeAllWindows()
157app = None
160def get_app():
161 from .qt_compat import qg
162 try:
163 global app
164 if app is None:
165 qg.QSurfaceFormat.setDefaultFormat(qg.QSurfaceFormat())
166 app = PyrockoQApplication()
167 return app
168 except NameError: # can happen during shutdown
169 return None
172def rint(x):
173 return int(round(x))
176def make_QPolygonF(xdata, ydata):
177 assert len(xdata) == len(ydata)
178 qpoints = qg.QPolygonF(len(ydata))
179 vptr = qpoints.data()
180 vptr.setsize(len(ydata)*8*2)
181 aa = num.ndarray(
182 shape=(len(ydata), 2),
183 dtype=num.float64,
184 buffer=memoryview(vptr))
185 aa.setflags(write=True)
186 aa[:, 0] = xdata
187 aa[:, 1] = ydata
188 return qpoints
191def get_colormap_qimage(cmap_name, vmin=None, vmax=None):
192 NCOLORS = 512
193 norm = Normalize()
194 norm.vmin = vmin
195 norm.vmax = vmax
197 return qg.QImage(
198 plot.mpl_get_cmap(cmap_name)(
199 norm(num.linspace(0., 1., NCOLORS)),
200 alpha=None, bytes=True),
201 NCOLORS, 1, qg.QImage.Format_RGBX8888)
204class Label(object):
205 def __init__(
206 self, p, x, y, label_str,
207 label_bg=None,
208 anchor='BL',
209 outline=False,
210 font=None,
211 color=None):
213 text = qg.QTextDocument()
214 if font:
215 text.setDefaultFont(font)
216 text.setDefaultStyleSheet('span { color: %s; }' % color.name())
217 text.setHtml('<span>%s</span>' % label_str)
218 s = text.size()
219 rect = qc.QRect(0, 0, int(s.width()), int(s.height()))
220 tx, ty = x, y
222 if 'B' in anchor:
223 ty -= rect.height()
224 if 'R' in anchor:
225 tx -= rect.width()
226 if 'M' in anchor:
227 ty -= rect.height() // 2
228 if 'C' in anchor:
229 tx -= rect.width() // 2
231 rect.translate(int(tx), int(ty))
232 self.rect = rect
233 self.text = text
234 self.outline = outline
235 self.label_bg = label_bg
236 self.color = color
237 self.p = p
239 def draw(self):
240 p = self.p
241 rect = self.rect
242 tx = rect.left()
243 ty = rect.top()
245 if self.outline:
246 oldpen = p.pen()
247 oldbrush = p.brush()
248 p.setBrush(self.label_bg)
249 rect.adjust(-2, 0, 2, 0)
250 p.drawRect(rect)
251 p.setPen(oldpen)
252 p.setBrush(oldbrush)
254 else:
255 if self.label_bg:
256 p.fillRect(rect, self.label_bg)
258 p.translate(int(tx), int(ty))
259 self.text.drawContents(p)
260 p.translate(-int(tx), -int(ty))
263def draw_label(p, x, y, label_str, label_bg, anchor='BL', outline=False):
264 fm = p.fontMetrics()
266 label = label_str
267 rect = fm.boundingRect(label)
269 tx, ty = x, y
270 if 'T' in anchor:
271 ty += rect.height()
272 if 'R' in anchor:
273 tx -= rect.width()
274 if 'M' in anchor:
275 ty += rect.height() // 2
276 if 'C' in anchor:
277 tx -= rect.width() // 2
279 rect.translate(int(tx), int(ty))
280 if outline:
281 oldpen = p.pen()
282 oldbrush = p.brush()
283 p.setBrush(label_bg)
284 rect.adjust(-2, 0, 2, 0)
285 p.drawRect(rect)
286 p.setPen(oldpen)
287 p.setBrush(oldbrush)
289 else:
290 p.fillRect(rect, label_bg)
292 p.drawText(int(tx), int(ty), label)
295def get_err_palette():
296 err_palette = qg.QPalette()
297 err_palette.setColor(qg.QPalette.Base, qg.QColor(255, 200, 200))
298 return err_palette
301class QSliderNoWheel(qw.QSlider):
303 def wheelEvent(self, ev):
304 ''
305 ev.ignore()
307 def keyPressEvent(self, ev):
308 ''
309 ev.ignore()
312class QSliderFloat(qw.QSlider):
314 sliderMovedFloat = qc.pyqtSignal(float)
315 valueChangedFloat = qc.pyqtSignal(float)
316 rangeChangedFloat = qc.pyqtSignal(float, float)
318 def __init__(self, *args, **kwargs):
319 qw.QSlider.__init__(self, *args, **kwargs)
320 self.setMinimum(0)
321 self.setMaximum(1000)
322 self.setSingleStep(10)
323 self.setPageStep(100)
324 self._fmin = 0.
325 self._fmax = 1.
326 self.valueChanged.connect(self._handleValueChanged)
327 self.sliderMoved.connect(self._handleSliderMoved)
329 def _f_to_i(self, fval):
330 fval = float(fval)
331 imin = self.minimum()
332 imax = self.maximum()
333 return max(
334 imin,
335 imin + min(
336 int(round(
337 (fval-self._fmin) * (imax - imin)
338 / (self._fmax-self._fmin))),
339 imax))
341 def _i_to_f(self, ival):
342 imin = self.minimum()
343 imax = self.maximum()
344 return self._fmin + (ival - imin) * (self._fmax - self._fmin) \
345 / (imax - imin)
347 def minimumFloat(self):
348 return self._fmin
350 def setMinimumFloat(self, fval):
351 self._fmin = float(fval)
352 self.rangeChangedFloat.emit(self._fmin, self._fmax)
354 def maximumFloat(self):
355 return self._fmax
357 def setMaximumFloat(self, fval):
358 self._fmax = float(fval)
359 self.rangeChangedFloat.emit(self._fmin, self._fmax)
361 def setRangeFloat(self, fmin, fmax):
362 self._fmin = float(fmin)
363 self._fmax = float(fmax)
364 self.rangeChangedFloat.emit(self._fmin, self._fmax)
366 def valueFloat(self):
367 return self._i_to_f(self.value())
369 def setValueFloat(self, fval):
370 qw.QSlider.setValue(self, self._f_to_i(fval))
372 def _handleValueChanged(self, ival):
373 self.valueChangedFloat.emit(self._i_to_f(ival))
375 def _handleSliderMoved(self, ival):
376 self.sliderMovedFloat.emit(self._i_to_f(ival))
379class MyValueEdit(qw.QLineEdit):
381 edited = qc.pyqtSignal(float)
383 def __init__(
384 self,
385 low_is_none=False,
386 high_is_none=False,
387 low_is_zero=False,
388 *args, **kwargs):
390 qw.QLineEdit.__init__(self, *args, **kwargs)
391 self.value = 0.
392 self.mi = 0.
393 self.ma = 1.
394 self.low_is_none = low_is_none
395 self.high_is_none = high_is_none
396 self.low_is_zero = low_is_zero
397 self.editingFinished.connect(
398 self.myEditingFinished)
399 self.lock = False
401 def setRange(self, mi, ma):
402 self.mi = mi
403 self.ma = ma
405 def setValue(self, value):
406 if not self.lock:
407 self.value = value
408 self.setPalette(qw.QApplication.palette())
409 self.adjust_text()
411 def myEditingFinished(self):
412 try:
413 t = str(self.text()).strip()
414 if self.low_is_none and t in ('off', 'below'):
415 value = self.mi
416 elif self.high_is_none and t in ('off', 'above'):
417 value = self.ma
418 elif self.low_is_zero and float(t) == 0.0:
419 value = self.mi
420 else:
421 value = float(t)
423 if not (self.mi <= value <= self.ma):
424 raise Exception('out of range')
426 if value != self.value:
427 self.value = value
428 self.lock = True
429 self.edited.emit(value)
430 self.setPalette(qw.QApplication.palette())
431 except Exception:
432 self.setPalette(get_err_palette())
434 self.lock = False
436 def adjust_text(self):
437 t = ('%8.5g' % self.value).strip()
439 if self.low_is_zero and self.value == self.mi:
440 t = '0'
442 if self.low_is_none and self.value == self.mi:
443 if self.high_is_none:
444 t = 'below'
445 else:
446 t = 'off'
448 if self.high_is_none and self.value == self.ma:
449 if self.low_is_none:
450 t = 'above'
451 else:
452 t = 'off'
454 if t in ('off', 'below', 'above'):
455 self.setStyleSheet('font-style: italic;')
456 else:
457 self.setStyleSheet(None)
459 self.setText(t)
462class ValControl(qw.QWidget):
464 valchange = qc.pyqtSignal(object, int)
466 def __init__(
467 self,
468 low_is_none=False,
469 high_is_none=False,
470 low_is_zero=False,
471 type=float,
472 *args):
474 qc.QObject.__init__(self, *args)
476 self.lname = qw.QLabel('name')
477 self.lname.setSizePolicy(
478 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum))
479 self.lvalue = MyValueEdit(
480 low_is_none=low_is_none,
481 high_is_none=high_is_none,
482 low_is_zero=low_is_zero)
483 self.lvalue.setFixedWidth(100)
484 self.slider = QSliderNoWheel(qc.Qt.Horizontal)
485 self.slider.setSizePolicy(
486 qw.QSizePolicy(qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
487 self.slider.setMaximum(10000)
488 self.slider.setSingleStep(100)
489 self.slider.setPageStep(1000)
490 self.slider.setTickPosition(qw.QSlider.NoTicks)
491 self.slider.setFocusPolicy(qc.Qt.ClickFocus)
493 self.low_is_none = low_is_none
494 self.high_is_none = high_is_none
495 self.low_is_zero = low_is_zero
497 self.slider.valueChanged.connect(
498 self.slided)
499 self.lvalue.edited.connect(
500 self.edited)
502 self.type = type
503 self.mute = False
505 def widgets(self):
506 return self.lname, self.lvalue, self.slider
508 def s2v(self, svalue):
509 if self.ma == 0 or self.mi == 0:
510 return 0
512 a = math.log(self.ma/self.mi) / 10000.
513 value = self.mi*math.exp(a*svalue)
514 value = self.type(value)
515 return value
517 def v2s(self, value):
518 value = self.type(value)
520 if value == 0 or self.mi == 0:
521 return 0
523 a = math.log(self.ma/self.mi) / 10000.
524 return int(round(math.log(value/self.mi) / a))
526 def setup(self, name, mi, ma, cur, ind):
527 self.lname.setText(name)
528 self.mi = mi
529 self.ma = ma
530 self.ind = ind
531 self.lvalue.setRange(self.s2v(0), self.s2v(10000))
532 self.set_value(cur)
534 def set_range(self, mi, ma):
535 if self.mi == mi and self.ma == ma:
536 return
538 vput = None
539 if self.cursl == 0:
540 vput = mi
541 if self.cursl == 10000:
542 vput = ma
544 self.mi = mi
545 self.ma = ma
546 self.lvalue.setRange(self.s2v(0), self.s2v(10000))
548 if vput is not None:
549 self.set_value(vput)
550 else:
551 if self.cur < mi:
552 self.set_value(mi)
553 if self.cur > ma:
554 self.set_value(ma)
556 def set_value(self, cur):
557 if cur is None:
558 if self.low_is_none:
559 cur = self.mi
560 elif self.high_is_none:
561 cur = self.ma
563 if cur == 0.0:
564 if self.low_is_zero:
565 cur = self.mi
567 self.mute = True
568 self.cur = cur
569 self.cursl = self.v2s(cur)
570 self.slider.blockSignals(True)
571 self.slider.setValue(self.cursl)
572 self.slider.blockSignals(False)
573 self.lvalue.blockSignals(True)
574 if self.cursl in (0, 10000):
575 self.lvalue.setValue(self.s2v(self.cursl))
576 else:
577 self.lvalue.setValue(self.cur)
578 self.lvalue.blockSignals(False)
579 self.mute = False
581 def set_tracking(self, tracking):
582 self.slider.setTracking(tracking)
584 def get_value(self):
585 return self.cur
587 def slided(self, val):
588 if self.cursl != val:
589 self.cursl = val
590 cur = self.s2v(self.cursl)
592 if cur != self.cur:
593 self.cur = cur
594 self.lvalue.blockSignals(True)
595 self.lvalue.setValue(self.cur)
596 self.lvalue.blockSignals(False)
597 self.fire_valchange()
599 def edited(self, val):
600 if self.cur != val:
601 self.cur = val
602 cursl = self.v2s(val)
603 if (cursl != self.cursl):
604 self.slider.blockSignals(True)
605 self.slider.setValue(cursl)
606 self.slider.blockSignals(False)
607 self.cursl = cursl
609 self.fire_valchange()
611 def fire_valchange(self):
613 if self.mute:
614 return
616 cur = self.cur
618 if self.cursl == 0:
619 if self.low_is_none:
620 cur = None
622 elif self.low_is_zero:
623 cur = 0.0
625 if self.cursl == 10000 and self.high_is_none:
626 cur = None
628 self.valchange.emit(cur, int(self.ind))
631class LinValControl(ValControl):
633 def s2v(self, svalue):
634 value = svalue/10000. * (self.ma-self.mi) + self.mi
635 value = self.type(value)
636 return value
638 def v2s(self, value):
639 value = self.type(value)
640 if self.ma == self.mi:
641 return 0
642 return int(round((value-self.mi)/(self.ma-self.mi) * 10000.))
645class ColorbarControl(qw.QWidget):
647 AVAILABLE_CMAPS = (
648 'viridis',
649 'plasma',
650 'magma',
651 'binary',
652 'Reds',
653 'copper',
654 'seismic',
655 'RdBu',
656 'YlGn',
657 )
659 DEFAULT_CMAP = 'viridis'
661 cmap_changed = qc.pyqtSignal(str)
662 show_absolute_toggled = qc.pyqtSignal(bool)
663 show_integrate_toggled = qc.pyqtSignal(bool)
665 def __init__(self, *args, **kwargs):
666 super().__init__(*args, **kwargs)
668 self.lname = qw.QLabel('Colormap')
669 self.lname.setSizePolicy(
670 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum))
672 self.cmap_options = qw.QComboBox()
673 self.cmap_options.setIconSize(qc.QSize(64, 12))
674 for ic, cmap in enumerate(self.AVAILABLE_CMAPS):
675 pixmap = qg.QPixmap.fromImage(
676 get_colormap_qimage(cmap))
677 icon = qg.QIcon(pixmap.scaled(64, 12))
679 self.cmap_options.addItem(icon, '', cmap)
680 self.cmap_options.setItemData(ic, cmap, qc.Qt.ToolTipRole)
682 # self.cmap_options.setCurrentIndex(self.cmap_name)
683 self.cmap_options.currentIndexChanged.connect(self.set_cmap)
684 self.cmap_options.setSizePolicy(
685 qw.QSizePolicy(qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum))
687 self.colorslider = ColorbarSlider(self)
688 self.colorslider.setSizePolicy(
689 qw.QSizePolicy.MinimumExpanding | qw.QSizePolicy.ExpandFlag,
690 qw.QSizePolicy.MinimumExpanding | qw.QSizePolicy.ExpandFlag
691 )
692 self.clip_changed = self.colorslider.clip_changed
694 btn_size = qw.QSizePolicy(
695 qw.QSizePolicy.Maximum | qw.QSizePolicy.ShrinkFlag,
696 qw.QSizePolicy.Maximum | qw.QSizePolicy.ShrinkFlag)
698 self.symetry_toggle = qw.QPushButton()
699 self.symetry_toggle.setIcon(
700 qg.QIcon.fromTheme('object-flip-horizontal'))
701 self.symetry_toggle.setToolTip('Symetric clip values')
702 self.symetry_toggle.setSizePolicy(btn_size)
703 self.symetry_toggle.setCheckable(True)
704 self.symetry_toggle.toggled.connect(self.toggle_symetry)
705 self.symetry_toggle.setChecked(True)
707 self.reverse_toggle = qw.QPushButton()
708 self.reverse_toggle.setIcon(
709 qg.QIcon.fromTheme('object-rotate-right'))
710 self.reverse_toggle.setToolTip('Reverse the colormap')
711 self.reverse_toggle.setSizePolicy(btn_size)
712 self.reverse_toggle.setCheckable(True)
713 self.reverse_toggle.toggled.connect(self.toggle_reverse_cmap)
715 self.abs_toggle = qw.QPushButton()
716 self.abs_toggle.setIcon(
717 qg.QIcon.fromTheme('go-bottom'))
718 self.abs_toggle.setToolTip('Show absolute values')
719 self.abs_toggle.setSizePolicy(btn_size)
720 self.abs_toggle.setCheckable(True)
721 self.abs_toggle.toggled.connect(self.toggle_absolute)
723 self.int_toggle = qw.QPushButton()
724 self.int_toggle.setText('∫')
725 self.int_toggle.setToolTip(
726 u'Integrate traces (e.g. strain rate → strain)')
727 self.int_toggle.setSizePolicy(btn_size)
728 self.int_toggle.setCheckable(True)
729 self.int_toggle.setMaximumSize(
730 24,
731 self.int_toggle.maximumSize().height())
732 self.int_toggle.toggled.connect(self.show_integrate_toggled.emit)
734 v_splitter = qw.QFrame()
735 v_splitter.setFrameShape(qw.QFrame.VLine)
736 v_splitter.setFrameShadow(qw.QFrame.Sunken)
738 self.controls = qw.QWidget()
739 layout = qw.QHBoxLayout()
740 layout.addWidget(self.colorslider)
741 layout.addWidget(self.symetry_toggle)
742 layout.addWidget(self.reverse_toggle)
743 layout.addWidget(v_splitter)
744 layout.addWidget(self.abs_toggle)
745 layout.addWidget(self.int_toggle)
746 self.controls.setLayout(layout)
748 self.set_cmap_name(self.DEFAULT_CMAP)
750 def set_cmap(self, idx):
751 self.set_cmap_name(self.cmap_options.itemData(idx))
753 def set_cmap_name(self, cmap_name):
754 self.cmap_name = cmap_name
755 self.colorslider.set_cmap_name(cmap_name)
756 self.cmap_changed.emit(cmap_name)
758 def get_cmap(self):
759 return self.cmap_name
761 def toggle_symetry(self, toggled):
762 self.colorslider.set_symetry(toggled)
764 def toggle_reverse_cmap(self):
765 cmap = self.get_cmap()
766 if cmap.endswith('_r'):
767 r_cmap = cmap.rstrip('_r')
768 else:
769 r_cmap = cmap + '_r'
770 self.set_cmap_name(r_cmap)
772 def toggle_absolute(self, toggled):
773 self.symetry_toggle.setChecked(not toggled)
774 self.show_absolute_toggled.emit(toggled)
776 def widgets(self):
777 return (self.lname, self.cmap_options, self.controls)
780class ColorbarSlider(qw.QWidget):
781 DEFAULT_CMAP = 'viridis'
782 CORNER_THRESHOLD = 10
783 MIN_WIDTH = .05
785 clip_changed = qc.pyqtSignal(float, float)
787 class COMPONENTS(enum.Enum):
788 LeftLine = 1
789 RightLine = 2
790 Center = 3
792 def __init__(self, *args, cmap_name=None):
793 super().__init__()
794 self.cmap_name = cmap_name or self.DEFAULT_CMAP
795 self.clip_min = 0.
796 self.clip_max = 1.
798 self._sym_locked = True
799 self._mouse_inside = False
800 self._window = None
801 self._old_pos = None
802 self._component_grabbed = None
804 self.setMouseTracking(True)
806 def set_cmap_name(self, cmap_name):
807 self.cmap_name = cmap_name
808 self.repaint()
810 def get_cmap_name(self):
811 return self.cmap_name
813 def set_symetry(self, symetry):
814 self._sym_locked = symetry
815 if self._sym_locked:
816 clip_max = 1. - min(self.clip_min, 1.-self.clip_max)
817 clip_min = 1. - clip_max
818 self.set_clip(clip_min, clip_max)
820 def _set_window(self, window):
821 self._window = window
823 def _get_left_line(self):
824 rect = self._get_active_rect()
825 if not rect:
826 return
827 return qc.QLineF(rect.left(), 0, rect.left(), rect.height())
829 def _get_right_line(self):
830 rect = self._get_active_rect()
831 if not rect:
832 return
833 return qc.QLineF(rect.right(), 0, rect.right(), rect.height())
835 def _get_active_rect(self):
836 if not self._window:
837 return
838 rect = qc.QRect(self._window)
839 width = rect.width()
840 rect.setLeft(width * self.clip_min)
841 rect.setRight(width * self.clip_max)
842 return rect
844 def set_clip(self, clip_min, clip_max):
845 if clip_min < 0. or clip_max > 1.:
846 return
847 if clip_max - clip_min < self.MIN_WIDTH:
848 return
850 self.clip_min = clip_min
851 self.clip_max = clip_max
852 self.repaint()
853 self.clip_changed.emit(self.clip_min, self.clip_max)
855 def mousePressEvent(self, event):
856 ''
857 act_rect = self._get_active_rect()
858 if event.buttons() != qc.Qt.MouseButton.LeftButton:
859 self._component_grabbed = None
860 return
862 dist_left = abs(event.pos().x() - act_rect.left())
863 dist_right = abs(event.pos().x() - act_rect.right())
865 if 0 < dist_left < self.CORNER_THRESHOLD:
866 self._component_grabbed = self.COMPONENTS.LeftLine
867 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor))
868 elif 0 < dist_right < self.CORNER_THRESHOLD:
869 self._component_grabbed = self.COMPONENTS.RightLine
870 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor))
871 else:
872 self.setCursor(qg.QCursor())
874 def mouseReleaseEvent(self, event):
875 ''
876 self._component_grabbed = None
877 self.repaint()
879 def mouseDoubleClickEvent(self, event):
880 ''
881 self.set_clip(0., 1.)
883 def wheelEvent(self, event):
884 ''
885 event.accept()
886 if not self._sym_locked:
887 return
889 delta = event.angleDelta().y()
890 delta = -delta / 5e3
891 clip_min_new = max(self.clip_min + delta, 0.)
892 clip_max_new = min(self.clip_max - delta, 1.)
893 self._mouse_inside = True
894 self.set_clip(clip_min_new, clip_max_new)
896 def mouseMoveEvent(self, event):
897 ''
898 act_rect = self._get_active_rect()
900 if not self._component_grabbed:
901 dist_left = abs(event.pos().x() - act_rect.left())
902 dist_right = abs(event.pos().x() - act_rect.right())
904 if 0 <= dist_left < self.CORNER_THRESHOLD or \
905 0 <= dist_right < self.CORNER_THRESHOLD:
906 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor))
907 else:
908 self.setCursor(qg.QCursor())
910 if self._old_pos and self._component_grabbed:
911 shift = (event.pos() - self._old_pos).x() / self._window.width()
913 if self._component_grabbed is self.COMPONENTS.LeftLine:
914 clip_min_new = max(self.clip_min + shift, 0.)
915 clip_max_new = \
916 min(self.clip_max - shift, 1.) \
917 if self._sym_locked else self.clip_max
919 elif self._component_grabbed is self.COMPONENTS.RightLine:
920 clip_max_new = min(self.clip_max + shift, 1.)
921 clip_min_new = \
922 max(self.clip_min - shift, 0.) \
923 if self._sym_locked else self.clip_min
925 self.set_clip(clip_min_new, clip_max_new)
927 self._old_pos = event.pos()
929 def enterEvent(self, e):
930 ''
931 self._mouse_inside = True
932 self.repaint()
934 def leaveEvent(self, e):
935 ''
936 self._mouse_inside = False
937 self.repaint()
939 def paintEvent(self, e):
940 ''
941 p = qg.QPainter(self)
942 self._set_window(p.window())
944 p.drawImage(
945 p.window(),
946 get_colormap_qimage(self.cmap_name, self.clip_min, self.clip_max))
948 left_line = self._get_left_line()
949 right_line = self._get_right_line()
951 pen = qg.QPen()
952 pen.setWidth(2)
953 pen.setStyle(qc.Qt.DotLine)
954 pen.setBrush(qc.Qt.white)
955 p.setPen(pen)
956 p.setCompositionMode(
957 qg.QPainter.CompositionMode.CompositionMode_Difference)
959 p.drawLine(left_line)
960 p.drawLine(right_line)
962 label_rect = self._get_active_rect()
963 label_rect.setLeft(label_rect.left() + 5)
964 label_rect.setRight(label_rect.right() - 5)
965 label_left_rect = qc.QRectF(label_rect)
966 label_right_rect = qc.QRectF(label_rect)
967 label_left_align = qc.Qt.AlignLeft
968 label_right_align = qc.Qt.AlignRight
970 if label_rect.left() > 50:
971 label_left_rect.setRight(label_rect.left() - 10)
972 label_left_rect.setLeft(0)
973 label_left_align = qc.Qt.AlignRight
975 if self._window.right() - label_rect.right() > 50:
976 label_right_rect.setLeft(label_rect.right() + 10)
977 label_right_rect.setRight(self._window.right())
978 label_right_align = qc.Qt.AlignLeft
980 if self._mouse_inside or self._component_grabbed:
981 p.drawText(
982 label_left_rect,
983 label_left_align | qc.Qt.AlignVCenter,
984 '%d%%' % round(self.clip_min * 100))
985 p.drawText(
986 label_right_rect,
987 label_right_align | qc.Qt.AlignVCenter,
988 '%d%%' % round(self.clip_max * 100))
991class Progressbar(object):
992 def __init__(self, parent, name, can_abort=True):
993 self.parent = parent
994 self.name = name
995 self.label = qw.QLabel(name, parent)
996 self.pbar = qw.QProgressBar(parent)
997 self.aborted = False
998 self.time_last_update = 0.
999 if can_abort:
1000 self.abort_button = qw.QPushButton('Abort', parent)
1001 self.abort_button.clicked.connect(
1002 self.abort)
1003 else:
1004 self.abort_button = None
1006 def widgets(self):
1007 widgets = [self.label, self.bar()]
1008 if self.abort_button:
1009 widgets.append(self.abort_button)
1010 return widgets
1012 def bar(self):
1013 return self.pbar
1015 def abort(self):
1016 self.aborted = True
1019class Progressbars(qw.QFrame):
1020 def __init__(self, parent):
1021 qw.QFrame.__init__(self, parent)
1022 self.layout = qw.QGridLayout()
1023 self.setLayout(self.layout)
1024 self.bars = {}
1025 self.start_times = {}
1026 self.hide()
1028 def set_status(self, name, value, can_abort=True, force=False):
1029 value = int(round(value))
1030 now = time.time()
1031 if name not in self.start_times:
1032 self.start_times[name] = now
1033 if not force:
1034 return False
1035 else:
1036 if now < self.start_times[name] + 1.0:
1037 if value == 100:
1038 del self.start_times[name]
1039 if not force:
1040 return False
1042 self.start_times.get(name, 0.0)
1043 if name not in self.bars:
1044 if value == 100:
1045 return False
1046 self.bars[name] = Progressbar(self, name, can_abort=can_abort)
1047 self.make_layout()
1049 bar = self.bars[name]
1050 if bar.time_last_update < now - 0.1 or value == 100:
1051 bar.bar().setValue(value)
1052 bar.time_last_update = now
1054 if value == 100:
1055 del self.bars[name]
1056 if name in self.start_times:
1057 del self.start_times[name]
1058 self.make_layout()
1059 for w in bar.widgets():
1060 w.setParent(None)
1062 return bar.aborted
1064 def make_layout(self):
1065 while True:
1066 c = self.layout.takeAt(0)
1067 if c is None:
1068 break
1070 for ibar, bar in enumerate(self.bars.values()):
1071 for iw, w in enumerate(bar.widgets()):
1072 self.layout.addWidget(w, ibar, iw)
1074 if not self.bars:
1075 self.hide()
1076 else:
1077 self.show()
1080def tohex(c):
1081 return '%02x%02x%02x' % c
1084def to01(c):
1085 return c[0]/255., c[1]/255., c[2]/255.
1088def beautify_axes(axes):
1089 try:
1090 from cycler import cycler
1091 axes.set_prop_cycle(
1092 cycler('color', [to01(x) for x in plot.graph_colors]))
1094 except (ImportError, KeyError):
1095 axes.set_color_cycle(list(map(to01, plot.graph_colors)))
1097 xa = axes.get_xaxis()
1098 ya = axes.get_yaxis()
1099 for attr in ('labelpad', 'LABELPAD'):
1100 if hasattr(xa, attr):
1101 setattr(xa, attr, xa.get_label().get_fontsize())
1102 setattr(ya, attr, ya.get_label().get_fontsize())
1103 break
1106class FigureFrame(qw.QFrame):
1107 '''
1108 A widget to present a :py:mod:`matplotlib` figure.
1109 '''
1111 def __init__(self, parent=None, figure_cls=None):
1112 qw.QFrame.__init__(self, parent)
1113 fgcolor = plot.tango_colors['aluminium5']
1114 dpi = 0.5*(self.logicalDpiX() + self.logicalDpiY())
1116 font = qg.QFont()
1117 font.setBold(True)
1118 fontsize = font.pointSize()
1120 import matplotlib
1121 matplotlib.rcdefaults()
1122 matplotlib.rcParams['backend'] = 'Qt5Agg'
1124 matplotlib.rc('xtick', direction='out', labelsize=fontsize)
1125 matplotlib.rc('ytick', direction='out', labelsize=fontsize)
1126 matplotlib.rc('xtick.major', size=8, width=1)
1127 matplotlib.rc('xtick.minor', size=4, width=1)
1128 matplotlib.rc('ytick.major', size=8, width=1)
1129 matplotlib.rc('ytick.minor', size=4, width=1)
1130 matplotlib.rc('figure', facecolor='white', edgecolor=tohex(fgcolor))
1132 matplotlib.rc(
1133 'font',
1134 family='sans-serif',
1135 weight='bold',
1136 size=fontsize,
1137 **{'sans-serif': [
1138 font.family(),
1139 'DejaVu Sans', 'Bitstream Vera Sans', 'Lucida Grande',
1140 'Verdana', 'Geneva', 'Lucid', 'Arial', 'Helvetica']})
1142 matplotlib.rc('legend', fontsize=fontsize)
1144 matplotlib.rc('text', color=tohex(fgcolor))
1145 matplotlib.rc('xtick', color=tohex(fgcolor))
1146 matplotlib.rc('ytick', color=tohex(fgcolor))
1147 matplotlib.rc('figure.subplot', bottom=0.15)
1149 matplotlib.rc('axes', linewidth=1.0, unicode_minus=False)
1150 matplotlib.rc(
1151 'axes',
1152 facecolor='white',
1153 edgecolor=tohex(fgcolor),
1154 labelcolor=tohex(fgcolor))
1156 try:
1157 from cycler import cycler
1158 matplotlib.rc(
1159 'axes', prop_cycle=cycler(
1160 'color', [to01(x) for x in plot.graph_colors]))
1162 except (ImportError, KeyError):
1163 try:
1164 matplotlib.rc('axes', color_cycle=[
1165 to01(x) for x in plot.graph_colors])
1167 except KeyError:
1168 pass
1170 try:
1171 matplotlib.rc('axes', labelsize=fontsize)
1172 except KeyError:
1173 pass
1175 try:
1176 matplotlib.rc('axes', labelweight='bold')
1177 except KeyError:
1178 pass
1180 if figure_cls is None:
1181 from matplotlib.figure import Figure
1182 figure_cls = Figure
1184 from matplotlib.backends.backend_qt5agg import \
1185 NavigationToolbar2QT as NavigationToolbar
1187 from matplotlib.backends.backend_qt5agg \
1188 import FigureCanvasQTAgg as FigureCanvas
1190 layout = qw.QGridLayout()
1191 layout.setContentsMargins(0, 0, 0, 0)
1192 layout.setSpacing(0)
1194 self.setLayout(layout)
1195 self.figure = figure_cls(dpi=dpi)
1196 self.canvas = FigureCanvas(self.figure)
1197 self.canvas.setParent(self)
1198 self.canvas.setSizePolicy(
1199 qw.QSizePolicy(
1200 qw.QSizePolicy.Expanding,
1201 qw.QSizePolicy.Expanding))
1202 toolbar_frame = qw.QFrame()
1203 toolbar_frame.setFrameShape(qw.QFrame.StyledPanel)
1204 toolbar_frame_layout = qw.QHBoxLayout()
1205 toolbar_frame_layout.setContentsMargins(0, 0, 0, 0)
1206 toolbar_frame.setLayout(toolbar_frame_layout)
1207 self.toolbar = NavigationToolbar(self.canvas, self)
1208 layout.addWidget(self.canvas, 0, 0)
1209 toolbar_frame_layout.addWidget(self.toolbar)
1210 layout.addWidget(toolbar_frame, 1, 0)
1211 self.closed = False
1213 def gca(self):
1214 axes = self.figure.gca()
1215 beautify_axes(axes)
1216 return axes
1218 def gcf(self):
1219 return self.figure
1221 def draw(self):
1222 '''
1223 Draw with AGG, then queue for Qt update.
1224 '''
1225 self.canvas.draw()
1227 def closeEvent(self, ev):
1228 self.closed = True
1231class SmartplotFrame(FigureFrame):
1232 '''
1233 A widget to present a :py:mod:`pyrocko.plot.smartplot` figure.
1234 '''
1236 def __init__(
1237 self, parent=None, plot_args=[], plot_kwargs={}, plot_cls=None):
1239 from pyrocko.plot import smartplot
1241 FigureFrame.__init__(
1242 self,
1243 parent=parent,
1244 figure_cls=smartplot.SmartplotFigure)
1246 if plot_cls is None:
1247 plot_cls = smartplot.Plot
1249 self.plot = plot_cls(
1250 *plot_args,
1251 fig=self.figure,
1252 call_mpl_init=False,
1253 **plot_kwargs)
1256class WebKitFrame(qw.QFrame):
1257 '''
1258 A widget to present a html page using WebKit.
1259 '''
1261 def __init__(self, url=None, parent=None):
1262 try:
1263 from PyQt5.QtWebEngineWidgets import QWebEngineView as WebView
1264 except ImportError:
1265 from PyQt5.QtWebKitWidgets import QWebView as WebView
1266 qw.QFrame.__init__(self, parent)
1267 layout = qw.QGridLayout()
1268 layout.setContentsMargins(0, 0, 0, 0)
1269 layout.setSpacing(0)
1270 self.setLayout(layout)
1271 self.web_widget = WebView()
1272 layout.addWidget(self.web_widget, 0, 0)
1273 if url:
1274 self.web_widget.load(qc.QUrl(url))
1277class VTKFrame(qw.QFrame):
1278 '''
1279 A widget to present a VTK visualization.
1280 '''
1282 def __init__(self, actors=None, parent=None):
1283 import vtk
1284 from vtk.qt.QVTKRenderWindowInteractor import \
1285 QVTKRenderWindowInteractor
1287 qw.QFrame.__init__(self, parent)
1288 layout = qw.QGridLayout()
1289 layout.setContentsMargins(0, 0, 0, 0)
1290 layout.setSpacing(0)
1292 self.setLayout(layout)
1294 self.vtk_widget = QVTKRenderWindowInteractor(self)
1295 layout.addWidget(self.vtk_widget, 0, 0)
1297 self.renderer = vtk.vtkRenderer()
1298 self.vtk_widget.GetRenderWindow().AddRenderer(self.renderer)
1299 self.iren = self.vtk_widget.GetRenderWindow().GetInteractor()
1301 if actors:
1302 for a in actors:
1303 self.renderer.AddActor(a)
1305 def init(self):
1306 self.iren.Initialize()
1308 def add_actor(self, actor):
1309 self.renderer.AddActor(actor)
1312class PixmapFrame(qw.QLabel):
1313 '''
1314 A widget to preset a pixmap image.
1315 '''
1317 def __init__(self, filename=None, parent=None):
1319 qw.QLabel.__init__(self, parent)
1320 self.setAlignment(qc.Qt.AlignCenter)
1321 self.setContentsMargins(0, 0, 0, 0)
1322 self.menu = qw.QMenu(self)
1323 action = qw.QAction('Save as', self.menu)
1324 action.triggered.connect(self.save_pixmap)
1325 self.menu.addAction(action)
1327 if filename:
1328 self.load_pixmap(filename)
1330 def contextMenuEvent(self, event):
1331 self.menu.popup(qg.QCursor.pos())
1333 def load_pixmap(self, filename):
1334 self.pixmap = qg.QPixmap(filename)
1335 self.setPixmap(self.pixmap)
1337 def save_pixmap(self, filename=None):
1338 if not filename:
1339 filename, _ = qw.QFileDialog.getSaveFileName(
1340 self.parent(), caption='save as')
1341 self.pixmap.save(filename)
1344class Projection(object):
1345 def __init__(self):
1346 self.xr = 0., 1.
1347 self.ur = 0., 1.
1349 def set_in_range(self, xmin, xmax):
1350 if xmax == xmin:
1351 xmax = xmin + 1.
1353 self.xr = xmin, xmax
1355 def get_in_range(self):
1356 return self.xr
1358 def set_out_range(self, umin, umax):
1359 if umax == umin:
1360 umax = umin + 1.
1362 self.ur = umin, umax
1364 def get_out_range(self):
1365 return self.ur
1367 def __call__(self, x):
1368 umin, umax = self.ur
1369 xmin, xmax = self.xr
1370 return umin + (x-xmin)*((umax-umin)/(xmax-xmin))
1372 def clipped(self, x):
1373 umin, umax = self.ur
1374 xmin, xmax = self.xr
1375 return min(umax, max(umin, umin + (x-xmin)*((umax-umin)/(xmax-xmin))))
1377 def rev(self, u):
1378 umin, umax = self.ur
1379 xmin, xmax = self.xr
1380 return xmin + (u-umin)*((xmax-xmin)/(umax-umin))
1383class NoData(Exception):
1384 pass
1387g_working_system_time_range = util.get_working_system_time_range()
1389g_initial_time_range = []
1391try:
1392 g_initial_time_range.append(
1393 calendar.timegm((1950, 1, 1, 0, 0, 0)))
1394except Exception:
1395 g_initial_time_range.append(g_working_system_time_range[0])
1397try:
1398 g_initial_time_range.append(
1399 calendar.timegm((time.gmtime().tm_year + 1, 1, 1, 0, 0, 0)))
1400except Exception:
1401 g_initial_time_range.append(g_working_system_time_range[1])
1404def four_way_arrow(position, size):
1405 r = 5.
1406 w = 1.
1408 points = [
1409 (position[0]+size*float(a), position[1]+size*float(b))
1410 for (a, b) in [
1411 (0, r),
1412 (1.5*w, r-2*w),
1413 (0.5*w, r-2*w),
1414 (0.5*w, 0.5*w),
1415 (r-2*w, 0.5*w),
1416 (r-2*w, 1.5*w),
1417 (r, 0),
1418 (r-2*w, -1.5*w),
1419 (r-2*w, -0.5*w),
1420 (0.5*w, -0.5*w),
1421 (0.5*w, -(r-2*w)),
1422 (1.5*w, -(r-2*w)),
1423 (0, -r),
1424 (-1.5*w, -(r-2*w)),
1425 (-0.5*w, -(r-2*w)),
1426 (-0.5*w, -0.5*w),
1427 (-(r-2*w), -0.5*w),
1428 (-(r-2*w), -1.5*w),
1429 (-r, 0),
1430 (-(r-2*w), 1.5*w),
1431 (-(r-2*w), 0.5*w),
1432 (-0.5*w, 0.5*w),
1433 (-0.5*w, r-2*w),
1434 (-1.5*w, r-2*w)]]
1436 poly = qg.QPolygon(len(points))
1437 for ipoint, point in enumerate(points):
1438 poly.setPoint(ipoint, *(int(round(v)) for v in point))
1440 return poly
1443def tmin_effective(tmin, tmax, tduration, tposition):
1444 if None in (tmin, tmax, tduration, tposition):
1445 return tmin
1446 else:
1447 return tmin + (tmax - tmin) * tposition
1450def tmax_effective(tmin, tmax, tduration, tposition):
1451 if None in (tmin, tmax, tduration, tposition):
1452 return tmax
1453 else:
1454 return tmin + (tmax - tmin) * tposition + tduration
1457class RangeEdit(qw.QFrame):
1459 rangeChanged = qc.pyqtSignal()
1460 focusChanged = qc.pyqtSignal()
1461 tcursorChanged = qc.pyqtSignal()
1462 rangeEditPressed = qc.pyqtSignal()
1463 rangeEditReleased = qc.pyqtSignal()
1465 def __init__(self, parent=None):
1466 qw.QFrame.__init__(self, parent)
1467 self.setFrameStyle(qw.QFrame.StyledPanel | qw.QFrame.Plain)
1468 # self.setBackgroundRole(qg.QPalette.Button)
1469 # self.setAutoFillBackground(True)
1470 self.setMouseTracking(True)
1471 poli = qw.QSizePolicy(
1472 qw.QSizePolicy.Expanding,
1473 qw.QSizePolicy.Fixed)
1475 self.setSizePolicy(poli)
1476 self.setMinimumSize(100, 3*24)
1478 self._size_hint = qw.QPushButton().sizeHint()
1480 self._track_start = None
1481 self._track_range = None
1482 self._track_focus = None
1483 self._track_what = None
1485 self._tcursor = None
1486 self._hover_point = None
1488 self._provider = None
1489 self.tmin, self.tmax = None, None
1490 self.tduration, self.tposition = None, 0.
1492 def set_data_provider(self, provider):
1493 self._provider = provider
1495 def set_data_name(self, name):
1496 self._data_name = name
1498 def sizeHint(self):
1499 ''
1500 return self._size_hint
1502 def get_data_range(self):
1503 if self._provider:
1504 vals = []
1505 for data in self._provider.iter_data(self._data_name):
1506 vals.append(data.min())
1507 vals.append(data.max())
1509 if vals:
1510 return min(vals), max(vals)
1512 return None, None
1514 def get_histogram(self, projection, h):
1515 h = int(h)
1516 umin_w, umax_w = projection.get_out_range()
1517 tmin_w, tmax_w = projection.get_in_range()
1518 nbins = int(umax_w - umin_w)
1519 counts = num.zeros(nbins, dtype=int)
1520 if self._provider:
1521 for data in self._provider.iter_data(self._data_name):
1522 ibins = ((data - tmin_w) * (nbins / (tmax_w - tmin_w))) \
1523 .astype(int)
1524 num.clip(ibins, 0, nbins-1, ibins)
1525 counts += num.bincount(ibins, minlength=nbins)
1527 histogram = counts * h // (num.max(counts[1:-1]) or 1)
1528 bitmap = num.zeros((h, nbins), dtype=bool)
1529 for i in range(h):
1530 bitmap[h-1-i, :] = histogram > i
1532 bitmap = num.packbits(bitmap, axis=1, bitorder='little')
1534 return qg.QBitmap.fromData(
1535 qc.QSize(nbins, h),
1536 bitmap.tobytes(),
1537 qg.QImage.Format_MonoLSB)
1539 def draw_time_ticks(self, painter, projection, rect):
1541 palette = self.palette()
1542 alpha_brush = palette.highlight()
1543 color = alpha_brush.color()
1544 # color.setAlpha(60)
1545 painter.setPen(qg.QPen(color))
1547 tmin, tmax = projection.get_in_range()
1548 tinc, tinc_unit = plot.nice_time_tick_inc((tmax - tmin) / 7.)
1549 tick_times, _ = plot.time_tick_labels(tmin, tmax, tinc, tinc_unit)
1551 for tick_time in tick_times:
1552 x = int(round(projection(tick_time)))
1553 painter.drawLine(
1554 x, rect.top(), x, rect.top() + rect.height() // 5)
1556 def drawit(self, painter):
1558 palette = self.palette()
1560 upper_projection = self.upper_projection()
1561 lower_projection = self.lower_projection()
1563 upper_rect = self.upper_rect()
1564 lower_rect = self.lower_rect()
1565 focus_rect = self.focus_rect(upper_projection)
1567 fill_brush = palette.brush(qg.QPalette.Button)
1568 painter.fillRect(upper_rect, fill_brush)
1570 if focus_rect:
1571 painter.setBrush(palette.light())
1572 poly = qg.QPolygon(8)
1573 poly.setPoint(
1574 0, lower_rect.x(), lower_rect.y())
1575 poly.setPoint(
1576 1, lower_rect.x(), lower_rect.y()+lower_rect.height())
1577 poly.setPoint(
1578 2, lower_rect.x() + lower_rect.width(),
1579 lower_rect.y() + lower_rect.height())
1580 poly.setPoint(
1581 3, lower_rect.x() + lower_rect.width(), lower_rect.y())
1582 poly.setPoint(
1583 4, focus_rect.x() + focus_rect.width(),
1584 upper_rect.y() + upper_rect.height())
1585 poly.setPoint(
1586 5, focus_rect.x() + focus_rect.width(), upper_rect.y())
1587 poly.setPoint(
1588 6, focus_rect.x(), upper_rect.y())
1589 poly.setPoint(
1590 7, focus_rect.x(), upper_rect.y() + upper_rect.height())
1591 painter.drawPolygon(poly)
1592 else:
1593 fill_brush = palette.light()
1594 painter.fillRect(upper_rect, fill_brush)
1596 # painter.setBrush(palette.text())
1597 # poly = four_way_arrow((self.width() / 2.0, self.height() / 2.0), 2.)
1598 # painter.drawPolygon(poly)
1600 self.draw_time_ticks(painter, upper_projection, upper_rect)
1601 if focus_rect and self.tduration:
1602 self.draw_time_ticks(painter, lower_projection, lower_rect)
1604 xpen = qg.QPen(palette.color(qg.QPalette.ButtonText))
1605 painter.setPen(xpen)
1606 painter.drawPixmap(
1607 0, upper_rect.x(),
1608 self.get_histogram(upper_projection, upper_rect.height()))
1610 if focus_rect and self.tduration:
1611 painter.drawPixmap(
1612 0, lower_rect.y(),
1613 self.get_histogram(lower_projection, lower_rect.height()))
1615 # frame_pen = qg.QPen(palette.color(qg.QPalette.ButtonText))
1616 # painter.setPen(frame_pen)
1617 # painter.drawRect(upper_rect)
1618 # if self.tduration:
1619 # painter.drawRect(lower_rect)
1621 if self._tcursor is not None:
1622 x = int(round(upper_projection(self._tcursor)))
1623 painter.drawLine(x, upper_rect.top(), x, upper_rect.bottom())
1624 if focus_rect and self.tduration and lower_projection:
1625 x = int(round(lower_projection(self._tcursor)))
1626 painter.drawLine(x, lower_rect.top(), x, lower_rect.bottom())
1628 if self._hover_point and lower_rect.contains(self._hover_point) \
1629 and not self.tduration and not self._track_start:
1631 alpha_brush = palette.highlight()
1632 color = alpha_brush.color()
1633 color.setAlpha(30)
1634 alpha_brush.setColor(color)
1635 painter.fillRect(lower_rect, alpha_brush)
1637 def upper_projection(self):
1638 p = Projection()
1639 if None in (self.tmin, self.tmax):
1640 p.set_in_range(*g_initial_time_range)
1641 else:
1642 p.set_in_range(self.tmin, self.tmax)
1644 p.set_out_range(0., self.width())
1645 return p
1647 def lower_projection(self):
1648 tmin_eff = self.tmin_effective()
1649 tmax_eff = self.tmax_effective()
1650 if None in (tmin_eff, tmax_eff):
1651 return None
1653 p = Projection()
1654 p.set_in_range(tmin_eff, tmax_eff)
1655 p.set_out_range(0., self.width())
1656 return p
1658 def tmin_effective(self):
1659 return tmin_effective(
1660 self.tmin, self.tmax, self.tduration, self.tposition)
1662 def tmax_effective(self):
1663 return tmax_effective(
1664 self.tmin, self.tmax, self.tduration, self.tposition)
1666 def upper_rect(self):
1667 vmin = 0
1668 vmax = self.height() // 3
1669 umin, umax = 0, self.width()
1670 return qc.QRect(umin, vmin, umax-umin, vmax-vmin)
1672 def lower_rect(self):
1673 vmin = 2 * self.height() // 3
1674 vmax = self.height()
1675 umin, umax = 0, self.width()
1676 return qc.QRect(umin, vmin, umax-umin, vmax-vmin)
1678 def focus_rect(self, projection):
1679 vmin = 0
1680 vmax = self.height() // 3
1682 tmin_eff = self.tmin_effective()
1683 tmax_eff = self.tmax_effective()
1684 if None in (tmin_eff, tmax_eff):
1685 return None
1687 umin = rint(projection(tmin_eff))
1688 umax = rint(projection(tmax_eff))
1690 return qc.QRect(umin, vmin, umax-umin+1, vmax-vmin)
1692 def set_range(self, tmin, tmax):
1693 if None in (tmin, tmax):
1694 tmin = None
1695 tmax = None
1696 elif tmin == tmax:
1697 tmin -= 0.5
1698 tmax += 0.5
1700 self.tmin = tmin
1701 self.tmax = tmax
1703 self.rangeChanged.emit()
1704 self.update()
1706 def get_range(self):
1707 return self.tmin, self.tmax
1709 def set_focus(self, tduration, tposition):
1710 self.tduration = tduration
1711 self.tposition = tposition
1712 self.focusChanged.emit()
1713 self.update()
1715 def get_focus(self):
1716 return (self.tduration, self.tposition)
1718 def get_tcursor(self):
1719 return self._tcursor
1721 def update_data_range(self):
1722 self.set_range(*self.get_data_range())
1724 def paintEvent(self, paint_ev):
1725 ''
1726 painter = qg.QPainter(self)
1727 painter.setRenderHint(qg.QPainter.Antialiasing)
1728 self.drawit(painter)
1729 qw.QFrame.paintEvent(self, paint_ev)
1731 def mousePressEvent(self, mouse_ev):
1732 ''
1733 if mouse_ev.button() == qc.Qt.LeftButton:
1734 self.rangeEditPressed.emit()
1736 if None in (self.tmin, self.tmax):
1737 self.set_range(*g_initial_time_range)
1739 self._track_start = mouse_ev.x(), mouse_ev.y()
1740 self._track_range = self.get_range()
1741 self._track_focus = self.get_focus()
1742 # upper_projection = self.upper_projection()
1743 # focus_rect = self.focus_rect(upper_projection)
1744 upper_rect = self.upper_rect()
1745 lower_rect = self.lower_rect()
1746 if upper_rect.contains(mouse_ev.pos()):
1747 self._track_what = 'global'
1748 elif lower_rect.contains(mouse_ev.pos()):
1749 self._track_what = 'focus'
1750 if self.tduration is None:
1751 frac = 0.02
1752 tduration = (self.tmax - self.tmin) * (1.0 - frac)
1753 tposition = 0.5*frac
1754 self.set_focus(tduration, tposition)
1756 else:
1757 if self.tduration is not None:
1758 self._track_what = 'focus_slide'
1759 else:
1760 self._track_what = 'global_slide'
1762 self.update()
1764 def enterEvent(self, ev):
1765 ''
1766 self._tcursor = None # is set later by mouseMoveEvent
1767 self._hover_point = None
1768 self.tcursorChanged.emit()
1770 def leaveEvent(self, ev):
1771 ''
1772 self._tcursor = None
1773 self._hover_point = None
1774 self.tcursorChanged.emit()
1775 self.update()
1777 def mouseReleaseEvent(self, mouse_ev):
1778 ''
1779 if self._track_start:
1780 self.rangeEditReleased.emit()
1781 self.update()
1783 self._track_start = None
1784 self._track_range = None
1785 self._track_focus = None
1786 self._track_what = None
1787 if self.tduration is not None:
1788 if self.tduration >= self.tmax - self.tmin:
1789 self.set_focus(None, 0.0)
1790 elif self.tposition < 0.:
1791 self.set_focus(self.tduration, 0.0)
1792 elif self.tposition > 1.0 - self.tduration \
1793 / (self.tmax - self.tmin):
1794 self.set_focus(
1795 self.tduration, 1.0 - self.tduration
1796 / (self.tmax - self.tmin))
1798 def mouseDoubleClickEvent(self, mouse_ev):
1799 ''
1800 if mouse_ev.button() == qc.Qt.LeftButton:
1801 lower_rect = self.lower_rect()
1802 if lower_rect.contains(mouse_ev.pos()) \
1803 and self.tduration is not None:
1805 etmin = self.tmin_effective()
1806 etmax = self.tmax_effective()
1807 self.set_range(etmin, etmax)
1808 self.set_focus(None, 0.0)
1810 upper_rect = self.upper_rect()
1811 if upper_rect.contains(mouse_ev.pos()) \
1812 and self.tduration is not None:
1814 self.set_focus(None, 0.0)
1816 def mouseMoveEvent(self, mouse_ev):
1817 ''
1818 point = self.mapFromGlobal(mouse_ev.globalPos())
1819 self._hover_point = point
1821 if self._track_start is not None:
1822 x0, y0 = self._track_start
1823 dx = (point.x() - x0)/float(self.width())
1824 dy = (point.y() - y0)/float(self.height())
1825 xfrac = x0/float(self.width())
1826 tmin0, tmax0 = self._track_range
1827 tduration0, tposition0 = self._track_focus
1829 if self._track_what in ('global', 'global_slide'):
1830 if self._track_what == 'global':
1831 scale = math.exp(-dy)
1832 else:
1833 scale = 1.0
1835 dtr = (tmax0-tmin0) * (scale - 1.0)
1836 dt = dx*(tmax0-tmin0)*scale
1838 tmin = tmin0 - dt - dtr*xfrac
1839 tmax = tmax0 - dt + dtr*(1.-xfrac)
1841 self.set_range(tmin, tmax)
1843 tduration, tposition = self._track_focus
1844 if tduration is not None:
1845 etmin0 = tmin_effective(
1846 tmin0, tmax0, tduration0, tposition0)
1848 tposition = (etmin0 - tmin) / (tmax - tmin)
1849 self.set_focus(tduration0, tposition)
1851 elif self._track_what == 'focus':
1852 if tduration0 is not None:
1853 scale = math.exp(-dy)
1855 dtr = tduration0 * (scale - 1.0)
1856 dt = dx * tduration0 * scale
1858 etmin0 = tmin_effective(
1859 tmin0, tmax0, tduration0, tposition0)
1860 etmax0 = tmax_effective(
1861 tmin0, tmax0, tduration0, tposition0)
1863 tmin = etmin0 - dt - dtr*xfrac
1864 tmax = etmax0 - dt + dtr*(1.-xfrac)
1866 tduration = tmax - tmin
1868 tposition = (tmin - tmin0) / (tmax0 - tmin0)
1869 tposition = min(
1870 max(0., tposition),
1871 1.0 - tduration / (tmax0 - tmin0))
1873 if tduration < (tmax0 - tmin0):
1874 self.set_focus(tduration, tposition)
1875 else:
1876 self.set_focus(None, tposition)
1878 else:
1879 tduration, tposition = tmax0 - tmin0, 0.0
1880 self.set_focus(tduration, tposition)
1881 self._track_focus = (tduration, tposition)
1883 elif self._track_what == 'focus_slide':
1884 if tduration0 is not None:
1885 self.set_focus(
1886 tduration0,
1887 min(
1888 max(0., tposition0 + dx),
1889 1.0 - tduration0 / (tmax0 - tmin0)))
1891 else:
1893 upper_rect = self.upper_rect()
1894 lower_rect = self.lower_rect()
1895 upper_projection = self.upper_projection()
1896 lower_projection = self.lower_projection()
1898 app = get_app()
1899 have_focus = lower_projection and self.tduration is not None
1901 if upper_rect.contains(point):
1902 self.setCursor(qg.QCursor(qc.Qt.CursorShape.CrossCursor))
1903 self._tcursor = upper_projection.rev(point.x())
1904 app.status(
1905 'Click and drag to change global time interval. '
1906 'Move up/down to zoom.' + (
1907 ' Double-click to clear focus time interval.'
1908 if have_focus else ''))
1910 elif lower_rect.contains(point):
1911 self.setCursor(qg.QCursor(qc.Qt.CursorShape.CrossCursor))
1912 if have_focus:
1913 self._tcursor = lower_projection.rev(point.x())
1914 app.status(
1915 'Click and drag to change local time interval. '
1916 'Double-click to set global time interval from focus.')
1917 else:
1918 app.status(
1919 'Click to activate focus time window.')
1920 else:
1921 self.setCursor(qg.QCursor(qc.Qt.CursorShape.SizeHorCursor))
1922 self._tcursor = None
1923 if have_focus:
1924 app.status('Move focus time interval with fixed length.')
1925 else:
1926 app.status('Move global time interval with fixed length.')
1928 self.update()
1929 self.tcursorChanged.emit()