def keyPressEvent(self, event): if event.modifiers() & Qt.ControlModifier: key = event.key() if key == Qt.Key_A: self.selectAll() self.repaint() elif key == Qt.Key_Home: self.scrollTo(0) elif key == Qt.Key_End: self.scrollTo(self.dataSize() - self.bytesPerRow()) elif key == Qt.Key_Down: while True: offset = self.verticalScrollBar().value() * self.bytesPerRow() if self.origin != 0: if offset > 0: offset += self.origin offset -= self.bytesPerRow() if offset + 1 < self.dataSize(): self.scrollTo(offset + 1) #return so we don't pass on the key event return elif key == Qt.Key_Up: while True: offset = self.verticalScrollBar().value() * self.bytesPerRow() if self.origin != 0: if offset > 0: offset += self.origin offset -= self.bytesPerRow() if offset > 0: self.scrollTo(offset - 1) #return so we don't pass on the key event return QAbstractScrollArea.keyPressEvent(self, event) return
def keyPressEvent(self, event): if event.modifiers() & Qt.ControlModifier: key = event.key() if key == Qt.Key_A: self.selectAll() self.repaint() elif key == Qt.Key_Home: self.scrollTo(0) elif key == Qt.Key_End: self.scrollTo(self.dataSize() - self.bytesPerRow()) elif key == Qt.Key_Down: while True: offset = self.verticalScrollBar().value() * self.bytesPerRow() if self.origin != 0: if offset > 0: offset += self.origin offset -= self.bytesPerRow() if offset + 1 < self.dataSize(): self.scrollTo(offset + 1) #return so we don't pass on the key event return elif key == Qt.Key_Up: while True: offset = self.verticalScrollBar().value() * self.bytesPerRow() if self.origin != 0: if offset > 0: offset += self.origin offset -= self.bytesPerRow() if offset > 0: self.scrollTo(offset - 1) #return so we don't pass on the key event return QAbstractScrollArea.keyPressEvent(self, event) return
def __init__(self, parent): """ Copies the properties from the parent (WebEngineRenderer) object, creates the required instances of QWebPage, QWebView and QMainWindow and registers some Slots. """ QObject.__init__(self) # Copy properties from parent for key, value in parent.__dict__.items(): setattr(self, key, value) # Determine Proxy settings proxy = QNetworkProxy(QNetworkProxy.NoProxy) if 'http_proxy' in os.environ: proxy_url = QUrl(os.environ['http_proxy']) if proxy_url.scheme().startswith('http'): protocol = QNetworkProxy.HttpProxy else: protocol = QNetworkProxy.Socks5Proxy proxy = QNetworkProxy(protocol, proxy_url.host(), proxy_url.port(), proxy_url.userName(), proxy_url.password()) # Create and connect required PyQt5 objects self._page = CustomWebPage(logger=self.logger, ignore_alert=self.ignoreAlert, ignore_confirm=self.ignoreConfirm, ignore_prompt=self.ignorePrompt, interrupt_js=self.interruptJavaScript) self._qt_proxy = QNetworkProxy() self._qt_proxy.setApplicationProxy(proxy) self._view = QWebEngineView() self._view.setPage(self._page) _window_flags = Qt.WindowFlags() self._window = QMainWindow(flags=_window_flags) self._window.setCentralWidget(self._view) self._qt_network_access_manager = QNetworkAccessManager() # Import QWebSettings for key, value in self.qWebSettings.items(): self._page.settings().setAttribute(key, value) # Connect required event listeners # assert False, "Not finish" self._page.loadFinished.connect(self._on_load_finished) self._page.loadStarted.connect(self._on_load_started) self._qt_network_access_manager.sslErrors.connect(self._on_ssl_errors) self._qt_network_access_manager.finished.connect(self._on_each_reply) # The way we will use this, it seems to be unnecessary to have Scrollbars enabled self._scroll_area = QAbstractScrollArea() self._scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Show this widget self._window.show()
def setFont(self, f): # recalculate all of our metrics/offsets fm = QFontMetrics(f) self.font_width = fm.width('X') self.font_height = fm.height() self.updateScrollbars() # TODO: assert that we are using a fixed font & find out if we care? QAbstractScrollArea.setFont(self, f) return
def setFont(self, f): # recalculate all of our metrics/offsets fm = QFontMetrics(f) self.font_width = fm.width('X') self.font_height = fm.height() self.updateScrollbars() # TODO: assert that we are using a fixed font & find out if we care? QAbstractScrollArea.setFont(self, f) return
def strokeMouse( self, imgView: QAbstractScrollArea, start: Union[QPoint, Iterable[int]], end: Union[QPoint, Iterable[int]], modifier: int = Qt.NoModifier, numSteps: int = 10, ) -> None: """Drag the mouse between 2 points. Args: imgView: View that will receive mouse events. start: Start coordinates, inclusive. end: End coordinates, *also inclusive*. modifier: This modifier will be active when pressing, moving and releasing. numSteps: The number of mouse move events. See Also: :func:`strokeMouseFromCenter`. """ if not isinstance(start, QPoint): start = QPoint(*start) if not isinstance(end, QPoint): end = QPoint(*end) # Note: Due to the implementation of volumina.EventSwitch.eventFilter(), # mouse events intended for the ImageView MUST go through the viewport. # Move to start move = QMouseEvent(QEvent.MouseMove, start, Qt.NoButton, Qt.NoButton, modifier) QApplication.sendEvent(imgView.viewport(), move) # Press left button press = QMouseEvent(QEvent.MouseButtonPress, start, Qt.LeftButton, Qt.NoButton, modifier) QApplication.sendEvent(imgView.viewport(), press) # Move to end in several steps # numSteps = numSteps for i in range(numSteps): nextPoint = start + (end - start) * (old_div(float(i), numSteps)) move = QMouseEvent(QEvent.MouseMove, nextPoint, Qt.NoButton, Qt.NoButton, modifier) QApplication.sendEvent(imgView.viewport(), move) # Move to end move = QMouseEvent(QEvent.MouseMove, end, Qt.NoButton, Qt.NoButton, modifier) QApplication.sendEvent(imgView.viewport(), move) # Release left button release = QMouseEvent(QEvent.MouseButtonRelease, end, Qt.LeftButton, Qt.NoButton, modifier) QApplication.sendEvent(imgView.viewport(), release) # Wait for the gui to catch up QApplication.processEvents() self.waitForViews([imgView])
def strokeMouse( self, imgView: QAbstractScrollArea, startPoint: Union[QPointF, QPoint, Iterable[numbers.Real]], endPoint: Union[QPointF, QPoint, Iterable[numbers.Real]], modifier: int = Qt.NoModifier, numSteps: int = 10, ) -> None: """Drag the mouse between 2 points. Args: imgView: View that will receive mouse events. startPoint: Start coordinates, inclusive. endPoint: End coordinates, also inclusive. modifier: This modifier will be active when pressing, moving and releasing. numSteps: The number of mouse move events. See Also: :func:`strokeMouseFromCenter`. """ startPoint = _asQPointF(startPoint) endPoint = _asQPointF(endPoint) # Note: Due to the implementation of volumina.EventSwitch.eventFilter(), # mouse events intended for the ImageView MUST go through the viewport. # Move to start move = QMouseEvent(QEvent.MouseMove, startPoint, Qt.NoButton, Qt.NoButton, modifier) QApplication.sendEvent(imgView.viewport(), move) # Press left button press = QMouseEvent(QEvent.MouseButtonPress, startPoint, Qt.LeftButton, Qt.NoButton, modifier) QApplication.sendEvent(imgView.viewport(), press) # Move to end in several steps for i in range(1, numSteps + 1): a = i / numSteps nextPoint = (1 - a) * startPoint + a * endPoint move = QMouseEvent(QEvent.MouseMove, nextPoint, Qt.NoButton, Qt.NoButton, modifier) QApplication.sendEvent(imgView.viewport(), move) # Release left button release = QMouseEvent(QEvent.MouseButtonRelease, endPoint, Qt.LeftButton, Qt.NoButton, modifier) QApplication.sendEvent(imgView.viewport(), release) # Wait for the gui to catch up QApplication.processEvents() self.waitForViews([imgView])
def __init__(self, parent): QAbstractScrollArea.__init__(self, parent) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) # tracking works awfully badly under X # turn off until we work out a workaround self.verticalScrollBar().setTracking(False) self.horizontalScrollBar().setTracking(False) self.layers = viewerlayers.LayerManager() self.paintPoint = QPoint() # normally 0,0 unless we are panning # when moving the scroll bars # events get fired that we wish to ignore self.suppressscrollevent = False # set the background color to black so window # is black when nothing loaded and when panning # new areas are initially black. self.setBackgroundColor(Qt.black) # to do with tools self.rubberBand = None self.panCursor = None self.panGrabCursor = None self.zoomInCursor = None self.zoomOutCursor = None self.queryCursor = None self.vectorQueryCursor = None self.polygonCursor = None self.activeTool = VIEWER_TOOL_NONE self.panOrigin = None self.toolPoints = None # for line and polygon tools - list of points self.toolPointsFinished = True # True if we finished collecting # with line and poly tools self.toolPen = QPen() # for drawing the toolPoints self.toolPen.setWidth(1) self.toolPen.setColor(Qt.yellow) self.toolPen.setDashPattern([5, 5, 5, 5]) # Define the scroll wheel behaviour self.mouseWheelZoom = True # do we follow extent when geolinking? self.geolinkFollowExtent = True # to we query all layers or only displayed? self.queryOnlyDisplayed = False
def strokeMouseFromCenter( self, imgView: QAbstractScrollArea, start: Union[QPoint, Iterable[int]], end: Union[QPoint, Iterable[int]], modifier: int = Qt.NoModifier, numSteps: int = 10, ) -> None: """Drag the mouse between 2 points, relative to the view's center. Args: imgView: View that will receive mouse events. start: Start offset from the `imgView` center, inclusive. end: End offset from the `imgView` center, *also inclusive*. modifier: This modifier will be active when pressing, moving and releasing. numSteps: The number of mouse move events. See Also: :func:`strokeMouse`. """ if not isinstance(start, QPoint): start = QPoint(*start) if not isinstance(end, QPoint): end = QPoint(*end) # FIXME: The simpler "center" calculation below breaks tests on CI. # center = imgView.rect().center() center = imgView.rect().bottomRight() / 2 self.strokeMouse(imgView, start + center, end + center, modifier, numSteps)
def mouseMoveEvent(self, event): """ Mouse has been moved while dragging. If in zoom/pan mode we need to do something here. """ QAbstractScrollArea.mouseMoveEvent(self, event) if self.rubberBand is not None and self.rubberBand.isVisible(): # must be doing zoom in/out. extend rect rect = QRect(self.rubberBand.origin, event.pos()).normalized() self.rubberBand.setGeometry(rect) elif self.activeTool == VIEWER_TOOL_PAN: # panning. Work out the offset from where we # starting panning and draw the current image # at an offset pos = event.pos() xamount = pos.x() - self.panOrigin.x() yamount = pos.y() - self.panOrigin.y() self.paintPoint.setX(xamount) self.paintPoint.setY(yamount) # force repaint - self.paintPoint used by paintEvent() self.viewport().update() self.updateScrollBars()
class _WebEngineRendererHelper(QObject): """ This helper class is doing the real work. It is required to allow WebEngineRenderer.render() to be called "asynchronously" (but always from Qt's GUI thread). """ def __init__(self, parent): """ Copies the properties from the parent (WebEngineRenderer) object, creates the required instances of QWebPage, QWebView and QMainWindow and registers some Slots. """ QObject.__init__(self) # Copy properties from parent for key, value in parent.__dict__.items(): setattr(self, key, value) # Determine Proxy settings proxy = QNetworkProxy(QNetworkProxy.NoProxy) if 'http_proxy' in os.environ: proxy_url = QUrl(os.environ['http_proxy']) if proxy_url.scheme().startswith('http'): protocol = QNetworkProxy.HttpProxy else: protocol = QNetworkProxy.Socks5Proxy proxy = QNetworkProxy(protocol, proxy_url.host(), proxy_url.port(), proxy_url.userName(), proxy_url.password()) # Create and connect required PyQt5 objects self._page = CustomWebPage(logger=self.logger, ignore_alert=self.ignoreAlert, ignore_confirm=self.ignoreConfirm, ignore_prompt=self.ignorePrompt, interrupt_js=self.interruptJavaScript) self._qt_proxy = QNetworkProxy() self._qt_proxy.setApplicationProxy(proxy) self._view = QWebEngineView() self._view.setPage(self._page) _window_flags = Qt.WindowFlags() self._window = QMainWindow(flags=_window_flags) self._window.setCentralWidget(self._view) self._qt_network_access_manager = QNetworkAccessManager() # Import QWebSettings for key, value in self.qWebSettings.items(): self._page.settings().setAttribute(key, value) # Connect required event listeners # assert False, "Not finish" self._page.loadFinished.connect(self._on_load_finished) self._page.loadStarted.connect(self._on_load_started) self._qt_network_access_manager.sslErrors.connect(self._on_ssl_errors) self._qt_network_access_manager.finished.connect(self._on_each_reply) # The way we will use this, it seems to be unnecessary to have Scrollbars enabled self._scroll_area = QAbstractScrollArea() self._scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Show this widget self._window.show() def __del__(self): """ Clean up Qt5 objects. """ self._window.close() del self._window del self._view del self._page def render(self, res): """ The real worker. Loads the page (_load_page) and awaits the end of the given 'delay'. While it is waiting outstanding QApplication events are processed. After the given delay, the Window or Widget (depends on the value of 'grabWholeWindow' is drawn into a QScreen and post processed (_post_process_image). """ self._load_page(res, self.width, self.height, self.timeout) # Wait for end of timer. In this time, process # other outstanding Qt events. if self.wait > 0: if self.logger: self.logger.debug("Waiting {} seconds ".format(self.wait)) wait_to_time = time.time() + self.wait while time.time() < wait_to_time: if self.app.hasPendingEvents(): self.app.processEvents() if self.renderTransparentBackground: # Another possible drawing solution image = QImage(self._page.viewportSize(), QImage.Format_ARGB32) image.fill(QColor(255, 0, 0, 0).rgba()) # http://ariya.blogspot.com/2009/04/transparent-qwebview-and-qwebpage.html palette = self._view.palette() palette.setBrush(QPalette.Base, Qt.transparent) self._page.setPalette(palette) self._view.setAttribute(Qt.WA_OpaquePaintEvent, False) painter = QPainter(image) painter.setBackgroundMode(Qt.TransparentMode) self._window.render(painter) painter.end() else: if self.grabWholeWindow: # Note that this does not fully ensure that the # window still has the focus when the screen is # grabbed. This might result in a race condition. self._view.activateWindow() qt_screen = self.app.primaryScreen() image = qt_screen.grabWindow(sip.voidptr(0)) else: image = self._window.grab() return self._post_process_image(image) def _load_page(self, res, width, height, timeout): """ This method implements the logic for retrieving and displaying the requested page. """ # This is an event-based application. So we have to wait until # "loadFinished(bool)" raised. cancel_at = time.time() + timeout self.__loading = True self.__loadingResult = False # Default # When "res" is of type tuple, it has two elements where the first # element is the HTML code to render and the second element is a string # setting the base URL for the interpreted HTML code. # When resource is of type str or unicode, it is handled as URL which # shall be loaded if type(res) == tuple: url = res[1] else: url = res if self.encodedUrl: qt_url = QUrl().fromEncoded(url) else: qt_url = QUrl(url) # Set the required cookies, if any self.cookieJar = CookieJar(self.cookies, qt_url) self._qt_network_access_manager.setCookieJar(self.cookieJar) # Load the page if type(res) == tuple: self._page.setHtml(res[0], qt_url) # HTML, baseUrl else: self._page.load(qt_url) while self.__loading: if timeout > 0 and time.time() >= cancel_at: raise RuntimeError("Request timed out on {}".format(res)) while self.app.hasPendingEvents() and self.__loading: self.app.processEvents() if self.logger: self.logger.debug("Processing result") if not self.__loading_result: if self.logger: self.logger.warning("Failed to load {}".format(res)) # Set initial viewport (the size of the "window") size = self._page.contentsSize() if self.logger: self.logger.debug("contentsSize: %s", size) if width > 0: size.setWidth(width) if height > 0: size.setHeight(height) self._window.resize(size.toSize()) def _post_process_image(self, q_image): """ If 'scaleToWidth' or 'scaleToHeight' are set to a value greater than zero this method will scale the image using the method defined in 'scaleRatio'. """ if self.scaleToWidth > 0 or self.scaleToHeight > 0: # Scale this image if self.scaleRatio == 'keep': ratio = Qt.KeepAspectRatio elif self.scaleRatio in ['expand', 'crop']: ratio = Qt.KeepAspectRatioByExpanding else: # 'ignore' ratio = Qt.IgnoreAspectRatio q_image = q_image.scaled(self.scaleToWidth, self.scaleToHeight, ratio, Qt.SmoothTransformation) if self.scaleRatio == 'crop': q_image = q_image.copy(0, 0, self.scaleToWidth, self.scaleToHeight) return q_image @pyqtSlot(QNetworkReply, name='finished') def _on_each_reply(self, reply): """ Logs each requested uri """ self.logger.debug("Received {}".format(reply.url().toString())) # Event for "loadStarted()" signal @pyqtSlot(name='loadStarted') def _on_load_started(self): """ Slot that sets the '__loading' property to true """ if self.logger: self.logger.debug("loading started") self.__loading = True # Event for "loadFinished(bool)" signal @pyqtSlot(bool, name='loadFinished') def _on_load_finished(self, result): """ Slot that sets the '__loading' property to false and stores the result code in '__loading_result'. """ if self.logger: self.logger.debug("loading finished with result %s", result) self.__loading = False self.__loading_result = result # Event for "sslErrors(QNetworkReply *,const QList<QSslError>&)" signal @pyqtSlot('QNetworkReply*', 'QList<QSslError>', name='sslErrors') def _on_ssl_errors(self, reply, errors): """ Slot that writes SSL warnings into the log but ignores them. """ for e in errors: if self.logger: self.logger.warn("SSL: " + e.errorString()) reply.ignoreSslErrors() @property def window(self): return self._window
def mouseReleaseEvent(self, event): """ Mouse has been released, if we are in zoom/pan mode we do stuff here. """ QAbstractScrollArea.mouseReleaseEvent(self, event) if self.rubberBand is not None and self.rubberBand.isVisible(): # get the information about the rect they have drawn # note this is on self, rather than viewport() selection = self.rubberBand.geometry() geom = self.viewport().geometry() selectionsize = float(selection.width() * selection.height()) geomsize = float(geom.width() * geom.height()) self.rubberBand.hide() layer = self.layers.getTopRasterLayer() if layer is not None: if selectionsize < MIN_SELECTION_SIZE_PX: # this is practically a '0' size selection on a 4K screen and # an improbably small selection on an HD screen so assume user # has just clicked the image and set fraction to 0.5 fraction = 0.5 else: fraction = numpy.sqrt(selectionsize / geomsize) if self.activeTool == VIEWER_TOOL_ZOOMIN: if selectionsize < MIN_SELECTION_SIZE_PX: # user 'just clicked' (see if statement above). # NOTE: this fixes issues with 0 or negative dimensions # inside else: below that used ot hard-crash tuiview wldX, wldY = layer.coordmgr.display2world( selection.left(), selection.top()) layer.coordmgr.setZoomFactor( layer.coordmgr.imgPixPerWinPix * fraction) layer.coordmgr.setWorldCenter(wldX, wldY) # not sure why we need this but get black strips # around otherwise layer.coordmgr.recalcBottomRight() else: # I don't think anything needs to be added here dspTop = selection.top() dspLeft = selection.left() dspBottom = selection.bottom() dspRight = selection.right() (rastLeft, rastTop) = layer.coordmgr.display2pixel( dspLeft, dspTop) (rastRight, rastBottom) = layer.coordmgr.display2pixel( dspRight, dspBottom) #print layer.coordmgr layer.coordmgr.setTopLeftPixel(rastLeft, rastTop) layer.coordmgr.calcZoomFactor(rastRight, rastBottom) # not sure why we need this but get black strips # around otherwise layer.coordmgr.recalcBottomRight() #print layer.coordmgr elif self.activeTool == VIEWER_TOOL_ZOOMOUT: # the smaller the area the larger the zoom center = selection.center() wldX, wldY = layer.coordmgr.display2world( center.x(), center.y()) layer.coordmgr.setZoomFactor( layer.coordmgr.imgPixPerWinPix / fraction) layer.coordmgr.setWorldCenter(wldX, wldY) # not sure why we need this but get black strips # around otherwise layer.coordmgr.recalcBottomRight() # redraw self.layers.makeLayersConsistent(layer) self.layers.updateImages() self.viewport().update() self.updateScrollBars() # geolink self.emitGeolinkMoved() elif self.activeTool == VIEWER_TOOL_PAN: # change cursor back self.viewport().setCursor(self.panCursor) layer = self.layers.getTopRasterLayer() if layer is not None: # stop panning and move viewport dspXmove = -self.paintPoint.x() dspYmove = -self.paintPoint.y() (pixNewX, pixNewY) = layer.coordmgr.display2pixel(dspXmove, dspYmove) #print 'panning' #print layer.coordmgr layer.coordmgr.setTopLeftPixel(pixNewX, pixNewY) layer.coordmgr.recalcBottomRight() #print layer.coordmgr # reset self.paintPoint.setX(0) self.paintPoint.setY(0) # redraw self.layers.makeLayersConsistent(layer) self.layers.updateImages() self.viewport().update() self.updateScrollBars() # geolink self.emitGeolinkMoved()
def mousePressEvent(self, event): """ Mouse has been clicked down if we are in zoom/pan mode we need to start doing stuff here """ QAbstractScrollArea.mousePressEvent(self, event) pos = event.pos() if (self.activeTool == VIEWER_TOOL_ZOOMIN or self.activeTool == VIEWER_TOOL_ZOOMOUT): if self.rubberBand is None: self.rubberBand = QRubberBand(QRubberBand.Rectangle, self) self.rubberBand.setGeometry(QRect(pos, QSize())) self.rubberBand.show() self.rubberBand.origin = pos elif self.activeTool == VIEWER_TOOL_PAN: # remember pos self.panOrigin = pos # change cursor self.viewport().setCursor(self.panGrabCursor) elif self.activeTool == VIEWER_TOOL_QUERY: modifiers = event.modifiers() (dspX, dspY) = (pos.x(), pos.y()) self.newQueryPoint(dspX=dspX, dspY=dspY, modifiers=modifiers) elif self.activeTool == VIEWER_TOOL_VECTORQUERY: modifiers = event.modifiers() (dspX, dspY) = (pos.x(), pos.y()) self.newVectorQueryPoint(dspX, dspY, modifiers=modifiers) elif self.activeTool == VIEWER_TOOL_POLYGON: button = event.button() if button == Qt.LeftButton: # adding points if self.toolPoints is None or self.toolPointsFinished: # first point - starts and ends at same pos self.toolPoints = [pos, pos] self.toolPointsFinished = False else: # last point same as first - insert before last self.toolPoints.insert(-1, pos) elif button == Qt.MiddleButton and self.toolPoints is not None: # delete last point if len(self.toolPoints) > 2: del self.toolPoints[-2] elif button == Qt.RightButton and self.toolPoints is not None: # finished # create object for signal layer = self.layers.getTopRasterLayer() modifiers = event.modifiers() obj = PolygonToolInfo(self.toolPoints, layer, modifiers) self.polygonCollected.emit(obj) self.toolPointsFinished = True # done, but still display # redraw so paint() gets called self.viewport().update() elif self.activeTool == VIEWER_TOOL_POLYLINE: button = event.button() if button == Qt.LeftButton: # adding points if self.toolPoints is None or self.toolPointsFinished: # first point self.toolPoints = [pos] self.toolPointsFinished = False else: # add to list self.toolPoints.append(pos) elif button == Qt.MiddleButton and self.toolPoints is not None: # delete last point if len(self.toolPoints) > 1: self.toolPoints.pop() elif button == Qt.RightButton and self.toolPoints is not None: # finished # create object for signal if self.queryOnlyDisplayed: layer = self.layers.getTopDisplayedRasterLayer() else: layer = self.layers.getTopRasterLayer() modifiers = event.modifiers() obj = PolylineToolInfo(self.toolPoints, layer, modifiers) self.polylineCollected.emit(obj) self.toolPointsFinished = True # done, but still display # redraw so paint() gets called self.viewport().update()