Blob Blame History Raw
/*  cdrdao - write audio CD-Rs in disc-at-once mode
 *
 *  Copyright (C) 1998-2000  Andreas Mueller <mueller@daneb.ping.de>
 *
 *  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <gtkmm.h>
#include <gnome.h>
#include <glibmm/convert.h>
#include <iostream>

#include "config.h"
#include "xcdrdao.h"
#include "guiUpdate.h"
#include "MessageBox.h"
#include "SampleDisplay.h"
#include "TocEdit.h"
#include "TocEditView.h"
#include "util.h"

#include "AudioCDProject.h"
#include "AudioCDView.h"
#include "Project.h"
#include "TrackInfoDialog.h"
#include "AddFileDialog.h"
#include "AddSilenceDialog.h"

AudioCDView::AudioCDView(AudioCDProject *project) 
    : addFileDialog_(project)
{
  project_ = project;
  tocEditView_ = new TocEditView(project->tocEdit());

// These are not created until first needed, for faster startup
// and less memory usage.
  trackInfoDialog_ = 0;
  addSilenceDialog_ = 0;

  std::list<Gtk::TargetEntry> drop_types;

  drop_types.push_back(Gtk::TargetEntry("text/uri-list", Gtk::TargetFlags(0),
                                        TARGET_URI_LIST));

  drag_dest_set(drop_types);
  signal_drag_data_received().
      connect(sigc::mem_fun(*this, &AudioCDView::drag_data_received_cb));

  sampleDisplay_ = new SampleDisplay;
  sampleDisplay_->setTocEdit(project->tocEdit());

  sampleDisplay_->set_size_request(200,200);
  
  pack_start(*sampleDisplay_, TRUE, TRUE);
  sampleDisplay_->modify_font(Pango::FontDescription("Monospace 8"));
  sampleDisplay_->show();

  Gtk::HScrollbar *scrollBar =
    new Gtk::HScrollbar(*(sampleDisplay_->getAdjustment()));
  pack_start(*scrollBar, FALSE, FALSE);
  scrollBar->show();
  
  Gtk::Label *label;
  Gtk::HBox *selectionInfoBox = new Gtk::HBox;

//FIXME: Calculate entry width for the current font.
  gint entry_width = 90;

  markerPos_ = new Gtk::Entry;
  markerPos_->set_editable(true);
  markerPos_->set_size_request(entry_width, -1);
  markerPos_->signal_activate().
      connect(sigc::mem_fun(*this, &AudioCDView::markerSet));

  cursorPos_ = new Gtk::Label;
  cursorPos_->set_size_request(entry_width, -1);

  selectionStartPos_ = new Gtk::Entry;
  selectionStartPos_->set_editable(true);
  selectionStartPos_->set_size_request(entry_width, -1);
  selectionStartPos_->signal_activate().
      connect(sigc::mem_fun(*this, &AudioCDView::selectionSet));

  selectionEndPos_ = new Gtk::Entry;
  selectionEndPos_->set_editable(true);
  selectionEndPos_->set_size_request(entry_width, -1);
  selectionEndPos_->signal_activate().
      connect(sigc::mem_fun(*this, &AudioCDView::selectionSet));

  label = new Gtk::Label(_("Cursor: "));
  selectionInfoBox->pack_start(*label, FALSE, FALSE);
  selectionInfoBox->pack_start(*cursorPos_);
  label->show();
  cursorPos_->show();

  label = new Gtk::Label(_("Marker: "));
  selectionInfoBox->pack_start(*label, FALSE, FALSE);
  selectionInfoBox->pack_start(*markerPos_);
  label->show();
  markerPos_->show();

  label = new Gtk::Label(_("Selection: "));
  selectionInfoBox->pack_start(*label, FALSE, FALSE);
  selectionInfoBox->pack_start(*selectionStartPos_);
  label->show();
  selectionStartPos_->show();

  label = new Gtk::Label(" - ");
  selectionInfoBox->pack_start(*label, FALSE, FALSE);
  selectionInfoBox->pack_start(*selectionEndPos_);
  label->show();
  selectionEndPos_->show();
  
  selectionInfoBox->set_border_width(2);
  pack_start(*selectionInfoBox, FALSE, FALSE);
  selectionInfoBox->show();

  setMode(SELECT);

  sampleDisplay_->markerSet.connect(sigc::mem_fun(*this,
                        &AudioCDView::markerSetCallback));
  sampleDisplay_->selectionSet.connect(sigc::mem_fun(*this,
                        &AudioCDView::selectionSetCallback));
  sampleDisplay_->selectionCleared.connect(sigc::mem_fun(*this,
                        &AudioCDView::selectionClearedCallback));
  sampleDisplay_->cursorMoved.connect(sigc::mem_fun(*this,
  			&AudioCDView::cursorMovedCallback));
  sampleDisplay_->trackMarkSelected.connect(sigc::mem_fun(*this,
  			&AudioCDView::trackMarkSelectedCallback));
  sampleDisplay_->trackMarkMoved.connect(sigc::mem_fun(*this,
  			&AudioCDView::trackMarkMovedCallback));
  sampleDisplay_->viewModified.connect(sigc::mem_fun(*this,
		        &AudioCDView::viewModifiedCallback));

  tocEditView_->sampleViewFull();
}

AudioCDView::~AudioCDView()
{
  if (trackInfoDialog_)
    delete trackInfoDialog_;

  if (addSilenceDialog_)
    delete addSilenceDialog_;
}

void AudioCDView::add_menus(Glib::RefPtr<Gtk::UIManager> m_refUIManager)
{
  m_refActionGroup = Gtk::ActionGroup::create("AudioCDView");

  m_refActionGroup->add( Gtk::Action::create("TrackInfo", Gtk::Stock::PROPERTIES,
                         _("Track Info..."),
                         _("Edit track data")),
                         sigc::mem_fun(*this, &AudioCDView::trackInfo) );

  m_refActionGroup->add( Gtk::Action::create("Cut", Gtk::Stock::CUT,
                         _("Cut"),
                         _("Cut out selected samples")),
                         Gtk::AccelKey("<control>x"),
                         sigc::mem_fun(*this, &AudioCDView::cutTrackData) );

  m_refActionGroup->add( Gtk::Action::create("Paste", Gtk::Stock::PASTE,
                         _("Paste"),
                         _("Paste previously cut samples")),
                         Gtk::AccelKey("<control>v"),
                         sigc::mem_fun(*this, &AudioCDView::pasteTrackData) );

  m_refActionGroup->add( Gtk::Action::create("SelectAll",
                         _("Select All"),
                         _("Select entire sample")),
                         Gtk::AccelKey("<control>a"),
                         sigc::mem_fun(*this, &AudioCDView::selectAll) );

  m_refActionGroup->add( Gtk::Action::create("AddTrackMark",
                         _("Add Track Mark"),
                         _("Add track marker at current marker position")),
                         Gtk::AccelKey("<control>m"),
                         sigc::mem_fun(*this, &AudioCDView::addTrackMark) );

  m_refActionGroup->add( Gtk::Action::create("AddIndexMark",
                         _("Add Index Mark"),
                         _("Add index marker at current marker position")),
                         sigc::mem_fun(*this, &AudioCDView::addIndexMark) );

  m_refActionGroup->add( Gtk::Action::create("AddPreGap",
                         _("Add Pre-Gap"),
                         _("Add pre-gap at current marker position")),
                         sigc::mem_fun(*this, &AudioCDView::addPregap) );

  m_refActionGroup->add( Gtk::Action::create("RemoveTrackMark",
                         _("Remove Track Mark"),
                         _("Remove selected track/index marker or pre-gap")),
                         Gtk::AccelKey("<control>D"),
                         sigc::mem_fun(*this, &AudioCDView::removeTrackMark) );

  m_refActionGroup->add( Gtk::Action::create("AppendTrack",
                         _("Append Track"),
                         _("Append track with data from audio file")),
                         Gtk::AccelKey("<control>T"),
                         sigc::mem_fun(*this, &AudioCDView::appendTrack) );

  m_refActionGroup->add( Gtk::Action::create("AppendFile",
                         _("Append File"),
                         _("Append data from audio file to last track")),
                         Gtk::AccelKey("<control>F"),
                         sigc::mem_fun(*this, &AudioCDView::appendFile) );

  m_refActionGroup->add( Gtk::Action::create("InsertFile",
                         _("Insert File"),
                         _("Insert data from audio file at current marker position")),
                         Gtk::AccelKey("<control>I"),
                         sigc::mem_fun(*this, &AudioCDView::insertFile) );

  m_refActionGroup->add( Gtk::Action::create("AppendSilence",
                         _("Append Silence"),
                         _("Append silence to last track")),
                         sigc::mem_fun(*this, &AudioCDView::appendSilence) );

  m_refActionGroup->add( Gtk::Action::create("InsertSilence",
                         _("Insert Silence"),
                         _("Insert silence at current marker position")),
                         sigc::mem_fun(*this, &AudioCDView::insertSilence) );

  m_refUIManager->insert_action_group(m_refActionGroup);

  // Merge menuitems
  try
  {
    Glib::ustring ui_info =
        "<ui>"
        "  <menubar name='MenuBar'>"
        "    <menu action='EditMenu'>"
        "      <menuitem action='TrackInfo'/>"
        "    <separator/>"
        "      <menuitem action='Cut'/>"
        "      <menuitem action='Paste'/>"
        "    <separator/>"
        "      <menuitem action='SelectAll'/>"
        "    <separator/>"
        "      <menuitem action='AddTrackMark'/>"
        "      <menuitem action='AddIndexMark'/>"
        "      <menuitem action='AddPreGap'/>"
        "      <menuitem action='RemoveTrackMark'/>"
        "    <separator/>"
        "      <menuitem action='AppendTrack'/>"
        "      <menuitem action='AppendFile'/>"
        "      <menuitem action='InsertFile'/>"
        "    <separator/>"
        "      <menuitem action='AppendSilence'/>"
        "      <menuitem action='InsertSilence'/>"
        "    </menu>"
        "  </menubar>"
        "</ui>";

    m_refUIManager->add_ui_from_string(ui_info);
  }
  catch(const Glib::Error& ex)
  {
    std::cerr << "merging menus failed: " <<  ex.what();
  }
}

void AudioCDView::update(unsigned long level)
{
  if (level & (UPD_TOC_DIRTY | UPD_TOC_DATA)) {
    cursorPos_->set_text("");
  }

  if (level & UPD_TRACK_MARK_SEL) {
    int trackNr, indexNr;

    if (tocEditView_->trackSelection(&trackNr) && 
	tocEditView_->indexSelection(&indexNr)) {
      sampleDisplay_->setSelectedTrackMarker(trackNr, indexNr);
    }
    else {
      sampleDisplay_->setSelectedTrackMarker(0, 0);
    }
  }

  if (level & UPD_SAMPLES) {
    unsigned long smin, smax;

    tocEditView_->sampleView(&smin, &smax);
    sampleDisplay_->updateToc(smin, smax);
  }
  else if (level & (UPD_TRACK_DATA | UPD_TRACK_MARK_SEL)) {
    sampleDisplay_->updateTrackMarks();
  }

  if (level & UPD_SAMPLE_MARKER) {
    unsigned long marker;

    if (tocEditView_->sampleMarker(&marker)) {
      markerPos_->set_text(sample2string(marker));
      sampleDisplay_->setMarker(marker);
    }
    else {
      markerPos_->set_text("");
      sampleDisplay_->clearMarker();
    }
  }

  if (level & UPD_SAMPLE_SEL) {
    unsigned long start, end;

    if (tocEditView_->sampleSelection(&start, &end)) {
      selectionStartPos_->set_text(sample2string(start));
      selectionEndPos_->set_text(sample2string(end));
      sampleDisplay_->setRegion(start, end);
    } else {
      selectionStartPos_->set_text("");
      selectionEndPos_->set_text("");
      sampleDisplay_->clearRegion();
    }
  }

  if (level & UPD_PLAY_STATUS) {
    switch (project_->playStatus()) {
      case AudioCDProject::PLAYING:
        sampleDisplay_->setCursor(1, project_->playPosition() -
                                  project_->getDelay());
        // FIXME: What about using a separate cursor for playing?
        cursorPos_->set_text(sample2string(project_->playPosition() -
                                           project_->getDelay()));
        break;
      case AudioCDProject::PAUSED:
        sampleDisplay_->setCursor(1, project_->playPosition() -
                                  project_->getDelay());
        // FIXME: What about using a separate cursor for playing?
        cursorPos_->set_text(sample2string(project_->playPosition() -
                                           project_->getDelay()));
        break;
      case AudioCDProject::STOPPED:
        sampleDisplay_->setCursor(0, 0);
        break;
      default:
        std::cerr << "invalid play status" << std::endl;
    }
  }

  if (trackInfoDialog_ != 0)
    trackInfoDialog_->update(level, tocEditView_);

  addFileDialog_.update(level);

  if (addSilenceDialog_ != 0)
    addSilenceDialog_->update(level, tocEditView_);

}

void AudioCDView::zoomIn()
{
  unsigned long start, end;

  if (tocEditView_->sampleSelection(&start, &end)) {
    if (tocEditView_->sampleView(start, end)) {
      update(UPD_SAMPLES);
    }
  }
}

void AudioCDView::zoomx2()
{
  unsigned long start, end, len, center;

  tocEditView_->sampleView(&start, &end);

  len = end - start + 1;
  center = start + len / 2;

  start = center - len / 4;
  end = center + len / 4;

  if (tocEditView_->sampleView(start, end)) {
    update(UPD_SAMPLES);
  }
}

void AudioCDView::zoomOut()
{
  unsigned long start, end, len, center;

  tocEditView_->sampleView(&start, &end);

  len = end - start + 1;
  center = start + len / 2;

  if (center > len)
    start = center - len;
  else 
    start = 0;

  end = center + len;
  if (end >= tocEditView_->tocEdit()->toc()->length().samples())
    end = tocEditView_->tocEdit()->toc()->length().samples() - 1;

  if (tocEditView_->sampleView(start, end)) {
    update(UPD_SAMPLES);
  }
}

void AudioCDView::fullView()
{
  tocEditView_->sampleViewFull();
  update(UPD_SAMPLES);
}

int AudioCDView::getMarker(unsigned long *sample)
{
  if (tocEditView_->tocEdit()->lengthSample() == 0)
    return 0;

  if (sampleDisplay_->getMarker(sample) == 0) {
    project_->statusMessage(_("Please set marker."));
    return 0;
  }

  return 1;
}

void AudioCDView::trackMarkSelectedCallback(const Track *, int trackNr,
					   int indexNr)
{
  tocEditView_->trackSelection(trackNr);
  tocEditView_->indexSelection(indexNr);
  update(UPD_TRACK_MARK_SEL);
}

// Called when the user clicks on the SampleDisplay
void AudioCDView::markerSetCallback(unsigned long sample)
{
  tocEditView_->sampleMarker(sample);
  update(UPD_SAMPLE_MARKER);
}

// Called when the user makes a selection on the SampleDisplay
void AudioCDView::selectionSetCallback(unsigned long start,
                                       unsigned long end)
{
  if (mode_ == ZOOM ) {
    if (tocEditView_->sampleView(start, end)) {
      update(UPD_SAMPLES);
    }
  }
  else {
    tocEditView_->sampleSelect(start, end);
    update(UPD_SAMPLE_SEL);
  }
}

void AudioCDView::selectAll()
{
  tocEditView_->sampleSelectAll();
  update(UPD_SAMPLE_SEL);
}

void AudioCDView::selectionClearedCallback()
{
  if (mode_ != ZOOM) {
    if (tocEditView_->sampleSelectionClear()) {
      update(UPD_SAMPLE_SEL);
    }
  }
}

void AudioCDView::cursorMovedCallback(unsigned long pos)
{
  cursorPos_->set_text(sample2string(pos));
}

void AudioCDView::viewModifiedCallback(unsigned long start, unsigned long end)
{
  if (tocEditView_->sampleView(start, end)) {
    update(UPD_SAMPLES);
  }
}

void AudioCDView::setMode(Mode m)
{
  mode_ = m;
}

// Called when the user enters a value in the marker entry
void AudioCDView::markerSet()
{
  unsigned long s = string2sample(markerPos_->get_text().c_str());

  tocEditView_->sampleMarker(s);
  update(UPD_SAMPLE_MARKER);
}

// Called when the user enters a value in one of the two selection entries
void AudioCDView::selectionSet()
{
  unsigned long s1 =
    string2sample(selectionStartPos_->get_text().c_str());
  unsigned long s2 =
    string2sample(selectionEndPos_->get_text().c_str());

  tocEditView_->sampleSelect(s1, s2);
  update(UPD_SAMPLE_SEL);
}

void
AudioCDView::drag_data_received_cb(const Glib::RefPtr<Gdk::DragContext>&
                                   context, int x, int y,
                                   const Gtk::SelectionData& selection_data,
                                   guint info, guint time)
{
  switch (info) {

  case TARGET_URI_LIST:

    if (project_->playStatus() != AudioCDProject::STOPPED)
      return;

    std::string list = selection_data.get_data_as_string();
    int idx = 0, n;

    while ((n = list.find("\r\n", idx)) >= 0) {
      std::string sub = list.substr(idx, n - idx);
      idx = n + 2;

      std::string fn;
      try {
        fn = Glib::filename_from_uri(sub);
      } catch (std::exception& e) {
        fn.clear();
      }

      if (fn.empty())
        continue;

      // Process m3u file.
      FileExtension type = fileExtension(fn.c_str());

      if (type == FE_WAV || type == FE_M3U
#ifdef HAVE_MP3_SUPPORT
          || type == FE_MP3
#endif
#ifdef HAVE_OGG_SUPPORT
          || type == FE_OGG
#endif
          ) {
        project_->appendTrack(fn.c_str());
      }
    }
    break;
  }
}
void AudioCDView::trackInfo()
{
  int track;
  
  if (tocEditView_->trackSelection(&track)) {

    if (trackInfoDialog_ == 0)
      trackInfoDialog_ = new TrackInfoDialog();
  
    trackInfoDialog_->start(tocEditView_);

  } else {

    Gtk::MessageDialog md(*project_->getParentWindow (), _("Please select a track first"),
                          Gtk::MESSAGE_INFO);
    md.run();
  }
}

void AudioCDView::cutTrackData()
{
  if (!project_->tocEdit()->editable()) {
    project_->tocBlockedMsg("Cut");
    return;
  }

  switch (project_->tocEdit()->removeTrackData(tocEditView_)) {
  case 0:
    project_->statusMessage(_("Removed selected samples."));
    signal_tocModified(UPD_TOC_DATA | UPD_TRACK_DATA | UPD_SAMPLE_SEL | UPD_SAMPLE_MARKER | UPD_SAMPLES);
    break;
  case 1:
    project_->statusMessage(_("Please select samples."));
    break;
  case 2:
    project_->statusMessage(_("Selected sample range crosses track "
                              "boundaries."));
    break;
  }
}

void AudioCDView::pasteTrackData()
{
  if (!project_->tocEdit()->editable()) {
    project_->tocBlockedMsg("Paste");
    return;
  }

  switch (project_->tocEdit()->insertTrackData(tocEditView_)) {
  case 0:
    project_->statusMessage(_("Pasted samples."));
    signal_tocModified(UPD_TOC_DATA | UPD_TRACK_DATA | UPD_SAMPLE_SEL);
    break;
  case 1:
    project_->statusMessage(_("No samples in scrap."));
    break;
  }
}

void AudioCDView::addTrackMark()
{
  unsigned long sample;

  if (!project_->tocEdit()->editable()) {
    project_->tocBlockedMsg(_("Add Track Mark"));
    return;
  }

  if (getMarker(&sample)) {
    long lba;
    int snapped = snapSampleToBlock(sample, &lba);

    switch (project_->tocEdit()->addTrackMarker(lba)) {
    case 0:
      project_->statusMessage(_("Added track mark at %s%s."), Msf(lba).str(),
                              snapped ? _(" (snapped to next block)") : "");
      signal_tocModified(UPD_TOC_DATA | UPD_TRACK_DATA | UPD_SAMPLE_MARKER);
      break;

    case 2:
      project_->statusMessage(_("Cannot add track at this point."));
      break;

    case 3:
    case 4:
      project_->statusMessage(_("Resulting track would be shorter than "
                                "4 seconds."));
      break;

    case 5:
      project_->statusMessage(_("Cannot modify a data track."));
      break;

    default:
      project_->statusMessage(_("Internal error in addTrackMark(), please "
                                "report."));
      break;
    }
  }
}

void AudioCDView::addIndexMark()
{
  unsigned long sample;

  if (!project_->tocEdit()->editable()) {
    project_->tocBlockedMsg(_("Add Index Mark"));
    return;
  }

  if (getMarker(&sample)) {
    long lba;
    int snapped = snapSampleToBlock(sample, &lba);

    switch (project_->tocEdit()->addIndexMarker(lba)) {
    case 0:
      project_->statusMessage(_("Added index mark at %s%s."), Msf(lba).str(),
                              snapped ? _(" (snapped to next block)") : "");
      signal_tocModified(UPD_TRACK_DATA | UPD_SAMPLE_MARKER);
      break;

    case 2:
      project_->statusMessage(_("Cannot add index at this point."));
      break;

    case 3:
      project_->statusMessage(_("Track has already 98 index marks."));
      break;

    default:
      project_->statusMessage(_("Internal error in addIndexMark(), "
                                "please report."));
      break;
    }
  }
}

void AudioCDView::addPregap()
{
  unsigned long sample;

  if (!project_->tocEdit()->editable()) {
    project_->tocBlockedMsg(_("Add Pre-Gap"));
    return;
  }

  if (getMarker(&sample)) {
    long lba;
    int snapped = snapSampleToBlock(sample, &lba);

    switch (project_->tocEdit()->addPregap(lba)) {
    case 0:
      project_->statusMessage(_("Added pre-gap mark at %s%s."), Msf(lba).str(),
                              snapped ? _(" (snapped to next block)") : "");
      signal_tocModified(UPD_TRACK_DATA | UPD_SAMPLE_MARKER);
      break;

    case 2:
      project_->statusMessage(_("Cannot add pre-gap at this point."));
      break;

    case 3:
      project_->statusMessage(_("Track would be shorter than 4 seconds."));
      break;

    case 4:
      project_->statusMessage(_("Cannot modify a data track."));
      break;

    default:
      project_->statusMessage(_("Internal error in addPregap(), "
                                "please report."));
      break;
    }
  }
}

void AudioCDView::removeTrackMark()
{
  int trackNr;
  int indexNr;

  if (!project_->tocEdit()->editable()) {
    project_->tocBlockedMsg(_("Remove Track Mark"));
    return;
  }

  if (tocEditView_->trackSelection(&trackNr) &&
      tocEditView_->indexSelection(&indexNr)) {
    switch (project_->tocEdit()->removeTrackMarker(trackNr, indexNr)) {
    case 0:
      project_->statusMessage(_("Removed track/index marker."));
      signal_tocModified(UPD_TOC_DATA | UPD_TRACK_DATA | UPD_SAMPLE_MARKER);
      break;
    case 1:
      project_->statusMessage(_("Cannot remove first track."));
      break;
    case 3:
      project_->statusMessage(_("Cannot modify a data track."));
      break;
    default:
      project_->statusMessage(_("Internal error in removeTrackMark(), "
                                "please report."));
      break; 
    }
  }
  else {
    project_->statusMessage(_("Please select a track/index mark."));
  }
}

int AudioCDView::snapSampleToBlock(unsigned long sample, long *block)
{
  unsigned long rest = sample % SAMPLES_PER_BLOCK;

  *block = sample / SAMPLES_PER_BLOCK;

  if (rest == 0) 
    return 0;

  if (rest > SAMPLES_PER_BLOCK / 2)
    *block += 1;

  return 1;
}

void AudioCDView::trackMarkMovedCallback(const Track *, int trackNr,
					int indexNr, unsigned long sample)
{
  if (!project_->tocEdit()->editable()) {
    project_->tocBlockedMsg(_("Move Track Marker"));
    return;
  }

  long lba;
  int snapped = snapSampleToBlock(sample, &lba);

  switch (project_->tocEdit()->moveTrackMarker(trackNr, indexNr, lba)) {
  case 0:
    project_->statusMessage(_("Moved track marker to %s%s."), Msf(lba).str(),
                            snapped ? _(" (snapped to next block)") : "");
    break;

  case 6:
    project_->statusMessage(_("Cannot modify a data track."));
    break;
  default:
    project_->statusMessage(_("Illegal track marker position."));
    break;
  }

  tocEditView_->trackSelection(trackNr);
  tocEditView_->indexSelection(indexNr);

  update(UPD_TRACK_MARK_SEL);
}

void AudioCDView::appendTrack()
{
  addFileDialog_.mode(AddFileDialog::M_APPEND_TRACK);
  addFileDialog_.start();
}

void AudioCDView::appendFile()
{
  addFileDialog_.mode(AddFileDialog::M_APPEND_FILE);
  addFileDialog_.start();
}

void AudioCDView::insertFile()
{
  addFileDialog_.mode(AddFileDialog::M_INSERT_FILE);
  addFileDialog_.start();
}

void AudioCDView::appendSilence()
{
  if (addSilenceDialog_ == 0) {
    addSilenceDialog_ = new AddSilenceDialog();
    addSilenceDialog_->set_transient_for(*project_->getParentWindow ());
    addSilenceDialog_->signal_tocModified.
      connect(sigc::mem_fun(*this, &AudioCDView::update));
    addSilenceDialog_->signal_fullView.
      connect(sigc::mem_fun(*this, &AudioCDView::fullView));
  }

  addSilenceDialog_->mode(AddSilenceDialog::M_APPEND);
  addSilenceDialog_->start(tocEditView_);
}

void AudioCDView::insertSilence()
{
  if (addSilenceDialog_ == 0)
    addSilenceDialog_ = new AddSilenceDialog();

  addSilenceDialog_->mode(AddSilenceDialog::M_INSERT);
  addSilenceDialog_->start(tocEditView_);
}

const char *AudioCDView::sample2string(unsigned long sample)
{
  static char buf[50];

  unsigned long min = sample / (60 * 44100);
  sample %= 60 * 44100;

  unsigned long sec = sample / 44100;
  sample %= 44100;

  unsigned long frame = sample / 588;
  sample %= 588;

  sprintf(buf, "%2lu:%02lu:%02lu.%03lu", min, sec, frame, sample);
  
  return buf;
}

unsigned long AudioCDView::string2sample(const char *str)
{
  int m = 0;
  int s = 0;
  int f = 0;
  int n = 0;

  sscanf(str, "%d:%d:%d.%d", &m, &s, &f, &n);

  if (m < 0)
    m = 0;

  if (s < 0 || s > 59)
    s = 0;

  if (f < 0 || f > 74)
    f = 0;

  if (n < 0 || n > 587)
    n = 0;

  return Msf(m, s, f).samples() + n;
}