def scaled(size: QSizeF, constraint: QSizeF, mode=Qt.KeepAspectRatio) -> QSizeF: """ Return size scaled to fit in the constrains using the aspect mode `mode`. If width or height of constraint are negative they are ignored, ie. the result is not constrained in that dimension. """ size, constraint = QSizeF(size), QSizeF(constraint) if constraint.width() < 0 and constraint.height() < 0: return size if mode == Qt.IgnoreAspectRatio: if constraint.width() >= 0: size.setWidth(constraint.width()) if constraint.height() >= 0: size.setHeight(constraint.height()) elif mode == Qt.KeepAspectRatio: if constraint.width() < 0: constraint.setWidth(QWIDGETSIZE_MAX) if constraint.height() < 0: constraint.setHeight(QWIDGETSIZE_MAX) size.scale(constraint, mode) elif mode == Qt.KeepAspectRatioByExpanding: if constraint.width() < 0: constraint.setWidth(0) if constraint.height() < 0: constraint.setHeight(0) size.scale(constraint, mode) return size
def _rescale(self): if self._root is None: return scale = self._height_scale_factor() base = scale * self._root.value.height crect = self.contentsRect() leaf_count = len(list(leaves(self._root))) if self.orientation in [Left, Right]: drect = QSizeF(base, leaf_count) else: drect = QSizeF(leaf_count, base) eps = np.finfo(np.float64).eps if abs(drect.width()) < eps: sx = 1.0 else: sx = crect.width() / drect.width() if abs(drect.height()) < eps: sy = 1.0 else: sy = crect.height() / drect.height() transform = QTransform().scale(sx, sy) self._transform = transform self._itemgroup.setPos(crect.topLeft()) self._itemgroup.setGeometry(crect) for node_geom in postorder(self._layout): node, _ = node_geom.value item = self._items[node] item.setGeometryData(transform.map(item.sourcePath), transform.map(item.sourceAreaShape)) self._selection_items = None self._update_selection_items()
def sizeHint(self, which: Qt.SizeHint, constraint=QSizeF(-1, -1)) -> QSizeF: if which == Qt.PreferredSize: brect = self.item.boundingRect() brect = self.item.mapRectToParent(brect) scale = self.__transform size = brect.size() if scale is not None: # undo the scaling sx, sy = self.__scale size = QSizeF(float(Fraction(size.width()) / sx), float(Fraction(size.height()) / sy)) if constraint != QSizeF(-1, -1): size = scaled(size, constraint, self.__aspectMode) return size else: return QSizeF()
def pixmapTransform(self) -> QTransform: if self.__pixmap.isNull(): return QTransform() pxsize = QSizeF(self.__pixmap.size()) crect = self.contentsRect() transform = QTransform() transform = transform.translate(crect.left(), crect.top()) if self.__scaleContents: csize = scaled(pxsize, crect.size(), self.__aspectMode) else: csize = pxsize xscale = csize.width() / pxsize.width() yscale = csize.height() / pxsize.height() return transform.scale(xscale, yscale)
def paint(self, painter, option, widget=0): if self._pixmap.isNull(): return rect = self.contentsRect() pixsize = QSizeF(self._pixmap.size()) aspectmode = (Qt.KeepAspectRatio if self._keepAspect else Qt.IgnoreAspectRatio) if self._crop: height, width = pixsize.height(), pixsize.width() diff = abs(height - width) if height > width: y, x = diff / 2, 0 h, w = height - diff, width else: x, y = diff / 2, 0 w, h = width - diff, height source = QRectF(x, y, w, h) pixrect = QRectF(QPointF(0, 0), rect.size()) else: source = QRectF(QPointF(0, 0), pixsize) pixsize.scale(rect.size(), aspectmode) pixrect = QRectF(QPointF(0, 0), pixsize) if self._subset: painter.setOpacity(1.0) else: painter.setOpacity(0.35) pixrect.moveCenter(rect.center()) painter.save() painter.setPen(QPen(QColor(0, 0, 0, 50), 3)) painter.drawRoundedRect(pixrect, 2, 2) painter.setRenderHint(QPainter.SmoothPixmapTransform) painter.drawPixmap(pixrect, self._pixmap, source) painter.restore()
class OWLegend(QGraphicsObject): """ A legend for :obj:`.OWPlot`. Its items are arranged into a hierarchy by `category`. This is useful when points differ in more than one attribute. In such a case, there can be one category for point color and one for point shape. Usually the category name will be the name of the attribute, while the item's title will be the value. Arbitrary categories can be created, for an example see :meth:`.OWPlot.update_axes`, which creates a special category for unused axes. decimals .. image:: files/legend-categories.png In the image above, `type` and `milk` are categories with 7 and 2 possible values, respectively. """ def __init__(self, graph, scene): QGraphicsObject.__init__(self) if scene: scene.addItem(self) self.graph = graph self.curves = [] self.items = {} self.attributes = [] self.point_attrs = {} self.point_vals = {} self.default_values = { PointColor: Qt.black, PointSize: 8, PointSymbol: OWPoint.Ellipse } self.box_rect = QRectF() self.setFiltersChildEvents(True) self.setFlag(self.ItemHasNoContents, True) self.mouse_down = False self._orientation = Qt.Vertical self.max_size = QSizeF() self._floating = True self._floating_animation = None self._mouse_down_pos = QPointF() def clear(self): """ Removes all items from the legend """ for lst in self.items.values(): for i in lst: i.setParentItem(None) if self.scene(): self.scene().removeItem(i) self.items = {} self.update_items() def add_curve(self, curve): """ Adds a legend item with the same point symbol and name as ``curve``. If the curve's name contains the equal sign (=), it is split at that sign. The first part of the curve is a used as the category, and the second part as the value. """ i = curve.name.find('=') if i == -1: cat = '' name = curve.name else: cat = curve.name[:i] name = curve.name[i + 1:] self.add_item(cat, name, curve.point_item(0, 0, 0)) def add_item(self, category, value, point): """ Adds an item with title ``value`` and point symbol ``point`` to the specified ``category``. """ if category not in self.items: self.items[category] = [OWLegendTitle(category, self)] self.items[category].append(OWLegendItem(str(value), point, self)) self.update_items() def add_color_gradient(self, title, values): if len(values) < 2: # No point in showing a gradient with less that two values return if title in self.items: self.remove_category(title) item = OWLegendGradient(self.graph.contPalette, [str(v) for v in values], self) self.items[title] = [OWLegendTitle(title, self), item] self.update_items() def remove_category(self, category): """ Removes ``category`` and all items that belong to it. """ if category not in self.items: return if self.scene(): for item in self.items[category]: self.scene().removeItem(item) del self.items[category] def update_items(self): """ Updates the legend, repositioning the items according to the legend's orientation. """ self.box_rect = QRectF() x = y = 0 for lst in self.items.values(): for item in lst: if hasattr(item, 'text_item'): item.text_item.setDefaultTextColor( self.graph.color(OWPalette.Text)) if hasattr(item, 'rect_item'): item.rect_item.setBrush(self.graph.color(OWPalette.Canvas)) if hasattr(item, 'set_orientation'): item.set_orientation(self._orientation) if self._orientation == Qt.Vertical: for lst in self.items.values(): for item in lst: if self.max_size.height() and y and y + item.boundingRect( ).height() > self.max_size.height(): y = 0 x = x + item.boundingRect().width() self.box_rect = self.box_rect | item.boundingRect( ).translated(x, y) move_item_xy(item, x, y, self.graph.animate_plot) y = y + item.boundingRect().height() elif self._orientation == Qt.Horizontal: for lst in self.items.values(): max_h = max(item.boundingRect().height() for item in lst) for item in lst: if self.max_size.width() and x and x + item.boundingRect( ).width() > self.max_size.width(): x = 0 y = y + max_h self.box_rect = self.box_rect | item.boundingRect( ).translated(x, y) move_item_xy(item, x, y, self.graph.animate_plot) x = x + item.boundingRect().width() if lst: x = 0 y = y + max_h def mouseMoveEvent(self, event): self.graph.notify_legend_moved(event.scenePos()) if self._floating: p = event.scenePos() - self._mouse_down_pos if self._floating_animation and self._floating_animation.state( ) == QPropertyAnimation.Running: self.set_pos_animated(p) else: self.setPos(p) event.accept() def mousePressEvent(self, event): self.setCursor(Qt.ClosedHandCursor) self.mouse_down = True self._mouse_down_pos = event.scenePos() - self.pos() event.accept() def mouseReleaseEvent(self, event): self.unsetCursor() self.mouse_down = False self._mouse_down_pos = QPointF() event.accept() def boundingRect(self): return self.box_rect def paint(self, painter, option, widget=None): pass def set_orientation(self, orientation): """ Sets the legend's orientation to ``orientation``. """ self._orientation = orientation self.update_items() def orientation(self): return self._orientation def set_pos_animated(self, pos): if (self.pos() - pos).manhattanLength() < 6 or not self.graph.animate_plot: self.setPos(pos) else: t = 250 if self._floating_animation and self._floating_animation.state( ) == QPropertyAnimation.Running: t = t - self._floating_animation.currentTime() self._floating_animation.stop() self._floating_animation = QPropertyAnimation(self, 'pos') self._floating_animation.setEndValue(pos) self._floating_animation.setDuration(t) self._floating_animation.start(QPropertyAnimation.KeepWhenStopped) def set_floating(self, floating, pos=None): """ If floating is ``True``, the legend can be dragged with the mouse. Otherwise, it's fixed in its position. If ``pos`` is specified, the legend is moved there. """ if floating == self._floating: return self._floating = floating if pos: if floating: self.set_pos_animated(pos - self._mouse_down_pos) else: self.set_pos_animated(pos)
class OWLegend(QGraphicsObject): """ A legend for :obj:`.OWPlot`. Its items are arranged into a hierarchy by `category`. This is useful when points differ in more than one attribute. In such a case, there can be one category for point color and one for point shape. Usually the category name will be the name of the attribute, while the item's title will be the value. Arbitrary categories can be created, for an example see :meth:`.OWPlot.update_axes`, which creates a special category for unused axes. decimals .. image:: files/legend-categories.png In the image above, `type` and `milk` are categories with 7 and 2 possible values, respectively. """ def __init__(self, graph, scene): QGraphicsObject.__init__(self) if scene: scene.addItem(self) self.graph = graph self.curves = [] self.items = {} self.attributes = [] self.point_attrs = {} self.point_vals = {} self.default_values = { PointColor : Qt.black, PointSize : 8, PointSymbol : OWPoint.Ellipse } self.box_rect = QRectF() self.setFiltersChildEvents(True) self.setFlag(self.ItemHasNoContents, True) self.mouse_down = False self._orientation = Qt.Vertical self.max_size = QSizeF() self._floating = True self._floating_animation = None self._mouse_down_pos = QPointF() def clear(self): """ Removes all items from the legend """ for lst in self.items.values(): for i in lst: i.setParentItem(None) if self.scene(): self.scene().removeItem(i) self.items = {} self.update_items() def add_curve(self, curve): """ Adds a legend item with the same point symbol and name as ``curve``. If the curve's name contains the equal sign (=), it is split at that sign. The first part of the curve is a used as the category, and the second part as the value. """ i = curve.name.find('=') if i == -1: cat = '' name = curve.name else: cat = curve.name[:i] name = curve.name[i+1:] self.add_item(cat, name, curve.point_item(0, 0, 0)) def add_item(self, category, value, point): """ Adds an item with title ``value`` and point symbol ``point`` to the specified ``category``. """ if category not in self.items: self.items[category] = [OWLegendTitle(category, self)] self.items[category].append(OWLegendItem(str(value), point, self)) self.update_items() def add_color_gradient(self, title, values): if len(values) < 2: # No point in showing a gradient with less that two values return if title in self.items: self.remove_category(title) item = OWLegendGradient(self.graph.contPalette, [str(v) for v in values], self) self.items[title] = [OWLegendTitle(title, self), item] self.update_items() def remove_category(self, category): """ Removes ``category`` and all items that belong to it. """ if category not in self.items: return if self.scene(): for item in self.items[category]: self.scene().removeItem(item) del self.items[category] def update_items(self): """ Updates the legend, repositioning the items according to the legend's orientation. """ self.box_rect = QRectF() x = y = 0 for lst in self.items.values(): for item in lst: if hasattr(item, 'text_item'): item.text_item.setDefaultTextColor(self.graph.color(OWPalette.Text)) if hasattr(item, 'rect_item'): item.rect_item.setBrush(self.graph.color(OWPalette.Canvas)) if hasattr(item, 'set_orientation'): item.set_orientation(self._orientation) if self._orientation == Qt.Vertical: for lst in self.items.values(): for item in lst: if self.max_size.height() and y and y + item.boundingRect().height() > self.max_size.height(): y = 0 x = x + item.boundingRect().width() self.box_rect = self.box_rect | item.boundingRect().translated(x, y) move_item_xy(item, x, y, self.graph.animate_plot) y = y + item.boundingRect().height() elif self._orientation == Qt.Horizontal: for lst in self.items.values(): max_h = max(item.boundingRect().height() for item in lst) for item in lst: if self.max_size.width() and x and x + item.boundingRect().width() > self.max_size.width(): x = 0 y = y + max_h self.box_rect = self.box_rect | item.boundingRect().translated(x, y) move_item_xy(item, x, y, self.graph.animate_plot) x = x + item.boundingRect().width() if lst: x = 0 y = y + max_h def mouseMoveEvent(self, event): self.graph.notify_legend_moved(event.scenePos()) if self._floating: p = event.scenePos() - self._mouse_down_pos if self._floating_animation and self._floating_animation.state() == QPropertyAnimation.Running: self.set_pos_animated(p) else: self.setPos(p) event.accept() def mousePressEvent(self, event): self.setCursor(Qt.ClosedHandCursor) self.mouse_down = True self._mouse_down_pos = event.scenePos() - self.pos() event.accept() def mouseReleaseEvent(self, event): self.unsetCursor() self.mouse_down = False self._mouse_down_pos = QPointF() event.accept() def boundingRect(self): return self.box_rect def paint(self, painter, option, widget=None): pass def set_orientation(self, orientation): """ Sets the legend's orientation to ``orientation``. """ self._orientation = orientation self.update_items() def orientation(self): return self._orientation def set_pos_animated(self, pos): if (self.pos() - pos).manhattanLength() < 6 or not self.graph.animate_plot: self.setPos(pos) else: t = 250 if self._floating_animation and self._floating_animation.state() == QPropertyAnimation.Running: t = t - self._floating_animation.currentTime() self._floating_animation.stop() self._floating_animation = QPropertyAnimation(self, 'pos') self._floating_animation.setEndValue(pos) self._floating_animation.setDuration(t) self._floating_animation.start(QPropertyAnimation.KeepWhenStopped) def set_floating(self, floating, pos=None): """ If floating is ``True``, the legend can be dragged with the mouse. Otherwise, it's fixed in its position. If ``pos`` is specified, the legend is moved there. """ if floating == self._floating: return self._floating = floating if pos: if floating: self.set_pos_animated(pos - self._mouse_down_pos) else: self.set_pos_animated(pos)