#include "main_window.hpp" #include #include "my_slider.hpp" #include #include #include #include void MainWindow::set_song_loaded(bool loaded) { bool unloaded = !loaded; seek_bar.set_sensitive(loaded); pause_btn.set_sensitive(loaded); restart_btn.set_sensitive(loaded); stop_btn.set_sensitive(loaded); action_enabled_changed("toggle_pause", loaded); action_enabled_changed("stop_playback", loaded); action_enabled_changed("restart_playback", loaded); } void MainWindow::set_subtitle(std::optional subtitle) { subTitleLabel.set_visible(subtitle.has_value()); if (subtitle.has_value()) { subTitleLabel.set_text(subtitle.value()); set_title(subtitle.value() + Glib::ustring(" - ") + titleLabel.get_text()); } else { set_title(titleLabel.get_text()); } } void MainWindow::update_file(optional new_file) { playback_file = new_file; bool song_loaded = new_file.has_value(); set_song_loaded(song_loaded); if (song_loaded) { length = playback->GetLength(); length_component_count = TimeToComponentCount(length); length_label.set_text(TimeToString(length, length_component_count).c_str()); seek_bar.set_range(0, length); } else { length = 0; length_component_count = 2; length_label.set_text("00:00"); seek_bar.set_value(0.0); } for (auto row : stream_selection_box.get_children()) { row->remove_data(Glib::Quark("id")); } stream_selection_box.remove_all(); for (auto &stream : playback->get_streams()) { int id = stream.id; Glib::ustring name(stream.name.c_str()); Gtk::Label idLabel; idLabel.set_text(Glib::ustring(std::format("{}", id).c_str())); Gtk::Label nameLabel; nameLabel.set_text(name); Gtk::Box parentBox; parentBox.append(idLabel); parentBox.append(nameLabel); parentBox.set_orientation(Gtk::Orientation::HORIZONTAL); parentBox.set_hexpand(); nameLabel.set_hexpand(); Gtk::ListBoxRow listBoxRow; listBoxRow.set_child(parentBox); listBoxRow.set_hexpand(); listBoxRow.set_data(Glib::Quark("id"), new int(id)); stream_selection_box.append(listBoxRow); if (id == this->playback->get_current_stream()) { stream_selection_box.select_row(listBoxRow); } } stream_selection_box.signal_row_selected().connect([=,this](auto *row) { if (row == nullptr || row->get_data(Glib::Quark("id")) == nullptr) { return; } this->playback->play_stream(*(int*)row->get_data(Glib::Quark("id"))); }); std::optional title = playback->get_current_title(); song_name = title.value_or(""); subTitleLabel.set_visible(title.has_value()); if (title.has_value()) { set_subtitle(song_name); } else { set_subtitle({}); } bool length_valid = length > 0; seek_bar.set_visible(length_valid); seek_indeterminate.set_visible(!length_valid); time_label.set_visible(length_valid); length_label.set_visible(length_valid); } struct signals_split_by_expected { uint16_t expected; uint16_t unexpected; inline uint16_t all_signals() { return expected|unexpected; } inline uint16_t filter(uint16_t signal) { return all_signals() & signal; } inline bool emitted(uint16_t signal) { return filter(signal) != 0; } inline uint16_t filter_expected(uint16_t signal) { return expected & signal; } inline uint16_t filter_unexpected(uint16_t signal) { return unexpected & signal; } inline bool signal_expected(uint16_t signal) { return filter_expected(signal) == filter(signal); } inline bool signal_unexpected(uint16_t signal) { return filter_unexpected(signal) == filter(signal); } signals_split_by_expected(uint16_t expected, uint16_t unexpected) : expected(expected) , unexpected(unexpected) {} }; bool MainWindow::update() { uint16_t signals_expected_and_emitted = playback->handle_signals(expected_signals); uint16_t unexpected_signals = PlaybackSignalNone; expected_signals &= ~signals_expected_and_emitted; auto handle_signal = [this,signals_expected_and_emitted,&unexpected_signals](uint16_t signals) { uint16_t output = signals_expected_and_emitted & signals; uint16_t unexpected_output = PlaybackSignalNone; uint16_t remaining_signals = signals & ~output; if (remaining_signals != PlaybackSignalNone) { unexpected_output |= playback->handle_signals(remaining_signals); } unexpected_signals |= unexpected_output; return signals_split_by_expected(output, unexpected_output); }; auto signals_handleable = handle_signal(PlaybackSignalFileChanged|PlaybackSignalErrorOccurred|PlaybackSignalPaused|PlaybackSignalResumed|PlaybackSignalPitchChanged|PlaybackSignalSeeked|PlaybackSignalSpeedChanged|PlaybackSignalStarted|PlaybackSignalStopped|PlaybackSignalTempoChanged); if (signals_handleable.emitted(PlaybackSignalFileChanged)) { auto new_file = playback->get_current_file(); if (new_file != playback_file) { update_file(new_file); } } bool disambiguate_running = false; bool stopped = signals_handleable.emitted(PlaybackSignalStopped); bool started = signals_handleable.emitted(PlaybackSignalStarted); if (stopped && started) { disambiguate_running = true; started = playback->get_current_file().has_value(); stopped = !started; } if (stopped) { update_file({}); } double position = playback->GetPosition(); if (!signals_handleable.emitted(PlaybackSignalSeeked) || signals_handleable.signal_unexpected(PlaybackSignalSeeked)) { seek_bar.set_value(position); } time_label.set_text(TimeToString(position, length_component_count)); std::vector classes; if (playback->IsPaused()) { classes.push_back("paused"); pause_btn.set_icon_name("media-playback-start"); } else { pause_btn.set_icon_name("media-playback-pause"); } if (length <= 0) { classes.push_back("no-length"); } if (playback_file.has_value()) { classes.push_back("loaded"); } if (signals_handleable.signal_unexpected(PlaybackSignalPitchChanged)) { pitch_slider.set_value(playback->GetPitch()); } if (signals_handleable.signal_unexpected(PlaybackSignalSpeedChanged)) { speed_slider.set_value(playback->GetSpeed()); } if (signals_handleable.signal_unexpected(PlaybackSignalTempoChanged)) { tempo_slider.set_value(playback->GetTempo()); } if (!running) { timer_stopped = true; g_mutex_lock(timer_mutex); g_cond_signal(timer_stopped_condition); g_mutex_unlock(timer_mutex); return false; } else { return true; } } void MainWindow::expect_signal(uint16_t signal) { expected_signals |= signal; } void MainWindow::update_dark_mode() { Gtk::Settings::get_default()->set_property("gtk-application-prefer-dark-theme", Looper::Options::get_option("ui.gtk.dark_mode", false)); } MainWindow::MainWindow(Playback *playback, Glib::RefPtr app) : playback(playback), open_dialog("Open some music...") { set_application(app); update_dark_mode(); open_dialog.set_transient_for(*this); open_dialog.set_modal(true); open_dialog.signal_response().connect([this](int response) { if (response == Gtk::ResponseType::OK) { this->expect_signal(PlaybackSignalStarted|PlaybackSignalFileChanged); this->playback->Start(this->open_dialog.get_file().get()->get_path()); } open_dialog.set_visible(false); }); open_dialog.add_button("_Cancel", Gtk::ResponseType::CANCEL); open_dialog.add_button("_Open", Gtk::ResponseType::OK); auto filter_text = Gtk::FileFilter::create(); filter_text->set_name("Music files"); filter_text->add_mime_type("audio/*"); open_dialog.add_filter(filter_text); set_title("Looper"); set_icon_name("looper"); set_name("main-window"); add_action("about_window", [this]() { aboutWindow.present(); }); add_action("options_window", [this]() { optionsWindow.present(); }); add_action("open_file", [this]() { this->open_dialog.present(); }); add_action("toggle_pause", [this]() { this->expect_signal((this->playback->IsPaused() ? PlaybackSignalResumed : PlaybackSignalPaused)); this->playback->Pause(); }); add_action("stop_playback", [this]() { this->playback->Stop(); }); add_action("restart_playback", [this]() { this->expect_signal(PlaybackSignalSeeked); this->playback->Seek(0.0); }); titleBox.set_orientation(Gtk::Orientation::VERTICAL); titleBox.set_valign(Gtk::Align::CENTER); titleLabel.set_text(get_title()); titleLabel.set_css_classes({"title"}); subTitleLabel.set_visible(false); subTitleLabel.set_css_classes({"subtitle"}); titleLabel.insert_at_end(titleBox); subTitleLabel.insert_at_end(titleBox); headerBar.set_title_widget(titleBox); set_default_size(400, 400); openBtn.set_label("Open..."); openBtn.set_name("open-btn"); openBtn.set_icon_name("folder-open"); openBtn.set_action_name("win.open_file"); headerBar.pack_start(openBtn); running = true; timer_stopped = false; timer_stopped_condition = (GCond*)malloc(sizeof(GCond)); timer_mutex = (GMutex*)malloc(sizeof(GMutex)); g_cond_init(timer_stopped_condition); g_mutex_init(timer_mutex); top_box.set_orientation(Gtk::Orientation::HORIZONTAL); top_box.set_name("top-box"); bottom_box.set_orientation(Gtk::Orientation::HORIZONTAL); bottom_box.set_name("bottom-box"); controls_box.set_orientation(Gtk::Orientation::VERTICAL); controls_box.set_name("controls-box"); main_box.set_orientation(Gtk::Orientation::VERTICAL); main_box.set_name("main-box"); //main_box.set_valign(Gtk::Align::END); main_box.set_vexpand(true); controls_box.insert_at_end(main_box); top_box.insert_at_end(controls_box); bottom_box.insert_at_end(controls_box); pause_btn.set_action_name("win.toggle_pause"); pause_btn.set_name("pause-btn"); pause_btn.insert_at_end(top_box); restart_btn.set_name("restart-btn"); restart_btn.set_icon_name("view-refresh"); restart_btn.set_action_name("win.restart_playback"); restart_btn.insert_at_end(top_box); time_label.set_name("time-label"); time_label.insert_at_end(top_box); seek_indeterminate.set_name("seek-placeholder"); seek_indeterminate.set_text(""); seek_indeterminate.set_hexpand(true); seek_indeterminate.insert_at_end(top_box); seek_bar.signal_change_value().connect([this](Gtk::ScrollType scrollType, double value) -> bool { this->expect_signal(PlaybackSignalSeeked); this->playback->Seek(value); return true; }, true); seek_bar.set_hexpand(); seek_bar.set_name("seek-bar"); seek_bar.insert_at_end(top_box); length_label.set_name("length-label"); length_label.insert_at_end(top_box); stop_btn.set_action_name("win.stop_playback"); stop_btn.set_name("stop-btn"); stop_btn.set_icon_name("media-playback-stop"); stop_btn.insert_at_end(top_box); volume_slider.set_min_value(0.0); volume_slider.set_max_value(100.0); volume_slider.set_digits_after_decimal(0); volume_slider.set_suffix("%"); volume_slider.set_prefix("Volume: "); volume_slider.set_name("volume-slider"); volume_slider.value_changed.connect([this](double value) { this->playback->SetVolume(value); }); volume_slider.set_hexpand(false); volume_slider.insert_at_end(top_box); speed_slider.set_hexpand(); speed_slider.set_digits_after_decimal(2); speed_slider.set_prefix("Speed: "); speed_slider.set_suffix("x"); speed_slider.set_min_value(0.25); speed_slider.set_max_value(4); speed_slider.set_logarithmic(); speed_slider.set_value(playback->GetSpeed()); speed_slider.set_name("speed-slider"); speed_slider.value_changed.connect([this](double value) { this->expect_signal(PlaybackSignalSpeedChanged); this->playback->SetSpeed(value); }); speed_slider.insert_at_end(bottom_box); tempo_slider.set_hexpand(); tempo_slider.set_digits_after_decimal(2); tempo_slider.set_prefix("Tempo: "); tempo_slider.set_suffix("x"); tempo_slider.set_min_value(0.25); tempo_slider.set_max_value(4); tempo_slider.set_logarithmic(); tempo_slider.set_value(playback->GetTempo()); tempo_slider.set_name("tempo-slider"); tempo_slider.value_changed.connect([this](double value) { this->expect_signal(PlaybackSignalTempoChanged); this->playback->SetTempo(value); }); tempo_slider.insert_at_end(bottom_box); pitch_slider.set_hexpand(); pitch_slider.set_digits_after_decimal(2); pitch_slider.set_prefix("Pitch: "); pitch_slider.set_suffix("x"); pitch_slider.set_min_value(0.25); pitch_slider.set_max_value(4); pitch_slider.set_logarithmic(); pitch_slider.set_value(playback->GetPitch()); pitch_slider.set_name("pitch-slider"); pitch_slider.value_changed.connect([this](double value) { this->expect_signal(PlaybackSignalPitchChanged); this->playback->SetPitch(value); }); pitch_slider.insert_at_end(bottom_box); /*dropSpaceLabel.set_text("Drop a music file here to play it..."); dropSpaceLabel.set_can_focus(false); dropSpaceLabel.set_can_target(false); dropSpaceLabel.set_name("drop-space-label");*/ dropSpace.set_can_target(true); dropTarget = Gtk::DropTarget::create(G_TYPE_FILE, Gdk::DragAction::LINK); dropSpace.add_controller(dropTarget); //stream_selection_box.set_child(dropSpaceLabel); dropSpace.set_expand(true); dropSpace.set_vexpand_set(true); dropSpace.set_name("drop-space-scrolled-window"); dropSpace.set_child(stream_selection_box); stream_selection_box.set_vexpand(true); stream_selection_box.set_hexpand(true); stream_selection_box.set_vexpand_set(); stream_selection_box.set_name("stream-selection-box"); dropTarget->signal_drop().connect([this](const Glib::ValueBase &value, double a, double b) { auto obj = g_value_get_object(value.gobj()); g_object_ref(obj); Gio::File file((GFile*)obj); this->expect_signal(PlaybackSignalStarted|PlaybackSignalFileChanged); this->playback->Start(file.get_path()); return false; }, false); dropSpace.insert_at_start(main_box); set_child(main_box); update_file(playback->get_current_file()); pitch_slider.set_value(playback->GetPitch(), true); speed_slider.set_value(playback->GetSpeed(), true); tempo_slider.set_value(playback->GetTempo(), true); volume_slider.set_value(playback->GetVolume(), true); optionsWindow.set_visible(false); optionsWindow.set_transient_for(*this); optionsWindow.set_hide_on_close(true); optionsWindow.set_modal(); aboutWindow.set_visible(false); aboutWindow.set_transient_for(*this); aboutWindow.set_hide_on_close(true); aboutWindow.set_modal(); main_menu = Gio::Menu::create(); auto file_menu = Gio::Menu::create(); file_menu->append("Open...", "win.open_file"); file_menu->append("About...", "win.about_window"); file_menu->append("Options...", "win.options_window"); file_menu->append("Quit", "app.quit"); auto playback_menu = Gio::Menu::create(); playback_menu->append("Pause/Resume", "win.toggle_pause"); playback_menu->append("Restart from Beginning", "win.restart_playback"); playback_menu->append("Stop", "win.stop_playback"); main_menu->append_submenu("File", file_menu); main_menu->append_submenu("Playback", playback_menu); app->set_menubar(main_menu); options_menu = Gio::Menu::create(); options_menu->append("About", "win.about_window"); options_menu->append("Options", "win.options_window"); set_show_menubar(false); menuPopover.set_menu_model(options_menu); menuPopover.set_autohide(); menuBtn.set_popover(menuPopover); menuBtn.set_icon_name("application-menu"); headerBar.pack_end(menuBtn); optionsWindow.optionsSaved.connect(sigc::mem_fun(*this, &MainWindow::update_dark_mode)); set_titlebar(headerBar); Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::update), 100); } MainWindow::~MainWindow() { running = false; while (!timer_stopped) { g_cond_wait(timer_stopped_condition, timer_mutex); } free(timer_stopped_condition); free(timer_mutex); }