/* This file is part of Patchage. * Copyright 2007-2020 David Robillard * * Patchage is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * Patchage is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. * * You should have received a copy of the GNU General Public License * along with Patchage. If not, see . */ #include "Patchage.hpp" #include "Configuration.hpp" #include "Legend.hpp" #include "PatchageCanvas.hpp" #include "PatchageEvent.hpp" #include "UIFile.hpp" #include "patchage_config.h" #include "warnings.hpp" #if defined(HAVE_JACK_DBUS) # include "JackDbusDriver.hpp" #elif defined(PATCHAGE_LIBJACK) # include "JackDriver.hpp" # include #endif #ifdef PATCHAGE_JACK_SESSION # include #endif #ifdef HAVE_ALSA # include "AlsaDriver.hpp" #endif PATCHAGE_DISABLE_GANV_WARNINGS #include "ganv/Edge.hpp" #include "ganv/Module.hpp" PATCHAGE_RESTORE_WARNINGS PATCHAGE_DISABLE_FMT_WARNINGS #include PATCHAGE_RESTORE_WARNINGS #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef PATCHAGE_GTK_OSX # include static gboolean can_activate_cb(GtkWidget* widget, guint signal_id, gpointer data) { return gtk_widget_is_sensitive(widget); } static void terminate_cb(GtkosxApplication* app, gpointer data) { Patchage* patchage = (Patchage*)data; patchage->save(); Gtk::Main::quit(); } #endif static bool configure_cb(GtkWindow*, GdkEvent*, gpointer data) { static_cast(data)->store_window_location(); return FALSE; } static 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; } struct ProjectList_column_record : public Gtk::TreeModel::ColumnRecord { Gtk::TreeModelColumn label; }; #define INIT_WIDGET(x) x(_xml, (#x) + 1) Patchage::Patchage(int argc, char** argv) : _xml(UIFile::open("patchage")) #ifdef HAVE_ALSA , _alsa_driver(nullptr) #endif , _jack_driver(nullptr) , _conf(nullptr) , _gtk_main(nullptr) , 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_open_session) , INIT_WIDGET(_menu_save_session) , INIT_WIDGET(_menu_save_close_session) , 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(_xrun_progress) , 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) , _legend(nullptr) , _pane_initialized(false) , _attach(true) , _driver_detached(false) , _refresh(false) , _enable_refresh(true) , _jack_driver_autoattach(true) #ifdef HAVE_ALSA , _alsa_driver_autoattach(true) #endif { _conf = new Configuration(); _canvas = std::make_shared(this, 1600 * 2, 1200 * 2); while (argc > 0) { if (!strcmp(*argv, "-h") || !strcmp(*argv, "--help")) { std::cout << "Usage: patchage [OPTION]...\n"; std::cout << "Visually connect JACK and ALSA Audio/MIDI ports.\n\n"; std::cout << "Options:\n"; std::cout << "\t-h --help Show this help\n"; std::cout << "\t-A --no-alsa Do not automatically attach to ALSA\n"; std::cout << "\t-J --no-jack Do not automatically attack to JACK\n"; exit(0); #ifdef HAVE_ALSA } else if (!strcmp(*argv, "-A") || !strcmp(*argv, "--no-alsa")) { _alsa_driver_autoattach = false; #endif #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) } else if (!strcmp(*argv, "-J") || !strcmp(*argv, "--no-jack")) { _jack_driver_autoattach = false; #endif } argv++; argc--; } 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)); #ifdef PATCHAGE_JACK_SESSION _menu_open_session->signal_activate().connect( sigc::mem_fun(this, &Patchage::show_open_session_dialog)); _menu_save_session->signal_activate().connect( sigc::mem_fun(this, &Patchage::show_save_session_dialog)); _menu_save_close_session->signal_activate().connect( sigc::mem_fun(this, &Patchage::show_save_close_session_dialog)); #else _menu_open_session->hide(); _menu_save_session->hide(); _menu_save_close_session->hide(); #endif #ifdef HAVE_ALSA _menu_alsa_connect->signal_activate().connect( sigc::mem_fun(this, &Patchage::menu_alsa_connect)); _menu_alsa_disconnect->signal_activate().connect( sigc::mem_fun(this, &Patchage::menu_alsa_disconnect)); #else _menu_alsa_connect->set_sensitive(false); _menu_alsa_disconnect->set_sensitive(false); #endif _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::mem_fun(this, &Patchage::refresh)); _menu_view_human_names->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_view_human_names)); _menu_view_sort_ports->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_view_sort_ports)); _menu_view_arrange->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_arrange)); _menu_view_sprung_layout->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_sprung_layout_toggled)); _menu_view_messages->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_view_messages)); _menu_view_toolbar->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_view_toolbar)); _menu_help_about->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_help_about)); _menu_zoom_in->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_zoom_in)); _menu_zoom_out->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_zoom_out)); _menu_zoom_normal->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_zoom_normal)); _menu_zoom_full->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_zoom_full)); _menu_increase_font_size->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_increase_font_size)); _menu_decrease_font_size->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_decrease_font_size)); _menu_normal_font_size->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_normal_font_size)); 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); } for (int s = Gtk::STATE_NORMAL; s <= Gtk::STATE_INSENSITIVE; ++s) { _status_text->modify_base(static_cast(s), Gdk::Color("#000000")); _status_text->modify_text(static_cast(s), Gdk::Color("#FFFFFF")); } _error_tag = Gtk::TextTag::create(); _error_tag->property_foreground() = "#CC0000"; _status_text->get_buffer()->get_tag_table()->add(_error_tag); _warning_tag = Gtk::TextTag::create(); _warning_tag->property_foreground() = "#C4A000"; _status_text->get_buffer()->get_tag_table()->add(_warning_tag); _canvas->widget().show(); _main_win->present(); _conf->set_font_size(_canvas->get_default_font_size()); _conf->load(); _canvas->set_zoom(_conf->get_zoom()); _canvas->set_font_size(_conf->get_font_size()); if (_conf->get_sort_ports()) { _canvas->set_port_order(port_order, nullptr); } _main_win->resize(static_cast(_conf->get_window_size().x), static_cast(_conf->get_window_size().y)); _main_win->move(static_cast(_conf->get_window_location().x), static_cast(_conf->get_window_location().y)); _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) { error_msg(fmt::format("failed to set logo ({})", e.what())); } #endif #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) _jack_driver = new JackDriver(this); _jack_driver->signal_detached.connect( sigc::mem_fun(this, &Patchage::driver_detached)); _menu_jack_connect->signal_activate().connect( sigc::bind(sigc::mem_fun(_jack_driver, &JackDriver::attach), true)); _menu_jack_disconnect->signal_activate().connect( sigc::mem_fun(_jack_driver, &JackDriver::detach)); #endif #ifdef HAVE_ALSA _alsa_driver = new AlsaDriver(this); #endif connect_widgets(); update_state(); _menu_view_toolbar->set_active(_conf->get_show_toolbar()); _menu_view_sprung_layout->set_active(_conf->get_sprung_layout()); _menu_view_sort_ports->set_active(_conf->get_sort_ports()); _status_text->set_pixels_inside_wrap(2); _status_text->set_left_margin(4); _status_text->set_right_margin(4); _status_text->set_pixels_below_lines(2); g_signal_connect( _main_win->gobj(), "configure-event", G_CALLBACK(configure_cb), this); _canvas->widget().grab_focus(); // Idle callback, check if we need to refresh Glib::signal_timeout().connect( sigc::mem_fun(this, &Patchage::idle_callback), 100); #ifdef PATCHAGE_GTK_OSX // Set up Mac menu bar GtkosxApplication* osxapp = (GtkosxApplication*)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 } Patchage::~Patchage() { #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) delete _jack_driver; #endif #ifdef HAVE_ALSA delete _alsa_driver; #endif delete _conf; _about_win.destroy(); _xml.reset(); } void Patchage::attach() { _enable_refresh = false; #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) if (_jack_driver_autoattach) { _jack_driver->attach(true); } #endif #ifdef HAVE_ALSA if (_alsa_driver_autoattach) { _alsa_driver->attach(false); } #endif _enable_refresh = true; refresh(); update_toolbar(); } bool Patchage::idle_callback() { // Initial run, attach if (_attach) { attach(); _menu_view_messages->set_active(_conf->get_show_messages()); _attach = false; } // Process any JACK events #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) if (_jack_driver) { _jack_driver->process_events(this); } #endif // Process any ALSA events #ifdef HAVE_ALSA if (_alsa_driver) { _alsa_driver->process_events(this); } #endif // Do a full refresh if (_refresh) { refresh(); } else if (_driver_detached) { #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) if (_jack_driver && !_jack_driver->is_attached()) { _jack_driver->destroy_all(); } #endif #ifdef HAVE_ALSA if (_alsa_driver && !_alsa_driver->is_attached()) { _alsa_driver->destroy_all(); } #endif } _refresh = false; _driver_detached = false; // 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 defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) if (_jack_driver->is_attached()) { const jack_nframes_t buffer_size = _jack_driver->buffer_size(); const jack_nframes_t sample_rate = _jack_driver->sample_rate(); if (sample_rate != 0) { const int latency_ms = lrintf(buffer_size * 1000 / float(sample_rate)); std::stringstream ss; ss << " frames @ " << (sample_rate / 1000) << "kHz (" << latency_ms << "ms)"; _latency_label->set_label(ss.str()); _latency_label->set_visible(true); _buf_size_combo->set_active( static_cast(log2f(_jack_driver->buffer_size()) - 5)); updating = false; return; } } #endif _latency_label->set_visible(false); updating = false; } bool Patchage::update_load() { #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) if (_jack_driver->is_attached()) { char buf[8]; snprintf(buf, sizeof(buf), "%u", _jack_driver->get_xruns()); _xrun_progress->set_text(std::string(buf) + " Dropouts"); _xrun_progress->set_fraction(_jack_driver->get_max_dsp_load()); } #endif return true; } void Patchage::zoom(double z) { _conf->set_zoom(z); _canvas->set_zoom(z); } void Patchage::refresh() { if (_canvas && _enable_refresh) { _canvas->clear(); #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) if (_jack_driver) { _jack_driver->refresh(); } #endif #ifdef HAVE_ALSA if (_alsa_driver) { _alsa_driver->refresh(); } #endif } } 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_window_location({double(loc_x), double(loc_y)}); _conf->set_window_size({double(size_x), double(size_y)}); } void Patchage::clear_load() { #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) _xrun_progress->set_fraction(0.0); _jack_driver->reset_xruns(); _jack_driver->reset_max_dsp_load(); #endif } void Patchage::error_msg(const std::string& msg) { Glib::RefPtr buffer = _status_text->get_buffer(); buffer->insert_with_tag(buffer->end(), std::string("\n") + msg, _error_tag); _status_text->scroll_to_mark(buffer->get_insert(), 0); _menu_view_messages->set_active(true); } void Patchage::info_msg(const std::string& msg) { Glib::RefPtr buffer = _status_text->get_buffer(); buffer->insert(buffer->end(), std::string("\n") + msg); _status_text->scroll_to_mark(buffer->get_insert(), 0); } void Patchage::warning_msg(const std::string& msg) { Glib::RefPtr buffer = _status_text->get_buffer(); buffer->insert_with_tag( buffer->end(), std::string("\n") + msg, _warning_tag); _status_text->scroll_to_mark(buffer->get_insert(), 0); } static void load_module_location(GanvNode* node, void*) { if (GANV_IS_MODULE(node)) { Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node)); auto* pmod = dynamic_cast(gmod); if (pmod) { pmod->load_location(); } } } void Patchage::update_state() { _canvas->for_each_node(load_module_location, nullptr); } /** Update the sensitivity status of menus to reflect the present. * * (eg. disable "Connect to Jack" when Patchage is already connected to Jack) */ void Patchage::connect_widgets() { #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) _jack_driver->signal_attached.connect(sigc::bind( sigc::mem_fun(*_menu_jack_connect, &Gtk::MenuItem::set_sensitive), false)); _jack_driver->signal_attached.connect( sigc::mem_fun(this, &Patchage::refresh)); _jack_driver->signal_attached.connect(sigc::bind( sigc::mem_fun(*_menu_jack_disconnect, &Gtk::MenuItem::set_sensitive), true)); _jack_driver->signal_detached.connect(sigc::bind( sigc::mem_fun(*_menu_jack_connect, &Gtk::MenuItem::set_sensitive), true)); _jack_driver->signal_detached.connect(sigc::bind( sigc::mem_fun(*_menu_jack_disconnect, &Gtk::MenuItem::set_sensitive), false)); #endif #ifdef HAVE_ALSA _alsa_driver->signal_attached.connect(sigc::bind( sigc::mem_fun(*_menu_alsa_connect, &Gtk::MenuItem::set_sensitive), false)); _alsa_driver->signal_attached.connect(sigc::bind( sigc::mem_fun(*_menu_alsa_disconnect, &Gtk::MenuItem::set_sensitive), true)); _alsa_driver->signal_detached.connect(sigc::bind( sigc::mem_fun(*_menu_alsa_connect, &Gtk::MenuItem::set_sensitive), true)); _alsa_driver->signal_detached.connect(sigc::bind( sigc::mem_fun(*_menu_alsa_disconnect, &Gtk::MenuItem::set_sensitive), false)); #endif } #ifdef PATCHAGE_JACK_SESSION void Patchage::show_open_session_dialog() { Gtk::FileChooserDialog dialog( *_main_win, "Open Session", Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER); dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); Gtk::Button* open_but = dialog.add_button(Gtk::Stock::OPEN, Gtk::RESPONSE_OK); open_but->property_has_default() = true; if (dialog.run() != Gtk::RESPONSE_OK) { return; } const std::string dir = dialog.get_filename(); if (g_chdir(dir.c_str())) { error_msg("Failed to switch to session directory " + dir); return; } if (system("./jack-session") < 0) { error_msg("Error executing `./jack-session' in " + dir); } else { info_msg("Loaded session " + dir); } } static void print_edge(GanvEdge* edge, void* data) { std::ofstream* script = (std::ofstream*)data; Ganv::Edge* edgemm = Glib::wrap(edge); PatchagePort* src = dynamic_cast((edgemm)->get_tail()); PatchagePort* dst = dynamic_cast((edgemm)->get_head()); if (!src || !dst || src->type() == ALSA_MIDI || dst->type() == ALSA_MIDI) { return; } (*script) << "jack_connect '" << src->full_name() << "' '" << dst->full_name() << "' &\n"; } void Patchage::save_session(bool close) { Gtk::FileChooserDialog dialog( *_main_win, "Save Session", Gtk::FILE_CHOOSER_ACTION_SAVE); dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); Gtk::Button* save_but = dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); save_but->property_has_default() = true; if (dialog.run() != Gtk::RESPONSE_OK) { return; } std::string path = dialog.get_filename(); if (g_mkdir_with_parents(path.c_str(), 0740)) { error_msg("Failed to create session directory " + path); return; } path += '/'; jack_session_command_t* cmd = jack_session_notify(_jack_driver->client(), nullptr, close ? JackSessionSaveAndQuit : JackSessionSave, path.c_str()); const std::string script_path = path + "jack-session"; std::ofstream script(script_path.c_str()); script << "#!/bin/sh\n\n"; const std::string var("${SESSION_DIR}"); for (int c = 0; cmd[c].uuid; ++c) { std::string command = cmd[c].command; const size_t index = command.find(var); if (index != string::npos) { command.replace(index, var.length(), cmd[c].client_name); } script << command << " &\n"; } script << "\nsleep 3\n\n"; _canvas->for_each_edge(print_edge, &script); script.close(); g_chmod(script_path.c_str(), 0740); } void Patchage::show_save_session_dialog() { save_session(false); } void Patchage::show_save_close_session_dialog() { save_session(true); } #endif #ifdef HAVE_ALSA void Patchage::menu_alsa_connect() { _alsa_driver->attach(false); _alsa_driver->refresh(); } void Patchage::menu_alsa_disconnect() { _alsa_driver->detach(); refresh(); } #endif void Patchage::on_arrange() { if (_canvas) { _canvas->arrange(); } } void Patchage::on_sprung_layout_toggled() { const bool sprung = _menu_view_sprung_layout->get_active(); _canvas->set_sprung_layout(sprung); _conf->set_sprung_layout(sprung); } void Patchage::on_help_about() { _about_win->run(); _about_win->hide(); } 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::on_view_human_names() { bool human_names = show_human_names(); _canvas->for_each_node(update_labels, &human_names); } void Patchage::on_view_sort_ports() { const bool sort_ports = this->sort_ports(); _canvas->set_port_order(sort_ports ? port_order : nullptr, nullptr); _conf->set_sort_ports(sort_ports); refresh(); } void Patchage::on_zoom_in() { const float zoom = _canvas->get_zoom() * 1.25; _canvas->set_zoom(zoom); _conf->set_zoom(zoom); } void Patchage::on_zoom_out() { const float zoom = _canvas->get_zoom() * 0.75; _canvas->set_zoom(zoom); _conf->set_zoom(zoom); } void Patchage::on_zoom_normal() { _canvas->set_zoom(1.0); _conf->set_zoom(1.0); } void Patchage::on_zoom_full() { _canvas->zoom_full(); _conf->set_zoom(_canvas->get_zoom()); } void Patchage::on_increase_font_size() { const float points = _canvas->get_font_size() + 1.0; _canvas->set_font_size(points); _conf->set_font_size(points); } void Patchage::on_decrease_font_size() { const float points = _canvas->get_font_size() - 1.0; _canvas->set_font_size(points); _conf->set_font_size(points); } void Patchage::on_normal_font_size() { _canvas->set_font_size(_canvas->get_default_font_size()); _conf->set_font_size(_canvas->get_default_font_size()); } static 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); auto* tail = dynamic_cast((edgemm)->get_tail()); if (tail) { edgemm->set_color(patchage->conf()->get_port_color(tail->type())); } } void Patchage::on_legend_color_change(PortType id, const std::string&, uint32_t rgba) { _conf->set_port_color(id, rgba); _canvas->for_each_node(update_port_colors, this); _canvas->for_each_edge(update_edge_color, this); } void Patchage::on_messages_resized(Gtk::Allocation&) { const int max_pos = _main_paned->get_allocation().get_height(); _conf->set_messages_height(max_pos - _main_paned->get_position()); } void Patchage::save() { _conf->set_zoom(_canvas->get_zoom()); // Can be changed by ganv _conf->save(); } void Patchage::on_quit() { #ifdef HAVE_ALSA _alsa_driver->detach(); #endif #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) _jack_driver->detach(); #endif _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()); } } void Patchage::on_view_messages() { if (_menu_view_messages->get_active()) { Glib::RefPtr buffer = _status_text->get_buffer(); if (!_pane_initialized) { int y = 0; int line_height = 0; _status_text->get_line_yrange(buffer->begin(), y, line_height); const int pad = _status_text->get_pixels_inside_wrap(); const int max_pos = _main_paned->get_allocation().get_height(); const int min_height = (line_height + 2 * pad); const int conf_height = _conf->get_messages_height(); _main_paned->set_position(max_pos - std::max(conf_height, min_height)); _pane_initialized = true; } _log_scrolledwindow->show(); _status_text->scroll_to_mark(_status_text->get_buffer()->get_insert(), 0); _conf->set_show_messages(true); } else { _log_scrolledwindow->hide(); _conf->set_show_messages(false); } } void Patchage::on_view_toolbar() { if (_menu_view_toolbar->get_active()) { _toolbar->show(); } else { _toolbar->hide(); } _conf->set_show_toolbar(_menu_view_toolbar->get_active()); } bool Patchage::on_scroll(GdkEventScroll*) { return false; } void Patchage::buffer_size_changed() { #if defined(HAVE_JACK) || defined(HAVE_JACK_DBUS) const int selected = _buf_size_combo->get_active_row_number(); if (selected == -1) { update_toolbar(); } else { const jack_nframes_t buffer_size = 1 << (selected + 5); _jack_driver->set_buffer_size(buffer_size); update_toolbar(); } #endif }