1"""
2Helper utilities
3
4"""
5import sys
6import traceback
7import ctypes
8
9from contextlib import contextmanager
10from typing import Optional, Union
11
12from AnyQt.QtWidgets import (
13    QWidget, QMessageBox, QStyleOption, QStyle, QTextEdit, QScrollBar
14)
15from AnyQt.QtGui import (
16    QGradient, QLinearGradient, QRadialGradient, QBrush, QPainter,
17    QPaintEvent, QColor, QPixmap, QPixmapCache, QTextOption, QGuiApplication,
18    QTextCharFormat, QFont
19)
20from AnyQt.QtCore import Qt, QPointF, QPoint, QRect, QRectF, Signal, QEvent
21
22import sip
23
24
25@contextmanager
26def updates_disabled(widget):
27    """Disable QWidget updates (using QWidget.setUpdatesEnabled)
28    """
29    old_state = widget.updatesEnabled()
30    widget.setUpdatesEnabled(False)
31    try:
32        yield
33    finally:
34        widget.setUpdatesEnabled(old_state)
35
36
37@contextmanager
38def signals_disabled(qobject):
39    """Disables signals on an instance of QObject.
40    """
41    old_state = qobject.signalsBlocked()
42    qobject.blockSignals(True)
43    try:
44        yield
45    finally:
46        qobject.blockSignals(old_state)
47
48
49@contextmanager
50def disabled(qobject):
51    """Disables a disablable QObject instance.
52    """
53    if not (hasattr(qobject, "setEnabled") and hasattr(qobject, "isEnabled")):
54        raise TypeError("%r does not have 'enabled' property" % qobject)
55
56    old_state = qobject.isEnabled()
57    qobject.setEnabled(False)
58    try:
59        yield
60    finally:
61        qobject.setEnabled(old_state)
62
63
64@contextmanager
65def disconnected(signal, slot, type=Qt.UniqueConnection):
66    """
67    A context manager disconnecting a slot from a signal.
68    ::
69
70        with disconnected(scene.selectionChanged, self.onSelectionChanged):
71            # Can change item selection in a scene without
72            # onSelectionChanged being invoked.
73            do_something()
74
75    Warning
76    -------
77    The relative order of the slot in signal's connections is not preserved.
78
79    Raises
80    ------
81    TypeError:
82        If the slot was not connected to the signal
83    """
84    signal.disconnect(slot)
85    try:
86        yield
87    finally:
88        signal.connect(slot, type)
89
90
91def StyledWidget_paintEvent(self, event):
92    # type: (QWidget, QPaintEvent) -> None
93    """A default styled QWidget subclass  paintEvent function.
94    """
95    opt = QStyleOption()
96    opt.initFrom(self)
97    painter = QPainter(self)
98    self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
99
100
101class StyledWidget(QWidget):
102    """
103    """
104    paintEvent = StyledWidget_paintEvent  # type: ignore
105
106
107class ScrollBar(QScrollBar):
108    #: Emitted when the scroll bar receives a StyleChange event
109    styleChange = Signal()
110
111    def changeEvent(self, event: QEvent) -> None:
112        if event.type() == QEvent.StyleChange:
113            self.styleChange.emit()
114        super().changeEvent(event)
115
116
117def is_transparency_supported():  # type: () -> bool
118    """Is window transparency supported by the current windowing system.
119    """
120    if sys.platform == "win32":
121        return is_dwm_compositing_enabled()
122    elif sys.platform == "cygwin":
123        return False
124    elif sys.platform == "darwin":
125        if has_x11():
126            return is_x11_compositing_enabled()
127        else:
128            # Quartz compositor
129            return True
130    elif sys.platform.startswith("linux"):
131        # TODO: wayland??
132        return is_x11_compositing_enabled()
133    elif sys.platform.startswith("freebsd"):
134        return is_x11_compositing_enabled()
135    elif has_x11():
136        return is_x11_compositing_enabled()
137    else:
138        return False
139
140
141def has_x11():  # type: () -> bool
142    """Is Qt build against X11 server.
143    """
144    try:
145        from AnyQt.QtX11Extras import QX11Info
146        return True
147    except ImportError:
148        return False
149
150
151def is_x11_compositing_enabled():  # type: () -> bool
152    """Is X11 compositing manager running.
153    """
154    try:
155        from AnyQt.QtX11Extras import QX11Info
156    except ImportError:
157        return False
158    if hasattr(QX11Info, "isCompositingManagerRunning"):
159        return QX11Info.isCompositingManagerRunning()
160    else:
161        # not available on Qt5
162        return False  # ?
163
164
165def is_dwm_compositing_enabled():  # type: () -> bool
166    """Is Desktop Window Manager compositing (Aero) enabled.
167    """
168    enabled = ctypes.c_bool(False)
169    try:
170        DwmIsCompositionEnabled = \
171            ctypes.windll.dwmapi.DwmIsCompositionEnabled  # type: ignore
172    except (AttributeError, WindowsError):
173        # dwmapi or DwmIsCompositionEnabled is not present
174        return False
175
176    rval = DwmIsCompositionEnabled(ctypes.byref(enabled))
177
178    return rval == 0 and enabled.value
179
180
181def gradient_darker(grad, factor):
182    # type: (QGradient, float) -> QGradient
183    """Return a copy of the QGradient darkened by factor.
184
185    .. note:: Only QLinearGradeint and QRadialGradient are supported.
186
187    """
188    if type(grad) is QGradient:
189        if grad.type() == QGradient.LinearGradient:
190            grad = sip.cast(grad, QLinearGradient)
191        elif grad.type() == QGradient.RadialGradient:
192            grad = sip.cast(grad, QRadialGradient)
193
194    if isinstance(grad, QLinearGradient):
195        new_grad = QLinearGradient(grad.start(), grad.finalStop())
196    elif isinstance(grad, QRadialGradient):
197        new_grad = QRadialGradient(grad.center(), grad.radius(),
198                                   grad.focalPoint())
199    else:
200        raise TypeError
201
202    new_grad.setCoordinateMode(grad.coordinateMode())
203
204    for pos, color in grad.stops():
205        new_grad.setColorAt(pos, color.darker(factor))
206
207    return new_grad
208
209
210def brush_darker(brush: QBrush, factor: bool) -> QBrush:
211    """Return a copy of the brush darkened by factor.
212    """
213    grad = brush.gradient()
214    if grad:
215        return QBrush(gradient_darker(grad, factor))
216    else:
217        brush = QBrush(brush)
218        brush.setColor(brush.color().darker(factor))
219        return brush
220
221
222def create_gradient(base_color: QColor, stop=QPointF(0, 0),
223                    finalStop=QPointF(0, 1)) -> QLinearGradient:
224    """
225    Create a default linear gradient using `base_color` .
226    """
227    grad = QLinearGradient(stop, finalStop)
228    grad.setStops([(0.0, base_color),
229                   (0.5, base_color),
230                   (0.8, base_color.darker(105)),
231                   (1.0, base_color.darker(110)),
232                   ])
233    grad.setCoordinateMode(QLinearGradient.ObjectBoundingMode)
234    return grad
235
236
237def create_css_gradient(base_color: QColor, stop=QPointF(0, 0),
238                        finalStop=QPointF(0, 1)) -> str:
239    """
240    Create a Qt css linear gradient fragment based on the `base_color`.
241    """
242    gradient = create_gradient(base_color, stop, finalStop)
243    return css_gradient(gradient)
244
245
246def css_gradient(gradient: QLinearGradient) -> str:
247    """
248    Given an instance of a `QLinearGradient` return an equivalent qt css
249    gradient fragment.
250    """
251    stop, finalStop = gradient.start(), gradient.finalStop()
252    x1, y1, x2, y2 = stop.x(), stop.y(), finalStop.x(), finalStop.y()
253    stops = gradient.stops()
254    stops = "\n".join("    stop: {0:f} {1}".format(stop, color.name())
255                      for stop, color in stops)
256    return ("qlineargradient(\n"
257            "    x1: {x1}, y1: {y1}, x2: {x2}, y2: {y2},\n"
258            "{stops})").format(x1=x1, y1=y1, x2=x2, y2=y2, stops=stops)
259
260
261def message_critical(text, title=None, informative_text=None, details=None,
262                     buttons=None, default_button=None, exc_info=False,
263                     parent=None):
264    """Show a critical message.
265    """
266    if not text:
267        text = "An unexpected error occurred."
268
269    if title is None:
270        title = "Error"
271
272    return message(QMessageBox.Critical, text, title, informative_text,
273                   details, buttons, default_button, exc_info, parent)
274
275
276def message_warning(text, title=None, informative_text=None, details=None,
277                    buttons=None, default_button=None, exc_info=False,
278                    parent=None):
279    """Show a warning message.
280    """
281    if not text:
282        import random
283        text_candidates = ["Death could come at any moment.",
284                           "Murphy lurks about. Remember to save frequently."
285                           ]
286        text = random.choice(text_candidates)
287
288    if title is not None:
289        title = "Warning"
290
291    return message(QMessageBox.Warning, text, title, informative_text,
292                   details, buttons, default_button, exc_info, parent)
293
294
295def message_information(text, title=None, informative_text=None, details=None,
296                        buttons=None, default_button=None, exc_info=False,
297                        parent=None):
298    """Show an information message box.
299    """
300    if title is None:
301        title = "Information"
302    if not text:
303        text = "I am not a number."
304
305    return message(QMessageBox.Information, text, title, informative_text,
306                   details, buttons, default_button, exc_info, parent)
307
308
309def message_question(text, title, informative_text=None, details=None,
310                     buttons=None, default_button=None, exc_info=False,
311                     parent=None):
312    """Show an message box asking the user to select some
313    predefined course of action (set by buttons argument).
314
315    """
316    return message(QMessageBox.Question, text, title, informative_text,
317                   details, buttons, default_button, exc_info, parent)
318
319
320def message(icon, text, title=None, informative_text=None, details=None,
321            buttons=None, default_button=None, exc_info=False, parent=None):
322    """Show a message helper function.
323    """
324    if title is None:
325        title = "Message"
326    if not text:
327        text = "I am neither a postman nor a doctor."
328
329    if buttons is None:
330        buttons = QMessageBox.Ok
331
332    if details is None and exc_info:
333        details = traceback.format_exc(limit=20)
334
335    mbox = QMessageBox(icon, title, text, buttons, parent)
336
337    if informative_text:
338        mbox.setInformativeText(informative_text)
339
340    if details:
341        mbox.setDetailedText(details)
342        dtextedit = mbox.findChild(QTextEdit)
343        if dtextedit is not None:
344            dtextedit.setWordWrapMode(QTextOption.NoWrap)
345
346    if default_button is not None:
347        mbox.setDefaultButton(default_button)
348
349    return mbox.exec_()
350
351
352def innerGlowBackgroundPixmap(color, size, radius=5):
353    """ Draws radial gradient pixmap, then uses that to draw
354    a rounded-corner gradient rectangle pixmap.
355
356    Args:
357        color (QColor): used as outer color (lightness 245 used for inner)
358        size (QSize): size of output pixmap
359        radius (int): radius of inner glow rounded corners
360    """
361    key = "InnerGlowBackground " + \
362          color.name() + " " + \
363          str(radius)
364
365    bg = QPixmapCache.find(key)
366    if bg:
367        return bg
368
369    # set background colors for gradient
370    color = color.toHsl()
371    light_color = color.fromHsl(color.hslHue(), color.hslSaturation(), 245)
372    dark_color = color
373
374    # initialize radial gradient
375    center = QPoint(radius, radius)
376    pixRect = QRect(0, 0, radius * 2, radius * 2)
377    gradientPixmap = QPixmap(radius * 2, radius * 2)
378    gradientPixmap.fill(dark_color)
379
380    # draw radial gradient pixmap
381    pixPainter = QPainter(gradientPixmap)
382    pixPainter.setPen(Qt.NoPen)
383    gradient = QRadialGradient(center, radius - 1)
384    gradient.setColorAt(0, light_color)
385    gradient.setColorAt(1, dark_color)
386    pixPainter.setBrush(gradient)
387    pixPainter.drawRect(pixRect)
388    pixPainter.end()
389
390    # set tl and br to the gradient's square-shaped rect
391    tl = QPoint(0, 0)
392    br = QPoint(size.width(), size.height())
393
394    # fragments of radial gradient pixmap to create rounded gradient outline rectangle
395    frags = [
396        # top-left corner
397        QPainter.PixmapFragment.create(
398            QPointF(tl.x() + radius / 2, tl.y() + radius / 2),
399            QRectF(0, 0, radius, radius)
400        ),
401        # top-mid 'linear gradient'
402        QPainter.PixmapFragment.create(
403            QPointF(tl.x() + (br.x() - tl.x()) / 2, tl.y() + radius / 2),
404            QRectF(radius, 0, 1, radius),
405            scaleX=(br.x() - tl.x() - 2 * radius)
406        ),
407        # top-right corner
408        QPainter.PixmapFragment.create(
409            QPointF(br.x() - radius / 2, tl.y() + radius / 2),
410            QRectF(radius, 0, radius, radius)
411        ),
412        # left-mid 'linear gradient'
413        QPainter.PixmapFragment.create(
414            QPointF(tl.x() + radius / 2, tl.y() + (br.y() - tl.y()) / 2),
415            QRectF(0, radius, radius, 1),
416            scaleY=(br.y() - tl.y() - 2 * radius)
417        ),
418        # mid solid
419        QPainter.PixmapFragment.create(
420            QPointF(tl.x() + (br.x() - tl.x()) / 2, tl.y() + (br.y() - tl.y()) / 2),
421            QRectF(radius, radius, 1, 1),
422            scaleX=(br.x() - tl.x() - 2 * radius),
423            scaleY=(br.y() - tl.y() - 2 * radius)
424        ),
425        # right-mid 'linear gradient'
426        QPainter.PixmapFragment.create(
427            QPointF(br.x() - radius / 2, tl.y() + (br.y() - tl.y()) / 2),
428            QRectF(radius, radius, radius, 1),
429            scaleY=(br.y() - tl.y() - 2 * radius)
430        ),
431        # bottom-left corner
432        QPainter.PixmapFragment.create(
433            QPointF(tl.x() + radius / 2, br.y() - radius / 2),
434            QRectF(0, radius, radius, radius)
435        ),
436        # bottom-mid 'linear gradient'
437        QPainter.PixmapFragment.create(
438            QPointF(tl.x() + (br.x() - tl.x()) / 2, br.y() - radius / 2),
439            QRectF(radius, radius, 1, radius),
440            scaleX=(br.x() - tl.x() - 2 * radius)
441        ),
442        # bottom-right corner
443        QPainter.PixmapFragment.create(
444            QPointF(br.x() - radius / 2, br.y() - radius / 2),
445            QRectF(radius, radius, radius, radius)
446        ),
447    ]
448
449    # draw icon background to pixmap
450    outPix = QPixmap(size.width(), size.height())
451    outPainter = QPainter(outPix)
452    outPainter.setPen(Qt.NoPen)
453    outPainter.drawPixmapFragments(frags,
454                                   gradientPixmap,
455                                   QPainter.PixmapFragmentHints(QPainter.OpaqueHint))
456    outPainter.end()
457
458    QPixmapCache.insert(key, outPix)
459
460    return outPix
461
462
463def shadowTemplatePixmap(color, length):
464    """
465    Returns 1 pixel wide, `length` pixels long linear-gradient.
466
467    Args:
468        color (QColor): shadow color
469        length (int): length of cast shadow
470
471    """
472    key = "InnerShadowTemplate " + \
473          color.name() + " " + \
474          str(length)
475
476    # get cached template
477    shadowPixmap = QPixmapCache.find(key)
478    if shadowPixmap:
479        return shadowPixmap
480
481    shadowPixmap = QPixmap(1, length)
482    shadowPixmap.fill(Qt.transparent)
483
484    grad = QLinearGradient(0, 0, 0, length)
485    grad.setColorAt(0, color)
486    grad.setColorAt(1, Qt.transparent)
487
488    painter = QPainter()
489    painter.begin(shadowPixmap)
490    painter.fillRect(shadowPixmap.rect(), grad)
491    painter.end()
492
493    # cache template
494    QPixmapCache.insert(key, shadowPixmap)
495
496    return shadowPixmap
497
498
499def innerShadowPixmap(color, size, pos, length=5):
500    """
501    Args:
502        color (QColor): shadow color
503        size (QSize): size of pixmap
504        pos (int): shadow position int flag, use bitwise operations
505            1 - top
506            2 - right
507            4 - bottom
508            8 - left
509        length (int): length of cast shadow
510    """
511    key = "InnerShadow " + \
512          color.name() + " " + \
513          str(size) + " " + \
514          str(pos) + " " + \
515          str(length)
516    # get cached shadow if it exists
517    finalShadow = QPixmapCache.find(key)
518    if finalShadow:
519        return finalShadow
520
521    shadowTemplate = shadowTemplatePixmap(color, length)
522
523    finalShadow = QPixmap(size)
524    finalShadow.fill(Qt.transparent)
525    shadowPainter = QPainter(finalShadow)
526    shadowPainter.setCompositionMode(QPainter.CompositionMode_Darken)
527
528    # top/bottom rect
529    targetRect = QRect(0, 0, size.width(), length)
530
531    # shadow on top
532    if pos & 1:
533        shadowPainter.drawPixmap(targetRect, shadowTemplate, shadowTemplate.rect())
534    # shadow on bottom
535    if pos & 4:
536        shadowPainter.save()
537
538        shadowPainter.translate(QPointF(0, size.height()))
539        shadowPainter.scale(1, -1)
540        shadowPainter.drawPixmap(targetRect, shadowTemplate, shadowTemplate.rect())
541
542        shadowPainter.restore()
543
544    # left/right rect
545    targetRect = QRect(0, 0, size.height(), shadowTemplate.rect().height())
546
547    # shadow on the right
548    if pos & 2:
549        shadowPainter.save()
550
551        shadowPainter.translate(QPointF(size.width(), 0))
552        shadowPainter.rotate(90)
553        shadowPainter.drawPixmap(targetRect, shadowTemplate, shadowTemplate.rect())
554
555        shadowPainter.restore()
556    # shadow on left
557    if pos & 8:
558        shadowPainter.save()
559
560        shadowPainter.translate(0, size.height())
561        shadowPainter.rotate(-90)
562        shadowPainter.drawPixmap(targetRect, shadowTemplate, shadowTemplate.rect())
563
564        shadowPainter.restore()
565
566    shadowPainter.end()
567
568    # cache shadow
569    QPixmapCache.insert(key, finalShadow)
570
571    return finalShadow
572
573
574def clipboard_has_format(mimetype):
575    # type: (str) -> bool
576    """Does the system clipboard contain data for mimetype?"""
577    cb = QGuiApplication.clipboard()
578    if cb is None:
579        return False
580    mime = cb.mimeData()
581    if mime is None:
582        return False
583    return mime.hasFormat(mimetype)
584
585
586def clipboard_data(mimetype: str) -> Optional[bytes]:
587    """Return the binary data of the system clipboard for mimetype."""
588    cb = QGuiApplication.clipboard()
589    if cb is None:
590        return None
591    mime = cb.mimeData()
592    if mime is None:
593        return None
594    if mime.hasFormat(mimetype):
595        return bytes(mime.data(mimetype))
596    else:
597        return None
598
599
600_Color = Union[QColor, QBrush, Qt.GlobalColor, QGradient]
601
602
603def update_char_format(
604        baseformat: QTextCharFormat,
605        color: Optional[_Color] = None,
606        background: Optional[_Color] = None,
607        weight: Optional[int] = None,
608        italic: Optional[bool] = None,
609        underline: Optional[bool] = None,
610        font: Optional[QFont] = None
611) -> QTextCharFormat:
612    """
613    Return a copy of `baseformat` :class:`QTextCharFormat` with
614    updated color, weight, background and font properties.
615    """
616    charformat = QTextCharFormat(baseformat)
617    if color is not None:
618        charformat.setForeground(color)
619    if background is not None:
620        charformat.setBackground(background)
621    if font is not None:
622        assert weight is None and italic is None and underline is None
623        charformat.setFont(font)
624    else:
625        if weight is not None:
626            charformat.setFontWeight(weight)
627        if italic is not None:
628            charformat.setFontItalic(italic)
629        if underline is not None:
630            charformat.setFontUnderline(underline)
631    return charformat
632
633
634def update_font(
635        basefont: QFont,
636        weight: Optional[int] = None,
637        italic: Optional[bool] = None,
638        underline: Optional[bool] = None,
639        pixelSize: Optional[int] = None,
640        pointSize: Optional[float] = None
641) -> QFont:
642    """
643    Return a copy of `basefont` :class:`QFont` with updated properties.
644    """
645    font = QFont(basefont)
646
647    if weight is not None:
648        font.setWeight(weight)
649
650    if italic is not None:
651        font.setItalic(italic)
652
653    if underline is not None:
654        font.setUnderline(underline)
655
656    if pixelSize is not None:
657        font.setPixelSize(pixelSize)
658
659    if pointSize is not None:
660        font.setPointSizeF(pointSize)
661
662    return font
663