mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-05-17 00:46:01 +03:00
Send image to the API as SVG element
This commit is contained in:
@@ -53,11 +53,11 @@ from .items.serial_link_item import SerialLinkItem
|
||||
# other items
|
||||
from .items.note_item import NoteItem
|
||||
from .items.shape_item import ShapeItem
|
||||
from .items.visual_item import VisualItem
|
||||
from .items.rectangle_item import RectangleItem
|
||||
from .items.ellipse_item import EllipseItem
|
||||
from .items.image_item import ImageItem
|
||||
from .items.pixmap_image_item import PixmapImageItem
|
||||
from .items.svg_image_item import SvgImageItem
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -231,25 +231,17 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_ellipse = False
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addImage(self, image, image_path):
|
||||
def addImage(self,image_path):
|
||||
"""
|
||||
Adds an image.
|
||||
|
||||
:param image: QPixmap or QSvgRenderer instance
|
||||
:param image_path: path to the image
|
||||
"""
|
||||
|
||||
if isinstance(image, QtSvg.QSvgRenderer):
|
||||
# use a SVG image item if this is a valid SVG file
|
||||
image_item = SvgImageItem(image, image_path)
|
||||
else:
|
||||
image_item = PixmapImageItem(image, image_path)
|
||||
# center the image on the scene
|
||||
x = image_item.pos().x() - (image_item.boundingRect().width() / 2)
|
||||
y = image_item.pos().y() - (image_item.boundingRect().height() / 2)
|
||||
image_item.setPos(x, y)
|
||||
image_item = ImageItem(image_path=image_path, project=self._main_window.projectManager().project())
|
||||
|
||||
self.scene().addItem(image_item)
|
||||
self._topology.addImage(image_item)
|
||||
self._topology.addShape(image_item)
|
||||
|
||||
def addLink(self, source_node, source_port, destination_node, destination_port, link_id=None):
|
||||
"""
|
||||
@@ -484,13 +476,13 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_note = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_rectangle:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createShapeItem("rect", pos.x(), pos.y(), 0)
|
||||
self.createVisualItem("rect", pos.x(), pos.y(), 0)
|
||||
self._main_window.uiDrawRectangleAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_rectangle = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_ellipse:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createShapeItem("ellipse", pos.x(), pos.y(), 0)
|
||||
self.createVisualItem("ellipse", pos.x(), pos.y(), 0)
|
||||
self._main_window.uiDrawEllipseAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_ellipse = False
|
||||
@@ -809,7 +801,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
reload_action.triggered.connect(self.reloadActionSlot)
|
||||
menu.addAction(reload_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem) or isinstance(item, ShapeItem) or isinstance(item, ImageItem), items)):
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem) or isinstance(item, VisualItem), items)):
|
||||
duplicate_action = QtWidgets.QAction("Duplicate", menu)
|
||||
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
|
||||
duplicate_action.triggered.connect(self.duplicateActionSlot)
|
||||
@@ -1519,11 +1511,13 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._topology.addNode(node)
|
||||
return node_item
|
||||
|
||||
def createShapeItem(self, type, x, y, z, rotation=0, svg=None, shape_id=None):
|
||||
def createVisualItem(self, type, x, y, z, rotation=0, svg=None, shape_id=None):
|
||||
if type == "ellipse":
|
||||
item = EllipseItem(pos=QtCore.QPoint(x, y), rotation=rotation, project=self._main_window.projectManager().project(), shape_id=shape_id, svg=svg)
|
||||
elif type == "rect":
|
||||
item = RectangleItem(pos=QtCore.QPoint(x, y), rotation=rotation, project=self._main_window.projectManager().project(), shape_id=shape_id, svg=svg)
|
||||
elif type == "image":
|
||||
item = ImageItem(pos=QtCore.QPoint(x, y), rotation=rotation, project=self._main_window.projectManager().project(), shape_id=shape_id, svg=svg)
|
||||
self.scene().addItem(item)
|
||||
self._topology.addShape(item)
|
||||
|
||||
|
||||
@@ -19,82 +19,58 @@
|
||||
Graphical representation of an image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtWidgets, QtCore
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtSvg
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from .visual_item import VisualItem
|
||||
|
||||
|
||||
class ImageItem():
|
||||
class ImageItem(QtSvg.QGraphicsSvgItem, VisualItem):
|
||||
|
||||
"""
|
||||
Class to insert an image on the scene.
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
|
||||
def __init__(self, image_path, pos=None):
|
||||
def __init__(self, image_path=None, pos=None, svg=None, **kws):
|
||||
|
||||
self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)
|
||||
self._image_path = image_path
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
super().__init__(pos=pos, **kws)
|
||||
|
||||
def filePath(self):
|
||||
"""
|
||||
Return image file
|
||||
"""
|
||||
return self._image_path
|
||||
if self._image_path:
|
||||
renderer = QImageSvgRenderer(image_path)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
def delete(self):
|
||||
# By default center the image
|
||||
if pos is None:
|
||||
x = self.pos().x() - (self.boundingRect().width() / 2)
|
||||
y = self.pos().y() - (self.boundingRect().height() / 2)
|
||||
self.setPos(x, y)
|
||||
|
||||
if svg:
|
||||
svg = self.fromSvg(svg)
|
||||
|
||||
if self._id is None:
|
||||
self.create()
|
||||
|
||||
def fromSvg(self, svg):
|
||||
renderer = QImageSvgRenderer(svg)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
def toSvg(self):
|
||||
"""
|
||||
Deletes this image item.
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
return self.renderer().svg()
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this image item.
|
||||
|
||||
:return: SvgImageItem instance
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
try:
|
||||
Topology.instance().removeImage(self)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Image", "Cannot delete the image: {}".format(str(e)))
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
Paints the contents of an item in local coordinates.
|
||||
|
||||
:param painter: QPainter instance
|
||||
:param option: QStyleOptionGraphicsItem instance
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
|
||||
brect = self.boundingRect()
|
||||
# don't draw anything if the object is too small
|
||||
if brect.width() < 20 or brect.height() < 20:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
super().setZValue(value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
|
||||
image_item = ImageItem(self.renderer(), self._image_path, pos=QtCore.QPointF(self.x() + 20, self.y() + 20))
|
||||
image_item.setZValue(self.zValue())
|
||||
return image_item
|
||||
|
||||
@@ -21,12 +21,14 @@ Base class for shape items (Rectangle, ellipse etc.).
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from .visual_item import VisualItem
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShapeItem:
|
||||
class ShapeItem(VisualItem):
|
||||
|
||||
# Map QT stroke to SVG style
|
||||
QT_DASH_TO_SVG = {
|
||||
@@ -42,29 +44,16 @@ class ShapeItem:
|
||||
"""
|
||||
Base class to draw shapes on the scene.
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
|
||||
def __init__(self, project=None, pos=None, shape_id=None, svg=None, width=200, height=200, z=0, rotation=0, **kws):
|
||||
|
||||
self._id = shape_id
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable)
|
||||
def __init__(self, width=200, height=200, svg=None, **kws):
|
||||
|
||||
super().__init__(svg=svg, **kws)
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._border = 5
|
||||
self._edge = None
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._graphics_view = MainWindow.instance().uiGraphicsView
|
||||
|
||||
self._project = project
|
||||
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
if z:
|
||||
self.setZValue(z)
|
||||
if rotation:
|
||||
self.setRotation(rotation)
|
||||
|
||||
if svg is None:
|
||||
self.setRect(0, 0, width, height)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
@@ -74,67 +63,7 @@ class ShapeItem:
|
||||
else:
|
||||
self.fromSvg(svg)
|
||||
if self._id is None:
|
||||
self._project.post("/shapes", self._createShapeCallback, body=self.__json__())
|
||||
|
||||
def shape_id(self):
|
||||
return self._id
|
||||
|
||||
def _createShapeCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up shape: {}".format(result["message"]))
|
||||
return False
|
||||
self._id = result["shape_id"]
|
||||
|
||||
def updateShape(self):
|
||||
if self._id:
|
||||
self._project.put("/shapes/" + self._id, self.updateShapeCallback, body=self.__json__())
|
||||
|
||||
def updateShapeCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up shape: {}".format(result["message"]))
|
||||
return False
|
||||
self.setPos(QtCore.QPoint(result["x"], result["y"]))
|
||||
self.setZValue(result["z"])
|
||||
self.setRotation(result["rotation"])
|
||||
self.fromSvg(result["svg"])
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
if key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() == 0:
|
||||
self.setRotation(359)
|
||||
else:
|
||||
self.setRotation(self.rotation() - 1)
|
||||
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
else:
|
||||
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
|
||||
self.create()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
@@ -254,19 +183,6 @@ class ShapeItem:
|
||||
if self.zValue() >= 0:
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def delete(self, skip_controller=False):
|
||||
"""
|
||||
Deletes this shape.
|
||||
|
||||
:param skip_controller: Do not replicate change on the controller (usefull when it's already deleted on controller
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeShape(self)
|
||||
if self._id and not skip_controller:
|
||||
self._project.delete("/shapes/" + self._id, None, body=self.__json__())
|
||||
|
||||
def drawLayerInfo(self, painter):
|
||||
"""
|
||||
Draws the layer position.
|
||||
@@ -290,27 +206,6 @@ class ShapeItem:
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsItem.setZValue(self, value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def itemChange(self, change, value):
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if not value:
|
||||
self.updateShape()
|
||||
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
|
||||
|
||||
def _styleSvg(self, element):
|
||||
"""
|
||||
Add style from the shape item to the SVG element that we will
|
||||
@@ -333,22 +228,6 @@ class ShapeItem:
|
||||
element.set("style", style)
|
||||
return element
|
||||
|
||||
def __json__(self):
|
||||
return {
|
||||
"x": int(self.pos().x()),
|
||||
"y": int(self.pos().y()),
|
||||
"z": int(self.zValue()),
|
||||
"rotation": int(self.rotation()),
|
||||
"svg": self.toSvg()
|
||||
}
|
||||
|
||||
def _colorFromSvg(self, value):
|
||||
value = value.strip('#')
|
||||
if len(value) == 6: # If alpha channel is missing
|
||||
value = "ff" + value
|
||||
value = int(value, base=16)
|
||||
return QtGui.QColor.fromRgba(value)
|
||||
|
||||
def fromSvg(self, svg):
|
||||
"""
|
||||
Import element informations from an SVG
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a SVG image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtSvg
|
||||
from .image_item import ImageItem
|
||||
|
||||
|
||||
class SvgImageItem(ImageItem, QtSvg.QGraphicsSvgItem):
|
||||
|
||||
"""
|
||||
Class to insert a SVG image on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, renderer, image_path, pos=None):
|
||||
|
||||
QtSvg.QGraphicsSvgItem.__init__(self)
|
||||
ImageItem.__init__(self, image_path, pos)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this image item.
|
||||
|
||||
:return: SvgImageItem instance
|
||||
"""
|
||||
|
||||
image_item = SvgImageItem(self.renderer(), self._image_path, QtCore.QPointF(self.x() + 20, self.y() + 20))
|
||||
image_item.setZValue(self.zValue())
|
||||
return image_item
|
||||
171
gns3/items/visual_item.py
Normal file
171
gns3/items/visual_item.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
|
||||
import logging
|
||||
import binascii
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VisualItem:
|
||||
|
||||
"""
|
||||
Base class for non emulation item
|
||||
"""
|
||||
|
||||
def __init__(self, project=None, pos=None, shape_id=None, svg=None, z=0, rotation=0, **kws):
|
||||
self._id = shape_id
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._graphics_view = MainWindow.instance().uiGraphicsView
|
||||
|
||||
self._project = project
|
||||
|
||||
# Store a hash of the SVG to avoid him
|
||||
# to be send if he doesn't change
|
||||
self._hash_svg = None
|
||||
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
if z:
|
||||
self.setZValue(z)
|
||||
if rotation:
|
||||
self.setRotation(rotation)
|
||||
|
||||
|
||||
def shape_id(self):
|
||||
return self._id
|
||||
|
||||
def create(self):
|
||||
self._project.post("/shapes", self._createShapeCallback, body=self.__json__())
|
||||
|
||||
def _createShapeCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up shape: {}".format(result["message"]))
|
||||
return False
|
||||
self._id = result["shape_id"]
|
||||
|
||||
def updateShape(self):
|
||||
if self._id:
|
||||
self._project.put("/shapes/" + self._id, self.updateShapeCallback, body=self.__json__())
|
||||
|
||||
def updateShapeCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up shape: {}".format(result["message"]))
|
||||
return False
|
||||
self.setPos(QtCore.QPoint(result["x"], result["y"]))
|
||||
self.setZValue(result["z"])
|
||||
self.setRotation(result["rotation"])
|
||||
if "svg" in result:
|
||||
self.fromSvg(result["svg"])
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
if key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() == 0:
|
||||
self.setRotation(359)
|
||||
else:
|
||||
self.setRotation(self.rotation() - 1)
|
||||
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
else:
|
||||
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
|
||||
|
||||
def _colorFromSvg(self, value):
|
||||
value = value.strip('#')
|
||||
if len(value) == 6: # If alpha channel is missing
|
||||
value = "ff" + value
|
||||
value = int(value, base=16)
|
||||
return QtGui.QColor.fromRgba(value)
|
||||
|
||||
def __json__(self):
|
||||
data = {
|
||||
"x": int(self.pos().x()),
|
||||
"y": int(self.pos().y()),
|
||||
"z": int(self.zValue()),
|
||||
"rotation": int(self.rotation())
|
||||
}
|
||||
svg = self.toSvg()
|
||||
hash_svg = binascii.crc32(svg.encode())
|
||||
print(hash_svg)
|
||||
if hash_svg != self._hash_svg:
|
||||
data["svg"] = svg
|
||||
self._hash_svg = hash_svg
|
||||
return data
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsItem.setZValue(self, value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def delete(self, skip_controller=False):
|
||||
"""
|
||||
Deletes this shape.
|
||||
|
||||
:param skip_controller: Do not replicate change on the controller (usefull when it's already deleted on controller
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeShape(self)
|
||||
if self._id and not skip_controller:
|
||||
self._project.delete("/shapes/" + self._id, None, body=self.__json__())
|
||||
|
||||
def itemChange(self, change, value):
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if not value:
|
||||
self.updateShape()
|
||||
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
|
||||
|
||||
@@ -706,12 +706,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Slot called when inserting an image on the scene.
|
||||
"""
|
||||
|
||||
files_dir = self._project_manager.project().filesDir()
|
||||
if files_dir is None:
|
||||
QtWidgets.QMessageBox.critical(self, "Image", "Please create a node first")
|
||||
return
|
||||
|
||||
# supported image file formats
|
||||
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.gif *.pbm *.pgm *.png *.ppm *.xbm *.xpm);;All files (*.*)"
|
||||
|
||||
@@ -721,34 +715,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._pictures_dir = os.path.dirname(path)
|
||||
|
||||
image = QtGui.QPixmap(path)
|
||||
if image.isNull():
|
||||
QtWidgets.QMessageBox.critical(self, "Image", "Image file format not supported")
|
||||
return
|
||||
|
||||
destination_dir = os.path.join(files_dir, "project-files", "images")
|
||||
try:
|
||||
os.makedirs(destination_dir, exist_ok=True)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Image", "Could not create the image directory: {}".format(e))
|
||||
return
|
||||
|
||||
image_filename = os.path.basename(path)
|
||||
destination_image_path = os.path.join(destination_dir, image_filename)
|
||||
if not os.path.isfile(destination_image_path):
|
||||
# copy the image to the project files directory
|
||||
try:
|
||||
shutil.copyfile(path, destination_image_path)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Image", "Could not copy the image to the project image directory: {}".format(e))
|
||||
return
|
||||
|
||||
renderer = QtSvg.QSvgRenderer(path)
|
||||
if renderer.isValid():
|
||||
# use a SVG image item if this is a valid SVG file
|
||||
image = renderer
|
||||
|
||||
# path to the image is relative to the project-files dir
|
||||
self.uiGraphicsView.addImage(image, os.path.join("images", image_filename))
|
||||
self.uiGraphicsView.addImage(path)
|
||||
|
||||
def _drawRectangleActionSlot(self):
|
||||
"""
|
||||
|
||||
@@ -26,21 +26,33 @@ from . import QtGui
|
||||
class QImageSvgRenderer(QtSvg.QSvgRenderer):
|
||||
"""
|
||||
Renderer pixmap and svg to SVG item
|
||||
|
||||
:param path_or_data: Svg element of path to a SVG
|
||||
"""
|
||||
def __init__(self, path):
|
||||
def __init__(self, path_or_data):
|
||||
super().__init__()
|
||||
super().load(path)
|
||||
if not os.path.exists(path_or_data):
|
||||
self._svg = path_or_data
|
||||
path_or_data = path_or_data.encode("utf-8")
|
||||
super().load(path_or_data)
|
||||
else:
|
||||
super().load(path_or_data)
|
||||
# If we can't render a SVG we load and base64 the image to create a SVG
|
||||
if not self.isValid():
|
||||
image = QtGui.QImage(path_or_data)
|
||||
data = QtCore.QByteArray()
|
||||
buf = QtCore.QBuffer(data)
|
||||
image.save(buf, 'PNG')
|
||||
self._svg = """<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{width}" height="{height}">
|
||||
<image width="{width}" height="{height}" xlink:href="data:image/png;base64,{data}"/>
|
||||
</svg>""".format(data=bytes(data.toBase64()).decode(),
|
||||
width=image.rect().width(),
|
||||
height=image.rect().height())
|
||||
super().load(self._svg.encode())
|
||||
|
||||
# If we can't render a SVG we load and base64 the image to create a SVG
|
||||
if not self.isValid() and os.path.exists(path):
|
||||
image = QtGui.QImage(path)
|
||||
data = QtCore.QByteArray()
|
||||
buf = QtCore.QBuffer(data)
|
||||
image.save(buf, 'PNG')
|
||||
xml = """<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<image width="{width}" height="{height}" xlink:href="data:image/png;base64,{data}"/>
|
||||
</svg>""".format(data=bytes(data.toBase64()).decode(),
|
||||
width=image.rect().width(),
|
||||
height=image.rect().height())
|
||||
super().load(xml.encode())
|
||||
def svg(self):
|
||||
"""
|
||||
:returns: SVG source code
|
||||
"""
|
||||
return self._svg
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
Contains this entire topology: nodes and links.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from .qt import QtCore
|
||||
from .modules import MODULES
|
||||
from .modules.module_error import ModuleError
|
||||
@@ -206,33 +208,6 @@ class Topology(QtCore.QObject):
|
||||
return shape
|
||||
return None
|
||||
|
||||
def addImage(self, image):
|
||||
"""
|
||||
Adds a new image to this topology.
|
||||
|
||||
:param image: ImageItem instance
|
||||
"""
|
||||
|
||||
self._images.append(image)
|
||||
|
||||
def removeImage(self, image):
|
||||
"""
|
||||
Removes an image from this topology.
|
||||
|
||||
:param image: ImageItem instance
|
||||
"""
|
||||
|
||||
if image in self._images:
|
||||
self._images.remove(image)
|
||||
|
||||
if not self._project:
|
||||
return
|
||||
|
||||
# We delete by security only images in the project files directory
|
||||
if not os.path.isabs(image.filePath()):
|
||||
if image.filePath() not in [ image.filePath() for image in self._images ]:
|
||||
os.remove(os.path.join(self._project.filesDir(), "project-files", image.filePath()))
|
||||
|
||||
def nodes(self):
|
||||
"""
|
||||
Returns all the nodes in this topology.
|
||||
@@ -328,10 +303,28 @@ class Topology(QtCore.QObject):
|
||||
self._main_window.uiGraphicsView.addLink(source_node, source_port, destination_node, destination_port, link_id=link_data["link_id"])
|
||||
|
||||
def createShape(self, shape_data):
|
||||
if "ellipse" in shape_data["svg"]:
|
||||
self._main_window.uiGraphicsView.createShapeItem("ellipse", shape_data["x"], shape_data["y"], shape_data["z"], rotation=shape_data["rotation"], shape_id=shape_data["shape_id"], svg=shape_data["svg"])
|
||||
elif "rect" in shape_data["svg"]:
|
||||
self._main_window.uiGraphicsView.createShapeItem("rect", shape_data["x"], shape_data["y"], shape_data["z"], rotation=shape_data["rotation"], shape_id=shape_data["shape_id"], svg=shape_data["svg"])
|
||||
"""
|
||||
Take info from the API and create a shape
|
||||
|
||||
:param shape_data: Dict send by the API
|
||||
"""
|
||||
svg = ET.fromstring(shape_data["svg"])
|
||||
try:
|
||||
#If SVG is more complex we consider it as an image
|
||||
if len(svg[0]) != 0:
|
||||
type = "image"
|
||||
else:
|
||||
tag = svg[0].tag
|
||||
if tag == "ellipse":
|
||||
type = "ellipse"
|
||||
elif tag == "rect":
|
||||
type = "rect"
|
||||
else:
|
||||
type = "image"
|
||||
except IndexError:
|
||||
# If unknow we render it as a raw SVG image
|
||||
type = "image"
|
||||
self._main_window.uiGraphicsView.createVisualItem(type, shape_data["x"], shape_data["y"], shape_data["z"], rotation=shape_data["rotation"], shape_id=shape_data["shape_id"], svg=shape_data["svg"])
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
|
||||
48
tests/items/test_image_item.py
Normal file
48
tests/items/test_image_item.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from gns3.items.image_item import ImageItem
|
||||
from gns3.qt import QtGui, QtCore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def image(project, controller):
|
||||
path = "resources/images/gns3_icon_128x128.png"
|
||||
return ImageItem(image_path=path, project=project)
|
||||
|
||||
|
||||
def test_toSvg(image):
|
||||
svg = ET.fromstring(image.toSvg())
|
||||
assert float(svg.get("width")) == 128.0
|
||||
assert float(svg.get("height")) == 128.0
|
||||
|
||||
|
||||
def test_fromSvg(image, project):
|
||||
image2 = ImageItem(svg=image.toSvg(), project=project)
|
||||
assert image2.toSvg() == image.toSvg()
|
||||
|
||||
|
||||
def test_json(image):
|
||||
image._hash_svg = None
|
||||
assert "svg" in image.__json__()
|
||||
# If we call the function twice and the svg didn't change we don't send the modification
|
||||
assert "svg" not in image.__json__()
|
||||
|
||||
|
||||
@@ -34,16 +34,50 @@ def test_topology_node(vpcs_device):
|
||||
assert len(topology.nodes()) == 0
|
||||
|
||||
|
||||
def test_createShape():
|
||||
def test_createShape_ellipse():
|
||||
topology = Topology()
|
||||
shape_data = {
|
||||
"x": 42,
|
||||
"y": 12,
|
||||
"z": 0,
|
||||
"rotation": 0,
|
||||
"shape_id": str(uuid.uuid4()),
|
||||
"svg": "<svg height=\"105.0\" width=\"158.0\"><ellipse cx=\"79\" cy=\"52\" rx=\"79\" ry=\"53\" style=\"stroke-width:2;stroke:#000000;fill:#ffffff;\" /></svg>",
|
||||
}
|
||||
topology._main_window = MagicMock()
|
||||
topology.createShape(shape_data)
|
||||
topology._main_window.uiGraphicsView.createShapeItem.assert_called_with("ellipse", 42, 12, 0, svg=shape_data["svg"], shape_id=shape_data["shape_id"])
|
||||
topology._main_window.uiGraphicsView.createVisualItem.assert_called_with("ellipse", 42, 12, 0, rotation=0, svg=shape_data["svg"], shape_id=shape_data["shape_id"])
|
||||
|
||||
|
||||
def test_createShape_rect():
|
||||
topology = Topology()
|
||||
shape_data = {
|
||||
"x": 42,
|
||||
"y": 12,
|
||||
"z": 0,
|
||||
"rotation": 0,
|
||||
"shape_id": str(uuid.uuid4()),
|
||||
"svg": "<svg height=\"105.0\" width=\"158.0\"><rect/></svg>",
|
||||
}
|
||||
topology._main_window = MagicMock()
|
||||
topology.createShape(shape_data)
|
||||
topology._main_window.uiGraphicsView.createVisualItem.assert_called_with("rect", 42, 12, 0, rotation=0, svg=shape_data["svg"], shape_id=shape_data["shape_id"])
|
||||
|
||||
|
||||
def test_createShape_svg():
|
||||
"""
|
||||
If SVG is more complex we consider it as an image
|
||||
"""
|
||||
topology = Topology()
|
||||
shape_data = {
|
||||
"x": 42,
|
||||
"y": 12,
|
||||
"z": 0,
|
||||
"rotation": 0,
|
||||
"shape_id": str(uuid.uuid4()),
|
||||
"svg": "<svg height=\"105.0\" width=\"158.0\"><rect><line/></rect></svg>",
|
||||
}
|
||||
topology._main_window = MagicMock()
|
||||
topology.createShape(shape_data)
|
||||
topology._main_window.uiGraphicsView.createVisualItem.assert_called_with("image", 42, 12, 0, rotation=0, svg=shape_data["svg"], shape_id=shape_data["shape_id"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user