def activate(self): # attach to child event signals self.ui.speed.add_listener(self.__on_playback_speed_changed) # attach popovers self.timer_button.set_popover(self.ui.sleep_timer.get_popover()) self.playback_speed_button.set_popover(self.ui.speed.get_popover()) self.warnings = Warnings(self.warnings_button) self.warnings_button.set_popover(self.warnings.get_popover())
class Titlebar: """ This class contains all titlebar logic. """ _application_settings: ApplicationSettings = inject.attr( ApplicationSettings) _artwork_cache: ArtworkCache = inject.attr(ArtworkCache) _importer: Importer = inject.attr(Importer) _files: Files = inject.attr(Files) _library: Library = inject.attr(Library) # main ui class ui = None # Titlebar timer for ui updates on position play_status_updater = None # Is the mouse button currently down on the progress scale? progress_scale_clicked = False current_book = None # Remaining time for this book in seconds # This doesn't include the current track # and will only be refreshed when the loaded track changes! current_remaining = 0 # Elapsed time for this book in seconds # This doesn't include the current track # and will only be refreshed when the loaded track changes! current_elapsed = 0 def __init__(self): self.ui = cozy.ui.main_view.CozyUI() # init buttons self.play_button = self.ui.get_object("play_button") self.prev_button = self.ui.get_object("prev_button") self.volume_button = self.ui.get_object("volume_button") self.volume_button.set_value(self._application_settings.volume) self.timer_button = self.ui.get_object("timer_button") self.playback_speed_button = self.ui.get_object( "playback_speed_button") self.search_button = self.ui.get_object("search_button") self.warnings_button = self.ui.get_object("warnings_button") self.menu_button = self.ui.get_object("menu_button") self.remaining_event_box = self.ui.get_object("remaining_event_box") # init labels self.title_label = self.ui.get_object("title_label") self.subtitle_label = self.ui.get_object("subtitle_label") self.current_label = self.ui.get_object("current_label") self.current_label.set_visible(False) self.remaining_label = self.ui.get_object("remaining_label") self.remaining_label.set_visible(False) # init images self.play_img = self.ui.get_object("play_img") self.pause_img = self.ui.get_object("pause_img") self.cover_img = self.ui.get_object("cover_img") self.cover_img_box = self.ui.get_object("cover_img_box") # init progress scale self.progress_scale = self.ui.get_object("progress_scale") self.progress_scale.set_increments(30.0, 60.0) self.progress_scale.set_visible(False) self.status_stack = self.ui.get_object("status_stack") self.status_label = self.ui.get_object("status_label") self.update_progress_bar = self.ui.get_object("update_progress_bar") self.throbber = self.ui.get_object("spinner") self.throbber.set_visible(False) self.progress_bar = self.ui.get_object("progress_bar") self.__init_signals() # elementaryos specific stuff if tools.is_elementary(): self.cover_img_box.props.width_request = 28 self.cover_img_box.props.height_request = 28 self.volume_button.props.relief = Gtk.ReliefStyle.NONE else: self.volume_button.get_style_context().remove_class("flat") # app menu self.menu_builder = Gtk.Builder.new_from_resource( "/com/github/geigi/cozy/titlebar_menu.ui") menu = self.menu_builder.get_object("titlebar_menu") self.menu_button.set_menu_model(menu) def __init_signals(self): self.play_button.connect("clicked", self.__on_play_pause_clicked) self.prev_button.connect("clicked", self.__on_rewind_clicked) self.volume_button.connect("value-changed", self.__on_volume_changed) self.remaining_event_box.connect("button-release-event", self._on_remaining_clicked) # init progress scale self.progress_scale.connect("value-changed", self.update_ui_time) self.progress_scale.connect("button-release-event", self.__on_progress_clicked) self.progress_scale.connect("button-press-event", self.__on_progress_press) self.progress_scale.connect("key-press-event", self.__on_progress_key_pressed) player.add_player_listener(self.__player_changed) self._importer.add_listener(self._on_importer_event) self._files.add_listener(self._on_files_event) self._library.add_listener(self._on_library_event) def activate(self): # attach to child event signals self.ui.speed.add_listener(self.__on_playback_speed_changed) # attach popovers self.timer_button.set_popover(self.ui.sleep_timer.get_popover()) self.playback_speed_button.set_popover(self.ui.speed.get_popover()) self.warnings = Warnings(self.warnings_button) self.warnings_button.set_popover(self.warnings.get_popover()) def block_ui_buttons(self, block, scan=False): """ Block the ui buttons when gui actions are in progress. :param block: Boolean """ sensitive = not block self.play_button.set_sensitive(sensitive) self.volume_button.set_sensitive(sensitive) self.prev_button.set_sensitive(sensitive) self.timer_button.set_sensitive(sensitive) self.playback_speed_button.set_sensitive(sensitive) if scan: self.search_button.set_sensitive(sensitive) def get_ui_buttons_blocked(self): """ Are the UI buttons currently blocked? """ return not self.play_button.get_sensitive() def play(self): """ """ self.play_button.set_image(self.pause_img) self.__set_play_status_updater(True) def pause(self): """ """ self.play_button.set_image(self.play_img) self.__set_play_status_updater(False) def stop(self): """ Remove all information about a playing book from the titlebar. """ self.play_button.set_image(self.play_img) self.__set_play_status_updater(False) self.title_label.set_text("") self.subtitle_label.set_text("") self.cover_img.set_from_pixbuf(None) self.progress_scale.set_range(0, 0) self.progress_scale.set_visible(False) self.progress_scale.set_sensitive(False) self.remaining_label.set_visible(False) self.current_label.set_visible(False) self.block_ui_buttons(True) def set_title_cover(self, pixbuf, size): """ Sets the cover in the title bar. """ if pixbuf: surface = Gdk.cairo_surface_create_from_pixbuf( pixbuf, self.ui.window.get_scale_factor(), None) self.cover_img.set_from_surface(surface) self.cover_img.set_tooltip_text( player.get_current_track().book.name) else: self.cover_img.set_from_icon_name("book-open-variant-symbolic", Gtk.IconSize.MENU) self.cover_img.props.pixel_size = size def set_progress_scale_width(self, width): self.progress_scale.props.width_request = width def update_ui_time(self, widget): """ Displays the value of the progress slider in the text boxes as time. """ track = player.get_current_track() if not track: log.debug("update_ui_time: track was None.") return val = int(self.progress_scale.get_value()) if self._application_settings.titlebar_remaining_time: total = self.progress_scale.get_adjustment().get_upper() remaining_secs: int = int((total - val)) current_text = seconds_to_str(val, total) remaining_text = seconds_to_str(remaining_secs, total) else: remaining_secs = int((track.length / self.ui.speed.get_speed()) - val) remaining_text = seconds_to_str(remaining_secs, track.length) current_text = seconds_to_str(val, track.length) self.current_label.set_markup("<tt><b>" + current_text + "</b></tt>") self.remaining_label.set_markup("<tt><b>-" + remaining_text + "</b></tt>") if self.ui.book_overview.book and self.current_book.id == self.ui.book_overview.book.id: self.ui.book_overview.update_time() def update_track_ui(self): # set data of new stream in ui track = player.get_current_track() if track is None: return self.title_label.set_text(track.book.name) self.subtitle_label.set_text(track.name) self.block_ui_buttons(False) self.progress_scale.set_sensitive(True) self.progress_scale.set_visible(True) # only change cover when book has changed if self.current_book is not track.book: self.current_book = track.book if tools.is_elementary(): size = 28 else: size = 40 self.set_title_cover( self._artwork_cache.get_cover_pixbuf( track.book, self.ui.window.get_scale_factor(), size), size) self.current_remaining = get_book_remaining(self.current_book, False) self.current_elapsed = get_book_progress(self.current_book, False) self.__update_progress_scale_range() if self._application_settings.titlebar_remaining_time: self.progress_scale.set_value(self.current_elapsed / self.ui.speed.get_speed()) else: self.progress_scale.set_value(0) self.update_ui_time(None) self.current_label.set_visible(True) self.remaining_label.set_visible(True) def switch_to_working(self, message, first): """ Switch the UI state to working. This is used for example when an import is currently happening. This blocks the user from doing some stuff like starting playback. """ self.throbber.set_visible(True) self.throbber.start() self.status_label.set_text(message) if len(self._library.books) < 1 and message == _( "Importing Audiobooks"): return self.status_stack.props.visible_child_name = "working" def switch_to_playing(self): """ Switch the UI state back to playing. This enables all UI functionality for the user. """ self.status_stack.props.visible_child_name = "playback" self.throbber.stop() self.throbber.set_visible(False) self.progress_bar.set_fraction(0) self.update_progress_bar.set_fraction(0) def load_last_book(self): if Settings.get().last_played_book: self.update_track_ui() self.update_ui_time(self.progress_scale) cur_m, cur_s = player.get_current_duration_ui() self.__set_progress_scale_value(cur_m * 60 + cur_s) pos = int(player.get_current_track().position) if self._application_settings.replay: log.info("Replaying the previous 30 seconds.") amount = 30 * 1000000000 if (pos < amount): pos = 0 else: pos = pos - amount self.__set_progress_scale_value( int(pos / 1000000000 / self.ui.speed.get_speed())) def _on_remaining_clicked(self, widget, sender): """ Switch between displaying the time for a track or the whole book. """ if widget.get_name != "titlebar_remaining_time_eventbox": if self._application_settings.titlebar_remaining_time: self._application_settings.titlebar_remaining_time = False else: self._application_settings.titlebar_remaining_time = True self._on_progress_setting_changed() return True def _on_progress_setting_changed(self): self.__update_time() self.__update_progress_scale_range() self.update_ui_time(None) def __on_play_pause_clicked(self, button): """ Play/Pause the player. """ player.play_pause(None) pos = self.ui.get_playback_start_position() player.jump_to_ns(pos) def __on_rewind_clicked(self, button): """ """ seconds = 30 * self.ui.speed.get_speed() if self.ui.first_play: ns = seconds * 1000000000 track = player.get_current_track() pos = track.position if (pos > ns): pos -= ns else: pos = 0 player.save_current_track_position(pos=pos, track=track) else: player.rewind(seconds) # we want to see the jump imediatly therefore we apply the new time manually if self.progress_scale.get_value() > 30: self.progress_scale.set_value(self.progress_scale.get_value() - 30) else: self.progress_scale.set_value(0) def __on_volume_changed(self, widget, value): """ Sets the ui value in the player. """ player.set_volume(value) self._application_settings.volume = value def __on_progress_press(self, widget, sender): """ Remember that progress scale is clicked so it won't get updates from the player. """ self.progress_scale_clicked = True # If the user drags the slider we don't want to jump back # another 30 seconds on first play if self.ui.first_play: self.ui.first_play = False return False def __on_progress_clicked(self, widget, sender): """ Jump to the slided time and release the progress scale update lock. """ value = self.progress_scale.get_value() * self.ui.speed.get_speed() if self._application_settings.titlebar_remaining_time: track, time = get_track_from_book_time(self.current_book, value) if track.id == player.get_current_track().id: player.jump_to(time) else: player.load_file(track) player.play_pause(None, True) self.__set_progress_scale_value(time / self.ui.speed.get_speed()) player.jump_to(time) else: player.jump_to(value) self.progress_scale_clicked = False return False def __on_progress_key_pressed(self, widget, event): """ Jump to the modified time. """ old_val = self.progress_scale.get_value() if event.keyval == Gdk.KEY_Up or event.keyval == Gdk.KEY_Left: if old_val > 30.0: player.jump_to(old_val - 30) else: player.jump_to(0) elif event.keyval == Gdk.KEY_Down or event.keyval == Gdk.KEY_Right: upper = self.progress_scale.get_adjustment().get_upper() if old_val + 30.0 < upper: player.jump_to(old_val + 30) else: player.jump_to(upper) return False def __update_progress_scale_range(self): """ Update the progress scale range including the current playback speed. """ current_track = player.get_current_track() if not current_track or not self.current_book: return if self._application_settings.titlebar_remaining_time: total = get_book_duration( self.current_book) / self.ui.speed.get_speed() else: total = current_track.length / self.ui.speed.get_speed() self.progress_scale.set_range(0, total) def __set_progress_scale_value(self, value): """ Set a given progress scale value. :param value: This value already needs playback speed compensation. """ if self._application_settings.titlebar_remaining_time: value += (self.current_elapsed / self.ui.speed.get_speed()) self.progress_scale.set_value(value) def __set_play_status_updater(self, enable): """ Starts/stops the play status ui update timer. Restarts if enable is True and the timer is already running. :params enable: Boolean """ if self.play_status_updater: self.play_status_updater.stop() self.play_status_updater = None if enable and player.is_playing() and self.play_status_updater is None: self.play_status_updater = IntervalTimer(1, self.__update_time) self.play_status_updater.start() def __update_time(self): """ Update the current and remaining time. """ if not self.progress_scale_clicked: cur_m, cur_s = player.get_current_duration_ui() Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.__set_progress_scale_value, (cur_m * 60 + cur_s)) def __on_playback_speed_changed(self, event, message): """ Handler for events that occur the playback speed object. """ self.__ensure_book_object_is_up_to_date() if event == "playback-speed-changed": speed = message m, s = player.get_current_duration_ui() value = 60 * m + s self.__update_progress_scale_range() self.__set_progress_scale_value(value) self.update_ui_time(None) def __ensure_book_object_is_up_to_date(self): # Racecondition: Sometimes the "track-changed" event is fired in playback_speed first before titlebar. Then it might happen, that the current_book is None or not uptodate. # Only way to fix this is having a global truth that is accessed from everywhere. # TODO if player.get_current_track() and ( not self.current_book or player.get_current_track() and self.current_book.id != player.get_current_track().book.id): self.update_track_ui() def __player_changed(self, event, message): """ Listen to and handle all gst player messages that are important for the ui. """ if event == "track-changed": self.update_track_ui() def _on_importer_event(self, event: str, message): if event == "scan-progress" and isinstance(message, float): self.progress_bar.set_fraction(message) self.update_progress_bar.set_fraction(message) def _on_files_event(self, event: str, message): if event == "copy-progress" and isinstance(message, float): self.update_progress_bar.set_fraction(message) def _on_library_event(self, event: str, message): if event == "rebase-progress" and isinstance(message, float): self.update_progress_bar.set_fraction(message) def close(self): log.info("Closing.") if self.play_status_updater: self.play_status_updater.stop()