예제 #1
0
class AlgorithmProgressWidget(QWidget):
    """
    Widget consisting of a progress bar and a button.
    """
    def __init__(self, parent=None):
        super(AlgorithmProgressWidget, self).__init__(parent)
        self.progress_bar = None
        self.details_button = QPushButton('Details')
        self.details_button.clicked.connect(self.show_dialog)
        self.layout = QHBoxLayout()
        self.layout.addStretch()
        self.layout.addWidget(self.details_button)
        self.setLayout(self.layout)
        self.presenter = AlgorithmProgressPresenter(self)

    def show_progress_bar(self):
        if self.progress_bar is None:
            self.progress_bar = QProgressBar()
            self.progress_bar.setAlignment(Qt.AlignHCenter)
            self.layout.insertWidget(0, self.progress_bar)
            self.layout.removeItem(self.layout.takeAt(1))

    def hide_progress_bar(self):
        if self.progress_bar is not None:
            self.layout.insertStretch(0)
            self.layout.removeWidget(self.progress_bar)
            self.progress_bar.close()
            self.progress_bar = None

    def show_dialog(self):
        dialog = AlgorithmMonitorDialog(self, self.presenter.model)
        dialog.show()
예제 #2
0
class SpotifyWebPrompt(QWidget):
    """
    This widget handles all the interaction with the user to obtain the
    credentials for the Spotify Web API.
    """

    done = Signal(RefreshingToken)

    def __init__(self, client_id: str, client_secret: str, redirect_uri: str,
                 *args) -> None:
        """
        Starts the API initialization flow, which is the following:
            1. The user inputs the credentials.
            ** self.on_submit_creds is called **
            2. The user logs in.
            ** self.on_login is called **
            3. If the credentials were correct, the Web API is set up and
               started from outside this widget. Otherwise, go back to step 1.
            ** self.done is emitted **
        """

        super().__init__(*args)

        logging.info("Initializing the Spotify Web API prompt interface")
        self.redirect_uri = redirect_uri

        # Creating the layout
        self.layout = QHBoxLayout(self)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)

        # The web form for the user to input the credentials.
        self.web_form = SpotifyWebForm(client_id=client_id,
                                       client_secret=client_secret)
        # on_submit_spotify_web creds will be called once the credentials have
        # been input.
        self.web_form.button.clicked.connect(self.on_submit_creds)
        self.layout.addWidget(self.web_form)

        # The web browser for the user to login and grant access.
        # It's hidden at the beggining and will appear once the credentials
        # are input.
        self.browser = WebBrowser()
        self.browser.hide()
        # The initial screen with the web form will be shown if the user
        # clicks on the Go Back button.
        self.browser.go_back_button.pressed.connect(
            lambda: (self.browser.hide(), self.web_form.show()))
        # Any change in the browser URL will redirect to on__login to check if
        # the login was succesful.
        self.browser.web_view.urlChanged.connect(self.on_login)
        self.layout.addWidget(self.browser)

    @property
    def client_id(self) -> str:
        return self.web_form.client_id

    @property
    def client_secret(self) -> str:
        return self.web_form.client_secret

    @Slot()
    def on_submit_creds(self) -> None:
        """
        Checking if the submitted credentials were correct, and starting the
        logging-in step.
        """

        # Obtaining the input data
        form_client_id = self.web_form.client_id
        form_client_secret = self.web_form.client_secret
        logging.info("Input creds: '%s' & '%s'", form_client_id,
                     form_client_secret)

        # Checking that the data isn't empty
        empty_field = False
        if form_client_id == '':
            self.web_form.input_client_id.highlight()
            empty_field = True
        else:
            self.web_form.input_client_id.undo_highlight()

        if form_client_secret == '':
            self.web_form.input_client_secret.highlight()
            empty_field = True
        else:
            self.web_form.input_client_secret.undo_highlight()

        if empty_field:
            return

        # Hiding the form and showing the web browser for the next step
        self.web_form.hide()
        self.browser.show()

        # Creating the request URL to obtain the authorization token
        self.creds = RefreshingCredentials(
            form_client_id, form_client_secret, self.redirect_uri)
        self.scope = scopes.user_read_currently_playing
        url = self.creds.user_authorisation_url(self.scope)
        self.browser.url = url

    @Slot()
    def on_login(self) -> None:
        """
        This function is called once the user has logged into Spotify to
        obtain the access token.

        Part of this function is a reimplementation of
        `tekore.prompt_user_token`. It does the same thing but in a more
        automatized way, because Qt has access over the web browser too.
        """

        url = self.browser.url
        logging.info("Now at: %s", url)

        # If the URL isn't the Spotify response URI (localhost), do nothing
        if url.find(self.redirect_uri) == -1:
            return

        # Trying to get the auth token from the URL with Tekore's
        # parse_code_from_url(), which throws a KeyError if the URL doesn't
        # contain an auth token or if it contains more than one.
        try:
            code = parse_code_from_url(url)
        except KeyError as e:
            logging.info("ERROR: %s", str(e))
            return

        # Now the user token has to be requested to Spotify, while
        # checking for errors to make sure the credentials were correct.
        # This will only happen with the client secret because it's only
        # checked when requesting the user token.
        try:
            # A RefreshingToken is used instead of a regular Token so that
            # it's automatically refreshed before it expires. self.creds is
            # of type `RefreshingCredentials`, so it returns always a
            # RefreshingToken.
            token = self.creds.request_user_token(code)
        except OAuthError as e:
            self.browser.hide()
            self.web_form.show()
            self.web_form.show_error(str(e))
            return

        # Removing the GUI elements used to obtain the credentials
        self.layout.removeWidget(self.web_form)
        self.web_form.hide()
        self.layout.removeWidget(self.browser)
        self.browser.hide()

        # Finally starting the Web API
        self.done.emit(token)
