"""
Volume Color Map Editor Panel.
"""

import itertools, math

import pymol

from pymol.Qt import QtGui, QtCore
from pymol.Qt import QtWidgets

Qt = QtCore.Qt

DOT_RADIUS = 5
ALPHA_LOG_BASE = 10.0

DEFAULT_COLORS = [
    (1., 1., 0.),
    (1., 0., 0.),
    (0., 0., 1.),
    (0., 1., 0.),
    (0., 1., 1.),
    (1., 0., 1.),
]

EPS = 1e-6

DEFAULT_TEXT_DIALOG_WIDTH = 500

VOLUME_HELP = '''
VOLUME PANEL HELP

--------------------------------------------------
Canvas Mouse Actions (no Point under Cursor)

  L-Click            Add point
  CTRL+L-Click       Add 3 points (isosurface)

  CTRL+R-Drag        Zoom in

--------------------------------------------------
Mouse Actions with Point under Cursor

  L-Click            Edit point color
  R-Click            Edit point value
  SHIFT+R-Click      Edit point opacity
  CTRL+L-Click       Edit color of 3 points

  M-Click            Remove Point
  SHIFT+L-Click      Remove Point
  CTRL+M-Click       Remove 3 points
  CTRL+SHIFT+L-Click Remove 3 points

  L-Drag             Move point
  CTRL+L-Drag        Move 3 points (horizontal only)
  R-Drag             Move point along one axis only

--------------------------------------------------
L = Left mouse button
M = Middle mouse button
R = Right mouse button

--------------------------------------------------
See also the "volume_color" command for getting and
setting volume colors on the command line.
'''


