/*
 * Copyright (C) 2006-2019 Andreas Persson
 * Copyright (C) 2007-2026 Christian Schoenebeck
 *
 * 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, 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 program; see the file COPYING. If not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */

#include "global.h"
#include "paramedit.h"

#include "compat.h"
#include "Settings.h"

#ifdef GLIBMM_HEADER_FILE
# include GLIBMM_HEADER_FILE(glibmm.h)
#else
# include <glibmm.h>
#endif

#include <gtkmm/messagedialog.h>

namespace {
    struct CCText {
        const char* const txt;
        bool isExtension; ///< True if this is a controller only supported by LinuxSampler, but not supperted by Gigasampler/GigaStudio.
    };
    static const CCText controlChangeTexts[] = {
        // 3 special ones (not being CCs)
        { _("none") }, { _("channelaftertouch") }, { _("velocity") },
        {0}, // bank select MSB (hard coded in sampler, so discouraged to be used here, even though considerable)
        { _("modwheel") }, // "Modulation Wheel or Lever",
        { _("breath") }, // "Breath Controller",
        { _("undefined"), true },
        { _("foot") }, // "Foot Controller",
        { _("portamentotime") }, // "Portamento Time",
        { _("data entry MSB"), true },
        { _("volume"), true },
        { _("balance"), true },
        { _("undefined"), true },
        { _("pan"), true },
        { _("expression"), true },
        { _("effect1") }, // "Effect Control 1",
        { _("effect2") }, // "Effect Control 2",
        { _("undefined"), true },
        { _("undefined"), true },
        { _("genpurpose1") }, // "General Purpose Controller 1",
        { _("genpurpose2") }, // "General Purpose Controller 2",
        { _("genpurpose3") }, // "General Purpose Controller 3",
        { _("genpurpose4") }, // "General Purpose Controller 4",
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        
        // LSB variant of the various controllers above
        // (so discouraged to be used here for now)
        {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0},
        {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0},
        {0}, {0}, {0}, {0},
        
        { _("sustainpedal") }, // "Damper Pedal on/off (Sustain)",
        { _("portamento") }, // "Portamento On/Off",
        { _("sostenuto") }, // "Sustenuto On/Off",
        { _("softpedal") }, // "Soft Pedal On/Off",
        { _("legato"), true },
        { _("hold2"), true },
        { _("soundvariation"), true },
        { _("timbre"), true },
        { _("releasetime"), true },
        { _("attacktime"), true },
        { _("brightness"), true },
        { _("decaytime"), true },
        { _("vibratorate"), true },
        { _("vibratodepth"), true },
        { _("vibratodelay"), true },
        { _("undefined"), true },
        { _("genpurpose5") }, // "General Purpose Controller 5",
        { _("genpurpose6") }, // "General Purpose Controller 6",
        { _("genpurpose7") }, // "General Purpose Controller 7",
        { _("genpurpose8") }, // "General Purpose Controller 8",
        { _("portamentoctrl"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        {0}, // high resolution velocity prefix (so discouraged to be used here)
        { _("undefined"), true },
        { _("undefined"), true },
        { _("effect1depth") }, // "Effects 1 Depth",
        { _("effect2depth") }, // "Effects 2 Depth",
        { _("effect3depth") }, // "Effects 3 Depth",
        { _("effect4depth") }, // "Effects 4 Depth",
        { _("effect5depth") }, // "Effects 5 Depth"
        { _("dataincrement"), true },
        { _("datadecrement"), true },
        {0}, // NRPN LSB (so discouraged to be used here)
        {0}, // NRPN MSB (so discouraged to be used here)
        {0}, // RPN LSB (so discouraged to be used here)
        {0}, // RPN MSB (so discouraged to be used here)
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true },
        { _("undefined"), true } // CC 119
        // (all other ones that follow [CC 120- CC 127] are hard coded channel
        // mode messages, so those are discouraged to be used here)
    };
    static const char* const lfoWaveTexts[] = {
        _("Sine"),
        _("Triangle"),
        _("Saw"),
        _("Square"),
    };
}

#define controlChangeTextsSize  (sizeof(controlChangeTexts) / sizeof(CCText))
#define lfoWaveTextsSize        (sizeof(lfoWaveTexts) / sizeof(char*))

static void _apply_alt_value_color(bool alt, Gtk::Widget* widget) {
    if (!widget) return;

    if (alt)
        widget->override_color(GEColors::multipleValuesColor,
                               Gtk::STATE_FLAG_NORMAL);
    else
        gtk_widget_override_color((GtkWidget*)widget->gobj(),
                                  GTK_STATE_FLAG_NORMAL, NULL);
}


GEComboBoxText::GEComboBoxText() {
    // we must compare against get_active_row_number() which might return -1
    altIndex = -2;

    Gtk::CellRendererText* renderer = Gtk::manage(new Gtk::CellRendererText());
    pack_start(*renderer, true);
    add_attribute(renderer->property_text(), columns.text);
    add_attribute(renderer->property_foreground(), columns.color);

    model = Gtk::ListStore::create(columns);
    set_model(model);
}

void GEComboBoxText::append(const Glib::ustring& text){
    auto row = *model->append();
    row[columns.text] = text;
    // performance optimization: init with default color to avoid unnecessary
    // heavy UI rebuilds
    row[columns.color] = GEColors::comboTextColor;
}

void GEComboBoxText::remove_all() {
    model->clear();
}

void GEComboBoxText::set_alt_value(bool alt) {
    const int currentIndex = get_active_row_number();
    // optimization mandatory: prevent redundant (heavy) updates of model below
    if ((!alt && altIndex == -2) || (alt && altIndex == currentIndex)) return;

    const Glib::ustring multiColor = GEColors::multipleValuesColor;
    const Glib::ustring comboTextColor = GEColors::comboTextColor;

    auto rows = model->children();
    for (int i = 0; i < rows.size(); ++i) {
        auto row = rows[i];
        Glib::ustring color =
            (alt && i == currentIndex) ? multiColor : comboTextColor;
        // neither Gtkmm nor Gtk check for real value change, causing huge
        // unnecessary UI rebuild overhead
        if (row[columns.color] == color) continue;
        // CAUTION: this is a heavy operation!
        //
        // The problem here is, that Gtk immediately rebuilds the entire
        // combobox widget, without throttling the event or dispatching or
        // anything, and as this assignment is done in a loop here, and on top
        // of that also being multiple instances of this class, this hence
        // easily sums up to multiple seconds. Unfortunately Gtk(3), unlike
        // other toolkits, does not provide a way to temporarily disable UI
        // updates, otherwise we could disable it before the loop and re-enable
        // after the loop. Gtk does use internally SIGNAL_ROW_CHANGED to trigger
        // this rebuild, but I have not found a way to temporarily disable the
        // signal for Gtk. It is possible to disable the signal for own signal
        // handlers, but not for Gtk's own internal handler of this signal.
        row[columns.color] = color;
    }

    altIndex = alt ? currentIndex : -2;

    signal_alt_value_changed.emit(alt);
}

bool GEComboBoxText::has_alt_value() const {
    return altIndex != -2;
}


LabelWidget::LabelWidget(const char* labelText, Gtk::Widget& widget) :
    label(Glib::ustring(labelText) + ":"),
    widget(widget)
{
    m_alt = false;
#if HAS_GTKMM_ALIGNMENT
    label.set_alignment(Gtk::ALIGN_START);
#else
    label.set_halign(Gtk::Align::START);
#endif
    Settings::singleton()->showTooltips.get_proxy().signal_changed().connect(
        sigc::mem_fun(*this, &LabelWidget::on_show_tooltips_changed)
    );

    // workaround for a crash with certain gtkmm versions: postpone calling
    // on_show_tooltips_changed() because widget.gobj() might be uninitialized
    // at this point yet
    Glib::signal_idle().connect_once( // timeout starts given amount of ms after the main loop became idle again ...
        sigc::mem_fun(*this, &LabelWidget::on_show_tooltips_changed),
        300
    );
}

void LabelWidget::apply_alt_value_color(bool alt, Gtk::Widget* widget) {
    _apply_alt_value_color(alt, widget);
}

void LabelWidget::on_show_tooltips_changed() {
    const bool b = Settings::singleton()->showTooltips;
    label.set_has_tooltip(b);
    widget.set_has_tooltip(b);
}

void LabelWidget::set_sensitive(bool sensitive)
{
    label.set_sensitive(sensitive);
    widget.set_sensitive(sensitive);
}

Glib::ustring LabelWidget::shortDescription() const {
    if (!GEPropWidgetAPI::shortDescription().empty())
        return GEPropWidgetAPI::shortDescription();
    return label.get_text();
}

void LabelWidget::set_alt_value(bool alt) {
    if (m_alt == alt) return;
    m_alt = alt;

    _apply_alt_value_color(alt, &widget);

    signal_alt_value_changed.emit(alt);
}

bool LabelWidget::has_alt_value() const {
    return m_alt;
}

ReadOnlyLabelWidget::ReadOnlyLabelWidget(const char* leftHandText)
    : LabelWidget(leftHandText, eventbox)
{
#if HAS_GTKMM_ALIGNMENT
    text.set_alignment(Gtk::ALIGN_START, Gtk::ALIGN_START);
#else
    label.set_halign(Gtk::Align::START);
    label.set_valign(Gtk::Align::START);
#endif
    // on Gtk 3 there is absolutely no margin by default
    #if GTKMM_MAJOR_VERSION >= 3
    eventbox.set_margin_top( LabelWidget::verticalMargin );
    eventbox.set_margin_bottom( LabelWidget::verticalMargin );
    #endif
    eventbox.add(text);
}

ReadOnlyLabelWidget::ReadOnlyLabelWidget(const char* leftHandText, const char* rightHandText)
   : LabelWidget(leftHandText, eventbox)
{
#if HAS_GTKMM_ALIGNMENT
    text.set_alignment(Gtk::ALIGN_START, Gtk::ALIGN_START);
#else
    text.set_halign(Gtk::Align::START);
    text.set_valign(Gtk::Align::START);
#endif
    text.set_text(rightHandText);
    eventbox.add(text);
}

static double stepForDecimals(int decimals) {
    switch (decimals) {
        case 0: return 1.0;
        case 1: return 0.1;
        case 2: default: return 0.01;
    }
}

static double pageForDecimals(int decimals) {
    switch (decimals) {
        case 0: return 10.0;
        case 1: return 1.0;
        case 2: default: return 0.1;
    }
}

NumEntry::NumEntry(const char* labelText, double lower, double upper,
                   int decimals) :
    LabelWidget(labelText, box),
#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
    adjust(lower, lower, upper, stepForDecimals(decimals), pageForDecimals(decimals)),
#else
    adjust(Gtk::Adjustment::create(lower, lower, upper, stepForDecimals(decimals), pageForDecimals(decimals))),
#endif
    scale(adjust),
    spinbutton(adjust)
{
    scale.set_size_request(70);
    spinbutton.set_digits(decimals);
    spinbutton.set_value(0);
    spinbutton.set_numeric();
    scale.set_draw_value(false);
    box.pack_start(spinbutton, Gtk::PACK_SHRINK);
    box.add(scale);
}

void NumEntry::set_alt_value(bool alt) {
    if (m_alt == alt) return;
    m_alt = alt;

    _apply_alt_value_color(alt, &spinbutton);

    signal_alt_value_changed.emit(alt);
}

bool NumEntry::has_alt_value() const {
    return m_alt;
}

void NumEntry::on_show_tooltips_changed() {
    LabelWidget::on_show_tooltips_changed();

    const bool b = Settings::singleton()->showTooltips;
    spinbutton.set_has_tooltip(b);
    scale.set_has_tooltip(b);
}

NumEntryGain::NumEntryGain(const char* labelText,
			   double lower, double upper,
			   int decimals, double coeff) :
    NumEntry(labelText, lower, upper, decimals),
    value(0),
    coeff(coeff),
    connected(true)
{
    spinbutton.set_increments(0.1, 1.0);
    spinbutton.signal_value_changed().connect(
        sigc::mem_fun(*this, &NumEntryGain::value_changed));
}

std::string NumEntryGain::gainToStr(int32_t value) const {
    std::ostringstream oss;
    oss << (value / coeff);
    return oss.str();
}

void NumEntryGain::value_changed()
{
    if (!connected) return;

    const double f = pow(10, spinbutton.get_digits());
    int new_value = round_to_int(spinbutton.get_value() * f);
    if (new_value != round_to_int(value / coeff * f)) {
        value = round_to_int(new_value / f * coeff);
        sig_changed();
    }
}

void NumEntryGain::set_value(int32_t value)
{
    if (value != this->value) {
        this->value = value;

        connected = false;
        spinbutton.set_value(value / coeff);
        connected = true;

        sig_changed();
    }
}


NumEntryPermille::NumEntryPermille(const char* labelText,
                                   double lower, double upper, int decimals) :
    NumEntry(labelText, lower, upper, decimals),
    value(0)
{
    spinbutton.signal_value_changed().connect(
        sigc::mem_fun(*this, &NumEntryPermille::value_changed));
}

void NumEntryPermille::value_changed()
{
    uint16_t new_value = uint16_t(spinbutton.get_value() * 10 + 0.5);
    if (new_value != value) {
        value = uint16_t(spinbutton.get_value() * 10 + 0.5);
        sig_changed();
    }
}

void NumEntryPermille::set_value(uint16_t value)
{
    if (value != this->value) {
        spinbutton.set_value(value / 10.0);
    }
}


NoteEntry::NoteEntry(const char* labelText) :
    NumEntryTemp<uint8_t>(labelText)
{
    spin_button_show_notes(spinbutton);
}

namespace {
    const char* notes[] = {
        _("C"), _("C#"), _("D"), _("D#"), _("E"), _("F"),_("F#"),
        _("G"), _("G#"), _("A"), _("A#"), _("B")
    };

