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