class VolumeEditorWidget(QtWidgets.QWidget):
    def __init__(self, parent=None, volume_name='', cmd=None):
        super(VolumeEditorWidget, self).__init__(parent)
        self.setObjectName("volume_editor_widget")
        self.setMouseTracking(True)
        self.points = []
        self.point = -1
        self.color_cycle = itertools.cycle(DEFAULT_COLORS)
        self.path = None
        self.cmd = cmd
        self.volume_name = volume_name
        self.real_time = True
        self.dragged = False
        self.vmin = 0.0
        self.vmax = 1.0
        self.original_vmin = 0.0
        self.original_vmax = 1.0
        self.amax = 1.0
        self.left_margin = 35
        self.bottom_margin = 20
        self.constrain = None
        self.init_pos = None
        self.zoom_pos = None
        self.ignore_set_colors = False
        self.color_dialog = None
        self.text_dialog = None
        self.text_boxes = {}
        self.hover_point = -1

    def sizeHint(self):
        return QtCore.QSize(600, 200)

    def paintGrid(self, painter, rect):
        pen = QtGui.QPen(self.line_color)
        painter.setPen(pen)
        x0 = rect.x()
        x1 = rect.x() + rect.width()
        y0 = rect.y()
        y1 = rect.y() + rect.height()
        painter.drawLine(x0, y1, x1, y1)
        painter.drawLine(x0, y0, x0, y1)
        h = rect.height()
        num_lines = 10
        pen.setStyle(Qt.PenStyle.DashLine)
        painter.setPen(pen)
        for line in range(1, num_lines):
            y = y0 + h * (1.0 - self.alphaToY(line / float(num_lines)))
            y = round(y)
            painter.drawLine(x0, y, x1, y)

    def paintColorDots(self, painter, rect):
        pen = QtGui.QPen(Qt.GlobalColor.gray)
        pen.setStyle(Qt.PenStyle.SolidLine)
        painter.setPen(pen)
        scaled_pts = []
        h = rect.height()
        for point in self.points:
            x, y, r, g, b = point
            x = rect.left() + rect.width() * self.dataToX(x)
            y = rect.top() + rect.height() * (1.0 - self.alphaToY(y))
            x = round(x)
            y = round(y)
            if scaled_pts:
                painter.drawLine(scaled_pts[-1][0], scaled_pts[-1][1], x, y)
            scaled_pts.append((x, y, r, g, b))

        for x, y, r, g, b in scaled_pts:
            painter.setBrush(QtGui.QColor.fromRgbF(r, g, b))
            painter.drawEllipse(x - DOT_RADIUS, y - DOT_RADIUS, 2 * DOT_RADIUS,
                                2 * DOT_RADIUS)

        if 0 <= self.hover_point < len(scaled_pts):
            # use larger radius for hover dot
            radius = DOT_RADIUS + 2
            x, y, r, g, b = scaled_pts[self.hover_point]
            painter.setBrush(QtGui.QColor.fromRgbF(r, g, b))
            painter.drawEllipse(x - radius, y - radius, 2 * radius,
                                2 * radius)

    def paintHistogram(self, painter, rect):
        if self.path:
            vrange = self.original_vmax - self.original_vmin
            if vrange == 0.0:
                return
            norm_min = (self.vmin - self.original_vmin) / vrange
            norm_max = (self.vmax - self.original_vmin) / vrange
            h = rect.height() - 2
            dnorm = norm_max-norm_min
            iwidth = 1.0 / (rect.width())
            painter_path = QtGui.QPainterPath()
            for i in range(rect.width()):
                pos = (i * iwidth * dnorm + norm_min) * len(self.path)
                ipos = int(pos)
                if pos < 0 or ipos >= len(self.path) - 1:
                    continue
                y0 = self.path[ipos][1]
                y1 = self.path[ipos+1][1]
                y = y0 + (y1-y0) * (pos - int(pos)) # lerp
                x = rect.left() + i
                y = h - self.alphaToY(y) * h + 1
                if painter_path.elementCount() == 0:
                    painter_path.moveTo(x, y)
                else:
                    painter_path.lineTo(x, y)

            pen = QtGui.QPen(Qt.GlobalColor.red)
            pen.setStyle(Qt.PenStyle.SolidLine)
            painter.setPen(pen)
            painter.drawPath(painter_path)

    def paintValueBox(self,
                      painter,
                      font_metrics,
                      x,
                      y,
                      right_just,
                      value,
                      format="%.3f"):
        s = format % value
        sw = font_metrics.boundingRect(s).size().width()
        sh = font_metrics.height()
        if right_just:
            rect = QtCore.QRect(x - sw - 4, y - sh, sw + 4, sh + 2)
        else:
            rect = QtCore.QRect(x, y - sh, sw + 4, sh + 2)
        painter.fillRect(rect,
                QtGui.QColor(96, 96, 128) if self.line_color == Qt.GlobalColor.lightGray else
                QtGui.QColor(0xFF, 0xFF, 0xFF))
        painter.drawRect(rect)
        painter.drawText(rect.x() + 2, y - 2, s)
        return rect

    def paintAxes(self, painter, rect):
        low = int(math.ceil(self.vmin))
        hi = int(math.floor(self.vmax)) + 1
        pen = painter.pen()
        pen.setStyle(Qt.PenStyle.SolidLine)
        pen.setColor(self.line_color)
        painter.setPen(pen)
        fm = QtGui.QFontMetrics(painter.font())
        fw = fm.averageCharWidth()
        fh = fm.height() - 2
        x0 = rect.left()
        x1 = rect.right()
        y0 = rect.bottom()
        y1 = y0 + 4
        lastx = x0

        # horizontal axis
        for tick in range(low, hi):
            s = str(tick)
            w = fw * len(s)
            x = x0 + w / 2 + rect.width() * (tick - self.vmin
                                             ) / float(self.vmax - self.vmin)
            if x - lastx > w + 2 * fw:
                x = round(x)
                painter.drawLine(x, y0, x, y1)
                painter.drawText(round(x - w / 2), y1 + fh - 2, s)
                lastx = x

        #vertical axis
        x1 = rect.left()
        lasty = y0
        for tick in range(1, 10):
            t = tick / 10.0
            y = y0 - self.alphaToY(t) * rect.height()
            if lasty - y > fh and y > 2 * fh:
                y = round(y)
                painter.drawLine(x1 - 5, y, x1, y)
                painter.drawText(x1 - 5 - 3 * fw, round(y - 2 + fh / 2), str(t))
                lasty = y

        # text boxes
        self.text_boxes["vmin"] = self.paintValueBox(painter, fm,
                                                     rect.left(), y1 + fh,
                                                     False, self.vmin)
        self.text_boxes["vmax"] = self.paintValueBox(painter, fm,
                                                     rect.right(), y1 + fh,
                                                     True, self.vmax)
        self.text_boxes["amax"] = self.paintValueBox(
            painter, fm, x0 - 4 * fw, 2 + fh, False, self.amax, format="%.2f")

    def paintZoomArea(self, painter, rect):
        if self.init_pos and self.zoom_pos:
            rect.setLeft(self.init_pos.x())
            rect.setRight(self.zoom_pos.x())
            painter.fillRect(rect, QtGui.QBrush(QtGui.QColor(0, 64, 128, 128)))

    def paintEvent(self, event):
        """
        Paints the editor widget.
        """
        painter = QtGui.QPainter()
        self.paint_rect = event.rect()
        self.paint_rect.adjust(self.left_margin, 0, 0, -self.bottom_margin)

        # tweak color depening on the panel floating state
        # disabled: always use default style
        is_floating = True  # self.parent().parent().isFloating()
        self.line_color = Qt.GlobalColor.darkGray if is_floating else Qt.GlobalColor.lightGray

        painter.begin(self)
        painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
        self.paintGrid(painter, self.paint_rect)
        self.paintAxes(painter, self.paint_rect)
        painter.setClipRect(self.paint_rect)
        self.paintHistogram(painter, self.paint_rect)
        painter.setClipping(False)
        self.paintColorDots(painter, self.paint_rect)
        self.paintZoomArea(painter, self.paint_rect)
        painter.end()

    def enterValue(self, title, value, min_value, max_value):
        """
        Handles entering new values into alpha / min / max boxes.
        """
        new_value, status = QtWidgets.QInputDialog.getDouble(
            self, "", title, value, min_value, max_value, decimals=6)
        if status:
            return new_value

        return value

    def mousePressEvent(self, event):
        # process textbox clicks
        if event.button() == Qt.MouseButton.LeftButton:
            for key, rect in self.text_boxes.items():
                if rect.contains(event.pos()):
                    if key == "amax":
                        self.amax = self.enterValue("Maximum Alpha Value",
                                                    self.amax, EPS, 1.0)
                    elif key == "vmin":
                        self.vmin = self.enterValue("Minimum Data Value",
                                                    self.vmin, -1e8,
                                                    self.vmax - EPS)
                    else:
                        self.vmax = self.enterValue("Maximum Data Value",
                                                    self.vmax, self.vmin + EPS,
                                                    1e8)
                    self.repaint()
                    return

        if self.paint_rect.adjusted(
            -DOT_RADIUS, -DOT_RADIUS, DOT_RADIUS, DOT_RADIUS).contains(
            event.pos()):
            self.dragged = False
            self.point = self.findPoint(event.pos())
            self.init_pos = event.pos()
            self.zoom_pos = None
            self.constraint = None
            if self.point < 0 and event.button() == Qt.MouseButton.LeftButton:
                self.addPoint(
                    event.pos(), event.modifiers() == Qt.KeyboardModifier.ControlModifier)
                # suppress color picker
                self.dragged = True

    def mouseReleaseEvent(self, event):
        if not self.dragged and self.point >= 0:
            if event.button() == Qt.MouseButton.RightButton:
                x, y, r, g, b = self.points[self.point]
                # in 2.0: help says NoModifier, implemented is ControlModifier
                if event.modifiers() in (Qt.KeyboardModifier.ControlModifier, Qt.KeyboardModifier.NoModifier):
                    value = self.points[self.point][0]
                    prev_x = self.points[self.point-1][0] if self.point > 0 else self.vmin
                    next_x = self.points[self.point+1][0] if self.point < len(self.points)-1 else self.vmax
                    x = self.enterValue("Data value",
                        value, prev_x, next_x)
                elif event.modifiers() == Qt.KeyboardModifier.ShiftModifier:
                    value = self.points[self.point][1]
                    y = self.enterValue("Alpha value (opacity)",
                        value, 0.0, 1.0)
                self.points[self.point] = (x, y, r, g, b)
                self.repaint()
                if self.real_time:
                    self.updateVolumeColors()
            if (event.button() == Qt.MouseButton.MiddleButton or
                (event.button() == Qt.MouseButton.LeftButton and
                 event.modifiers() & Qt.KeyboardModifier.ShiftModifier)):
                self.removePoints(
                    event.modifiers() & Qt.KeyboardModifier.ControlModifier)
            elif event.button() == Qt.MouseButton.LeftButton:
                self.setPointColor(self.point,
                        event.modifiers() == Qt.KeyboardModifier.ControlModifier)

        self.point = -1
        self.hover_point = -1

        if self.init_pos and self.zoom_pos:
            if self.init_pos.x() != self.zoom_pos.x():
                # zoom in
                self.vmin, self.vmax = sorted([
                    self.xToData(self.convertX(self.init_pos.x())),
                    self.xToData(self.convertX(self.zoom_pos.x()))])

        self.zoom_pos = None
        self.repaint()
        self.updateVolumeColors()

    def changePointColor(self, color):
        """
        Changes color of specified point or three points.
        """
        x, y, _, _, _ = self.points[self.color_point]
        r = color.redF()
        g = color.greenF()
        b = color.blueF()
        self.points[self.color_point] = (x, y, r, g, b)
        if self.color_triple:
            # set color for all three points
            if self.color_point > 0:
                x, y, _, _, _ = self.points[self.color_point - 1]
                self.points[self.color_point - 1] = (x, y, r, g, b)
            if self.color_point < len(self.points) - 1:
                x, y, _, _, _ = self.points[self.color_point + 1]
                self.points[self.color_point + 1] = (x, y, r, g, b)

        self.repaint()

    def updatePointColor(self, color):
        """
        This is called when color is changed in real time.
        """
        self.changePointColor(color)
        if self.real_time:
            self.updateVolumeColors()

    def colorDialogClosed(self, result):
        """
        This is called when color dialog is closed.
        """
        if result == QtWidgets.QDialog.DialogCode.Accepted:
            color = self.color_dialog.currentColor()
        else:
            color = self.original_color
        self.changePointColor(color)
        self.updateVolumeColors()

    def setPointColor(self, point, triple):
        """
        Opens color picker and sets color of one or three points.
        """
        self.color_point = point
        self.color_triple = triple
        _, _, r, g, b = self.points[self.color_point]
        if not self.color_dialog:
            self.color_dialog = QtWidgets.QColorDialog(self)
            self.color_dialog.currentColorChanged.connect(
                self.updatePointColor)
            self.color_dialog.finished.connect(self.colorDialogClosed)
        self.original_color = QtGui.QColor.fromRgbF(r, g, b)
        self.color_dialog.setCurrentColor(self.original_color)
        # open modal color dialog
        self.color_dialog.open()

    def toggleRealTimeUpdates(self, value):
        self.real_time = value

    def getColors(self):
        """
        Returns color map colors and coordinates as a flat list.

        @rtype: list of float
        @return: List of x, r, g, b, y values.
        """
        colors = []
        for point in self.points:
            x, y, r, g, b = point
            colors.append(x)
            colors.append(r)
            colors.append(g)
            colors.append(b)
            colors.append(y)
        return colors

    def convertX(self, x_pos):
        """
        Converts mouse X position within the widget to <0,1> range.
        """
        x = (x_pos - self.left_margin) / float(self.width() - self.left_margin)
        return min(max(x, 0.0), 1.0)

    def convertY(self, y_pos):
        """
        Converts mouse Y position within the widget to <0,1> range.
        """
        y = 1.0 - y_pos / float(self.height() - self.bottom_margin)
        return min(max(y, 0.0), 1.0)

    def xToData(self, x):
        """
        Converts <0, 1> value to actual data range.
        """
        return self.vmin + x * (self.vmax - self.vmin)

    def dataToX(self, d):
        """
        Converts data value to <0, 1> range.
        """
        return (d - self.vmin) / (self.vmax - self.vmin)

    def yToAlpha(self, y):
        """
        Converts <0, 1> normalized Y position to alpha value.
        """
        y = (ALPHA_LOG_BASE**y - 1.0) / (ALPHA_LOG_BASE - 1.0)
        return y * self.amax

    def alphaToY(self, a):
        """
        Converts alpha value to <0, 1> normalized Y position.
        """
        if self.amax == 0.0:
            return 0.0
        y = a / self.amax
        return math.log(1.0 + (ALPHA_LOG_BASE - 1.0) * y, ALPHA_LOG_BASE)

    def updateVolumeColors(self):
        """
        Updates volume colors in PyMOL display.
        """
        self.ignore_set_colors = True
        self.cmd.volume_color(self.volume_name, self.getColors())
        self.ignore_set_colors = False

    def mouseMoveEvent(self, event):
        if event.buttons() in (Qt.MouseButton.LeftButton, Qt.MouseButton.RightButton):
            if (event.buttons() == Qt.MouseButton.RightButton and
                    event.modifiers() == Qt.KeyboardModifier.ControlModifier):
                # zoom in
                self.zoom_pos = event.pos()
                self.repaint()
            elif self.point >= 0:
                self.dragged = True
                if event.buttons(
                ) & Qt.MouseButton.RightButton and not self.constraint:
                    # constrained movement
                    dpos = event.pos() - self.init_pos
                    self.constraint = 'x' if (
                        abs(dpos.x()) > abs(dpos.y())) else 'y'
                self.movePoints(event)
        else:
            if not self.paint_rect.adjusted(-DOT_RADIUS, -DOT_RADIUS,
                    DOT_RADIUS, DOT_RADIUS).contains(event.pos()):
                new_point = -1
            else:
                new_point = self.findPoint(event.pos())
            if new_point != self.hover_point:
                self.hover_point = new_point
                self.repaint()

    def wheelEvent(self, event):
        """
        Handles mouse wheel event to change data and alpha range and to
        control vertical position of color points.

        """
        try:
            delta = event.delta()
        except AttributeError:
            # Qt5
            delta = event.angleDelta().y()

        delta /= -1000.0

        for key, rect in self.text_boxes.items():
            if rect.contains(event.position().toPoint()):
                vrange = self.vmax - self.vmin
                if key == "amax":
                    self.amax = max(min(self.amax * (1.0 + delta), 1.0), 0.0)
                elif key == "vmin":
                    self.vmin = min(self.vmin + vrange * delta,
                                    self.vmax - EPS)
                else:
                    self.vmax = max(self.vmax + vrange * delta,
                                    self.vmin + EPS)
                self.repaint()
                return

        for i, p in enumerate(self.points):
            x, y, r, g, b = p
            y -= y * delta
            y = min(max(y, 0.0), 1.0)
            self.points[i] = (x, y, r, g, b)

        self.repaint()
        self.updateVolumeColors()

    def movePoints(self, event):
        """
        Moves selected point(s).
        """
        # delta == 2 if moving three points
        delta = 2 if event.modifiers() == Qt.KeyboardModifier.ControlModifier else 1

        num_points = len(self.points)
        x, y, r, g, b = self.points[self.point]
        new_x = self.xToData(self.convertX(event.x()))
        dx = new_x - x

        if dx < 0:
            min_x = self.points[self.point - delta][
                0] if self.point > delta - 1 else self.vmin
            x0 = self.points[self.point - delta + 1][
                0] if self.point > 0 else x
            if x0 + dx < min_x:
                dx = min_x - x0
        else:
            max_x = self.points[self.point + delta][
                0] if self.point < num_points - delta else self.vmax
            x0 = self.points[self.point + delta - 1][
                0] if self.point < num_points - delta + 1 else x
            if x0 + dx > max_x:
                dx = max_x - x0

        new_x = x + dx
        new_y = self.yToAlpha(self.convertY(event.y()))

        # apply constrained motion
        new_x = x if self.constraint == 'y' else new_x
        new_y = y if self.constraint == 'x' or delta > 1 else new_y

        self.points[self.point] = (new_x, new_y, r, g, b)

        if delta > 1:
            dx = new_x - x
            if self.point > 0:
                x, y, r, g, b = self.points[self.point - 1]
                self.points[self.point - 1] = (x + dx, y, r, g, b)
            if self.point < num_points - 1:
                x, y, r, g, b = self.points[self.point + 1]
                self.points[self.point + 1] = (x + dx, y, r, g, b)

        self.repaint()
        s = "value: %.3f\nalpha: %.3f" % (new_x, new_y)
        QtWidgets.QToolTip.showText(self.mapToGlobal(event.pos()), s)
        if self.real_time:
            self.updateVolumeColors()

    def addPoint(self, pos, three_points):
        """
        Add a new color point.
        @param pos: Position in widget's coordinates.
        @type pos: L{QPoint}
        """
        # get color of new point
        r, g, b = next(self.color_cycle)
        new_x = self.convertX(pos.x())
        new_y = self.yToAlpha(self.convertY(pos.y()))
        new_index = len(self.points)
        for index, point in enumerate(self.points):
            if new_x < self.dataToX(point[0]):
                new_index = index
                break

        self.points.insert(new_index, (self.xToData(new_x), new_y, r, g, b))

        if three_points:
            new_x = self.convertX(pos.x()-10)
            self.points.insert(new_index, (self.xToData(new_x), 0, r, g, b))
            new_x = self.convertX(pos.x()+10)
            self.points.insert(new_index+2, (self.xToData(new_x), 0, r, g, b))
            new_index += 1

        self.point = new_index
        self.repaint()
        self.updateVolumeColors()

    def removePoints(self, three_points):
        """
        Removes one or three color points.
        """
        if self.point >= 0:
            del self.points[self.point]
            if three_points:
                if self.point < len(self.points):
                    del self.points[self.point]
                if self.point > 0:
                    del self.points[self.point - 1]
            self.point = -1
            self.repaint()
            self.updateVolumeColors()

    def findPoint(self, pos):
        """
        Finds a color point at a given cursor position.
        """
        for index, point in enumerate(self.points):
            x, y, r, g, b = point
            dx = self.dataToX(x) * (self.width() - self.left_margin
                                    ) - pos.x() + self.left_margin
            dy = (self.height() - self.bottom_margin) * (1.0 - self.alphaToY(y)
                                                         ) - pos.y()
            if dx * dx + dy * dy < 4 * DOT_RADIUS * DOT_RADIUS:
                return index
        return -1

    def setHistogram(self, histogram):
        """
        Sets histogram of the volume.

        @param histogram: Volume histogram: min value, max value, avg value,
        std deviation, value0, value1, value2...
        @type histogram: list of floats
        """
        self.vmin = histogram[0]
        self.vmax = histogram[1]

        # check for flat data (useless, but avoid errors in GUI)
        if self.vmin == self.vmax:
            self.vmin -= 1.0
            self.vmax += 1.0
        elif math.isnan(self.vmin) or math.isnan(self.vmax):
            print('Warning: setHistogram vmin={} vmax={}'.format(
                self.vmin, self.vmax))
            self.vmin = self.original_vmin
            self.vmax = self.original_vmax
            return

        self.original_vmin = self.vmin
        self.original_vmax = self.vmax
        self.path = []

        hist = histogram[4:]
        N = len(hist)
        if N == 0:
            return

        # cut extreme peaks in distribution
        shist = sorted(hist)
        q90 = shist[int(N * 0.9)]
        max_value = min(q90 * 4, shist[N - 1])
        if max_value == 0.0:
            return

        xstep = 1.0 / N
        ynorm = 1.0 / max_value
        x = 0.0
        for v in hist:
            x += xstep
            y = v * ynorm
            self.path.append((x, y))

    def setColors(self, colors):
        """
        Sets color map in the widget.

        @param colors: Flat list of values and corresponding RGBA colors
        listed as value0, R0, G0, B0, A0, value1, R1, G1, B1, A1, ...
        @type colors: list of floats
        """
        if self.ignore_set_colors:
            return
        self.points = []
        for p in range(0, len(colors), 5):
            v = colors[p]
            r = colors[p + 1]
            g = colors[p + 2]
            b = colors[p + 3]
            a = colors[p + 4]
            x = v
            y = a

            if math.isnan(x):
                print('Warning: setColors x={}'.format(x))
                return

            self.points.append((x, y, r, g, b))

        self.update()

    def reset(self):
        """
        Resets vmin and vmax to original values.
        """
        self.vmin = self.original_vmin
        self.vmax = self.original_vmax
        self.repaint()
        self.updateVolumeColors()

    def displayTextDialog(self, text):
        """
        Opens a generic text dialog and displays provided text.
        """
        if not self.text_dialog:
            self.text_dialog = QtWidgets.QDialog()
            layout = QtWidgets.QVBoxLayout()
            self.text_dialog.setLayout(layout)
            self.text_dialog.text_display = QtWidgets.QPlainTextEdit()
            self.text_dialog.text_display.setReadOnly(True)
            self.text_dialog.text_display.setStyleSheet(
                "font-family: monospace, courier")
            size = self.text_dialog.size()
            size.setWidth(DEFAULT_TEXT_DIALOG_WIDTH)
            self.text_dialog.resize(size)
            layout.addWidget(self.text_dialog.text_display)

        self.text_dialog.text_display.setPlainText(text)
        self.text_dialog.show()
        self.text_dialog.raise_()

    def displayHelp(self):
        """
        Displays text dialog with help.
        """
        self.displayTextDialog(VOLUME_HELP)

    def displayScript(self):
        """
        Displays contents of volume color ramp as script.
        """
        import random

        r = self.getColors()
        rname = 'ramp%03d' % random.randint(0, 999)
        s = ['### cut below here and paste into script ###\n']
        s.append('cmd.volume_ramp_new(%s, [\\\n' % repr(rname))
        for i in range(0, len(r), 5):
            s.append('    %6.2f, %.2f, %.2f, %.2f, %.2f, \\\n' %
                     tuple(r[i:i + 5]))
        s.append('    ])\n')
        s.append('### cut above here and paste into script ###\n')

        s += [
            '\n',
            'Paste into a .pml or .py script or your pymolrc file and use this\n',
            'named color ramp on the PyMOL command line like this:\n',
            '\n',
            'PyMOL> volume_color yourvolume, %s\n' % rname,
        ]

        self.displayTextDialog(''.join(s))

    def windowTopLevelChanged(self, floating):
        """
        Update widget colors based on floating state.
        """
        if floating:
            self.update_cb.setStyleSheet("color: black;")
        else:
            self.update_cb.setStyleSheet("color: lightgrey;")


