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()
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()
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\"><...></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', ]))
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)
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
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)
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
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()
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()
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)
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([])
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)
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: ...
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)
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
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()
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([])