class XStream(QObject):
    """Stream object to imitate Python output."""

    _stdout: Optional['XStream'] = None
    _stderr: Optional['XStream'] = None
    messageWritten = pyqtSignal(str)

    def write(self, msg: str):
        """Output the message."""
        if not self.signalsBlocked():
            self.messageWritten.emit(msg)

    @staticmethod
    def stdout() -> 'XStream':
        """Replace stdout."""
        if not XStream._stdout:
            XStream._stdout = XStream()
            sys.stdout = XStream._stdout
        return XStream._stdout

    @staticmethod
    def stderr() -> 'XStream':
        """Replace stderr."""
        if not XStream._stderr:
            XStream._stderr = XStream()
            sys.stderr = XStream._stderr
        return XStream._stderr

    @staticmethod
    def back():
        """Disconnect from Qt widget."""
        sys.stdout = _SYS_STDOUT
        sys.stderr = _SYS_STDERR
        XStream._stdout = None
        XStream._stderr = None
Exemple #2
0
class BaseTableWidget(QTableWidget):
    """Two tables has some shared function."""

    deleteRequest = pyqtSignal()

    def __init__(self,
                 RowCount: int,
                 HorizontalHeaderItems: Tuple[str],
                 parent=None):
        super(BaseTableWidget, self).__init__(parent)
        self.setSizePolicy(
            QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
        self.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.setRowCount(RowCount)
        self.setColumnCount(len(HorizontalHeaderItems) + 1)
        for i, e in enumerate(('Name', ) + HorizontalHeaderItems):
            self.setHorizontalHeaderItem(i, QTableWidgetItem(e))

    def rowTexts(self, row: int, *, hasName: bool = False) -> List[str]:
        """Get the whole row of texts.
        
        + Edit point: hasName = False
        + Edit link: hasName = True
        """
        texts = []
        for column in self.effectiveRange(hasName):
            item = self.item(row, column)
            if item is None:
                texts.append('')
            else:
                texts.append(item.text())
        return texts

    def dataTuple(self) -> Tuple[TypeVar("Element", VPoint, VLink)]:
        """Return data set as a container."""
        return tuple(self.data())

    def selectedRows(self) -> List[int]:
        """Get what row is been selected."""
        tmp_set = set([])
        for r in self.selectedRanges():
            tmp_set.update([i for i in range(r.topRow(), r.bottomRow() + 1)])
        return sorted(tmp_set)

    def keyPressEvent(self, event):
        """Hit the delete key,
        will emit delete signal from this table.
        """
        if event.key() == Qt.Key_Delete:
            self.deleteRequest.emit()

    def clear(self):
        """Overrided the clear function, just removed all items."""
        for row in range(self.rowCount()):
            self.removeRow(0)
Exemple #3
0
class _PreviewWindow(PreviewCanvas):
    """Customized preview window has some functions of mouse interaction.

    Emit signal call to change current point when pressed a dot.
    """

    set_joint_number = pyqtSignal(int)

    def __init__(self, get_solutions: Callable[[], str], parent: QWidget):
        """Add a function use to get current point from parent."""
        super(_PreviewWindow, self).__init__(get_solutions, parent)
        self.pressed = False
        self.get_joint_number = parent.joint_name.currentIndex

    def mousePressEvent(self, event):
        """Check if get close to a joint."""
        mx = (event.x() - self.ox) / self.zoom
        my = (event.y() - self.oy) / -self.zoom
        for node, (x, y) in self.pos.items():
            if node in self.same:
                continue
            if hypot(x - mx, y - my) <= 5:
                self.set_joint_number.emit(node)
                self.pressed = True
                break

    def mouseReleaseEvent(self, event):
        """Cancel the drag."""
        self.pressed = False

    def mouseMoveEvent(self, event):
        """Drag to move the joint."""
        if not self.pressed:
            return
        row = self.get_joint_number()
        if not row > -1:
            return
        mx = (event.x() - self.ox) / self.zoom
        my = (event.y() - self.oy) / -self.zoom
        if -120 <= mx <= 120:
            self.pos[row] = (mx, self.pos[row][1])
        else:
            self.pos[row] = (120 if -120 <= mx else -120, self.pos[row][1])
        if -120 <= my <= 120:
            self.pos[row] = (self.pos[row][0], my)
        else:
            self.pos[row] = (self.pos[row][0], 120 if -120 <= my else -120)
        self.update()
Exemple #4
0
class LoadCommitButton(QPushButton):
    """The button of load commit."""

    loaded = pyqtSignal(int)

    def __init__(self, id, parent=None):
        super(LoadCommitButton,
              self).__init__(QIcon(QPixmap(":icons/dataupdate.png")),
                             " #{}".format(id), parent)
        self.setToolTip("Reset to commit #{}.".format(id))
        self.id = id

    def mouseReleaseEvent(self, event):
        """Load the commit when release button."""
        super(LoadCommitButton, self).mouseReleaseEvent(event)
        self.loaded.emit(self.id)

    def isLoaded(self, id: int):
        """Set enable if this commit is been loaded."""
        self.setEnabled(id != self.id)
Exemple #5
0
class LoadCommitButton(QPushButton):
    """The button of load commit."""

    loaded = pyqtSignal(int)

    def __init__(self, id_int: int, parent: QWidget):
        super(LoadCommitButton,
              self).__init__(QIcon(QPixmap(":icons/data_update.png")),
                             f" # {id_int}", parent)
        self.setToolTip(f"Reset to commit # {id_int}.")
        self.id = id_int

    def mouseReleaseEvent(self, event):
        """Load the commit when release button."""
        super(LoadCommitButton, self).mouseReleaseEvent(event)
        self.loaded.emit(self.id)

    def set_loaded(self, id_int: int):
        """Set enable if this commit is been loaded."""
        self.setEnabled(id_int != self.id)
class DynamicCanvas(BaseCanvas):
    """The canvas in main window.
    
    + Parse and show PMKS expression.
    + Show paths.
    + Show settings of dimensional synthesis widget.
    + Mouse interactions.
    + Zoom to fit function.
    """

    mouse_track = pyqtSignal(float, float)
    mouse_browse_track = pyqtSignal(float, float)
    mouse_getSelection = pyqtSignal(tuple, bool)
    mouse_freemoveSelection = pyqtSignal(tuple)
    mouse_noSelection = pyqtSignal()
    mouse_getAltAdd = pyqtSignal()
    mouse_getDoubleClickEdit = pyqtSignal(int)
    zoom_change = pyqtSignal(int)

    def __init__(self, parent=None):
        super(DynamicCanvas, self).__init__(parent)
        self.setMouseTracking(True)
        self.setStatusTip("Use mouse wheel or middle button to look around.")
        #Functions from the main window.
        self.getTriangle = parent.getTriangle
        self.rightInput = parent.rightInput
        self.pathInterval = parent.pathInterval
        #The current mouse coordinates.
        self.Selector = Selector()
        #Entities.
        self.Points = tuple()
        self.Links = tuple()
        #Point selection.
        self.selectionRadius = 10
        self.pointsSelection = ()
        #Linkage transparency.
        self.transparency = 1.
        #Path solving range.
        self.ranges = {}
        #Set showDimension to False.
        self.showDimension = False
        #Free move mode.
        self.freemove = FreeMode.NoFreeMove
        #Auto preview function.
        self.autoPathShow = True
        #Show solution.
        self.solutionShow = False
        #Dependent functions to set zoom bar.
        self.__setZoom = parent.ZoomBar.setValue
        self.__zoom = parent.ZoomBar.value
        self.__zoomFactor = parent.scalefactor_option.value
        #Default margin factor.
        self.marginFactor = 0.95
        #Widget size.
        self.width_old = None
        self.height_old = None

    def updateFigure(self, Points: Tuple[VPoint], Links: Tuple[VLink],
                     path: List[Tuple[float, float]]):
        """Update with Point and Links data."""
        self.Points = Points
        self.Links = Links
        self.Path.path = path
        self.update()

    @pyqtSlot(int)
    def setLinkWidth(self, linkWidth: int):
        """Update width of linkages."""
        self.linkWidth = linkWidth
        self.update()

    @pyqtSlot(int)
    def setPathWidth(self, pathWidth: int):
        """Update width of linkages."""
        self.pathWidth = pathWidth
        self.update()

    @pyqtSlot(bool)
    def setPointMark(self, showPointMark: bool):
        """Update show point mark or not."""
        self.showPointMark = showPointMark
        self.update()

    @pyqtSlot(bool)
    def setShowDimension(self, showDimension: bool):
        """Update show dimension or not."""
        self.showDimension = showDimension
        self.update()

    @pyqtSlot(bool)
    def setCurveMode(self, curve: bool):
        """Update show as curve mode or not."""
        self.Path.curve = curve
        self.update()

    @pyqtSlot(int)
    def setFontSize(self, fontSize: int):
        """Update font size."""
        self.fontSize = fontSize
        self.update()

    @pyqtSlot(int)
    def setZoom(self, zoom: int):
        """Update zoom factor."""
        self.zoom = zoom / 100 * self.rate
        self.update()

    def setShowTargetPath(self, showTargetPath: bool):
        """Update show target path or not."""
        self.showTargetPath = showTargetPath
        self.update()

    def setFreeMove(self, freemove: int):
        """Update freemove mode number."""
        self.freemove = FreeMode(freemove)
        self.update()

    @pyqtSlot(int)
    def setSelectionRadius(self, selectionRadius: int):
        """Update radius of point selector."""
        self.selectionRadius = selectionRadius

    @pyqtSlot(int)
    def setTransparency(self, transparency: int):
        """Update transparency.
        
        0%: opaque.
        """
        self.transparency = (100 - transparency) / 100
        self.update()

    @pyqtSlot(int)
    def setMarginFactor(self, marginFactor: int):
        """Update margin factor when zoom to fit."""
        self.marginFactor = 1 - marginFactor / 100
        self.update()

    @pyqtSlot(int)
    def setJointSize(self, jointsize: int):
        """Update size for each joint."""
        self.jointsize = jointsize
        self.update()

    @pyqtSlot(list)
    def changePointsSelection(self, pointsSelection: List[int]):
        """Update the selected points."""
        self.pointsSelection = pointsSelection
        self.update()

    def setSolvingPath(self, targetPath: Dict[str, Tuple[Tuple[float,
                                                               float]]]):
        """Update target path."""
        self.targetPath = targetPath
        self.update()

    def setPathShow(self, p: int):
        """Update path present mode.
        
        -2: Hide all paths.
        -1: Show all paths.
        i: Show path i.
        """
        self.Path.show = p
        self.update()

    def setAutoPath(self, autoPathShow: bool):
        """Enable auto preview function."""
        self.autoPathShow = autoPathShow
        self.update()

    def setSolutionShow(self, solutionShow: bool):
        self.solutionShow = solutionShow
        self.update()

    def updateRanges(self, ranges: Dict[str, Tuple[float, float, float]]):
        """Update the ranges of dimensional synthesis."""
        self.ranges.clear()
        self.ranges.update({
            tag: QRectF(
                QPointF(values[0] - values[2] / 2, values[1] + values[2] / 2),
                QSizeF(values[2], values[2]))
            for tag, values in ranges.items()
        })
        self.update()

    def paintEvent(self, event):
        """Drawing functions."""
        width = self.width()
        height = self.height()
        if ((self.width_old is not None) and ((self.width_old != width) or
                                              (self.height_old != height))):
            self.ox += (width - self.width_old) / 2
            self.oy += (height - self.height_old) / 2
        super(DynamicCanvas, self).paintEvent(event)
        self.painter.setFont(QFont('Arial', self.fontSize))
        if self.freemove != FreeMode.NoFreeMove:
            #Draw a colored frame for free move mode.
            pen = QPen()
            if self.freemove == FreeMode.Translate:
                pen.setColor(QColor(161, 105, 229))
            elif self.freemove == FreeMode.Rotate:
                pen.setColor(QColor(219, 162, 6))
            elif self.freemove == FreeMode.Reflect:
                pen.setColor(QColor(79, 249, 193))
            pen.setWidth(8)
            self.painter.setPen(pen)
            self.__drawFrame()
        #Draw links except ground.
        for vlink in self.Links[1:]:
            self.__drawLink(vlink)
        #Draw path.
        if self.Path.show != -2:
            self.__drawPath()
        #Draw solving path.
        if self.showTargetPath:
            self.__drawSlvsRanges()
            self._BaseCanvas__drawTargetPath()
        #Draw points.
        for i, vpoint in enumerate(self.Points):
            self.__drawPoint(i, vpoint)
        #Rectangular selection
        if self.Selector.RectangularSelection:
            pen = QPen(Qt.gray)
            pen.setWidth(1)
            self.painter.setPen(pen)
            self.painter.drawRect(
                QRectF(QPointF(self.Selector.x, self.Selector.y),
                       QPointF(self.Selector.sx, self.Selector.sy)))
        self.painter.end()
        #Record the widget size.
        self.width_old = width
        self.height_old = height

    def __drawFrame(self):
        """Draw a outer frame."""
        positive_x = self.width() - self.ox
        positive_y = -self.oy
        negative_x = -self.ox
        negative_y = self.height() - self.oy
        self.painter.drawLine(QPointF(negative_x, positive_y),
                              QPointF(positive_x, positive_y))
        self.painter.drawLine(QPointF(negative_x, negative_y),
                              QPointF(positive_x, negative_y))
        self.painter.drawLine(QPointF(negative_x, positive_y),
                              QPointF(negative_x, negative_y))
        self.painter.drawLine(QPointF(positive_x, positive_y),
                              QPointF(positive_x, negative_y))

    def __drawPoint(self, i: int, vpoint: VPoint):
        """Draw a point."""
        if vpoint.type == 1 or vpoint.type == 2:
            #Draw slider
            silder_points = vpoint.c
            for j, (cx, cy) in enumerate(silder_points):
                if vpoint.type == 1:
                    if j == 0:
                        self._BaseCanvas__drawPoint(
                            i, cx, cy, vpoint.links[j] == 'ground',
                            vpoint.color)
                    else:
                        pen = QPen(vpoint.color)
                        pen.setWidth(2)
                        self.painter.setPen(pen)
                        r = 5
                        self.painter.drawRect(
                            QRectF(
                                QPointF(cx * self.zoom + r,
                                        cy * -self.zoom + r),
                                QPointF(cx * self.zoom - r,
                                        cy * -self.zoom - r)))
                elif vpoint.type == 2:
                    if j == 0:
                        self._BaseCanvas__drawPoint(
                            i, cx, cy, vpoint.links[j] == 'ground',
                            vpoint.color)
                    else:
                        #Turn off point mark.
                        showPointMark = self.showPointMark
                        self.showPointMark = False
                        self._BaseCanvas__drawPoint(
                            i, cx, cy, vpoint.links[j] == 'ground',
                            vpoint.color)
                        self.showPointMark = showPointMark
            pen = QPen(vpoint.color.darker())
            pen.setWidth(2)
            self.painter.setPen(pen)
            x_all = tuple(cx for cx, cy in silder_points)
            if x_all:
                p_left = silder_points[x_all.index(min(x_all))]
                p_right = silder_points[x_all.index(max(x_all))]
                if p_left == p_right:
                    y_all = tuple(cy for cx, cy in silder_points)
                    p_left = silder_points[y_all.index(min(y_all))]
                    p_right = silder_points[y_all.index(max(y_all))]
                self.painter.drawLine(
                    QPointF(p_left[0] * self.zoom, p_left[1] * -self.zoom),
                    QPointF(p_right[0] * self.zoom, p_right[1] * -self.zoom))
        else:
            self._BaseCanvas__drawPoint(i, vpoint.cx, vpoint.cy,
                                        vpoint.grounded(), vpoint.color)
        #For selects function.
        if i in self.pointsSelection:
            pen = QPen(QColor(161, 16, 239))
            pen.setWidth(3)
            self.painter.setPen(pen)
            self.painter.drawRect(vpoint.cx * self.zoom - 12,
                                  vpoint.cy * -self.zoom - 12, 24, 24)

    def __drawLink(self, vlink: VLink):
        """Draw a link."""
        points = []
        for i in vlink.points:
            vpoint = self.Points[i]
            if vpoint.type == 1 or vpoint.type == 2:
                coordinate = vpoint.c[0 if
                                      (vlink.name == vpoint.links[0]) else 1]
                x = coordinate[0] * self.zoom
                y = coordinate[1] * -self.zoom
            else:
                x = vpoint.cx * self.zoom
                y = vpoint.cy * -self.zoom
            points.append((x, y))
        pen = QPen(vlink.color)
        pen.setWidth(self.linkWidth)
        self.painter.setPen(pen)
        brush = QColor(226, 219, 190)
        brush.setAlphaF(self.transparency)
        self.painter.setBrush(brush)
        #Rearrange: Put the nearest point to the next position.
        qpoints = convex_hull(points)
        if qpoints:
            self.painter.drawPolygon(*qpoints)
        self.painter.setBrush(Qt.NoBrush)
        if ((not self.showPointMark) or (vlink.name == 'ground')
                or (not qpoints)):
            return
        pen.setColor(Qt.darkGray)
        self.painter.setPen(pen)
        cenX = sum(p[0] for p in points) / len(points)
        cenY = sum(p[1] for p in points) / len(points)
        self.painter.drawText(QPointF(cenX, cenY), '[{}]'.format(vlink.name))

    def __drawPath(self):
        """Draw paths. Recording first."""
        pen = QPen()
        if self.autoPathShow and self.rightInput():
            """Replace to auto preview path."""
            exprs = self.getTriangle(self.Points)
            self.Path.path = expr_path(
                exprs, {n: 'P{}'.format(n)
                        for n in range(len(self.Points))}, self.Points,
                self.pathInterval())
            if self.solutionShow:
                for expr in exprs:
                    self._BaseCanvas__drawSolution(expr[0], expr[1:-1],
                                                   expr[-1], self.Points)
        if hasattr(self, 'path_record'):
            paths = self.path_record
        else:
            paths = self.Path.path
        for i, path in enumerate(paths):
            if ((self.Path.show != i) and (self.Path.show != -1)
                    or (len(path) <= 1)):
                continue
            try:
                color = self.Points[i].color
            except:
                color = colorQt('Green')
            pen.setColor(color)
            pen.setWidth(self.pathWidth)
            self.painter.setPen(pen)
            if self.Path.curve:
                self._BaseCanvas__drawCurve(path)
            else:
                self._BaseCanvas__drawDot(path)

    def __drawSlvsRanges(self):
        """Draw solving range."""
        pen = QPen()
        self.painter.setFont(QFont("Arial", self.fontSize + 5))
        pen.setWidth(5)
        for i, (tag, rect) in enumerate(self.ranges.items()):
            range_color = QColor(colorNum(i + 1))
            range_color.setAlpha(30)
            self.painter.setBrush(range_color)
            range_color.setAlpha(255)
            pen.setColor(range_color)
            self.painter.setPen(pen)
            cx = rect.x() * self.zoom
            cy = rect.y() * -self.zoom
            if rect.width():
                self.painter.drawRect(
                    QRectF(cx, cy,
                           rect.width() * self.zoom,
                           rect.height() * self.zoom))
            else:
                self.painter.drawEllipse(QPointF(cx, cy), 3, 3)
            range_color.setAlpha(255)
            pen.setColor(range_color)
            self.painter.setPen(pen)
            self.painter.drawText(QPointF(cx + 6, cy - 6), tag)
            self.painter.setBrush(Qt.NoBrush)

    def recordStart(self, limit: int):
        """Start a limit from main window."""
        self.path_record = [deque([], limit) for i in range(len(self.Points))]

    def recordPath(self):
        """Recording path."""
        for i, vpoint in enumerate(self.Points):
            self.path_record[i].append((vpoint.cx, vpoint.cy))

    def getRecordPath(self):
        """Return paths."""
        path = tuple(
            tuple(path) if (len(set(path)) > 1) else ()
            for path in self.path_record)
        del self.path_record
        return path

    def wheelEvent(self, event):
        """Set zoom bar value by mouse wheel."""
        value = event.angleDelta().y()
        self.__setZoom(self.__zoom() +
                       (self.__zoomFactor() * value) / abs(value))

    def mousePressEvent(self, event):
        """Press event.
        
        Middle button: Move canvas of view.
        Left button: Select the point(s).
        """
        self.Selector.x = event.x() - self.ox
        self.Selector.y = event.y() - self.oy
        if event.buttons() == Qt.MiddleButton:
            self.Selector.MiddleButtonDrag = True
            x = self.Selector.x / self.zoom
            y = self.Selector.y / -self.zoom
            self.mouse_browse_track.emit(x, y)
        if event.buttons() == Qt.LeftButton:
            self.Selector.LeftButtonDrag = True
            self.__mouseSelectedPoint()
            if self.Selector.selection:
                self.mouse_getSelection.emit(tuple(self.Selector.selection),
                                             True)

    def mouseDoubleClickEvent(self, event):
        """Mouse double click.
        
        + Middle button: Zoom to fit.
        + Left button: Edit point function.
        """
        button = event.button()
        if button == Qt.MidButton:
            self.zoomToFit()
        if (button
                == Qt.LeftButton) and (self.freemove != FreeMode.NoFreeMove):
            self.Selector.x = event.x() - self.ox
            self.Selector.y = event.y() - self.oy
            self.__mouseSelectedPoint()
            if self.Selector.selection:
                self.mouse_getSelection.emit((self.Selector.selection[0], ),
                                             True)
                self.mouse_getDoubleClickEdit.emit(self.Selector.selection[0])

    def __mouseSelectedPoint(self):
        """Select one point."""
        self.__selectedPointFunc(
            self.Selector.selection,
            lambda x, y: self.Selector.distance(x, y) < self.selectionRadius)

    def __rectangularSelectedPoint(self):
        """Select points by rectangle."""
        self.__selectedPointFunc(self.Selector.selection_rect,
                                 self.Selector.inRect)

    def __selectedPointFunc(self, selection: List[int],
                            inSelection: Callable[[float, float], bool]):
        """Select point(s) function."""
        selection.clear()
        for i, vpoint in enumerate(self.Points):
            if inSelection(vpoint.cx * self.zoom, vpoint.cy * -self.zoom):
                if i not in selection:
                    selection.append(i)

    def mouseReleaseEvent(self, event):
        """Release mouse button.
        
        + Alt & Left button: Add a point.
        + Left button: Select a point.
        + Free move mode: Edit the point(s) coordinate.
        """
        if self.Selector.LeftButtonDrag:
            self.Selector.selection_old = list(self.pointsSelection)
            km = QApplication.keyboardModifiers()
            #Add Point
            if km == Qt.AltModifier:
                self.mouse_getAltAdd.emit()
            #Only one clicked.
            elif ((abs(event.x() - self.ox - self.Selector.x) <
                   self.selectionRadius / 2)
                  and (abs(event.y() - self.oy - self.Selector.y) <
                       self.selectionRadius / 2)):
                if ((not self.Selector.selection) and km != Qt.ControlModifier
                        and km != Qt.ShiftModifier):
                    self.mouse_noSelection.emit()
            #Edit point coordinates.
            elif (self.freemove != FreeMode.NoFreeMove):
                self.mouse_freemoveSelection.emit(
                    tuple((row, (self.Points[row].cx, self.Points[row].cy))
                          for row in self.pointsSelection))
        self.Selector.selection_rect.clear()
        self.Selector.MiddleButtonDrag = False
        self.Selector.LeftButtonDrag = False
        self.Selector.RectangularSelection = False
        self.update()

    def mouseMoveEvent(self, event):
        """Move mouse.
        
        + Middle button: Translate canvas view.
        + Left button: Free move mode / Rectangular selection.
        """
        x = (event.x() - self.ox) / self.zoom
        y = (event.y() - self.oy) / -self.zoom
        if self.Selector.MiddleButtonDrag:
            self.ox = event.x() - self.Selector.x
            self.oy = event.y() - self.Selector.y
            self.update()
        elif self.Selector.LeftButtonDrag:
            if self.freemove != FreeMode.NoFreeMove:
                if self.pointsSelection:
                    if self.freemove == FreeMode.Translate:
                        #Free move translate function.
                        mouse_x = x - self.Selector.x / self.zoom
                        mouse_y = y - self.Selector.y / -self.zoom
                        for row in self.pointsSelection:
                            vpoint = self.Points[row]
                            vpoint.move(
                                (mouse_x + vpoint.x, mouse_y + vpoint.y))
                    elif self.freemove == FreeMode.Rotate:
                        #Free move rotate function.
                        alpha = atan2(y, x) - atan2(
                            self.Selector.y / -self.zoom,
                            self.Selector.x / self.zoom)
                        for row in self.pointsSelection:
                            vpoint = self.Points[row]
                            r = hypot(vpoint.x, vpoint.y)
                            beta = atan2(vpoint.y, vpoint.x)
                            vpoint.move(
                                (r * cos(alpha + beta), r * sin(alpha + beta)))
                    elif self.freemove == FreeMode.Reflect:
                        #Free move reflect function.
                        fx = 1 if (x > 0) else -1
                        fy = 1 if (y > 0) else -1
                        for row in self.pointsSelection:
                            vpoint = self.Points[row]
                            if vpoint.type == 0:
                                vpoint.move((vpoint.x * fx, vpoint.y * fy))
                            else:
                                vpoint.move((vpoint.x * fx, vpoint.y * fy),
                                            (vpoint.x * fx, vpoint.y * fy))
            else:
                #Rectangular selection
                self.Selector.RectangularSelection = True
                self.Selector.sx = event.x() - self.ox
                self.Selector.sy = event.y() - self.oy
                self.__rectangularSelectedPoint()
                km = QApplication.keyboardModifiers()
                if self.Selector.selection_rect:
                    if km == Qt.ControlModifier or km == Qt.ShiftModifier:
                        self.mouse_getSelection.emit(
                            tuple(
                                set(self.Selector.selection_old +
                                    self.Selector.selection_rect)), False)
                    else:
                        self.mouse_getSelection.emit(
                            tuple(self.Selector.selection_rect), False)
                else:
                    self.mouse_noSelection.emit()
            self.update()
        self.mouse_track.emit(x, y)

    def __zoomToFitLimit(self):
        """Limitations of four side."""
        inf = float('inf')
        x_right = inf
        x_left = -inf
        y_top = -inf
        y_bottom = inf
        #Paths
        has_path = bool(self.Path.path and (self.Path.show != -2))
        if has_path:
            for i, path in enumerate(self.Path.path):
                if self.Path.show != -1 and self.Path.show != i:
                    continue
                for x, y in path:
                    if x < x_right:
                        x_right = x
                    if x > x_left:
                        x_left = x
                    if y < y_bottom:
                        y_bottom = y
                    if y > y_top:
                        y_top = y
        #Points
        for vpoint in self.Points:
            if has_path and (not vpoint.grounded()):
                continue
            if vpoint.cx < x_right:
                x_right = vpoint.cx
            if vpoint.cx > x_left:
                x_left = vpoint.cx
            if vpoint.cy < y_bottom:
                y_bottom = vpoint.cy
            if vpoint.cy > y_top:
                y_top = vpoint.cy
        #Solving paths
        if self.showTargetPath:
            for path in self.targetPath.values():
                for x, y in path:
                    if x < x_right:
                        x_right = x
                    if x > x_left:
                        x_left = x
                    if y < y_bottom:
                        y_bottom = y
                    if y > y_top:
                        y_top = y
        #Ranges
        for rect in self.ranges.values():
            x_r = rect.x()
            x_l = rect.x() + rect.width()
            y_t = rect.y()
            y_b = rect.y() - rect.height()
            if x_r < x_right:
                x_right = x_r
            if x_l > x_left:
                x_left = x_l
            if y_b < y_bottom:
                y_bottom = y_b
            if y_t > y_top:
                y_top = y_t
        return x_right, x_left, y_top, y_bottom

    def zoomToFit(self):
        """Zoom to fit function."""
        width = self.width()
        height = self.height()
        width = width if not width == 0 else 1
        height = height if not height == 0 else 1
        x_right, x_left, y_top, y_bottom = self.__zoomToFitLimit()
        inf = float('inf')
        if (inf in (x_right, y_bottom)) or (-inf in (x_left, y_top)):
            self.zoom_change.emit(200)
            self.ox = width / 2
            self.oy = height / 2
            self.update()
            return
        x_diff = x_left - x_right
        y_diff = y_top - y_bottom
        x_diff = x_diff if (x_diff != 0) else 1
        y_diff = y_diff if (y_diff != 0) else 1
        if (width / x_diff) < (height / y_diff):
            factor = width / x_diff
        else:
            factor = height / y_diff
        self.zoom_change.emit(int(factor * self.marginFactor * 50))
        self.ox = width / 2 - (x_left + x_right) / 2 * self.zoom
        self.oy = height / 2 + (y_top + y_bottom) / 2 * self.zoom
        self.update()
Exemple #7
0
class FileWidget(QWidget, Ui_Form):
    """The table that stored workbook data,
    including IO functions.
    """

    load_id = pyqtSignal(int)

    def __init__(self, parent):
        super(FileWidget, self).__init__(parent)
        self.setupUi(self)
        """UI part
        
        + ID
        + Date
        + Description
        + Author
        + Previous
        + Branch
        """
        self.CommitTable.setColumnWidth(0, 70)
        self.CommitTable.setColumnWidth(1, 70)
        self.CommitTable.setColumnWidth(2, 130)
        self.CommitTable.setColumnWidth(3, 70)
        self.CommitTable.setColumnWidth(4, 70)
        self.CommitTable.setColumnWidth(5, 70)
        """The main window functions.
        
        + Get current point data.
        + Get current link data.
        """
        self.pointDataFunc = parent.Entities_Point.data
        self.linkDataFunc = parent.Entities_Link.data
        self.storageDataFunc = lambda: tuple(
            (parent.mechanism_storage.item(row).text(),
             parent.mechanism_storage.item(row).expr)
            for row in range(parent.mechanism_storage.count()))
        """Functions to get and set data.
        
        + Call it to get main window be shown as saved.
        + Add empty link with color.
        + Main window will load the entered expression.
        + Reset the main window.
        + Call to load storages.
        + Call after loaded paths.
        """
        self.isSavedFunc = parent.workbookSaved
        self.linkGroupFunc = parent.emptyLinkGroup
        self.parseFunc = parent.parseExpression
        self.clearFunc = parent.clear
        self.loadStorageFunc = parent.loadStorage
        """Mentioned in 'core.widgets.custom',
        because DimensionalSynthesis created after FileWidget.
        
        self.CollectDataFunc #Call to get collections data.
        self.TriangleDataFunc #Call to get triangle data.
        self.InputsDataFunc #Call to get inputs variables data.
        self.AlgorithmDataFunc #Call to get algorithm data.
        self.pathDataFunc #Call to get path data.
        self.loadCollectFunc #Call to load collections data.
        self.loadTriangleFunc #Call to load triangle data.
        self.loadInputsFunc #Call to load inputs variables data.
        self.loadAlgorithmFunc #Call after loaded algorithm results.
        self.loadPathFunc #Call after loaded paths.
        """
        #Close database when destroyed.
        self.destroyed.connect(self.colseDatabase)
        #Undo Stack
        self.CommandStack = parent.CommandStack
        #Reset
        self.reset()

    def reset(self):
        """Clear all the things that dependent on database."""
        #peewee Quary(CommitModel) type
        self.history_commit = None
        self.Script = ""
        self.fileName = QFileInfo("Untitled")
        self.lastTime = datetime.datetime.now()
        self.changed = False
        self.Stack = 0
        self.CommandStack.clear()
        for row in range(self.CommitTable.rowCount()):
            self.CommitTable.removeRow(0)
        self.BranchList.clear()
        self.AuthorList.clear()
        self.FileAuthor.clear()
        self.FileDescription.clear()
        self.branch_current.clear()
        self.commit_search_text.clear()
        self.commit_current_id.setValue(0)

    def connectDatabase(self, fileName: str):
        """Connect database."""
        self.colseDatabase()
        db.init(fileName)
        db.connect()
        db.create_tables([CommitModel, UserModel, BranchModel], safe=True)

    @pyqtSlot()
    def colseDatabase(self):
        if not db.deferred:
            db.close()

    def save(self, fileName: str, isBranch=False):
        """Save database.
        
        + Append to new branch function.
        """
        author_name = self.FileAuthor.text()
        if not author_name:
            author_name = self.FileAuthor.placeholderText()
        branch_name = '' if isBranch else self.branch_current.text()
        commit_text = self.FileDescription.text()
        while not author_name:
            author_name, ok = QInputDialog.getText(
                self, "Author", "Please enter author's name:",
                QLineEdit.Normal, "Anonymous")
            if not ok:
                return
        while not branch_name.isidentifier():
            branch_name, ok = QInputDialog.getText(
                self, "Branch", "Please enter a branch name:",
                QLineEdit.Normal, "master")
            if not ok:
                return
        while not commit_text:
            commit_text, ok = QInputDialog.getText(self, "Commit",
                                                   "Please add a comment:",
                                                   QLineEdit.Normal,
                                                   "Update mechanism.")
            if not ok:
                return
        if ((fileName != self.fileName.absoluteFilePath())
                and os.path.isfile(fileName)):
            os.remove(fileName)
            print("The original file has been overwritten.")
        self.connectDatabase(fileName)
        isError = False
        with db.atomic():
            if author_name in (user.name for user in UserModel.select()):
                author_model = (UserModel.select().where(
                    UserModel.name == author_name).get())
            else:
                author_model = UserModel(name=author_name)
            if branch_name in (branch.name for branch in BranchModel.select()):
                branch_model = (BranchModel.select().where(
                    BranchModel.name == branch_name).get())
            else:
                branch_model = BranchModel(name=branch_name)
            pointData = self.pointDataFunc()
            linkcolor = {
                vlink.name: vlink.colorSTR
                for vlink in self.linkDataFunc()
            }
            args = {
                'author':
                author_model,
                'description':
                commit_text,
                'mechanism':
                compress("M[{}]".format(", ".join(vpoint.expr
                                                  for vpoint in pointData))),
                'linkcolor':
                compress(linkcolor),
                'storage':
                compress(self.storageDataFunc()),
                'pathdata':
                compress(self.pathDataFunc()),
                'collectiondata':
                compress(self.CollectDataFunc()),
                'triangledata':
                compress(self.TriangleDataFunc()),
                'inputsdata':
                compress(self.InputsDataFunc()),
                'algorithmdata':
                compress(self.AlgorithmDataFunc()),
                'branch':
                branch_model
            }
            try:
                args['previous'] = (CommitModel.select().where(
                    CommitModel.id == self.commit_current_id.value()).get())
            except CommitModel.DoesNotExist:
                args['previous'] = None
            new_commit = CommitModel(**args)
            try:
                author_model.save()
                branch_model.save()
                new_commit.save()
            except Exception as e:
                print(str(e))
                db.rollback()
                isError = True
            else:
                self.history_commit = (CommitModel.select().order_by(
                    CommitModel.id))
        if isError:
            os.remove(fileName)
            print("The file was removed.")
            return
        self.read(fileName)
        print("Saving \"{}\" successful.".format(fileName))
        size = QFileInfo(fileName).size()
        print(
            "Size: {}".format("{} MB".format(round(size / 1024 /
                                                   1024, 2)) if size / 1024 //
                              1024 else "{} KB".format(round(size / 1024, 2))))

    def read(self, fileName: str):
        """Load database commit."""
        self.connectDatabase(fileName)
        history_commit = CommitModel.select().order_by(CommitModel.id)
        commit_count = len(history_commit)
        if not commit_count:
            QMessageBox.warning(self, "Warning",
                                "This file is a non-committed database.")
            return
        self.clearFunc()
        self.reset()
        self.history_commit = history_commit
        for commit in self.history_commit:
            self.addCommit(commit)
        print("{} commit(s) was find in database.".format(commit_count))
        self.loadCommit(self.history_commit.order_by(-CommitModel.id).get())
        self.fileName = QFileInfo(fileName)
        self.isSavedFunc()

    def importMechanism(self, fileName: str):
        """Pick and import the latest mechanism from a branch."""
        self.connectDatabase(fileName)
        commit_all = CommitModel.select().join(BranchModel)
        branch_all = BranchModel.select().order_by(BranchModel.name)
        if self.history_commit != None:
            self.connectDatabase(self.fileName.absoluteFilePath())
        else:
            self.colseDatabase()
        branch_name, ok = QInputDialog.getItem(
            self, "Branch", "Select the latest commit in the branch to load.",
            [branch.name for branch in branch_all], 0, False)
        if not ok:
            return
        try:
            commit = (commit_all.where(
                BranchModel.name == branch_name).order_by(
                    CommitModel.date).get())
        except CommitModel.DoesNotExist:
            QMessageBox.warning(self, "Warning",
                                "This file is a non-committed database.")
        else:
            self.importCommit(commit)

    def addCommit(self, commit: CommitModel):
        """Add commit data to all widgets.
        
        + Commit ID
        + Date
        + Description
        + Author
        + Previous commit
        + Branch
        + Add to table widget.
        """
        row = self.CommitTable.rowCount()
        self.CommitTable.insertRow(row)

        self.commit_current_id.setValue(commit.id)
        button = LoadCommitButton(commit.id, self)
        button.loaded.connect(self.loadCommitID)
        self.load_id.connect(button.isLoaded)
        self.CommitTable.setCellWidget(row, 0, button)

        date = ("{t.year:02d}-{t.month:02d}-{t.day:02d} " +
                "{t.hour:02d}:{t.minute:02d}:{t.second:02d}").format(
                    t=commit.date)

        self.CommitTable.setItem(row, 2, QTableWidgetItem(commit.description))

        author_name = commit.author.name
        all_authors = [
            self.AuthorList.item(row).text()
            for row in range(self.AuthorList.count())
        ]
        if author_name not in all_authors:
            self.AuthorList.addItem(author_name)

        if commit.previous:
            previous_id = "#{}".format(commit.previous.id)
        else:
            previous_id = "None"

        branch_name = commit.branch.name
        all_branchs = [
            self.BranchList.item(row).text()
            for row in range(self.BranchList.count())
        ]
        if branch_name not in all_branchs:
            self.BranchList.addItem(branch_name)
        self.branch_current.setText(branch_name)

        for i, text in enumerate(
            [date, commit.description, author_name, previous_id, branch_name]):
            item = QTableWidgetItem(text)
            item.setToolTip(text)
            self.CommitTable.setItem(row, i + 1, item)

    def loadCommitID(self, id: int):
        """Check the id is correct."""
        try:
            commit = self.history_commit.where(CommitModel.id == id).get()
        except CommitModel.DoesNotExist:
            QMessageBox.warning(self, "Warning", "Commit ID is not exist.")
        except AttributeError:
            QMessageBox.warning(self, "Warning", "Nothing submitted.")
        else:
            self.loadCommit(commit)

    def loadCommit(self, commit: CommitModel):
        """Load the commit pointer."""
        if not self.checkSaved():
            return
        #Reset the main window status.
        self.clearFunc()
        #Load the commit to widgets.
        print("Loading commit #{}.".format(commit.id))
        self.load_id.emit(commit.id)
        self.commit_current_id.setValue(commit.id)
        self.branch_current.setText(commit.branch.name)
        #Load the expression.
        self.linkGroupFunc(decompress(commit.linkcolor))
        self.parseFunc(decompress(commit.mechanism))
        #Load the storages.
        self.loadStorageFunc(decompress(commit.storage))
        #Load pathdata.
        self.loadPathFunc(decompress(commit.pathdata))
        #Load collectiondata.
        self.loadCollectFunc(decompress(commit.collectiondata))
        #Load triangledata.
        self.loadTriangleFunc(decompress(commit.triangledata))
        #Load inputsdata.
        self.loadInputsFunc(decompress(commit.inputsdata))
        #Load algorithmdata.
        self.loadAlgorithmFunc(decompress(commit.algorithmdata))
        #Workbook loaded.
        self.isSavedFunc()
        print("The specified phase has been loaded.")

    def importCommit(self, commit: CommitModel):
        """Just load the expression. (No clear step!)"""
        self.parseFunc(decompress(commit.mechanism))
        print("The specified phase has been merged.")

    @pyqtSlot()
    def on_commit_stash_clicked(self):
        """Reload the least commit ID."""
        self.loadCommitID(self.commit_current_id.value())

    def loadExample(self, isImport=False) -> bool:
        """Load example to new workbook."""
        if not self.checkSaved():
            return False
        #load example by expression.
        example_name, ok = QInputDialog.getItem(
            self, "Examples", "Select a example to load:",
            sorted(k for k in example_list), 0, False)
        if not ok:
            return False
        if not isImport:
            self.reset()
            self.clearFunc()
        self.parseFunc(example_list[example_name])
        self.fileName = QFileInfo(example_name)
        self.isSavedFunc()
        print("Example \"{}\" has been loaded.".format(example_name))
        return True

    def checkSaved(self) -> bool:
        """Check and warn if user is not saved yet."""
        if not self.changed:
            return True
        reply = QMessageBox.question(
            self, "Message",
            "Are you sure to load?\nAny changes won't be saved.",
            (QMessageBox.Ok | QMessageBox.Cancel), QMessageBox.Ok)
        return reply == QMessageBox.Ok

    @pyqtSlot(str)
    def on_commit_search_text_textEdited(self, text: str):
        """Commit filter (by description and another)."""
        if not text:
            for row in range(self.CommitTable.rowCount()):
                self.CommitTable.setRowHidden(row, False)
            return
        for row in range(self.CommitTable.rowCount()):
            self.CommitTable.setRowHidden(
                row, not ((text in self.CommitTable.item(row, 2).text()) or
                          (text in self.CommitTable.item(row, 3).text())))

    @pyqtSlot(str)
    def on_AuthorList_currentTextChanged(self, text: str):
        """Change default author's name when select another author."""
        self.FileAuthor.setPlaceholderText(text)

    @pyqtSlot()
    def on_branch_checkout_clicked(self):
        """Switch to the last commit of branch."""
        if not self.BranchList.currentRow() > -1:
            return
        branch_name = self.BranchList.currentItem().text()
        if branch_name != self.branch_current.text():
            leastCommit = (self.history_commit.join(BranchModel).where(
                BranchModel.name == branch_name).order_by(
                    -CommitModel.date).get())
            self.loadCommit(leastCommit)

    @pyqtSlot()
    def on_branch_delete_clicked(self):
        """Delete all commits in the branch."""
        if not self.BranchList.currentRow() > -1:
            return
        branch_name = self.BranchList.currentItem().text()
        if branch_name != self.branch_current.text():
            fileName = self.fileName.absoluteFilePath()
            #Connect on database to remove all the commit in this branch.
            with db.atomic():
                branch_quary = (BranchModel.select().where(
                    BranchModel.name == branch_name))
                (CommitModel.delete().where(
                    CommitModel.branch.in_(branch_quary)).execute())
                (BranchModel.delete().where(
                    BranchModel.name == branch_name).execute())
            db.close()
            print("Branch {} was deleted.".format(branch_name))
            #Reload database.
            self.read(fileName)
        else:
            QMessageBox.warning(self, "Warning",
                                "Cannot delete current branch.")
Exemple #8
0
class CollectionsStructure(QWidget, Ui_Form):
    """Structure widget."""

    layout_sender = pyqtSignal(Graph, dict)

    def __init__(self, parent=None):
        super(CollectionsStructure, self).__init__(parent)
        self.setupUi(self)
        self.outputTo = parent.outputTo
        self.saveReplyBox = parent.saveReplyBox
        self.inputFrom = parent.inputFrom
        self.add_points_by_graph = parent.add_points_by_graph
        self.unsaveFunc = parent.workbookNoSave
        self.collections = []
        self.collections_layouts = []
        self.collections_grounded = []
        self.graph_engine.addItems(EngineList)
        self.graph_engine.setCurrentIndex(2)
        self.graph_engine.currentIndexChanged.connect(
            self.on_reload_atlas_clicked)

    def clearSelection(self):
        """Clear the selection preview data."""
        self.grounded_list.clear()
        self.selection_window.clear()
        self.Expression_edges.clear()
        self.NL.setText('0')
        self.NJ.setText('0')
        self.DOF.setText('0')

    def clear(self):
        """Clear all sub-widgets."""
        self.grounded_merge.setEnabled(False)
        self.triangle_button.setEnabled(False)
        self.collections.clear()
        self.collection_list.clear()
        self.clearSelection()

    @pyqtSlot()
    def on_clear_button_clicked(self):
        """Ask user before clear."""
        if not self.collections:
            return
        reply = QMessageBox.question(self, "Delete",
                                     "Sure to remove all your collections?")
        if reply != QMessageBox.Yes:
            return
        self.clear()
        self.unsaveFunc()

    def engineErrorMsg(self, e: EngineError):
        """Show up error message."""
        QMessageBox.warning(
            self, str(e), "Please install and make sure Graphviz is working.")

    @pyqtSlot()
    @pyqtSlot(str)
    def on_reload_atlas_clicked(self, p0=None):
        """Reload atlas with the engine."""
        if not self.collections:
            return
        self.collections_layouts.clear()
        self.collection_list.clear()
        self.selection_window.clear()
        self.Expression_edges.clear()
        self.NL.setText('0')
        self.NJ.setText('0')
        self.DOF.setText('0')
        self.grounded_list.clear()
        progdlg = QProgressDialog("Drawing atlas...", "Cancel", 0,
                                  len(self.collections), self)
        progdlg.setWindowTitle("Type synthesis")
        progdlg.resize(400, progdlg.height())
        progdlg.setModal(True)
        progdlg.show()
        engineSTR = self.graph_engine.currentText().split(" - ")[1]
        for i, G in enumerate(self.collections):
            QCoreApplication.processEvents()
            if progdlg.wasCanceled():
                return
            item = QListWidgetItem("No. {}".format(i + 1))
            try:
                engine = engine_picker(G, engineSTR)
                item.setIcon(
                    graph(G,
                          self.collection_list.iconSize().width(), engine))
            except EngineError as e:
                progdlg.setValue(progdlg.maximum())
                self.engineErrorMsg(e)
                break
            else:
                self.collections_layouts.append(engine)
                item.setToolTip(str(G.edges))
                self.collection_list.addItem(item)
                progdlg.setValue(i + 1)

    def addCollection(self, edges: Tuple[Tuple[int, int]]):
        """Add collection by in put edges."""
        G = Graph(edges)
        try:
            if not edges:
                raise TestError("is empty graph.")
            for n in G.nodes:
                if len(list(G.neighbors(n))) < 2:
                    raise TestError("is not close chain")
            for H in self.collections:
                if is_isomorphic(G, H):
                    raise TestError("is isomorphic")
        except TestError as e:
            QMessageBox.warning(self, "Add Collection Error",
                                "Error: {}".format(e))
            return
        self.collections.append(G)
        self.unsaveFunc()
        self.on_reload_atlas_clicked()

    def addCollections(self, collections: List[Tuple[Tuple[int, int]]]):
        """Add collections."""
        for c in collections:
            self.addCollection(c)

    @pyqtSlot()
    def on_add_by_edges_button_clicked(self):
        """Add collection by input string."""
        edgesSTR = ""
        while not edgesSTR:
            edgesSTR, ok = QInputDialog.getText(
                self, "Add by edges",
                "Please enter a connection expression:\n" +
                "Example: [(0, 1), (1, 2), (2, 3), (3, 0)]")
            if not ok:
                return
        try:
            edges = eval(edgesSTR)
            if any(len(edge) != 2 for edge in edges):
                raise SyntaxError("Wrong format")
        except Exception as e:
            QMessageBox.warning(self, str(e), "Error: {}".format(e))
            return
        else:
            self.addCollection(edges)

    @pyqtSlot()
    def on_add_by_files_button_clicked(self):
        """Append atlas by text files."""
        fileNames = self.inputFrom("Edges data", ["Text File (*.txt)"],
                                   multiple=True)
        if not fileNames:
            return
        read_data = []
        for fileName in fileNames:
            with open(fileName, 'r') as f:
                read_data += f.read().split('\n')
        collections = []
        for edges in read_data:
            try:
                collections.append(Graph(eval(edges)))
            except:
                QMessageBox.warning(self, "Wrong format",
                                    "Please check the edges text format.")
                return
        if not collections:
            return
        self.collections += collections
        self.on_reload_atlas_clicked()

    @pyqtSlot()
    def on_save_atlas_clicked(self):
        """Save function as same as type synthesis widget."""
        count = self.collection_list.count()
        if not count:
            return
        lateral, ok = QInputDialog.getInt(self, "Atlas",
                                          "The number of lateral:", 5, 1, 10)
        if not ok:
            return
        fileName = self.outputTo("Atlas image", Qt_images)
        if not fileName:
            return
        icon_size = self.collection_list.iconSize()
        width = icon_size.width()
        image_main = QImage(
            QSize(lateral * width if count > lateral else count * width,
                  ((count // lateral) + bool(count % lateral)) * width),
            self.collection_list.item(0).icon().pixmap(
                icon_size).toImage().format())
        image_main.fill(QColor(Qt.white).rgb())
        painter = QPainter(image_main)
        for row in range(count):
            image = self.collection_list.item(row).icon().pixmap(
                icon_size).toImage()
            painter.drawImage(
                QPointF(row % lateral * width, row // lateral * width), image)
        painter.end()
        pixmap = QPixmap()
        pixmap.convertFromImage(image_main)
        pixmap.save(fileName, format=QFileInfo(fileName).suffix())
        self.saveReplyBox("Atlas", fileName)

    @pyqtSlot()
    def on_save_edges_clicked(self):
        """Save function as same as type synthesis widget."""
        count = self.collection_list.count()
        if not count:
            return
        fileName = self.outputTo("Atlas edges expression",
                                 ["Text file (*.txt)"])
        if not fileName:
            return
        with open(fileName, 'w') as f:
            f.write('\n'.join(str(G.edges) for G in self.collections))
        self.saveReplyBox("edges expression", fileName)

    @pyqtSlot(QListWidgetItem, QListWidgetItem)
    def on_collection_list_currentItemChanged(self, item, p0):
        """Show the data of collection.
        
        Save the layout position to keep the graphs
        will be in same appearance.
        """
        has_item = bool(item)
        self.delete_button.setEnabled(has_item)
        self.grounded_button.setEnabled(has_item)
        self.triangle_button.setEnabled(has_item)
        if not item:
            return
        self.selection_window.clear()
        item_ = QListWidgetItem(item.text())
        row = self.collection_list.row(item)
        G = self.collections[row]
        self.ground_engine = self.collections_layouts[row]
        item_.setIcon(
            graph(G,
                  self.selection_window.iconSize().width(),
                  self.ground_engine))
        self.selection_window.addItem(item_)
        self.Expression_edges.setText(str(list(G.edges)))
        self.NL.setText(str(len(G.nodes)))
        self.NJ.setText(str(len(G.edges)))
        self.DOF.setText(
            str(3 * (int(self.NL.text()) - 1) - 2 * int(self.NJ.text())))

    @pyqtSlot()
    def on_Expression_copy_clicked(self):
        """Copy the expression."""
        string = self.Expression_edges.text()
        if string:
            QApplication.clipboard().setText(string)
            self.Expression_edges.selectAll()

    @pyqtSlot()
    def on_delete_button_clicked(self):
        """Delete the selected collection."""
        row = self.collection_list.currentRow()
        if not row > -1:
            return
        reply = QMessageBox.question(
            self, "Delete",
            "Sure to remove #{} from your collections?".format(row))
        if reply != QMessageBox.Yes:
            return
        self.clearSelection()
        self.collection_list.takeItem(row)
        del self.collections[row]
        self.unsaveFunc()

    @pyqtSlot()
    def on_triangle_button_clicked(self):
        """Triangular iteration."""
        G = self.collections[self.collection_list.currentRow()]
        self.layout_sender.emit(G, self.ground_engine)

    @pyqtSlot()
    def on_grounded_button_clicked(self):
        """Grounded combinations."""
        current_item = self.collection_list.currentItem()
        self.collections_grounded.clear()
        self.grounded_list.clear()
        G = self.collections[self.collection_list.row(current_item)]
        item = QListWidgetItem("Released")
        try:
            icon = graph(G,
                         self.grounded_list.iconSize().width(),
                         self.ground_engine)
        except EngineError as e:
            self.engineErrorMsg(e)
            return
        item.setIcon(icon)
        self.collections_grounded.append(G)
        self.grounded_list.addItem(item)
        for node in G.nodes:
            G_ = Graph(G)
            G_.remove_node(node)
            error = False
            for H in self.collections_grounded:
                if is_isomorphic(G_, H):
                    error = True
            if error:
                continue
            item = QListWidgetItem("link_{} constrainted".format(node))
            icon = graph(G,
                         self.grounded_list.iconSize().width(),
                         self.ground_engine,
                         except_node=node)
            item.setIcon(icon)
            self.collections_grounded.append(G_)
            self.grounded_list.addItem(item)
        self.grounded_merge.setEnabled(bool(self.grounded_list.count()))

    @pyqtSlot()
    def on_grounded_merge_clicked(self):
        """Merge the grounded result."""
        item = self.grounded_list.currentItem()
        if not item:
            return
        G = self.collections_grounded[0]
        text = item.text()
        if text == "Released":
            ground_link = None
        else:
            ground_link = int(text.replace(" constrainted", "").split("_")[1])
        reply = QMessageBox.question(
            self, "Message", "Merge \"{}\" chain to your canvas?".format(text))
        if reply == QMessageBox.Yes:
            self.add_points_by_graph(G, self.ground_engine, ground_link)
class DynamicCanvasInterface(BaseCanvas):
    """Abstract class for wrapping main canvas class."""

    tracking = pyqtSignal(float, float)
    browse_tracking = pyqtSignal(float, float)
    selected = pyqtSignal(tuple, bool)
    free_moved = pyqtSignal(tuple)
    noselected = pyqtSignal()
    alt_add = pyqtSignal(float, float)
    doubleclick_edit = pyqtSignal(int)
    zoom_changed = pyqtSignal(int)
    fps_updated = pyqtSignal()
    set_target_point = pyqtSignal(float, float)

    def __init__(self, parent: 'mw.MainWindow'):
        super(DynamicCanvasInterface, self).__init__(parent)
        self.setMouseTracking(True)
        self.setStatusTip("Use mouse wheel or middle button to look around.")
        # The current mouse coordinates.
        self.selector = _Selector()
        # Entities.
        self.vpoints: Tuple[VPoint, ...] = ()
        self.vlinks: Tuple[VLink, ...] = ()
        self.vangles: Tuple[float, ...] = ()
        # Solution.
        self.exprs: List[Tuple[str, ...]] = []
        # Select function.
        self.select_mode = 0
        self.sr = 10
        self.selections: List[int] = []
        # Link transparency.
        self.transparency = 1.
        # Path solving range.
        self.ranges = {}
        # Set show_dimension to False.
        self.show_dimension = False
        # Free move mode.
        self.free_move = FreeMode.NoFreeMove
        # Path preview.
        self.pathpreview: List[List[Tuple[float, float]]] = []
        self.sliderpathpreview: Dict[int, List[Tuple[float, float]]] = {}
        self.previewpath = parent.previewpath
        # Path record.
        self.path_record = []
        # Zooming center.
        # 0: By cursor.
        # 1: By canvas center.
        self.zoomby = 0
        # Mouse snapping value.
        self.snap = 5.
        # Default margin factor.
        self.margin_factor = 0.95
        # Widget size.
        self.width_old = None
        self.height_old = None

    def __draw_frame(self):
        """Draw a outer frame."""
        pos_x = self.width() - self.ox
        pos_y = -self.oy
        neg_x = -self.ox
        neg_y = self.height() - self.oy
        self.painter.drawLine(QPointF(neg_x, pos_y), QPointF(pos_x, pos_y))
        self.painter.drawLine(QPointF(neg_x, neg_y), QPointF(pos_x, neg_y))
        self.painter.drawLine(QPointF(neg_x, pos_y), QPointF(neg_x, neg_y))
        self.painter.drawLine(QPointF(pos_x, pos_y), QPointF(pos_x, neg_y))

    def __draw_point(self, i: int, vpoint: VPoint):
        """Draw a point."""
        if vpoint.type in {VPoint.P, VPoint.RP}:
            pen = QPen(vpoint.color)
            pen.setWidth(2)

            # Draw slot point and pin point.
            for j, (cx, cy) in enumerate(vpoint.c):
                if not vpoint.links:
                    grounded = False
                else:
                    grounded = vpoint.links[j] == 'ground'
                # Slot point.
                if (j == 0) or (vpoint.type == VPoint.P):
                    pen.setColor(vpoint.color)
                    self.painter.setPen(pen)
                    cp = QPointF(cx, -cy) * self.zoom
                    jr = self.joint_size * (2 if j == 0 else 1)
                    rp = QPointF(jr, -jr)
                    self.painter.drawRect(QRectF(cp + rp, cp - rp))
                    if self.show_point_mark:
                        pen.setColor(Qt.darkGray)
                        self.painter.setPen(pen)
                        text = f"[Point{i}]"
                        if self.show_dimension:
                            text += f":({cx:.02f}, {cy:.02f})"
                        self.painter.drawText(cp + rp, text)
                else:
                    self.drawPoint(i, cx, cy, grounded, vpoint.color)

            # Slider line
            pen.setColor(vpoint.color.darker())
            self.painter.setPen(pen)
            qline_m = QLineF(
                QPointF(vpoint.c[1][0], -vpoint.c[1][1]) * self.zoom,
                QPointF(vpoint.c[0][0], -vpoint.c[0][1]) * self.zoom)
            nv = qline_m.normalVector()
            nv.setLength(self.joint_size)
            nv.setPoints(nv.p2(), nv.p1())
            qline_1 = nv.normalVector()
            qline_1.setLength(qline_m.length())
            self.painter.drawLine(qline_1)
            nv.setLength(nv.length() * 2)
            nv.setPoints(nv.p2(), nv.p1())
            qline_2 = nv.normalVector()
            qline_2.setLength(qline_m.length())
            qline_2.setAngle(qline_2.angle() + 180)
            self.painter.drawLine(qline_2)
        else:
            self.drawPoint(i, vpoint.cx, vpoint.cy, vpoint.grounded(),
                           vpoint.color)

        # For selects function.
        if (self.select_mode == 0) and (i in self.selections):
            pen = QPen(QColor(161, 16, 239))
            pen.setWidth(3)
            self.painter.setPen(pen)
            self.painter.drawRect(vpoint.cx * self.zoom - 12,
                                  vpoint.cy * -self.zoom - 12, 24, 24)

    def __points_pos(self, vlink: VLink) -> List[Tuple[float, float]]:
        """Get geometry of the vlink."""
        points = []
        for i in vlink.points:
            vpoint = self.vpoints[i]
            if vpoint.type == VPoint.R:
                x = vpoint.cx * self.zoom
                y = vpoint.cy * -self.zoom
            else:
                coordinate = vpoint.c[0 if
                                      (vlink.name == vpoint.links[0]) else 1]
                x = coordinate[0] * self.zoom
                y = coordinate[1] * -self.zoom
            points.append((x, y))
        return points

    def __draw_link(self, vlink: VLink):
        """Draw a link."""
        if (vlink.name == 'ground') or (not vlink.points):
            return
        points = self.__points_pos(vlink)
        pen = QPen()
        # Rearrange: Put the nearest point to the next position.
        qpoints = convex_hull(points, as_qpoint=True)
        if ((self.select_mode == 1)
                and (self.vlinks.index(vlink) in self.selections)):
            pen.setWidth(self.link_width + 6)
            pen.setColor(QColor(161, 16, 239))
            self.painter.setPen(pen)
            self.painter.drawPolygon(*qpoints)
        pen.setWidth(self.link_width)
        pen.setColor(vlink.color)
        self.painter.setPen(pen)
        brush = QColor(226, 219, 190)
        brush.setAlphaF(self.transparency)
        self.painter.setBrush(brush)
        self.painter.drawPolygon(*qpoints)
        self.painter.setBrush(Qt.NoBrush)
        if not self.show_point_mark:
            return
        pen.setColor(Qt.darkGray)
        self.painter.setPen(pen)
        p_count = len(points)
        cen_x = sum(p[0] for p in points) / p_count
        cen_y = sum(p[1] for p in points) / p_count
        self.painter.drawText(QRectF(cen_x - 50, cen_y - 50, 100, 100),
                              Qt.AlignCenter, f'[{vlink.name}]')

    def __draw_path(self):
        """Draw paths. Recording first."""
        paths = self.path_record or self.Path.path or self.pathpreview
        if len(self.vpoints) != len(paths):
            return
        if paths == self.pathpreview:
            o_path = chain(enumerate(self.pathpreview),
                           self.sliderpathpreview.items())
        else:
            o_path = enumerate(paths)
        pen = QPen()
        for i, path in o_path:
            if (self.Path.show != i) and (self.Path.show != -1):
                continue
            if self.vpoints[i].color:
                color = self.vpoints[i].color
            else:
                color = color_qt('Green')
            pen.setColor(color)
            pen.setWidth(self.path_width)
            self.painter.setPen(pen)
            if self.Path.curve:
                self.drawCurve(path)
            else:
                self.drawDot(path)

    def __draw_slvs_ranges(self):
        """Draw solving range."""
        pen = QPen()
        pen.setWidth(5)
        for i, (tag, rect) in enumerate(self.ranges.items()):
            range_color = QColor(color_num(i + 1))
            range_color.setAlpha(30)
            self.painter.setBrush(range_color)
            range_color.setAlpha(255)
            pen.setColor(range_color)
            self.painter.setPen(pen)
            cx = rect.x() * self.zoom
            cy = rect.y() * -self.zoom
            if rect.width():
                self.painter.drawRect(
                    QRectF(QPointF(cx, cy),
                           QSizeF(rect.width(), rect.height()) * self.zoom))
            else:
                self.painter.drawEllipse(QPointF(cx, cy), 3, 3)
            range_color.setAlpha(255)
            pen.setColor(range_color)
            self.painter.setPen(pen)
            self.painter.drawText(QPointF(cx, cy) + QPointF(6, -6), tag)
            self.painter.setBrush(Qt.NoBrush)

    def __emit_free_move(self, targets: List[int]):
        """Emit free move targets to edit."""
        self.free_moved.emit(
            tuple((num, (
                self.vpoints[num].cx,
                self.vpoints[num].cy,
                self.vpoints[num].angle,
            )) for num in targets))

    def __select_func(self, *, rect: bool = False):
        """Select function."""
        self.selector.selection_rect.clear()
        if self.select_mode == 0:

            def catch(x: float, y: float) -> bool:
                """Detection function for points."""
                if rect:
                    return self.selector.in_rect(x, y)
                else:
                    return self.selector.is_close(x, y, self.sr / self.zoom)

            for i, vpoint in enumerate(self.vpoints):
                if catch(vpoint.cx, vpoint.cy):
                    if i not in self.selector.selection_rect:
                        self.selector.selection_rect.append(i)

        elif self.select_mode == 1:

            def catch(link: VLink) -> bool:
                """Detection function for links.

                + Is polygon: Using Qt polygon geometry.
                + If just a line: Create a range for mouse detection.
                """
                points = self.__points_pos(link)
                if len(points) > 2:
                    polygon = QPolygonF(convex_hull(points, as_qpoint=True))
                else:
                    polygon = QPolygonF(
                        convex_hull([(x + self.sr, y + self.sr)
                                     for x, y in points] +
                                    [(x - self.sr, y - self.sr)
                                     for x, y in points],
                                    as_qpoint=True))
                if rect:
                    return polygon.intersects(
                        QPolygonF(self.selector.to_rect(self.zoom)))
                else:
                    return polygon.containsPoint(
                        QPointF(self.selector.x, -self.selector.y) * self.zoom,
                        Qt.WindingFill)

            for i, vlink in enumerate(self.vlinks):
                if i == 0:
                    continue
                if catch(vlink):
                    if i not in self.selector.selection_rect:
                        self.selector.selection_rect.append(i)

        elif self.select_mode == 2:

            def catch(exprs: Tuple[str, ...]) -> bool:
                """Detection function for solution polygons."""
                points, _ = self.solutionPolygon(exprs[0], exprs[1:-1],
                                                 exprs[-1], self.vpoints)
                polygon = QPolygonF(points)
                if rect:
                    return polygon.intersects(
                        QPolygonF(self.selector.to_rect(self.zoom)))
                else:
                    return polygon.containsPoint(
                        QPointF(self.selector.x, self.selector.y),
                        Qt.WindingFill)

            for i, expr in enumerate(self.exprs):
                if catch(expr):
                    if i not in self.selector.selection_rect:
                        self.selector.selection_rect.append(i)

    def __snap(self, num: float, *, is_zoom: bool = True) -> float:
        """Close to a multiple of coefficient."""
        snap_val = self.snap * self.zoom if is_zoom else self.snap
        if not snap_val:
            return num
        times = num // snap_val
        remainder = num % snap_val
        if remainder < (snap_val / 2):
            return snap_val * times
        else:
            return snap_val * (times + 1)

    def __zoom_to_fit_limit(self) -> Tuple[float, float, float, float]:
        """Limitations of four side."""
        inf = float('inf')
        x_right = inf
        x_left = -inf
        y_top = -inf
        y_bottom = inf
        # Paths
        if self.Path.show != -2:
            paths = self.path_record or self.Path.path or self.pathpreview
            if paths == self.pathpreview:
                o_path = chain(enumerate(self.pathpreview),
                               self.sliderpathpreview.items())
            else:
                o_path = enumerate(paths)
            for i, path in o_path:
                if (self.Path.show != -1) and (self.Path.show != i):
                    continue
                for x, y in path:
                    if x < x_right:
                        x_right = x
                    if x > x_left:
                        x_left = x
                    if y < y_bottom:
                        y_bottom = y
                    if y > y_top:
                        y_top = y
        # Points
        for vpoint in self.vpoints:
            if vpoint.cx < x_right:
                x_right = vpoint.cx
            if vpoint.cx > x_left:
                x_left = vpoint.cx
            if vpoint.cy < y_bottom:
                y_bottom = vpoint.cy
            if vpoint.cy > y_top:
                y_top = vpoint.cy
        # Solving paths
        if self.show_target_path:
            for path in self.target_path.values():
                for x, y in path:
                    if x < x_right:
                        x_right = x
                    if x > x_left:
                        x_left = x
                    if y < y_bottom:
                        y_bottom = y
                    if y > y_top:
                        y_top = y
        # Ranges
        for rect in self.ranges.values():
            x_r = rect.x()
            x_l = rect.x() + rect.width()
            y_t = rect.y()
            y_b = rect.y() - rect.height()
            if x_r < x_right:
                x_right = x_r
            if x_l > x_left:
                x_left = x_l
            if y_b < y_bottom:
                y_bottom = y_b
            if y_t > y_top:
                y_top = y_t
        return x_right, x_left, y_top, y_bottom

    def emit_free_move_all(self):
        """Edit all points to edit."""
        self.__emit_free_move(list(range(len(self.vpoints))))

    def paintEvent(self, event):
        """Drawing functions."""
        width = self.width()
        height = self.height()
        if self.width_old is None:
            self.width_old = width
        if self.height_old is None:
            self.height_old = height
        if (self.width_old != width) or (self.height_old != height):
            self.ox += (width - self.width_old) / 2
            self.oy += (height - self.height_old) / 2
        # 'self' is the instance of 'DynamicCanvas'.
        BaseCanvas.paintEvent(self, event)
        # Draw links except ground.
        for vlink in self.vlinks[1:]:
            self.__draw_link(vlink)
        # Draw path.
        if self.Path.show != -2:
            self.__draw_path()
        # Draw solving path.
        if self.show_target_path:
            self.painter.setFont(QFont("Arial", self.font_size + 5))
            self.__draw_slvs_ranges()
            self.drawTargetPath()
            self.painter.setFont(QFont("Arial", self.font_size))
        # Draw points.
        for i, vpoint in enumerate(self.vpoints):
            self.__draw_point(i, vpoint)
        # Draw solutions.
        if self.select_mode == 2:
            for i, expr in enumerate(self.exprs):
                func = expr[0]
                params = expr[1:-1]
                target = expr[-1]
                self.drawSolution(func, params, target, self.vpoints)
                if i in self.selections:
                    pos, _ = self.solutionPolygon(func, params, target,
                                                  self.vpoints)
                    pen = QPen()
                    pen.setWidth(self.link_width + 3)
                    pen.setColor(QColor(161, 16, 239))
                    self.painter.setPen(pen)
                    self.painter.drawPolygon(QPolygonF(pos))
        # Draw a colored frame for free move mode.
        if self.free_move != FreeMode.NoFreeMove:
            pen = QPen()
            if self.free_move == FreeMode.Translate:
                pen.setColor(QColor(161, 16, 229))
            elif self.free_move == FreeMode.Rotate:
                pen.setColor(QColor(219, 162, 6))
            elif self.free_move == FreeMode.Reflect:
                pen.setColor(QColor(79, 249, 193))
            pen.setWidth(8)
            self.painter.setPen(pen)
            self.__draw_frame()
        # Rectangular selection
        if self.selector.picking:
            pen = QPen(Qt.gray)
            pen.setWidth(1)
            self.painter.setPen(pen)
            self.painter.drawRect(self.selector.to_rect(self.zoom))
        # Show FPS
        self.fps_updated.emit()
        self.painter.end()
        # Record the widget size.
        self.width_old = width
        self.height_old = height

    def mousePressEvent(self, event):
        """Press event.

        Middle button: Move canvas of view.
        Left button: Select the point (only first point will be catch).
        """
        self.selector.x = (event.x() - self.ox) / self.zoom
        self.selector.y = (event.y() - self.oy) / -self.zoom
        button = event.buttons()
        if button == Qt.MiddleButton:
            self.selector.middle_dragged = True
            self.browse_tracking.emit(self.selector.x, self.selector.y)
        elif button == Qt.LeftButton:
            self.selector.left_dragged = True
            if not self.show_target_path:
                self.__select_func()
                if self.selector.selection_rect:
                    self.selected.emit(tuple(self.selector.selection_rect[:1]),
                                       True)

    def mouseDoubleClickEvent(self, event):
        """Mouse double click.

        + Middle button: Zoom to fit.
        + Left button: Edit point function.
        """
        button = event.buttons()
        if button == Qt.MidButton:
            self.zoomToFit()
        elif (button == Qt.LeftButton) and (not self.show_target_path):
            self.selector.x = (event.x() - self.ox) / self.zoom
            self.selector.y = (event.y() - self.oy) / -self.zoom
            self.__select_func()
            if self.selector.selection_rect:
                self.selected.emit(tuple(self.selector.selection_rect[:1]),
                                   True)
                if self.free_move == FreeMode.NoFreeMove:
                    self.doubleclick_edit.emit(self.selector.selection_rect[0])

    def mouseReleaseEvent(self, event):
        """Release mouse button.

        + Alt & Left button: Add a point.
        + Left button: Select a point.
        + Free move mode: Edit the point(s) coordinate.
        """
        if self.selector.left_dragged:
            self.selector.selection_old = list(self.selections)
            if ((self.select_mode == 0)
                    and (self.free_move != FreeMode.NoFreeMove)
                    and (not self.show_target_path)):
                # Edit point coordinates.
                self.__emit_free_move(self.selections)
            else:
                km = QApplication.keyboardModifiers()
                if km == Qt.AltModifier:
                    # Add Point
                    self.alt_add.emit(
                        self.__snap(self.selector.x, is_zoom=False),
                        self.__snap(self.selector.y, is_zoom=False))
                elif ((not self.selector.selection_rect)
                      and (not self.show_target_path)
                      and km != Qt.ControlModifier and km != Qt.ShiftModifier):
                    self.noselected.emit()
        self.selector.release()
        self.update()

    def mouseMoveEvent(self, event):
        """Move mouse.

        + Middle button: Translate canvas view.
        + Left button: Free move mode / Rectangular selection.
        """
        x = (event.x() - self.ox) / self.zoom
        y = (event.y() - self.oy) / -self.zoom
        if self.selector.middle_dragged:
            self.ox = event.x() - self.selector.x * self.zoom
            self.oy = event.y() + self.selector.y * self.zoom
            self.update()
        elif self.selector.left_dragged:
            if self.show_target_path:
                self.set_target_point.emit(x, y)
            elif self.free_move == FreeMode.NoFreeMove:
                # Rectangular selection.
                self.selector.picking = True
                self.selector.sx = self.__snap(x, is_zoom=False)
                self.selector.sy = self.__snap(y, is_zoom=False)
                self.__select_func(rect=True)
                selection = self.selector.current_selection()
                if selection:
                    self.selected.emit(selection, False)
                else:
                    self.noselected.emit()
                unit_text = ('point', 'link', 'solution')[self.select_mode]
                QToolTip.showText(
                    event.globalPos(), f"({self.selector.x:.02f}, "
                    f"{self.selector.y:.02f})\n"
                    f"({self.selector.sx:.02f}, "
                    f"{self.selector.sy:.02f})\n"
                    f"{len(selection)} "
                    f"{unit_text}(s)", self)
            elif self.select_mode == 0:
                if self.free_move == FreeMode.Translate:
                    # Free move translate function.
                    mouse_x = self.__snap(x - self.selector.x, is_zoom=False)
                    mouse_y = self.__snap(y - self.selector.y, is_zoom=False)
                    QToolTip.showText(event.globalPos(),
                                      f"{mouse_x:+.02f}, {mouse_y:+.02f}",
                                      self)
                    for num in self.selections:
                        vpoint = self.vpoints[num]
                        vpoint.move((mouse_x + vpoint.x, mouse_y + vpoint.y))
                elif self.free_move == FreeMode.Rotate:
                    # Free move rotate function.
                    alpha = atan2(y, x) - atan2(self.selector.y,
                                                self.selector.x)
                    QToolTip.showText(event.globalPos(),
                                      f"{degrees(alpha):+.02f}°", self)
                    for num in self.selections:
                        vpoint = self.vpoints[num]
                        r = hypot(vpoint.x, vpoint.y)
                        beta = atan2(vpoint.y, vpoint.x)
                        vpoint.move(
                            (r * cos(beta + alpha), r * sin(beta + alpha)))
                        if vpoint.type in {VPoint.P, VPoint.RP}:
                            vpoint.rotate(self.vangles[num] +
                                          degrees(beta + alpha))
                elif self.free_move == FreeMode.Reflect:
                    # Free move reflect function.
                    fx = 1 if x > 0 else -1
                    fy = 1 if y > 0 else -1
                    QToolTip.showText(event.globalPos(), f"{fx:+d}, {fy:+d}",
                                      self)
                    for num in self.selections:
                        vpoint = self.vpoints[num]
                        if vpoint.type == VPoint.R:
                            vpoint.move((vpoint.x * fx, vpoint.y * fy))
                        else:
                            vpoint.move((vpoint.x * fx, vpoint.y * fy))
                            if (x > 0) != (y > 0):
                                vpoint.rotate(180 - self.vangles[num])
                if self.free_move != FreeMode.NoFreeMove:
                    self.updatePreviewPath()
            self.update()
        self.tracking.emit(x, y)

    def zoomToFit(self):
        """Zoom to fit function."""
        width = self.width()
        height = self.height()
        width = width if width else 1
        height = height if height else 1
        x_right, x_left, y_top, y_bottom = self.__zoom_to_fit_limit()
        inf = float('inf')
        if (inf in {x_right, y_bottom}) or (-inf in {x_left, y_top}):
            self.zoom_changed.emit(200)
            self.ox = width / 2
            self.oy = height / 2
            self.update()
            return
        x_diff = x_left - x_right
        y_diff = y_top - y_bottom
        x_diff = x_diff if x_diff else 1
        y_diff = y_diff if y_diff else 1
        if (width / x_diff) < (height / y_diff):
            factor = width / x_diff
        else:
            factor = height / y_diff
        self.zoom_changed.emit(int(factor * self.margin_factor * 50))
        self.ox = (width - (x_left + x_right) * self.zoom) / 2
        self.oy = (height + (y_top + y_bottom) * self.zoom) / 2
        self.update()
Exemple #10
0
class _BaseTableWidget(QTableWidget, metaclass=QAbcMeta):
    """Two tables has some shared function."""

    rowSelectionChanged = pyqtSignal(list)
    deleteRequest = pyqtSignal()

    def __init__(self, row: int, headers: Sequence[str], parent: QWidget):
        super(_BaseTableWidget, self).__init__(parent)
        self.setSizePolicy(
            QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
        self.setStatusTip(
            "This table will show about the entities items in current view mode."
        )
        self.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)

        self.setRowCount(row)
        self.setColumnCount(len(headers))
        for i, e in enumerate(headers):
            self.setHorizontalHeaderItem(i, QTableWidgetItem(e))

        self.itemSelectionChanged.connect(self.__emitSelectionChanged)

    def rowTexts(self, row: int, *, has_name: bool = False) -> List[str]:
        """Get the whole row of texts.

        + Edit point: has_name = False
        + Edit link: has_name = True
        """
        texts = []
        for column in self.effectiveRange(has_name):
            item = self.item(row, column)
            if item is None:
                texts.append('')
            else:
                texts.append(item.text())
        return texts

    @abstractmethod
    def effectiveRange(self, has_name: bool) -> Iterator[int]:
        """Return valid column range for row text."""
        ...

    @abstractmethod
    def data(self) -> Iterator[Any]:
        """Return table data in subclass."""
        ...

    def dataTuple(self) -> Tuple[Union[VPoint, VLink], ...]:
        """Return data set as a container."""
        return tuple(self.data())

    def selectedRows(self) -> List[int]:
        """Get what row is been selected."""
        return [
            row for row in range(self.rowCount())
            if self.item(row, 0).isSelected()
        ]

    def setSelections(self,
                      selections: Sequence[int],
                      key_detect: bool = False):
        """Auto select function, get the signal from canvas."""
        self.setFocus()
        keyboard_modifiers = QApplication.keyboardModifiers()
        if key_detect:
            continue_select, not_select = {
                Qt.ShiftModifier: (True, False),
                Qt.ControlModifier: (True, True),
            }.get(keyboard_modifiers, (False, False))
            self.__setSelectedRanges(selections,
                                     continue_select=continue_select,
                                     un_select=not_select)
        else:
            self.__setSelectedRanges(
                selections,
                continue_select=(keyboard_modifiers == Qt.ShiftModifier),
                un_select=False)

    def __setSelectedRanges(self, selections: Sequence[int], *,
                            continue_select: bool, un_select: bool):
        """Different mode of select function."""
        selected_rows = self.selectedRows()
        if not continue_select:
            self.clearSelection()
        self.setCurrentCell(selections[-1], 0)
        for row in selections:
            is_selected = (row not in selected_rows) if un_select else True
            self.setRangeSelected(
                QTableWidgetSelectionRange(row, 0, row,
                                           self.columnCount() - 1),
                is_selected)
            self.scrollToItem(self.item(row, 0))

    def keyPressEvent(self, event):
        """Hit the delete key,
        will emit delete signal from this table.
        """
        if event.key() == Qt.Key_Delete:
            self.deleteRequest.emit()

    def clear(self):
        """Overridden the clear function, just removed all items."""
        for row in range(self.rowCount()):
            self.removeRow(0)

    @pyqtSlot()
    def clearSelection(self):
        """Overridden the 'clearSelection' slot to emit 'rowSelectionChanged'"""
        super(_BaseTableWidget, self).clearSelection()
        self.rowSelectionChanged.emit([])

    @pyqtSlot()
    def __emitSelectionChanged(self):
        """Let canvas to show the point selections."""
        self.rowSelectionChanged.emit(self.selectedRows())
Exemple #11
0
class PointTableWidget(_BaseTableWidget):
    """Custom table widget for points."""

    selectionLabelUpdate = pyqtSignal(list)

    def __init__(self, parent: QWidget):
        super(PointTableWidget, self).__init__(0, (
            'Number',
            'Links',
            'Type',
            'Color',
            'X',
            'Y',
            'Current',
        ), parent)
        self.setColumnWidth(0, 60)
        self.setColumnWidth(1, 130)
        self.setColumnWidth(2, 60)
        self.setColumnWidth(3, 90)
        self.setColumnWidth(4, 60)
        self.setColumnWidth(5, 60)
        self.setColumnWidth(6, 130)

    def data(self) -> Iterator[VPoint]:
        """Yield the digitization of all table data."""
        for row in range(self.rowCount()):
            links = self.item(row, 1).text()
            color = self.item(row, 3).text()
            x = float(self.item(row, 4).text())
            y = float(self.item(row, 5).text())
            # p_type = (type: str, angle: float)
            p_type = self.item(row, 2).text().split(':')
            if p_type[0] == 'R':
                type_int = 0
                angle = 0.
            else:
                angle = float(p_type[1])
                type_int = 1 if p_type[0] == 'P' else 2
            vpoint = VPoint(links, type_int, angle, color, x, y, color_qt)
            vpoint.move(*self.currentPosition(row))
            yield vpoint

    def expression(self) -> str:
        """Return expression string."""
        exprs = ", ".join(vpoint.expr for vpoint in self.data())
        return f"M[{exprs}]"

    def editPoint(self, row: int, links: str, type_str: str, color: str,
                  x: float, y: float):
        """Edit a point."""
        for i, e in enumerate(
            [f'Point{row}', links, type_str, color, x, y, f"({x}, {y})"]):
            item = QTableWidgetItem(str(e))
            item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
            if i == 3:
                item.setIcon(color_icon(e))
            self.setItem(row, i, item)

    def rename(self, row: int):
        """When index changed, the points need to rename."""
        for j in range(row, self.rowCount()):
            self.setItem(j, 0, QTableWidgetItem(f'Point{j}'))

    def currentPosition(self, row: int) -> List[Tuple[float, float]]:
        """Get the current coordinate from a point."""
        type_str = self.item(row, 2).text().split(':')
        coords_text = self.item(row, 6).text().replace(';', ',')
        coords = eval(f"[{coords_text}]")
        if (type_str[0] in ('P', 'RP')) and (len(coords) == 1):
            x, y = coords[0]
            self.item(row, 6).setText(f"({x}, {y}); ({x}, {y})")
            coords.append(coords[0])
        return coords

    def updateCurrentPosition(self, coords: Sequence[Tuple[float, float]]):
        """Update the current coordinate for a point."""
        for i, c in enumerate(coords):
            if type(c[0]) == float:
                text = f"({c[0]}, {c[1]})"
            else:
                text = "; ".join(f"({x}, {y})" for x, y in c)
            item = QTableWidgetItem(text)
            item.setToolTip(text)
            self.setItem(i, 6, item)

    def getBackPosition(self):
        """Let all the points go back to origin coordinate."""
        self.updateCurrentPosition(
            tuple((float(self.item(row, 4).text()),
                   float(self.item(row, 5).text()))
                  for row in range(self.rowCount())))

    def getLinks(self, row: int) -> List[str]:
        item = self.item(row, 1)
        if not item:
            return []
        return [s for s in item.text().split(',') if s]

    def setSelections(self,
                      selections: Sequence[int],
                      key_detect: bool = False):
        """Need to update selection label on status bar."""
        super(PointTableWidget, self).setSelections(selections, key_detect)
        self.selectionLabelUpdate.emit(self.selectedRows())

    def effectiveRange(self, has_name: bool) -> Iterator[int]:
        """Row range that can be delete."""
        if has_name:
            return range(self.columnCount())
        else:
            return range(1, self.columnCount() - 1)

    @pyqtSlot()
    def clearSelection(self):
        """Overridden the 'clearSelection' slot,
        so it will emit signal to clean the selection.
        """
        super(PointTableWidget, self).clearSelection()
        self.selectionLabelUpdate.emit([])
class DataDict(QObject):

    """A wrapper class contain the data of nodes."""

    not_saved = pyqtSignal()
    all_saved = pyqtSignal()

    def __init__(self):
        super(DataDict, self).__init__()
        self.__data = {}
        self.__saved = {}
        self.__macros = {}

    def clear(self):
        """Clear data."""
        self.__data.clear()
        self.__saved.clear()
        self.__macros.clear()

    def __getitem__(self, key: Hashable) -> str:
        """Get item string."""
        if key in self.__data:
            return self.__data[key]
        else:
            return ""

    def __setitem__(self, key: Hashable, context: str):
        """Set item."""
        self.__saved[key] = self[key] == context
        self.__data[key] = context
        if not self.__saved[key]:
            self.not_saved.emit()

    def __delitem__(self, key: Hashable):
        """Delete the key and avoid raise error."""
        if key in self.__data:
            del self.__data[key]
            del self.__saved[key]
            for m, code in tuple(self.__macros.items()):
                if code == key:
                    del self.__macros[m]

    def __len__(self) -> int:
        """Length."""
        return len(self.__data)

    def __repr__(self) -> str:
        """Text format."""
        return str(self.__data)

    def __contains__(self, key: Hashable) -> bool:
        """Return True if index is in the data."""
        return key in self.__data

    def update(self, target: Dict[Hashable, str]):
        """Update data."""
        for key, context in target.items():
            self[key] = context

    def items(self) -> Iterable[Tuple[int, str]]:
        """Items of data."""
        return self.__data.items()

    def set_saved(self, key: Hashable, saved: bool):
        """Saved status adjustment."""
        self.__saved[key] = saved

    def is_saved(self, key: Hashable) -> bool:
        """Return saved status."""
        return self.__saved[key]

    def is_all_saved(self) -> bool:
        """Return True if all saved."""
        return all(self.is_saved(key) for key in self.__data)

    def save_all(self):
        """Change all saved status."""
        for key in self.__data:
            self.__saved[key] = True
        self.all_saved.emit()

    def new_num(self) -> int:
        """Get a unused number."""
        i = hash('kmol')
        while i in self.__data:
            i = hash(str(i))
        else:
            self[i] = ""
            return i

    def add_macro(self, name: str, key: Hashable):
        """Add a macro."""
        if key not in self.__data:
            raise KeyError("{} is not in data.".format(key))
        self.__macros[name] = key

    def macros(self) -> Iterable[Tuple[str, Hashable]]:
        """Return macro scripts."""
        return self.__macros.items()
Exemple #13
0
class PreviewWindow(PreviewCanvas):
    """Preview window has some functions of mouse interaction."""

    set_joint_number = pyqtSignal(int)

    def __init__(self, get_solutions_func, parent):
        super(PreviewWindow, self).__init__(get_solutions_func, parent)
        self.pressed = False
        self.get_joint_number = parent.joint_name.currentIndex

    def mousePressEvent(self, event):
        """Check if get close to a joint."""
        mx = (event.x() - self.ox) / self.zoom
        my = (event.y() - self.oy) / -self.zoom
        for node, (x, y) in self.pos.items():
            if node in self.same:
                continue
            if sqrt((mx - x)**2 + (my - y)**2) <= 5:
                self.set_joint_number.emit(node)
                self.pressed = True
                break

    def mouseReleaseEvent(self, event):
        """Cancel the drag."""
        self.pressed = False

    def mouseMoveEvent(self, event):
        """Drag to move the joint."""
        if not self.pressed:
            return
        row = self.get_joint_number()
        if not row > -1:
            return
        mx = (event.x() - self.ox) / self.zoom
        my = (event.y() - self.oy) / -self.zoom
        if -120 <= mx <= 120:
            self.pos[row] = (mx, self.pos[row][1])
        else:
            self.pos[row] = (120 if -120 <= mx else -120, self.pos[row][1])
        if -120 <= my <= 120:
            self.pos[row] = (self.pos[row][0], my)
        else:
            self.pos[row] = (self.pos[row][0], 120 if -120 <= my else -120)
        self.update()

    def friends(self, node1: int, reliable: bool = False) -> int:
        """Return a generator yield the nodes
        that has solution on the same link.
        """
        #All edges of all nodes.
        edges = dict(edges_view(self.G))
        for n, l in self.cus.items():
            edges[int(n.replace('P', ''))] = (l, )
        #Reverse dict of 'self.same'.
        same_r = {v: k for k, v in self.same.items()}
        #for all link of node1.
        links1 = set(edges[node1])
        if node1 in same_r:
            links1.update(edges[same_r[node1]])
        #for all link.
        for node2 in edges:
            if (node1 == node2) or (node2 in self.same):
                continue
            links2 = set(edges[node2])
            if node2 in same_r:
                links2.update(edges[same_r[node2]])
            #Reference by intersection and status.
            if (links1 & links2) and (not self.getStatus(node2) != reliable):
                yield node2

    def sort_nodes(self, nodes: Sequence[int]):
        """Sort the nodes by x value of position."""
        return sorted(nodes, key=lambda n: self.pos[n][0], reverse=True)
Exemple #14
0
class WorkerThread(QThread):
    """The QThread class to handle algorithm."""

    progress_update = pyqtSignal(int, str)
    result = pyqtSignal(dict, float)
    done = pyqtSignal()

    def __init__(self, type_num: AlgorithmType, mech_params: Dict[str, Any],
                 settings: Dict[str, Any]):
        super(WorkerThread, self).__init__(None)
        self.stoped = False
        self.mutex = QMutex()
        self.type_num = type_num
        self.mech_params = mech_params
        self.settings = settings
        self.loop = 1

    def setLoop(self, loop):
        """Set the loop times."""
        self.loop = loop

    def run(self):
        """Start the algorithm loop."""
        with QMutexLocker(self.mutex):
            self.stoped = False
        for name, path in self.mech_params['Target'].items():
            print("- [{}]: {}".format(
                name, tuple((round(x, 2), round(y, 2)) for x, y in path)))
        mechanismObj = build_planar(self.mech_params)
        if self.type_num == AlgorithmType.RGA:
            foo = Genetic
        elif self.type_num == AlgorithmType.Firefly:
            foo = Firefly
        elif self.type_num == AlgorithmType.DE:
            foo = DiffertialEvolution
        self.fun = foo(
            mechanismObj,
            self.settings,
            progress_fun=self.progress_update.emit,
            interrupt_fun=self.__isStoped,
        )
        T0 = timeit.default_timer()
        self.currentLoop = 0
        for self.currentLoop in range(self.loop):
            print("Algorithm [{}]: {}".format(self.currentLoop + 1,
                                              self.type_num))
            if self.stoped:
                #Cancel the remaining tasks.
                print("Canceled.")
                continue
            mechanism, time_spand = self.__algorithm()
            self.result.emit(mechanism, time_spand)
        T1 = timeit.default_timer()
        totalTime = round(T1 - T0, 2)
        print("total cost time: {} [s]".format(totalTime))
        self.done.emit()

    def __algorithm(self) -> Tuple[Dict[str, Any], float]:
        """Get the algorithm result."""
        t0 = timeit.default_timer()
        fitnessParameter, time_and_fitness = self.__generateProcess()
        t1 = timeit.default_timer()
        time_spand = round(t1 - t0, 2)
        cpu = numpy.distutils.cpuinfo.cpu.info[0]
        lastGen = time_and_fitness[-1][0]
        mechanism = {
            'time': time_spand,
            'lastGen': lastGen,
            'interrupted': str(lastGen) if self.stoped else 'False',
            'settings': self.settings,
            'hardwareInfo': {
                'os':
                "{} {} {}".format(platform.system(), platform.release(),
                                  platform.machine()),
                'memory':
                "{} GB".format(round(virtual_memory().total / (1024.**3), 4)),
                'cpu':
                cpu.get("model name", cpu.get('ProcessorNameString', ''))
            },
            'TimeAndFitness': time_and_fitness
        }
        mechanism['Algorithm'] = self.type_num.value
        mechanism.update(self.mech_params)
        mechanism.update(fitnessParameter)
        print("cost time: {} [s]".format(time_spand))
        return mechanism, time_spand

    def __generateProcess(self):
        """Execute algorithm and sort out the result."""
        fitnessParameter, time_and_fitness = self.fun.run()
        return (fitnessParameter, time_and_fitness)

    def __isStoped(self) -> bool:
        """Return stop status for Cython function."""
        return self.stoped

    def stop(self):
        """Stop the algorithm."""
        with QMutexLocker(self.mutex):
            self.stoped = True
Exemple #15
0
class WorkerThread(QThread):
    """The QThread class to handle algorithm."""

    progress_update = pyqtSignal(int, str)
    result = pyqtSignal(dict, float)
    done = pyqtSignal()

    def __init__(self, type_num: AlgorithmType, mech_params: Dict[str, Any],
                 settings: Dict[str, Any]):
        """Input settings from dialog, then call public method 'start'
        to start the algorithm.
        """
        super(WorkerThread, self).__init__()
        self.is_stop = False
        self.type_num = type_num
        self.mech_params = mech_params
        self.settings = settings
        self.loop = 1
        self.currentLoop = 0
        self.fun = None

    def setLoop(self, loop: int):
        """Set the loop times."""
        self.loop = loop

    def run(self):
        """Start the algorithm loop."""
        for name, path in self.mech_params['Target'].items():
            print(f"- [{name}]: {path}")
        t0 = time()
        for self.currentLoop in range(self.loop):
            print(f"Algorithm [{self.currentLoop + 1}]: {self.type_num}")
            if self.is_stop:
                # Cancel the remaining tasks.
                print("Canceled.")
                continue
            mechanism, time_spend = self.__algorithm()
            self.result.emit(mechanism, time_spend)
        print(f"total cost time: {time() - t0:.02f} [s]")
        self.done.emit()

    def __algorithm(self) -> Tuple[Dict[str, Any], float]:
        """Get the algorithm result."""
        t0 = time()
        params, tf = self.__generate_process()
        time_spend = time() - t0
        cpu = numpy.distutils.cpuinfo.cpu.info[0]
        last_gen = tf[-1][0]
        mechanism = {
            'Algorithm': self.type_num.value,
            'time': time_spend,
            'last_gen': last_gen,
            'interrupted': str(last_gen) if self.is_stop else 'False',
            'settings': self.settings,
            'hardware_info': {
                'os': f"{system()} {platform.release()} {platform.machine()}",
                'memory': f"{virtual_memory().total / (1 << 30):.04f} GB",
                'cpu': cpu.get("model name", cpu.get('ProcessorNameString',
                                                     '')),
            },
            'time_fitness': tf,
        }
        mechanism.update(self.mech_params)
        mechanism.update(params)
        print(f"cost time: {time_spend:.02f} [s]")
        return mechanism, time_spend

    def __generate_process(
            self) -> Tuple[Dict[str, Any], List[Tuple[int, float, float]]]:
        """Re-create function object then execute algorithm."""
        if self.type_num == AlgorithmType.RGA:
            foo = Genetic
        elif self.type_num == AlgorithmType.Firefly:
            foo = Firefly
        else:
            foo = Differential
        self.fun = foo(
            Planar(self.mech_params),
            self.settings,
            progress_fun=self.progress_update.emit,
            interrupt_fun=self.__is_stop,
        )
        params, tf = self.fun.run()
        # Note: Remove numpy 'scalar' format.
        return eval(str(params)), tf

    def __is_stop(self) -> bool:
        """Return stop status for Cython function."""
        return self.is_stop

    def stop(self):
        """Stop the algorithm."""
        self.is_stop = True
Exemple #16
0
class TextEditor(QsciScintilla):
    """QScintilla text editor."""

    currtWordChanged = pyqtSignal(str)

    def __init__(self, parent: QWidget):
        """UI settings."""
        super(TextEditor, self).__init__(parent)

        #Set the default font.
        if platform.system().lower() == "windows":
            font_name = "Courier New"
        else:
            font_name = "Mono"
        self.font = QFont(font_name)
        self.font.setFixedPitch(True)
        self.font.setPointSize(14)
        self.setFont(self.font)
        self.setMarginsFont(self.font)
        self.setUtf8(True)

        #Margin 0 is used for line numbers.
        fontmetrics = QFontMetrics(self.font)
        self.setMarginsFont(self.font)
        self.setMarginWidth(0, fontmetrics.width("0000") + 4)
        self.setMarginLineNumbers(0, True)
        self.setMarginsBackgroundColor(QColor("#cccccc"))

        #Brace matching.
        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)

        #Current line visible with special background color.
        self.setCaretLineVisible(True)
        self.setCaretLineBackgroundColor(QColor("#ffe4e4"))

        #Set lexer.
        lexer = QsciLexerCustomPython()
        lexer.setDefaultFont(self.font)
        self.setLexer(lexer)
        self.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1,
                           font_name.encode('utf-8'))

        #Don't want to see the horizontal scrollbar at all.
        self.setWrapMode(QsciScintilla.WrapWord)
        self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)

        #Auto completion.
        self.setAutoCompletionCaseSensitivity(True)
        self.setAutoCompletionSource(QsciScintilla.AcsDocument)
        self.setAutoCompletionThreshold(1)

        #Edge mode.
        self.setEdgeMode(QsciScintilla.EdgeLine)
        self.setEdgeColumn(80)
        self.setEdgeColor(Qt.blue)

        #Indentations.
        self.setAutoIndent(True)
        self.setIndentationsUseTabs(False)
        self.setTabWidth(4)
        self.setTabIndents(True)
        self.setBackspaceUnindents(True)
        self.setIndentationGuides(True)

        #Indicator.
        self.indicatorDefine(QsciScintilla.BoxIndicator, 0)
        self.SendScintilla(QsciScintilla.SCI_SETINDICATORCURRENT, 0)
        self.cursorPositionChanged.connect(self.__catchWords)

        #Widget size.
        self.setMinimumSize(400, 450)

    def __currentWordPosition(self) -> Tuple[int, int]:
        """Return pos of current word."""
        pos = self.positionFromLineIndex(*self.getCursorPosition())
        return (
            self.SendScintilla(QsciScintilla.SCI_WORDSTARTPOSITION, pos, True),
            self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, pos, True),
        )

    @pyqtSlot(int, int)
    def __catchWords(self, line: int, index: int):
        """Catch words that is same with current word."""
        self.clearIndicatorRange(0, 0,
                                 *self.lineIndexFromPosition(self.length()), 0)
        wpos_start, wpos_end = self.__currentWordPosition()
        self.currtWordChanged.emit(self.text()[wpos_start:wpos_end])
        self.fillIndicatorRange(*self.lineIndexFromPosition(wpos_start),
                                *self.lineIndexFromPosition(wpos_end), 0)

    def wheelEvent(self, event):
        """Mouse wheel event."""
        if QApplication.keyboardModifiers() != Qt.ControlModifier:
            super(TextEditor, self).wheelEvent(event)
            return
        if event.angleDelta().y() >= 0:
            self.zoomIn()
        else:
            self.zoomOut()

    def keyPressEvent(self, event):
        """Input key event."""
        key = event.key()
        text = self.selectedText()

        #Commas and parentheses.
        parentheses = _parentheses
        commas = _commas

        #Wrap the selected text.
        if text:
            for match_key, t0, t1 in parentheses:
                if key == match_key:
                    self.replaceSelectedText(t0 + text + t1)
                    return

        super(TextEditor, self).keyPressEvent(event)

        #Auto close of parentheses.
        for match_key, t0, t1 in parentheses:
            if key == match_key:
                self.insert(t1)
                return

        #Add space for commas.
        for co in commas:
            if key == co:
                self.insert(" ")
                line, index = self.getCursorPosition()
                self.setCursorPosition(line, index + 1)
                return
Exemple #17
0
class PointTableWidget(BaseTableWidget):
    """Custom table widget for points."""

    name = 'Point'
    rowSelectionChanged = pyqtSignal(list)
    selectionLabelUpdate = pyqtSignal(list, list)

    def __init__(self, parent=None):
        super(PointTableWidget,
              self).__init__(0,
                             ('Links', 'Type', 'Color', 'X', 'Y', 'Current'),
                             parent)
        self.setColumnWidth(0, 60)
        self.setColumnWidth(1, 130)
        self.setColumnWidth(2, 60)
        self.setColumnWidth(3, 90)
        self.setColumnWidth(4, 60)
        self.setColumnWidth(5, 60)
        self.setColumnWidth(6, 130)
        self.itemSelectionChanged.connect(self.__emitSelectionChanged)

    def data(self) -> VPoint:
        """Yield the digitization of all table data."""
        for row in range(self.rowCount()):
            Links = self.item(row, 1).text()
            color = self.item(row, 3).text()
            x = float(self.item(row, 4).text())
            y = float(self.item(row, 5).text())
            '''
            Type = (type:str, angle:float)
            '''
            Type = self.item(row, 2).text().split(':')
            if Type[0] == 'R':
                Type = 0
                angle = 0.
            elif Type[0] == 'P' or Type[0] == 'RP':
                angle = float(Type[1])
                Type = {'P': 1, 'RP': 2}[Type[0]]
            vpoint = VPoint(Links, Type, angle, color, x, y, colorQt)
            vpoint.move(*self.currentPosition(row))
            yield vpoint

    def editArgs(self, row: int, Links: str, Type: str, Color: str, x: float,
                 y: float):
        """Edite a point."""
        for i, e in enumerate([
                'Point{}'.format(row), Links, Type, Color, x, y,
                "({}, {})".format(x, y)
        ]):
            item = QTableWidgetItem(str(e))
            item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
            if i == 3:
                item.setIcon(colorIcons(e))
            self.setItem(row, i, item)

    def rename(self, row: int):
        """When index changed, the points need to rename."""
        for j in range(row, self.rowCount()):
            self.setItem(j, 0, QTableWidgetItem(self.name + str(j)))

    def currentPosition(self, row: int) -> List[Tuple[float, float]]:
        """Get the current coordinate from a point."""
        Type = self.item(row, 2).text().split(':')
        coords = eval("[{}]".format(
            self.item(row, 6).text().replace(';', ',')))
        if (len(coords) < 2) and ((Type[0] == 'P') or (Type[0] == 'RP')):
            self.item(row,
                      6).setText("({0}, {1}); ({0}, {1})".format(*coords[0]))
            coords.append(coords[0])
        return coords

    def updateCurrentPosition(self, coords: Tuple[Tuple[Tuple[float, float]]]):
        """Update the current coordinate for a point."""
        for i, c in enumerate(coords):
            if type(c[0]) == float:
                text = "({}, {})".format(*c)
            else:
                text = "; ".join("({}, {})".format(x, y) for x, y in c)
            item = QTableWidgetItem(text)
            item.setToolTip(text)
            self.setItem(i, 6, item)

    def getBackPosition(self):
        """Let all the points go back to origin coordinate."""
        self.updateCurrentPosition(
            tuple((float(self.item(row, 4).text()),
                   float(self.item(row, 5).text()))
                  for row in range(self.rowCount())))

    @pyqtSlot(tuple, bool)
    def setSelections(self, selections: Tuple[int], keyDetect: bool):
        """Auto select function, get the signal from canvas."""
        self.setFocus()
        keyboardModifiers = QApplication.keyboardModifiers()
        if keyDetect:
            if keyboardModifiers == Qt.ShiftModifier:
                self.__setSelectedRanges(selections,
                                         continueSelect=True,
                                         UnSelect=False)
            elif keyboardModifiers == Qt.ControlModifier:
                self.__setSelectedRanges(selections,
                                         continueSelect=True,
                                         UnSelect=True)
            else:
                self.__setSelectedRanges(selections,
                                         continueSelect=False,
                                         UnSelect=False)
        else:
            continueSelect = (keyboardModifiers == Qt.ShiftModifier)
            self.__setSelectedRanges(selections,
                                     continueSelect=continueSelect,
                                     UnSelect=False)
        distance = []
        selectedRows = self.selectedRows()
        if len(selectedRows) > 1:
            data = self.dataTuple()
            for i, row in enumerate(selectedRows):
                if i == len(selectedRows) - 1:
                    break
                distance.append(
                    round(data[row].distance(data[selectedRows[i + 1]]), 4))
        self.selectionLabelUpdate.emit(selectedRows, distance)

    def __setSelectedRanges(self, selections: Tuple[int], continueSelect: bool,
                            UnSelect: bool):
        """Different mode of select function."""
        selectedRows = self.selectedRows()
        if not continueSelect:
            self.clearSelection()
        self.setCurrentCell(selections[-1], 0)
        for row in selections:
            isSelected = not row in selectedRows
            self.setRangeSelected(
                QTableWidgetSelectionRange(row, 0, row,
                                           self.columnCount() - 1),
                isSelected if UnSelect else True)
            self.scrollToItem(self.item(row, 0))

    def getLinks(self, row: int) -> List[str]:
        item = self.item(row, 1)
        if not item:
            return []
        return [s for s in item.text().split(',') if s]

    def effectiveRange(self, hasName: bool):
        """Row range that can be delete."""
        if hasName:
            return range(self.columnCount())
        else:
            return range(1, self.columnCount() - 1)

    @pyqtSlot()
    def __emitSelectionChanged(self):
        """Let canvas to show the point selections."""
        self.rowSelectionChanged.emit(self.selectedRows())

    @pyqtSlot()
    def clearSelection(self):
        """Overrided the 'clearSelection' slot,
        so it will emit "selectionLabelUpdate"
        signal to clean the selection.
        """
        super(PointTableWidget, self).clearSelection()
        self.selectionLabelUpdate.emit([], [])
Exemple #18
0
class DatabaseWidget(QWidget, Ui_Form):
    """The table that stored workbook data and changes."""

    load_id = pyqtSignal(int)

    def __init__(self, parent: 'mw.MainWindow'):
        super(DatabaseWidget, self).__init__(parent)
        self.setupUi(self)

        # ID
        self.CommitTable.setColumnWidth(0, 70)
        # Date
        self.CommitTable.setColumnWidth(1, 70)
        # Description
        self.CommitTable.setColumnWidth(2, 130)
        # Author
        self.CommitTable.setColumnWidth(3, 70)
        # Previous
        self.CommitTable.setColumnWidth(4, 70)
        # Branch
        self.CommitTable.setColumnWidth(5, 70)

        # Check file changed function.
        self.__check_file_changed = parent.checkFileChanged
        # Check workbook saved function.
        self.__workbook_saved = parent.workbookSaved

        # Call to get point expressions.
        self.__point_expr_func = parent.EntitiesPoint.expression
        # Call to get link data.
        self.__link_expr_func = parent.EntitiesLink.colors
        # Call to get storage data.
        self.__storage_data_func = parent.getStorage
        # Call to get collections data.
        self.__collect_data_func = parent.CollectionTabPage.collect_data
        # Call to get triangle data.
        self.__triangle_data_func = parent.CollectionTabPage.triangle_data
        # Call to get inputs variables data.
        self.__inputs_data_func = parent.InputsWidget.inputPairs
        # Call to get algorithm data.
        self.__algorithm_data_func = parent.DimensionalSynthesis.mechanism_data
        # Call to get path data.
        self.__path_data_func = parent.InputsWidget.pathData

        # Add empty links function.
        self.__add_links_func = parent.addEmptyLinks
        # Parse function.
        self.__parse_func = parent.parseExpression

        # Call to load inputs variables data.
        self.__load_inputs_func = parent.InputsWidget.addInputsVariables
        # Add storage function.
        self.__add_storage_func = parent.addMultipleStorage
        # Call to load paths.
        self.__load_path_func = parent.InputsWidget.loadPaths
        # Call to load collections data.
        self.__load_collect_func = parent.CollectionTabPage.StructureWidget.addCollections
        # Call to load triangle data.
        self.__load_triangle_func = parent.CollectionTabPage.TriangularIterationWidget.addCollections
        # Call to load algorithm results.
        self.__load_algorithm_func = parent.DimensionalSynthesis.loadResults

        # Clear function for main window.
        self.__clear_func = parent.clear

        # Close database when destroyed.
        self.destroyed.connect(self.__close_database)
        # Undo Stack
        self.__command_clear = parent.CommandStack.clear

        # Reset
        self.history_commit = None
        self.file_name = QFileInfo("Untitled")
        self.last_time = datetime.datetime.now()
        self.changed = False
        self.Stack = 0
        self.reset()

    def reset(self):
        """Clear all the things that dependent on database."""
        self.history_commit: Optional[CommitModel] = None
        self.file_name = QFileInfo("Untitled")
        self.last_time = datetime.datetime.now()
        self.changed = False
        self.Stack = 0
        self.__command_clear()
        for row in range(self.CommitTable.rowCount()):
            self.CommitTable.removeRow(0)
        self.BranchList.clear()
        self.AuthorList.clear()
        self.FileAuthor.clear()
        self.FileDescription.clear()
        self.branch_current.clear()
        self.commit_search_text.clear()
        self.commit_current_id.setValue(0)
        self.__close_database()

    def setFileName(self, file_name: str):
        """Set file name."""
        self.file_name = QFileInfo(file_name)

    def __connect_database(self, file_name: str):
        """Connect database."""
        self.__close_database()
        _db.init(file_name)
        _db.connect()
        _db.create_tables([CommitModel, UserModel, BranchModel], safe=True)

    @pyqtSlot()
    def __close_database(self):
        if not _db.deferred:
            _db.close()

    def save(self, file_name: str, is_branch: bool = False):
        """Save database, append commit to new branch function."""
        author_name = self.FileAuthor.text(
        ) or self.FileAuthor.placeholderText()
        branch_name = '' if is_branch else self.branch_current.text()
        commit_text = self.FileDescription.text()
        while not author_name:
            author_name, ok = QInputDialog.getText(
                self, "Author", "Please enter author's name:",
                QLineEdit.Normal, "Anonymous")
            if not ok:
                return
        while not branch_name.isidentifier():
            branch_name, ok = QInputDialog.getText(
                self, "Branch", "Please enter a branch name:",
                QLineEdit.Normal, "master")
            if not ok:
                return
        while not commit_text:
            commit_text, ok = QInputDialog.getText(self, "Commit",
                                                   "Please add a comment:",
                                                   QLineEdit.Normal,
                                                   "Update mechanism.")
            if not ok:
                return
        if (file_name !=
                self.file_name.absoluteFilePath()) and isfile(file_name):
            os_remove(file_name)
            print("The original file has been overwritten.")
        self.__connect_database(file_name)
        is_error = False
        with _db.atomic():
            if author_name in (user.name for user in UserModel.select()):
                author_model = (UserModel.select().where(
                    UserModel.name == author_name).get())
            else:
                author_model = UserModel(name=author_name)
            if branch_name in (branch.name for branch in BranchModel.select()):
                branch_model = (BranchModel.select().where(
                    BranchModel.name == branch_name).get())
            else:
                branch_model = BranchModel(name=branch_name)
            args = {
                'author':
                author_model,
                'description':
                commit_text,
                'mechanism':
                _compress(self.__point_expr_func()),
                'linkcolor':
                _compress(self.__link_expr_func()),
                'storage':
                _compress(list(self.__storage_data_func())),
                'pathdata':
                _compress(self.__path_data_func()),
                'collectiondata':
                _compress(self.__collect_data_func()),
                'triangledata':
                _compress(self.__triangle_data_func()),
                'inputsdata':
                _compress(
                    tuple((b, d) for b, d, a in self.__inputs_data_func())),
                'algorithmdata':
                _compress(self.__algorithm_data_func()),
                'branch':
                branch_model,
            }
            try:
                args['previous'] = (CommitModel.select().where(
                    CommitModel.id == self.commit_current_id.value()).get())
            except CommitModel.DoesNotExist:
                args['previous'] = None
            new_commit = CommitModel(**args)
            try:
                author_model.save()
                branch_model.save()
                new_commit.save()
            except Exception as e:
                print(str(e))
                _db.rollback()
                is_error = True
            else:
                self.history_commit = CommitModel.select().order_by(
                    CommitModel.id)
        if is_error:
            os_remove(file_name)
            print("The file was removed.")
            return
        self.read(file_name)
        print(f"Saving \"{file_name}\" successful.")
        size = QFileInfo(file_name).size()
        print("Size: " + (f"{size / 1024 / 1024:.02f} MB" if size / 1024 //
                          1024 else f"{size / 1024:.02f} KB"))

    def read(self, file_name: str):
        """Load database commit."""
        self.__connect_database(file_name)
        history_commit = CommitModel.select().order_by(CommitModel.id)
        commit_count = len(history_commit)
        if not commit_count:
            QMessageBox.warning(self, "Warning",
                                "This file is a non-committed database.")
            return
        self.__clear_func()
        self.reset()
        self.history_commit = history_commit
        for commit in self.history_commit:
            self.__add_commit(commit)
        print(f"{commit_count} commit(s) was find in database.")
        self.__load_commit(self.history_commit.order_by(-CommitModel.id).get())
        self.file_name = QFileInfo(file_name)
        self.__workbook_saved()

    def importMechanism(self, file_name: str):
        """Pick and import the latest mechanism from a branch."""
        self.__connect_database(file_name)
        commit_all = CommitModel.select().join(BranchModel)
        branch_all = BranchModel.select().order_by(BranchModel.name)
        if self.history_commit:
            self.__connect_database(self.file_name.absoluteFilePath())
        else:
            self.__close_database()
        branch_name, ok = QInputDialog.getItem(
            self, "Branch", "Select the latest commit in the branch to load.",
            [branch.name for branch in branch_all], 0, False)
        if not ok:
            return
        try:
            commit = (commit_all.where(
                BranchModel.name == branch_name).order_by(
                    CommitModel.date).get())
        except CommitModel.DoesNotExist:
            QMessageBox.warning(self, "Warning",
                                "This file is a non-committed database.")
        else:
            self.__import_commit(commit)

    def __add_commit(self, commit: CommitModel):
        """Add commit data to all widgets.

        + Commit ID
        + Date
        + Description
        + Author
        + Previous commit
        + Branch
        + Add to table widget.
        """
        row = self.CommitTable.rowCount()
        self.CommitTable.insertRow(row)

        self.commit_current_id.setValue(commit.id)
        button = LoadCommitButton(commit.id, self)
        button.loaded.connect(self.__load_commit_id)
        self.load_id.connect(button.set_loaded)
        self.CommitTable.setCellWidget(row, 0, button)

        self.CommitTable.setItem(row, 2, QTableWidgetItem(commit.description))

        author_name = commit.author.name
        for row in range(self.AuthorList.count()):
            if author_name == self.AuthorList.item(row).text():
                break
        else:
            self.AuthorList.addItem(author_name)

        branch_name = commit.branch.name
        for row in range(self.BranchList.count()):
            if branch_name == self.BranchList.item(row).text():
                break
        else:
            self.BranchList.addItem(branch_name)
        self.branch_current.setText(branch_name)
        t = commit.date
        for i, text in enumerate(
            (f"{t.year:02d}-{t.month:02d}-{t.day:02d} "
             f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}", commit.description,
             author_name,
             f"#{commit.previous.id}" if commit.previous else "None",
             branch_name)):
            item = QTableWidgetItem(text)
            item.setToolTip(text)
            self.CommitTable.setItem(row, i + 1, item)

    def __load_commit_id(self, id_int: int):
        """Check the id_int is correct."""
        try:
            commit = self.history_commit.where(CommitModel.id == id_int).get()
        except CommitModel.DoesNotExist:
            QMessageBox.warning(self, "Warning", "Commit ID is not exist.")
        except AttributeError:
            QMessageBox.warning(self, "Warning", "Nothing submitted.")
        else:
            self.__load_commit(commit)

    def __load_commit(self, commit: CommitModel):
        """Load the commit pointer."""
        if self.__check_file_changed():
            return
        # Reset the main window status.
        self.__clear_func()
        # Load the commit to widgets.
        print(f"Loading commit # {commit.id}.")
        self.load_id.emit(commit.id)
        self.commit_current_id.setValue(commit.id)
        self.branch_current.setText(commit.branch.name)
        # Load the expression.
        self.__add_links_func(_decompress(commit.linkcolor))
        self.__parse_func(_decompress(commit.mechanism))
        # Load inputs data.
        input_data: Sequence[Tuple[int, int]] = _decompress(commit.inputsdata)
        self.__load_inputs_func(input_data)
        # Load the storage.
        storage_data: List[Tuple[str, str]] = _decompress(commit.storage)
        self.__add_storage_func(storage_data)
        # Load path data.
        path_data: Dict[str,
                        Sequence[Tuple[float,
                                       float]]] = _decompress(commit.pathdata)
        self.__load_path_func(path_data)
        # Load collection data.
        collection_data: List[Tuple[Tuple[int, int],
                                    ...]] = _decompress(commit.collectiondata)
        self.__load_collect_func(collection_data)
        # Load triangle data.
        triangle_data: Dict[str, Dict[str,
                                      Any]] = _decompress(commit.triangledata)
        self.__load_triangle_func(triangle_data)
        # Load algorithm data.
        algorithm_data: List[Dict[str,
                                  Any]] = _decompress(commit.algorithmdata)
        self.__load_algorithm_func(algorithm_data)

        # Workbook loaded.
        self.__workbook_saved()
        print("The specified phase has been loaded.")

        # Show overview dialog.
        dlg = OverviewDialog(self,
                             f"{commit.branch.name} - commit # {commit.id}",
                             storage_data, input_data, path_data,
                             collection_data, triangle_data, algorithm_data)
        dlg.show()
        dlg.exec_()

    def __import_commit(self, commit: CommitModel):
        """Just load the expression. (No clear step!)"""
        self.__parse_func(_decompress(commit.mechanism))
        print("The specified phase has been merged.")

    @pyqtSlot(name='on_commit_stash_clicked')
    def stash(self):
        """Reload the least commit ID."""
        self.__load_commit_id(self.commit_current_id.value())

    def loadExample(self, is_import: bool = False) -> bool:
        """Load example to new workbook."""
        if self.__check_file_changed():
            return False
        # load example by expression.
        example_name, ok = QInputDialog.getItem(self, "Examples",
                                                "Select an example to load:",
                                                sorted(example_list), 0, False)
        if not ok:
            return False
        expr, inputs = example_list[example_name]
        if not is_import:
            self.reset()
            self.__clear_func()
        self.__parse_func(expr)
        if not is_import:
            # Import without input data.
            self.__load_inputs_func(inputs)
        self.file_name = QFileInfo(example_name)
        self.__workbook_saved()
        print(f"Example \"{example_name}\" has been loaded.")
        return True

    @pyqtSlot(str, name='on_commit_search_text_textEdited')
    def __set_search_text(self, text: str):
        """Commit filter (by description and another)."""
        if not text:
            for row in range(self.CommitTable.rowCount()):
                self.CommitTable.setRowHidden(row, False)
            return
        for row in range(self.CommitTable.rowCount()):
            self.CommitTable.setRowHidden(
                row, not ((text in self.CommitTable.item(row, 2).text()) or
                          (text in self.CommitTable.item(row, 3).text())))

    @pyqtSlot(str, name='on_AuthorList_currentTextChanged')
    def __set_author(self, text: str):
        """Change default author's name when select another author."""
        self.FileAuthor.setPlaceholderText(text)

    @pyqtSlot(name='on_branch_checkout_clicked')
    def __checkout_branch(self):
        """Switch to the last commit of branch."""
        if not self.BranchList.currentRow() > -1:
            return
        branch_name = self.BranchList.currentItem().text()
        if branch_name == self.branch_current.text():
            return
        least_commit = (self.history_commit.join(BranchModel).where(
            BranchModel.name == branch_name).order_by(-CommitModel.date).get())
        self.__load_commit(least_commit)

    @pyqtSlot(name='on_branch_delete_clicked')
    def __delete_branch(self):
        """Delete all commits in the branch."""
        if not self.BranchList.currentRow() > -1:
            return
        branch_name = self.BranchList.currentItem().text()
        if branch_name == self.branch_current.text():
            QMessageBox.warning(self, "Warning",
                                "Cannot delete current branch.")
            return
        file_name = self.file_name.absoluteFilePath()
        # Connect on database to remove all the commit in this branch.
        with _db.atomic():
            CommitModel.delete().where(
                CommitModel.branch.in_(BranchModel.select().where(
                    BranchModel.name == branch_name))).execute()
            BranchModel.delete().where(
                BranchModel.name == branch_name).execute()
        _db.close()
        print(f"Branch {branch_name} was deleted.")
        # Reload database.
        self.read(file_name)
Exemple #19
0
class ExprTableWidget(_BaseTableWidget):
    """Expression table.

    + Free move request: link name, length
    """

    reset = pyqtSignal(bool)
    free_move_request = pyqtSignal(bool)

    def __init__(self, parent: QWidget):
        column_count = ('Function', 'p0', 'p1', 'p2', 'p3', 'p4', 'target')
        super(ExprTableWidget, self).__init__(0, column_count, parent)
        for column in range(self.columnCount()):
            self.setColumnWidth(column, 80)
        self.exprs = []

        @pyqtSlot(QTableWidgetItem)
        def adjust_request(item: QTableWidgetItem):
            """This function is use to change link length
            without to drag the points.
            """
            if item:
                self.free_move_request.emit(item.text().startswith('L'))
            else:
                self.free_move_request.emit(False)

        # Double click behavior.
        self.currentItemChanged.connect(adjust_request)

    def setExpr(self, exprs: List[Tuple[str]],
                data_dict: Dict[str, Union[Tuple[float, float],
                                           float]], unsolved: Tuple[int]):
        """Set the table items for new coming expression."""
        if exprs != self.exprs:
            self.clear()
            self.setRowCount(len(exprs) + len(unsolved))
        row = 0
        for expr in exprs:
            # Target
            self.setItem(row,
                         self.columnCount() - 1, QTableWidgetItem(expr[-1]))
            # Parameters
            for column, e in enumerate(expr[:-1]):
                if e in data_dict:
                    if type(data_dict[e]) == float:
                        # Pure digit
                        text = f"{e}:{data_dict[e]:.02f}"
                    else:
                        # Coordinate
                        text = f"{e}:({data_dict[e][0]:.02f}, {data_dict[e][1]:.02f})"
                else:
                    # Function name
                    text = e
                item = QTableWidgetItem(text)
                item.setToolTip(text)
                self.setItem(row, column, item)
            row += 1
        for p in unsolved:
            # Declaration
            self.setItem(row, 0, QTableWidgetItem("Unsolved"))
            # Target
            self.setItem(row,
                         self.columnCount() - 1, QTableWidgetItem(f"P{p}"))
            row += 1
        self.exprs = exprs

    def data(self) -> None:
        """Not used generator."""
        return

    def effectiveRange(self, has_name: bool) -> Iterator[int]:
        """Return column count."""
        return range(self.columnCount())

    def clear(self):
        """Emit to close the link free move widget."""
        super(ExprTableWidget, self).clear()
        self.reset.emit(False)
Exemple #20
0
class TextEditor(QsciScintilla):
    """QScintilla text editor."""

    word_changed = pyqtSignal()

    def __init__(self, parent: QWidget):
        """UI settings."""
        super(TextEditor, self).__init__(parent)

        # Set the default font.
        if platform.system() == "Windows":
            font_name = "Courier New"
        else:
            font_name = "Mono"
        self.font = QFont(font_name)
        self.font.setFixedPitch(True)
        self.font.setPointSize(14)
        self.setFont(self.font)
        self.setMarginsFont(self.font)
        self.setUtf8(True)

        # Margin 0 is used for line numbers.
        font_metrics = QFontMetrics(self.font)
        self.setMarginsFont(self.font)
        self.setMarginWidth(0, font_metrics.width("0000") + 4)
        self.setMarginLineNumbers(0, True)
        self.setMarginsBackgroundColor(QColor("#cccccc"))

        # Brace matching.
        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)

        # Current line visible with special background color.
        self.setCaretLineVisible(True)
        self.setCaretLineBackgroundColor(QColor("#ffe4e4"))

        # Set lexer.
        self.lexer_option = "Markdown"
        self.set_highlighter("Markdown")
        self.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1,
                           font_name.encode('utf-8'))

        # Don't want to see the horizontal scrollbar at all.
        self.setWrapMode(QsciScintilla.WrapWord)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        # Auto completion.
        self.setAutoCompletionCaseSensitivity(True)
        self.setAutoCompletionSource(QsciScintilla.AcsDocument)
        self.setAutoCompletionThreshold(1)

        # Edge mode.
        self.setEdgeMode(QsciScintilla.EdgeNone)
        self.setEdgeColumn(80)
        self.setEdgeColor(Qt.blue)

        # Indentations.
        self.setAutoIndent(True)
        self.setIndentationsUseTabs(False)
        self.setTabWidth(4)
        self.setTabIndents(True)
        self.setBackspaceUnindents(True)
        self.setIndentationGuides(True)

        # Widget size.
        self.setMinimumSize(400, 450)

        # Remove trailing blanks.
        self.__no_trailing_blanks = True

        # Spell checker indicator [0]
        self.indicatorDefine(QsciScintilla.SquiggleIndicator, 0)

        # Keyword indicator [1]
        self.indicatorDefine(QsciScintilla.BoxIndicator, 1)
        self.cursorPositionChanged.connect(self.__catch_word)

    @pyqtSlot(int, int)
    def __catch_word(self, line: int, index: int):
        """Catch and indicate current word."""
        self.__clear_indicator_all(1)
        pos = self.positionFromLineIndex(line, index)
        _, _, word = self.__word_at_pos(pos)
        word = r'\b' + word + r'\b'
        for m in re.finditer(word.encode('utf-8'),
                             self.text().encode('utf-8'), re.IGNORECASE):
            self.fillIndicatorRange(*self.lineIndexFromPosition(m.start()),
                                    *self.lineIndexFromPosition(m.end()), 1)

    @pyqtSlot(str)
    def set_highlighter(self, option: str):
        """Set highlighter by list."""
        self.lexer_option = option
        lexer = QSCIHIGHLIGHTERS[option]()
        lexer.setDefaultFont(self.font)
        self.setLexer(lexer)

    @pyqtSlot(bool)
    def setEdgeMode(self, option: bool):
        """Set edge mode option."""
        super(TextEditor, self).setEdgeMode(
            QsciScintilla.EdgeLine if option else QsciScintilla.EdgeNone)

    @pyqtSlot(bool)
    def set_remove_trailing_blanks(self, option: bool):
        """Set remove trailing blanks during 'setText' method."""
        self.__no_trailing_blanks = option

    def wheelEvent(self, event):
        """Mouse wheel event."""
        if QApplication.keyboardModifiers() != Qt.ControlModifier:
            super(TextEditor, self).wheelEvent(event)
            return
        if event.angleDelta().y() >= 0:
            self.zoomIn()
        else:
            self.zoomOut()

    def contextMenuEvent(self, event):
        """Custom context menu."""
        # Spell refactor.
        menu: QMenu = self.createStandardContextMenu()
        menu.addSeparator()
        correction_action = QAction("&Refactor Words", self)
        correction_action.triggered.connect(self.__spell_correction)
        menu.addAction(correction_action)
        menu.exec(self.mapToGlobal(event.pos()))

    def __replace_all(self, word: str, replace_word: str):
        """Replace the word for all occurrence."""
        found = self.findFirst(word, False, False, True, True)
        while found:
            self.replace(replace_word)
            found = self.findNext()

    def __word_at_pos(self, pos: int) -> Tuple[int, int, str]:
        """Return pos of current word."""
        return (self.SendScintilla(QsciScintilla.SCI_WORDSTARTPOSITION, pos,
                                   True),
                self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, pos,
                                   True),
                self.wordAtLineIndex(*self.getCursorPosition()))

    @pyqtSlot()
    def __spell_correction(self):
        """Refactor words."""
        pos = self.positionFromLineIndex(*self.getCursorPosition())
        start, end, words = self.__word_at_pos(pos)
        if not words:
            return

        # Camel case.
        word = words
        for m in re.finditer(r'[A-Za-z][a-z]+', words):
            if m.start() < pos - start < m.end():
                word = m.group(0)
                break

        answer, ok = QInputDialog.getItem(self, "Spell correction",
                                          f"Refactor word: \"{word}\"",
                                          _spell.candidates(word))
        if ok:
            self.__replace_all(words, words.replace(word, answer))

    def __cursor_move_next(self):
        """Move text cursor to next character."""
        line, index = self.getCursorPosition()
        self.setCursorPosition(line, index + 1)

    def __cursor_next_char(self) -> str:
        """Next character of cursor."""
        pos = self.positionFromLineIndex(*self.getCursorPosition())
        return self.text(pos, pos + 1)

    def keyPressEvent(self, event):
        """Input key event."""
        key = event.key()
        selected_text = self.selectedText()

        # Commas and parentheses.
        parentheses = list(_parentheses)
        commas = list(_commas)
        if self.lexer_option == "Markdown":
            parentheses.extend(_parentheses_markdown)
            commas.extend(_commas_markdown)
        elif self.lexer_option == "HTML":
            parentheses.extend(_parentheses_html)
            commas.extend(_commas_markdown)

        # Skip the closed parentheses.
        for k1, k2, t0, t1 in parentheses:
            if key == k2:
                if self.__cursor_next_char() == t1:
                    self.__cursor_move_next()
                    return

        # Wrap the selected text.
        if selected_text:
            for k1, k2, t0, t1 in parentheses:
                if key == k1:
                    self.replaceSelectedText(t0 + selected_text + t1)
                    self.word_changed.emit()
                    return

        line, _ = self.getCursorPosition()
        doc_pre = self.text(line)
        super(TextEditor, self).keyPressEvent(event)
        doc_post = self.text(line)
        if doc_pre != doc_post:
            self.word_changed.emit()
        self.__spell_check_line()

        # Auto close of parentheses.
        for k1, k2, t0, t1 in parentheses:
            if key == k1:
                self.insert(t1)
                return

        # Add space for commas.
        for co in commas:
            if key == co:
                self.insert(" ")
                self.__cursor_move_next()
                return

    def __clear_indicator_all(self, indicator: int):
        """Clear all indicators."""
        self.clearIndicatorRange(0, 0,
                                 *self.lineIndexFromPosition(self.length()),
                                 indicator)

    def __spell_check_all(self):
        """Spell check for all text."""
        self.__clear_indicator_all(0)
        for start, end in _spell_check(self.text()):
            self.fillIndicatorRange(*self.lineIndexFromPosition(start),
                                    *self.lineIndexFromPosition(end), 0)

    def __clear_line_indicator(self, line: int, indicator: int):
        """Clear all indicators."""
        self.clearIndicatorRange(line, 0, line, self.lineLength(line),
                                 indicator)

    def __spell_check_line(self):
        """Spell check for current line."""
        line, index = self.getCursorPosition()
        self.__clear_line_indicator(line, 0)
        for start, end in _spell_check(self.text(line)):
            self.fillIndicatorRange(line, start, line, end, 0)

    def remove_trailing_blanks(self):
        """Remove trailing blanks in text editor."""
        scroll_bar: QScrollBar = self.verticalScrollBar()
        pos = scroll_bar.sliderPosition()

        line, index = self.getCursorPosition()
        doc = ""
        for line_str in self.text().splitlines():
            doc += line_str.rstrip() + '\n'
        super(TextEditor, self).setText(doc)

        self.setCursorPosition(line, self.lineLength(line) - 1)
        scroll_bar.setSliderPosition(pos)

    def setText(self, doc: str):
        """Remove trailing blanks in text editor."""
        super(TextEditor, self).setText(doc)
        if self.__no_trailing_blanks:
            self.remove_trailing_blanks()
        self.__spell_check_all()
