1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2014 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a base class for showing a document map.
8"""
9
10from PyQt5.QtCore import Qt, QSize, QRect, QCoreApplication
11from PyQt5.QtGui import QColor, QBrush, QPainter
12from PyQt5.QtWidgets import QWidget, QAbstractScrollArea
13
14
15class E5MapWidget(QWidget):
16    """
17    Class implementing a base class for showing a document map.
18    """
19    def __init__(self, parent=None):
20        """
21        Constructor
22
23        @param parent reference to the parent widget (QWidget)
24        """
25        super().__init__(parent)
26        self.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent)
27
28        self.__width = 14
29        self.__lineBorder = 1
30        self.__lineHeight = 2
31        self.__backgroundColor = QColor("#e7e7e7")
32        self.__setSliderColor()
33
34        self._master = None
35        self.__enabled = False
36        self.__rightSide = True
37
38        if parent is not None and isinstance(parent, QAbstractScrollArea):
39            self.setMaster(parent)
40
41    def __setSliderColor(self):
42        """
43        Private method to set the slider color depending upon the background
44        color.
45        """
46        if self.__backgroundColor.toHsv().value() < 128:
47            # dark background, use white slider
48            self.__sliderColor = Qt.GlobalColor.white
49        else:
50            # light background, use black slider
51            self.__sliderColor = Qt.GlobalColor.black
52
53    def __updateMasterViewportWidth(self):
54        """
55        Private method to update the master's viewport width.
56        """
57        if self._master:
58            if self.__enabled:
59                width = self.__width
60            else:
61                width = 0
62            if self.__rightSide:
63                self._master.setViewportMargins(0, 0, width, 0)
64            else:
65                self._master.setViewportMargins(width, 0, 0, 0)
66
67    def setMaster(self, master):
68        """
69        Public method to set the map master widget.
70
71        @param master map master widget (QAbstractScrollArea)
72        """
73        self._master = master
74        self._master.setVerticalScrollBarPolicy(
75            Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
76        self._master.verticalScrollBar().valueChanged.connect(self.update)
77        self._master.verticalScrollBar().rangeChanged.connect(self.update)
78        self.__updateMasterViewportWidth()
79
80    def setWidth(self, width):
81        """
82        Public method to set the widget width.
83
84        @param width widget width (integer)
85        """
86        if width != self.__width:
87            self.__width = max(6, width)    # minimum width 6 pixels
88            self.__updateMasterViewportWidth()
89            self.update()
90
91    def width(self):
92        """
93        Public method to get the widget's width.
94
95        @return widget width (integer)
96        """
97        return self.__width
98
99    def setMapPosition(self, onRight):
100        """
101        Public method to set, whether the map should be shown to the right or
102        left of the master widget.
103
104        @param onRight flag indicating to show the map on the right side of
105            the master widget
106        @type bool
107        """
108        if onRight != self.__rightSide:
109            self.__rightSide = onRight
110            self.__updateMasterViewportWidth()
111            self.update()
112
113    def isOnRightSide(self):
114        """
115        Public method to test, if the map is shown on the right side of the
116        master widget.
117
118        @return flag indicating that the map is to the right of the master
119            widget
120        @rtype bool
121        """
122        return self.__rightSide
123
124    def setLineDimensions(self, border, height):
125        """
126        Public method to set the line (indicator) dimensions.
127
128        @param border border width on each side in x-direction (integer)
129        @param height height of the line in pixels (integer)
130        """
131        if border != self.__lineBorder or height != self.__lineHeight:
132            self.__lineBorder = max(1, border)  # min border 1 pixel
133            self.__lineHeight = max(1, height)  # min height 1 pixel
134            self.update()
135
136    def lineDimensions(self):
137        """
138        Public method to get the line (indicator) dimensions.
139
140        @return tuple with border width (integer) and line height (integer)
141        """
142        return self.__lineBorder, self.__lineHeight
143
144    def setEnabled(self, enable):
145        """
146        Public method to set the enabled state.
147
148        @param enable flag indicating the enabled state (boolean)
149        """
150        if enable != self.__enabled:
151            self.__enabled = enable
152            self.setVisible(enable)
153            self.__updateMasterViewportWidth()
154
155    def isEnabled(self):
156        """
157        Public method to check the enabled state.
158
159        @return flag indicating the enabled state (boolean)
160        """
161        return self.__enabled
162
163    def setBackgroundColor(self, color):
164        """
165        Public method to set the widget background color.
166
167        @param color color for the background (QColor)
168        """
169        if color != self.__backgroundColor:
170            self.__backgroundColor = color
171            self.__setSliderColor()
172            self.update()
173
174    def backgroundColor(self):
175        """
176        Public method to get the background color.
177
178        @return background color (QColor)
179        """
180        return QColor(self.__backgroundColor)
181
182    def sizeHint(self):
183        """
184        Public method to give an indication about the preferred size.
185
186        @return preferred size (QSize)
187        """
188        return QSize(self.__width, 0)
189
190    def paintEvent(self, event):
191        """
192        Protected method to handle a paint event.
193
194        @param event paint event (QPaintEvent)
195        """
196        # step 1: fill the whole painting area
197        painter = QPainter(self)
198        painter.fillRect(event.rect(), self.__backgroundColor)
199
200        # step 2: paint the indicators
201        self._paintIt(painter)
202
203        # step 3: paint the slider
204        if self._master:
205            penColor = self.__sliderColor
206            painter.setPen(penColor)
207            brushColor = Qt.GlobalColor.transparent
208            painter.setBrush(QBrush(brushColor))
209            painter.drawRect(self.__generateSliderRange(
210                self._master.verticalScrollBar()))
211
212    def _paintIt(self, painter):
213        """
214        Protected method for painting the widget's indicators.
215
216        Note: This method should be implemented by subclasses.
217
218        @param painter reference to the painter object (QPainter)
219        """
220        pass
221
222    def mousePressEvent(self, event):
223        """
224        Protected method to handle a mouse button press.
225
226        @param event reference to the mouse event (QMouseEvent)
227        """
228        if event.button() == Qt.MouseButton.LeftButton and self._master:
229            vsb = self._master.verticalScrollBar()
230            value = self.position2Value(event.pos().y() - 1)
231            vsb.setValue(value - 0.5 * vsb.pageStep())  # center on page
232        self.__mousePressPos = None
233
234    def mouseMoveEvent(self, event):
235        """
236        Protected method to handle a mouse moves.
237
238        @param event reference to the mouse event (QMouseEvent)
239        """
240        if event.buttons() & Qt.MouseButton.LeftButton and self._master:
241            vsb = self._master.verticalScrollBar()
242            value = self.position2Value(event.pos().y() - 1)
243            vsb.setValue(value - 0.5 * vsb.pageStep())  # center on page
244
245    def wheelEvent(self, event):
246        """
247        Protected slot handling mouse wheel events.
248
249        @param event reference to the wheel event (QWheelEvent)
250        """
251        isVertical = event.angleDelta().x() == 0
252        if (
253            self._master and
254            event.modifiers() == Qt.KeyboardModifier.NoModifier and
255            isVertical
256        ):
257            QCoreApplication.sendEvent(self._master.verticalScrollBar(), event)
258
259    def calculateGeometry(self):
260        """
261        Public method to recalculate the map widget's geometry.
262        """
263        if self._master:
264            cr = self._master.contentsRect()
265            vsb = self._master.verticalScrollBar()
266            if vsb.isVisible():
267                vsbw = vsb.contentsRect().width()
268            else:
269                vsbw = 0
270            left, top, right, bottom = self._master.getContentsMargins()
271            if right > vsbw:
272                vsbw = 0
273            if self.__rightSide:
274                self.setGeometry(
275                    QRect(cr.right() - self.__width - vsbw, cr.top(),
276                          self.__width, cr.height()))
277            else:
278                self.setGeometry(
279                    QRect(0, cr.top(), self.__width, cr.height()))
280            self.update()
281
282    def scaleFactor(self, slider=False):
283        """
284        Public method to determine the scrollbar's scale factor.
285
286        @param slider flag indicating to calculate the result for the slider
287            (boolean)
288        @return scale factor (float)
289        """
290        if self._master:
291            delta = 0 if slider else 2
292            vsb = self._master.verticalScrollBar()
293            posHeight = vsb.height() - delta - 1
294            valHeight = vsb.maximum() - vsb.minimum() + vsb.pageStep()
295            return float(posHeight) / valHeight
296        else:
297            return 1.0
298
299    def value2Position(self, value, slider=False):
300        """
301        Public method to convert a scrollbar value into a position.
302
303        @param value value to convert (integer)
304        @param slider flag indicating to calculate the result for the slider
305            (boolean)
306        @return position (integer)
307        """
308        if self._master:
309            offset = 0 if slider else 1
310            vsb = self._master.verticalScrollBar()
311            return (value - vsb.minimum()) * self.scaleFactor(slider) + offset
312        else:
313            return value
314
315    def position2Value(self, position, slider=False):
316        """
317        Public method to convert a position into a scrollbar value.
318
319        @param position scrollbar position to convert (integer)
320        @param slider flag indicating to calculate the result for the slider
321            (boolean)
322        @return scrollbar value (integer)
323        """
324        if self._master:
325            offset = 0 if slider else 1
326            vsb = self._master.verticalScrollBar()
327            return vsb.minimum() + max(
328                0, (position - offset) / self.scaleFactor(slider))
329        else:
330            return position
331
332    def generateIndicatorRect(self, position):
333        """
334        Public method to generate an indicator rectangle.
335
336        @param position indicator position (integer)
337        @return indicator rectangle (QRect)
338        """
339        return QRect(self.__lineBorder, position - self.__lineHeight // 2,
340                     self.__width - self.__lineBorder, self.__lineHeight)
341
342    def __generateSliderRange(self, scrollbar):
343        """
344        Private method to generate the slider rectangle.
345
346        @param scrollbar reference to the vertical scrollbar (QScrollBar)
347        @return slider rectangle (QRect)
348        """
349        pos1 = self.value2Position(scrollbar.value(), slider=True)
350        pos2 = self.value2Position(scrollbar.value() + scrollbar.pageStep(),
351                                   slider=True)
352        return QRect(0, pos1, self.__width - 1, pos2 - pos1)
353