/*
 * 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 "compat.h"
#include "global.h"
#include "regionchooser.h"

#include <algorithm>
#include <assert.h>

#include <cairomm/context.h>
#include <gdkmm/general.h>
#if HAS_GDKMM_SEAT
# include <gdkmm/seat.h>
#endif
#include <gdkmm/cursor.h>
#if HAS_GTKMM_STOCK
# include <gtkmm/stock.h>
#endif
#include <gdkmm/pixbuf.h>
#include <gtkmm/spinbutton.h>
#include <gtkmm/dialog.h>

#include "Settings.h"
#include "gfx/builtinpix.h"
#include "draw.h"

#define REGION_BLOCK_HEIGHT             30
#define KEYBOARD_HEIGHT                 40

struct RegionFeatures {
    int sampleRefs;
    int loops;
    int validDimRegs;

    RegionFeatures() {
        sampleRefs = loops = validDimRegs = 0;
    }
};

static RegionFeatures regionFeatures(gig::Region* rgn) {
    RegionFeatures f;
    for (int i = 0; i < rgn->DimensionRegions; ++i) {
        gig::DimensionRegion* dr = rgn->pDimensionRegions[i];
        DimensionCase c = dimensionCaseOf(dr);
        if (!isUsedCase(c, rgn)) continue;
        f.validDimRegs++;
        if (dr->pSample) f.sampleRefs++;
        // the user doesn't care about loop if there is no valid sample reference
        if (dr->pSample && dr->SampleLoops) f.loops++;
    }
    return f;
}

void SortedRegions::update(gig::Instrument* instrument) {
    // Usually, the regions in a gig file are ordered after their key
    // range, but there are files where they are not. The
    // RegionChooser code needs a sorted list of regions.
    regions.clear();
    if (instrument) {
        for (size_t r = 0; gig::Region* rgn = instrument->GetRegionAt(r); ++r) {
            regions.push_back(rgn);
        }
        sort(regions.begin(), regions.end(), *this);
    }
}

gig::Region* SortedRegions::first() {
    region_iterator = regions.begin();
    return region_iterator == regions.end() ? 0 : *region_iterator;
}

gig::Region* SortedRegions::next() {
    ++region_iterator;
    return region_iterator == regions.end() ? 0 : *region_iterator;
}



RegionChooser::RegionChooser(Gtk::Window& window) :
    activeKeyColor("red"),
    blue("#4796ff"),
    grey1("grey69"),
    white("white"),
    black("black"),
    m_VirtKeybModeChoice(_("Virtual Keyboard Mode")),
    currentActiveKey(-1),
    modifyallregions(false),
    dimensionManager(window)
{
    set_size_request(500, KEYBOARD_HEIGHT + REGION_BLOCK_HEIGHT);

    loadBuiltInPix();

    // create blue hatched pattern
    {
        const int width = blueHatchedPattern->get_width();
        const int height = blueHatchedPattern->get_height();
        const int stride = blueHatchedPattern->get_rowstride();

        // manually convert from RGBA to ARGB
        this->blueHatchedPatternARGB = blueHatchedPattern->copy();
        const int pixelSize = stride / width;
        const int totalPixels = width * height;
        assert(pixelSize == 4);
        unsigned char* ptr = this->blueHatchedPatternARGB->get_pixels();
        for (int iPixel = 0; iPixel < totalPixels; ++iPixel, ptr += pixelSize) {
            const unsigned char r = ptr[0];
            const unsigned char g = ptr[1];
            const unsigned char b = ptr[2];
            const unsigned char a = ptr[3];
            ptr[0] = b;
            ptr[1] = g;
            ptr[2] = r;
            ptr[3] = a;
        }

        Cairo::RefPtr<Cairo::ImageSurface> imageSurface = Cairo::ImageSurface::create(
#if HAS_CAIROMM_CPP11_ENUMS
            this->blueHatchedPatternARGB->get_pixels(), Cairo::Surface::Format::ARGB32, width, height, stride
#else
            this->blueHatchedPatternARGB->get_pixels(), Cairo::FORMAT_ARGB32, width, height, stride
#endif
        );
        this->blueHatchedSurfacePattern = Cairo::SurfacePattern::create(imageSurface);
#if HAS_CAIROMM_CPP11_ENUMS
        this->blueHatchedSurfacePattern->set_extend(Cairo::Pattern::Extend::REPEAT);
#else
        this->blueHatchedSurfacePattern->set_extend(Cairo::EXTEND_REPEAT);
#endif
    }

    // create gray blue hatched pattern
    {
        const int width = grayBlueHatchedPattern->get_width();
        const int height = grayBlueHatchedPattern->get_height();
        const int stride = grayBlueHatchedPattern->get_rowstride();

        // manually convert from RGBA to ARGB
        this->grayBlueHatchedPatternARGB = grayBlueHatchedPattern->copy();
        const int pixelSize = stride / width;
        const int totalPixels = width * height;
        assert(pixelSize == 4);
        unsigned char* ptr = this->grayBlueHatchedPatternARGB->get_pixels();
        for (int iPixel = 0; iPixel < totalPixels; ++iPixel, ptr += pixelSize) {
            const unsigned char r = ptr[0];
            const unsigned char g = ptr[1];
            const unsigned char b = ptr[2];
            const unsigned char a = ptr[3];
            ptr[0] = b;
            ptr[1] = g;
            ptr[2] = r;
            ptr[3] = a;
        }

        Cairo::RefPtr<Cairo::ImageSurface> imageSurface = Cairo::ImageSurface::create(
#if HAS_CAIROMM_CPP11_ENUMS
            this->grayBlueHatchedPatternARGB->get_pixels(), Cairo::Surface::Format::ARGB32, width, height, stride
#else
            this->grayBlueHatchedPatternARGB->get_pixels(), Cairo::FORMAT_ARGB32, width, height, stride
#endif
        );
        this->grayBlueHatchedSurfacePattern = Cairo::SurfacePattern::create(imageSurface);
#if HAS_CAIROMM_CPP11_ENUMS
        this->grayBlueHatchedSurfacePattern->set_extend(Cairo::Pattern::Extend::REPEAT);
#else
        this->grayBlueHatchedSurfacePattern->set_extend(Cairo::EXTEND_REPEAT);
#endif
    }

    instrument = 0;
    region = 0;
    initial_multiselect_region = NULL;
    resize.active = false;
    move.active = false;
    cursor_is_resize = false;
    h1 = REGION_BLOCK_HEIGHT;

    multiSelectKeyDown = false;
    primaryKeyDown = false;
    shiftKeyDown = false;

    // properties of the virtual keyboard
    {
        const char* choices[] = { _("normal"), _("chord"), 0 };
        static const virt_keyboard_mode_t values[] = {
            VIRT_KEYBOARD_MODE_NORMAL,
            VIRT_KEYBOARD_MODE_CHORD
        };
        m_VirtKeybModeChoice.set_choices(choices, values);
        m_VirtKeybModeChoice.set_value(VIRT_KEYBOARD_MODE_NORMAL);
    }
    m_VirtKeybVelocityLabelDescr.set_text(_("Note-On Velocity:"));
    m_VirtKeybVelocityLabel.set_text("-");
    m_VirtKeybOffVelocityLabelDescr.set_text(_("Note-Off Velocity:"));
    m_VirtKeybOffVelocityLabel.set_text("-");
    m_HoveringParameterLabel.set_margin_right(10);
    m_VirtKeybPropsBox.pack_start(m_VirtKeybModeChoice.label, Gtk::PACK_SHRINK);
    m_VirtKeybPropsBox.pack_start(m_VirtKeybModeChoice.widget, Gtk::PACK_SHRINK);
    m_VirtKeybPropsBox.pack_start(m_VirtKeybVelocityLabelDescr, Gtk::PACK_SHRINK);
    m_VirtKeybPropsBox.pack_start(m_VirtKeybVelocityLabel, Gtk::PACK_SHRINK);
    m_VirtKeybPropsBox.pack_start(m_VirtKeybOffVelocityLabelDescr, Gtk::PACK_SHRINK);
    m_VirtKeybPropsBox.pack_start(m_VirtKeybOffVelocityLabel, Gtk::PACK_SHRINK);
    m_VirtKeybPropsBox.pack_end(m_HoveringParameterLabel, Gtk::PACK_SHRINK);
    m_VirtKeybPropsBox.set_spacing(10);
    m_VirtKeybPropsBox.show();
    for (int i = 0 ; i < 128 ; i++) key_pressed[i] = false;

    actionGroup = ActionGroup::create();
#if USE_GLIB_ACTION
    actionGroup->add_action(
        "Properties", sigc::mem_fun(*this, &RegionChooser::show_region_properties)
    );
    actionGroup->add_action(
        "Remove", sigc::mem_fun(*this, &RegionChooser::delete_region)
    );
    actionGroup->add_action(
        "Add", sigc::mem_fun(*this, &RegionChooser::add_region)
    );
    actionGroup->add_action(
        "Dimensions", sigc::mem_fun(*this, &RegionChooser::manage_dimensions)
    );
    insert_action_group("PopupMenuInsideRegion", actionGroup);
#else
    actionGroup->add(Gtk::Action::create("Properties",
                                         Gtk::Stock::PROPERTIES),
                     sigc::mem_fun(*this,
                                   &RegionChooser::show_region_properties));
    actionGroup->add(Gtk::Action::create("Remove", Gtk::Stock::REMOVE),
                     sigc::mem_fun(*this, &RegionChooser::delete_region));
    actionGroup->add(Gtk::Action::create("Add", Gtk::Stock::ADD),
                     sigc::mem_fun(*this, &RegionChooser::add_region));
    actionGroup->add(Gtk::Action::create("Dimensions", _("Dimensions...")),
                     sigc::mem_fun(*this, &RegionChooser::manage_dimensions));
#endif

#if USE_GTKMM_BUILDER
    uiManager = Gtk::Builder::create();
    Glib::ustring ui_info =
        "<interface>"
        "  <menu id='menu-PopupMenuInsideRegion'>"
        "    <section>"
        "      <item>"
        "        <attribute name='label' translatable='yes'>Properties</attribute>"
        "        <attribute name='action'>PopupMenuInsideRegion.Properties</attribute>"
        "      </item>"
        "      <item>"
        "        <attribute name='label' translatable='yes'>Dimensions</attribute>"
        "        <attribute name='action'>PopupMenuInsideRegion.Dimensions</attribute>"
        "      </item>"
        "      <item>"
        "        <attribute name='label' translatable='yes'>Remove</attribute>"
        "        <attribute name='action'>PopupMenuInsideRegion.Remove</attribute>"
        "      </item>"
        "    </section>"
        "  </menu>"
        "  <menu id='menu-PopupMenuOutsideRegion'>"
        "    <section>"
        "      <item>"
        "        <attribute name='label' translatable='yes'>Add</attribute>"
        "        <attribute name='action'>PopupMenuInsideRegion.Add</attribute>"
        "      </item>"
        "    </section>"
        "  </menu>"
        "</interface>";
    uiManager->add_from_string(ui_info);
    
    popup_menu_inside_region = new Gtk::Menu(
         Glib::RefPtr<Gio::Menu>::cast_dynamic(
             uiManager->get_object("menu-PopupMenuInsideRegion")
         )
    );
    popup_menu_outside_region = new Gtk::Menu(
         Glib::RefPtr<Gio::Menu>::cast_dynamic(
             uiManager->get_object("menu-PopupMenuOutsideRegion")
         )
    );
#else
    uiManager = Gtk::UIManager::create();
    uiManager->insert_action_group(actionGroup);
    Glib::ustring ui_info =
        "<ui>"
        "  <popup name='PopupMenuInsideRegion'>"
        "    <menuitem action='Properties'/>"
        "    <menuitem action='Dimensions'/>"
        "    <menuitem action='Remove'/>"
        "  </popup>"
        "  <popup name='PopupMenuOutsideRegion'>"
        "    <menuitem action='Add'/>"
        "  </popup>"
        "</ui>";
    uiManager->add_ui_from_string(ui_info);

    popup_menu_inside_region = dynamic_cast<Gtk::Menu*>(
        uiManager->get_widget("/PopupMenuInsideRegion"));
    popup_menu_outside_region = dynamic_cast<Gtk::Menu*>(
        uiManager->get_widget("/PopupMenuOutsideRegion"));

#endif // USE_GTKMM_BUILDER

#if GTKMM_MAJOR_VERSION > 3 || (GTKMM_MAJOR_VERSION == 3 && GTKMM_MINOR_VERSION > 24)
# warning GTKMM4 event registration code missing for regionchooser!
    //add_events(Gdk::EventMask::BUTTON_PRESS_MASK);
#else
    add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK |
               Gdk::POINTER_MOTION_MASK | Gdk::POINTER_MOTION_HINT_MASK);
#endif

    dimensionManager.region_to_be_changed_signal.connect(
        region_to_be_changed_signal.make_slot()
    );
    dimensionManager.region_changed_signal.connect(
        region_changed_signal.make_slot()
    );
    dimensionManager.region_changed_signal.connect(
        sigc::hide(
            sigc::mem_fun(*this, &RegionChooser::on_dimension_manager_changed)
        )
    );
    keyboard_key_hit_signal.connect(
        sigc::mem_fun(*this, &RegionChooser::on_note_on_event)
    );
    keyboard_key_released_signal.connect(
        sigc::mem_fun(*this, &RegionChooser::on_note_off_event)
    );
    set_tooltip_text(_("Right click here for adding new region. Use mouse pointer for moving (dragging) or resizing existing regions (by pointing at region's boundary). Right click on an existing region for more actions."));

    Settings::singleton()->showTooltips.get_proxy().signal_changed().connect(
        sigc::mem_fun(*this, &RegionChooser::on_show_tooltips_changed)
    );

    window.signal_key_press_event().connect(
        sigc::mem_fun(*this, &RegionChooser::onKeyPressed)
    );
    window.signal_key_release_event().connect(
        sigc::mem_fun(*this, &RegionChooser::onKeyReleased)
    );

    on_show_tooltips_changed();
}

RegionChooser::~RegionChooser()
{
}

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

    set_has_tooltip(b);
}

void RegionChooser::assign_alt_values_function(FnAltValues fn) {
    fnAltValuesOf = fn;
    // redraw regions with (or without) alt-value colors
    queue_draw();
}

void RegionChooser::on_hovering_property(GEPropWidgetAPI* widget) {
    // update parameter name label for hovering parameter control widget
    Glib::ustring s;
    if (widget) {
        s = "↓ " + widget->shortDescription();
        // drop trailing colon (if any)
        if (endsWith(s, ":", true))
            s = s.substr(0, s.length() - 1);
    }
    m_HoveringParameterLabel.set_text(s);

    // redraw regions with (or without) alt-value colors
    queue_draw();
}

void RegionChooser::setModifyAllRegions(bool b) {
    modifyallregions = b;
    // redraw required parts
    queue_draw();
}

void RegionChooser::invalidate_key(int key) {
    const int h = KEYBOARD_HEIGHT;
    const int w = get_width() - 1;
    int x1 = key_to_x(key - 0.5, w);
    int x2 = key_to_x(key + 1.5, w);

    Gdk::Rectangle rect(x1 + 1, h1 + 1, x2 - x1 - 1, h - 2);
    get_window()->invalidate_rect(rect, false);
}

void RegionChooser::on_note_on_event(int key, int velocity) {
    key_pressed[key] = true;
    invalidate_key(key);
    m_VirtKeybVelocityLabel.set_text(ToString(velocity));
}

void RegionChooser::on_note_off_event(int key, int velocity) {
    key_pressed[key] = false;
    invalidate_key(key);
    m_VirtKeybOffVelocityLabel.set_text(ToString(velocity));
}


#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
bool RegionChooser::on_expose_event(GdkEventExpose* e) {
    double clipx1 = e->area.x;
    double clipx2 = e->area.x + e->area.width;
    double clipy1 = e->area.y;
    double clipy2 = e->area.y + e->area.height;

    const Cairo::RefPtr<Cairo::Context>& cr =
        get_window()->create_cairo_context();
#if 0
}
#endif
#else
bool RegionChooser::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
    double clipx1, clipx2, clipy1, clipy2;
    cr->get_clip_extents(clipx1, clipy1, clipx2, clipy2);
#endif

    cr->save();
    cr->set_line_width(1);

#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
    const Gdk::Color bg = get_style()->get_bg(Gtk::STATE_NORMAL);
#else
#if GTKMM_MAJOR_VERSION > 3 || (GTKMM_MAJOR_VERSION == 3 && GTKMM_MINOR_VERSION > 24)
    GdkRGBA gdkBgRGBA;
    gtk_style_context_get_background_color(get_style_context()->gobj(), &gdkBgRGBA);
    const Gdk::RGBA bg = Glib::wrap(&gdkBgRGBA, true);
# else
    const Gdk::RGBA bg = get_style_context()->get_background_color();
# endif
#endif
    Gdk::Cairo::set_source_rgba(cr, bg);
    cr->paint();

    if (clipy2 > h1) {
        draw_keyboard(cr, clipx1, clipx2);
    }

    if (clipy1 < h1 && instrument) {
        draw_regions(cr, clipx1, clipx2);
    }

    cr->restore();

    return true;
}

void RegionChooser::draw_keyboard(const Cairo::RefPtr<Cairo::Context>& cr,
                                  int clip_low, int clip_high) {
    const int h = KEYBOARD_HEIGHT;
    const int w = get_width() - 1;
    const int bh = int(h * 0.55);

    Gdk::Cairo::set_source_rgba(cr, black);
    cr->rectangle(0.5, h1 + 0.5, w, h - 1);
    cr->stroke();

    int x1 = key_to_x(20.5, w);
    Gdk::Cairo::set_source_rgba(cr, grey1);
    cr->rectangle(1, h1 + 1, x1 - 1, h - 2);
    cr->fill();

    int x2 = key_to_x(109.5, w);
    Gdk::Cairo::set_source_rgba(cr, white);
    cr->rectangle(x1 + 1, h1 + 1, x2 - x1 - 1, h - 2);
    cr->fill();

    Gdk::Cairo::set_source_rgba(cr, grey1);
    cr->rectangle(x2 + 1, h1 + 1, w - x2 - 1, h - 2);
    cr->fill();

    Gdk::Cairo::set_source_rgba(cr, black);

    int clipkey1 = std::max(0, x_to_key_right(clip_low - 1, w));
    int clipkey2 = std::min(x_to_key_right(clip_high - 1, w) + 1, 128);

    for (int i = clipkey1 ; i < clipkey2 ; i++) {
        int note = (i + 3) % 12;
        int x = key_to_x(i, w);

        if (note == 1 || note == 4 || note == 6 ||
            note == 9 || note == 11) {
            // black key: short line in the middle, with a rectangle
            // on top
            int x2 = key_to_x(i + 0.5, w);
            cr->move_to(x2 + 0.5, h1 + bh + 0.5);
            cr->line_to(x2 + 0.5, h1 + h - 1);
            cr->stroke();

            int x3 = key_to_x(i + 1, w);
            cr->rectangle(x, h1 + 1, x3 - x + 1, bh);
            cr->fill();
        } else if (note == 3 || note == 8) {
            // C or F: long line to the left
            cr->move_to(x + 0.5, h1 + 1);
            cr->line_to(x + 0.5, h1 + h - 1);
            cr->stroke();
        }

        if (key_pressed[i]) draw_key(cr, i);

        if (note == 3) draw_digit(cr, i);
    }
}

// Whether region belongs to user's manual multi-selection (e.g. Ctrl-Clicked).
bool RegionChooser::is_expanded_selected(gig::Region* r) const {
    return r ? expandedSelectedRegions.count(r) : false;
}

void RegionChooser::draw_regions(const Cairo::RefPtr<Cairo::Context>& cr,
                                 int clip_low, int clip_high) {
    const int w = get_width() - 1;

    // must be called once per render cycle before calling drawAltValuesFor()
    initDrawAltValues();

    Gdk::Cairo::set_source_rgba(cr, black);
    gig::Region* next_region;
    int x3 = -1;

    // draw regions' background
    for (gig::Region* r = regions.first() ; r ; r = next_region) {
        next_region = regions.next();

        const bool regionSelected = is_expanded_selected(r);
        const bool nextRegionSelected = is_expanded_selected(next_region);

        if (x3 < 0) {
            x3 = key_to_x(r->KeyRange.low, w);
            if (x3 >= clip_high) break;
        }
        // Draw region's background block by block, i.e. if a consecutive span
        // of regions have the same (background) appearance then draw them
        // altogether here. Split background drawing for next loop iteration if
        // and only if next region needs a different (background) appearance.
        if (!next_region ||
            r->KeyRange.high + 1 != next_region->KeyRange.low ||
            r == region || next_region == region ||
            regionSelected != nextRegionSelected)
        {
            int x2 = key_to_x(r->KeyRange.high + 1, w);
            if (x2 >= clip_low) {
                cr->move_to(x3, 0.5);
                cr->line_to(x2 + 0.5, 0.5);
                cr->line_to(x2 + 0.5, h1 - 0.5);
                cr->line_to(x3, h1 - 0.5);
                cr->stroke();

                if (region == r)
                    Gdk::Cairo::set_source_rgba(cr, blue);
                else if (modifyallregions)
                    cr->set_source(blueHatchedSurfacePattern);
                else if (regionSelected)
                    cr->set_source(grayBlueHatchedSurfacePattern);
                else
                    Gdk::Cairo::set_source_rgba(cr, white);

                cr->rectangle(x3 + 1, 1, x2 - x3 - 1, h1 - 2);
                cr->fill();
                Gdk::Cairo::set_source_rgba(cr, black);
            }
            x3 = -1;
        }
    }

    // draw symbolic icons or colored rectangles for alternative values on region
    for (gig::Region* r = regions.first() ; r ; r = regions.next()) {
        int x = key_to_x(r->KeyRange.low, w);
        int x2 = key_to_x(r->KeyRange.high + 1, w);
        const int wRgn = x2 - x;

        // draw colored rectangles (or stripes) for each parameter multi-value
        const bool altValuesDrawn =
            drawAltValuesFor(r, cr, x+2, 1, wRgn-3, h1-2);

        // reset color to black after having drawn in a different color
        Gdk::Cairo::set_source_rgba(cr, black);

        // don't draw icons if alt-values stripes were drawn on this region
        if (altValuesDrawn) continue;

        RegionFeatures features = regionFeatures(r);

        // draw symbolic icons on region as summary for common features or issues
        const bool bShowLoopSymbol = features.loops > 0;
        const bool bShowSampleRefSymbol = features.sampleRefs < features.validDimRegs;
        if (bShowLoopSymbol || bShowSampleRefSymbol) {
            const int margin = 2;
            //printf("x=%d x2=%d wRgn=%d\n", x, x2, wRgn);

            cr->save();
            cr->set_line_width(1);
            cr->rectangle(x, 1, wRgn, h1 - 1);
            cr->clip();
            if (bShowSampleRefSymbol) {
                const int wPic = 8;
                const int hPic = 8;
                Gdk::Cairo::set_source_pixbuf(
                    cr, (features.sampleRefs) ? yellowDot : redDot,
                    x + (wRgn-wPic)/2.f,
                    (bShowLoopSymbol) ? margin : (h1-hPic)/2.f
                );
                cr->paint();
            }
            if (bShowLoopSymbol) {
                const int wPic = 12;
                const int hPic = 14;
                Gdk::Cairo::set_source_pixbuf(
                    cr, (features.loops == features.validDimRegs) ? blackLoop : grayLoop,
                    x + (wRgn-wPic)/2.f,
                    (bShowSampleRefSymbol) ? h1 - hPic - margin : (h1-hPic)/2.f
                );
                cr->paint();
            }
            cr->restore();
        }
    }

    // draw vertical separation lines between regions
    for (gig::Region* r = regions.first() ; r ; r = regions.next()) {
        int x = key_to_x(r->KeyRange.low, w);

        if (x < clip_low) continue;
        if (x >= clip_high) break;

        cr->move_to(x + 0.5, 1);
        cr->line_to(x + 0.5, h1 - 1);
        cr->stroke();
    }

    // if there is no region yet, show the user some hint text that he may
    // right click on this area to create a new region
    if (!regions.first()) {
        Glib::RefPtr<Pango::Context> context = get_pango_context();
        Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(context);
        layout->set_alignment(Pango::ALIGN_CENTER);
        layout->set_text(Glib::ustring("*** ") + _("Right click here to create a region.") + " ***");
        layout->set_width(get_width() * Pango::SCALE);
        //layout->set_height(get_height() * Pango::SCALE);
        layout->set_spacing(10);
        Gdk::Cairo::set_source_rgba(cr, blue);
        // get the text dimensions
        int text_width, text_height;
        layout->get_pixel_size(text_width, text_height);
        cr->move_to(0, (REGION_BLOCK_HEIGHT - text_height) / 2);
#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 16) || GTKMM_MAJOR_VERSION < 2
        pango_cairo_show_layout(cr->cobj(), layout->gobj());
#else
        layout->show_in_cairo_context(cr);
#endif
    }
}

bool RegionChooser::is_black_key(int key) {
    const int note = (key + 3) % 12;
    return note == 1 || note == 4 || note == 6 || note == 9 || note == 11;
}

void RegionChooser::draw_digit(const Cairo::RefPtr<Cairo::Context>& cr,
                               int key) {
    const int h = KEYBOARD_HEIGHT;
    const int w = get_width() - 1;
    Glib::RefPtr<Pango::Layout> layout =
        Pango::Layout::create(get_pango_context());
    char buf[30];
    snprintf(buf, sizeof(buf), "<span size=\"8000\">%d</span>", key / 12 - 1);
    layout->set_markup(buf);
    Pango::Rectangle rectangle = layout->get_logical_extents();
    double text_w = double(rectangle.get_width()) / Pango::SCALE;
    double text_h = double(rectangle.get_height()) / Pango::SCALE;
    double x = w * (key + 0.75) / 128.0;
    Gdk::Cairo::set_source_rgba(cr, black);
    cr->move_to(int(x - text_w / 2 + 1), int(h1 + h - text_h + 0.5));
#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 16) || GTKMM_MAJOR_VERSION < 2
    pango_cairo_show_layout(cr->cobj(), layout->gobj());
#else
    layout->show_in_cairo_context(cr);
#endif
}

void RegionChooser::draw_key(const Cairo::RefPtr<Cairo::Context>& cr,
                             int key) {
    const int h = KEYBOARD_HEIGHT;
    const int w = get_width() - 1;
    const int bh = int(h * 0.55);

    Gdk::Cairo::set_source_rgba(cr, activeKeyColor);

    int note = (key + 3) % 12;
    int x = key_to_x(key, w) + 1;
    int x2 = key_to_x(key + 1.5, w);
    int x3 = key_to_x(key + 1, w);
    int x4 = key_to_x(key - 0.5, w);
    int w1 = x3 - x;
    switch (note) {
    case 0: case 5: case 10:
        cr->rectangle(x, h1 + 1, w1, bh);
        cr->fill();
        cr->rectangle(x4 + 1, h1 + bh + 1, x2 - x4 - 1, h - bh - 2);
        cr->fill();
        break;
    case 2: case 7:
        cr->rectangle(x, h1 + 1, w1, bh);
        cr->fill();
        cr->rectangle(x4 + 1, h1 + bh + 1, x3 - x4 - 1, h - bh - 2);
        cr->fill();
        break;
    case 3: case 8:
        cr->rectangle(x, h1 + 1, w1, bh);
        cr->fill();
        cr->rectangle(x, h1 + bh + 1, x2 - x, h - bh - 2);
        cr->fill();
        break;
    default:
        cr->rectangle(x, h1 + 1, w1, bh - 1);
        cr->fill();
        break;
    }
    Gdk::Cairo::set_source_rgba(cr, black);
}

/** @brief Preparations for drawAltValuesFor().
 *
 * Auto-generates a set of colors for all alternative values going to be
 * rendered by the subsequent drawAltValuesFor() calls. The colors are firmly
 * assigned to specific alternative values, such that for each alternative
 * value the same color would be used in each region.
 */
