def __init__(self, parent: MainWindowBase) -> None: super(InputsWidget, self).__init__(parent) self.setupUi(self) # parent's function pointer self.free_move_button = parent.free_move_button self.entities_point = parent.entities_point self.entities_link = parent.entities_link self.vpoints = parent.vpoint_list self.vlinks = parent.vlink_list self.main_canvas = 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.command_stack = parent.command_stack self.set_coords_as_current = parent.set_coords_as_current self.get_back_position = parent.get_back_position # 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)) # Play button 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 createBottomRightGroupBox(self): self.bottomRightGroupBox = QGroupBox("Group 3") self.bottomRightGroupBox.setCheckable(True) self.bottomRightGroupBox.setChecked(True) lineEdit = QLineEdit('s3cRe7') lineEdit.setEchoMode(QLineEdit.Password) spinBox = QSpinBox(self.bottomRightGroupBox) spinBox.setValue(50) dateTimeEdit = QDateTimeEdit(self.bottomRightGroupBox) dateTimeEdit.setDateTime(QDateTime.currentDateTime()) slider = QSlider(Qt.Horizontal, self.bottomRightGroupBox) slider.setValue(40) scrollBar = QScrollBar(Qt.Horizontal, self.bottomRightGroupBox) scrollBar.setValue(60) dial = QDial(self.bottomRightGroupBox) dial.setValue(30) dial.setNotchesVisible(True) layout = QGridLayout() layout.addWidget(lineEdit, 0, 0, 1, 2) layout.addWidget(spinBox, 1, 0, 1, 2) layout.addWidget(dateTimeEdit, 2, 0, 1, 2) layout.addWidget(slider, 3, 0) layout.addWidget(scrollBar, 4, 0) layout.addWidget(dial, 3, 1, 2, 1) layout.setRowStretch(5, 1) self.bottomRightGroupBox.setLayout(layout)
def __init__(self, parent: QWidget): super(QRotatableView, self).__init__(parent) scene = QGraphicsScene(self) self.setScene(scene) self.dial = QDial() self.dial.setMinimumSize(QSize(150, 150)) self.dial.setSingleStep(100) self.dial.setPageStep(100) self.dial.setInvertedAppearance(True) self.dial.setWrapping(True) self.dial.setNotchTarget(0.1) self.dial.setNotchesVisible(True) self.dial.valueChanged.connect(self.__value_changed) self.set_maximum(360) graphics_item = scene.addWidget(self.dial) graphics_item.setRotation(-90) # make the QGraphicsView invisible. self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setFixedHeight(self.dial.height()) self.setFixedWidth(self.dial.width()) self.setStyleSheet("border: 0px;")
class QRotatableView(QGraphicsView): """Rotate QDial widget.""" value_changed = Signal(float) def __init__(self, parent: QWidget): super(QRotatableView, self).__init__(parent) scene = QGraphicsScene(self) self.setScene(scene) self.dial = QDial() self.dial.setMinimumSize(QSize(150, 150)) self.dial.setSingleStep(100) self.dial.setPageStep(100) self.dial.setInvertedAppearance(True) self.dial.setWrapping(True) self.dial.setNotchTarget(0.1) self.dial.setNotchesVisible(True) self.dial.valueChanged.connect(self.__value_changed) self.set_maximum(360) graphics_item = scene.addWidget(self.dial) graphics_item.setRotation(-90) # make the QGraphicsView invisible. self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setFixedHeight(self.dial.height()) self.setFixedWidth(self.dial.width()) self.setStyleSheet("border: 0px;") @Slot(int) def __value_changed(self, value: int) -> None: """Value changed signal.""" self.value_changed.emit(value / 100) def value(self) -> float: """Get value method.""" return self.dial.value() / 100 @Slot(float) def set_value(self, value: float) -> None: """Set value method.""" self.dial.setValue(int(value % 360 * 100)) def minimum(self) -> float: """Set maximum method.""" return self.dial.minimum() / 100 @Slot(float) def set_minimum(self, value: float) -> None: """Set minimum.""" self.dial.setMinimum(int(value * 100)) def maximum(self) -> float: """Set maximum method.""" return self.dial.maximum() / 100 @Slot(float) def set_maximum(self, value: float) -> None: """Set minimum.""" self.dial.setMaximum(int(value * 100)) def setEnabled(self, enabled: bool) -> None: """Set enabled.""" super(QRotatableView, self).setEnabled(enabled) self.dial.setEnabled(enabled)
def __init__(self): super().__init__() self.setWindowTitle("OkPlayer") icon = QIcon() icon.addPixmap(QPixmap("ok_64x64.ico"), QIcon.Normal, QIcon.Off) self.setWindowIcon(icon) self.recent_file_acts = [] self.init_menu() self.now = datetime.now() # Setting self.setting = {} self.load_setting() # Status bar self.learning_time_ms = 0 self.learning_time_ms_total = self.setting.get( "learning_time_ms_total", 0) self.status_bar = self.statusBar() self.label_learning_time = QLabel(self) self.label_learning_time.setAlignment(Qt.AlignRight) self.status_bar.addPermanentWidget(self.label_learning_time) self.label_learning_time.setText( f"Learning time: 00:00" f" / total {ms2min_sec(self.learning_time_ms_total)}") # Timer for learning time self.timer_learning_time = QTimer(self) self.timer_learning_time.timeout.connect(self.update_learning_time) self.timer_learning_time.setInterval(1000) # Player self.player = QMediaPlayer(self) self.player.mediaStatusChanged.connect(self.qmp_status_changed) self.player.positionChanged.connect(self.qmp_position_changed) self.player.setNotifyInterval(50) self.player.setVolume(50) self.player_buf = QBuffer() self.path_media = "" self.music_data = None self.duration_ms = 0 self.duration_str = "" # A/B Loop self.pos_loop_a = None self.pos_loop_b = None # Layout self.label_music = QLabel("No music", self) self.ico_play = qta.icon("fa.play") self.ico_pause = qta.icon("fa.pause") layout = QVBoxLayout() layout_volume = QHBoxLayout() layout_btn_progress = QVBoxLayout() layout_music_btns = QHBoxLayout() self.btn_rewind = QPushButton(qta.icon("fa.backward"), "", self) self.btn_rewind.clicked.connect(self.rewind) self.btn_play = QPushButton(self.ico_play, "", self) self.btn_play.clicked.connect(self.play) self.btn_fastforward = QPushButton(qta.icon("fa.forward"), "", self) self.btn_fastforward.clicked.connect(self.fastforward) self.btn_rewind.setFocusPolicy(Qt.NoFocus) self.btn_play.setFocusPolicy(Qt.NoFocus) self.btn_fastforward.setFocusPolicy(Qt.NoFocus) layout_music_btns.addWidget(self.btn_rewind) layout_music_btns.addWidget(self.btn_play) layout_music_btns.addWidget(self.btn_fastforward) layout_progress = QHBoxLayout() self.progressbar = MusicProgressBar(self) self.progressbar.sig_pb_pos.connect(self.set_media_position) self.elapsed_time = QLineEdit(f"00:00 / 00:00", self) self.elapsed_time.setReadOnly(True) self.elapsed_time.setAlignment(Qt.AlignHCenter) layout_progress.addWidget(self.progressbar) layout_progress.addWidget(self.elapsed_time) layout_btn_progress.addWidget(self.label_music) layout_btn_progress.addLayout(layout_music_btns) layout_btn_progress.addLayout(layout_progress) # Volume self.qdial_volume = QDial(self) self.qdial_volume.setMinimumWidth(110) self.qdial_volume.setWrapping(False) self.qdial_volume.setNotchesVisible(True) self.qdial_volume.setMinimum(0) self.qdial_volume.setMaximum(100) self.qdial_volume.setValue(self.player.volume()) self.qdial_volume.valueChanged.connect(self.qdial_changed) layout_volume.addLayout(layout_btn_progress) layout_volume.addWidget(self.qdial_volume) # Lyrics self.display_lyrics = LyricsDisplay(self) layout.addLayout(layout_volume) layout.addWidget(self.display_lyrics) central_widget = QWidget() central_widget.setLayout(layout) self.setCentralWidget(central_widget) # Auto Play self.update_recent_file_action() path = self.setting.get("LastPlayedPath", "") if osp.isfile(path): self.load_music_file(path) self.setFocus()
class MainWindow(QMainWindow): max_recent_files = 10 def __init__(self): super().__init__() self.setWindowTitle("OkPlayer") icon = QIcon() icon.addPixmap(QPixmap("ok_64x64.ico"), QIcon.Normal, QIcon.Off) self.setWindowIcon(icon) self.recent_file_acts = [] self.init_menu() self.now = datetime.now() # Setting self.setting = {} self.load_setting() # Status bar self.learning_time_ms = 0 self.learning_time_ms_total = self.setting.get( "learning_time_ms_total", 0) self.status_bar = self.statusBar() self.label_learning_time = QLabel(self) self.label_learning_time.setAlignment(Qt.AlignRight) self.status_bar.addPermanentWidget(self.label_learning_time) self.label_learning_time.setText( f"Learning time: 00:00" f" / total {ms2min_sec(self.learning_time_ms_total)}") # Timer for learning time self.timer_learning_time = QTimer(self) self.timer_learning_time.timeout.connect(self.update_learning_time) self.timer_learning_time.setInterval(1000) # Player self.player = QMediaPlayer(self) self.player.mediaStatusChanged.connect(self.qmp_status_changed) self.player.positionChanged.connect(self.qmp_position_changed) self.player.setNotifyInterval(50) self.player.setVolume(50) self.player_buf = QBuffer() self.path_media = "" self.music_data = None self.duration_ms = 0 self.duration_str = "" # A/B Loop self.pos_loop_a = None self.pos_loop_b = None # Layout self.label_music = QLabel("No music", self) self.ico_play = qta.icon("fa.play") self.ico_pause = qta.icon("fa.pause") layout = QVBoxLayout() layout_volume = QHBoxLayout() layout_btn_progress = QVBoxLayout() layout_music_btns = QHBoxLayout() self.btn_rewind = QPushButton(qta.icon("fa.backward"), "", self) self.btn_rewind.clicked.connect(self.rewind) self.btn_play = QPushButton(self.ico_play, "", self) self.btn_play.clicked.connect(self.play) self.btn_fastforward = QPushButton(qta.icon("fa.forward"), "", self) self.btn_fastforward.clicked.connect(self.fastforward) self.btn_rewind.setFocusPolicy(Qt.NoFocus) self.btn_play.setFocusPolicy(Qt.NoFocus) self.btn_fastforward.setFocusPolicy(Qt.NoFocus) layout_music_btns.addWidget(self.btn_rewind) layout_music_btns.addWidget(self.btn_play) layout_music_btns.addWidget(self.btn_fastforward) layout_progress = QHBoxLayout() self.progressbar = MusicProgressBar(self) self.progressbar.sig_pb_pos.connect(self.set_media_position) self.elapsed_time = QLineEdit(f"00:00 / 00:00", self) self.elapsed_time.setReadOnly(True) self.elapsed_time.setAlignment(Qt.AlignHCenter) layout_progress.addWidget(self.progressbar) layout_progress.addWidget(self.elapsed_time) layout_btn_progress.addWidget(self.label_music) layout_btn_progress.addLayout(layout_music_btns) layout_btn_progress.addLayout(layout_progress) # Volume self.qdial_volume = QDial(self) self.qdial_volume.setMinimumWidth(110) self.qdial_volume.setWrapping(False) self.qdial_volume.setNotchesVisible(True) self.qdial_volume.setMinimum(0) self.qdial_volume.setMaximum(100) self.qdial_volume.setValue(self.player.volume()) self.qdial_volume.valueChanged.connect(self.qdial_changed) layout_volume.addLayout(layout_btn_progress) layout_volume.addWidget(self.qdial_volume) # Lyrics self.display_lyrics = LyricsDisplay(self) layout.addLayout(layout_volume) layout.addWidget(self.display_lyrics) central_widget = QWidget() central_widget.setLayout(layout) self.setCentralWidget(central_widget) # Auto Play self.update_recent_file_action() path = self.setting.get("LastPlayedPath", "") if osp.isfile(path): self.load_music_file(path) self.setFocus() def init_menu(self): """Init menu.""" color_icon = "#87939A" menu_bar = self.menuBar() menu_bar.setNativeMenuBar(False) # Don't use mac native menu bar # File file_menu = menu_bar.addMenu("&File") # Open open_action = QAction(qta.icon("ei.folder-open", color=color_icon), "&Open", self) open_action.setShortcut("Ctrl+O") open_action.setStatusTip("Open file") open_action.triggered.connect(self.open_music_file) file_menu.addAction(open_action) file_menu.addSeparator() # Recent Files for i in range(MainWindow.max_recent_files): self.recent_file_acts.append( QAction(self, visible=False, triggered=self.load_recent_music)) for i in range(MainWindow.max_recent_files): file_menu.addAction(self.recent_file_acts[i]) file_menu.addSeparator() # Exit exit_action = QAction(qta.icon("mdi.exit-run", color=color_icon), "&Exit", self) exit_action.setShortcut("Ctrl+Q") exit_action.setStatusTip("Exit App") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # Help help_menu = menu_bar.addMenu("&Help") about_action = QAction( "&About", self, statusTip="Show the application's About box", triggered=self.about, ) help_menu.addAction(about_action) def about(self): """Show messagebox for about.""" QMessageBox.about( self, "About music player a/b loop", "The music player a/b loop is made by <b>ok97465</b>", ) def update_recent_file_action(self): """Update recent file action.""" files = self.setting.get("recent_files", []) num_recent_files = min(len(files), MainWindow.max_recent_files) for i in range(num_recent_files): text = osp.splitext(osp.basename(files[i]))[0] self.recent_file_acts[i].setText(text) self.recent_file_acts[i].setData(files[i]) self.recent_file_acts[i].setVisible(True) for j in range(num_recent_files, MainWindow.max_recent_files): self.recent_file_acts[j].setVisible(False) def open_music_file(self): """Open music file.""" self.stop() fname = QFileDialog.getOpenFileName( self, "Open music file", "/home/ok97465", filter="Music Files (*.mp3, *.m4a)", ) self.load_music_file(fname[0]) def load_music_file(self, path: str): """Load music file""" if not osp.isfile(path): return self.path_media = path path_lyrics = path[:-3] + "vtt" self.display_lyrics.read_vtt(path_lyrics) fp = io.BytesIO() self.music_data = AudioSegment.from_file(path) self.music_data.export(fp, format="wav") self.player_buf.setData(fp.getvalue()) self.player_buf.open(QIODevice.ReadOnly) self.player.setMedia(QMediaContent(), self.player_buf) def load_recent_music(self): """Load recent music.""" action = self.sender() if action: self.stop() self.load_music_file(action.data()) def load_setting(self): """Load setting file.""" try: with open("setting.json", "r") as fp: self.setting = json.load(fp) except FileNotFoundError: pass def keyPressEvent(self, event): key = event.key() shift = event.modifiers() & Qt.ShiftModifier if shift: if key == Qt.Key_O: self.adjust_ab_loop(-100) else: if key in [Qt.Key_H, Qt.Key_Left, Qt.Key_A]: self.rewind(ms=5000) elif key in [Qt.Key_L, Qt.Key_Right, Qt.Key_D]: self.fastforward(ms=5000) elif key in [Qt.Key_J]: self.rewind(ms=1000 * 38) elif key in [Qt.Key_K, Qt.Key_F]: self.fastforward(ms=1000 * 38) elif key == Qt.Key_Up: self.control_volume(5) elif key == Qt.Key_Down: self.control_volume(-5) elif key in [Qt.Key_I, Qt.Key_W, Qt.Key_Menu]: self.set_ab_loop() elif key == Qt.Key_O: self.adjust_ab_loop(500) elif key in [Qt.Key_Space, Qt.Key_Hangul_Hanja]: self.play() elif key in [Qt.Key_S]: self.save_ab_loop() elif key in [Qt.Key_Q, Qt.Key_U, Qt.Key_Slash]: self.send_AB_loop_lyrics_to_papago() super().keyPressEvent(event) def set_ab_loop(self): """Set A/B loop.""" if self.pos_loop_b: self.pos_loop_b = None self.pos_loop_a = None elif self.pos_loop_a: self.pos_loop_b = self.player.position() self.player.setPosition(self.pos_loop_a) else: self.pos_loop_a = self.player.position() self.progressbar.pos_loop_a = self.pos_loop_a self.progressbar.pos_loop_b = self.pos_loop_b self.progressbar.repaint() def adjust_ab_loop(self, offset_ms): """Adjust A/B loop.""" if self.pos_loop_b: self.pos_loop_b += offset_ms self.pos_loop_a += offset_ms def save_ab_loop(self): """Save A/B loop""" if self.pos_loop_b is None: return is_playing = False if self.player.state() == QMediaPlayer.PlayingState: is_playing = True if is_playing: self.player.pause() path_new = (self.path_media[:-4] + f"{self.pos_loop_a}_{self.pos_loop_b}" + self.path_media[-4:]) seg = self.music_data[self.pos_loop_a:self.pos_loop_b] seg.export(path_new, format="mp3") if is_playing: self.player.play() def play(self): """Play music file.""" if self.player.state() == QMediaPlayer.PlayingState: self.player.pause() self.btn_play.setIcon(self.ico_play) self.timer_learning_time.stop() else: self.player.play() self.btn_play.setIcon(self.ico_pause) self.timer_learning_time.start() def stop(self): """Stop.""" self.save_current_media_info() self.player.stop() self.player_buf.close() self.path_media = "" self.pos_loop_b = None self.pos_loop_a = None self.timer_learning_time.stop() self.label_music.setText("No music") self.btn_play.setIcon(self.ico_play) def control_volume(self, step: int): """Control volume.""" volume = self.player.volume() if step < 0: new_volume = max([0, volume + step]) else: new_volume = min([100, volume + step]) self.qdial_volume.setValue(new_volume) def navigate_media(self, ms: int): """Navigate the position of media.""" position_ms = self.player.position() if ms < 0: new_position_ms = max([0, position_ms + ms]) else: new_position_ms = min([self.duration_ms, position_ms + ms]) self.player.setPosition(new_position_ms) def rewind(self, ms: int = 5000): """Re-wind media of QMediaPlayer.""" self.navigate_media(ms * -1) def fastforward(self, ms: int = 5000): """fastfoward media of QMediaPlayer.""" self.navigate_media(ms) def qmp_status_changed(self): """Handle status of QMediaPlayer if the status is changed.""" status = self.player.mediaStatus() if status == QMediaPlayer.LoadedMedia and self.path_media: duration_ms = self.player.duration() self.duration_ms = duration_ms self.duration_str = ms2min_sec(duration_ms) self.elapsed_time.setText(f"00:00 / {self.duration_str}") self.progressbar.setMaximum(duration_ms) music_basename = osp.splitext(osp.basename(self.path_media))[0] self.label_music.setText(music_basename) self.player.play() # read previous position path = self.path_media position = self.setting.get(path, 0) self.player.setPosition(position) # update recent files files = self.setting.get("recent_files", []) try: files.remove(path) except ValueError: pass files.insert(0, path) del files[MainWindow.max_recent_files:] self.setting["recent_files"] = files self.update_recent_file_action() # Player state state = self.player.state() if state in [QMediaPlayer.PausedState, QMediaPlayer.StoppedState]: self.btn_play.setIcon(self.ico_play) self.timer_learning_time.stop() elif state == QMediaPlayer.PlayingState: self.btn_play.setIcon(self.ico_pause) self.timer_learning_time.start() def qmp_position_changed(self, position_ms: int): """Handle position of qmedia if the position is changed.""" if self.pos_loop_b: if (position_ms == self.duration_ms) or (self.pos_loop_b < position_ms): self.player.setPosition(self.pos_loop_a) self.progressbar.setValue(position_ms) self.elapsed_time.setText( f"{ms2min_sec(position_ms)} / {self.duration_str}") self.display_lyrics.update_media_pos(position_ms) def qdial_changed(self, pos: int): """Handle Qdial position.""" self.player.setVolume(pos) def send_AB_loop_lyrics_to_papago(self): """Send AB loop lyrics to papago.""" if not self.pos_loop_b: return lyrics = self.display_lyrics.get_lyrics_in_range( self.pos_loop_a, self.pos_loop_b) lyrics = lyrics.replace("\n", "") webbrowser.open(f"https://papago.naver.com/?sk=en&tk=ko&st={lyrics}", autoraise=False) @Slot(int) def set_media_position(self, position_ms: int): """Set the position of Qmedia.""" self.player.setPosition(position_ms) def save_current_media_info(self): """Save current media info to setting file.""" if not osp.isfile(self.path_media): return if self.path_media: position = self.player.position() self.setting[self.path_media] = position self.setting["LastPlayedPath"] = self.path_media def update_learning_time(self): """Update learning time.""" self.learning_time_ms += 1000 self.learning_time_ms_total += 1000 self.label_learning_time.setText( f"Learning time : {ms2min_sec(self.learning_time_ms)}" f" / total : {ms2min_sec(self.learning_time_ms_total)}") def closeEvent(self, event): """Save setting.""" self.stop() self.setting["learning_time_ms_total"] = self.learning_time_ms_total with open("setting.json", "w") as fp: json.dump(self.setting, fp, indent=2) now = self.now cur = sqlite3.connect("history.db") cur.execute("CREATE TABLE IF NOT EXISTS LearningTimeData(" "DayOfWeek INTEGER, " "month INTEGER, " "day INTEGER, " "timestamp REAL, " "LearningTime_ms INTEGER)") cur.execute( "insert into LearningTimeData Values (?,?,?,?,?)", (now.weekday(), now.month, now.day, now.timestamp(), self.learning_time_ms), ) cur.commit() cur.close()
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) -> None: super(InputsWidget, self).__init__(parent) self.setupUi(self) # parent's function pointer self.free_move_button = parent.free_move_button self.entities_point = parent.entities_point self.entities_link = parent.entities_link self.vpoints = parent.vpoint_list self.vlinks = parent.vlink_list self.main_canvas = 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.command_stack = parent.command_stack self.set_coords_as_current = parent.set_coords_as_current self.get_back_position = parent.get_back_position # 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)) # Play button 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) -> None: """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) -> None: """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) -> None: """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) -> Dict[str, Sequence[_Coord]]: """Return current path data.""" return self.__path_data @Slot(tuple) def set_selection(self, selections: Sequence[int]) -> None: """Set one selection from canvas.""" self.joint_list.setCurrentRow(selections[0]) @Slot() def clear_selection(self) -> None: """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, _=None) -> None: """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()) base_point = self.vpoints[p0] type_int = base_point.type if type_int == VJoint.R: for i, vpoint in enumerate(self.vpoints): if i == p0: continue if base_point.same_link(vpoint): if base_point.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"[{base_point.type_str}] Point{p0}") @Slot(int, name='on_driver_list_currentRowChanged') def __set_add_var_enabled(self, _=None) -> None: """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) -> 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 if not self.vpoints[p0].same_link(self.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 self.vpoints[p0].type == VJoint.R: QMessageBox.warning(self, "Wrong pair", "There already have a same pair.") return if p0 == p1: # One joint by offset value = self.vpoints[p0].true_offset() else: # Two joints by angle value = self.vpoints[p0].slope_angle(self.vpoints[p1]) self.command_stack.push( AddInput( '->'.join(( f'Point{p0}', f"Point{p1}", f"{value:.02f}", )), self.variable_list)) def add_inputs_variables(self, variables: Sequence[Tuple[int, int]]) -> None: """Add from database.""" for p0, p1 in variables: self.__add_inputs_variable(p0, p1) @Slot(QListWidgetItem, name='on_variable_list_itemClicked') def __dial_ok(self, _=None) -> None: """Set the angle of base link and drive link.""" if self.inputs_play_shaft.isActive(): return 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) -> 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.command_stack.push(DeleteInput(i, self.variable_list)) @Slot(name='on_variable_remove_clicked') def remove_var(self, row: int = -1) -> None: """Remove and reset angle.""" if row == -1: row = self.variable_list.currentRow() if not row > -1: return self.variable_stop.click() self.command_stack.push(DeleteInput(row, self.variable_list)) self.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) -> None: """Auto check the points and type.""" self.joint_list.clear() for i in range(self.entities_point.rowCount()): type_text = self.entities_point.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) -> None: self.dial.setValue(int(value * 100 % self.dial.maximum())) @Slot(int) def __update_var(self, value: int) -> None: """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.main_canvas.record_path() self.oldVar = value def variable_value_reset(self) -> None: """Reset the value of QDial.""" if self.inputs_play_shaft.isActive(): self.variable_play.setChecked(False) self.inputs_play_shaft.stop() self.get_back_position() for i, (p0, p1, a) in enumerate(self.input_pairs()): self.variable_list.item(i).setText('->'.join([ f'Point{p0}', f'Point{p1}', f"{self.vpoints[p0].slope_angle(self.vpoints[p1]):.02f}", ])) self.__dial_ok() self.solve() @Slot(bool, name='on_variable_play_toggled') def __play(self, toggled: bool) -> None: """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) -> None: """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) -> None: """Save to file path data.""" if toggled: self.main_canvas.record_start( int(self.dial_spinbox.maximum() / self.record_interval.value())) return path = self.main_canvas.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]) -> None: """Add path function.""" self.command_stack.push( AddPath(self.record_list, name, self.__path_data, path)) self.record_list.setCurrentRow(self.record_list.count() - 1) def load_paths(self, paths: Dict[str, Sequence[_Coord]]) -> None: """Add multiple path.""" for name, path in paths.items(): self.add_path(name, path) @Slot(name='on_record_remove_clicked') def __remove_path(self) -> None: """Remove path data.""" row = self.record_list.currentRow() if not row > 0: return self.command_stack.push( DeletePath(row, self.record_list, self.__path_data)) 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) -> None: """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, p: QPoint) -> None: """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] if name in self.__path_data: data = self.__path_data[name] else: # Auto preview path data = self.main_canvas.path_preview targets = 0 for text in ("Show", "Copy data from"): self.pop_menu_record_list.addSeparator() for i, path in enumerate(data): if len(set(path)) > 1: action = self.pop_menu_record_list.addAction( f"{text} Point{i}") action.index = i targets += 1 copy_action.setEnabled(targets > 0) action = self.pop_menu_record_list.exec_( self.record_list.mapToGlobal(p)) if action is None: self.pop_menu_record_list.clear() return text = action.text() if action == 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 text.startswith("Copy data from"): # Copy data to clipboard (csv) QApplication.clipboard().setText('\n'.join( f"{x},{y}" for x, y in data[action.index])) elif text.startswith("Show"): # Switch points enabled status if action.index == -1: self.record_show.setChecked(True) self.main_canvas.set_path_show(action.index) self.pop_menu_record_list.clear() @Slot(bool, name='on_record_show_toggled') def __set_path_show(self, toggled: bool) -> None: """Show all paths or hide.""" self.main_canvas.set_path_show(-1 if toggled else -2) @Slot(int, name='on_record_list_currentRowChanged') def __set_path(self, _=None) -> None: """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) -> Sequence[_Coord]: """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) -> None: 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)