Exemple #21
0
class DimensionalSynthesis(QWidget, Ui_Form):
    
    """Dimensional synthesis widget."""
    
    fixPointRange = pyqtSignal(dict)
    pathChanged = pyqtSignal(dict)
    mergeResult = pyqtSignal(int, tuple)
    
    def __init__(self, parent):
        super(DimensionalSynthesis, self).__init__(parent)
        self.setupUi(self)
        self.mechanismParams = {}
        self.path = {}
        #A pointer reference of 'collections'.
        self.collections = parent.CollectionTabPage.CollectionsTriangularIteration.collections
        #Data and functions.
        self.mechanism_data = []
        self.inputFrom = parent.inputFrom
        self.unsaveFunc = parent.workbookNoSave
        self.Settings = deepcopy(defaultSettings)
        self.algorithmParams_default()
        #Canvas
        def get_solutions_func():
            try:
                return replace_by_dict(self.mechanismParams)
            except KeyError:
                return tuple()
        self.PreviewCanvas = PreviewCanvas(get_solutions_func, self)
        self.preview_layout.addWidget(self.PreviewCanvas)
        self.show_solutions.clicked.connect(self.PreviewCanvas.setShowSolutions)
        #Splitter
        self.up_splitter.setSizes([80, 100])
        #Table widget column width.
        self.ground_joints.setColumnWidth(0, 50)
        self.ground_joints.setColumnWidth(1, 80)
        self.ground_joints.setColumnWidth(2, 70)
        self.ground_joints.setColumnWidth(3, 70)
        self.ground_joints.setColumnWidth(4, 80)
        #Default value of algorithm parameters.
        self.type0.clicked.connect(self.algorithmParams_default)
        self.type1.clicked.connect(self.algorithmParams_default)
        self.type2.clicked.connect(self.algorithmParams_default)
        #Signals
        self.Result_list.clicked.connect(self.hasResult)
        self.clear_button.clicked.connect(self.clear_settings)
        self.clear()
    
    def clear(self):
        """Clear all sub-widgets."""
        self.mechanism_data.clear()
        self.Result_list.clear()
        self.clear_settings()
        self.hasResult()
    
    def clear_settings(self):
        """Clear sub-widgets that contain the setting."""
        self.on_path_clear_clicked(ask=False)
        self.path.clear()
        self.mechanismParams.clear()
        self.PreviewCanvas.clear()
        self.Settings.clear()
        self.Settings = deepcopy(defaultSettings)
        self.profile_name.setText("No setting")
        self.type2.setChecked(True)
        self.ground_joints.setRowCount(0)
        self.target_points.clear()
        self.Expression.clear()
        self.Link_Expression.clear()
        self.updateRange()
        self.isGenerate()
    
    def loadResults(self,
        mechanism_data: List[Dict[str, Any]]
    ):
        """Append results of workbook database to memory."""
        for e in mechanism_data:
            self.mechanism_data.append(e)
            self.add_result(e)
    
    def currentPathChanged(self):
        """Call the canvas to update to current target path."""
        self.pathChanged.emit({
            name: tuple(path)
            for name, path in self.path.items()
        })
        self.isGenerate()
    
    def currentPath(self) -> List[Tuple[float, float]]:
        """Return the pointer of current target path."""
        item = self.target_points.currentItem()
        if item:
            return self.path[item.text()]
        else:
            return []
    
    @pyqtSlot(str)
    def on_target_points_currentTextChanged(self, text=None):
        """Switch to the current target path."""
        self.path_list.clear()
        for x, y in self.currentPath():
            self.path_list.addItem("({:.04f}, {:.04f})".format(x, y))
        self.currentPathChanged()
    
    @pyqtSlot()
    def on_path_clear_clicked(self, ask: bool =True):
        """Clear the current target path."""
        if ask:
            reply = QMessageBox.question(self,
                "Clear path",
                "Are you sure to clear the current path?"
            )
            if reply != QMessageBox.Yes:
                return
        self.currentPath().clear()
        self.path_list.clear()
        self.currentPathChanged()
    
    @pyqtSlot()
    def on_path_copy_clicked(self):
        """Copy the current path coordinates to clipboard."""
        QApplication.clipboard().setText('\n'.join(
            "{},{}".format(x, y)
            for x, y in self.currentPath()
        ))
    
    @pyqtSlot()
    def on_path_paste_clicked(self):
        """Paste path data from clipboard."""
        self.readPathFromCSV(charSplit(";|,|\n", QApplication.clipboard().text()))
    
    @pyqtSlot()
    def on_importCSV_clicked(self):
        """Paste path data from a text file."""
        fileName = self.inputFrom(
            "Path data",
            ["Text File (*.txt)", "CSV File (*.csv)"]
        )
        if not fileName:
            return
        data = []
        with open(fileName, newline='') as stream:
            reader = csv.reader(stream, delimiter=' ', quotechar='|')
            for row in reader:
                data += ' '.join(row).split(',')
        self.readPathFromCSV(data)
    
    def readPathFromCSV(self, data: List[str]):
        """Trun STR to FLOAT then add them to current target path."""
        try:
            data = [
                (round(float(data[i]), 4), round(float(data[i + 1]), 4))
                for i in range(0, len(data), 2)
            ]
        except:
            QMessageBox.warning(self, "File error",
                "Wrong format.\nIt should be look like this:" +
                "\n0.0,0.0[\\n]" * 3
            )
        else:
            for e in data:
                self.add_point(e[0], e[1])
    
    @pyqtSlot()
    def on_importXLSX_clicked(self):
        """Paste path data from a Excel file."""
        fileName = self.inputFrom(
            "Excel file",
            ["Microsoft Office Excel (*.xlsx *.xlsm *.xltx *.xltm)"]
        )
        if not fileName:
            return
        wb = openpyxl.load_workbook(fileName)
        ws = wb.get_sheet_by_name(wb.get_sheet_names()[0])
        data = []
        #Keep finding until there is no value.
        i = 1
        while True:
            x = ws.cell(row=i, column=1).value
            y = ws.cell(row=i, column=2).value
            if x == None or y == None:
                break
            try:
                data.append((round(float(x), 4), round(float(y), 4)))
            except:
                QMessageBox.warning(self,
                    "File error",
                    "Wrong format.\n" +
                    "The datasheet seems to including non-digital cell."
                )
                break
            i += 1
        for x, y in data:
            self.add_point(x, y)
    
    @pyqtSlot()
    def on_pathAdjust_clicked(self):
        """Show up path adjust dialog and
        get back the changes of current target path.
        """
        dlg = Path_adjust_show(self)
        dlg.show()
        if not dlg.exec_():
            return
        self.on_path_clear_clicked()
        for e in dlg.r_path:
            self.add_point(e[0], e[1])
        self.currentPathChanged()
    
    def add_point(self, x: float, y: float):
        """Add path data to list widget and
        current target path.
        """
        x = round(x, 4)
        y = round(y, 4)
        self.currentPath().append((x, y))
        self.path_list.addItem("({:.04f}, {:.04f})".format(x, y))
        self.currentPathChanged()
    
    @pyqtSlot()
    def on_close_path_clicked(self):
        """Add a the last point same as first point."""
        currentPath = self.currentPath()
        if (self.path_list.count() > 1) and (currentPath[0] != currentPath[-1]):
            self.add_point(*currentPath[0])
    
    @pyqtSlot()
    def on_point_up_clicked(self):
        """Target point move up."""
        row = self.path_list.currentRow()
        if not ((row > 0) and (self.path_list.count() > 1)):
            return
        path = self.currentPath()
        path.insert(row - 1, (path[row][0], path[row][1]))
        del path[row + 1]
        x, y = self.path_list.currentItem().text()[1:-1].split(", ")
        self.path_list.insertItem(row - 1, "({}, {})".format(x, y))
        self.path_list.takeItem(row + 1)
        self.path_list.setCurrentRow(row - 1)
        self.currentPathChanged()
    
    @pyqtSlot()
    def on_point_down_clicked(self):
        """Target point move down."""
        row = self.path_list.currentRow()
        if not (
            (row < self.path_list.count() - 1) and
            (self.path_list.count() > 1)
        ):
            return
        path = self.currentPath()
        path.insert(row + 2, (path[row][0], path[row][1]))
        del path[row]
        x, y = self.path_list.currentItem().text()[1:-1].split(", ")
        self.path_list.insertItem(row+2, "({}, {})".format(x, y))
        self.path_list.takeItem(row)
        self.path_list.setCurrentRow(row+1)
        self.currentPathChanged()
    
    @pyqtSlot()
    def on_point_delete_clicked(self):
        """Delete a target point."""
        row = self.path_list.currentRow()
        if not row > -1:
            return
        del self.currentPath()[row]
        self.path_list.takeItem(row)
        self.currentPathChanged()
    
    def isGenerate(self):
        """Set button enable if all the data are already."""
        self.pointNum.setText(
            "<html><head/><body><p><span style=\"font-size:12pt; color:#00aa00;\">" +
            str(self.path_list.count()) +
            "</span></p></body></html>"
        )
        n = bool(self.mechanismParams) and (self.path_list.count() > 1)
        self.pathAdjust.setEnabled(n)
        self.generate_button.setEnabled(n)
    
    @pyqtSlot()
    def on_generate_button_clicked(self):
        """Start synthesis."""
        #Check if the number of target points are same.
        leng = -1
        for path in self.path.values():
            if leng<0:
                leng = len(path)
            if len(path)!=leng:
                QMessageBox.warning(self, "Target Error",
                    "The length of target paths should be the same."
                )
                return
        #Get the algorithm type.
        if self.type0.isChecked():
            type_num = AlgorithmType.RGA
        elif self.type1.isChecked():
            type_num = AlgorithmType.Firefly
        elif self.type2.isChecked():
            type_num = AlgorithmType.DE
        #Deep copy it so the pointer will not the same.
        mechanismParams = deepcopy(self.mechanismParams)
        mechanismParams['Target'] = deepcopy(self.path)
        for key in ('Driver', 'Follower'):
            for name in mechanismParams[key]:
                row = name_in_table(self.ground_joints, name)
                mechanismParams[key][name] = (
                    self.ground_joints.cellWidget(row, 2).value(),
                    self.ground_joints.cellWidget(row, 3).value(),
                    self.ground_joints.cellWidget(row, 4).value()
                )
        for name in ['IMax', 'IMin', 'LMax', 'LMin', 'FMax', 'FMin', 'AMax', 'AMin']:
            mechanismParams[name] = self.Settings[name]
        setting = {'report': self.Settings['report']}
        if 'maxGen' in self.Settings:
            setting['maxGen'] = self.Settings['maxGen']
        elif 'minFit' in self.Settings:
            setting['minFit'] = self.Settings['minFit']
        elif 'maxTime' in self.Settings:
            setting['maxTime'] = self.Settings['maxTime']
        setting.update(self.Settings['algorithmPrams'])
        #Start progress dialog.
        dlg = Progress_show(
            type_num,
            mechanismParams,
            setting,
            self
        )
        dlg.show()
        if not dlg.exec_():
            return
        for m in dlg.mechanisms:
            self.mechanism_data.append(m)
            self.add_result(m)
        self.setTime(dlg.time_spand)
        self.unsaveFunc()
        QMessageBox.information(self,
            "Dimensional Synthesis",
            "Your tasks is all completed.",
            QMessageBox.Ok
        )
        print("Finished.")
    
    def setTime(self, time: float):
        """Set the time label."""
        self.timeShow.setText(
            "<html><head/><body><p><span style=\"font-size:16pt\">" +
            "{}[min] {:.02f}[s]".format(int(time // 60), time % 60) +
            "</span></p></body></html>"
        )
    
    def add_result(self, result: Dict[str, Any]):
        """Add result items, except add to the list."""
        item = QListWidgetItem(result['Algorithm'])
        interrupt = result['interrupted']
        if interrupt=='False':
            item.setIcon(QIcon(QPixmap(":/icons/task-completed.png")))
        elif interrupt=='N/A':
            item.setIcon(QIcon(QPixmap(":/icons/question-mark.png")))
        else:
            item.setIcon(QIcon(QPixmap(":/icons/interrupted.png")))
        text = "{} ({})".format(
            result['Algorithm'],
            "No interrupt." if interrupt=='False' else "Interrupt at {}".format(interrupt)
        )
        if interrupt == 'N/A':
            text += "\n※Completeness is not clear."
        item.setToolTip(text)
        self.Result_list.addItem(item)
    
    @pyqtSlot()
    def on_deleteButton_clicked(self):
        """Delete a result."""
        row = self.Result_list.currentRow()
        if not row>-1:
            return
        reply = QMessageBox.question(self,
            "Delete",
            "Delete this result from list?"
        )
        if reply != QMessageBox.Yes:
            return
        del self.mechanism_data[row]
        self.Result_list.takeItem(row)
        self.unsaveFunc()
        self.hasResult()
    
    @pyqtSlot()
    def hasResult(self):
        """Set enable if there has any result."""
        for button in [
            self.mergeButton,
            self.deleteButton,
            self.Result_load_settings,
            self.Result_chart,
            self.Result_clipboard
        ]:
            button.setEnabled(self.Result_list.currentRow()>-1)
    
    @pyqtSlot(QModelIndex)
    def on_Result_list_doubleClicked(self, index):
        """Double click result item can show up preview dialog."""
        row = self.Result_list.currentRow()
        if not row>-1:
            return
        dlg = PreviewDialog(self.mechanism_data[row], self.get_path(row), self)
        dlg.show()
    
    @pyqtSlot()
    def on_mergeButton_clicked(self):
        """Merge mechanism into main canvas."""
        row = self.Result_list.currentRow()
        if not row>-1:
            return
        reply = QMessageBox.question(self,
            "Merge",
            "Merge this result to your canvas?"
        )
        if reply == QMessageBox.Yes:
            self.mergeResult.emit(row, self.get_path(row))
    
    def get_path(self, row: int):
        """Using result data to generate paths of mechanism."""
        Result = self.mechanism_data[row]
        expr_angles, expr_links, expr_points = triangle_class(Result['Expression'])
        if len(expr_angles)>1:
            return tuple()
        '''
        expr_angles: ('a0', ...)
        expr_links: ('L0', 'L1', 'L2', ...)
        expr_points: ('A', 'B', 'C', 'D', 'E', ...)
        '''
        Paths = tuple([] for i in range(len(expr_points)))
        for a in range(360 + 1):
            data_dict = {e: Result[e] for e in expr_links}
            data_dict.update({e: Result[e] for e in Result['Driver']})
            data_dict.update({e: Result[e] for e in Result['Follower']})
            data_dict.update({expr_angles[0]: radians(a)})
            expr_parser(Result['Expression'], data_dict)
            for i, e in enumerate(expr_points):
                x, y = data_dict[e]
                if x!=nan:
                    Paths[i].append((x, y))
        return tuple(
            tuple(path) if len(set(path))>1 else ()
            for path in Paths
        )
    
    @pyqtSlot()
    def on_Result_chart_clicked(self):
        """Show up the chart dialog."""
        dlg = ChartDialog("Convergence Value", self.mechanism_data, self)
        dlg.show()
    
    @pyqtSlot()
    def on_Result_clipboard_clicked(self):
        """Copy pretty print result as text."""
        QApplication.clipboard().setText(
            pprint.pformat(self.mechanism_data[self.Result_list.currentRow()])
        )
    
    @pyqtSlot()
    def on_save_button_clicked(self):
        """Save as new profile to collection widget."""
        if not self.mechanismParams:
            return
        name, ok = QInputDialog.getText(self,
            "Profile name",
            "Please enter the profile name:"
        )
        if not ok:
            return
        i = 0
        while (name not in self.collections) and (not name):
            name = "Structure_{}".format(i)
        mechanismParams = deepcopy(self.mechanismParams)
        for key in [
            'Driver',
            'Follower',
            'Target'
        ]:
            for name in mechanismParams[key]:
                mechanismParams[key][name] = None
        self.collections[name] = mechanismParams
        self.unsaveFunc()
    
    @pyqtSlot()
    def on_load_profile_clicked(self):
        """Load profile from collections dialog."""
        dlg = CollectionsDialog(self)
        dlg.show()
        if not dlg.exec_():
            return
        self.clear_settings()
        self.mechanismParams = dlg.mechanismParams
        self.profile_name.setText(dlg.name_loaded)
        self.Expression.setText(self.mechanismParams['Expression'])
        self.Link_Expression.setText(self.mechanismParams['Link_Expression'])
        self.set_profile()
        self.isGenerate()
    
    def set_profile(self):
        """Set profile to sub-widgets."""
        params = self.mechanismParams
        self.path.clear()
        self.target_points.clear()
        for name in sorted(params['Target']):
            self.target_points.addItem(name)
            path = params['Target'][name]
            if path:
                self.path[name] = path.copy()
            else:
                self.path[name] = []
        if self.target_points.count():
            self.target_points.setCurrentRow(0)
        gj = {}
        for key in ('Driver', 'Follower'):
            gj.update(params[key])
        self.ground_joints.setRowCount(0)
        self.ground_joints.setRowCount(len(gj))
        def spinbox(v, prefix=False):
            s = QDoubleSpinBox(self)
            s.setMinimum(-1000000.0)
            s.setMaximum(1000000.0)
            s.setSingleStep(10.0)
            s.setValue(v)
            if prefix:
                s.setPrefix("±")
            return s
        nd = {k: int(v.replace('P', '')) for k, v in params['name_dict'].items()}
        for row, name in enumerate(sorted(gj)):
            coord = gj[name]
            self.ground_joints.setItem(row, 0, QTableWidgetItem(name))
            self.ground_joints.setItem(row, 1,
                QTableWidgetItem('Driver' if name in params['Driver'] else 'Follower')
            )
            self.ground_joints.setCellWidget(row, 2,
                spinbox(coord[0] if coord else params['pos'][nd[name]][0])
            )
            self.ground_joints.setCellWidget(row, 3,
                spinbox(coord[1] if coord else params['pos'][nd[name]][1])
            )
            self.ground_joints.setCellWidget(row, 4,
                spinbox(coord[2] if coord else 50., True)
            )
        for row in range(self.ground_joints.rowCount()):
            for column in range(2, 5):
                self.ground_joints.cellWidget(row, column).valueChanged.connect(self.updateRange)
        self.updateRange()
        self.PreviewCanvas.from_profile(self.mechanismParams)
    
    @pyqtSlot()
    def on_Result_load_settings_clicked(self):
        """Load settings from a result."""
        self.hasResult()
        row = self.Result_list.currentRow()
        if not row>-1:
            return
        self.clear_settings()
        Result = self.mechanism_data[row]
        if Result['Algorithm'] == str(AlgorithmType.RGA):
            self.type0.setChecked(True)
        elif Result['Algorithm'] == str(AlgorithmType.Firefly):
            self.type1.setChecked(True)
        elif Result['Algorithm'] == str(AlgorithmType.DE):
            self.type2.setChecked(True)
        self.profile_name.setText("External setting")
        #External setting.
        self.Expression.setText(Result['Expression'])
        self.Link_Expression.setText(Result['Link_Expression'])
        #Copy to mechanism params.
        self.mechanismParams.clear()
        for key in [
            'Driver',
            'Follower',
            'Target'
        ]:
            self.mechanismParams[key] = Result[key].copy()
        for key in [
            'Link_Expression',
            'Expression',
            'constraint',
            'Graph',
            'name_dict',
            'pos',
            'cus',
            'same'
        ]:
            self.mechanismParams[key] = Result[key]
        self.set_profile()
        self.setTime(Result['time'])
        settings = Result['settings']
        self.Settings = {
            'report': settings['report'],
            'IMax': Result['IMax'], 'IMin': Result['IMin'],
            'LMax': Result['LMax'], 'LMin': Result['LMin'],
            'FMax': Result['FMax'], 'FMin': Result['FMin'],
            'AMax': Result['AMax'], 'AMin': Result['AMin']
        }
        if 'maxGen' in settings:
            self.Settings['maxGen'] = settings['maxGen']
        elif 'minFit' in settings:
            self.Settings['minFit'] = settings['minFit']
        elif 'maxTime' in settings:
            self.Settings['maxTime'] = settings['maxTime']
        algorithmPrams = settings.copy()
        del algorithmPrams['report']
        self.Settings['algorithmPrams'] = algorithmPrams
    
    def algorithmParams_default(self):
        """Set the algorithm settings to default."""
        if self.type0.isChecked():
            self.Settings['algorithmPrams'] = GeneticPrams.copy()
        elif self.type1.isChecked():
            self.Settings['algorithmPrams'] = FireflyPrams.copy()
        elif self.type2.isChecked():
            self.Settings['algorithmPrams'] = DifferentialPrams.copy()
    
    @pyqtSlot()
    def on_advanceButton_clicked(self):
        """Get the settings from advance dialog."""
        if self.type0.isChecked():
            type_num = AlgorithmType.RGA
        elif self.type1.isChecked():
            type_num = AlgorithmType.Firefly
        elif self.type2.isChecked():
            type_num = AlgorithmType.DE
        dlg = Options_show(type_num, self.Settings)
        dlg.show()
        if not dlg.exec_():
            return
        tablePL = lambda row: dlg.PLTable.cellWidget(row, 1).value()
        self.Settings = {
            'report': dlg.report.value(),
            'IMax': tablePL(0), 'IMin': tablePL(1),
            'LMax': tablePL(2), 'LMin': tablePL(3),
            'FMax': tablePL(4), 'FMin': tablePL(5),
            'AMax': tablePL(6), 'AMin': tablePL(7)
        }
        if dlg.maxGen_option.isChecked():
            self.Settings['maxGen'] = dlg.maxGen.value()
        elif dlg.minFit_option.isChecked():
            self.Settings['minFit'] = dlg.minFit.value()
        elif dlg.maxTime_option.isChecked():
            #Three spinbox value translate to second.
            self.Settings['maxTime'] = (
                dlg.maxTime_h.value() * 3600 +
                dlg.maxTime_m.value() * 60 +
                dlg.maxTime_s.value()
            )
        tableAP = lambda row: dlg.APTable.cellWidget(row, 1).value()
        popSize = dlg.popSize.value()
        if type_num == AlgorithmType.RGA:
            self.Settings['algorithmPrams'] = {
                'nPop': popSize,
                'pCross': tableAP(0),
                'pMute': tableAP(1),
                'pWin': tableAP(2),
                'bDelta': tableAP(3)
            }
        elif type_num == AlgorithmType.Firefly:
            self.Settings['algorithmPrams'] = {
                'n': popSize,
                'alpha': tableAP(0),
                'betaMin': tableAP(1),
                'gamma': tableAP(2),
                'beta0': tableAP(3)
            }
        elif type_num == AlgorithmType.DE:
            self.Settings['algorithmPrams'] = {
                'NP': popSize,
                'strategy': tableAP(0),
                'F': tableAP(1),
                'CR': tableAP(2)
            }
    
    @pyqtSlot(float)
    def updateRange(self, p0=None):
        """Update range values to main canvas."""
        def t(x, y):
            item = self.ground_joints.item(x, y)
            if item:
                return item.text()
            else:
                return self.ground_joints.cellWidget(x, y).value()
        self.fixPointRange.emit({
            t(row, 0): (t(row, 2), t(row, 3), t(row, 4))
            for row in range(self.ground_joints.rowCount())
        })
    
    @pyqtSlot()
    def on_Expression_copy_clicked(self):
        """Copy profile expression."""
        text = self.Expression.text()
        if text:
            QApplication.clipboard().setText(text)
    
    @pyqtSlot()
    def on_Link_Expression_copy_clicked(self):
        """Copy profile linkage expression."""
        text = self.Link_Expression.text()
        if text:
            QApplication.clipboard().setText(text)
Exemple #22
0
class InputsWidget(QWidget, Ui_Form):

    """There has following functions:

    + Function of mechanism variables settings.
    + Path recording.
    """

    aboutToResolve = pyqtSignal()

    def __init__(self, parent: 'mw.MainWindow'):
        super(InputsWidget, self).__init__(parent)
        self.setupUi(self)

        # parent's function pointer.
        self.free_move_button = parent.free_move_button
        self.EntitiesPoint = parent.EntitiesPoint
        self.EntitiesLink = parent.EntitiesLink
        self.MainCanvas = parent.MainCanvas
        self.solve = parent.solve
        self.reloadCanvas = parent.reloadCanvas
        self.outputTo = parent.outputTo
        self.conflict = parent.conflict
        self.DOF = lambda: parent.DOF
        self.rightInput = parent.rightInput
        self.CommandStack = parent.CommandStack
        self.setCoordsAsCurrent = parent.setCoordsAsCurrent

        # QDial: Angle panel.
        self.dial = QDial()
        self.dial.setStatusTip("Input widget of rotatable joint.")
        self.dial.setEnabled(False)
        self.dial.valueChanged.connect(self.__update_var)
        self.dial_spinbox.valueChanged.connect(self.__set_var)
        self.inputs_dial_layout.addWidget(RotatableView(self.dial))

        # QDial ok check.
        self.variable_list.currentRowChanged.connect(self.__dial_ok)

        # Play button.
        action = QShortcut(QKeySequence("F5"), self)
        action.activated.connect(self.variable_play.click)
        self.variable_stop.clicked.connect(self.variableValueReset)

        # Timer for play button.
        self.inputs_playShaft = QTimer(self)
        self.inputs_playShaft.setInterval(10)
        self.inputs_playShaft.timeout.connect(self.__change_index)

        # Change the point coordinates with current position.
        self.update_pos.clicked.connect(self.setCoordsAsCurrent)

        """Inputs record context menu
        
        + Copy data from Point0
        + Copy data from Point1
        + ...
        """
        self.pop_menu_record_list = QMenu(self)
        self.record_list.customContextMenuRequested.connect(
            self.__record_list_context_menu
        )
        self.__path_data: Dict[str, Sequence[Tuple[float, float]]] = {}

    def clear(self):
        """Clear function to reset widget status."""
        self.__path_data.clear()
        for i in range(self.record_list.count() - 1):
            self.record_list.takeItem(1)
        self.variable_list.clear()

    def __set_angle_mode(self):
        """Change to angle input."""
        self.dial.setMinimum(0)
        self.dial.setMaximum(36000)
        self.dial_spinbox.setMinimum(0)
        self.dial_spinbox.setMaximum(360)

    def __set_unit_mode(self):
        """Change to unit input."""
        self.dial.setMinimum(-50000)
        self.dial.setMaximum(50000)
        self.dial_spinbox.setMinimum(-500)
        self.dial_spinbox.setMaximum(500)

    def pathData(self):
        """Return current path data."""
        return self.__path_data

    @pyqtSlot(tuple)
    def setSelection(self, selections: Tuple[int]):
        """Set one selection from canvas."""
        self.joint_list.setCurrentRow(selections[0])

    @pyqtSlot()
    def clearSelection(self):
        """Clear the points selection."""
        self.driver_list.clear()
        self.joint_list.setCurrentRow(-1)

    @pyqtSlot(int, name='on_joint_list_currentRowChanged')
    def __update_relate_points(self, _: int):
        """Change the point row from input widget."""
        self.driver_list.clear()

        item: Optional[QListWidgetItem] = self.joint_list.currentItem()
        if item is None:
            return
        p0 = _variable_int(item.text())

        vpoints = self.EntitiesPoint.dataTuple()
        type_int = vpoints[p0].type
        if type_int == VPoint.R:
            for i, vpoint in enumerate(vpoints):
                if i == p0:
                    continue
                if vpoints[p0].same_link(vpoint):
                    if vpoints[p0].grounded() and vpoint.grounded():
                        continue
                    self.driver_list.addItem(f"[{vpoint.typeSTR}] Point{i}")
        elif type_int in {VPoint.P, VPoint.RP}:
            self.driver_list.addItem(f"[{vpoints[p0].typeSTR}] Point{p0}")

    @pyqtSlot(int, name='on_driver_list_currentRowChanged')
    def __set_add_var_enabled(self, _: int):
        """Set enable of 'add variable' button."""
        driver = self.driver_list.currentIndex()
        self.variable_add.setEnabled(driver != -1)

    @pyqtSlot(name='on_variable_add_clicked')
    def __add_inputs_variable(self, p0: Optional[int] = None, p1: Optional[int] = None):
        """Add variable with '->' sign."""
        if p0 is None:
            item: Optional[QListWidgetItem] = self.joint_list.currentItem()
            if item is None:
                return
            p0 = _variable_int(item.text())
        if p1 is None:
            item: Optional[QListWidgetItem] = self.driver_list.currentItem()
            if item is None:
                return
            p1 = _variable_int(item.text())

        # Check DOF.
        if self.DOF() <= self.inputCount():
            QMessageBox.warning(
                self,
                "Wrong DOF",
                "The number of variable must no more than degrees of freedom."
            )
            return

        # Check same link.
        vpoints = self.EntitiesPoint.dataTuple()
        if not vpoints[p0].same_link(vpoints[p1]):
            QMessageBox.warning(
                self,
                "Wrong pair",
                "The base point and driver point should at the same link."
            )
            return

        # Check repeated pairs.
        for p0_, p1_, a in self.inputPairs():
            if {p0, p1} == {p0_, p1_} and vpoints[p0].type == VPoint.R:
                QMessageBox.warning(
                    self,
                    "Wrong pair",
                    "There already have a same pair."
                )
                return

        name = f'Point{p0}'
        self.CommandStack.beginMacro(f"Add variable of {name}")
        if p0 == p1:
            # One joint by offset.
            value = vpoints[p0].true_offset()
        else:
            # Two joints by angle.
            value = vpoints[p0].slope_angle(vpoints[p1])
        self.CommandStack.push(AddVariable('->'.join((
            name,
            f'Point{p1}',
            f"{value:.02f}",
        )), self.variable_list))
        self.CommandStack.endMacro()

    def addInputsVariables(self, variables: Sequence[Tuple[int, int]]):
        """Add from database."""
        for p0, p1 in variables:
            self.__add_inputs_variable(p0, p1)

    @pyqtSlot()
    def __dial_ok(self):
        """Set the angle of base link and drive link."""
        row = self.variable_list.currentRow()
        enabled = row > -1
        rotatable = (
            enabled and
            not self.free_move_button.isChecked() and
            self.rightInput()
        )
        self.dial.setEnabled(rotatable)
        self.dial_spinbox.setEnabled(rotatable)
        self.oldVar = self.dial.value() / 100.
        self.variable_play.setEnabled(rotatable)
        self.variable_speed.setEnabled(rotatable)
        item: Optional[QListWidgetItem] = self.variable_list.currentItem()
        if item is None:
            return
        expr = item.text().split('->')
        p0 = int(expr[0].replace('Point', ''))
        p1 = int(expr[1].replace('Point', ''))
        value = float(expr[2])
        if p0 == p1:
            self.__set_unit_mode()
        else:
            self.__set_angle_mode()
        self.dial.setValue(value * 100 if enabled else 0)

    def variableExcluding(self, row: Optional[int] = None):
        """Remove variable if the point was been deleted. Default: all."""
        one_row: bool = row is not None
        for i, (b, d, a) in enumerate(self.inputPairs()):
            # If this is not origin point any more.
            if one_row and (row != b):
                continue
            self.CommandStack.beginMacro(f"Remove variable of Point{row}")
            self.CommandStack.push(DeleteVariable(i, self.variable_list))
            self.CommandStack.endMacro()

    @pyqtSlot(name='on_variable_remove_clicked')
    def __remove_var(self):
        """Remove and reset angle."""
        row = self.variable_list.currentRow()
        if not row > -1:
            return
        reply = QMessageBox.question(
            self,
            "Remove variable",
            "Do you want to remove this variable?"
        )
        if reply != QMessageBox.Yes:
            return
        self.variable_stop.click()
        self.CommandStack.beginMacro(f"Remove variable of Point{row}")
        self.CommandStack.push(DeleteVariable(row, self.variable_list))
        self.CommandStack.endMacro()
        self.EntitiesPoint.getBackPosition()
        self.solve()

    def interval(self) -> float:
        """Return interval value."""
        return self.record_interval.value()

    def inputCount(self) -> int:
        """Use to show input variable count."""
        return self.variable_list.count()

    def inputPairs(self) -> Iterator[Tuple[int, int, float]]:
        """Back as point number code."""
        for row in range(self.variable_list.count()):
            var = self.variable_list.item(row).text().split('->')
            p0 = int(var[0].replace('Point', ''))
            p1 = int(var[1].replace('Point', ''))
            angle = float(var[2])
            yield (p0, p1, angle)

    def variableReload(self):
        """Auto check the points and type."""
        self.joint_list.clear()
        for i in range(self.EntitiesPoint.rowCount()):
            type_text = self.EntitiesPoint.item(i, 2).text()
            self.joint_list.addItem(f"[{type_text}] Point{i}")
        self.variableValueReset()

    @pyqtSlot(float)
    def __set_var(self, value: float):
        self.dial.setValue(int(value * 100 % self.dial.maximum()))

    @pyqtSlot(int)
    def __update_var(self, value: int):
        """Update the value when rotating QDial."""
        item = self.variable_list.currentItem()
        value /= 100.
        self.dial_spinbox.blockSignals(True)
        self.dial_spinbox.setValue(value)
        self.dial_spinbox.blockSignals(False)
        if item:
            item_text = item.text().split('->')
            item_text[-1] = f"{value:.02f}"
            item.setText('->'.join(item_text))
            self.aboutToResolve.emit()
        if (
            self.record_start.isChecked() and
            abs(self.oldVar - value) > self.record_interval.value()
        ):
            self.MainCanvas.recordPath()
            self.oldVar = value

    def variableValueReset(self):
        """Reset the value of QDial."""
        if self.inputs_playShaft.isActive():
            self.variable_play.setChecked(False)
            self.inputs_playShaft.stop()
        self.EntitiesPoint.getBackPosition()
        vpoints = self.EntitiesPoint.dataTuple()
        for i, (p0, p1, a) in enumerate(self.inputPairs()):
            self.variable_list.item(i).setText('->'.join([
                f'Point{p0}',
                f'Point{p1}',
                f"{vpoints[p0].slope_angle(vpoints[p1]):.02f}",
            ]))
        self.__dial_ok()
        self.solve()

    @pyqtSlot(bool, name='on_variable_play_toggled')
    def __play(self, toggled: bool):
        """Triggered when play button was changed."""
        self.dial.setEnabled(not toggled)
        self.dial_spinbox.setEnabled(not toggled)
        if toggled:
            self.inputs_playShaft.start()
        else:
            self.inputs_playShaft.stop()
            if self.update_pos_option.isChecked():
                self.setCoordsAsCurrent()

    @pyqtSlot()
    def __change_index(self):
        """QTimer change index."""
        index = self.dial.value()
        speed = self.variable_speed.value()
        extreme_rebound = (
            self.conflict.isVisible() and
            self.extremeRebound.isChecked()
        )
        if extreme_rebound:
            speed = -speed
            self.variable_speed.setValue(speed)
        index += int(speed * 6 * (3 if extreme_rebound else 1))
        index %= self.dial.maximum()
        self.dial.setValue(index)

    @pyqtSlot(bool, name='on_record_start_toggled')
    def __start_record(self, toggled: bool):
        """Save to file path data."""
        if toggled:
            self.MainCanvas.recordStart(int(
                self.dial_spinbox.maximum() / self.record_interval.value()
            ))
            return
        path = self.MainCanvas.getRecordPath()
        name, ok = QInputDialog.getText(
            self,
            "Recording completed!",
            "Please input name tag:"
        )
        i = 0
        name = name or f"Record_{i}"
        while name in self.__path_data:
            name = f"Record_{i}"
            i += 1
        QMessageBox.information(
            self,
            "Record",
            "The name tag is being used or empty."
        )
        self.addPath(name, path)

    def addPath(self, name: str, path: Sequence[Tuple[float, float]]):
        """Add path function."""
        self.CommandStack.beginMacro(f"Add {{Path: {name}}}")
        self.CommandStack.push(AddPath(
            self.record_list,
            name,
            self.__path_data,
            path
        ))
        self.CommandStack.endMacro()
        self.record_list.setCurrentRow(self.record_list.count() - 1)

    def loadPaths(self, paths: Dict[str, Sequence[Tuple[float, float]]]):
        """Add multiple path."""
        for name, path in paths.items():
            self.addPath(name, path)

    @pyqtSlot(name='on_record_remove_clicked')
    def __remove_path(self):
        """Remove path data."""
        row = self.record_list.currentRow()
        if not row > 0:
            return
        name = self.record_list.item(row).text()
        self.CommandStack.beginMacro(f"Delete {{Path: {name}}}")
        self.CommandStack.push(DeletePath(
            row,
            self.record_list,
            self.__path_data
        ))
        self.CommandStack.endMacro()
        self.record_list.setCurrentRow(self.record_list.count() - 1)
        self.reloadCanvas()

    @pyqtSlot(QListWidgetItem, name='on_record_list_itemDoubleClicked')
    def __path_dlg(self, item: QListWidgetItem):
        """View path data."""
        name = item.text().split(":")[0]
        try:
            data = self.__path_data[name]
        except KeyError:
            return
        points_text = ", ".join(f"Point{i}" for i in range(len(data)))
        reply = QMessageBox.question(
            self,
            "Path data",
            f"This path data including {points_text}.",
            (QMessageBox.Save | QMessageBox.Close),
            QMessageBox.Close
        )
        if reply != QMessageBox.Save:
            return
        file_name = self.outputTo(
            "path data",
            ["Comma-Separated Values (*.csv)", "Text file (*.txt)"]
        )
        if not file_name:
            return
        with open(file_name, 'w', newline='') as stream:
            writer = csv.writer(stream)
            for point in data:
                for coordinate in point:
                    writer.writerow(coordinate)
                writer.writerow(())
        print(f"Output path data: {file_name}")

    @pyqtSlot(QPoint)
    def __record_list_context_menu(self, point):
        """Show the context menu.

        Show path [0], [1], ...
        Or copy path coordinates.
        """
        row = self.record_list.currentRow()
        if not row > -1:
            return
        showall_action = self.pop_menu_record_list.addAction("Show all")
        showall_action.index = -1
        copy_action = self.pop_menu_record_list.addAction("Copy as new")
        name = self.record_list.item(row).text().split(':')[0]
        try:
            data = self.__path_data[name]
        except KeyError:
            # Auto preview path.
            data = self.MainCanvas.Path.path
            showall_action.setEnabled(False)
        else:
            for action_text in ("Show", "Copy data from"):
                self.pop_menu_record_list.addSeparator()
                for i in range(len(data)):
                    if data[i]:
                        action = self.pop_menu_record_list.addAction(
                            f"{action_text} Point{i}"
                        )
                        action.index = i
        action_exec = self.pop_menu_record_list.exec_(
            self.record_list.mapToGlobal(point)
        )
        if action_exec:
            if action_exec == copy_action:
                # Copy path data.
                num = 0
                name_copy = f"{name}_{num}"
                while name_copy in self.__path_data:
                    name_copy = f"{name}_{num}"
                    num += 1
                self.addPath(name_copy, data)
            elif "Copy data from" in action_exec.text():
                # Copy data to clipboard.
                QApplication.clipboard().setText('\n'.join(
                    f"{x},{y}" for x, y in data[action_exec.index]
                ))
            elif "Show" in action_exec.text():
                # Switch points enabled status.
                if action_exec.index == -1:
                    self.record_show.setChecked(True)
                self.MainCanvas.setPathShow(action_exec.index)
        self.pop_menu_record_list.clear()

    @pyqtSlot(bool, name='on_record_show_toggled')
    def __set_path_show(self, toggled: bool):
        """Show all paths or hide."""
        self.MainCanvas.setPathShow(-1 if toggled else -2)

    @pyqtSlot(int, name='on_record_list_currentRowChanged')
    def __set_path(self, _: int):
        """Reload the canvas when switch the path."""
        if not self.record_show.isChecked():
            self.record_show.setChecked(True)
        self.reloadCanvas()

    def currentPath(self):
        """Return current path data to main canvas.

        + No path.
        + Show path data.
        + Auto preview.
        """
        row = self.record_list.currentRow()
        if row in {0, -1}:
            return ()
        path_name = self.record_list.item(row).text().split(':')[0]
        return self.__path_data.get(path_name, ())

    @pyqtSlot(name='on_variable_up_clicked')
    @pyqtSlot(name='on_variable_down_clicked')
    def __set_variable_priority(self):
        row = self.variable_list.currentRow()
        if not row > -1:
            return
        item = self.variable_list.currentItem()
        self.variable_list.insertItem(
            row + (-1 if self.sender() == self.variable_up else 1),
            self.variable_list.takeItem(row)
        )
        self.variable_list.setCurrentItem(item)