void RegionChooser::initDrawAltValues() {
    if (!fnAltValuesOf || dimregs.empty()) return;

    altValueColors.clear();

    // collect all (valid & used) selected DimensionRegions (of all regions)
    std::vector<gig::DimensionRegion*> dimregs;
    for (gig::DimensionRegion* dr : this->dimregs) {
        if (!dr) continue;
        DimensionCase c = dimensionCaseOf(dr);
        gig::Region* rgn = dr->GetParent();
        if (!isUsedCase(c, rgn)) continue;
        dimregs.push_back(dr);
    }

    // request the alternative parameter values for each dimension region
    std::set<std::string> altValues = fnAltValuesOf(dimregs);
    const size_t n = altValues.size();
    if (!n) return;

    // auto generate all colors that we need for rendering all alternative values
    std::vector<Gdk::RGBA> colors;
    colors.resize(n);
    for (int i = 0; i < n; ++i) {
        double hue = 360.0 * i / n;
        // shift hue by 120° so that we rather use green than red if there is
        // only one value to be drawn
        hue = fmod(hue + 120.0, 360.0);
        colors[i].set_hsv(hue, 1.0, 0.93);
    }

    // assign each alternative value a fixed color
    size_t i = 0;
    for (const std::string& altValue : altValues) {
        altValueColors[altValue] = colors[i];
        ++i;
    }
}

