Beispiel #1
0
class GraphEnumerateThread(BaseThread):
    """Graphs enumeration thread."""

    progress_update = Signal(int)
    count_update = Signal(QTreeWidgetItem, int)
    result = Signal(list)

    def __init__(self, jobs: Sequence[QTreeWidgetItem], degenerate: int,
                 parent: QWidget):
        super(GraphEnumerateThread, self).__init__(parent)
        self.jobs = jobs
        self.degenerate = degenerate

    def run(self):
        """Run and return conventional graph."""
        cg_list = {}
        answers = []
        for i, item in enumerate(self.jobs):
            if self.is_stop:
                break

            root = item.parent()
            la = assortment_eval(root.text(0))
            cla = assortment_eval(item.text(0))
            if la not in cg_list:
                cg_list[la] = contracted_graph(la, lambda: self.is_stop)

            answer = conventional_graph(cg_list[la], cla, self.degenerate,
                                        lambda: self.is_stop)
            self.count_update.emit(item, len(answer))
            answers.extend(answer)
            self.progress_update.emit(1 + i)

        self.result.emit(answers)
        self.finished.emit()
Beispiel #2
0
class LinkSynthesisThread(BaseThread):
    """Link assortment synthesis thread."""

    progress_update = Signal(int)
    result = Signal(dict)
    size_update = Signal(int)

    def __init__(self, nl: int, nj: int, parent: QWidget):
        super(LinkSynthesisThread, self).__init__(parent)
        self.nl = nl
        self.nj = nj

    def run(self):
        """Run and return contracted link assortment."""
        try:
            la_list = link_synthesis(self.nl, self.nj, lambda: self.is_stop)
        except ValueError:
            self.progress_update.emit(1)
            self.result.emit({})
            self.finished.emit()
            return

        self.size_update.emit(len(la_list))
        assortment = {}
        for i, la in enumerate(la_list):
            if self.is_stop:
                break
            assortment[la] = contracted_link_synthesis(la,
                                                       lambda: self.is_stop)
            self.progress_update.emit(1 + i)
        self.result.emit(assortment)
        self.finished.emit()
Beispiel #3
0
class PandocTransformThread(QThread):
    """Transform from Pandoc to normal markdown."""

    send = Signal(str)

    def __init__(self, doc: str, parent: QWidget):
        super(PandocTransformThread, self).__init__(parent)
        self.doc = doc

    def run(self):
        self.doc = self.doc.replace('@others',
                                    "<p style=\"color:red\">&lt;...&gt;</p>")
        self.doc = re.sub(r"\$\$([^$]+)\$\$", r"```\1```", self.doc)
        self.doc = re.sub(r"\$([^$\n\r]+)\$", r"`\1`", self.doc)
        self.doc = re.sub(r"\[@[\w-]+\]", r"\[99\]", self.doc)
        self.doc = re.sub(r"{@(\w+):[\w-]+}", r"\1. 99", self.doc)
        self.usleep(1)
        self.send.emit(f"<style>{CODE_STYLE}</style>" +
                       markdown(self.doc,
                                extras=[
                                    'numbering',
                                    'tables',
                                    'metadata',
                                    'fenced-code-blocks',
                                    'cuddled-lists',
                                    'tag-friendly',
                                ]))
Beispiel #4
0
class XStream(QObject):
    """Stream object to imitate Python output."""

    __stdout: Optional[XStream] = None
    __stderr: Optional[XStream] = None
    message_written = Signal(str)

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

    @staticmethod
    def replaced() -> bool:
        return XStream.__stdout is not None

    @staticmethod
    def stdout() -> XStream:
        """Replace stdout."""
        if not XStream.replaced():
            XStream.__stdout = XStream()
            sys.stdout = XStream.__stdout
            logger.removeHandler(_std_handler)
        return XStream.__stdout

    @staticmethod
    def back():
        """Disconnect from Qt widget."""
        sys.stdout = _SYS_STDOUT
        sys.stderr = _SYS_STDERR
        XStream.__stdout = None
        XStream.__stderr = None
        logger.addHandler(_std_handler)
