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
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)
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()
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)
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()
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.")
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()
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())
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()
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)
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
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
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
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([], [])
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)
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)
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()
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)
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)