/**
 * If user hovers mouse over a parameter control: print a rectangle (or
 * vertical stripes) in a different color for each parameter value. Values of
 * not selected dimension regions are intentionally drawn in (transparent) black
 * color.
 *
 * @b IMPORTANT: In each render cycle initDrawAltValues() must be called once
 * before calling this method.
 *
 * @returns true if values were drawn, false if no values drawn
 */
bool RegionChooser::drawAltValuesFor(gig::Region* rgn,
                                     const Cairo::RefPtr<Cairo::Context>& cr,
                                     int x, int y, int w, int h)
{
    if (!fnAltValuesOf || dimregs.empty()) return false;

    if (!modifyallregions && this->region != rgn && !is_expanded_selected(rgn))
        return false;

    // collect all (valid & used) DimensionRegions of passed region
    std::vector<gig::DimensionRegion*> dimregs;
    for (int idr = 0; idr < 256; ++idr) {
        gig::DimensionRegion* dr = rgn->pDimensionRegions[idr];
        if (!dr) continue;
        DimensionCase c = dimensionCaseOf(dr);
        gig::Region* r = dr->GetParent();
        if (!isUsedCase(c, r)) continue;
        dimregs.push_back(dr);
    }

    if (dimregs.empty()) return false;

    std::set<std::string> altValues = fnAltValuesOf(dimregs);

    const size_t n = altValues.size();
    if (!n) return false;

    // start of clipping path
    cr->save();

    // create clipping path (rectangle with rounded corners)
    geDrawBubble(cr, x, y + float(h)/4, w, float(h)/2, 6);
    cr->clip();

    // draw individual stripes (constrained to clipping path)
    int i = 0;
    for (const std::string& val : altValues) {
        Gdk::RGBA& color = altValueColors[val];
        color.set_alpha(0.7);
        Gdk::Cairo::set_source_rgba(cr, color);
        cr->rectangle(x + float(i)/n * w,
                      y + float(h)/4,
                      float(w)/n,
                      float(h)/2);
        cr->fill();
        ++i;
    }

    // end of clipping path
    cr->restore();

    return true;
}