Beispiel #5
0
class XStream(QObject):
    """Stream object to imitate Python output."""

    _stdout = None
    _stderr = None
    message_written = Signal(str)

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

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

    @staticmethod
    def stderr():
        """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
Beispiel #6
0
class LoadCommitButton(QPushButton):
    """The button of load commit."""

    loaded = Signal(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)
Beispiel #7
0
class FileKeeper(QThread):

    """File keeper thread for each file."""

    file_changed = Signal(str, QTreeWidgetItem)

    def __init__(self, nodes: Sequence[QTreeWidgetItem], parent: QWidget):
        super(FileKeeper, self).__init__(parent)
        self.finished.connect(self.deleteLater)
        self.nodes = {}
        self.files = {}
        for node in nodes:
            path = getpath(node)
            self.nodes[path] = node
            self.files[path] = stat(path).st_mtime
        self.stopped = False
        self.passed = False

    def run(self):
        """Watch the files."""
        while not self.stopped:
            for f in self.files:
                if self.passed or not isfile(f):
                    continue
                stemp = stat(f).st_mtime
                if self.files[f] != stemp:
                    self.file_changed.emit(f, self.nodes[f])
                    self.files[f] = stemp
            self.msleep(1)

    @Slot()
    def stop(self):
        self.stopped = True

    def set_passed(self, passed: bool):
        """Skip checking."""
        if not passed:
            for f in self.files:
                self.files[f] = stat(f).st_mtime
        self.passed = passed
Beispiel #8
0
class TextEditor(QsciScintilla):

    """QScintilla text editor."""

    word_changed = Signal()

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

        # Set the default font.
        if system() == "Linux":
            font_name = "DejaVu Sans Mono"
        elif system() == "Windows":
            font_name = "Courier New"
        elif system() == "Darwin":
            font_name = "Andale Mono"
        else:
            font_name = "Courier New"
        self.font = QFont(font_name)
        self.font.setFixedPitch(True)
        self.font.setPointSize(14)
        self.setFont(self.font)
        self.setMarginsFont(self.font)
        self.setUtf8(True)
        self.setEolMode(QsciScintilla.EolUnix)

        # 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(2)

        # 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)
        self.word = ""

        # Undo redo
        self.__set_command(QsciCommand.Redo, Qt.ControlModifier | Qt.ShiftModifier | Qt.Key_Z)

    def __set_command(self, command_type: int, shortcut: int):
        """Set editor shortcut to replace the default setting."""
        commands: QsciCommandSet = self.standardCommands()
        command = commands.boundTo(shortcut)
        if command is not None:
            command.setKey(0)
        command: QsciCommand = commands.find(command_type)
        command.setKey(shortcut)

    @Slot(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)
        _, _, self.word = self.__word_at_pos(pos)
        for m in _finditer(r'\b' + self.word + r'\b', self.text(), re.IGNORECASE):
            self.fillIndicatorRange(
                *self.lineIndexFromPosition(m.start()),
                *self.lineIndexFromPosition(m.end()),
                1
            )

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

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

    def setSelection(self, p1: int, p2: int, p3: Optional[int] = None, p4: Optional[int] = None):
        if p3 is p4 is None:
            line1, index1 = self.lineIndexFromPosition(p1)
            line2, index2 = self.lineIndexFromPosition(p2)
            super(TextEditor, self).setSelection(line1, index1, line2, index2)
        else:
            super(TextEditor, self).setSelection(p1, p2, p3, p4)

    @Slot(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.__refactor)
        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 the start and end pos of current word."""
        return (
            self.SendScintilla(QsciScintilla.SCI_WORDSTARTPOSITION, pos, True),
            self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, pos, True),
            self.wordAtLineIndex(*self.getCursorPosition())
        )

    @Slot()
    def __refactor(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 _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())
        if pos + 1 > self.length():
            return ""
        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 in {"Python", "C++"}:
            parentheses.extend(_parentheses_code)
        if self.lexer_option in {"Markdown", "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:
            if len(selected_text) == 1 and not selected_text.isalnum():
                pass
            elif selected_text[0].isalnum() == selected_text[-1].isalnum():
                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()

        # Remove leading spaces when create newline.
        if key in {Qt.Key_Return, Qt.Key_Enter}:
            if len(doc_pre) - len(doc_pre.lstrip(" ")) == 0:
                line, _ = self.getCursorPosition()
                doc_post = self.text(line)
                while 0 < len(doc_post) - len(doc_post.lstrip(" ")):
                    self.unindent(line)
                    doc_post = self.text(line)
            return

        # Auto close of parentheses.
        if not (selected_text or self.__cursor_next_char().isalnum()):
            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 and self.__cursor_next_char() != " ":
                self.insert(" ")
                self.__cursor_move_next()
                return

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

    def spell_check_all(self):
        """Spell check for all text."""
        self.__clear_indicator_all(0)
        for start, end in _spell_check(self.text()):
            line1, index1 = self.lineIndexFromPosition(start)
            line2, index2 = self.lineIndexFromPosition(end)
            self.fillIndicatorRange(line1, index1, line2, index2, 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'
        self.selectAll()
        self.replaceSelectedText(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()
Beispiel #9
0
class _ConfigureCanvas(PreviewCanvas):
    """Customized preview window has some functions of mouse interaction.

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

    edit_size = 1000
    set_joint_number = Signal(int)

    def __init__(self, parent: QWidget):
        """Add a function use to get current point from parent."""
        super(_ConfigureCanvas, self).__init__(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
        hv = _ConfigureCanvas.edit_size / 2

        if -hv <= mx <= hv:
            self.pos[row] = (mx, self.pos[row][1])
        else:
            if -hv <= mx:
                x = hv
            else:
                x = -hv
            self.pos[row] = (x, self.pos[row][1])

        if -hv <= my <= hv:
            self.pos[row] = (self.pos[row][0], my)
        else:
            if -hv <= my:
                y = hv
            else:
                y = -hv
            self.pos[row] = (self.pos[row][0], y)

        self.update()
Beispiel #10
0
class InputsWidget(QWidget, Ui_Form):

    """There has following functions:

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

    about_to_resolve = Signal()

    def __init__(self, parent: MainWindowBase):
        super(InputsWidget, self).__init__(parent)
        self.setupUi(self)

        # parent's function pointer.
        self.free_move_button = parent.free_move_button
        self.EntitiesPoint = parent.entities_point
        self.EntitiesLink = parent.entities_link
        self.MainCanvas = parent.main_canvas
        self.solve = parent.solve
        self.reload_canvas = parent.reload_canvas
        self.output_to = parent.output_to
        self.conflict = parent.conflict
        self.dof = parent.dof
        self.right_input = parent.right_input
        self.CommandStack = parent.command_stack
        self.set_coords_as_current = parent.set_coords_as_current

        # 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))

        # Angle panel available 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.variable_value_reset)

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

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

        # Inputs record context menu
        self.pop_menu_record_list = QMenu(self)
        self.record_list.customContextMenuRequested.connect(
            self.__record_list_context_menu
        )
        self.__path_data: Dict[str, Sequence[_Coord]] = {}

    def clear(self):
        """Clear function to reset widget status."""
        self.__path_data.clear()
        for _ 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 path_data(self):
        """Return current path data."""
        return self.__path_data

    @Slot(tuple)
    def set_selection(self, selections: Sequence[int]):
        """Set one selection from canvas."""
        self.joint_list.setCurrentRow(selections[0])

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

    @Slot(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.data_tuple()
        type_int = vpoints[p0].type
        if type_int == VJoint.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.type_str}] Point{i}")
        elif type_int in {VJoint.P, VJoint.RP}:
            self.driver_list.addItem(f"[{vpoints[p0].type_str}] Point{p0}")

    @Slot(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)

    @Slot(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.input_count():
            QMessageBox.warning(
                self,
                "Wrong DOF",
                "The number of variable must no more than degrees of freedom."
            )
            return

        # Check same link.
        vpoints = self.EntitiesPoint.data_tuple()
        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.input_pairs():
            if {p0, p1} == {p0_, p1_} and vpoints[p0].type == VJoint.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 add_inputs_variables(self, variables: Sequence[Tuple[int, int]]):
        """Add from database."""
        for p0, p1 in variables:
            self.__add_inputs_variable(p0, p1)

    @Slot()
    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.right_input()
        )
        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 variable_excluding(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.input_pairs()):
            # 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()

    @Slot(name='on_variable_remove_clicked')
    def remove_var(self, row: int = -1):
        """Remove and reset angle."""
        if row == -1:
            row = self.variable_list.currentRow()
        if not row > -1:
            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.get_back_position()
        self.solve()

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

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

    def input_pairs(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 variable_reload(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.variable_value_reset()

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

    @Slot(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.about_to_resolve.emit()
        if (
            self.record_start.isChecked() and
            abs(self.oldVar - value) > self.record_interval.value()
        ):
            self.MainCanvas.record_path()
            self.oldVar = value

    def variable_value_reset(self):
        """Reset the value of QDial."""
        if self.inputs_play_shaft.isActive():
            self.variable_play.setChecked(False)
            self.inputs_play_shaft.stop()
        self.EntitiesPoint.get_back_position()
        vpoints = self.EntitiesPoint.data_tuple()
        for i, (p0, p1, a) in enumerate(self.input_pairs()):
            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()

    @Slot(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_play_shaft.start()
        else:
            self.inputs_play_shaft.stop()
            if self.update_pos_option.isChecked():
                self.set_coords_as_current()

    @Slot()
    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)

    @Slot(bool, name='on_record_start_toggled')
    def __start_record(self, toggled: bool):
        """Save to file path data."""
        if toggled:
            self.MainCanvas.record_start(int(
                self.dial_spinbox.maximum() / self.record_interval.value()
            ))
            return
        path = self.MainCanvas.get_record_path()
        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.add_path(name, path)

    def add_path(self, name: str, path: Sequence[_Coord]):
        """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 load_paths(self, paths: Dict[str, Sequence[_Coord]]):
        """Add multiple path."""
        for name, path in paths.items():
            self.add_path(name, path)

    @Slot(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.reload_canvas()

    @Slot(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)))
        if QMessageBox.question(
            self,
            "Path data",
            f"This path data including {points_text}.",
            (QMessageBox.Save | QMessageBox.Close),
            QMessageBox.Close
        ) != QMessageBox.Save:
            return

        file_name = self.output_to(
            "path data",
            ["Comma-Separated Values (*.csv)", "Text file (*.txt)"]
        )
        if not file_name:
            return

        with open(file_name, 'w', encoding='utf-8', newline='') as stream:
            writer = csv.writer(stream)
            for point in data:
                for coordinate in point:
                    writer.writerow(coordinate)
                writer.writerow(())
        logger.info(f"Output path data: {file_name}")

    @Slot(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.add_path(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.set_path_show(action_exec.index)
        self.pop_menu_record_list.clear()

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

    @Slot(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.reload_canvas()

    def current_path(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, ())

    @Slot(name='on_variable_up_clicked')
    @Slot(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)
Beispiel #11
0
class BaseTableWidget(QTableWidget, Generic[_Data], metaclass=QABCMeta):

    """Two tables has some shared function."""

    row_selection_changed = Signal(list)
    delete_request = Signal()

    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))

        # Table widget column width.
        self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)

        @Slot()
        def __emit_selection_changed():
            self.row_selection_changed.emit(self.selected_rows())

        self.itemSelectionChanged.connect(__emit_selection_changed)

    def row_text(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.effective_range(has_name):
            item = self.item(row, column)
            if item is None:
                texts.append('')
            else:
                texts.append(item.text())
        return texts

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

    @abstractmethod
    def item_data(self, row: int) -> _Data:
        """Return a table data by row index."""
        ...

    def data(self) -> Iterator[_Data]:
        """Return table data in subclass."""
        yield from (self.item_data(row) for row in range(self.rowCount()))

    def data_tuple(self) -> Tuple[_Data, ...]:
        """Return data set as a container."""
        return tuple(self.data())

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

    def selectAll(self):
        """Override method of select all function."""
        self.setFocus(Qt.ShortcutFocusReason)
        super(BaseTableWidget, self).selectAll()

    def set_selections(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.__set_selected_ranges(
                selections,
                is_continue=continue_select,
                un_select=not_select
            )
        else:
            self.__set_selected_ranges(
                selections,
                is_continue=(keyboard_modifiers == Qt.ShiftModifier),
                un_select=False
            )

    def __set_selected_ranges(
        self,
        selections: Sequence[int],
        *,
        is_continue: bool,
        un_select: bool
    ):
        """Different mode of select function."""
        selected_rows = self.selected_rows()
        if not is_continue:
            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.delete_request.emit()

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

    @Slot()
    def clearSelection(self):
        """Overridden the 'clear_selection' slot to emit 'row_selection_changed'"""
        super(BaseTableWidget, self).clearSelection()
        self.row_selection_changed.emit([])
Beispiel #12
0
class DatabaseWidget(QWidget, Ui_Form):
    """The table that stored workbook data and changes."""

    load_id = Signal(int)

    def __init__(self, parent: MainWindowBase):
        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.check_file_changed
        # Check workbook saved function.
        self.__workbook_saved = parent.workbook_saved

        # Call to get point expressions.
        self.__point_expr_func = parent.entities_point.expression
        # Call to get link data.
        self.__link_expr_func = parent.entities_link.colors
        # Call to get storage data.
        self.__storage_data_func = parent.get_storage
        # Call to get collections data.
        self.__collect_data_func = parent.collection_tab_page.collect_data
        # Call to get triangle data.
        self.__triangle_data_func = parent.collection_tab_page.triangle_data
        # Call to get inputs variables data.
        self.__inputs_data_func = parent.inputs_widget.input_pairs
        # Call to get algorithm data.
        self.__algorithm_data_func = lambda: parent.dimensional_synthesis.mechanism_data
        # Call to get path data.
        self.__path_data_func = parent.inputs_widget.path_data

        # Add empty links function.
        self.__add_links_func = parent.add_empty_links
        # Parse function.
        self.__parse_func = parent.parse_expression

        # Call to load inputs variables data.
        self.__load_inputs_func = parent.inputs_widget.add_inputs_variables
        # Add storage function.
        self.__add_storage_func = parent.add_multiple_storage
        # Call to load paths.
        self.__load_path_func = parent.inputs_widget.load_paths
        # Call to load collections data.
        self.__load_collect_func = parent.collection_tab_page.structure_widget.add_collections
        # Call to load triangle data.
        self.__load_triangle_func = parent.collection_tab_page.configure_widget.add_collections
        # Call to load algorithm results.
        self.__load_algorithm_func = parent.dimensional_synthesis.load_results

        # 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.command_stack.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 set_file_name(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)

    @Slot()
    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)
            logger.debug("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 error:
                logger.error(error)
                _db.rollback()
                is_error = True
            else:
                self.history_commit = CommitModel.select().order_by(
                    CommitModel.id)
        if is_error:
            os_remove(file_name)
            logger.critical("The file was removed.")
            return
        self.read(file_name)
        logger.debug(f"Saving \"{file_name}\" successful.")
        size = QFileInfo(file_name).size()
        logger.debug("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)
        logger.debug(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 import_mechanism(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

        # Clear first
        self.__clear_func()

        # Load the commit to widgets.
        dlg = QProgressDialog(f"Loading commit # {commit.id}.", "Cancel", 0, 8,
                              self)
        dlg.setLabelText(f"Setting commit ...")
        dlg.show()
        self.load_id.emit(commit.id)
        self.commit_current_id.setValue(commit.id)
        self.branch_current.setText(commit.branch.name)

        # Expression
        dlg.setValue(1)
        dlg.setLabelText("Loading mechanism ...")
        self.__add_links_func(_decompress(commit.linkcolor))
        self.__parse_func(_decompress(commit.mechanism))

        # Inputs data
        dlg.setValue(2)
        dlg.setLabelText("Loading input data ...")
        input_data: Sequence[Tuple[int, int]] = _decompress(commit.inputsdata)
        self.__load_inputs_func(input_data)

        # Storage
        dlg.setValue(3)
        dlg.setLabelText("Loading storage ...")
        storage_data: List[Tuple[str, str]] = _decompress(commit.storage)
        self.__add_storage_func(storage_data)

        # Path data
        dlg.setValue(4)
        dlg.setLabelText("Loading paths ...")
        path_data: Dict[str,
                        Sequence[Tuple[float,
                                       float]]] = _decompress(commit.pathdata)
        self.__load_path_func(path_data)

        # Collection data
        dlg.setValue(5)
        dlg.setLabelText("Loading graph collections ...")
        collection_data: List[Tuple[Tuple[int, int],
                                    ...]] = _decompress(commit.collectiondata)
        self.__load_collect_func(collection_data)

        # Configuration data
        dlg.setValue(6)
        dlg.setLabelText("Loading synthesis configurations ...")
        config_data: Dict[str, Dict[str,
                                    Any]] = _decompress(commit.triangledata)
        self.__load_triangle_func(config_data)

        # Algorithm data
        dlg.setValue(7)
        dlg.setLabelText("Loading synthesis configurations ...")
        algorithm_data: List[Dict[str,
                                  Any]] = _decompress(commit.algorithmdata)
        self.__load_algorithm_func(algorithm_data)

        # Workbook loaded
        dlg.setValue(8)
        dlg.deleteLater()
        self.__workbook_saved()

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

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

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

    def load_example(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()
        logger.info(f"Example \"{example_name}\" has been loaded.")
        return True

    @Slot(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())))

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

    @Slot(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)

    @Slot(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()
        logger.info(f"Branch {branch_name} was deleted.")
        # Reload database.
        self.read(file_name)
Beispiel #13
0
class DynamicCanvasInterface(BaseCanvas, ABC):
    """Abstract class for wrapping main canvas class."""

    tracking = Signal(float, float)
    browse_tracking = Signal(float, float)
    selected = Signal(tuple, bool)
    selected_tips = Signal(QPoint, str)
    selected_tips_hide = Signal()
    free_moved = Signal(tuple)
    noselected = Signal()
    alt_add = Signal(float, float)
    doubleclick_edit = Signal(int)
    zoom_changed = Signal(int)
    fps_updated = Signal()
    set_target_point = Signal(float, float)

    @abstractmethod
    def __init__(self, parent: MainWindowBase):
        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 = SelectMode.Joint
        self.sr = 10
        self.selections: List[int] = []
        # Link transparency
        self.transparency = 1.
        # Show dimension
        self.show_dimension = False
        # Free move mode
        self.free_move = FreeMode.NoFreeMove
        # Path preview
        self.path_preview: List[List[_Coord]] = []
        self.slider_path_preview: Dict[int, List[_Coord]] = {}
        self.preview_path = parent.preview_path
        # 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 external 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 {VJoint.P, VJoint.RP}:
            pen = QPen(QColor(*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 == VJoint.P:
                    pen.setColor(Qt.black if self.monochrome else QColor(
                        *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.draw_point(i, cx, cy, grounded, vpoint.color)

            # Slider line
            pen.setColor(QColor(*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.draw_point(i, vpoint.cx, vpoint.cy, vpoint.grounded(),
                            vpoint.color)

        # For selects function.
        if self.select_mode == SelectMode.Joint 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[_Coord]:
        """Get geometry of the vlink."""
        points = []
        for i in vlink.points:
            vpoint = self.vpoints[i]
            if vpoint.type == VJoint.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 == SelectMode.Link
                and self.vlinks.index(vlink) in self.selections):
            pen.setWidth(self.link_width + 6)
            pen.setColor(Qt.black if self.monochrome else QColor(161, 16, 239))
            self.painter.setPen(pen)
            self.painter.drawPolygon(*qpoints)
        pen.setWidth(self.link_width)
        pen.setColor(Qt.black if self.monochrome else QColor(*vlink.color))
        self.painter.setPen(pen)
        brush = QColor(Qt.darkGray) if self.monochrome else 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.path_preview
        if len(self.vpoints) != len(paths):
            return
        if paths is self.path_preview:
            o_path = chain(enumerate(self.path_preview),
                           self.slider_path_preview.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.monochrome:
                color = Qt.gray
            elif self.vpoints[i].color is None:
                color = color_qt('Green')
            else:
                color = QColor(*self.vpoints[i].color)
            pen.setColor(color)
            pen.setWidth(self.path_width)
            self.painter.setPen(pen)
            if self.path.curve:
                self.draw_curve(path)
            else:
                self.draw_dot(path)

    def __emit_free_move(self, targets: Sequence[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 == SelectMode.Joint:

            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) and i not in self.selector.selection_rect:
                    self.selector.selection_rect.append(i)

        elif self.select_mode == SelectMode.Link:

            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 and catch(
                        vlink) and i not in self.selector.selection_rect:
                    self.selector.selection_rect.append(i)

        elif self.select_mode == SelectMode.Solution:

            def catch(exprs: Sequence[str]) -> bool:
                """Detection function for solution polygons."""
                points, _ = self.solution_polygon(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) and 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
        if num % snap_val >= snap_val / 2:
            times += 1
        return snap_val * times

    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.path_preview
            if paths is self.path_preview:
                o_path = chain(enumerate(self.path_preview),
                               self.slider_path_preview.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(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.draw_target_path()
            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 == SelectMode.Solution:
            for i, expr in enumerate(self.exprs):
                func = expr[0]
                params = expr[1:-1]
                target = expr[-1]
                self.draw_solution(func, params, target, self.vpoints)
                if i in self.selections:
                    pos, _ = self.solution_polygon(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 __mouse_pos(self, event) -> Tuple[float, float]:
        """Return the mouse position mapping to main canvas."""
        return (event.x() - self.ox) / self.zoom, (event.y() -
                                                   self.oy) / -self.zoom

    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, self.selector.y = self.__mouse_pos(event)
        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
            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.zoom_to_fit()
        elif button == Qt.LeftButton and (not self.show_target_path):
            self.selector.x, self.selector.y = self.__mouse_pos(event)
            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:
            km = QApplication.keyboardModifiers()
            self.selector.selection_old = list(self.selections)
            if (self.select_mode == SelectMode.Joint
                    and self.free_move != FreeMode.NoFreeMove):
                x, y = self.__mouse_pos(event)
                if self.selector.x != x and self.selector.y != y:
                    # Edit point coordinates.
                    self.__emit_free_move(self.selections)
                elif ((not self.selector.selection_rect)
                      and km != Qt.ControlModifier and km != Qt.ShiftModifier):
                    self.noselected.emit()
            else:
                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 km != Qt.ControlModifier and km != Qt.ShiftModifier):
                    self.noselected.emit()
        self.selected_tips_hide.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, y = self.__mouse_pos(event)
        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.free_move == FreeMode.NoFreeMove:
                if self.show_target_path:
                    self.set_target_point.emit(x, y)
                else:
                    # 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()

                    self.selected_tips.emit(
                        event.globalPos(),
                        f"({self.selector.x:.02f}, {self.selector.y:.02f})\n"
                        f"({self.selector.sx:.02f}, {self.selector.sy:.02f})\n"
                        f"{len(selection)} {_selection_unit[self.select_mode]}(s)"
                    )
            elif self.select_mode == SelectMode.Joint:
                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)
                    self.selected_tips.emit(
                        event.globalPos(), f"{mouse_x:+.02f}, {mouse_y:+.02f}")
                    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)
                    self.selected_tips.emit(event.globalPos(),
                                            f"{degrees(alpha):+.02f}°")
                    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 {VJoint.P, VJoint.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
                    self.selected_tips.emit(event.globalPos(),
                                            f"{fx:+d}, {fy:+d}")
                    for num in self.selections:
                        vpoint = self.vpoints[num]
                        if vpoint.type == VJoint.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 and self.selections:
                    self.update_preview_path()

            self.update()
        self.tracking.emit(x, y)

    def zoom_to_fit(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()

    @abstractmethod
    def update_preview_path(self) -> None:
        ...
Beispiel #14
0
class StructureWidget(QWidget, Ui_Form):
    """Structure widget.

    Preview the structures that was been added in collection list by user.
    """

    layout_sender = Signal(Graph, dict)

    def __init__(self, parent: MainWindowBase):
        """Get IO dialog functions from parent."""
        super(StructureWidget, self).__init__(parent)
        self.setupUi(self)
        self.output_to = parent.output_to
        self.save_reply_box = parent.save_reply_box
        self.input_from = parent.input_from
        self.addPointsByGraph = parent.add_points_by_graph
        self.unsaveFunc = parent.workbook_no_save
        self.is_monochrome = parent.monochrome_option.isChecked

        # Data structures.
        self.collections: List[Graph] = []
        self.collections_layouts: List[Dict[int, Tuple[float, float]]] = []
        self.collections_grounded: List[Graph] = []

        # Engine list.
        self.graph_engine.addItems(engines)

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

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

        self.clear()
        self.unsaveFunc()

    @Slot(name='on_graph_link_as_node_clicked')
    @Slot(name='on_graph_show_label_clicked')
    @Slot(name='on_reload_atlas_clicked')
    @Slot(int, name='on_graph_engine_currentIndexChanged')
    def __reload_atlas(self):
        """Reload atlas with the engine."""
        current_pos = self.collection_list.currentRow()
        self.collections_layouts.clear()
        self.collection_list.clear()
        self.__clear_selection()

        if not self.collections:
            return

        progress_dlg = QProgressDialog("Drawing atlas...", "Cancel", 0,
                                       len(self.collections), self)
        progress_dlg.setAttribute(Qt.WA_DeleteOnClose, True)
        progress_dlg.setWindowTitle("Type synthesis")
        progress_dlg.resize(400, progress_dlg.height())
        progress_dlg.setModal(True)
        progress_dlg.show()
        engine_str = self.graph_engine.currentText()
        for i, g in enumerate(self.collections):
            QCoreApplication.processEvents()
            if progress_dlg.wasCanceled():
                progress_dlg.deleteLater()
                return

            item = QListWidgetItem(f"No. {i + 1}")
            engine = engine_picker(g, engine_str,
                                   self.graph_link_as_node.isChecked())
            item.setIcon(
                to_graph(g,
                         self.collection_list.iconSize().width(), engine,
                         self.graph_link_as_node.isChecked(),
                         self.graph_show_label.isChecked(),
                         self.is_monochrome()))
            self.collections_layouts.append(engine)
            item.setToolTip(f"{g.edges}")
            self.collection_list.addItem(item)
            progress_dlg.setValue(i + 1)

        progress_dlg.deleteLater()
        self.collection_list.setCurrentRow(current_pos)

    def __is_valid_graph(self, g: Graph) -> str:
        """Test graph and return True if it is valid."""
        if not g.edges:
            return "is an empty graph"

        if not g.is_connected():
            return "is not a close chain"
        if not is_planar(g):
            return "is not a planar chain"
        if g.has_cut_link():
            return "has cut link"

        try:
            external_loop_layout(g, True)
        except ValueError as error:
            return str(error)

        for h in self.collections:
            if g.is_isomorphic(h):
                return f"is isomorphic with: {h.edges}"
        return ""

    def add_collection(self, edges: Sequence[Tuple[int, int]]):
        """Add collection by in put edges."""
        graph = Graph(edges)
        error = self.__is_valid_graph(graph)
        if error:
            QMessageBox.warning(self, "Add Collection Error",
                                f"Error: {error}")
            return
        self.collections.append(graph)
        self.unsaveFunc()
        self.__reload_atlas()

    def add_collections(self, collections: Sequence[Sequence[Tuple[int,
                                                                   int]]]):
        """Add collections."""
        for c in collections:
            self.add_collection(c)

    @Slot(name='on_add_by_edges_button_clicked')
    def __add_from_edges(self):
        """Add collection by input string."""
        edges_str = ""
        while not edges_str:
            edges_str, 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(edges_str)
            if any(len(edge) != 2 for edge in edges):
                raise ValueError("wrong format")
        except (SyntaxError, ValueError) as error:
            QMessageBox.warning(self, str(error), f"Error: {error}")
            return
        else:
            self.add_collection(edges)

    @Slot(name='on_add_by_files_button_clicked')
    def __add_from_files(self):
        """Append atlas by text files."""
        file_names = self.input_from("Edges data", ["Text File (*.txt)"],
                                     multiple=True)
        if not file_names:
            return

        read_data = []
        for file_name in file_names:
            with open(file_name, 'r', encoding='utf-8') as f:
                for line in f:
                    read_data.append(line)

        collections = []
        for edges in read_data:
            try:
                collections.append(Graph(eval(edges)))
            except (SyntaxError, TypeError):
                QMessageBox.warning(self, "Wrong format",
                                    "Please check the edges text format.")
                return
        if not collections:
            return
        self.collections += collections
        self.__reload_atlas()

    @Slot(name='on_capture_graph_clicked')
    def __save_graph(self):
        """Save the current graph."""
        if self.selection_window.count() != 1:
            return

        file_name = self.output_to("Atlas image", qt_image_format)
        if not file_name:
            return

        pixmap: QPixmap = self.selection_window.item(0).icon().pixmap(
            self.selection_window.iconSize())
        pixmap.save(file_name)
        self.save_reply_box("Graph", file_name)

    @Slot(name='on_save_atlas_clicked')
    def __save_atlas(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)
        if not ok:
            return

        file_name = self.output_to("Atlas image", qt_image_format)
        if not file_name:
            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(Qt.transparent)
        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(file_name)
        self.save_reply_box("Atlas", file_name)

    @Slot(name='on_save_edges_clicked')
    def __save_edges(self):
        """Save function as same as type synthesis widget."""
        count = self.collection_list.count()
        if not count:
            return
        file_name = self.output_to("Atlas edges expression",
                                   ["Text file (*.txt)"])
        if not file_name:
            return
        with open(file_name, 'w', encoding='utf-8') as f:
            f.write('\n'.join(str(G.edges) for G in self.collections))
        self.save_reply_box("edges expression", file_name)

    @Slot(int, name='on_collection_list_currentRowChanged')
    def __set_selection(self, row: int):
        """Show the data of collection.

        Save the layout position to keep the graphs
        will be in same appearance.
        """
        item: Optional[QListWidgetItem] = self.collection_list.item(row)
        has_item = item is not None
        self.delete_button.setEnabled(has_item)
        self.configure_button.setEnabled(has_item)
        self.selection_window.clear()
        if item is None:
            return

        # Preview item.
        link_is_node = self.graph_link_as_node.isChecked()
        item_preview = QListWidgetItem(item.text())
        row = self.collection_list.row(item)
        g = self.collections[row]
        self.ground_engine = self.collections_layouts[row]
        item_preview.setIcon(
            to_graph(g,
                     self.selection_window.iconSize().width(),
                     self.ground_engine, link_is_node,
                     self.graph_show_label.isChecked(), self.is_monochrome()))
        self.selection_window.addItem(item_preview)

        # Set attributes.
        self.edges_text.setText(str(list(g.edges)))
        self.nl_label.setText(str(len(g.nodes)))
        self.nj_label.setText(str(len(g.edges)))
        self.dof_label.setText(str(g.dof()))
        self.is_degenerate_label.setText(str(g.is_degenerate()))
        self.link_assortment_label.setText(str(l_a(g)))
        self.contracted_link_assortment_label.setText(str(c_l_a(g)))

        # "Link as node" layout cannot do these action.
        self.configure_button.setEnabled(not link_is_node)
        self.grounded_merge.setEnabled(not link_is_node)

        # Automatic ground.
        self.__grounded()

    def __clear_selection(self):
        """Clear the selection preview data."""
        self.grounded_list.clear()
        self.selection_window.clear()
        self.edges_text.clear()
        self.nl_label.setText('0')
        self.nj_label.setText('0')
        self.dof_label.setText('0')
        self.is_degenerate_label.setText("N/A")
        self.link_assortment_label.setText("N/A")
        self.contracted_link_assortment_label.setText("N/A")

    @Slot(name='on_expr_copy_clicked')
    def __copy_expr(self):
        """Copy the expression."""
        string = self.edges_text.text()
        if string:
            QApplication.clipboard().setText(string)
            self.edges_text.selectAll()

    @Slot(name='on_delete_button_clicked')
    def __delete_collection(self):
        """Delete the selected collection."""
        row = self.collection_list.currentRow()
        if not row > -1:
            return

        if QMessageBox.question(
                self, "Delete", f"Sure to remove #{row} from your collections?"
        ) != QMessageBox.Yes:
            return

        self.collection_list.takeItem(row)
        del self.collections[row]
        self.__clear_selection()
        self.unsaveFunc()

    @Slot(name='on_configure_button_clicked')
    def __configuration(self):
        """Triangular iteration."""
        self.layout_sender.emit(
            self.collections[self.collection_list.currentRow()],
            self.ground_engine.copy())

    def __grounded(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")
        icon = to_graph(g,
                        self.grounded_list.iconSize().width(),
                        self.ground_engine,
                        self.graph_link_as_node.isChecked(),
                        self.graph_show_label.isChecked(),
                        self.is_monochrome())
        item.setIcon(icon)
        self.collections_grounded.append(g)
        self.grounded_list.addItem(item)

        for node, graph_ in labeled_enumerate(g):
            item = QListWidgetItem(f"link_{node}")
            icon = to_graph(g,
                            self.grounded_list.iconSize().width(),
                            self.ground_engine,
                            self.graph_link_as_node.isChecked(),
                            self.graph_show_label.isChecked(),
                            self.is_monochrome(),
                            except_node=node)
            item.setIcon(icon)
            self.collections_grounded.append(graph_)
            self.grounded_list.addItem(item)

    @Slot(name='on_grounded_merge_clicked')
    def __grounded_merge(self):
        """Merge the grounded result."""
        item = self.grounded_list.currentItem()
        if not item:
            return

        graph = self.collections_grounded[0]
        text = item.text()
        if text == "Released":
            ground_link = None
        else:
            ground_link = int(text.split("_")[1])
        if QMessageBox.question(
                self, "Message",
                f"Merge \"{text}\" chain to your canvas?") == QMessageBox.Yes:
            self.addPointsByGraph(graph, self.ground_engine, ground_link)
Beispiel #15
0
class DataDict(QObject):

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

    not_saved = Signal()
    all_saved = Signal()

    def __init__(self):
        super(DataDict, self).__init__()
        self.__data: Dict[Hashable, str] = {}
        self.__saved: Dict[Hashable, bool] = {}
        self.__pos: Dict[Hashable, int] = {}
        self.__macros: Dict[str, Hashable] = {}

    def clear(self):
        """Clear data."""
        self.__data.clear()
        self.__saved.clear()
        self.__pos.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
        if not self.__saved[key]:
            self.not_saved.emit()
        self.__data[key] = context

    def __delitem__(self, key: Hashable):
        """Delete the key."""
        self.pop(key)

    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) -> ItemsView[Hashable, str]:
        """Items of data."""
        return self.__data.items()

    def pop(self, key: Hashable, k: _VT = None) -> Union[Hashable, _VT]:
        """Delete the key and return the value."""
        if key in self.__data:
            data = self.__data.pop(key)
            self.__saved.pop(key, None)
            self.__pos.pop(key, None)
            for m, code in tuple(self.__macros.items()):
                if code == key:
                    del self.__macros[m]
            return data
        return k

    def set_saved(self, key: Hashable, saved: bool):
        """Saved status adjustment."""
        self.__saved[key] = saved
        self.is_all_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."""
        all_saved = all(self.is_saved(key) for key in self.__data)
        if all_saved:
            self.all_saved.emit()
        else:
            self.not_saved.emit()
        return all_saved

    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) -> ItemsView[str, Hashable]:
        """Return macro scripts."""
        return self.__macros.items()

    def set_pos(self, key: Hashable, pos: int):
        """Set the scroll bar position of the data."""
        self.__pos[key] = pos

    def pos(self, key: Hashable) -> int:
        """Get the scroll bar position of the data."""
        if key in self.__pos:
            return self.__pos[key]
        else:
            return 0
Beispiel #16
0
class WorkerThread(BaseThread):
    """The QThread class to handle algorithm."""

    progress_update = Signal(int, str)
    result = Signal(dict)

    def __init__(self, type_num: AlgorithmType, mech_params: Dict[str, Any],
                 settings: Dict[str, Any], parent: QWidget):
        super(WorkerThread, self).__init__(parent)
        self.type_num = type_num
        self.mech_params = mech_params
        self.planar = Planar(self.mech_params)
        self.settings = settings
        self.loop = 1
        self.current_loop = 0
        self.fun = None

    def is_two_kernel(self) -> bool:
        return self.planar.is_two_kernel()

    def set_loop(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():
            logger.debug(f"- [P{name}] ({len(path)})")
        t0 = time()
        for self.current_loop in range(self.loop):
            logger.info(
                f"Algorithm [{self.current_loop + 1}]: {self.type_num}")
            if self.is_stop:
                # Cancel the remaining tasks.
                logger.info("Canceled.")
                continue
            self.result.emit(self.__algorithm())
        logger.info(f"total cost time: {time() - t0:.02f} [s]")
        self.finished.emit()

    def __algorithm(self) -> Dict[str, Any]:
        """Get the algorithm result."""
        t0 = time()
        expression, 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,
            'last_fitness': tf[-1][1],
            'interrupted': str(last_gen) if self.is_stop else 'False',
            'settings': self.settings,
            'hardware_info': {
                'os': f"{system()} {release()} {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['Expression'] = expression
        logger.info(f"cost time: {time_spend:.02f} [s]")
        return mechanism

    def __generate_process(self) -> Tuple[str, 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(
            self.planar,
            self.settings,
            progress_fun=self.progress_update.emit,
            interrupt_fun=lambda: self.is_stop,
        )
        return self.fun.run()
Beispiel #17
0
class PointTableWidget(BaseTableWidget[VPoint]):

    """Custom table widget for points."""

    selectionLabelUpdate = Signal(list)

    def __init__(self, parent: QWidget):
        super(PointTableWidget, self).__init__(0, (
            'Number',
            'Links',
            'Type',
            'Color',
            'X',
            'Y',
            'Current',
        ), parent)

    def item_data(self, row: int) -> VPoint:
        """Return data of VPoint."""
        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':
            j_type = VJoint.R
            angle = 0.
        else:
            angle = float(p_type[1])
            j_type = VJoint.P if p_type[0] == 'P' else VJoint.RP
        vpoint = VPoint([
            link for link in links.replace(" ", '').split(',') if link
        ], j_type, angle, color, x, y, color_rgb)
        vpoint.move(*self.current_position(row))
        return vpoint

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

    def edit_point(self, row: int, links: str, type_str: str, color: str, x: str, y: str):
        """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 current_position(self, row: int) -> List[_Coord]:
        """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("; ".join([f"({x:.06f}, {y:.06f})"] * 2))
            coords.append(coords[0])
        return coords

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

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

    def get_links(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 set_selections(self, selections: Sequence[int], key_detect: bool = False):
        """Need to update selection label on status bar."""
        super(PointTableWidget, self).set_selections(selections, key_detect)
        self.selectionLabelUpdate.emit(self.selected_rows())

    def effective_range(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)

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