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