--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/E5Gui/E5MapWidget.py Sat May 15 18:45:04 2021 +0200 @@ -0,0 +1,352 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a base class for showing a document map. +""" + +from PyQt5.QtCore import Qt, QSize, QRect, QCoreApplication +from PyQt5.QtGui import QColor, QBrush, QPainter +from PyQt5.QtWidgets import QWidget, QAbstractScrollArea + + +class E5MapWidget(QWidget): + """ + Class implementing a base class for showing a document map. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super().__init__(parent) + self.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent) + + self.__width = 14 + self.__lineBorder = 1 + self.__lineHeight = 2 + self.__backgroundColor = QColor("#e7e7e7") + self.__setSliderColor() + + self._master = None + self.__enabled = False + self.__rightSide = True + + if parent is not None and isinstance(parent, QAbstractScrollArea): + self.setMaster(parent) + + def __setSliderColor(self): + """ + Private method to set the slider color depending upon the background + color. + """ + if self.__backgroundColor.toHsv().value() < 128: + # dark background, use white slider + self.__sliderColor = Qt.GlobalColor.white + else: + # light background, use black slider + self.__sliderColor = Qt.GlobalColor.black + + def __updateMasterViewportWidth(self): + """ + Private method to update the master's viewport width. + """ + if self._master: + if self.__enabled: + width = self.__width + else: + width = 0 + if self.__rightSide: + self._master.setViewportMargins(0, 0, width, 0) + else: + self._master.setViewportMargins(width, 0, 0, 0) + + def setMaster(self, master): + """ + Public method to set the map master widget. + + @param master map master widget (QAbstractScrollArea) + """ + self._master = master + self._master.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + self._master.verticalScrollBar().valueChanged.connect(self.update) + self._master.verticalScrollBar().rangeChanged.connect(self.update) + self.__updateMasterViewportWidth() + + def setWidth(self, width): + """ + Public method to set the widget width. + + @param width widget width (integer) + """ + if width != self.__width: + self.__width = max(6, width) # minimum width 6 pixels + self.__updateMasterViewportWidth() + self.update() + + def width(self): + """ + Public method to get the widget's width. + + @return widget width (integer) + """ + return self.__width + + def setMapPosition(self, onRight): + """ + Public method to set, whether the map should be shown to the right or + left of the master widget. + + @param onRight flag indicating to show the map on the right side of + the master widget + @type bool + """ + if onRight != self.__rightSide: + self.__rightSide = onRight + self.__updateMasterViewportWidth() + self.update() + + def isOnRightSide(self): + """ + Public method to test, if the map is shown on the right side of the + master widget. + + @return flag indicating that the map is to the right of the master + widget + @rtype bool + """ + return self.__rightSide + + def setLineDimensions(self, border, height): + """ + Public method to set the line (indicator) dimensions. + + @param border border width on each side in x-direction (integer) + @param height height of the line in pixels (integer) + """ + if border != self.__lineBorder or height != self.__lineHeight: + self.__lineBorder = max(1, border) # min border 1 pixel + self.__lineHeight = max(1, height) # min height 1 pixel + self.update() + + def lineDimensions(self): + """ + Public method to get the line (indicator) dimensions. + + @return tuple with border width (integer) and line height (integer) + """ + return self.__lineBorder, self.__lineHeight + + def setEnabled(self, enable): + """ + Public method to set the enabled state. + + @param enable flag indicating the enabled state (boolean) + """ + if enable != self.__enabled: + self.__enabled = enable + self.setVisible(enable) + self.__updateMasterViewportWidth() + + def isEnabled(self): + """ + Public method to check the enabled state. + + @return flag indicating the enabled state (boolean) + """ + return self.__enabled + + def setBackgroundColor(self, color): + """ + Public method to set the widget background color. + + @param color color for the background (QColor) + """ + if color != self.__backgroundColor: + self.__backgroundColor = color + self.__setSliderColor() + self.update() + + def backgroundColor(self): + """ + Public method to get the background color. + + @return background color (QColor) + """ + return QColor(self.__backgroundColor) + + def sizeHint(self): + """ + Public method to give an indication about the preferred size. + + @return preferred size (QSize) + """ + return QSize(self.__width, 0) + + def paintEvent(self, event): + """ + Protected method to handle a paint event. + + @param event paint event (QPaintEvent) + """ + # step 1: fill the whole painting area + painter = QPainter(self) + painter.fillRect(event.rect(), self.__backgroundColor) + + # step 2: paint the indicators + self._paintIt(painter) + + # step 3: paint the slider + if self._master: + penColor = self.__sliderColor + painter.setPen(penColor) + brushColor = Qt.GlobalColor.transparent + painter.setBrush(QBrush(brushColor)) + painter.drawRect(self.__generateSliderRange( + self._master.verticalScrollBar())) + + def _paintIt(self, painter): + """ + Protected method for painting the widget's indicators. + + Note: This method should be implemented by subclasses. + + @param painter reference to the painter object (QPainter) + """ + pass + + def mousePressEvent(self, event): + """ + Protected method to handle a mouse button press. + + @param event reference to the mouse event (QMouseEvent) + """ + if event.button() == Qt.MouseButton.LeftButton and self._master: + vsb = self._master.verticalScrollBar() + value = self.position2Value(event.pos().y() - 1) + vsb.setValue(value - 0.5 * vsb.pageStep()) # center on page + self.__mousePressPos = None + + def mouseMoveEvent(self, event): + """ + Protected method to handle a mouse moves. + + @param event reference to the mouse event (QMouseEvent) + """ + if event.buttons() & Qt.MouseButton.LeftButton and self._master: + vsb = self._master.verticalScrollBar() + value = self.position2Value(event.pos().y() - 1) + vsb.setValue(value - 0.5 * vsb.pageStep()) # center on page + + def wheelEvent(self, event): + """ + Protected slot handling mouse wheel events. + + @param event reference to the wheel event (QWheelEvent) + """ + isVertical = event.angleDelta().x() == 0 + if ( + self._master and + event.modifiers() == Qt.KeyboardModifier.NoModifier and + isVertical + ): + QCoreApplication.sendEvent(self._master.verticalScrollBar(), event) + + def calculateGeometry(self): + """ + Public method to recalculate the map widget's geometry. + """ + if self._master: + cr = self._master.contentsRect() + vsb = self._master.verticalScrollBar() + if vsb.isVisible(): + vsbw = vsb.contentsRect().width() + else: + vsbw = 0 + left, top, right, bottom = self._master.getContentsMargins() + if right > vsbw: + vsbw = 0 + if self.__rightSide: + self.setGeometry( + QRect(cr.right() - self.__width - vsbw, cr.top(), + self.__width, cr.height())) + else: + self.setGeometry( + QRect(0, cr.top(), self.__width, cr.height())) + self.update() + + def scaleFactor(self, slider=False): + """ + Public method to determine the scrollbar's scale factor. + + @param slider flag indicating to calculate the result for the slider + (boolean) + @return scale factor (float) + """ + if self._master: + delta = 0 if slider else 2 + vsb = self._master.verticalScrollBar() + posHeight = vsb.height() - delta - 1 + valHeight = vsb.maximum() - vsb.minimum() + vsb.pageStep() + return float(posHeight) / valHeight + else: + return 1.0 + + def value2Position(self, value, slider=False): + """ + Public method to convert a scrollbar value into a position. + + @param value value to convert (integer) + @param slider flag indicating to calculate the result for the slider + (boolean) + @return position (integer) + """ + if self._master: + offset = 0 if slider else 1 + vsb = self._master.verticalScrollBar() + return (value - vsb.minimum()) * self.scaleFactor(slider) + offset + else: + return value + + def position2Value(self, position, slider=False): + """ + Public method to convert a position into a scrollbar value. + + @param position scrollbar position to convert (integer) + @param slider flag indicating to calculate the result for the slider + (boolean) + @return scrollbar value (integer) + """ + if self._master: + offset = 0 if slider else 1 + vsb = self._master.verticalScrollBar() + return vsb.minimum() + max( + 0, (position - offset) / self.scaleFactor(slider)) + else: + return position + + def generateIndicatorRect(self, position): + """ + Public method to generate an indicator rectangle. + + @param position indicator position (integer) + @return indicator rectangle (QRect) + """ + return QRect(self.__lineBorder, position - self.__lineHeight // 2, + self.__width - self.__lineBorder, self.__lineHeight) + + def __generateSliderRange(self, scrollbar): + """ + Private method to generate the slider rectangle. + + @param scrollbar reference to the vertical scrollbar (QScrollBar) + @return slider rectangle (QRect) + """ + pos1 = self.value2Position(scrollbar.value(), slider=True) + pos2 = self.value2Position(scrollbar.value() + scrollbar.pageStep(), + slider=True) + return QRect(0, pos1, self.__width - 1, pos2 - pos1)