def VolumePanelDocked(parent, *args, **kwargs):
    widget = QtWidgets.QWidget(parent)
    window = QtWidgets.QDockWidget(parent)
    _VolumePanel(widget, window, *args, **kwargs)
    window.setWidget(widget)
    parent.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, window)

    # disabled: always use default style
    # window.topLevelChanged.connect(widget.editor.windowTopLevelChanged)

    return window


def VolumePanelDialog(parent, *args, **kwargs):
    window = QtWidgets.QDialog(parent)
    _VolumePanel(window, window, *args, **kwargs)
    return window


VolumePanel = VolumePanelDocked


class _VolumePanel(object):
    def __init__(self, widget, window=None, name='', _self=None):

        if window:
            window.setWindowTitle(name + ' - Volume Color Map Editor')

        cmd = _self

        layout = QtWidgets.QVBoxLayout()
        widget.setLayout(layout)

        widget.editor = VolumeEditorWidget(widget, name, _self)
        layout.addWidget(widget.editor)
        layout.setContentsMargins(5, 5, 5, 5)
        get_colors_btn = QtWidgets.QPushButton("Get colors as script")
        get_colors_btn.setAutoDefault(False)
        get_colors_btn.clicked.connect(widget.editor.displayScript)
        help_btn = QtWidgets.QPushButton("Help")
        help_btn.setAutoDefault(False)
        help_btn.clicked.connect(widget.editor.displayHelp)
        reset_btn = QtWidgets.QPushButton("Reset Data Range")
        reset_btn.setAutoDefault(False)
        reset_btn.clicked.connect(widget.editor.reset)
        widget.editor.update_cb = QtWidgets.QCheckBox(
            "Update volume colors in real-time")
        widget.editor.update_cb.setObjectName("volume_checkbox")
        widget.editor.update_cb.setChecked(True)
        widget.editor.update_cb.setMinimumWidth(30)
        widget.editor.update_cb.toggled.connect(
            widget.editor.toggleRealTimeUpdates)

        button_layout = QtWidgets.QHBoxLayout()
        button_layout.addWidget(get_colors_btn)
        button_layout.addWidget(reset_btn)
        button_layout.addWidget(help_btn)
        button_layout.addStretch()
        button_layout.addWidget(widget.editor.update_cb)

        layout.addLayout(button_layout)

        histogram = cmd.get_volume_histogram(name)
        widget.editor.setHistogram(histogram)

        colors = cmd.volume_color(name)
        widget.editor.setColors(colors)
