13
0
livetrax/gtk2_ardour/recorder_ui.cc
Ben Loftis 483047635c Rec page: implement Undo actions and shortcuts
* undo is not (currently) a Global action, it's an Editor action
* ... but we want the ability to undo a recording

One option would be to chagne Undo to a Global action, which would have a
 sizable impact on code and existing shortcuts.

Instead I'm choosing to implement a Rec-page-specific Undo action & shortcut

It's conceivable that someday we would want the Recorder page to ONLY undo
 record operations, and the Mixer page to ONLY undo mixer operations, or
 something like that.  This lays the foundation for that.
2022-12-16 13:18:37 -06:00

1881 lines
55 KiB
C++

/*
* Copyright (C) 2020 Robin Gareus <robin@gareus.org>
*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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 more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifdef WAF_BUILD
#include "gtk2ardour-config.h"
#endif
#include <algorithm>
#include <gtkmm/stock.h>
#include <gtkmm/menu.h>
#include <gtkmm/menuitem.h>
#include "pbd/string_convert.h"
#include "ardour/audioengine.h"
#include "ardour/audio_port.h"
#include "ardour/audio_track.h"
#include "ardour/io_plug.h"
#include "ardour/midi_port.h"
#include "ardour/midi_track.h"
#include "ardour/monitor_return.h"
#include "ardour/profile.h"
#include "ardour/region.h"
#include "ardour/session.h"
#include "ardour/solo_mute_release.h"
#include "gtkmm2ext/colors.h"
#include "gtkmm2ext/gtk_ui.h"
#include "gtkmm2ext/keyboard.h"
#include "gtkmm2ext/utils.h"
#include "gtkmm2ext/window_title.h"
#include "widgets/ardour_icon.h"
#include "widgets/prompter.h"
#include "widgets/tooltips.h"
#include "actions.h"
#include "ardour_dialog.h"
#include "ardour_ui.h"
#include "gui_thread.h"
#include "instrument_selector.h"
#include "public_editor.h"
#include "recorder_group_tabs.h"
#include "recorder_ui.h"
#include "timers.h"
#include "timers.h"
#include "track_record_axis.h"
#include "ui_config.h"
#include "utils.h"
#include "pbd/i18n.h"
using namespace ARDOUR;
using namespace Gtkmm2ext;
using namespace ArdourWidgets;
using namespace Gtk;
using namespace std;
using namespace Menu_Helpers;
#define PX_SCALE(px) std::max ((float)px, rintf ((float)px* UIConfiguration::instance ().get_ui_scale ()))
RecorderUI::RecorderUI ()
: Tabbable (_content, _("Recorder"), X_("recorder"))
, _toolbar_sep (1.0)
, _btn_rec_all (_("All"))
, _btn_rec_none (_("None"))
, _btn_rec_forget (_("Discard Last Take"))
, _btn_peak_reset (_("Reset Peak Hold"))
, _monitor_in_button (_("All In"))
, _monitor_disk_button (_("All Disk"))
, _btn_new_plist (_("New Playlist for All Tracks"))
, _btn_new_plist_rec (_("New Playlist for Rec-Armed"))
, _auto_input_button (_("Auto-Input"), ArdourButton::led_default_elements)
, _toolbar_button_height (SizeGroup::create (Gtk::SIZE_GROUP_VERTICAL))
, _toolbar_recarm_width (SizeGroup::create (Gtk::SIZE_GROUP_HORIZONTAL))
, _toolbar_monitoring_width (SizeGroup::create (Gtk::SIZE_GROUP_HORIZONTAL))
, _meter_box_width (50)
, _meter_area_cols (2)
, _vertical (false)
, _ruler_sep (1.0)
{
load_bindings ();
register_actions ();
_transport_ctrl.setup (ARDOUR_UI::instance ());
_transport_ctrl.map_actions ();
_transport_ctrl.set_no_show_all ();
signal_tabbed_changed.connect (sigc::mem_fun (*this, &RecorderUI::tabbed_changed));
/* monitoring */
_auto_input_button.set_related_action (ActionManager::get_action ("Transport", "ToggleAutoInput"));
_auto_input_button.set_name ("transport option button");
_monitor_in_button.set_related_action (ActionManager::get_action ("Transport", "SessionMonitorIn"));
_monitor_in_button.set_name ("monitor button");
_monitor_disk_button.set_related_action (ActionManager::get_action ("Transport", "SessionMonitorDisk"));
_monitor_disk_button.set_name ("monitor button");
/* rec all/none */
_recs_label.set_text(_("Arm Tracks:"));
_btn_rec_all.set_name ("generic button");
_btn_rec_all.set_related_action (ActionManager::get_action (X_("Recorder"), X_("arm-all")));
_btn_rec_none.set_name ("generic button");
_btn_rec_none.set_related_action (ActionManager::get_action (X_("Recorder"), X_("arm-none")));
_btn_rec_forget.set_name ("generic button");
_btn_rec_forget.set_related_action (ActionManager::get_action (X_("Editor"), X_("remove-last-capture")));
_btn_peak_reset.set_name ("generic button");
_btn_peak_reset.set_related_action (ActionManager::get_action (X_("Recorder"), X_("reset-input-peak-hold")));
/*playlists*/
_btn_new_plist.set_name ("generic button");
_btn_new_plist.set_related_action (ActionManager::get_action (X_("Editor"), X_("new-playlists-for-all-tracks")));
_btn_new_plist_rec.set_name ("generic button");
_btn_new_plist_rec.set_related_action (ActionManager::get_action (X_("Editor"), X_("new-playlists-for-armed-tracks")));
/* standardize some button width. */
_toolbar_recarm_width->add_widget (_btn_rec_none);
_toolbar_recarm_width->add_widget (_btn_rec_all);
_toolbar_monitoring_width->add_widget (_monitor_in_button);
_toolbar_monitoring_width->add_widget (_monitor_disk_button);
/* standardize some button heights. */
_toolbar_button_height->add_widget (_btn_rec_all);
_toolbar_button_height->add_widget (_btn_rec_none);
_toolbar_button_height->add_widget (_btn_rec_forget);
_toolbar_button_height->add_widget (_monitor_in_button);
_toolbar_button_height->add_widget (_monitor_disk_button);
_toolbar_button_height->add_widget (_auto_input_button);
_toolbar_button_height->add_widget (_btn_new_plist);
_toolbar_button_height->add_widget (_btn_new_plist_rec);
_meter_area.set_spacing (0);
_meter_area.pack_start (_meter_table, true, true);
_meter_area.signal_size_request().connect (sigc::mem_fun (*this, &RecorderUI::meter_area_size_request));
_meter_area.signal_size_allocate ().connect (mem_fun (this, &RecorderUI::meter_area_size_allocate));
_meter_scroller.add (_meter_area);
_meter_scroller.set_policy (POLICY_AUTOMATIC, POLICY_AUTOMATIC);
_scroller_base.set_can_focus ();
_scroller_base.add_events (Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK);
_scroller_base.signal_button_press_event().connect (sigc::mem_fun(*this, &RecorderUI::scroller_button_event));
_scroller_base.signal_button_release_event().connect (sigc::mem_fun(*this, &RecorderUI::scroller_button_event));
_scroller_base.set_size_request (-1, PX_SCALE (20));
_scroller_base.signal_expose_event ().connect (sigc::bind (sigc::ptr_fun (&ArdourWidgets::ArdourIcon::expose_with_text), &_scroller_base, ArdourWidgets::ArdourIcon::ShadedPlusSign,
_("Right-click or Double-click here\nto add Tracks")));
/* LAYOUT */
_rec_area.set_spacing (0);
_rec_area.pack_end (_scroller_base, true, true);
_rec_area.pack_end (_ruler_sep, false, false, 0);
/* HBox [ groups | tracks] */
_rec_group_tabs = new RecorderGroupTabs (this);
_rec_groups.pack_start (*_rec_group_tabs, false, false);
_rec_groups.pack_start (_rec_area, true, true);
/* Vertical scroll, all tracks */
_rec_scroller.add (_rec_groups);
_rec_scroller.set_shadow_type(SHADOW_IN);
_rec_scroller.set_policy (POLICY_NEVER, POLICY_AUTOMATIC);
/* HBox, ruler on top [ space above headers | time-ruler ] */
_ruler_box.pack_start (_space, false, false);
_ruler_box.pack_start (_ruler, true, true);
/* VBox, ruler + scroll-area for tracks */
_rec_container.pack_start (_ruler_box, false, false);
_rec_container.pack_start (_rec_scroller, true, true);
_pane.add (_rec_container);
_pane.add (_meter_scroller);
/* Top-level VBox */
_content.pack_start (_toolbar_sep, false, false, 1);
_content.pack_start (_toolbar, false, false, 2);
_content.pack_start (_pane, true, true);
/* button_table setup is similar to transport_table in ardour_ui */
int vpadding = 1;
int hpadding = 2;
int spacepad = 3;
int col = 0;
_button_table.attach (_transport_ctrl, col, col + 1, 0, 1, FILL, FILL, hpadding, vpadding);
col += 1;
_button_table.attach (_duration_info_box, col, col + 1, 0, 1, FILL, FILL, hpadding, vpadding);
_button_table.attach (_xrun_info_box, col + 1, col + 2, 0, 1, FILL, FILL, hpadding, vpadding);
_button_table.attach (_btn_rec_forget, col, col + 2, 1, 2, FILL, SHRINK, hpadding, vpadding);
col += 2;
_button_table.attach (*(manage (new ArdourVSpacer ())), col, col + 1, 0, 2, FILL, FILL, spacepad, vpadding);
col += 1;
_button_table.attach (_recs_label, col, col + 2, 0, 1, FILL, FILL, hpadding, vpadding);
_button_table.attach (_btn_rec_all, col, col + 1, 1, 2, FILL, FILL, hpadding, vpadding);
_button_table.attach (_btn_rec_none, col + 1, col + 2, 1, 2, FILL, FILL, hpadding, vpadding);
col += 2;
_button_table.attach (*(manage (new ArdourVSpacer ())), col, col + 1, 0, 2, FILL, FILL, spacepad, vpadding);
col += 1;
_button_table.attach (_auto_input_button, col, col + 2, 0, 1 , FILL, SHRINK, hpadding, vpadding);
_button_table.attach (_monitor_in_button, col, col + 1, 1, 2 , FILL, SHRINK, hpadding, vpadding);
_button_table.attach (_monitor_disk_button, col + 1, col + 2, 1, 2 , FILL, SHRINK, hpadding, vpadding);
col += 2;
_button_table.attach (*(manage (new ArdourVSpacer ())), col, col + 1, 0, 2, FILL, FILL, spacepad, vpadding);
col += 1;
_button_table.attach (_btn_new_plist, col, col + 2, 0, 1 , FILL, SHRINK, hpadding, vpadding);
_button_table.attach (_btn_new_plist_rec, col, col + 2, 1, 2 , FILL, SHRINK, hpadding, vpadding);
col += 2;
_button_table.attach (*(manage (new ArdourVSpacer ())), col, col + 1, 0, 2, FILL, FILL, spacepad, vpadding);
col += 1;
_toolbar.pack_start (_button_table, false, false);
_toolbar.pack_end (_btn_peak_reset, false, false, 4);
_toolbar.pack_end (_remain_info_box, false, false, 4);
/* tooltips */
set_tooltip (_btn_rec_all, _("Record enable all tracks"));
set_tooltip (_btn_rec_none, _("Disable recording of all tracks"));
set_tooltip (_btn_peak_reset, _("Reset peak-hold indicator of all input meters"));
set_tooltip (_auto_input_button, _("Track Input Monitoring automatically follows transport state"));
set_tooltip (_monitor_in_button, _("Force all tracks to monitor Input, unless they are explicitly set to monitor Disk"));
set_tooltip (_monitor_disk_button, _("Force all tracks to monitor Disk playback, unless they are explicitly set to Input"));
set_tooltip (_btn_new_plist, _("Create a new playlist for all tracks and switch to it."));
set_tooltip (_btn_new_plist_rec, _("Create a new playlist for all rec-armed tracks"));
set_tooltip (_xrun_info_box, _("X-runs: Soundcard buffer under- or over-run occurrences in the last recording take"));
set_tooltip (_remain_info_box, _("Remaining Time: Recording time available on the current disk with currently armed tracks"));
set_tooltip (_duration_info_box, _("Duration: Length of the most recent (or current) recording take"));
set_tooltip (_btn_rec_forget, _("Delete the region AND the audio files of the last recording take"));
/* show [almost] all */
_btn_rec_all.show ();
_btn_rec_none.show ();
_btn_rec_forget.show ();
_btn_peak_reset.show ();
_btn_new_plist.show ();
_btn_new_plist_rec.show ();
_button_table.show ();
_monitor_in_button.show ();
_monitor_disk_button.show ();
_auto_input_button.show ();
_space.show ();
_ruler_box.show ();
_ruler_sep.show ();
_scroller_base.show ();
_toolbar_sep.show ();
_rec_area.show ();
_rec_scroller.show ();
_rec_groups.show ();
_rec_group_tabs->show ();
_rec_container.show ();
_duration_info_box.show ();
_xrun_info_box.show ();
_remain_info_box.show ();
_meter_table.show ();
_meter_area.show ();
_meter_scroller.show ();
_pane.show ();
_content.show ();
/* setup keybidings */
_content.set_data ("ardour-bindings", bindings);
/* subscribe to signals */
AudioEngine::instance ()->Running.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::start_updating, this), gui_context ());
AudioEngine::instance ()->Stopped.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::stop_updating, this), gui_context ());
AudioEngine::instance ()->Halted.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::stop_updating, this), gui_context ());
AudioEngine::instance ()->PortConnectedOrDisconnected.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::port_connected_or_disconnected, this, _2, _4), gui_context ());
AudioEngine::instance ()->PortPrettyNameChanged.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::port_pretty_name_changed, this, _1), gui_context ());
AudioEngine::instance ()->PhysInputChanged.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::add_or_remove_io, this, _1, _2, _3), gui_context ());
PresentationInfo::Change.connect (*this, invalidator (*this), boost::bind (&RecorderUI::presentation_info_changed, this, _1), gui_context());
Config->ParameterChanged.connect (*this, invalidator (*this), boost::bind (&RecorderUI::parameter_changed, this, _1), gui_context ());
UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &RecorderUI::parameter_changed));
//ARDOUR_UI::instance()->Escape.connect (*this, invalidator (*this), boost::bind (&RecorderUI::escape, this), gui_context());
/* init */
update_title ();
update_sensitivity ();
float fract;
XMLNode const* settings = ARDOUR_UI::instance()->recorder_settings();
if (!settings || !settings->get_property ("recorder-vpane-pos", fract) || fract > 1.0) {
fract = 0.75f;
}
_pane.set_divider (0, fract);
}
RecorderUI::~RecorderUI ()
{
delete _rec_group_tabs;
}
void
RecorderUI::cleanup ()
{
_visible_recorders.clear ();
stop_updating ();
_engine_connections.drop_connections ();
}
Gtk::Window*
RecorderUI::use_own_window (bool and_fill_it)
{
bool new_window = !own_window ();
Gtk::Window* win = Tabbable::use_own_window (and_fill_it);
if (win && new_window) {
win->set_name ("RecorderWindow");
ARDOUR_UI::instance ()->setup_toplevel_window (*win, _("Recorder"), this);
win->signal_event ().connect (sigc::bind (sigc::ptr_fun (&Keyboard::catch_user_event_for_pre_dialog_focus), win));
win->set_data ("ardour-bindings", bindings);
update_title ();
#if 0 // TODO
if (!win->get_focus()) {
win->set_focus (scroller);
}
#endif
}
contents ().show ();
return win;
}
void
RecorderUI::tabbed_changed (bool tabbed)
{
if (tabbed) {
_transport_ctrl.hide ();
} else {
_transport_ctrl.show ();
}
}
XMLNode&
RecorderUI::get_state () const
{
XMLNode* node = new XMLNode (X_("Recorder"));
node->add_child_nocopy (Tabbable::get_state ());
node->set_property (X_("recorder-vpane-pos"), _pane.get_divider ());
return *node;
}
int
RecorderUI::set_state (const XMLNode& node, int version)
{
return Tabbable::set_state (node, version);
}
void
RecorderUI::load_bindings ()
{
bindings = Bindings::get_bindings (X_("Recorder"));
}
void
RecorderUI::register_actions ()
{
Glib::RefPtr<ActionGroup> group = ActionManager::create_action_group (bindings, X_("Recorder"));
ActionManager::register_action (group, "reset-input-peak-hold", _("Reset Input Peak Hold"), sigc::mem_fun (*this, &RecorderUI::peak_reset));
ActionManager::register_action (group, "arm-all", _("Record Arm All Tracks"), sigc::mem_fun (*this, &RecorderUI::arm_all));
ActionManager::register_action (group, "arm-none", _("Disable Record Arm of All Tracks"), sigc::mem_fun (*this, &RecorderUI::arm_none));
ActionManager::register_action (group, "rec-undo", _("Undo"), sigc::mem_fun (*this, &RecorderUI::rec_undo));
ActionManager::register_action (group, "rec-redo", _("Redo"), sigc::mem_fun (*this, &RecorderUI::rec_redo));
ActionManager::register_action (group, "alternate-rec-redo", _("Redo"), sigc::mem_fun (*this, &RecorderUI::rec_redo));
}
void
RecorderUI::set_session (Session* s)
{
SessionHandlePtr::set_session (s);
_ruler.set_session (s);
_duration_info_box.set_session (s);
_xrun_info_box.set_session (s);
_remain_info_box.set_session (s);
_transport_ctrl.set_session (s);
_rec_group_tabs->set_session (s);
update_sensitivity ();
if (!_session) {
_recorders.clear ();
_visible_recorders.clear ();
return;
}
XMLNode* node = ARDOUR_UI::instance()->recorder_settings();
set_state (*node, Stateful::loading_state_version);
_session->DirtyChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::update_title, this), gui_context ());
_session->StateSaved.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::update_title, this), gui_context ());
_session->RouteAdded.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::add_routes, this, _1), gui_context ());
TrackRecordAxis::CatchDeletion.connect (*this, invalidator (*this), boost::bind (&RecorderUI::remove_route, this, _1), gui_context ());
TrackRecordAxis::EditNextName.connect (*this, invalidator (*this), boost::bind (&RecorderUI::tra_name_edit, this, _1, _2), gui_context ());
_session->config.ParameterChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::parameter_changed, this, _1), gui_context ());
Region::RegionsPropertyChanged.connect (*this, invalidator (*this), boost::bind (&RecorderUI::regions_changed, this, _1, _2), gui_context());
_session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::gui_extents_changed, this), gui_context());
_session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::gui_extents_changed, this), gui_context());
_session->RecordStateChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::update_sensitivity, this), gui_context());
_session->UpdateRouteRecordState.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::update_recordstate, this), gui_context());
_session->IOPluginsChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::io_plugins_changed, this), gui_context());
/* map_parameters */
parameter_changed ("show-group-tabs");
update_title ();
initial_track_display ();
start_updating ();
}
void
RecorderUI::session_going_away ()
{
ENSURE_GUI_THREAD (*this, &RecorderUI::session_going_away);
SessionHandlePtr::session_going_away ();
update_title ();
}
void
RecorderUI::update_title ()
{
if (!own_window ()) {
return;
}
if (_session) {
string n;
if (_session->snap_name () != _session->name ()) {
n = _session->snap_name ();
} else {
n = _session->name ();
}
if (_session->dirty ()) {
n = "*" + n;
}
WindowTitle title (n);
title += S_("Window|Recorder");
title += Glib::get_application_name ();
own_window ()->set_title (title.get_string ());
} else {
WindowTitle title (S_("Window|Recorder"));
title += Glib::get_application_name ();
own_window ()->set_title (title.get_string ());
}
}
void
RecorderUI::update_sensitivity ()
{
const bool en = _session ? true : false;
const bool have_ms = Config->get_use_monitor_bus();
ActionManager::get_action (X_("Recorder"), X_("arm-all"))->set_sensitive (en);
ActionManager::get_action (X_("Recorder"), X_("arm-none"))->set_sensitive (en);
for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) {
i->second->allow_monitoring (have_ms && en);
i->second->set_sensitive (en);
if (!en) {
i->second->clear ();
}
}
}
void
RecorderUI::update_recordstate ()
{
for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) {
i->second->update_rec_stat ();
}
}
void
RecorderUI::update_monitorstate (std::string pn, bool en)
{
InputPortMap::iterator im = _input_ports.find (pn);
if (im != _input_ports.end()) {
im->second->update_monitorstate (en);
}
}
void
RecorderUI::parameter_changed (string const& p)
{
if (p == "input-meter-layout") {
start_updating ();
} else if (p == "input-meter-scopes") {
start_updating ();
} else if (p == "use-monitor-bus") {
bool have_ms = Config->get_use_monitor_bus();
for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) {
i->second->allow_monitoring (have_ms);
}
} else if (p == "show-group-tabs") {
bool const s = _session ? _session->config.get_show_group_tabs () : true;
if (s) {
_rec_group_tabs->show ();
} else {
_rec_group_tabs->hide ();
}
}
}
bool
RecorderUI::scroller_button_event (GdkEventButton* ev)
{
if ((ev->type == GDK_2BUTTON_PRESS && ev->button == 1) || (ev->type == GDK_BUTTON_RELEASE && Keyboard::is_context_menu_event (ev))) {
ARDOUR_UI::instance()->add_route ();
return true;
}
return false;
}
void
RecorderUI::start_updating ()
{
if (_input_ports.size ()) {
stop_updating ();
}
PortManager::AudioInputPorts const aip (AudioEngine::instance ()->audio_input_ports ());
PortManager::MIDIInputPorts const mip (AudioEngine::instance ()->midi_input_ports ());
size_t iop_audio = 0;
size_t iop_midi = 0;
boost::shared_ptr<IOPlugList> iop;
if (_session) {
iop = _session->io_plugs ();
for (auto& p : *iop) {
PortManager::AudioInputPorts const& aip (p->audio_input_ports ());
PortManager::MIDIInputPorts const& mip (p->midi_input_ports ());
iop_audio += aip.size ();
iop_midi += mip.size ();
}
}
if (aip.size () + mip.size () + iop_audio + iop_midi == 0) {
return;
}
switch (UIConfiguration::instance ().get_input_meter_layout ()) {
case LayoutAutomatic:
if (aip.size () + mip.size () + iop_audio + iop_midi > 16) {
_vertical = true;
} else {
_vertical = false;
}
break;
case LayoutVertical:
_vertical = true;
break;
case LayoutHorizontal:
_vertical = false;
break;
}
/* Audio */
for (PortManager::AudioInputPorts::const_iterator i = aip.begin (); i != aip.end (); ++i) {
_input_ports[i->first] = boost::shared_ptr<RecorderUI::InputPort> (new InputPort (i->first, DataType::AUDIO, this, _vertical));
set_connections (i->first);
}
/* MIDI */
for (PortManager::MIDIInputPorts::const_iterator i = mip.begin (); i != mip.end (); ++i) {
string pn = AudioEngine::instance()->get_pretty_name_by_name (i->first);
if (PortManager::port_is_control_only (pn)) {
continue;
}
_input_ports[i->first] = boost::shared_ptr<RecorderUI::InputPort> (new InputPort (i->first, DataType::MIDI, this, _vertical));
set_connections (i->first);
}
if (iop) {
for (auto const& p : *iop) {
io_plugin_add (p);
}
}
update_io_widget_labels ();
meter_area_layout ();
_meter_area.queue_resize ();
MonitorPort& mp (AudioEngine::instance()->monitor_port ());
mp.MonitorInputChanged.connect (_monitor_connection, invalidator (*this), boost::bind (&RecorderUI::update_monitorstate, this, _1, _2), gui_context());
const bool en = _session ? true : false;
const bool have_ms = Config->get_use_monitor_bus();
for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) {
i->second->update_monitorstate (mp.monitoring (i->first));
i->second->allow_monitoring (have_ms && en);
i->second->set_sensitive (en);
}
_fast_screen_update_connection.disconnect ();
/* https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#G-PRIORITY-HIGH-IDLE:CAPS */
_fast_screen_update_connection = Glib::signal_timeout().connect (sigc::mem_fun (*this, &RecorderUI::update_meters), 40, GDK_PRIORITY_REDRAW + 10);
}
void
RecorderUI::stop_updating ()
{
_fast_screen_update_connection.disconnect ();
_monitor_connection.disconnect ();
container_clear (_meter_table);
_input_ports.clear ();
_ioplugins.clear ();
_going_away_connections.drop_connections ();
}
void
RecorderUI::add_or_remove_io (DataType dt, vector<string> ports, bool add)
{
_fast_screen_update_connection.disconnect ();
bool spill_changed = false;
if (_input_ports.empty () && add) {
_monitor_connection.disconnect ();
MonitorPort& mp (AudioEngine::instance()->monitor_port ());
mp.MonitorInputChanged.connect (_monitor_connection, invalidator (*this), boost::bind (&RecorderUI::update_monitorstate, this, _1, _2), gui_context());
}
if (add) {
for (vector<string>::const_iterator i = ports.begin (); i != ports.end (); ++i) {
string pn = AudioEngine::instance()->get_pretty_name_by_name (*i);
if (dt==DataType::MIDI && PortManager::port_is_control_only (pn)) {
continue;
}
_input_ports[*i] = boost::shared_ptr<RecorderUI::InputPort> (new InputPort (*i, dt, this, _vertical));
set_connections (*i);
}
} else {
for (vector<string>::const_iterator i = ports.begin (); i != ports.end (); ++i) {
_input_ports.erase (*i);
spill_changed |= 0 != _spill_port_names.erase (*i);
}
}
post_add_remove (spill_changed);
}
void
RecorderUI::io_plugins_changed ()
{
_fast_screen_update_connection.disconnect ();
boost::shared_ptr<IOPlugList> iop (_session->io_plugs ());
for (auto& p : *iop) {
if (_ioplugins.find (p) != _ioplugins.end ()) {
continue;
}
io_plugin_add (p);
}
post_add_remove (false);
}
void
RecorderUI::io_plugin_add (boost::shared_ptr<IOPlug> p)
{
PortManager::AudioInputPorts const& aip (p->audio_input_ports ());
PortManager::MIDIInputPorts const& mip (p->midi_input_ports ());
_ioplugins.insert (p);
p->DropReferences.connect (_going_away_connections, invalidator (*this), boost::bind (&RecorderUI::io_plugin_going_away, this, boost::weak_ptr<IOPlug>(p)), gui_context ());
for (auto i = aip.begin (); i != aip.end (); ++i) {
_input_ports[i->first] = boost::shared_ptr<RecorderUI::InputPort> (new InputPort (i->first, DataType::AUDIO, this, _vertical, true));
set_connections (i->first);
}
for (auto i = mip.begin (); i != mip.end (); ++i) {
_input_ports[i->first] = boost::shared_ptr<RecorderUI::InputPort> (new InputPort (i->first, DataType::MIDI, this, _vertical, true));
set_connections (i->first);
}
}
void
RecorderUI::io_plugin_going_away (boost::weak_ptr<IOPlug> wp)
{
_fast_screen_update_connection.disconnect ();
bool spill_changed = false;
boost::shared_ptr<IOPlug> p = wp.lock ();
if (!p) {
assert (0);
return;
}
PortManager::AudioInputPorts const& aip (p->audio_input_ports ());
PortManager::MIDIInputPorts const& mip (p->midi_input_ports ());
for (auto i = aip.begin (); i != aip.end (); ++i) {
_input_ports.erase (i->first);
spill_changed |= 0 != _spill_port_names.erase (i->first);
}
for (auto i = mip.begin (); i != mip.end (); ++i) {
_input_ports.erase (i->first);
spill_changed |= 0 != _spill_port_names.erase (i->first);
}
_ioplugins.erase (p);
post_add_remove (spill_changed);
}
void
RecorderUI::post_add_remove (bool spill_changed)
{
update_io_widget_labels ();
update_sensitivity ();
meter_area_layout ();
_meter_area.queue_resize ();
if (spill_changed) {
update_rec_table_layout ();
}
if (_input_ports.size ()) {
_fast_screen_update_connection = Glib::signal_timeout().connect (sigc::mem_fun (*this, &RecorderUI::update_meters), 40, GDK_PRIORITY_REDRAW + 10);
}
}
void
RecorderUI::update_io_widget_labels ()
{
uint32_t n_audio = 0;
uint32_t n_midi = 0;
uint32_t n_io_audio = 0;
uint32_t n_io_midi = 0;
InputPortSet ips;
for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) {
ips.insert (i->second);
}
for (InputPortSet::const_iterator i = ips.begin (); i != ips.end (); ++i) {
boost::shared_ptr<InputPort> const& ip = *i;
if (ip->ioplug ()) {
switch (ip->data_type ()) {
case DataType::AUDIO:
ip->set_frame_label (string_compose (_("I/O Plugin Audio %1"), ++n_io_audio));
break;
case DataType::MIDI:
ip->set_frame_label (string_compose (_("I/O Plugin MIDI %1"), ++n_io_midi));
break;
}
} else {
switch (ip->data_type ()) {
case DataType::AUDIO:
ip->set_frame_label (string_compose (_("Audio Input %1"), ++n_audio));
break;
case DataType::MIDI:
ip->set_frame_label (string_compose (_("MIDI Input %1"), ++n_midi));
break;
}
}
}
}
bool
RecorderUI::update_meters ()
{
PortManager::AudioInputPorts const aip (AudioEngine::instance ()->audio_input_ports ());
boost::shared_ptr<IOPlugList> iop;
if (_session) {
iop = _session->io_plugs ();
}
/* scope data needs to be read continuously */
for (PortManager::AudioInputPorts::const_iterator i = aip.begin (); i != aip.end (); ++i) {
InputPortMap::iterator im = _input_ports.find (i->first);
if (im != _input_ports.end()) {
im->second->update (*(i->second.scope));
}
}
if (iop) {
for (auto const& p : *iop) {
PortManager::AudioInputPorts const& aip (p->audio_input_ports ());
for (auto i = aip.begin (); i != aip.end (); ++i) {
InputPortMap::iterator im = _input_ports.find (i->first);
if (im != _input_ports.end()) {
im->second->update (*(i->second.scope));
}
}
}
}
if (!contents ().get_mapped ()) {
return true;
}
for (PortManager::AudioInputPorts::const_iterator i = aip.begin (); i != aip.end (); ++i) {
InputPortMap::iterator im = _input_ports.find (i->first);
if (im != _input_ports.end()) {
im->second->update (accurate_coefficient_to_dB (i->second.meter->level), accurate_coefficient_to_dB (i->second.meter->peak));
}
}
PortManager::MIDIInputPorts const mip (AudioEngine::instance ()->midi_input_ports ());
for (PortManager::MIDIInputPorts::const_iterator i = mip.begin (); i != mip.end (); ++i) {
InputPortMap::iterator im = _input_ports.find (i->first);
if (im != _input_ports.end()) {
im->second->update ((float const*)i->second.meter->chn_active);
im->second->update (*(i->second.monitor));
}
}
if (iop) {
for (auto const& p : *iop) {
PortManager::AudioInputPorts const& aip (p->audio_input_ports ());
PortManager::MIDIInputPorts const& mip (p->midi_input_ports ());
for (auto i = aip.begin (); i != aip.end (); ++i) {
InputPortMap::iterator im = _input_ports.find (i->first);
if (im != _input_ports.end()) {
im->second->update (accurate_coefficient_to_dB (i->second.meter->level), accurate_coefficient_to_dB (i->second.meter->peak));
}
}
for (PortManager::MIDIInputPorts::const_iterator i = mip.begin (); i != mip.end (); ++i) {
InputPortMap::iterator im = _input_ports.find (i->first);
if (im != _input_ports.end()) {
im->second->update ((float const*)i->second.meter->chn_active);
im->second->update (*(i->second.monitor));
}
}
}
}
for (list<TrackRecordAxis*>::const_iterator i = _recorders.begin (); i != _recorders.end (); ++i) {
(*i)->fast_update ();
}
if (_session && _session->actively_recording ()) {
/* maybe grow showing rec-regions */
gui_extents_changed ();
}
return true;
}
int
RecorderUI::calc_columns (int child_width, int parent_width)
{
int n_col = parent_width / child_width;
if (n_col <= 2) {
/* at least 2 columns*/
return 2;
} else if (n_col <= 4) {
/* allow 3 (2 audio + 1 MIDI) */
return n_col;
}
/* otherwise only even number of cols */
return n_col & ~1;
}
void
RecorderUI::meter_area_layout ()
{
container_clear (_meter_table);
int col = 0;
int row = 0;
int spc = 2;
InputPortSet ips;
for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) {
boost::shared_ptr<InputPort> const& ip = i->second;
ip->show ();
ips.insert (ip);
}
for (InputPortSet::const_iterator i = ips.begin (); i != ips.end (); ++i) {
boost::shared_ptr<InputPort> const& ip = *i;
_meter_table.attach (*ip, col, col + 1, row, row + 1, SHRINK|FILL, SHRINK, spc, spc);
if (++col >= _meter_area_cols) {
col = 0;
++row;
}
}
}
void
RecorderUI::meter_area_size_allocate (Allocation& allocation)
{
int mac = calc_columns (_meter_box_width, _meter_area.get_width ());
#if 0
printf ("RecorderUI::meter_area_size_allocate: %dx%d | mbw: %d cols:%d new-cols: %d\n",
allocation.get_width (), allocation.get_height (),
_meter_box_width, _meter_area_cols, mac);
#endif
if (_meter_area_cols == mac || _input_ports.size () == 0) {
return;
}
_meter_area_cols = mac;
meter_area_layout ();
_meter_area.queue_resize ();
}
void
RecorderUI::meter_area_size_request (GtkRequisition* requisition)
{
int width = 2;
int height = 2;
int spc = 2;
for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) {
boost::shared_ptr<InputPort> const& ip = i->second;
Requisition r = ip->size_request ();
width = std::max (width, r.width + spc * 2);
height = std::max (height, r.height + spc * 2);
}
_meter_box_width = width;
//height *= ceilf (_input_ports.size () / (float)_meter_area_cols);
Requisition r = _meter_table.size_request ();
requisition->width = _meter_box_width * 2; // at least 2 columns wide
requisition->height = std::max (r.height, height);
#if 0
printf ("RecorderUI::meter_area_size_request: %dx%d\n", requisition->width, requisition->height);
#endif
}
void
RecorderUI::port_connected_or_disconnected (string p1, string p2)
{
if (_input_ports.find (p1) != _input_ports.end ()) {
set_connections (p1);
}
if (_input_ports.find (p2) != _input_ports.end ()) {
set_connections (p2);
}
}
void
RecorderUI::port_pretty_name_changed (string pn)
{
if (_input_ports.find (pn) != _input_ports.end ()) {
_input_ports[pn]->setup_name ();
}
}
void
RecorderUI::regions_changed (boost::shared_ptr<ARDOUR::RegionList>, PBD::PropertyChange const& what_changed)
{
PBD::PropertyChange interests;
interests.add (ARDOUR::Properties::length);
if (what_changed.contains (interests)) {
gui_extents_changed ();
}
}
void
RecorderUI::gui_extents_changed ()
{
pair<timepos_t, timepos_t> ext = PublicEditor::instance().session_gui_extents ();
if (ext.first == timepos_t::max (ext.first.time_domain()) || ext.first >= ext.second) {
return;
}
samplepos_t start = ext.first.samples();
samplepos_t end = ext.second.samples();
for (list<TrackRecordAxis*>::const_iterator i = _recorders.begin (); i != _recorders.end (); ++i) {
(*i)->rec_extent (start, end);
}
/* round to the next minute */
if (_session) {
const samplecnt_t one_minute = 60 * _session->nominal_sample_rate ();
start = (start / one_minute) * one_minute;
end = ((end / one_minute) + 1) * one_minute;
}
_ruler.set_gui_extents (start, end);
for (list<TrackRecordAxis*>::const_iterator i = _recorders.begin (); i != _recorders.end (); ++i) {
(*i)->set_gui_extents (start, end);
}
}
void
RecorderUI::set_connections (string const& p)
{
if (!_session) {
return;
}
WeakRouteList wrl;
boost::shared_ptr<RouteList> rl = _session->get_tracks ();
for (RouteList::const_iterator r = rl->begin(); r != rl->end(); ++r) {
if ((*r)->input()->connected_to (p)) {
wrl.push_back (*r);
}
}
_input_ports[p]->set_connections (wrl);
// TODO: think.
// only clear when port is spilled and cnt == 0 ?
// otherwise only update spilled tracks if port is spilled?
if (!_spill_port_names.empty ()) {
for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) {
i->second->spill (false);
}
_spill_port_names.clear ();
update_rec_table_layout ();
}
}
void
RecorderUI::add_track (string const& p)
{
if (!_session) {
return;
}
new_track_for_port (_input_ports[p]->data_type (), p);
}
void
RecorderUI::spill_port (string const& p)
{
bool ok = false;
if (_input_ports[p]->spilled ()) {
ok = _input_ports[p]->spill (true);
}
bool update;
if (ok) {
pair<set<string>::iterator, bool> rv = _spill_port_names.insert (p);
update = rv.second;
} else {
update = 0 != _spill_port_names.erase (p);
}
if (update) {
update_rec_table_layout ();
}
}
void
RecorderUI::initial_track_display ()
{
boost::shared_ptr<RouteList> r = _session->get_tracks ();
RouteList rl (*r);
_recorders.clear ();
add_routes (rl);
}
void
RecorderUI::add_routes (RouteList& rl)
{
rl.sort (Stripable::Sorter ());
for (RouteList::iterator r = rl.begin (); r != rl.end (); ++r) {
/* we're only interested in Tracks */
if (!boost::dynamic_pointer_cast<Track> (*r)) {
continue;
}
TrackRecordAxis* rec = new TrackRecordAxis (/**this,*/ _session, *r);
_recorders.push_back (rec);
}
gui_extents_changed ();
update_rec_table_layout ();
}
void
RecorderUI::remove_route (TrackRecordAxis* ra)
{
if (!_session || _session->deletion_in_progress ()) {
_recorders.clear ();
return;
}
list<TrackRecordAxis*>::iterator i = find (_recorders.begin (), _recorders.end (), ra);
if (i != _recorders.end ()) {
_rec_area.remove (**i);
_recorders.erase (i);
}
update_rec_table_layout ();
}
void
RecorderUI::tra_name_edit (TrackRecordAxis* tra, bool next)
{
list<TrackRecordAxis*>::iterator i = find (_visible_recorders.begin (), _visible_recorders.end (), tra);
if (i == _visible_recorders.end ()) {
return;
}
if (next && ++i != _visible_recorders.end ()) {
(*i)->start_rename ();
} else if (!next && i != _visible_recorders.begin ()) {
(*--i)->start_rename ();
}
}
struct TrackRecordAxisSorter {
bool operator() (const TrackRecordAxis* ca, const TrackRecordAxis* cb)
{
boost::shared_ptr<Stripable> const& a = ca->stripable ();
boost::shared_ptr<Stripable> const& b = cb->stripable ();
return Stripable::Sorter(true)(a, b);
}
};
void
RecorderUI::presentation_info_changed (PBD::PropertyChange const& what_changed)
{
if (what_changed.contains (Properties::hidden)) {
update_rec_table_layout ();
} else if (what_changed.contains (Properties::order)) {
/* test if effective order changed. When deleting tracks
* the PI:order_key changes, but the layout does not change.
*/
list<TrackRecordAxis*> rec (_recorders);
_recorders.sort (TrackRecordAxisSorter ());
if (_recorders != rec) {
update_rec_table_layout ();
}
}
}
void
RecorderUI::update_rec_table_layout ()
{
_visible_recorders.clear ();
_recorders.sort (TrackRecordAxisSorter ());
_ruler_width_update_connection.disconnect ();
list<TrackRecordAxis*>::const_iterator i;
for (i = _recorders.begin (); i != _recorders.end (); ++i) {
if ((*i)->route ()->presentation_info ().hidden ()) {
if ((*i)->get_parent ()) {
_rec_area.remove (**i);
}
continue;
}
/* spill */
if (!_spill_port_names.empty ()) {
bool connected = false;
for (set<string>::const_iterator j = _spill_port_names.begin(); j != _spill_port_names.end(); ++j) {
if ((*i)->route ()->input()->connected_to (*j)) {
connected = true;
break;
}
}
if (!connected) {
if ((*i)->get_parent ()) {
_rec_area.remove (**i);
}
continue;
}
}
if (!(*i)->get_parent ()) {
_rec_area.pack_start (**i, false, false);
} else {
_rec_area.reorder_child (**i, -1);
}
(*i)->show ();
_visible_recorders.push_back (*i);
if (!_ruler_width_update_connection.connected ()) {
_ruler_width_update_connection = (*i)->signal_size_allocate().connect (sigc::bind (sigc::mem_fun (*this, &RecorderUI::update_spacer_width), *i));
}
}
if (!_ruler_width_update_connection.connected ()) {
_ruler.hide ();
} else {
_ruler.show ();
}
_rec_group_tabs->set_dirty ();
}
list<TrackRecordAxis*>
RecorderUI::visible_recorders () const
{
return _visible_recorders;
}
void
RecorderUI::update_spacer_width (Allocation&, TrackRecordAxis* rec)
{
int w = rec->summary_xpos ();
if (_rec_group_tabs->get_visible ()) {
w += _rec_group_tabs->get_width ();
}
_space.set_size_request (w, -1); //< Note: this is idempotent
_ruler.set_right_edge (rec->summary_width ());
}
void
RecorderUI::new_track_for_port (DataType dt, string const& port_name)
{
ArdourDialog d (_("Create track for input"), true, false);
Entry track_name_entry;
InstrumentSelector instrument_combo(InstrumentSelector::ForTrackDefault);
ComboBoxText strict_io_combo;
string pn = AudioEngine::instance()->get_pretty_name_by_name (port_name);
if (!pn.empty ()) {
track_name_entry.set_text (pn);
} else {
track_name_entry.set_text (port_name);
}
strict_io_combo.append (_("Flexible-I/O"));
strict_io_combo.append (_("Strict-I/O"));
strict_io_combo.set_active (Config->get_strict_io () ? 1 : 0);
Label* l;
Table t;
int row = 0;
t.set_spacings (6);
l = manage (new Label (string_compose (_("Create new track connected to port '%1'"), pn.empty() ? port_name : pn)));
t.attach (*l, 0, 2, row, row + 1, EXPAND | FILL, SHRINK);
++row;
l = manage (new Label (_("Track name:")));
t.attach (*l, 0, 1, row, row + 1, SHRINK, SHRINK);
t.attach (track_name_entry, 1, 2, row, row + 1, EXPAND | FILL, SHRINK);
++row;
if (dt == DataType::MIDI) {
l = manage (new Label (_("Instrument:")));
t.attach (*l, 0, 1, row, row + 1, SHRINK, SHRINK);
t.attach (instrument_combo, 1, 2, row, row + 1, EXPAND | FILL, SHRINK);
++row;
}
if (Profile->get_mixbus ()) {
strict_io_combo.set_active (1);
} else {
l = manage (new Label (_("Strict I/O:")));
t.attach (*l, 0, 1, row, row + 1, SHRINK, SHRINK);
t.attach (strict_io_combo, 1, 3, row, row + 1, FILL, SHRINK);
set_tooltip (strict_io_combo, _("With strict-i/o enabled, Effect Processors will not modify the number of channels on a track. The number of output channels will always match the number of input channels."));
}
d.get_vbox()->pack_start (t, false, false);
d.get_vbox()->set_border_width (12);
d.add_button(Stock::CANCEL, RESPONSE_CANCEL);
d.add_button(Stock::OK, RESPONSE_OK);
d.set_default_response (RESPONSE_OK);
d.set_position (WIN_POS_MOUSE);
d.show_all ();
track_name_entry.signal_activate().connect (sigc::bind (sigc::mem_fun (d, &Dialog::response), RESPONSE_OK));
if (d.run() != RESPONSE_OK) {
return;
}
d.hide ();
bool strict_io = strict_io_combo.get_active_row_number () == 1;
string track_name = track_name_entry.get_text();
uint32_t outputs = 2;
if (_session->master_out ()) {
outputs = max (outputs, _session->master_out ()->n_inputs ().n_audio ());
}
if (dt == DataType::AUDIO) {
boost::shared_ptr<Route> r;
try {
list<boost::shared_ptr<AudioTrack> > tl = _session->new_audio_track (1, outputs, NULL, 1, track_name, PresentationInfo::max_order, Normal, false);
r = tl.front ();
} catch (...) {
return;
}
if (r) {
r->set_strict_io (strict_io);
r->input ()->audio (0)->connect (port_name);
}
} else if (dt == DataType::MIDI) {
boost::shared_ptr<Route> r;
try {
list<boost::shared_ptr<MidiTrack> > tl = _session->new_midi_track (
ChanCount (DataType::MIDI, 1), ChanCount (DataType::MIDI, 1),
strict_io,
instrument_combo.selected_instrument (), (Plugin::PresetRecord*) 0,
(RouteGroup*) 0,
1, track_name, PresentationInfo::max_order, Normal, false);
r = tl.front ();
} catch (...) {
return;
}
if (r) {
r->input ()->midi (0)->connect (port_name);
}
}
}
void
RecorderUI::arm_all ()
{
if (_session) {
_session->set_all_tracks_record_enabled (true);
}
}
void
RecorderUI::arm_none ()
{
if (_session) {
_session->set_all_tracks_record_enabled (false);
}
}
void
RecorderUI::rec_undo ()
{
if (_session) {
_session->undo (1);
}
}
void
RecorderUI::rec_redo ()
{
if (_session) {
_session->redo (1);
}
}
void
RecorderUI::peak_reset ()
{
AudioEngine::instance ()->reset_input_meters ();
for (auto& p : _ioplugins) {
p->reset_input_meters ();
}
}
/* ****************************************************************************/
bool RecorderUI::InputPort::_size_groups_initialized = false;
Glib::RefPtr<Gtk::SizeGroup> RecorderUI::InputPort::_name_size_group;
Glib::RefPtr<Gtk::SizeGroup> RecorderUI::InputPort::_ctrl_size_group;
Glib::RefPtr<Gtk::SizeGroup> RecorderUI::InputPort::_monitor_size_group;
RecorderUI::InputPort::InputPort (string const& name, DataType dt, RecorderUI* parent, bool vertical, bool ioplug)
: _dt (dt)
, _monitor (dt, AudioEngine::instance()->sample_rate (), vertical ? InputPortMonitor::Vertical : InputPortMonitor::Horizontal)
, _alignment (0.5, 0.5, 0, 0)
, _frame (vertical ? ArdourWidgets::Frame::Vertical : ArdourWidgets::Frame::Horizontal)
, _spill_button ("", ArdourButton::default_elements, true)
, _monitor_button (_("PFL"), ArdourButton::default_elements)
, _name_button (name)
, _name_label ("", ALIGN_CENTER, ALIGN_CENTER, false)
, _add_button ("+")
, _port_name (name)
, _ioplug (ioplug)
, _solo_release (0)
{
if (!_size_groups_initialized) {
_size_groups_initialized = true;
_name_size_group = Gtk::SizeGroup::create (Gtk::SIZE_GROUP_HORIZONTAL);
_ctrl_size_group = Gtk::SizeGroup::create (Gtk::SIZE_GROUP_HORIZONTAL);
_monitor_size_group = Gtk::SizeGroup::create (Gtk::SIZE_GROUP_BOTH);
}
Box* box_t;
Box* box_n;
Table* ctrls = manage (new Table);
if (vertical) {
box_t = manage (new VBox);
box_n = manage (new VBox);
} else {
box_t = manage (new HBox);
box_n = manage (new VBox);
}
_spill_button.set_name ("generic button");
_spill_button.set_sizing_text(_("(none)"));
_spill_button.signal_clicked.connect (sigc::bind (sigc::mem_fun (*parent, &RecorderUI::spill_port), name));
_monitor_button.set_name ("solo button");
//_monitor_button.signal_clicked.connect (sigc::bind (sigc::mem_fun (*parent, &RecorderUI::monitor_port), name));
_monitor_button.signal_button_press_event().connect (sigc::mem_fun(*this, &InputPort::monitor_press), false);
_monitor_button.signal_button_release_event().connect (sigc::mem_fun(*this, &InputPort::monitor_release), false);
set_tooltip (_monitor_button, _("Solo/Listen to this input"));
_add_button.set_name ("generic button");
_add_button.set_icon (ArdourIcon::PlusSign);
_add_button.signal_clicked.connect (sigc::bind (sigc::mem_fun (*parent, &RecorderUI::add_track), name));
set_tooltip (_add_button, _("Add a track for this input port"));
_name_button.set_corner_radius (2);
_name_button.set_name ("generic button");
_name_button.set_text_ellipsize (Pango::ELLIPSIZE_MIDDLE);
if (!_ioplug) {
_name_button.signal_clicked.connect (sigc::mem_fun (*this, &RecorderUI::InputPort::rename_port));
}
_name_label.set_ellipsize (Pango::ELLIPSIZE_MIDDLE);
setup_name ();
ctrls->attach (_spill_button, 0, 2, 0, 1, EXPAND|FILL, EXPAND|FILL, 1, 1);
if (dt == DataType::AUDIO) {
ctrls->attach (_add_button, 0, 1, 1, 2, SHRINK|FILL, EXPAND|FILL, 1, 1);
ctrls->attach (_monitor_button, 1, 2, 1, 2, SHRINK|FILL, EXPAND|FILL, 1, 1);
} else {
ctrls->attach (_add_button, 0, 2, 1, 2, EXPAND|FILL, EXPAND|FILL, 1, 1);
}
box_n->pack_start (_name_button, true, true);
#if 0 // MIXBUS ?
box_n->pack_start (_name_label, true, true);
#endif
int nh;
if (vertical) {
nh = 64 * UIConfiguration::instance ().get_ui_scale ();
box_t->pack_start (_monitor, false, false);
box_t->pack_start (*ctrls, false, false, 1);
box_t->pack_start (*box_n, false, false, 1);
_name_label.set_max_width_chars (9);
} else {
nh = 120 * UIConfiguration::instance ().get_ui_scale ();
box_t->pack_start (*ctrls, false, false, 1);
box_t->pack_start (*box_n, false, false, 1);
box_t->pack_start (_monitor, false, false);
_name_label.set_max_width_chars (18);
}
_name_button.set_layout_ellipsize_width (nh * PANGO_SCALE);
if (!vertical) {
/* match width of all name labels */
_name_size_group->add_widget (*box_n);
/* match width of control boxes */
_ctrl_size_group->add_widget (*ctrls);
}
/* equal size for all meters + event monitors */
_monitor_size_group->add_widget (_monitor);
Gdk::Color bg;
if (_ioplug) {
Gtkmm2ext::set_color_from_rgba (bg, UIConfiguration::instance ().color ("neutral:background"));
} else {
Gtkmm2ext::set_color_from_rgba (bg, UIConfiguration::instance ().color ("neutral:background2"));
}
_frame.modify_bg (Gtk::STATE_NORMAL, bg);
/* top level packing with border */
_alignment.add (*box_t);
_alignment.set_padding (2, 2, 4, 4);
_frame.add (_alignment);
_frame.set_border_width (3);
_frame.set_padding (3);
add (_frame);
show_all ();
update_rec_stat ();
}
RecorderUI::InputPort::~InputPort ()
{
delete _solo_release;
}
void
RecorderUI::InputPort::clear ()
{
delete _solo_release;
_solo_release = 0;
_monitor.clear ();
}
void
RecorderUI::InputPort::update (float l, float p)
{
_monitor.update (l, p);
}
void
RecorderUI::InputPort::update (CircularSampleBuffer& csb)
{
_monitor.update (csb);
}
void
RecorderUI::InputPort::update (float const* v)
{
_monitor.update (v);
}
void
RecorderUI::InputPort::update (CircularEventBuffer& ceb)
{
_monitor.update (ceb);
}
void
RecorderUI::InputPort::set_frame_label (std::string const& lbl)
{
_frame.set_label (lbl);
}
void
RecorderUI::InputPort::update_rec_stat ()
{
bool armed = false;
for (WeakRouteList::const_iterator r = _connected_routes.begin(); r != _connected_routes.end(); ++r) {
boost::shared_ptr<Route> rt = r->lock ();
if (!rt || !rt->rec_enable_control ()) {
continue;
}
if (rt->rec_enable_control ()->get_value ()) {
armed = true;
break;
}
}
if (armed) {
_frame.set_edge_color (0xff0000ff); // red
} else {
_frame.set_edge_color (0x000000ff); // black
}
}
void
RecorderUI::InputPort::set_connections (WeakRouteList wrl)
{
_connected_routes = wrl;
size_t cnt = wrl.size ();
if (cnt > 0) {
_spill_button.set_text (string_compose("(%1)", cnt));
_spill_button.set_sensitive (true);
set_tooltip (_spill_button, string_compose(_("This port feeds %1 tracks. Click to show them"), cnt));
} else {
_spill_button.set_text (_("(none)"));
_spill_button.set_sensitive (false);
set_tooltip (_spill_button, _("This port is not feeding any tracks"));
}
update_rec_stat ();
}
void
RecorderUI::InputPort::setup_name ()
{
string pn = AudioEngine::instance()->get_pretty_name_by_name (_port_name);
if (!pn.empty ()) {
_name_button.set_text (pn);
_name_label.set_text (_port_name);
} else {
_name_button.set_text (_port_name);
_name_label.set_text ("");
}
if (_ioplug) {
set_tooltip (_name_button, string_compose (_("I/O Plugin input port '%1'"), _port_name));
} else {
set_tooltip (_name_button, string_compose (_("Set or edit the custom name for input port '%1'"), _port_name));
}
}
void
RecorderUI::InputPort::rename_port ()
{
Prompter prompter (true, true);
prompter.set_name ("Prompter");
prompter.add_button (Stock::REMOVE, RESPONSE_NO);
prompter.add_button (Stock::OK, RESPONSE_ACCEPT);
prompter.set_title (_("Customize port name"));
prompter.set_prompt (_("Port name"));
prompter.set_initial_text (AudioEngine::instance()->get_pretty_name_by_name (_port_name));
string name;
switch (prompter.run ()) {
case RESPONSE_ACCEPT:
prompter.get_result (name);
break;
case RESPONSE_NO:
/* use blank name, reset */
break;
default:
return;
}
AudioEngine::instance()->set_port_pretty_name (_port_name, name);
}
bool
RecorderUI::InputPort::spill (bool en)
{
bool active = _spill_button.get_active ();
bool act = active;
if (!en) {
act = false;
}
if (_connected_routes.size () == 0) {
act = false;
}
if (active != act) {
_spill_button.set_active (act);
}
return act;
}
bool
RecorderUI::InputPort::spilled () const
{
return _spill_button.get_active ();
}
void
RecorderUI::InputPort::allow_monitoring (bool en)
{
if (_dt != DataType::AUDIO || _ioplug) {
en = false;
}
if (!en && _monitor_button.get_active ()) {
_monitor_button.set_active (false);
}
_monitor_button.set_sensitive (en);
}
void
RecorderUI::InputPort::update_monitorstate (bool en)
{
if (_dt == DataType::AUDIO) {
_monitor_button.set_active (en);
}
}
bool
RecorderUI::InputPort::monitor_press (GdkEventButton* ev)
{
if (ev->type == GDK_2BUTTON_PRESS || ev->type == GDK_3BUTTON_PRESS ) {
return true;
}
if (Keyboard::is_context_menu_event (ev)) {
return false;
}
if (ev->button != 1 && !Keyboard::is_momentary_push_event (ev)) {
return false;
}
MonitorPort& mp (AudioEngine::instance()->monitor_port ());
Session* s = AudioEngine::instance()->session ();
assert (s);
if (Keyboard::is_momentary_push_event (ev)) {
/* momentary */
_solo_release = new SoloMuteRelease (mp.monitoring (_port_name));
}
if (Keyboard::modifier_state_equals (ev->state, Keyboard::ModifierMask (Keyboard::PrimaryModifier|Keyboard::TertiaryModifier))) {
/* Primary-Tertiary-click applies change to all */
if (_solo_release) {
s->prepare_momentary_solo (_solo_release);
}
if (!_monitor_button.get_active ()) {
std::vector<std::string> ports;
AudioEngine::instance()->get_physical_inputs (DataType::AUDIO, ports);
std::list<std::string> portlist;
std::copy (ports.begin (), ports.end (), std::back_inserter (portlist));
mp.set_active_monitors (portlist);
} else {
mp.clear_ports (false);
}
} else if (Keyboard::modifier_state_contains (ev->state, Keyboard::ModifierMask (Keyboard::PrimaryModifier|Keyboard::SecondaryModifier)) || (!_monitor_button.get_active () && Config->get_exclusive_solo ())) {
/* Primary-Secondary-click: exclusive solo */
if (_solo_release) {
s->prepare_momentary_solo (_solo_release, true);
} else {
/* clear solo state */
s->prepare_momentary_solo (0, true);
}
/* exclusively solo */
if (!_monitor_button.get_active ()) {
mp.add_port (_port_name);
} else {
delete _solo_release;
_solo_release = 0;
}
} else {
if (_solo_release) {
s->prepare_momentary_solo (_solo_release);
}
/* Toggle Port Listen */
if (!_monitor_button.get_active ()) {
mp.add_port (_port_name);
} else {
mp.remove_port (_port_name);
}
}
return false;
}
bool
RecorderUI::InputPort::monitor_release (GdkEventButton* ev)
{
if (_solo_release) {
_solo_release->release (AudioEngine::instance()->session (), false);
delete _solo_release;
_solo_release = 0;
}
return false;
}
string const&
RecorderUI::InputPort::name () const
{
return _port_name;
}
DataType
RecorderUI::InputPort::data_type () const
{
return _dt;
}
/* ****************************************************************************/
RecorderUI::RecRuler::RecRuler ()
: _width (200)
, _left (0)
, _right (0)
{
_layout = Pango::Layout::create (get_pango_context ());
_layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
_layout->set_text ("88:88:88");
_layout->get_pixel_size (_time_width, _time_height);
}
void
RecorderUI::RecRuler::set_right_edge (int w)
{
if (_width == w) {
return;
}
_width = w;
set_dirty ();
}
void
RecorderUI::RecRuler::set_gui_extents (samplepos_t start, samplepos_t end)
{
if (_left == start && _right == end) {
return;
}
_left = start;
_right = end;
set_dirty ();
}
void
RecorderUI::RecRuler::render (Cairo::RefPtr<Cairo::Context> const& cr, cairo_rectangle_t* r)
{
cr->rectangle (r->x, r->y, r->width, r->height);
cr->clip ();
if (!_session || _left >= _right) {
return;
}
const int width = std::min (_width, get_width ());
const int height = get_height ();
const int n_labels = floor (width / (_time_width * 1.75));
const samplecnt_t time_span = _right - _left;
const samplecnt_t time_granularity = ceil ((double)time_span / n_labels / _session->sample_rate ()) * _session->sample_rate ();
const double px_per_sample = width / (double) time_span;
const samplepos_t lower = (_left / time_granularity) * time_granularity;
Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance().color ("ruler text"));
cr->set_line_width (1);
for (int i = 0; i < 2 + n_labels; ++i) {
samplepos_t when = lower + i * time_granularity;
int xpos = (when - _left) * px_per_sample;
if (xpos < 0) {
continue;
}
char buf[32];
int lw, lh;
AudioClock::print_minsec (when, buf, sizeof (buf), _session->sample_rate (), 0);
_layout->set_text (string(buf).substr(1));
_layout->get_pixel_size (lw, lh);
if (xpos + lw > width) {
break;
}
int x0 = xpos + 2;
int y0 = height - _time_height - 3;
cr->move_to (xpos + .5 , 0);
cr->line_to (xpos + .5 , height);
cr->stroke ();
cr->move_to (x0, y0);
_layout->show_in_cairo_context (cr);
}
}
void
RecorderUI::RecRuler::on_size_request (Requisition* req)
{
req->width = 200;
req->height = _time_height + 4;
}
bool
RecorderUI::RecRuler::on_button_press_event (GdkEventButton* ev)
{
if (!_session || _session->actively_recording()) {
return false;
}
// TODO start "drag" editor->_dragging_playhead = true
// CursorDrag::start_grab
// RecRuler internal drag (leave editor + TC transmission alone?!)
_session->request_locate (_left + (double) (_right - _left) * ev->x / get_width ());
return true;
}