예제 #3
0
class MainWindow(QWidget):
    def __init__(self, config: Config) -> None:
        """
        Main window with the GUI and whatever player is being used.
        """

        super().__init__()
        self.setWindowTitle('vidify')

        # Setting the window to stay on top
        if config.stay_on_top:
            self.setWindowFlags(Qt.WindowStaysOnTopHint)

        # Setting the fullscreen and window size
        if config.fullscreen:
            self.showFullScreen()
        else:
            self.resize(config.width or 800, config.height or 600)

        # Loading the used fonts (Inter)
        font_db = QFontDatabase()
        for font in Res.fonts:
            font_db.addApplicationFont(font)

        # Initializing the player and saving the config object in the window.
        self.layout = QHBoxLayout(self)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)
        self.player = initialize_player(config.player, config)
        logging.info("Using %s as the player", config.player)
        self.config = config

        # The API initialization is more complex. For more details, please
        # check the flow diagram in vidify.api. First we have to check if
        # the API is saved in the config:
        try:
            api_data = get_api_data(config.api)
        except KeyError:
            # Otherwise, the user is prompted for an API. After choosing one,
            # it will be initialized from outside this function.
            logging.info("API not found: prompting the user")
            self.API_selection = APISelection()
            self.layout.addWidget(self.API_selection)
            self.API_selection.api_chosen.connect(self.on_api_selection)
        else:
            logging.info("Using %s as the API", config.api)
            self.initialize_api(api_data)

    @Slot(str)
    def on_api_selection(self, api_str: str) -> None:
        """
        Method called when the API is selected with APISelection.
        The provided api string must be an existent entry
        inside the APIData enumeration.
        """

        # Removing the widget used to obtain the API string
        self.layout.removeWidget(self.API_selection)
        self.API_selection.setParent(None)
        self.API_selection.hide()
        del self.API_selection

        # Saving the API in the config
        self.config.api = api_str

        # Starting the API initialization
        self.initialize_api(APIData[api_str])

    def initialize_api(self, api_data: APIData) -> None:
        """
        Initializes an API with the information from APIData.
        """

        # The API may need interaction with the user to obtain credentials
        # or similar data. This function will already take care of the
        # rest of the initialization.
        if api_data.gui_init_fn is not None:
            fn = getattr(self, api_data.gui_init_fn)
            fn()
            return

        # Initializing the API with dependency injection.
        mod = importlib.import_module(api_data.module)
        cls = getattr(mod, api_data.class_name)
        self.api = cls()

        self.wait_for_connection(
            self.api.connect_api, message=api_data.connect_msg,
            event_loop_interval=api_data.event_loop_interval)

    def wait_for_connection(self, conn_fn: Callable[[], None],
                            message: Optional[str] = None,
                            event_loop_interval: int = 1000) -> None:

        """
        Creates an APIConnecter instance and waits for the API to be
        available, or times out otherwise.
        """

        self.event_loop_interval = event_loop_interval
        self.api_connecter = APIConnecter(
            conn_fn, message or "Waiting for connection")
        self.api_connecter.success.connect(self.on_conn_success)
        self.api_connecter.fail.connect(self.on_conn_fail)
        self.layout.addWidget(self.api_connecter)
        self.api_connecter.start()

    @Slot()
    def on_conn_fail(self) -> None:
        """
        If the API failed to connect, the app will be closed.
        """

        print("Timed out waiting for the connection")
        QCoreApplication.exit(1)

    @Slot(float)
    def on_conn_success(self, start_time: float) -> None:
        """
        Once the connection has been established correctly, the API can
        be started properly.
        """

        logging.info("Succesfully connected to the API")
        self.layout.removeWidget(self.api_connecter)
        del self.api_connecter

        # Initializing the optional audio synchronization extension, now
        # that there's access to the API's data. Note that this feature
        # is only available on Linux.
        if self.config.audiosync:
            from vidify.audiosync import AudiosyncWorker
            self.audiosync = AudiosyncWorker(self.api.player_name)
            self.audiosync.success.connect(self.on_audiosync_success)
            self.audiosync.failed.connect(self.on_audiosync_fail)

        # Loading the player
        self.setStyleSheet(f"background-color:{Colors.black};")
        self.layout.addWidget(self.player)
        self.play_video(self.api.artist, self.api.title, start_time)

        # Connecting to the signals generated by the API
        self.api.new_song_signal.connect(self.play_video)
        self.api.position_signal.connect(self.change_video_position)
        self.api.status_signal.connect(self.change_video_status)

        # Starting the event loop if it was initially passed as
        # a parameter.
        if self.event_loop_interval is not None:
            self.start_event_loop(self.api.event_loop,
                                  self.event_loop_interval)

    def start_event_loop(self, event_loop: Callable[[], None],
                         ms: int) -> None:
        """
        Starts a "manual" event loop with a timer every `ms` milliseconds.
        This is used with the SwSpotify API and the Web API to check every
        `ms` seconds if a change has happened, like if the song was paused.
        """

        logging.info("Starting event loop")
        timer = QTimer(self)

        # Qt doesn't accept a method as the parameter so it's converted
        # to a function.
        if isinstance(event_loop, types.MethodType):
            timer.timeout.connect(lambda: event_loop())
        else:
            timer.timeout.connect(event_loop)
        timer.start(ms)

    @Slot(bool)
    def change_video_status(self, is_playing: bool) -> None:
        """
        Slot used for API updates of the video status.
        """

        self.player.pause = not is_playing

        # If there is an audiosync thread running, this will pause the sound
        # recording and youtube downloading.
        if self.config.audiosync and self.audiosync.status != 'idle':
            self.audiosync.is_running = is_playing

    @Slot(int)
    def change_video_position(self, ms: int) -> None:
        """
        Slot used for API updates of the video position.
        """

        if not self.config.audiosync:
            self.player.position = ms

        # Audiosync is aborted if the position of the video changed, since
        # the audio being recorded won't make sense.
        if self.config.audiosync and self.audiosync.status != 'idle':
            self.audiosync.abort()

    @Slot(str, str, float)
    def play_video(self, artist: str, title: str, start_time: float) -> None:
        """
        Slot used to play a video. This is called when the API is first
        initialized from this GUI, and afterwards from the event loop handler
        whenever a new song is detected.

        If an error was detected when downloading the video, the default one
        is shown instead.

        Both audiosync and youtubedl work in separate threads to avoid
        blocking the GUI. This method will start both of them.
        """

        # Checking that the artist and title are valid first of all
        if self.api.artist in (None, '') and self.api.title in (None, ''):
            logging.info("The provided artist and title are empty.")
            self.on_youtubedl_fail()
            if self.config.audiosync:
                self.on_audiosync_fail()
            return

        # This delay is used to know the elapsed time until the video
        # actually starts playing, used in the audiosync feature.
        self.timestamp = start_time
        query = f"ytsearch:{format_name(artist, title)} Official Video"

        if self.config.audiosync:
            self.launch_audiosync(query)

        self.launch_youtubedl(query)

    def launch_audiosync(self, query: str) -> None:
        """
        Starts the audiosync thread, that will call either
        self.on_audiosync_success, or self.on_audiosync_fail once it's
        finished.

        First trying to stop the previous audiosync thread, as only
        one audiosync thread can be running at once.

        Note: QThread.start() is guaranteed to work once QThread.run()
        has returned. Thus, this will wait until it's done and launch
        the new one.
        """

        self.audiosync.abort()
        self.audiosync.wait()
        self.audiosync.youtube_title = query
        self.audiosync.start()
        logging.info("Started a new audiosync job")

    def launch_youtubedl(self, query: str) -> None:
        """
        Starts a YoutubeDL thread that will call either
        self.on_youtubedl_success or self.on_youtubedl_fail once it's done.
        """

        logging.info("Starting the youtube-dl thread")
        self.youtubedl = YouTubeDLWorker(
            query, self.config.debug, self.config.width, self.config.height)
        self.yt_thread = QThread()
        self.youtubedl.moveToThread(self.yt_thread)
        self.yt_thread.started.connect(self.youtubedl.get_url)
        self.youtubedl.success.connect(self.on_yt_success)
        self.youtubedl.fail.connect(self.on_youtubedl_fail)
        self.youtubedl.finish.connect(self.yt_thread.exit)
        self.yt_thread.start()

    @Slot()
    def on_youtubedl_fail(self) -> None:
        """
        If Youtube-dl for whatever reason failed to load the video, a fallback
        error video is shown, along with a message to let the user know what
        happened.
        """

        self.player.start_video(Res.default_video, self.api.is_playing)
        print("The video wasn't found, either because of an issue with your"
              " internet connection or because the provided data was invalid."
              " For more information, enable the debug mode.")

    @Slot(str)
    def on_yt_success(self, url: str) -> None:
        """
        Obtains the video URL from the Youtube-dl thread and starts playing
        the video. Also shows the lyrics if enabled. The position of the video
        isn't set if it's using audiosync, because this is done by the
        AudiosyncWorker thread.
        """

        self.player.start_video(url, self.api.is_playing)

        if not self.config.audiosync:
            try:
                self.player.position = self.api.position
            except NotImplementedError:
                self.player.position = 0

        # Finally, the lyrics are displayed. If the video wasn't found, an
        # error message is shown.
        if self.config.lyrics:
            print(get_lyrics(self.api.artist, self.api.title))

    @Slot()
    def on_audiosync_fail(self) -> None:
        """
        Currently, when audiosync fails, nothing happens.
        """

        logging.info("Audiosync module failed to return the lag")

    @Slot(int)
    def on_audiosync_success(self, lag: int) -> None:
        """
        Slot used after the audiosync function has finished. It sets the
        returned lag in milliseconds on the player.

        This assumes that the song wasn't paused until this issue is fixed:
        https://github.com/vidify/audiosync/issues/12
        """

        logging.info("Audiosync module returned %d ms", lag)

        # The current API position according to what's being recorded.
        playback_delay = round((time.time() - self.timestamp) * 1000) \
            - self.player.position
        lag += playback_delay

        # The user's custom audiosync delay. This is basically the time taken
        # until the module started recording (which may depend on the user
        # hardware and other things). Thus, it will almost always be a
        # negative value.
        lag += self.config.audiosync_calibration

        logging.info("Total delay is %d ms", lag)
        if lag > 0:
            self.player.position += lag
        elif lag < 0:
            # If a negative delay is larger than the current player position,
            # the player position is set to zero after the lag has passed
            # with a timer.
            if self.player.position < -lag:
                self.sync_timer = QTimer(self)
                self.sync_timer.singleShot(
                    -lag, lambda: self.change_video_position(0))
            else:
                self.player.position += lag

    def init_spotify_web_api(self) -> None:
        """
        SPOTIFY WEB API CUSTOM FUNCTION

        Note: the Tekore imports are done inside the functions so that
        Tekore isn't needed for whoever doesn't plan to use the Spotify
        Web API.
        """

        from vidify.api.spotify.web import get_token
        from vidify.gui.api.spotify_web import SpotifyWebPrompt

        token = get_token(self.config.refresh_token, self.config.client_id,
                          self.config.client_secret)

        if token is not None:
            # If the previous token was valid, the API can already start.
            logging.info("Reusing a previously generated token")
            self.start_spotify_web_api(token, save_config=False)
        else:
            # Otherwise, the credentials are obtained with the GUI. When
            # a valid auth token is ready, the GUI will initialize the API
            # automatically exactly like above. The GUI won't ask for a
            # redirect URI for now.
            logging.info("Asking the user for credentials")
            # The SpotifyWebPrompt handles the interaction with the user and
            # emits a `done` signal when it's done.
            self._spotify_web_prompt = SpotifyWebPrompt(
                self.config.client_id, self.config.client_secret,
                self.config.redirect_uri)
            self._spotify_web_prompt.done.connect(self.start_spotify_web_api)
            self.layout.addWidget(self._spotify_web_prompt)

    def start_spotify_web_api(self, token: 'RefreshingToken',
                              save_config: bool = True) -> None:
        """
        SPOTIFY WEB API CUSTOM FUNCTION

        Initializes the Web API, also saving them in the config for future
        usage (if `save_config` is true).
        """
        from vidify.api.spotify.web import SpotifyWebAPI

        logging.info("Initializing the Spotify Web API")

        # Initializing the web API
        self.api = SpotifyWebAPI(token)
        api_data = APIData['SPOTIFY_WEB']
        self.wait_for_connection(
            self.api.connect_api, message=api_data.connect_msg,
            event_loop_interval=api_data.event_loop_interval)

        # The obtained credentials are saved for the future
        if save_config:
            logging.info("Saving the Spotify Web API credentials")
            self.config.client_secret = self._spotify_web_prompt.client_secret
            self.config.client_id = self._spotify_web_prompt.client_id
            self.config.refresh_token = token.refresh_token

        # The credentials prompt widget is removed after saving the data. It
        # may not exist because start_spotify_web_api was called directly,
        # so errors are taken into account.
        try:
            self.layout.removeWidget(self._spotify_web_prompt)
            self._spotify_web_prompt.hide()
            del self._spotify_web_prompt
        except AttributeError:
            pass