void RegionChooser::set_instrument(gig::Instrument* instrument)
{
    this->instrument = instrument;
    regions.update(instrument);
    initial_multiselect_region = region = regions.first();
    queue_draw();
    region_selected();
    dimensionManager.set_region(region);
}

// Cmd key on Mac, Ctrl key on all other OSs
static const guint primaryKeyL =
    #if defined(__APPLE__)
    GDK_KEY_Meta_L;
    #else
    GDK_KEY_Control_L;
    #endif

static const guint primaryKeyR =
    #if defined(__APPLE__)
    GDK_KEY_Meta_R;
    #else
    GDK_KEY_Control_R;
    #endif

#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
bool RegionChooser::onKeyPressed(Gdk::EventKey& _key) {
    GdkEventKey* key = _key.gobj();
#else
bool RegionChooser::onKeyPressed(GdkEventKey* key) {
#endif
    //printf("key down 0x%x\n", key->keyval);
    if (key->keyval == GDK_KEY_Control_L || key->keyval == GDK_KEY_Control_R)
        multiSelectKeyDown = true;
    if (key->keyval == primaryKeyL || key->keyval == primaryKeyR)
        primaryKeyDown = true;
    if (key->keyval == GDK_KEY_Shift_L || key->keyval == GDK_KEY_Shift_R)
        shiftKeyDown = true;

    return false;
}

#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
bool RegionChooser::onKeyReleased(Gdk::EventKey& _key) {
    GdkEventKey* key = _key.gobj();
#else
bool RegionChooser::onKeyReleased(GdkEventKey* key) {
#endif
    //printf("key up 0x%x\n", key->keyval);
    if (key->keyval == GDK_KEY_Control_L || key->keyval == GDK_KEY_Control_R)
        multiSelectKeyDown = false;
    if (key->keyval == primaryKeyL || key->keyval == primaryKeyR)
        primaryKeyDown = false;
    if (key->keyval == GDK_KEY_Shift_L || key->keyval == GDK_KEY_Shift_R)
        shiftKeyDown = false;

    return false;
}

bool RegionChooser::on_button_release_event(GdkEventButton* event)
{
    const int k = x_to_key(event->x, get_width() - 1);

    // handle-note off on virtual keyboard
    if (event->type == GDK_BUTTON_RELEASE) {
        int velocity = (event->y >= REGION_BLOCK_HEIGHT + KEYBOARD_HEIGHT - 1) ? 127 :
                       int(float(event->y - REGION_BLOCK_HEIGHT) / float(KEYBOARD_HEIGHT) * 128.0f) + 1;
        if (velocity <= 0) velocity = 1;
        switch (m_VirtKeybModeChoice.get_value()) {
            case VIRT_KEYBOARD_MODE_CHORD:
                if (event->y >= REGION_BLOCK_HEIGHT)
                    keyboard_key_released_signal.emit(k, velocity);
                break;
            case VIRT_KEYBOARD_MODE_NORMAL:
            default:
                if (currentActiveKey >= 0 && currentActiveKey <= 127) {
                    keyboard_key_released_signal.emit(currentActiveKey, velocity);
                    currentActiveKey = -1;
                }
                break;
        }
    }

    if (resize.active) {
#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
        get_window()->pointer_ungrab(event->time);
#else
# if GTKMM_MAJOR_VERSION < 3 || (GTKMM_MAJOR_VERSION == 3 && GTKMM_MINOR_VERSION < 20)
        Glib::wrap(event->device, true)->ungrab(event->time);
# else
        Glib::wrap(event->device, true)->get_seat()->ungrab();
# endif
#endif
        resize.active = false;

        if (!is_in_resize_zone(event->x, event->y) && cursor_is_resize) {
            get_window()->set_cursor();
            cursor_is_resize = false;
        }
    } else if (move.active) {
#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
        get_window()->pointer_ungrab(event->time);
#else
# if GTKMM_MAJOR_VERSION < 3 || (GTKMM_MAJOR_VERSION == 3 && GTKMM_MINOR_VERSION < 20)
        Glib::wrap(event->device, true)->ungrab(event->time);
# else
        Glib::wrap(event->device, true)->get_seat()->ungrab();
# endif
#endif
        move.active = false;

        if (is_in_resize_zone(event->x, event->y)) {
#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
            get_window()->set_cursor(Gdk::Cursor(Gdk::SB_H_DOUBLE_ARROW));
#else
            get_window()->set_cursor(
# if GTKMM_MAJOR_VERSION < 3 || (GTKMM_MAJOR_VERSION == 3 && GTKMM_MINOR_VERSION < 20)
                Gdk::Cursor::create(Gdk::SB_H_DOUBLE_ARROW)
# else
                Gdk::Cursor::create(
                    Glib::wrap(event->device, true)->get_seat()->get_display(),
                    Gdk::SB_H_DOUBLE_ARROW
                )
# endif
            );
#endif
            cursor_is_resize = true;
        }
    }
    return true;
}

void RegionChooser::update_after_resize()
{
    if (resize.mode == resize.moving_high_limit) {
        if (resize.region->KeyRange.high != resize.pos - 1) {
            instrument_struct_to_be_changed_signal.emit(instrument);
            resize.region->SetKeyRange(resize.region->KeyRange.low,
                                       resize.pos - 1);
            regions.update(instrument);
            instrument_changed.emit();
            instrument_struct_changed_signal.emit(instrument);
        }
    } else if (resize.mode == resize.moving_low_limit) {
        if (resize.region->KeyRange.low != resize.pos) {
            instrument_struct_to_be_changed_signal.emit(instrument);
            resize.region->SetKeyRange(resize.pos,
                                       resize.region->KeyRange.high);
            regions.update(instrument);
            instrument_changed.emit();
            instrument_struct_changed_signal.emit(instrument);
        }
    }
}

void RegionChooser::update_after_move(int pos)
{
    instrument_struct_to_be_changed_signal.emit(instrument);
    const int range = region->KeyRange.high - region->KeyRange.low;
    const int diff  = pos - int(region->KeyRange.low);
    region->SetKeyRange(pos, pos + range);
    if (Settings::singleton()->moveRootNoteWithRegionMoved) {
        for (int i = 0; i < 256; ++i) {
            gig::DimensionRegion* dimrgn = region->pDimensionRegions[i];
            if (!dimrgn || !dimrgn->pSample || !dimrgn->PitchTrack) continue;
            dimrgn->UnityNote += diff;
        }
    }
    regions.update(instrument);
    instrument_changed.emit();
    instrument_struct_changed_signal.emit(instrument);
}

void RegionChooser::on_dimregs_selection_changed(const std::set<gig::DimensionRegion*>& dimregs)
{
    this->dimregs = dimregs;
    // redraw regions with (or without) alt-value colors
    queue_draw();
}

bool RegionChooser::on_button_press_event(GdkEventButton* event)
{
    if (!instrument) return true;

    const int w = get_width() - 1;
    const int k = x_to_key(event->x, w);

    if (event->type == GDK_BUTTON_PRESS) {
        if (event->y >= REGION_BLOCK_HEIGHT) {
            int velocity = (event->y >= REGION_BLOCK_HEIGHT + KEYBOARD_HEIGHT - 1) ? 127 :
                           int(float(event->y - REGION_BLOCK_HEIGHT) / float(KEYBOARD_HEIGHT) * 128.0f) + 1;
            currentActiveKey = k;
            keyboard_key_hit_signal.emit(k, velocity);
        }
    }

    // left mouse button double click
    // (skip if potential multi-select actions are ongoing)
    if (event->type == GDK_2BUTTON_PRESS && event->button == 1 &&
        !multiSelectKeyDown && !shiftKeyDown && !primaryKeyDown)
    {
        if (event->y < REGION_BLOCK_HEIGHT) {
            // show dimension manager dialog for this region
            manage_dimensions();
        }
    }

    if (event->y >= REGION_BLOCK_HEIGHT) return true;
    if (event->type == GDK_BUTTON_PRESS && event->button == 3) {
        gig::Region* r = get_region(k);
        if (r) {
            set_region(r);
            popup_menu_inside_region->popup(event->button, event->time);
        } else {
            new_region_pos = k;
            popup_menu_outside_region->popup(event->button, event->time);
        }
    } else {
        if (is_in_resize_zone(event->x, event->y)) {
#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
            get_window()->pointer_grab(false,
                                       Gdk::BUTTON_RELEASE_MASK |
                                       Gdk::POINTER_MOTION_MASK |
                                       Gdk::POINTER_MOTION_HINT_MASK,
                                       Gdk::Cursor(Gdk::SB_H_DOUBLE_ARROW),
                                       event->time);
#else
# if GTKMM_MAJOR_VERSION < 3 || (GTKMM_MAJOR_VERSION == 3 && GTKMM_MINOR_VERSION < 20)
            Glib::wrap(event->device, true)->grab(get_window(),
                                                  Gdk::OWNERSHIP_NONE,
                                                  false,
                                                  Gdk::BUTTON_RELEASE_MASK |
                                                  Gdk::POINTER_MOTION_MASK |
                                                  Gdk::POINTER_MOTION_HINT_MASK,
                                                  Gdk::Cursor::create(Gdk::SB_H_DOUBLE_ARROW),
                                                  event->time);
# else
            Glib::wrap(event->device, true)->get_seat()->grab(
                get_window(),
                Gdk::SeatCapabilities::SEAT_CAPABILITY_ALL_POINTING,
                false,
                Gdk::Cursor::create(
                    Glib::wrap(event->device, true)->get_seat()->get_display(),
                    Gdk::SB_H_DOUBLE_ARROW
                ),
                reinterpret_cast<GdkEvent*>(event)
            );
# endif
#endif
            resize.active = true;
        } else {
            gig::Region* r = get_region(k);
            if (r) {
                // Workaround to prevent key-up events not being delivered if
                // Gtk entry field currently got focus (due to value change).
                // A simple grab_focus() doesn't work unfortunately.
                fix_key_entry_focus_signal();

                if (multiSelectKeyDown)
                    toggle_expanded_selection(r);
                else if (shiftKeyDown)
                    expand_continuous_selection_to(r);
                else
                    set_region(r);

#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
                get_window()->pointer_grab(false,
                                           Gdk::BUTTON_RELEASE_MASK |
                                           Gdk::POINTER_MOTION_MASK |
                                           Gdk::POINTER_MOTION_HINT_MASK,
                                           Gdk::Cursor(Gdk::FLEUR),
                                           event->time);
#else
# if GTKMM_MAJOR_VERSION < 3 || (GTKMM_MAJOR_VERSION == 3 && GTKMM_MINOR_VERSION < 20)
            Glib::wrap(event->device, true)->grab(get_window(),
                                                  Gdk::OWNERSHIP_NONE,
                                                  false,
                                                  Gdk::BUTTON_RELEASE_MASK |
                                                  Gdk::POINTER_MOTION_MASK |
                                                  Gdk::POINTER_MOTION_HINT_MASK,
                                                  Gdk::Cursor::create(Gdk::FLEUR),
                                                  event->time);
# else
            Glib::wrap(event->device, true)->get_seat()->grab(
                get_window(),
                Gdk::SeatCapabilities::SEAT_CAPABILITY_ALL_POINTING,
                false,
                Gdk::Cursor::create(
                    Glib::wrap(event->device, true)->get_seat()->get_display(),
                    Gdk::FLEUR
                ),
                reinterpret_cast<GdkEvent*>(event)
            );
# endif
#endif
                move.active = true;
                move.offset = event->x - key_to_x(region->KeyRange.low, w);
            }
        }
    }
    return true;
}

gig::Region* RegionChooser::get_region(int key)
{
    for (gig::Region* r = regions.first() ; r ; r = regions.next()) {
        if (key < r->KeyRange.low) return 0;
        if (key <= r->KeyRange.high) return r;
    }
    return 0;
}

// Make region the primary selection to be edited.
// If expandSelection is true then additionally modify the "expanded selection"
// to CONSECUTIVELY span from the initially selected region up to that new
// primary selection, i.e. those additional regions would be target for editor's
// modifications as well.
void RegionChooser::set_region(gig::Region* region, bool expandSelection) {
    if (expandSelection) {
        expand_continuous_selection_to(region);
        return;
    }
    if (!expandedSelectedRegions.empty()) {
        expandedSelectedRegions.clear();
        expanded_region_selection_changed();
    }
    initial_multiselect_region = region;
    set_region_internal(region);
}

// Protected method, used to avoid code duplication in this unit: handle primary
// region selection change.
void RegionChooser::set_region_internal(gig::Region* region) {
    this->region = region;
    queue_draw();
    region_selected();
    dimensionManager.set_region(region);
}

// If the passed region is not yet member of the "expanded selection" of
// regions, then it is added to the "expanded selection". If it is already
// member of the "expanded selection" though, then it is removed instead.
void RegionChooser::toggle_expanded_selection(gig::Region* r) {
    if (!r || r == this->region) return;

    const auto it = expandedSelectedRegions.find(r);
    if (it != expandedSelectedRegions.end())
        expandedSelectedRegions.erase(it);
    else
        expandedSelectedRegions.insert(r);

    expanded_region_selection_changed();
    queue_draw();
}

// Increase or decrease the CONSECUTIVE span of "expanded selection" of regions
// from the initially selected region up to the passed region.
// Additionally make the passed region the new primary selection.
void RegionChooser::expand_continuous_selection_to(gig::Region* r) {
    if (!r || r == initial_multiselect_region) {
        expandedSelectedRegions.clear();
        expanded_region_selection_changed();
        goto finish;
    }

    // find 1st region for the new expanded selection and add it as very 1st
    // element to the set of the new expanded selection
    size_t i;
    for (i = 0; gig::Region* rgn = instrument->GetRegionAt(i); ++i) {
        if (rgn == initial_multiselect_region || rgn == r) {
            expandedSelectedRegions.clear();
            expandedSelectedRegions.insert(rgn);
            goto start;
        }
    }
    goto finish;

    // collect all other regions up to end and add them to the set of of new
    // expanded selection as well
    start:
    for (++i; gig::Region* rgn = instrument->GetRegionAt(i); ++i) {
        expandedSelectedRegions.insert(rgn);
        if (rgn == initial_multiselect_region || rgn == r) break;
    }

    expanded_region_selection_changed();

    // set the new primary selection
    finish:
    set_region_internal(r);
}

// Make the next (right hand side) region the primary selection for editing.
// If expandSelection is true then additionally modify the "expanded selection"
// to CONSECUTIVELY span from the initially selected region up to that new
// primary selection, i.e. those additional regions would be target for editor's
// modifications as well.
void RegionChooser::select_next_region(bool expandSelection) {
    if (!instrument) return;
    if (!region) {
        for (int i = 0; i < 128; ++i) {
            ::gig::Region* rgn = instrument->GetRegion(i);
            if (rgn) {
                set_region(rgn);
                return;
            }
        }
    } else {
        bool currentFound = false;
        for (int i = 0; i < 128; ++i) {
            ::gig::Region* rgn = instrument->GetRegion(i);
            if (!rgn) continue;
            if (currentFound) {
                if (rgn != region) {
                    set_region(rgn, expandSelection);
                    return;
                }
            } else {
                if (rgn == region) currentFound = true;
            }
        }
    }
}


// Make the previous (left hand side) region the primary selection for editing.
// If expandSelection is true then additionally modify the "expanded selection"
// to CONSECUTIVELY span from the initially selected region up to that new
// primary selection, i.e. those additional regions would be target for editor's
// modifications as well.
void RegionChooser::select_prev_region(bool expandSelection) {
    if (!instrument) return;
    if (!region) {
        for (int i = 0; i < 128; ++i) {
            ::gig::Region* rgn = instrument->GetRegion(i);
            if (rgn) {
                set_region(rgn);
                return;
            }
        }
    } else {
        bool currentFound = false;
        for (int i = 127; i >= 0; --i) {
            ::gig::Region* rgn = instrument->GetRegion(i);
            if (!rgn) continue;
            if (currentFound) {
                if (rgn != region) {
                    set_region(rgn, expandSelection);
                    return;
                }
            } else {
                if (rgn == region) currentFound = true;
            }
        }
    }
}

void RegionChooser::motion_resize_region(int x, int y)
{
    const int w = get_width() - 1;

    int k = int(double(x) / w * 128.0 + 0.5);

    if (k < resize.min) k = resize.min;
    else if (k > resize.max) k = resize.max;

    if (k != resize.pos) {
        if (resize.mode == resize.undecided) {
            if (k < resize.pos) {
                // edit high limit of prev_region
                resize.max = resize.region->KeyRange.low;
                resize.region = resize.prev_region;
                resize.mode = resize.moving_high_limit;
            } else {
                // edit low limit of region
                resize.min = resize.prev_region->KeyRange.high + 1;
                resize.mode = resize.moving_low_limit;
            }
        }
        resize.pos = k;

        int x1, x2;
        if (resize.mode == resize.moving_high_limit) {
            if (resize.region->KeyRange.high < resize.pos - 1) {
                x1 = resize.region->KeyRange.high;
                x2 = resize.pos - 1;
            } else {
                x1 = resize.pos - 1;
                x2 = resize.region->KeyRange.high;
            }
        } else {
            if (resize.region->KeyRange.low < resize.pos) {
                x1 = resize.region->KeyRange.low;
                x2 = resize.pos;
            } else {
                x1 = resize.pos;
                x2 = resize.region->KeyRange.low;
            }
        }
        x1 = key_to_x(x1, w);
        x2 = key_to_x(x2 + 1, w) + 1;
        Gdk::Rectangle rect(x1, 0, x2 - x1, h1);

        update_after_resize();

        //get_window()->invalidate_rect(rect, false);
        get_window()->invalidate(false); // repaint entire region, otherwise it would create visual artifacts
    }
}

void RegionChooser::motion_move_region(int x, int y)
{
    const int w = get_width() - 1;

    int l = int(double(x - move.offset) / w * 128.0 + 0.5);

    if (l == region->KeyRange.low) return;
    int new_l = region->KeyRange.low;
    int regionsize = region->KeyRange.high - region->KeyRange.low;
    int a = 0;
    if (l > region->KeyRange.low) {
        for (gig::Region* r = regions.first() ; ; r = regions.next()) {
            if (r != region) {
                int b = r ? r->KeyRange.low : 128;

                // gap: from a to b (not inclusive b)

                if (region->KeyRange.high >= b) {
                    // not found the current gap yet, just continue
                } else {

                    if (a > l) {
                        // this gap is too far to the right, break
                        break;
                    }

                    int newhigh = std::min(l + regionsize, b - 1);
                    int newlo = newhigh - regionsize;

                    if (newlo >= a) {
                        // yes it fits - it's a candidate
                        new_l = newlo;
                    }
                }
                if (!r) break;
                a = r->KeyRange.high + 1;
            }
        }
    } else {
        for (gig::Region* r = regions.first() ; ; r = regions.next()) {
            if (r != region) {
                int b = r ? r->KeyRange.low : 128;

                // gap from a to b (not inclusive b)

                if (l + regionsize >= b) {
                    // not found the current gap yet, just continue
                } else {

                    if (a > region->KeyRange.low) {
                        // this gap is too far to the right, break
                        break;
                    }

                    int newlo = std::max(l, a);
                    int newhigh = newlo + regionsize;

                    if (newhigh < b) {
                        // yes it fits - break as the first one is the best
                        new_l = newlo;
                        break;
                    }
                }
                if (!r) break;
                a = r->KeyRange.high + 1;
            }
        }
    }
    if (new_l == region->KeyRange.low) return;

    int x1 = key_to_x(std::min(int(region->KeyRange.low), new_l), w);
    int x2 = key_to_x(std::max(int(region->KeyRange.high),
                               new_l + regionsize) + 1, w) + 1;

    Gdk::Rectangle rect(x1, 0, x2 - x1, h1);
    update_after_move(new_l);

    get_window()->invalidate_rect(rect, false);
}


bool RegionChooser::on_motion_notify_event(GdkEventMotion* event)
{
    Glib::RefPtr<Gdk::Window> window = get_window();
    int x, y;
#if HAS_GDKMM_SEAT
    x = event->x;
    y = event->y;
/*
    Gdk::ModifierType state = Gdk::ModifierType(event->state);
 */
#else
    Gdk::ModifierType state = Gdk::ModifierType(0);
    window->get_pointer(x, y, state);
#endif

    // handle virtual MIDI keyboard
    if (m_VirtKeybModeChoice.get_value() != VIRT_KEYBOARD_MODE_CHORD &&
        currentActiveKey > 0 &&
        event->y >= REGION_BLOCK_HEIGHT &&
        event->y < REGION_BLOCK_HEIGHT + KEYBOARD_HEIGHT)
    {
        const int k = x_to_key(event->x, get_width() - 1);
        if (k != currentActiveKey) {
            int velocity =
                (event->y >= REGION_BLOCK_HEIGHT + KEYBOARD_HEIGHT - 1) ? 127 :
                int(float(event->y - REGION_BLOCK_HEIGHT) /
                    float(KEYBOARD_HEIGHT) * 128.0f) + 1;
            if (velocity <= 0) velocity = 1;
            keyboard_key_released_signal.emit(currentActiveKey, velocity);
            currentActiveKey = k;
            keyboard_key_hit_signal.emit(k, velocity);
        }
    }

    if (resize.active) {
        motion_resize_region(x, y);
    } else if (move.active) {
        motion_move_region(x, y);
    } else {
        if (is_in_resize_zone(x, y)) {
            if (!cursor_is_resize) {
#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
                window->set_cursor(Gdk::Cursor(Gdk::SB_H_DOUBLE_ARROW));
#else
                window->set_cursor(
# if GTKMM_MAJOR_VERSION < 3 || (GTKMM_MAJOR_VERSION == 3 && GTKMM_MINOR_VERSION < 20)
                    Gdk::Cursor::create(Gdk::SB_H_DOUBLE_ARROW)
# else
                    Gdk::Cursor::create(
                        Glib::wrap(event->device, true)->get_seat()->get_display(),
                        Gdk::SB_H_DOUBLE_ARROW
                    )
# endif
                );
#endif
                cursor_is_resize = true;
            }
        } else if (cursor_is_resize) {
            window->set_cursor();
            cursor_is_resize = false;
        }
    }

    return true;
}

bool RegionChooser::is_in_resize_zone(double x, double y) {
    const int w = get_width() - 1;

    if (instrument && y >= 0 && y <= h1) {
        gig::Region* prev_region = 0;
        gig::Region* next_region;
        for (gig::Region* r = regions.first(); r ; r = next_region) {
            next_region = regions.next();

            int lo = key_to_x(r->KeyRange.low, w);
            if (x <= lo - 2) break;
            if (x < lo + 2) {
                resize.region = r;
                resize.pos = r->KeyRange.low;
                resize.max = r->KeyRange.high;

                if (prev_region && prev_region->KeyRange.high + 1 == r->KeyRange.low) {
                    // we don't know yet if it's the high limit of
                    // prev_region or the low limit of r that's going
                    // to be edited
                    resize.mode = resize.undecided;
                    resize.min = prev_region->KeyRange.low + 1;
                    resize.prev_region = prev_region;
                    return resize.min != resize.max;
                }

                // edit low limit
                resize.mode = resize.moving_low_limit;
                resize.min = prev_region ? prev_region->KeyRange.high + 1 : 0;
                return resize.min != resize.max;
            }
            if (!next_region || r->KeyRange.high + 1 != next_region->KeyRange.low) {
                int hi = key_to_x(r->KeyRange.high + 1, w);
                if (x <= hi - 2) break;
                if (x < hi + 2) {
                    // edit high limit
                    resize.region = r;
                    resize.pos = r->KeyRange.high + 1;
                    resize.mode = resize.moving_high_limit;
                    resize.min = r->KeyRange.low + 1;
                    resize.max = next_region ? next_region->KeyRange.low : 128;
                    return resize.min != resize.max;
                }
            }
            prev_region = r;
        }
    }
    return false;
}

sigc::signal<void>& RegionChooser::signal_region_selected()
{
    return region_selected;
}

sigc::signal<void>& RegionChooser::signal_instrument_changed()
{
    return instrument_changed;
}

void RegionChooser::show_region_properties()
{
    if (!region) return;
    Gtk::Dialog dialog(_("Region Properties"), true /*modal*/);
    // add "Keygroup" checkbox
    Gtk::CheckButton checkBoxKeygroup(_("Member of a Keygroup (Exclusive Group)"));
    checkBoxKeygroup.set_active(region->KeyGroup);
#if USE_GTKMM_BOX
    dialog.get_content_area()->pack_start(checkBoxKeygroup);
#else
    dialog.get_vbox()->pack_start(checkBoxKeygroup);
#endif
    checkBoxKeygroup.show();
    // add "Keygroup" spinbox
#if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
    Gtk::Adjustment adjustment(1, 1, 999);
    Gtk::SpinButton spinBox(adjustment);
#else
    Gtk::SpinButton spinBox(Gtk::Adjustment::create(1, 1, 999));
#endif
    if (region->KeyGroup) spinBox.set_value(region->KeyGroup);
#if USE_GTKMM_BOX
    dialog.get_content_area()->pack_start(spinBox);
#else
    dialog.get_vbox()->pack_start(spinBox);
#endif
    spinBox.show();
    // add OK and CANCEL buttons to the dialog
#if HAS_GTKMM_STOCK
    dialog.add_button(Gtk::Stock::OK, 0);
    dialog.add_button(Gtk::Stock::CANCEL, 1);
#else
    dialog.add_button(_("_OK"), 0);
    dialog.add_button(_("_Cancel"), 1);
#endif
    dialog.set_position(Gtk::WIN_POS_MOUSE);
#if HAS_GTKMM_SHOW_ALL_CHILDREN
    dialog.show_all_children();
#endif
    if (!dialog.run()) { // OK selected ...
        region->KeyGroup =
            (checkBoxKeygroup.get_active()) ? spinBox.get_value_as_int() : 0;
    }
}

void RegionChooser::add_region()
{
    instrument_struct_to_be_changed_signal.emit(instrument);

    region = instrument->AddRegion();
    region->SetKeyRange(new_region_pos, new_region_pos);

    instrument_struct_changed_signal.emit(instrument);
    regions.update(instrument);

    queue_draw();
    region_selected();
    dimensionManager.set_region(region);
    instrument_changed();
}

void RegionChooser::delete_region()
{
    instrument_struct_to_be_changed_signal.emit(instrument);
    instrument->DeleteRegion(region);
    instrument_struct_changed_signal.emit(instrument);
    regions.update(instrument);

    region = 0;
    queue_draw();
    region_selected();
    dimensionManager.set_region(region);
    instrument_changed();
}

void RegionChooser::manage_dimensions()
{
    gig::Region* region = get_region();
    if (!region) return;
    dimensionManager.show(region);
}

void RegionChooser::on_dimension_manager_changed() {
    region_selected();
    instrument_changed();
}

sigc::signal<void>& RegionChooser::signal_fix_key_entry_focus() {
    return fix_key_entry_focus_signal;
}

sigc::signal<void, gig::Instrument*>& RegionChooser::signal_instrument_struct_to_be_changed() {
    return instrument_struct_to_be_changed_signal;
}

sigc::signal<void, gig::Instrument*>& RegionChooser::signal_instrument_struct_changed() {
    return instrument_struct_changed_signal;
}

sigc::signal<void, gig::Region*>& RegionChooser::signal_region_to_be_changed() {
    return region_to_be_changed_signal;
}

sigc::signal<void, gig::Region*>& RegionChooser::signal_region_changed_signal() {
    return region_changed_signal;
}

sigc::signal<void, int/*key*/, int/*velocity*/>& RegionChooser::signal_keyboard_key_hit() {
    return keyboard_key_hit_signal;
}

sigc::signal<void, int/*key*/, int/*velocity*/>& RegionChooser::signal_keyboard_key_released() {
    return keyboard_key_released_signal;
}

sigc::signal<void>& RegionChooser::signal_expanded_region_selection_changed() {
    return expanded_region_selection_changed;
}
