// Copyright 2007-2021 David Robillard // SPDX-License-Identifier: GPL-3.0-or-later #include "Patchage.hpp" #include "Action.hpp" #include "AudioDriver.hpp" #include "Canvas.hpp" #include "CanvasModule.hpp" #include "CanvasPort.hpp" #include "Configuration.hpp" #include "Coord.hpp" #include "Driver.hpp" #include "Drivers.hpp" #include "Event.hpp" #include "Legend.hpp" #include "Options.hpp" #include "PortType.hpp" #include "Reactor.hpp" #include "Setting.hpp" #include "TextViewLog.hpp" #include "UIFile.hpp" #include "Widget.hpp" #include "event_to_string.hpp" #include "handle_event.hpp" #include "patchage_config.h" // IWYU pragma: keep #include "warnings.hpp" PATCHAGE_DISABLE_GANV_WARNINGS #include "ganv/Edge.hpp" #include "ganv/Module.hpp" #include "ganv/Node.hpp" #include "ganv/Port.hpp" #include "ganv/module.h" #include "ganv/types.h" PATCHAGE_RESTORE_WARNINGS PATCHAGE_DISABLE_FMT_WARNINGS #include PATCHAGE_RESTORE_WARNINGS #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef PATCHAGE_GTK_OSX # include # include namespace { gboolean can_activate_cb(GtkWidget* widget, guint, gpointer) { return gtk_widget_is_sensitive(widget); } void terminate_cb(GtkosxApplication*, gpointer data) { auto* patchage = static_cast(data); patchage->save(); Gtk::Main::quit(); } } // namespace #endif namespace patchage { namespace { bool configure_cb(GtkWindow*, GdkEvent*, gpointer data) { static_cast(data)->store_window_location(); return FALSE; } int port_order(const GanvPort* a, const GanvPort* b, void*) { const auto* pa = dynamic_cast(Glib::wrap(a)); const auto* pb = dynamic_cast(Glib::wrap(b)); if (pa && pb) { if (pa->order() && pb->order()) { return *pa->order() - *pb->order(); } if (pa->order()) { return -1; } if (pb->order()) { return 1; } return pa->name().compare(pb->name()); } return 0; } template void on_setting_toggled(Reactor* const reactor, const Gtk::CheckMenuItem* const item) { (*reactor)(action::ChangeSetting{{S{item->get_active()}}}); } } // namespace #define INIT_WIDGET(x) x(_xml, (#x) + 1) Patchage::Patchage(Options options) : _xml(UIFile::open("patchage")) , INIT_WIDGET(_about_win) , INIT_WIDGET(_main_scrolledwin) , INIT_WIDGET(_main_win) , INIT_WIDGET(_main_vbox) , INIT_WIDGET(_menubar) , INIT_WIDGET(_menu_alsa_connect) , INIT_WIDGET(_menu_alsa_disconnect) , INIT_WIDGET(_menu_file_quit) , INIT_WIDGET(_menu_export_image) , INIT_WIDGET(_menu_help_about) , INIT_WIDGET(_menu_jack_connect) , INIT_WIDGET(_menu_jack_disconnect) , INIT_WIDGET(_menu_view_arrange) , INIT_WIDGET(_menu_view_sprung_layout) , INIT_WIDGET(_menu_view_messages) , INIT_WIDGET(_menu_view_toolbar) , INIT_WIDGET(_menu_view_refresh) , INIT_WIDGET(_menu_view_human_names) , INIT_WIDGET(_menu_view_sort_ports) , INIT_WIDGET(_menu_zoom_in) , INIT_WIDGET(_menu_zoom_out) , INIT_WIDGET(_menu_zoom_normal) , INIT_WIDGET(_menu_zoom_full) , INIT_WIDGET(_menu_increase_font_size) , INIT_WIDGET(_menu_decrease_font_size) , INIT_WIDGET(_menu_normal_font_size) , INIT_WIDGET(_toolbar) , INIT_WIDGET(_clear_load_but) , INIT_WIDGET(_dropouts_label) , INIT_WIDGET(_buf_size_combo) , INIT_WIDGET(_latency_label) , INIT_WIDGET(_legend_alignment) , INIT_WIDGET(_main_paned) , INIT_WIDGET(_log_scrolledwindow) , INIT_WIDGET(_status_text) , _conf([this](const Setting& setting) { on_conf_change(setting); }) , _log(_status_text) , _canvas(new Canvas{_log, _action_sink, 1600 * 2, 1200 * 2}) , _drivers(_log, [this](const Event& event) { on_driver_event(event); }) , _reactor(_conf, _drivers, *_canvas, _log) , _action_sink([this](const Action& action) { _reactor(action); }) , _options{options} { Glib::set_application_name("Patchage"); _about_win->property_program_name() = "Patchage"; _about_win->property_logo_icon_name() = "patchage"; gtk_window_set_default_icon_name("patchage"); // Create list model for buffer size selector Glib::RefPtr buf_size_store = Gtk::ListStore::create(_buf_size_columns); for (size_t i = 32; i <= 4096; i *= 2) { Gtk::TreeModel::Row row = *(buf_size_store->append()); row[_buf_size_columns.label] = std::to_string(i); } _buf_size_combo->set_model(buf_size_store); _buf_size_combo->pack_start(_buf_size_columns.label); _main_scrolledwin->add(_canvas->widget()); _main_scrolledwin->property_hadjustment().get_value()->set_step_increment(10); _main_scrolledwin->property_vadjustment().get_value()->set_step_increment(10); _main_scrolledwin->signal_scroll_event().connect( sigc::mem_fun(this, &Patchage::on_scroll)); _clear_load_but->signal_clicked().connect( sigc::mem_fun(this, &Patchage::clear_load)); _buf_size_combo->signal_changed().connect( sigc::mem_fun(this, &Patchage::buffer_size_changed)); _status_text->signal_size_allocate().connect( sigc::mem_fun(this, &Patchage::on_messages_resized)); _menu_file_quit->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_quit)); _menu_export_image->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_export_image)); _menu_view_refresh->signal_activate().connect(sigc::bind( sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::Refresh{}})); _menu_view_human_names->signal_activate().connect( sigc::bind(sigc::ptr_fun(&on_setting_toggled), &_reactor, _menu_view_human_names.get())); _menu_view_sort_ports->signal_activate().connect( sigc::bind(sigc::ptr_fun(&on_setting_toggled), &_reactor, _menu_view_sort_ports.get())); _menu_view_arrange->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_arrange)); _menu_view_sprung_layout->signal_activate().connect( sigc::bind(sigc::ptr_fun(&on_setting_toggled), &_reactor, _menu_view_sprung_layout.get())); _menu_view_messages->signal_activate().connect( sigc::bind(sigc::ptr_fun(&on_setting_toggled), &_reactor, _menu_view_messages.get())); _menu_view_toolbar->signal_activate().connect( sigc::bind(sigc::ptr_fun(&on_setting_toggled), &_reactor, _menu_view_toolbar.get())); _menu_help_about->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_help_about)); _menu_zoom_in->signal_activate().connect(sigc::bind( sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::ZoomIn{}})); _menu_zoom_out->signal_activate().connect(sigc::bind( sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::ZoomOut{}})); _menu_zoom_normal->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::ZoomNormal{}})); _menu_zoom_full->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::ZoomFull{}})); _menu_increase_font_size->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::IncreaseFontSize{}})); _menu_decrease_font_size->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::DecreaseFontSize{}})); _menu_normal_font_size->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &Patchage::on_menu_action), Action{action::ResetFontSize{}})); if (_canvas->supports_sprung_layout()) { _menu_view_sprung_layout->set_active(true); } else { _menu_view_sprung_layout->set_active(false); _menu_view_sprung_layout->set_sensitive(false); } // Present window so that display attributes like font size are available _canvas->widget().show(); _main_win->present(); // Set the default font size based on the current GUI environment _conf.set(_canvas->get_default_font_size()); // Load configuration file (but do not apply it yet, see below) _conf.load(); _legend = new Legend(_conf); _legend->signal_color_changed.connect( sigc::mem_fun(this, &Patchage::on_legend_color_change)); _legend_alignment->add(*Gtk::manage(_legend)); _legend->show_all(); _about_win->set_transient_for(*_main_win); #ifdef __APPLE__ try { _about_win->set_logo(Gdk::Pixbuf::create_from_file( bundle_location() + "/Resources/Patchage.icns")); } catch (const Glib::Exception& e) { _log.error(fmt::format("Failed to set logo ({})", std::string(e.what()))); } #endif // Enable JACK menu items if driver is present if (_drivers.jack()) { _menu_jack_connect->signal_activate().connect(sigc::bind( sigc::mem_fun(_drivers.jack().get(), &AudioDriver::attach), true)); _menu_jack_disconnect->signal_activate().connect( sigc::mem_fun(_drivers.jack().get(), &AudioDriver::detach)); } else { _menu_jack_connect->set_sensitive(false); _menu_jack_disconnect->set_sensitive(false); } // Enable ALSA menu items if driver is present if (_drivers.alsa()) { _menu_alsa_connect->signal_activate().connect( sigc::bind(sigc::mem_fun(_drivers.alsa().get(), &Driver::attach), false)); _menu_alsa_disconnect->signal_activate().connect( sigc::mem_fun(_drivers.alsa().get(), &Driver::detach)); } else { _menu_alsa_connect->set_sensitive(false); _menu_alsa_disconnect->set_sensitive(false); } g_signal_connect( _main_win->gobj(), "configure-event", G_CALLBACK(configure_cb), this); _canvas->widget().grab_focus(); #ifdef PATCHAGE_GTK_OSX // Set up Mac menu bar GtkosxApplication* osxapp = static_cast( g_object_new(GTKOSX_TYPE_APPLICATION, nullptr)); _menubar->hide(); _menu_file_quit->hide(); gtkosx_application_set_menu_bar(osxapp, GTK_MENU_SHELL(_menubar->gobj())); gtkosx_application_insert_app_menu_item( osxapp, GTK_WIDGET(_menu_help_about->gobj()), 0); g_signal_connect(_menubar->gobj(), "can-activate-accel", G_CALLBACK(can_activate_cb), nullptr); g_signal_connect( osxapp, "NSApplicationWillTerminate", G_CALLBACK(terminate_cb), this); gtkosx_application_ready(osxapp); #endif // Apply all configuration settings to ensure the GUI is synced _conf.each([this](const Setting& setting) { on_conf_change(setting); }); // Set up an idle callback to process events and update the GUI if necessary Glib::signal_timeout().connect(sigc::mem_fun(this, &Patchage::idle_callback), 100); } Patchage::~Patchage() { _about_win.destroy(); _xml.reset(); } void Patchage::attach() { if (_drivers.jack() && _options.jack_driver_autoattach) { _drivers.jack()->attach(true); } if (_drivers.alsa() && _options.alsa_driver_autoattach) { _drivers.alsa()->attach(false); } process_events(); update_toolbar(); } bool Patchage::idle_callback() { // Initial run, attach if (_attach) { attach(); _menu_view_messages->set_active(_conf.get()); _attach = false; } // Process any events from drivers process_events(); // Update load every 5 idle callbacks static int count = 0; if (++count == 5) { update_load(); count = 0; } return true; } void Patchage::update_toolbar() { static bool updating = false; if (updating) { return; } updating = true; if (_drivers.jack() && _drivers.jack()->is_attached()) { const auto buffer_size = _drivers.jack()->buffer_size(); const auto sample_rate = _drivers.jack()->sample_rate(); if (sample_rate != 0) { const auto latency_ms = buffer_size * 1000 / static_cast(sample_rate); _latency_label->set_label(fmt::format( " frames @ {} kHz ({:0.2f} ms)", sample_rate / 1000, latency_ms)); _latency_label->set_visible(true); _buf_size_combo->set_active( static_cast(log2f(_drivers.jack()->buffer_size()) - 5)); updating = false; return; } } _latency_label->set_visible(false); updating = false; } bool Patchage::update_load() { if (_drivers.jack() && _drivers.jack()->is_attached()) { const auto xruns = _drivers.jack()->xruns(); if (xruns > 0u) { _dropouts_label->set_text(fmt::format(" Dropouts: {}", xruns)); _dropouts_label->show(); _clear_load_but->show(); } else { _dropouts_label->set_text(" Dropouts: 0"); _dropouts_label->hide(); _clear_load_but->hide(); } } return true; } void Patchage::store_window_location() { int loc_x = 0; int loc_y = 0; _main_win->get_position(loc_x, loc_y); int size_x = 0; int size_y = 0; _main_win->get_size(size_x, size_y); _conf.set( {static_cast(loc_x), static_cast(loc_y)}); _conf.set( {static_cast(size_x), static_cast(size_y)}); } void Patchage::clear_load() { _dropouts_label->set_text(" Dropouts: 0"); _dropouts_label->hide(); _clear_load_but->hide(); if (_drivers.jack()) { _drivers.jack()->reset_xruns(); } } void Patchage::operator()(const setting::AlsaAttached& setting) { if (setting.value) { _menu_alsa_connect->set_sensitive(false); _menu_alsa_disconnect->set_sensitive(true); if (_drivers.alsa()) { _drivers.alsa()->refresh([this](const Event& event) { handle_event(_conf, _metadata, *_canvas, _log, event); }); } } else { _menu_alsa_connect->set_sensitive(true); _menu_alsa_disconnect->set_sensitive(false); _canvas->remove_ports([](const CanvasPort* port) { return port->type() == PortType::alsa_midi; }); } } void Patchage::operator()(const setting::JackAttached& setting) { if (setting.value) { _menu_jack_connect->set_sensitive(false); _menu_jack_disconnect->set_sensitive(true); if (_drivers.jack()) { _drivers.jack()->refresh([this](const Event& event) { handle_event(_conf, _metadata, *_canvas, _log, event); }); } } else { _menu_jack_connect->set_sensitive(true); _menu_jack_disconnect->set_sensitive(false); _canvas->remove_ports([](const CanvasPort* port) { return (port->type() == PortType::jack_audio || port->type() == PortType::jack_midi || port->type() == PortType::jack_osc || port->type() == PortType::jack_cv); }); } } void Patchage::operator()(const setting::FontSize& setting) { if (static_cast(_canvas->get_font_size()) != setting.value) { _canvas->set_font_size(setting.value); } } static void update_labels(GanvNode* node, void* data) { const bool human_names = *static_cast(data); if (GANV_IS_MODULE(node)) { Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node)); auto* pmod = dynamic_cast(gmod); if (pmod) { for (Ganv::Port* gport : *gmod) { auto* pport = dynamic_cast(gport); if (pport) { pport->show_human_name(human_names); } } } } } void Patchage::operator()(const setting::HumanNames& setting) { bool human_names = setting.value; _menu_view_human_names->set_active(human_names); _canvas->for_each_node(update_labels, &human_names); } void Patchage::operator()(const setting::MessagesHeight& setting) { if (_log_scrolledwindow->is_visible()) { const int min_height = _log.min_height(); const int max_pos = _main_paned->get_allocation().get_height(); const int conf_height = setting.value; _main_paned->set_position(max_pos - std::max(conf_height, min_height)); } } void Patchage::operator()(const setting::MessagesVisible& setting) { if (setting.value) { _log_scrolledwindow->show(); _status_text->scroll_to_mark(_status_text->get_buffer()->get_insert(), 0); } else { _log_scrolledwindow->hide(); } _menu_view_messages->set_active(setting.value); } inline guint highlight_color(guint c, guint delta) { const guint max_char = 255; const guint r = MIN((c >> 24) + delta, max_char); const guint g = MIN(((c >> 16) & 0xFF) + delta, max_char); const guint b = MIN(((c >> 8) & 0xFF) + delta, max_char); const guint a = c & 0xFF; return ((r << 24u) | (g << 16u) | (b << 8u) | a); } static void update_port_colors(GanvNode* node, void* data) { auto* patchage = static_cast(data); if (!GANV_IS_MODULE(node)) { return; } Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node)); auto* pmod = dynamic_cast(gmod); if (!pmod) { return; } for (Ganv::Port* p : *pmod) { auto* port = dynamic_cast(p); if (port) { const uint32_t rgba = patchage->conf().get_port_color(port->type()); port->set_fill_color(rgba); port->set_border_color(highlight_color(rgba, 0x20)); } } } static void update_edge_color(GanvEdge* edge, void* data) { auto* patchage = static_cast(data); Ganv::Edge* edgemm = Glib::wrap(edge); if (edgemm) { auto* tail = dynamic_cast((edgemm)->get_tail()); if (tail) { edgemm->set_color(patchage->conf().get_port_color(tail->type())); } } } void Patchage::operator()(const setting::PortColor&) { _canvas->for_each_node(update_port_colors, this); _canvas->for_each_edge(update_edge_color, this); } void Patchage::operator()(const setting::SortedPorts& setting) { _menu_view_sort_ports->set_active(setting.value); if (setting.value) { _canvas->set_port_order(port_order, nullptr); } else { _canvas->set_port_order(nullptr, nullptr); } } void Patchage::operator()(const setting::SprungLayout& setting) { _canvas->set_sprung_layout(setting.value); _menu_view_sprung_layout->set_active(setting.value); } void Patchage::operator()(const setting::ToolbarVisible& setting) { if (setting.value) { _toolbar->show(); _menu_view_toolbar->set_active(true); } else { _toolbar->hide(); _menu_view_toolbar->set_active(false); } } void Patchage::operator()(const setting::WindowLocation& setting) { const int new_x = static_cast(setting.value.x); const int new_y = static_cast(setting.value.y); int current_x = 0; int current_y = 0; _main_win->get_position(current_x, current_y); if (new_x != current_x || new_y != current_y) { _main_win->move(new_x, new_y); } } void Patchage::operator()(const setting::WindowSize& setting) { const int new_w = static_cast(setting.value.x); const int new_h = static_cast(setting.value.y); int current_w = 0; int current_h = 0; _main_win->get_size(current_w, current_h); if (new_w != current_w || new_h != current_h) { _main_win->resize(new_w, new_h); } } void Patchage::operator()(const setting::Zoom& setting) { if (static_cast(_canvas->get_zoom()) != setting.value) { _canvas->set_zoom(setting.value); } } void Patchage::on_driver_event(const Event& event) { std::lock_guard lock{_events_mutex}; _driver_events.emplace(event); } void Patchage::process_events() { std::lock_guard lock{_events_mutex}; while (!_driver_events.empty()) { const Event& event = _driver_events.front(); _log.info(event_to_string(event)); handle_event(_conf, _metadata, *_canvas, _log, event); _driver_events.pop(); } } void Patchage::on_conf_change(const Setting& setting) { std::visit(*this, setting); } void Patchage::on_arrange() { if (_canvas) { _canvas->arrange(); } } void Patchage::on_help_about() { _about_win->run(); _about_win->hide(); } void Patchage::on_legend_color_change(PortType id, const std::string&, uint32_t rgba) { _reactor(action::ChangeSetting{{setting::PortColor{id, rgba}}}); } void Patchage::on_messages_resized(Gtk::Allocation&) { const int max_pos = _main_paned->get_allocation().get_height(); _conf.set(max_pos - _main_paned->get_position()); } void Patchage::save() { _conf.set(_canvas->get_zoom()); // Can be changed by ganv _conf.save(); } void Patchage::quit() { _main_win->hide(); } void Patchage::on_quit() { if (_drivers.alsa()) { _drivers.alsa()->detach(); } if (_drivers.jack()) { _drivers.jack()->detach(); } _main_win->hide(); } void Patchage::on_export_image() { Gtk::FileChooserDialog dialog("Export Image", Gtk::FILE_CHOOSER_ACTION_SAVE); dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); dialog.set_default_response(Gtk::RESPONSE_OK); dialog.set_transient_for(*_main_win); using Types = std::map; Types types; types["*.dot"] = "Graphviz DOT"; types["*.pdf"] = "Portable Document Format"; types["*.ps"] = "PostScript"; types["*.svg"] = "Scalable Vector Graphics"; for (const auto& t : types) { Gtk::FileFilter filt; filt.add_pattern(t.first); filt.set_name(t.second); dialog.add_filter(filt); } auto* bg_but = new Gtk::CheckButton("Draw _Background", true); auto* extra = new Gtk::Alignment(1.0, 0.5, 0.0, 0.0); bg_but->set_active(true); extra->add(*Gtk::manage(bg_but)); extra->show_all(); dialog.set_extra_widget(*Gtk::manage(extra)); if (dialog.run() == Gtk::RESPONSE_OK) { const std::string filename = dialog.get_filename(); if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) { Gtk::MessageDialog confirm(std::string("File exists! Overwrite ") + filename + "?", true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true); confirm.set_transient_for(dialog); if (confirm.run() != Gtk::RESPONSE_YES) { return; } } _canvas->export_image(filename.c_str(), bg_but->get_active()); } } bool Patchage::on_scroll(GdkEventScroll*) { return false; } void Patchage::on_menu_action(const Action& action) { _reactor(action); } void Patchage::buffer_size_changed() { if (_drivers.jack()) { const int selected = _buf_size_combo->get_active_row_number(); if (selected == -1) { update_toolbar(); } else { const uint32_t buffer_size = 1u << (selected + 5); _drivers.jack()->set_buffer_size(buffer_size); update_toolbar(); } } } } // namespace patchage