예제 #4
0
파일: gui.py 프로젝트: klauer/lightpath
class LightApp(Display):
    """
    Main widget display for the lightpath

    Shows tables of devices and the current destination of the beam, as well
    as the status of the MPS system for LCLS

    Parameters
    ----------
    controller: LightController
        LightController object

    beamline : str, optional
        Beamline to initialize the application with, otherwise the most
        upstream beamline will be selected

    dark : bool, optional
        Load the UI with the `qdarkstyle` interface

    parent : optional
    """
    shown_types = [
        dtypes.Attenuator, dtypes.GateValve, dtypes.IPM, dtypes.LODCM,
        dtypes.OffsetMirror, dtypes.PIM, PPSStopper, dtypes.PulsePicker,
        dtypes.Slits, dtypes.Stopper, dtypes.XFLS
    ]

    def __init__(self, controller, beamline=None, parent=None, dark=True):
        super().__init__(parent=parent)
        # Store Lightpath information
        self.light = controller
        self.path = None
        self.detail_screen = None
        self.device_buttons = dict()
        self._lock = threading.Lock()
        # Create empty layout
        self.lightLayout = QHBoxLayout()
        self.lightLayout.setSpacing(1)
        self.widget_rows.setLayout(self.lightLayout)
        self.device_types.setLayout(QGridLayout())
        self.overview.setLayout(QHBoxLayout())
        self.overview.layout().setSpacing(2)
        self.overview.layout().setContentsMargins(2, 2, 2, 2)
        # Setup the fancy overview slider
        slide_scroll = self.scroll.horizontalScrollBar()
        self.slide.setRange(slide_scroll.minimum(), slide_scroll.maximum())
        self.slide.sliderMoved.connect(slide_scroll.setSliderPosition)
        slide_scroll.rangeChanged.connect(self.slide.setRange)
        slide_scroll.valueChanged.connect(self.slide.setSliderPosition)
        # Add destinations
        for line in self.destinations():
            self.destination_combo.addItem(line)

        # Connect signals to slots
        self.destination_combo.currentIndexChanged.connect(
            self.change_path_display)
        self.device_combo.activated[str].connect(self.focus_on_device)
        self.impediment_button.pressed.connect(self.focus_on_device)
        self.remove_check.toggled.connect(self.filter)
        self.upstream_check.toggled.connect(self.filter)
        self.detail_hide.clicked.connect(self.hide_detailed)
        # Store LightRow objects to manage subscriptions
        self.rows = list()
        # Select the beamline to begin with
        beamline = beamline or self.destinations()[0]
        try:
            idx = self.destinations().index(beamline.upper())
        except ValueError:
            logger.error("%s is not a valid beamline", beamline)
            idx = 0
        # Move the ComboBox
        self.destination_combo.setCurrentIndex(idx)
        # Add all of our device type options
        max_columns = 3
        for i, row in enumerate(np.array_split(self.shown_types, max_columns)):
            for j, device_type in enumerate(row):
                # Add box to layout
                box = QCheckBox(device_type.__name__)
                box.setChecked(True)
                self.device_types.layout().addWidget(box, j, i)
                # Hook up box to hide function
                self.device_buttons[box] = device_type
                box.toggled.connect(self.filter)
        # Setup the UI
        self.change_path_display()
        self.resizeSlider()
        # Change the stylesheet
        if dark:
            typhos.use_stylesheet(dark=True)

    def destinations(self):
        """
        All possible beamline destinations sorted by end point
        """
        return sorted(list(self.light.beamlines.keys()),
                      key=lambda x: self.light.beamlines[x].range[0])

    def load_device_row(self, device):
        """
        Create LightRow for device
        """
        # Create two widgets
        widgets = (LightRow(device), LightRow(device))
        # Condense the second
        widgets[1].condense()
        return widgets

    def select_devices(self, beamline):
        """
        Select a subset of beamline devices to show in the display

        Parameters
        ----------
        beamline : str
            Beamline to display

        upstream : bool, optional
            Include upstream devices in the display
        """
        # Clear any remaining subscriptions
        if self.path:
            self.clear_subs()
        # Find pool of devices and create subscriptions
        self.path = self.light.beamlines[beamline]
        # Defer running updates until UI is created
        self.path.subscribe(self.update_path, run=False)
        logger.debug("Selected %s devices ...", len(self.path.path))
        return self.path.path

    def selected_beamline(self):
        """
        Current beamline selected by the combo box
        """
        return self.destination_combo.currentText()

    @property
    def hidden_devices(self):
        """Device types set to currently be visible"""
        return [
            dtype for button, dtype in self.device_buttons.items()
            if not button.isChecked()
        ]

    @pyqtSlot()
    @pyqtSlot(bool)
    def change_path_display(self, value=None):
        """
        Change the display devices based on the state of the control buttons
        """
        with self._lock:
            logger.debug("Resorting beampath display ...")
            # Remove old detailed screen
            self.hide_detailed()
            # Grab all the light rows
            rows = [
                self.load_device_row(d)
                for d in self.select_devices(self.selected_beamline())
            ]
            # Clear layout if previously loaded rows exist
            if self.rows:
                # Clear our subscribtions
                for row in self.rows:
                    # Remove from layout
                    self.lightLayout.removeWidget(row[0])
                    self.overview.layout().removeWidget(row[1])
                    # Disconnect
                    for widget in row:
                        widget.clear_sub()
                        widget.deleteLater()
                # Clear subscribed row cache
                self.rows.clear()
                self.device_combo.clear()
            # Hide nothing when switching beamlines
            boxes = self.device_types.children()
            boxes.extend([self.upstream_check, self.remove_check])
            for box in boxes:
                if isinstance(box, QCheckBox):
                    box.setChecked(True)
            # Add all the widgets to the display
            for i, row in enumerate(rows):
                # Cache row to later clear subscriptions
                self.rows.append(row)
                # Add widget to layout
                self.lightLayout.addWidget(row[0])
                self.overview.layout().addWidget(row[1])
                # Connect condensed widget to focus_on_device
                row[1].device_drawing.clicked.connect(
                    partial(self.focus_on_device, name=row[1].device.name))
                # Connect large widget to show Typhos screen
                row[0].device_drawing.clicked.connect(
                    partial(self.show_detailed, row[0].device))
                # Add device to combo
                self.device_combo.addItem(row[0].device.name)
        # Initialize interface
        for row in self.rows:
            for widget in row:
                widget.update_state()
        # Update the state of the path
        self.update_path()

    def ui_filename(self):
        """
        Name of designer UI file
        """
        return 'lightapp.ui'

    def ui_filepath(self):
        """
        Full path to :attr:`.ui_filename`
        """
        return os.path.join(os.path.dirname(os.path.abspath(__file__)),
                            self.ui_filename())

    def update_path(self, *args, **kwargs):
        """
        Update the PyDMRectangles to show devices as in the beam or not
        """
        with self._lock:
            block = self.path.impediment
            # Set the current impediment label
            if block:
                self.current_impediment.setText(block.name)
                self.impediment_button.setEnabled(True)
            else:
                self.current_impediment.setText('None')
                self.impediment_button.setEnabled(False)
            for row in self.rows:
                device = row[0].device
                # If our device is before or at the impediment, it is lit
                if not block or (device.md.z <= block.md.z):
                    _in = True
                    # Check whether this device is passing beam
                    _out = block != device
                # Otherwise, it is off
                else:
                    _in, _out = (False, False)
                # Update widget display
                for widget in row:
                    widget.update_light(_in, _out)

    @pyqtSlot()
    @pyqtSlot(str)
    def focus_on_device(self, name=None):
        """Scroll to the desired device"""
        # If not provided a name, use the impediment
        name = name or self.current_impediment.text()
        # Map of names
        names = [row[0].device.name for row in self.rows]
        # Find index
        try:
            idx = names.index(name)
        except ValueError:
            logger.error("Can not set focus on device %r", name)
            return
        # Grab widget
        self.rows[idx][0].setHidden(False)
        self.scroll.ensureWidgetVisible(self.rows[idx][0])

    @pyqtSlot(bool)
    def filter(self, *args):
        """Hide devices along the beamline for a more succinct view"""
        for row in self.rows:
            device = row[0].device
            # Hide if a hidden instance of a device type
            hidden_device_type = type(device) in self.hidden_devices
            # Hide if removed
            hidden_removed = (not self.remove_check.isChecked()
                              and row[0].last_state == DeviceState.Removed)
            # Hide if upstream
            beamline = self.selected_beamline()
            hidden_upstream = (not self.upstream_check.isChecked()
                               and device.md.beamline != beamline)
            # Hide device if any of the criteria are met
            row[0].setHidden(hidden_device_type or hidden_removed
                             or hidden_upstream)
        # Change the slider size to match changing view
        self.resizeSlider()

    def clear_subs(self):
        """
        Clear the subscription event
        """
        self.path.clear_sub(self.update_path)

    @pyqtSlot()
    def show_detailed(self, device):
        """Show the Typhos display for a device"""
        # Hide the last widget
        self.hide_detailed()
        # Create a Typhos display
        try:
            self.detail_screen = TyphosDeviceDisplay.from_device(device)
        except Exception:
            logger.exception("Unable to create display for %r", device.name)
            return
        # Add to widget
        self.detail_layout.insertWidget(1, self.detail_screen, 0,
                                        Qt.AlignHCenter)
        self.device_detail.show()

    @pyqtSlot()
    def hide_detailed(self):
        """Hide Typhos display for a device"""
        # Catch the issue when there is no detail_screen already
        self.device_detail.hide()
        if self.detail_screen:
            # Remove from layout
            self.detail_layout.removeWidget(self.detail_screen)
            # Destroy widget
            self.detail_screen.deleteLater()
            self.detail_screen = None

    def resizeSlider(self):
        # Visible area of beamline
        visible = self.scroll.width() / self.scroll.widget().width()
        # Take same fraction of bar up in handle width
        slider_size = round(self.slide.width() * visible)
        # Set Stylesheet
        self.slide.setStyleSheet('QSlider::handle'
                                 '{width: %spx;'
                                 'background: rgb(124, 252, 0);}'
                                 '' % slider_size)

    def show(self):
        # Comandeered to assure that slider is initialized properly
        super().show()
        self.resizeSlider()

    def resizeEvent(self, evt):
        # Further resize-ing of the widget should affect the fancy slider
        super().resizeEvent(evt)
        self.resizeSlider()
