Beispiel #1
0
    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]] = {}
Beispiel #2
0
    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)
Beispiel #3
0
 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;")
Beispiel #4
0
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)
Beispiel #5
0
    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()
Beispiel #6
0
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()
Beispiel #7
0
class InputsWidget(QWidget, Ui_Form):
    """There has following functions:

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

    about_to_resolve = Signal()

    def __init__(self, parent: MainWindowBase) -> 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)