    int note_value(const Glib::ustring& note, double* value)
    {
        const char* str = note.c_str();

        int i;
        for (i = 11 ; i >= 0 ; i--) {
            if (strncasecmp(str, notes[i], strlen(notes[i])) == 0) break;
        }
        if (i >= 0) {
            char* endptr;
            long x = strtol(str + strlen(notes[i]), &endptr, 10);
            if (endptr != str + strlen(notes[i])) {
                *value = std::max(0L, std::min(i + (x + 1) * 12, 127L));
                return true;
            }
        } else {
            char* endptr;
            long x = strtol(str, &endptr, 10);
            if (endptr != str) {
                *value = std::max(0L, std::min(x, 127L));
                return true;
            }
        }

#if HAS_GTKMM_CPP11_ENUMS
        return Gtk::SpinButton::INPUT_ERROR;
#else
        return Gtk::INPUT_ERROR;
#endif
    }
}

int note_value(const Glib::ustring& note)
{
    double value = 0;
    note_value(note, &value);
    return value;
}

std::string note_str(uint8_t note)
{
    char buf[10];
    snprintf(buf, sizeof(buf), "%s%d", notes[note % 12], note / 12 - 1);
    return buf;
}

namespace {
    // Convert the Entry text to a number
#if GTKMM_MAJOR_VERSION > 3 || (GTKMM_MAJOR_VERSION == 3 && (GTKMM_MINOR_VERSION > 91 || (GTKMM_MINOR_VERSION == 91 && GTKMM_MICRO_VERSION >= 2))) // GTKMM >= 3.91.2
    int on_input(double& new_value, Gtk::SpinButton* spinbutton) {
        return note_value(spinbutton->get_text(), &new_value);
    }
#else
    int on_input(double* new_value, Gtk::SpinButton* spinbutton) {
        return note_value(spinbutton->get_text(), new_value);
    }
#endif