예제 #5
0
class CyclePlotWidget(QWidget):
    """ Widget to display cycling data and labels showing data at the point
        under the mouse.
    
        Parameters
        ----------
        parent : CycleTracks
            CycleTracks main window object.
        style : str, optional
            Plot style to apply.
    """

    currentPointChanged = Signal(dict)
    """ **signal**  currentPointChanged(dict `values`)
        
        Emitted when a point in the plot is hovered over. The dict provides
        the date, speed, distance, calories and time data for the chosen point.
    """

    pointSelected = Signal(object)
    """ **signal** pointSelected(datetime `currentPoint`)
        
        Emitted when the plot is double clicked, with the date from the
        current point.
    """
    def __init__(self, parent, style="dark"):

        super().__init__()

        self.plotState = None
        self.plotLabel = None
        self.parent = parent

        self._makePlot(parent, style=style)

        self.plotToolBar = PlotToolBar()
        self.plotToolBar.viewAllClicked.connect(self.plotWidget.viewAll)
        self.plotToolBar.viewRangeClicked.connect(
            self.plotWidget.resetMonthRange)
        self.plotToolBar.highlightPBClicked.connect(
            self.plotWidget._highlightPBs)

        self.plotLayout = QHBoxLayout()
        self.plotLayout.addWidget(self.plotWidget)
        self.plotLayout.addWidget(self.plotToolBar)
        self.layout = QVBoxLayout()
        self.layout.addLayout(self.plotLayout)
        self.layout.addWidget(self.plotLabel)

        self.setLayout(self.layout)

    def _makePlot(self, *args, **kwargs):
        self.plotWidget = Plot(*args, **kwargs)
        if self.plotLabel is None:
            self.plotLabel = CyclePlotLabel(self.plotWidget.style)
        else:
            self.plotLabel.setStyle(self.plotWidget.style)
        self.plotLabel.labelClicked.connect(self.plotWidget.switchSeries)
        self.plotWidget.currentPointChanged.connect(self.plotLabel.setLabels)

        self.plotWidget.pointSelected.connect(self.pointSelected)

    @Slot()
    def newData(self):
        self.plotWidget.updatePlots()

    @Slot(object)
    def setCurrentPointFromDate(self, date):
        self.plotWidget.setCurrentPointFromDate(date)

    @Slot(object, bool)
    def setXAxisRange(self, months, fromRecentSession=True):
        self.plotWidget.setXAxisRange(months,
                                      fromRecentSession=fromRecentSession)

    @Slot(str)
    def setStyle(self, style, force=False):
        if force or self.plotWidget.style.name != style:
            self.plotState = self.plotWidget.getState()
            self.plotLayout.removeWidget(self.plotWidget)
            self.plotWidget.deleteLater()
            self._makePlot(self.parent, style=style)
            self.plotWidget.setState(self.plotState)
            self.plotLayout.insertWidget(0, self.plotWidget)

    def addCustomStyle(self, name, style, setStyle=True):
        self.plotWidget.style.addStyle(name, style)
        if setStyle:
            self.setStyle(name, force=True)

    def removeCustomStyle(self, name):
        # TODO remove style from file
        # and update current?
        self.plotWidget.style.removeStyle(name)

    def getStyle(self, name):
        return self.plotWidget.style.getStyleDict(name)

    def getStyleKeys(self):
        return self.plotWidget.style.keys

    def getStyleSymbolKeys(self):
        return self.plotWidget.style.symbolKeys

    def getValidStyles(self):
        return self.plotWidget.style.validStyles

    def getDefaultStyles(self):
        return self.plotWidget.style.defaultStyles