looper/backends/ui/gtk/main_window.cpp
2024-05-02 14:52:11 -07:00

426 lines
No EOL
17 KiB
C++

#include "main_window.hpp"
#include <gtkmm.h>
#include "my_slider.hpp"
#include <filesystem>
#include <format>
#include <gtkmm/menubutton.h>
#include <util.hpp>
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<Glib::ustring> 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<std::string> 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<std::string> 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<Glib::ustring> 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<bool>("ui.gtk.dark_mode", false));
}
MainWindow::MainWindow(Playback *playback, Glib::RefPtr<Gtk::Application> 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);
}