/* This file is part of Patchage. * Copyright 2007-2013 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 #include #include #include #include #include #include #include #include #include #include #include #include #include "ganv/Module.hpp" #include "ganv/Edge.hpp" #include "patchage_config.h" #include "UIFile.hpp" #include "Patchage.hpp" #include "PatchageCanvas.hpp" #include "PatchageEvent.hpp" #include "StateManager.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 PATCHAGE_GTK_OSX #include static gboolean can_activate_cb(GtkWidget* widget, guint signal_id, gpointer data) { return gtk_widget_is_sensitive(widget); } #endif #ifdef HAVE_ALSA #include "AlsaDriver.hpp" #endif using std::cout; using std::endl; using std::string; struct ProjectList_column_record : public Gtk::TreeModel::ColumnRecord { Gtk::TreeModelColumn label; }; #define INIT_WIDGET(x) x(_xml, ((const char*)#x) + 1) Patchage::Patchage(int argc, char** argv) : _xml(UIFile::open("patchage")) #ifdef HAVE_ALSA , _alsa_driver(NULL) #endif , _jack_driver(NULL) , _state_manager(NULL) , INIT_WIDGET(_about_win) , INIT_WIDGET(_main_scrolledwin) , INIT_WIDGET(_main_win) , INIT_WIDGET(_menubar) , INIT_WIDGET(_menu_alsa_connect) , INIT_WIDGET(_menu_alsa_disconnect) , INIT_WIDGET(_menu_file_quit) , INIT_WIDGET(_menu_draw) , 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_messages) , INIT_WIDGET(_menu_view_refresh) , INIT_WIDGET(_menu_zoom_in) , INIT_WIDGET(_menu_zoom_out) , INIT_WIDGET(_menu_zoom_normal) , INIT_WIDGET(_menu_increase_font_size) , INIT_WIDGET(_menu_decrease_font_size) , INIT_WIDGET(_menu_normal_font_size) , INIT_WIDGET(_messages_clear_but) , INIT_WIDGET(_messages_close_but) , INIT_WIDGET(_messages_win) , INIT_WIDGET(_status_text) , _attach(true) , _driver_detached(false) , _refresh(false) , _enable_refresh(true) , _jack_driver_autoattach(true) #ifdef HAVE_ALSA , _alsa_driver_autoattach(true) #endif { _state_manager = new StateManager(); _canvas = boost::shared_ptr(new PatchageCanvas(this, 1600*2, 1200*2)); while (argc > 0) { if (!strcmp(*argv, "-h") || !strcmp(*argv, "--help")) { cout << "Usage: patchage [OPTION]..." << endl; cout << "Visually connect JACK and ALSA Audio/MIDI ports." << endl << endl; cout << "Options:" << endl; cout << "\t-h --help Show this help" << endl; cout << "\t-A --no-alsa Do not automatically attach to ALSA" << endl; cout << "\t-J --no-jack Do not automatically attack to JACK" << endl; 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"); _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)); #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->set_sensitive(false); #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_draw->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_draw)); _menu_view_refresh->signal_activate().connect( sigc::mem_fun(this, &Patchage::refresh)); _menu_view_arrange->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_arrange)); _menu_view_messages->signal_activate().connect( sigc::mem_fun(this, &Patchage::on_show_messages)); _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_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)); _messages_clear_but->signal_clicked().connect( sigc::mem_fun(this, &Patchage::on_messages_clear)); _messages_close_but->signal_clicked().connect( sigc::mem_fun(this, &Patchage::on_messages_close)); _error_tag = Gtk::TextTag::create(); _error_tag->property_foreground() = "#FF0000"; _status_text->get_buffer()->get_tag_table()->add(_error_tag); _warning_tag = Gtk::TextTag::create(); _warning_tag->property_foreground() = "#FFFF00"; _status_text->get_buffer()->get_tag_table()->add(_warning_tag); _canvas->widget().show(); _main_win->present(); _state_manager->load(); _main_win->resize( static_cast(_state_manager->get_window_size().x), static_cast(_state_manager->get_window_size().y)); _main_win->move( static_cast(_state_manager->get_window_location().x), static_cast(_state_manager->get_window_location().y)); _about_win->set_transient_for(*_main_win); #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(); _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; gtk_osxapplication_ready(osxapp); _menubar->hide(); gtk_osxapplication_set_menu_bar(osxapp, GTK_MENU_SHELL(_menubar->gobj())); gtk_osxapplication_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), NULL); #endif } Patchage::~Patchage() { store_window_location(); _state_manager->save(); #if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS) delete _jack_driver; #endif #ifdef HAVE_ALSA delete _alsa_driver; #endif delete _state_manager; _about_win.destroy(); _messages_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(); #endif _enable_refresh = true; refresh(); } bool Patchage::idle_callback() { // Initial run, attach if (_attach) { attach(); _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; return true; } void Patchage::zoom(double z) { _state_manager->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 } } /** Update the stored window location and size in the StateManager (in memory). */ void Patchage::store_window_location() { int loc_x, loc_y, size_x, size_y; _main_win->get_position(loc_x, loc_y); _main_win->get_size(size_x, size_y); Coord window_location; window_location.x = loc_x; window_location.y = loc_y; Coord window_size; window_size.x = size_x; window_size.y = size_y; _state_manager->set_window_location(window_location); _state_manager->set_window_size(window_size); } void Patchage::error_msg(const std::string& msg) { Glib::RefPtr buffer = _status_text->get_buffer(); buffer->insert_with_tag(buffer->end(), msg + "\n", _error_tag); _status_text->scroll_to_mark(buffer->get_insert(), 0); _messages_win->present(); } void Patchage::info_msg(const std::string& msg) { Glib::RefPtr buffer = _status_text->get_buffer(); buffer->insert(buffer->end(), msg + "\n"); _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(), msg + "\n", _warning_tag); _status_text->scroll_to_mark(buffer->get_insert(), 0); } static void load_module_location(GanvNode* node, void* data) { if (GANV_IS_MODULE(node)) { Ganv::Module* cmodule = Glib::wrap(GANV_MODULE(node)); PatchageModule* pmodule = dynamic_cast(cmodule); if (pmodule) { pmodule->load_location(); } } } void Patchage::update_state() { _canvas->for_each_node(load_module_location, NULL); } /** 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() << "' &" << endl; } 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(), NULL, close ? JackSessionSaveAndQuit : JackSessionSave, path.c_str()); const std::string script_path = path + "jack-session"; std::ofstream script(script_path.c_str()); script << "#!/bin/sh" << endl << endl; 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 << " &" << endl; } script << endl; script << "sleep 3" << endl; script << endl; _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() { assert(_canvas); _canvas->arrange(); } void Patchage::on_help_about() { _about_win->run(); _about_win->hide(); } void Patchage::on_zoom_in() { _canvas->set_zoom(_canvas->get_zoom() + 0.1); } void Patchage::on_zoom_out() { _canvas->set_zoom(_canvas->get_zoom() - 0.1); } void Patchage::on_zoom_normal() { _canvas->set_zoom(1.0); } void Patchage::on_increase_font_size() { _canvas->set_font_size(_canvas->get_font_size() + 1.0); } void Patchage::on_decrease_font_size() { _canvas->set_font_size(_canvas->get_font_size() - 1.0); } void Patchage::on_normal_font_size() { _canvas->set_font_size(_canvas->get_default_font_size()); } void Patchage::on_messages_clear() { _status_text->get_buffer()->erase( _status_text->get_buffer()->begin(), _status_text->get_buffer()->end()); } void Patchage::on_messages_close() { _messages_win->hide(); } 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(); _canvas.reset(); } void Patchage::on_draw() { Gtk::FileChooserDialog dialog("Draw to DOT", Gtk::FILE_CHOOSER_ACTION_SAVE); dialog.set_transient_for(*_main_win); dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); Gtk::Button* save_button = dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); save_button->property_has_default() = true; if (dialog.run() == Gtk::RESPONSE_OK) { std::string filename = dialog.get_filename(); if (filename.find(".") == std::string::npos) filename += ".dot"; if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) { Gtk::MessageDialog dialog( std::string("File exists! Overwrite ") + filename + "?", true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true); dialog.set_transient_for(*_main_win); if (dialog.run() != Gtk::RESPONSE_YES) { return; } } _canvas->export_dot(filename.c_str()); } } void Patchage::on_show_messages() { _messages_win->present(); } bool Patchage::on_scroll(GdkEventScroll* ev) { return false; }