    // Convert the Adjustment position to text
    bool on_output(Gtk::SpinButton* spinbutton) {
        spinbutton->set_text(
            note_str(spinbutton->get_adjustment()->get_value() + 0.5));
        return true;
    }
}

// Make a SpinButton show notes instead of numbers
void spin_button_show_notes(Gtk::SpinButton& spin_button)
{
    spin_button.set_numeric(false);
    spin_button.set_width_chars(4);
    spin_button.signal_input().connect(
        sigc::bind(sigc::ptr_fun(&on_input), &spin_button));
    spin_button.signal_output().connect(
        sigc::bind(sigc::ptr_fun(&on_output), &spin_button));
}

ChoiceEntryBase::ChoiceEntryBase(const char* labelText, Gtk::Widget& widget) : LabelWidget(labelText, widget)
{
    eventbox.add(combobox);
}

void ChoiceEntryBase::set_alt_value(bool alt) {
    if (m_alt == alt) return;
    m_alt = alt;

    combobox.set_alt_value(alt);

    signal_alt_value_changed.emit(alt);
}

bool ChoiceEntryBase::has_alt_value() const {
    return m_alt;
}

void ChoiceEntryBase::on_show_tooltips_changed() {
    LabelWidget::on_show_tooltips_changed();

    const bool b = Settings::singleton()->showTooltips;
    combobox.set_has_tooltip(b);
}

ChoiceEntryLeverageCtrl::ChoiceEntryLeverageCtrl(const char* labelText) :
#if HAS_GTKMM_ALIGNMENT
    LabelWidget(labelText, align),
    align(0, 0, 0, 0)
#else
    LabelWidget(labelText, eventbox)
#endif
{
    for (int i = 0 ; i < controlChangeTextsSize ; i++) {
        if (controlChangeTexts[i].txt) {
            const int cc = i - 3;
            Glib::ustring s = (i < 3)
                ? controlChangeTexts[i].txt
                : Glib::ustring::compose("CC%1: %2%3", cc, controlChangeTexts[i].txt, controlChangeTexts[i].isExtension ? " [EXT]" : "");

            combobox.append(s);
        }
    }
    combobox.set_wrap_width(4);
    combobox.signal_changed().connect(
        sigc::mem_fun(*this, &ChoiceEntryLeverageCtrl::value_changed));
    eventbox.add(combobox);
#if HAS_GTKMM_ALIGNMENT
    align.add(eventbox);
#else
    combobox.set_halign(Gtk::Align::FILL);
    combobox.set_valign(Gtk::Align::FILL);
#endif
    value.type = gig::leverage_ctrl_t::type_none;
    value.controller_number = 0;
}

void ChoiceEntryLeverageCtrl::on_show_tooltips_changed() {
    LabelWidget::on_show_tooltips_changed();

    const bool b = Settings::singleton()->showTooltips;
    combobox.set_has_tooltip(b);
}

static void _showGigFormatExtWarning() {
    if (!Settings::singleton()->warnUserOnExtensions) return;
    Glib::ustring txt =
        _("<b>Format Extension</b>\n\nAll options marked with \"<b>[EXT]</b>\" are an extension to the original gig sound format. They will only work with LinuxSampler, but they will <b>not work</b> with Gigasampler/GigaStudio!\n\n(You may disable this warning in the <i>Settings</i> menu.)");
    Gtk::MessageDialog msg(txt, true, Gtk::MESSAGE_WARNING);
    msg.run();
}

void ChoiceEntryLeverageCtrl::value_changed()
{
    int rowno = combobox.get_active_row_number();
    switch (rowno)
    {
    case -1:
        break;
    case 0:
        value.type = gig::leverage_ctrl_t::type_none;
        break;
    case 1:
        value.type = gig::leverage_ctrl_t::type_channelaftertouch;
        break;
    case 2:
        value.type = gig::leverage_ctrl_t::type_velocity;
        break;
    default:
        value.type = gig::leverage_ctrl_t::type_controlchange;
        int x = 3;
        for (uint cc = 0 ; cc < controlChangeTextsSize - 3 ; cc++) {
            if (controlChangeTexts[cc + 3].txt) {
                if (rowno == x) {
                    value.controller_number = cc;
                    if (controlChangeTexts[cc + 3].isExtension) {
                        _showGigFormatExtWarning();
                    }
                    break;
                }
                x++;
            }
        }
        break;
    }
    if (rowno >= 0) sig_changed();
}

void ChoiceEntryLeverageCtrl::set_value(gig::leverage_ctrl_t value)
{
    int comboIndex;
    switch (value.type)
    {
    case gig::leverage_ctrl_t::type_none:
        comboIndex = 0;
        break;
    case gig::leverage_ctrl_t::type_channelaftertouch:
        comboIndex = 1;
        break;
    case gig::leverage_ctrl_t::type_velocity:
        comboIndex = 2;
        break;
    case gig::leverage_ctrl_t::type_controlchange: {
        comboIndex = -1;
        int x = 3;
        for (uint cc = 0 ; cc < controlChangeTextsSize - 3 ; cc++) {
            if (controlChangeTexts[cc + 3].txt) {
                if (value.controller_number == cc) {
                    comboIndex = x;
                    break;
                }
                x++;
            }
        }
        break;
    }
    default:
        comboIndex = -1;
        break;
    }
    combobox.set_active(comboIndex);
}

void ChoiceEntryLeverageCtrl::set_alt_value(bool alt) {
    if (m_alt == alt) return;
    m_alt = alt;

    combobox.set_alt_value(alt);

    signal_alt_value_changed.emit(alt);
}

bool ChoiceEntryLeverageCtrl::has_alt_value() const {
    return m_alt;
}


ChoiceEntryLfoWave::ChoiceEntryLfoWave(const char* labelText) :
#if HAS_GTKMM_ALIGNMENT
    LabelWidget(labelText, align),
    align(0, 0, 0, 0)
#else
    LabelWidget(labelText, eventbox)
#endif
{
    for (int i = 0 ; i < lfoWaveTextsSize; i++) {
        if (lfoWaveTexts[i]) {
            Glib::ustring s = (i == 0)
                ? lfoWaveTexts[i]
                : Glib::ustring::compose("%1 [EXT]", lfoWaveTexts[i]);

            combobox.append(s);
        }
    }
    //combobox.set_wrap_width(4);
    combobox.signal_changed().connect(
        sigc::mem_fun(*this, &ChoiceEntryLfoWave::value_changed));
    eventbox.add(combobox);
#if HAS_GTKMM_ALIGNMENT
    align.add(eventbox);
#else
    combobox.set_halign(Gtk::Align::FILL);
    combobox.set_valign(Gtk::Align::FILL);
#endif
    value = gig::lfo_wave_sine;
}

void ChoiceEntryLfoWave::set_alt_value(bool alt) {
    if (m_alt == alt) return;
    m_alt = alt;

    combobox.set_alt_value(alt);

    signal_alt_value_changed.emit(alt);
}

bool ChoiceEntryLfoWave::has_alt_value() const {
    return m_alt;
}

void ChoiceEntryLfoWave::on_show_tooltips_changed() {
    LabelWidget::on_show_tooltips_changed();

    const bool b = Settings::singleton()->showTooltips;
    combobox.set_has_tooltip(b);
}

void ChoiceEntryLfoWave::value_changed() {
    const int rowno = combobox.get_active_row_number();
    switch (rowno) {
        case -1:
            break;
        case 0:
            value = gig::lfo_wave_sine;
            break;
        case 1:
            value = gig::lfo_wave_triangle;
            _showGigFormatExtWarning();
            break;
        case 2:
            value = gig::lfo_wave_saw;
            _showGigFormatExtWarning();
            break;
        case 3:
            value = gig::lfo_wave_square;
            _showGigFormatExtWarning();
            break;
    }
    if (rowno >= 0) sig_changed();
}

void ChoiceEntryLfoWave::set_value(gig::lfo_wave_t value) {
    int comboIndex;
    switch (value) {
        case gig::lfo_wave_sine:
            comboIndex = 0;
            break;
        case gig::lfo_wave_triangle:
            comboIndex = 1;
            break;
        case gig::lfo_wave_saw:
            comboIndex = 2;
            break;
        case gig::lfo_wave_square:
            comboIndex = 3;
            break;
        default:
            comboIndex = -1;
            break;
    }
    combobox.set_active(comboIndex);
}


BoolBox::BoolBox(const char* labelText) : Gtk::CheckButton(labelText) {
    m_alt = false;
    signal_toggled().connect(sig_changed.make_slot());
    Settings::singleton()->showTooltips.get_proxy().signal_changed().connect(
        sigc::mem_fun(*this, &BoolBox::on_show_tooltips_changed)
    );
    on_show_tooltips_changed();
}

void BoolBox::set_alt_value(bool alt) {
    if (m_alt == alt) return;
    m_alt = alt;

    _apply_alt_value_color(alt, this);

    signal_alt_value_changed.emit(alt);
}

bool BoolBox::has_alt_value() const {
    return m_alt;
}

void BoolBox::on_show_tooltips_changed() {
    const bool b = Settings::singleton()->showTooltips;
    set_has_tooltip(b);
}


BoolEntry::BoolEntry(const char* labelText) :
    LabelWidget(labelText, checkbutton),
    checkbutton(labelText)
{
    checkbutton.signal_toggled().connect(sig_changed.make_slot());
}


StringEntry::StringEntry(const char* labelText) :
    LabelWidget(labelText, entry)
{
    entry.signal_changed().connect(sig_changed.make_slot());
}

gig::String StringEntry::get_value() const
{
    return gig_from_utf8(entry.get_text());
}

void StringEntry::set_value(const gig::String& value) {
    entry.set_text(gig_to_utf8(value));
}


StringEntryMultiLine::StringEntryMultiLine(const char* labelText) :
    LabelWidget(labelText, frame)
{
    text_buffer = text_view.get_buffer();
    frame.set_shadow_type(Gtk::SHADOW_IN);
    frame.add(text_view);
    text_buffer->signal_changed().connect(sig_changed.make_slot());
}

gig::String StringEntryMultiLine::get_value() const
{
    Glib::ustring value = text_buffer->get_text();
    for (int i = 0 ; (i = value.find("\x0a", i)) >= 0 ; i += 2)
        value.replace(i, 1, "\x0d\x0a");
    return gig_from_utf8(value);
}

void StringEntryMultiLine::set_value(const gig::String& value)
{
    Glib::ustring text = gig_to_utf8(value);
    for (int i = 0 ; (i = text.find("\x0d\x0a", i, 2)) >= 0 ; i++)
        text.replace(i, 2, "\x0a");
    text_buffer->set_text(text);
}

void StringEntryMultiLine::on_show_tooltips_changed() {
    LabelWidget::on_show_tooltips_changed();

    const bool b = Settings::singleton()->showTooltips;
    text_view.set_has_tooltip(b);
}


Table::Table(int x, int y) :
#if USE_GTKMM_GRID
    Gtk::Grid(),
    cols(x),
#else
    Gtk::Table(x, y),
#endif
    rowno(0)
{
}

void Table::add(BoolEntry& boolentry)
{
#if USE_GTKMM_GRID
    attach(boolentry.widget, 0, rowno, 2);
#else
    attach(boolentry.widget, 0, 2, rowno, rowno + 1,
           Gtk::FILL, Gtk::SHRINK);
#endif
    rowno++;
}

void Table::add(LabelWidget& prop)
{
#if USE_GTKMM_GRID
    attach(prop.label, 1, rowno);
    attach(prop.widget, 2, rowno);
#else
    attach(prop.label, 1, 2, rowno, rowno + 1,
           Gtk::FILL, Gtk::SHRINK);
    attach(prop.widget, 2, 3, rowno, rowno + 1,
           Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK);
#endif
    rowno++;
}
