diff options
author | David Robillard <d@drobilla.net> | 2008-01-13 17:45:17 +0000 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2008-01-13 17:45:17 +0000 |
commit | 2db91c126edc8fcaeda711d35369970576715719 (patch) | |
tree | 5a3a16c77acb7909334c6eb97fd0578c96f276ec | |
parent | f8e3d652e946a169db0c15e3d1a0d9ed15464e0c (diff) |
Apply MIDI keyboard and "scroomer" patch.
git-svn-id: svn://localhost/ardour2/trunk@2908 d708f5d6-7413-0410-9779-e7cbd77b26cf
-rw-r--r-- | gtk2_ardour/SConscript | 3 | ||||
-rw-r--r-- | gtk2_ardour/ardour2_ui_default.conf | 6 | ||||
-rw-r--r-- | gtk2_ardour/canvas.h | 1 | ||||
-rw-r--r-- | gtk2_ardour/canvas_vars.h | 4 | ||||
-rw-r--r-- | gtk2_ardour/editor.cc | 1 | ||||
-rw-r--r-- | gtk2_ardour/lineset.cc | 782 | ||||
-rw-r--r-- | gtk2_ardour/lineset.h | 208 | ||||
-rw-r--r-- | gtk2_ardour/midi_region_view.cc | 14 | ||||
-rw-r--r-- | gtk2_ardour/midi_region_view.h | 2 | ||||
-rw-r--r-- | gtk2_ardour/midi_scroomer.cc | 164 | ||||
-rw-r--r-- | gtk2_ardour/midi_scroomer.h | 39 | ||||
-rw-r--r-- | gtk2_ardour/midi_streamview.cc | 124 | ||||
-rw-r--r-- | gtk2_ardour/midi_streamview.h | 15 | ||||
-rw-r--r-- | gtk2_ardour/midi_time_axis.cc | 11 | ||||
-rw-r--r-- | gtk2_ardour/midi_time_axis.h | 6 | ||||
-rw-r--r-- | gtk2_ardour/piano_roll_header.cc | 653 | ||||
-rw-r--r-- | gtk2_ardour/piano_roll_header.h | 104 | ||||
-rw-r--r-- | gtk2_ardour/time_axis_view.cc | 24 | ||||
-rw-r--r-- | gtk2_ardour/time_axis_view.h | 1 | ||||
-rw-r--r-- | libs/gtkmm2ext/SConscript | 1 | ||||
-rw-r--r-- | libs/gtkmm2ext/gtkmm2ext/scroomer.h | 88 | ||||
-rw-r--r-- | libs/gtkmm2ext/scroomer.cc | 393 |
22 files changed, 2596 insertions, 48 deletions
diff --git a/gtk2_ardour/SConscript b/gtk2_ardour/SConscript index 4d34a46176..f03286243b 100644 --- a/gtk2_ardour/SConscript +++ b/gtk2_ardour/SConscript @@ -123,12 +123,14 @@ automation_region_view.cc bundle_manager.cc midi_port_dialog.cc midi_time_axis.cc +midi_scroomer.cc midi_streamview.cc axis_view.cc canvas-simpleline.c simpleline.cc canvas-simplerect.c simplerect.cc +lineset.cc canvas-waveview.c diamond.cc canvas-midi-event.cc @@ -190,6 +192,7 @@ opts.cc panner.cc panner2d.cc panner_ui.cc +piano_roll_header.cc playlist_selector.cc plugin_selector.cc plugin_ui.cc diff --git a/gtk2_ardour/ardour2_ui_default.conf b/gtk2_ardour/ardour2_ui_default.conf index 80f53f7181..c45e4da9c6 100644 --- a/gtk2_ardour/ardour2_ui_default.conf +++ b/gtk2_ardour/ardour2_ui_default.conf @@ -7,7 +7,8 @@ <Option name="waveform" value="000000cc"/> <Option name="clipped waveform" value="ff0000e5"/> <Option name="region base" value="bfbfc1aa"/> - <Option name="selected region base" value="8888ffaa"/> + <Option name="selected region base" value="b591a8ff"/> + <Option name="midi frame base" value="698f9d6d"/> <Option name="audio track base" value="c6d3d868"/> <Option name="audio bus base" value="dbd1ea68"/> <Option name="midi track base" value="c67e7e5f"/> @@ -100,5 +101,8 @@ <Option name="midi note fill mid" value="eeee338a"/> <Option name="midi note fill max" value="ee33338a"/> <Option name="midi note selected outline" value="5566ffee"/> + <Option name="piano roll white" value="aa585865"/> + <Option name="piano roll black" value="88393b6b"/> + <Option name="piano roll black outline" value="b5b5b576"/> </Canvas> </Ardour> diff --git a/gtk2_ardour/canvas.h b/gtk2_ardour/canvas.h index 75cb7157a9..9fa737258c 100644 --- a/gtk2_ardour/canvas.h +++ b/gtk2_ardour/canvas.h @@ -33,6 +33,7 @@ namespace Gnome { class Line; class Points; class ImageFrame; + class Lineset; } } diff --git a/gtk2_ardour/canvas_vars.h b/gtk2_ardour/canvas_vars.h index 7d291b7a5d..bc17a1d988 100644 --- a/gtk2_ardour/canvas_vars.h +++ b/gtk2_ardour/canvas_vars.h @@ -2,6 +2,7 @@ CANVAS_VARIABLE(canvasvar_WaveForm, "waveform") CANVAS_VARIABLE(canvasvar_WaveFormClip, "clipped waveform") CANVAS_VARIABLE(canvasvar_FrameBase, "region base") CANVAS_VARIABLE(canvasvar_SelectedFrameBase, "selected region base") +CANVAS_VARIABLE(canvasvar_MidiFrameBase, "midi frame base") CANVAS_VARIABLE(canvasvar_AudioTrackBase, "audio track base") CANVAS_VARIABLE(canvasvar_AudioBusBase, "audio bus base") CANVAS_VARIABLE(canvasvar_MidiTrackBase, "midi track base") @@ -94,3 +95,6 @@ CANVAS_VARIABLE(canvasvar_MidiNoteFillMin, "midi note fill min") CANVAS_VARIABLE(canvasvar_MidiNoteFillMid, "midi note fill mid") CANVAS_VARIABLE(canvasvar_MidiNoteFillMax, "midi note fill max") CANVAS_VARIABLE(canvasvar_MidiNoteSelectedOutline, "midi note selected outline") +CANVAS_VARIABLE(canvasvar_PianoRollWhite, "piano roll white") +CANVAS_VARIABLE(canvasvar_PianoRollBlack, "piano roll black") +CANVAS_VARIABLE(canvasvar_PianoRollBlackOutline, "piano roll black outline") diff --git a/gtk2_ardour/editor.cc b/gtk2_ardour/editor.cc index 41261b7079..3ff52d69e7 100644 --- a/gtk2_ardour/editor.cc +++ b/gtk2_ardour/editor.cc @@ -82,6 +82,7 @@ #include "actions.h" #include "sfdb_ui.h" #include "gui_thread.h" +#include "simpleline.h" #ifdef FFT_ANALYSIS #include "analysis_window.h" diff --git a/gtk2_ardour/lineset.cc b/gtk2_ardour/lineset.cc new file mode 100644 index 0000000000..46c06705bb --- /dev/null +++ b/gtk2_ardour/lineset.cc @@ -0,0 +1,782 @@ +/* + Copyright (C) 2007 Paul Davis + 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 "lineset.h" +#include "rgb_macros.h" + +#include <libgnomecanvas/libgnomecanvas.h> +#include <libgnomecanvasmm/group.h> +#include <libgnomecanvasmm/canvas.h> + +#include <algorithm> +#include <cmath> +#include <iostream> + +using namespace std; + +namespace Gnome { +namespace Canvas { + +LinesetClass Lineset::lineset_class; + +static const char* overlap_error_str = "Lineset error: Line overlap"; + +Lineset::Line::Line(double c, double w, uint32_t color) + : coord(c) + , width(w) { + UINT_TO_RGBA (color, &r, &g, &b, &a); +} + +/* Constructor for dummy lines that are used only with the coordinate */ +Lineset::Line::Line(double c) + : coord(c) { +} + +void +Lineset::Line::set_color(uint32_t color) { + UINT_TO_RGBA (color, &r, &g, &b, &a); +} + +const Glib::Class& +LinesetClass::init() { + if(!gtype_) { + class_init_func_ = &LinesetClass::class_init_function; + register_derived_type(Item::get_type()); + } + + return *this; +} + +void +LinesetClass::class_init_function(void* g_class, void* class_data) { +} + +Lineset::Lineset(Group& parent, Orientation o) + : Glib::ObjectBase("GnomeCanvasLineset") + , Item(Glib::ConstructParams(lineset_class.init())) + , cached_pos(lines.end()) + , orientation(o) + , x1(*this, "x1", 0.0) + , y1(*this, "y1", 0.0) + , x2(*this, "x2", 0.0) + , y2(*this, "y2", 0.0) + , in_update(false) + , update_region1(1.0) + , update_region2(0.0) + , bounds_changed(false) + , covered1(1.0) // covered1 > covered2 ==> nothing's covered + , covered2(0.0) { + + item_construct(parent); + + property_x1().signal_changed().connect(mem_fun(*this, &Lineset::bounds_need_update)); + property_y1().signal_changed().connect(mem_fun(*this, &Lineset::bounds_need_update)); + property_x2().signal_changed().connect(mem_fun(*this, &Lineset::bounds_need_update)); + property_y2().signal_changed().connect(mem_fun(*this, &Lineset::bounds_need_update)); +} + +Lineset::~Lineset() { +} + +bool +Lineset::line_compare(const Line& a, const Line& b) { + return a.coord < b.coord; +} + +void +Lineset::print_lines() { + for(Lines::iterator it = lines.begin(); it != lines.end(); ++it) { + cerr << " " << it->coord << " " << it->width << " " << (int)it->r << " " << (int)it->g << " " << (int)it->b << " " << (int)it->a << endl; + } +} + +void +Lineset::move_line(double coord, double dest) { + if(coord == dest) { + return; + } + + Lines::iterator it = line_at(coord); + + if(it != lines.end()) { + + + double width = it->width; + it->coord = dest; + + Lines::iterator ins = lower_bound(lines.begin(), lines.end(), *it, line_compare); + + lines.insert(ins, *it); + lines.erase(it); + + if(coord > dest) { + region_needs_update(dest, coord + width); + } + else { + region_needs_update(coord, dest + width); + } + } +} + +void +Lineset::change_line_width(double coord, double width) { + Lines::iterator it = line_at(coord); + + if(it != lines.end()) { + Line& l = *it; + ++it; + + if(it != lines.end()) { + if(l.coord + width > it->coord) { + cerr << overlap_error_str << endl; + return; + } + } + + l.width = width; + region_needs_update(coord, coord + width); + } +} + +void +Lineset::change_line_color(double coord, uint32_t color) { + Lines::iterator it = line_at(coord); + + if(it != lines.end()) { + it->set_color(color); + region_needs_update(it->coord, it->coord + it->width); + } +} + +void +Lineset::add_line(double coord, double width, uint32_t color) { + Line l(coord, width, color); + + Lines::iterator it = std::lower_bound(lines.begin(), lines.end(), l, line_compare); + + /* overlap checking */ + if(it != lines.end()) { + if(l.coord + l.width > it->coord) { + cerr << overlap_error_str << endl; + return; + } + } + if(it != lines.begin()) { + --it; + if(l.coord < it->coord + it->width) { + cerr << overlap_error_str << endl; + return; + } + ++it; + } + + lines.insert(it, l); + region_needs_update(coord, coord + width); +} + +void +Lineset::remove_line(double coord) { + Lines::iterator it = line_at(coord); + + if(it != lines.end()) { + double start = it->coord; + double end = start + it->width; + + lines.erase(it); + + region_needs_update(start, end); + } +} + +void +Lineset::remove_lines(double c1, double c2) { + if(!lines.empty()) { + region_needs_update(c1, c2); + } +} + +void +Lineset::remove_until(double coord) { + if(!lines.empty()) { + double first = lines.front().coord; + + // code + + region_needs_update(first, coord); + } +} + +void +Lineset::remove_from(double coord) { + if(!lines.empty()) { + double last = lines.back().coord + lines.back().width; + + // code + + region_needs_update(coord, last); + } +} + +void +Lineset::clear() { + if(!lines.empty()) { + double coord1 = lines.front().coord; + double coord2 = lines.back().coord + lines.back().width; + + lines.clear(); + region_needs_update(coord1, coord2); + } +} + +/* + * this function is optimized to work faster if we access elements that are adjacent to each other. + * so if a large number of lines are modified, it is wise to modify them in sorted order. + */ +Lineset::Lines::iterator +Lineset::line_at(double coord) { + if(cached_pos != lines.end()) { + if(coord < cached_pos->coord) { + /* backward search */ + while(--cached_pos != lines.end()) { + if(cached_pos->coord <= coord) { + if(cached_pos->coord + cached_pos->width < coord) { + /* coord is between two lines */ + return lines.end(); + } + else { + return cached_pos; + } + } + } + } + else { + /* forward search */ + while(cached_pos != lines.end()) { + if(cached_pos->coord > coord) { + /* we searched past the line that we want, so now see + if the previous line includes the coordinate */ + --cached_pos; + if(cached_pos->coord + cached_pos->width >= coord) { + return cached_pos; + } + else { + return lines.end(); + } + } + ++cached_pos; + } + } + } + else { + /* initialize the cached position */ + Line dummy(coord); + + cached_pos = lower_bound(lines.begin(), lines.end(), dummy, line_compare); + + /* The iterator found should point to the element after the one we want. */ + --cached_pos; + + if(cached_pos != lines.end()) { + if(cached_pos->coord <= coord) { + if(cached_pos->coord + cached_pos->width >= coord) { + return cached_pos; + } + else { + return lines.end(); + } + } + else { + return lines.end(); + } + } + else { + return lines.end(); + } + } + + return lines.end(); +} + +void +Lineset::redraw_request(ArtIRect& r) { + get_canvas()->request_redraw(r.x0, r.y0, r.x1, r.y1); +} + +void +Lineset::redraw_request(ArtDRect& r) { + int x0, y0, x1, y1; + Canvas& cv = *get_canvas(); + + //cerr << "redraw request: " << r.x0 << " " << r.y0 << " " << r.x1 << " " << r.y1 << endl; + + cv.w2c(r.x0, r.y0, x0, y0); + cv.w2c(r.x1, r.y1, x1, y1); + cv.request_redraw(x0, y0, x1, y1); +} + +void +Lineset::update_lines(bool need_redraw) { + //cerr << "update_lines need_redraw=" << need_redraw << endl; + if(!need_redraw) { + update_region1 = 1.0; + update_region2 = 0.0; + return; + } + + if(update_region2 > update_region1) { + ArtDRect redraw; + Lineset::bounds_vfunc(&redraw.x0, &redraw.y0, &redraw.x1, &redraw.y1); + i2w(redraw.x0, redraw.y0); + i2w(redraw.x1, redraw.y1); + + if(orientation == Vertical) { + redraw.x1 = redraw.x0 + update_region2; + redraw.x0 += update_region1; + } + else { + redraw.y1 = redraw.y0 + update_region2; + redraw.y0 += update_region1; + } + redraw_request(redraw); + update_region1 = 1.0; + update_region2 = 0.0; + } + + // if we need to calculate what becomes visible, use some of this + //cv.c2w (0, 0, world_v[X1], world_v[Y1]); + //cv.c2w (cv.get_width(), cv.get_height(), world_v[X2], world_v[Y2]); +} + +/* + * return false if a full redraw request has been made. + * return true if nothing or only parts of the rect area has been requested for redraw + */ +bool +Lineset::update_bounds() { + GnomeCanvasItem* item = GNOME_CANVAS_ITEM(gobj()); + ArtDRect old_b; + ArtDRect new_b; + ArtDRect redraw; + Canvas& cv = *get_canvas(); + + /* store the old bounding box */ + old_b.x0 = item->x1; + old_b.y0 = item->y1; + old_b.x1 = item->x2; + old_b.y1 = item->y2; + Lineset::bounds_vfunc(&new_b.x0, &new_b.y0, &new_b.x1, &new_b.y1); + + i2w(new_b.x0, new_b.y0); + i2w(new_b.x1, new_b.y1); + + item->x1 = new_b.x0; + item->y1 = new_b.y0; + item->x2 = new_b.x1; + item->y2 = new_b.y1; + + /* Update bounding box used in rendering function */ + cv.w2c(new_b.x0, new_b.y0, bbox.x0, bbox.y0); + cv.w2c(new_b.x1, new_b.y1, bbox.x1, bbox.y1); + + /* + * if the first primary axis property (x1 for Vertical, y1 for Horizontal) changed, we must redraw everything, + * because lines are positioned relative to this coordinate. Please excuse the confusion resulting from + * gnome canvas coordinate numbering (1, 2) and libart's (0, 1). + */ + if(orientation == Vertical) { + if(new_b.x0 == old_b.x0) { + /* No need to update everything */ + if(new_b.y0 != old_b.y0) { + redraw.x0 = old_b.x0; + redraw.y0 = min(old_b.y0, new_b.y0); + redraw.x1 = old_b.x1; + redraw.y1 = max(old_b.y0, new_b.y0); + redraw_request(redraw); + } + if(new_b.y1 != old_b.y1) { + redraw.x0 = old_b.x0; + redraw.y0 = min(old_b.y1, new_b.y1); + redraw.x1 = old_b.x1; + redraw.y1 = max(old_b.y1, new_b.y1); + redraw_request(redraw); + } + + if(new_b.x1 > old_b.x1) { + // we have a larger area ==> possibly more lines + request_lines(old_b.x1, new_b.x1); + redraw.x0 = old_b.x1; + redraw.y0 = min(old_b.y0, new_b.y0); + redraw.x1 = new_b.x1; + redraw.y1 = max(old_b.y1, new_b.y1); + redraw_request(redraw); + } + else if(new_b.x1 < old_b.x1) { + remove_lines(new_b.x1, old_b.x1); + redraw.x0 = new_b.x1; + redraw.y0 = min(old_b.y0, new_b.y0); + redraw.x1 = old_b.x1; + redraw.y1 = max(old_b.y1, new_b.y1); + redraw_request(redraw); + } + return true; + } + else { + /* update everything */ + //cerr << "update everything" << endl; + art_drect_union(&redraw, &old_b, &new_b); + redraw_request(redraw); + return false; + } + } + else { + if(new_b.y0 == old_b.y0) { + /* No need to update everything */ + if(new_b.x0 != old_b.x0) { + redraw.y0 = old_b.y0; + redraw.x0 = min(old_b.x0, new_b.x0); + redraw.y1 = old_b.y1; + redraw.x1 = max(old_b.x0, new_b.x0); + redraw_request(redraw); + } + if(new_b.x1 != old_b.x1) { + redraw.y0 = old_b.y0; + redraw.x0 = min(old_b.x1, new_b.x1); + redraw.y1 = old_b.y1; + redraw.x1 = max(old_b.x1, new_b.x1); + redraw_request(redraw); + } + + if(new_b.y1 > old_b.y1) { + // we have a larger area ==> possibly more lines + request_lines(old_b.y1, new_b.y1); + redraw.y0 = old_b.y1; + redraw.x0 = min(old_b.x0, new_b.x0); + redraw.y1 = new_b.y1; + redraw.x1 = max(old_b.x1, new_b.x1); + redraw_request(redraw); + } + else if(new_b.y1 < old_b.y1) { + remove_lines(new_b.y1, old_b.y1); + redraw.y0 = new_b.y1; + redraw.x0 = min(old_b.x0, new_b.x0); + redraw.y1 = old_b.y1; + redraw.x1 = max(old_b.x1, new_b.x1); + redraw_request(redraw); + } + return true; + } + else { + /* update everything */ + art_drect_union(&redraw, &old_b, &new_b); + redraw_request(redraw); + return false; + } + } +} + +/* + * Some key concepts + * don't allow modifying line data outside the update function. We don't want any line data outside the visible view range, + * and view range is only "known" in the update function + */ + +/* + * what to do here? + * 1. find out if any line data has been modified since last update. + * N. find out if the item moved. if it moved, the old bbox and the new bbox need to be updated. + */ +void +Lineset::update_vfunc(double* affine, ArtSVP* clip_path, int flags) { + GnomeCanvasItem* item = GNOME_CANVAS_ITEM(gobj()); + bool lines_need_redraw = true; + + /* + * need to call gnome_canvas_item_update here, to unset the need_update flag. + * but a call to Gnome::Canvas::Item::update_vfunc results in infinite recursion. + * that function is declared in gnome_canvas.c so no way to call it directly: + * Item::update_vfunc(affine, clip_path, flags); + * So just copy the code from that function. This has to be a bug or + * something I haven't figured out. + */ + GTK_OBJECT_UNSET_FLAGS (item, GNOME_CANVAS_ITEM_NEED_UPDATE); + GTK_OBJECT_UNSET_FLAGS (item, GNOME_CANVAS_ITEM_NEED_AFFINE); + GTK_OBJECT_UNSET_FLAGS (item, GNOME_CANVAS_ITEM_NEED_CLIP); + GTK_OBJECT_UNSET_FLAGS (item, GNOME_CANVAS_ITEM_NEED_VIS); + + //cerr << "update {" << endl; + in_update = true; + + // ahh. We must update bounds no matter what. If the group position changed, + // there is no way that we are notified of that. + + //if(bounds_changed) { + lines_need_redraw = update_bounds(); + bounds_changed = false; + //} + + update_lines(lines_need_redraw); + + in_update = false; + //cerr << "}" << endl; +} + +void +Lineset::draw_vfunc(const Glib::RefPtr<Gdk::Drawable>& drawable, int x, int y, int width, int height) { + cerr << "please don't use the GnomeCanvasLineset item in a non-aa Canvas" << endl; + abort(); +} + +inline void +Lineset::paint_vert(GnomeCanvasBuf* buf, Lineset::Line& line, int x1, int y1, int x2, int y2) { + if(line.width == 1.0) { + PAINT_VERTA(buf, line.r, line.g, line.b, line.a, x1, y1, y2); + } + else { + PAINT_BOX(buf, line.r, line.g, line.b, line.a, x1, y1, x2, y2); + } +} + +inline void +Lineset::paint_horiz(GnomeCanvasBuf* buf, Lineset::Line& line, int x1, int y1, int x2, int y2) { + if(line.width == 1.0) { + PAINT_HORIZA(buf, line.r, line.g, line.b, line.a, x1, x2, y1); + } + else { + PAINT_BOX(buf, line.r, line.g, line.b, line.a, x1, y1, x2, y2); + } +} + +void +Lineset::render_vfunc(GnomeCanvasBuf* buf) { + ArtIRect rect; + int pos0, pos1, offset; + + if (buf->is_bg) { + gnome_canvas_buf_ensure_buf (buf); + buf->is_bg = FALSE; + } + + /* get the rect that we are rendering to */ + art_irect_intersect(&rect, &bbox, &buf->rect); + +#if 0 + /* DEBUG render bounding box for this region. should result in the full + bounding box when all rendering regions are finished */ + PAINT_BOX(buf, 0xaa, 0xaa, 0xff, 0xbb, rect.x0, rect.y0, rect.x1, rect.y1); +#endif + +#if 0 + /* harlequin debugging, shows the rect that is actually drawn, distinct from + rects from other render cycles */ + gint r, g, b, a; + r = random() % 0xff; + g = random() % 0xff; + b = random() % 0xff; + PAINT_BOX(buf, r, g, b, 0x33, rect.x0, rect.y0, rect.x1, rect.y1); +#endif + + if(lines.empty()) { + return; + } + + Lines::iterator it = lines.begin(); + Lines::iterator end = --lines.end(); + + /** + * The first and the last line in this render have to be handled separately from those in between, because those lines + * may be cut off at the ends. + */ + + if(orientation == Vertical) { + offset = bbox.x0; + + // skip parts of lines that are to the right of the buffer, and paint the last line visible + for(; end != lines.end(); --end) { + pos0 = ((int) floor(end->coord)) + offset; + + if(pos0 < rect.x1) { + pos1 = min((pos0 + (int) floor(end->width)), rect.x1); + if(pos0 < rect.x0 && pos1 < rect.x0) { + return; + } + + paint_vert(buf, *end, pos0, rect.y0, pos1, rect.y1); + break; + } + } + + if(end == lines.end()) { + return; + } + + // skip parts of lines that are to the left of the buffer + for(; it != end; ++it) { + pos0 = ((int) floor(it->coord)) + offset; + pos1 = pos0 + ((int) floor(it->width)); + + if(pos1 > rect.x0) { + pos0 = max(pos0, rect.x0); + paint_vert(buf, *it, pos0, rect.y0, pos1, rect.y1); + ++it; + break; + } + } + + // render what's between the first and last lines + for(; it != end; ++it) { + pos0 = ((int) floor(it->coord)) + offset; + pos1 = pos0 + ((int) floor(it->width)); + + paint_vert(buf, *it, pos0, rect.y0, pos1, rect.y1); + } + } + else { + offset = bbox.y0; + + // skip parts of lines that are to the right of the buffer, and paint the last line visible + for(; end != lines.end(); --end) { + pos0 = ((int) floor(end->coord)) + offset; + + if(pos0 < rect.y1) { + pos1 = min((pos0 + (int) floor(end->width)), rect.y1); + if(pos0 < rect.y0 && pos1 < rect.y0) { + return; + } + + paint_horiz(buf, *end, rect.x0, pos0, rect.x1, pos1); + break; + } + } + + if(end == lines.end()) { + return; + } + + // skip parts of lines that are to the left of the buffer + for(; it != end; ++it) { + pos0 = ((int) floor(it->coord)) + offset; + pos1 = pos0 + ((int) floor(it->width)); + + if(pos1 > rect.y0) { + pos0 = max(pos0, rect.y0); + paint_horiz(buf, *it, rect.x0, pos0, rect.x1, pos1); + ++it; + break; + } + } + + // render what's between the first and last lines + for(; it != end; ++it) { + pos0 = ((int) floor(it->coord)) + offset; + pos1 = pos0 + ((int) floor(it->width)); + paint_horiz(buf, *it, rect.x0, pos0, rect.x1, pos1); + } + } +} + +void +Lineset::bounds_vfunc(double* _x1, double* _y1, double* _x2, double* _y2) { + *_x1 = x1; + *_y1 = y1; + *_x2 = x2 + 1; + *_y2 = y2 + 1; +} + + +double +Lineset::point_vfunc(double x, double y, int cx, int cy, GnomeCanvasItem** actual_item) { + double x1, y1, x2, y2; + double dx, dy; + + Lineset::bounds_vfunc(&x1, &y1, &x2, &y2); + + *actual_item = gobj(); + + if (x < x1) { + dx = x1 - x; + } + else if(x > x2) { + dx = x - x2; + } + else { + dx = 0.0; + } + + if (y < y1) { + dy = y1 - y; + } + else if(y > y2) { + dy = y - y2; + } + else { + if (dx == 0.0) { + // point is inside + return 0.0; + } + else { + dy = 0.0; + } + } + + return sqrt (dx * dx + dy * dy); +} + +/* If not overrided emit the signal */ +void +Lineset::request_lines(double c1, double c2) { + signal_request_lines(*this, c1, c2); +} + +void +Lineset::bounds_need_update() { + bounds_changed = true; + + if(!in_update) { + request_update(); + } +} + +void +Lineset::region_needs_update(double coord1, double coord2) { + if(update_region1 > update_region2) { + update_region1 = coord1; + update_region2 = coord2; + } + else { + update_region1 = min(update_region1, coord1); + update_region2 = max(update_region2, coord2); + } + + if(!in_update) { + request_update(); + } +} + +/* + * These have been defined to avoid endless recursion with gnomecanvasmm. + * Don't know why this happens + */ +bool Lineset::on_event(GdkEvent* p1) { + return false; +} +void Lineset::realize_vfunc() { } +void Lineset::unrealize_vfunc() { } +void Lineset::map_vfunc() { } +void Lineset::unmap_vfunc() { } + +} /* namespace Canvas */ +} /* namespace Gnome */ diff --git a/gtk2_ardour/lineset.h b/gtk2_ardour/lineset.h new file mode 100644 index 0000000000..a5d0516139 --- /dev/null +++ b/gtk2_ardour/lineset.h @@ -0,0 +1,208 @@ +/* + Copyright (C) 2007 Paul Davis + 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. +*/ + +#ifndef __gnome_canvas_lineset_h__ +#define __gnome_canvas_lineset_h__ + +#include <libgnomecanvasmm/item.h> + +namespace Gnome { +namespace Canvas { + +class LinesetClass : public Glib::Class { +public: + const Glib::Class& init(); + static void class_init_function(void* g_class, void* class_data); +}; + +/** + * A canvas item that displays a list of lines vertically or horizontally, + * spanning the entire size of the item. + */ +class Lineset : public Item { +public: + enum Orientation { + Vertical, + Horizontal + }; + + Lineset(Group& parent, Orientation); + virtual ~Lineset(); + + Glib::PropertyProxy<double> property_x1() { return x1.get_proxy(); } + Glib::PropertyProxy<double> property_y1() { return y1.get_proxy(); } + Glib::PropertyProxy<double> property_x2() { return x2.get_proxy(); } + Glib::PropertyProxy<double> property_y2() { return y2.get_proxy(); } + + /* + * Note: every line operation takes a coord parameter, as an index to + * the line it modifies. The index will identify a line if it is between + * line.coord and line.coord + line.width. + */ + + /** + * Move a line to a new position + * for this to work (to move the desired line) it is important that + * lines have unique coordinates. This also applies to every line + * accessing functions below + */ + void move_line(double coord, double dest); + + /** + * Change the width of a line. Only allow it if the new width doesn't + * overlap the next line (see below) + */ + void change_line_width(double coord, double width); + + /** + * Change the color of a line + */ + void change_line_color(double coord, uint32_t color); + + /** + * this function adds a line to draw. + * width is an offset, so that coord + width specifies the end of the line. + * lines should not overlap, as no layering information is provided. + * however, line_coord[i] + line_width[i] == line_coord[i+1] is + * be legal, as the coordinates are real numbers and represents + * real world coordinates. Two real world object sharing coordinates for start + * and end are not overlapping. + */ + void add_line(double coord, double width, uint32_t color); + + /** + * remove the line at coord + */ + void remove_line(double coord); + + /** + * remove all lines in a coordinate range + */ + void remove_lines(double c1, double c2); + + /** + * remove all lines with a coordinate lower than coord + */ + void remove_until(double coord); + + /** + * remove all lines with a coordinate equal to or higher than coord + */ + void remove_from(double coord); + + /** + * remove all lines + */ + void clear(); + + /** + * this is a request of information on lines in a coordinate range. + * for every line visible in the provided coordinate range, + * call add_line() on it. + * This is called when the area between c1 and c2 becomes visible, when + * previously outside any possible view. So the number of calls to this + * function will be kept at a minimum. + */ + virtual void request_lines(double c1, double c2); + + /** + * instead of overriding the update_lines function one can connect to this + * and add lines externally instead. If add_lines() is overrided, this + * signal will not be emitted. + */ + sigc::signal<void, Lineset&, double, double> signal_request_lines; + + /* overrided from Gnome::Canvas::Item */ + void update_vfunc(double* affine, ArtSVP* clip_path, int flags); + void realize_vfunc(); + void unrealize_vfunc(); + void map_vfunc(); + void unmap_vfunc(); + void draw_vfunc(const Glib::RefPtr<Gdk::Drawable>& drawable, int x, int y, int width, int height); + void render_vfunc(GnomeCanvasBuf* buf); + double point_vfunc(double x, double y, int cx, int cy, GnomeCanvasItem** actual_item); + void bounds_vfunc(double* x1, double* y1, double* x2, double* y2); + bool on_event(GdkEvent* p1); + + /* debug */ + void print_lines(); + +protected: + struct Line { + Line(double c, double w, uint32_t color); + Line(double c); + + void set_color(uint32_t color); + + double coord; + double width; + unsigned char r; + unsigned char g; + unsigned char b; + unsigned char a; + }; + + static inline void paint_vert(GnomeCanvasBuf* buf, Lineset::Line& line, int x1, int y1, int x2, int y2); + static inline void paint_horiz(GnomeCanvasBuf* buf, Lineset::Line& line, int x1, int y1, int x2, int y2); + + static bool line_compare(const Line& a, const Line& b); + + typedef std::list<Line> Lines; + void bounds_need_update(); + void region_needs_update(double coord1, double coord2); + bool update_bounds(); + void update_lines(bool need_redraw); + void redraw_request(ArtIRect&); + void redraw_request(ArtDRect&); + + Lines::iterator line_at(double coord); + + /* store that last accessed line so adjacent lines are found faster */ + Lines::iterator cached_pos; + + static LinesetClass lineset_class; + Orientation orientation; + Lines lines; + + /* properties */ + Glib::Property<double> x1; + Glib::Property<double> y1; + Glib::Property<double> x2; + Glib::Property<double> y2; + + /* cached bounding box in canvas coordinates*/ + ArtIRect bbox; + +private: + Lineset(); + Lineset(const Lineset&); + + bool in_update; + + /* a range that needs update update1 > update2 ==> no update needed */ + double update_region1; + double update_region2; + bool bounds_changed; + + double covered1; + double covered2; +}; + +} /* namespace Canvas */ +} /* namespace Gnome */ + +#endif /* __gnome_canvas_lineset_h__ */ diff --git a/gtk2_ardour/midi_region_view.cc b/gtk2_ardour/midi_region_view.cc index ec326f1e2e..177adbf38b 100644 --- a/gtk2_ardour/midi_region_view.cc +++ b/gtk2_ardour/midi_region_view.cc @@ -69,7 +69,10 @@ MidiRegionView::MidiRegionView (ArdourCanvas::Group *parent, RouteTimeAxisView & , _mouse_state(None) , _pressed_button(0) { + group->lower_to_bottom(); _note_group->raise_to_top(); + + frame->property_fill_color_rgba() = 0xff000033; } MidiRegionView::MidiRegionView (ArdourCanvas::Group *parent, RouteTimeAxisView &tv, boost::shared_ptr<MidiRegion> r, double spu, Gdk::Color& basic_color, TimeAxisViewItem::Visibility visibility) @@ -795,3 +798,14 @@ MidiRegionView::switch_source(boost::shared_ptr<Source> src) display_model(msrc->model()); } +void +MidiRegionView::set_frame_color() +{ + if (frame) { + if (_selected && should_show_selection) { + frame->property_fill_color_rgba() = ARDOUR_UI::config()->canvasvar_SelectedFrameBase.get(); + } else { + frame->property_fill_color_rgba() = ARDOUR_UI::config()->canvasvar_MidiFrameBase.get(); + } + } +} diff --git a/gtk2_ardour/midi_region_view.h b/gtk2_ardour/midi_region_view.h index 20c7671f5b..a709f523a3 100644 --- a/gtk2_ardour/midi_region_view.h +++ b/gtk2_ardour/midi_region_view.h @@ -72,6 +72,8 @@ class MidiRegionView : public RegionView void set_y_position_and_height (double, double); + void set_frame_color(); + void redisplay_model(); GhostRegion* add_ghost (AutomationTimeAxisView&); diff --git a/gtk2_ardour/midi_scroomer.cc b/gtk2_ardour/midi_scroomer.cc new file mode 100644 index 0000000000..a38911d98c --- /dev/null +++ b/gtk2_ardour/midi_scroomer.cc @@ -0,0 +1,164 @@ +/* + Copyright (C) 2008 Paul Davis + + 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 "midi_scroomer.h" + +#include <cairomm/context.h> + +#include <iostream> + +using namespace Gtkmm2ext; +using namespace Gtk; +using namespace std; + +//std::map<int, Glib::RefPtr<Gdk::Pixmap> > MidiScroomer::piano_pixmaps; + +MidiScroomer::MidiScroomer(Adjustment& adj) + : Gtkmm2ext::Scroomer(adj) { + + adj.set_lower(0); + adj.set_upper(127); + + /* set minimum view range to one octave */ + set_min_page_size(12); +} + +MidiScroomer::~MidiScroomer() { +} + +bool +MidiScroomer::on_expose_event(GdkEventExpose* ev) { + Cairo::RefPtr<Cairo::Context> cc = get_window()->create_cairo_context(); + GdkRectangle comp_rect, clip_rect; + Component first_comp = point_in(ev->area.y); + Component last_comp = point_in(ev->area.y + ev->area.height); + int height = get_height(); + int lnote, hnote; + double y2note = (double) 127 / height; + double note2y = (double) height / 127; + double note_width = 0.8 * get_width(); + double note_height = 1.4 * note2y; + double black_shift = 0.1 * note2y; + double colors[6]; + + //cerr << ev->area.y << " " << ev->area.height << endl; + + comp_rect.x = 0; + comp_rect.width = get_width(); + + for(int i = first_comp; i <= last_comp; ++i) { + Component comp = (Component) i; + set_comp_rect(comp_rect, comp); + + if(gdk_rectangle_intersect(&comp_rect, &ev->area, &clip_rect)) { + get_colors(colors, comp); + + cc->rectangle(clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height); + cc->set_source_rgb (colors[3], colors[4], colors[5]); + cc->fill_preserve(); + + cc->clip(); + cc->set_source_rgb(colors[0], colors[1], colors[2]); + cc->set_line_width(note_height); + + lnote = 127 - (int) floor((double) (clip_rect.y + clip_rect.height) * y2note) - 1; + hnote = 127 - (int) floor((double) clip_rect.y * y2note) + 1; + + for(int note = lnote; note < hnote + 1; ++note) { + double y = height - note * note2y; + bool draw = false; + + switch(note % 12) { + case 1: + case 6: + y -= black_shift; + draw = true; + break; + case 3: + case 10: + y += black_shift; + draw = true; + break; + case 8: + draw = true; + break; + default: + break; + } + + if(draw) { + cc->set_line_width(1.4 * note2y); + cc->move_to(0, y); + cc->line_to(note_width, y); + cc->stroke(); + } + } + + cc->reset_clip(); + } + } + + return true; +} + +void +MidiScroomer::get_colors(double color[], Component comp) { + switch (comp) { + case TopBase: + case BottomBase: + color[0] = 0.24; + color[1] = 0.24; + color[2] = 0.24; + color[3] = 0.33; + color[4] = 0.33; + color[5] = 0.33; + break; + case Handle1: + case Handle2: + color[0] = 0.38; + color[1] = 0.38; + color[2] = 0.38; + color[3] = 0.91; + color[4] = 0.91; + color[5] = 0.91; + break; + case Slider: + color[0] = 0.38; + color[1] = 0.38; + color[2] = 0.38; + color[3] = 0.77; + color[4] = 0.77; + color[5] = 0.77; + break; + default: + break; + } +} + +void +MidiScroomer::on_size_request(Gtk::Requisition* r) { + r->width = 16; + r->height = 100; + //r->width = 32; + //r->height = 512; +} + +void +MidiScroomer::on_size_allocate(Gtk::Allocation& a) { + Scroomer::on_size_allocate(a); +} diff --git a/gtk2_ardour/midi_scroomer.h b/gtk2_ardour/midi_scroomer.h new file mode 100644 index 0000000000..a0021de926 --- /dev/null +++ b/gtk2_ardour/midi_scroomer.h @@ -0,0 +1,39 @@ +/* + Copyright (C) 2008 Paul Davis + + 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. + +*/ + +#ifndef __ardour_midi_scroomer_h__ +#define __ardour_midi_scroomer_h__ + +#include <gtkmm2ext/scroomer.h> +#include <gdkmm/pixbuf.h> + +class MidiScroomer : public Gtkmm2ext::Scroomer { +public: + MidiScroomer(Gtk::Adjustment&); + ~MidiScroomer(); + + bool on_expose_event(GdkEventExpose*); + + void on_size_request(Gtk::Requisition*); + void on_size_allocate(Gtk::Allocation&); + + void get_colors(double color[], Component comp); +}; + +#endif /* __ardour_midi_scroomer_h__ */ diff --git a/gtk2_ardour/midi_streamview.cc b/gtk2_ardour/midi_streamview.cc index 9e0f9f368f..172d30b7ef 100644 --- a/gtk2_ardour/midi_streamview.cc +++ b/gtk2_ardour/midi_streamview.cc @@ -46,7 +46,7 @@ #include "gui_thread.h" #include "utils.h" #include "simplerect.h" -#include "simpleline.h" +#include "lineset.h" using namespace std; using namespace ARDOUR; @@ -55,7 +55,9 @@ using namespace Editing; MidiStreamView::MidiStreamView (MidiTimeAxisView& tv) : StreamView (tv) + , note_range_adjustment(0.0f, 0.0f, 0.0f) , _range(ContentsRange) + , _range_sum_cache(-1.0) , _lowest_note(60) , _highest_note(60) { @@ -63,19 +65,20 @@ MidiStreamView::MidiStreamView (MidiTimeAxisView& tv) stream_base_color = ARDOUR_UI::config()->canvasvar_MidiTrackBase.get(); else stream_base_color = ARDOUR_UI::config()->canvasvar_MidiBusBase.get(); - - canvas_rect->property_fill_color_rgba() = stream_base_color; - canvas_rect->property_outline_color_rgba() = RGBA_BLACK; use_rec_regions = tv.editor.show_waveforms_recording (); - - _note_line_group = new ArdourCanvas::Group (*canvas_group); - - for (uint8_t i=0; i < 127; ++i) { - _note_lines[i] = new ArdourCanvas::SimpleLine(*_note_line_group, - 0, note_to_y(i), 10, note_to_y(i)); - _note_lines[i]->property_color_rgba() = 0xEEEEEE55; - } + + _note_lines = new ArdourCanvas::Lineset(*canvas_group, ArdourCanvas::Lineset::Horizontal); + + _note_lines->property_x1() = 0; + _note_lines->property_y1() = 0; + _note_lines->property_x2() = trackview().editor.frame_to_pixel (max_frames); + _note_lines->property_y2() = 0; + + _note_lines->signal_event().connect (bind (mem_fun (_trackview.editor, &PublicEditor::canvas_stream_view_event), _note_lines, &_trackview)); + + note_range_adjustment.signal_value_changed().connect (mem_fun (*this, &MidiStreamView::note_range_adjustment_changed)); + ColorsChanged.connect(mem_fun(*this, &MidiStreamView::draw_note_lines)); } MidiStreamView::~MidiStreamView () @@ -159,7 +162,8 @@ void MidiStreamView::display_diskstream (boost::shared_ptr<Diskstream> ds) { StreamView::display_diskstream(ds); - draw_note_separators(); + draw_note_lines(); + NoteRangeChanged(); } // FIXME: code duplication with AudioStreamView @@ -216,7 +220,10 @@ MidiStreamView::redisplay_diskstream () region_layered (*i); } - draw_note_separators(); + note_range_adjustment.set_page_size(_highest_note - _lowest_note); + note_range_adjustment.set_value(_lowest_note); + NoteRangeChanged(); + draw_note_lines(); } @@ -224,22 +231,45 @@ void MidiStreamView::update_contents_y_position_and_height () { StreamView::update_contents_y_position_and_height(); - draw_note_separators(); + _note_lines->property_y2() = height; + draw_note_lines(); } void -MidiStreamView::draw_note_separators() +MidiStreamView::draw_note_lines() { - for (uint8_t i=0; i < 127; ++i) { - if (i >= _lowest_note-1 && i <= _highest_note) { - _note_lines[i]->property_x1() = 0; - _note_lines[i]->property_x2() = canvas_rect->property_x2() - 2; - _note_lines[i]->property_y1() = note_to_y(i); - _note_lines[i]->property_y2() = note_to_y(i); - _note_lines[i]->show(); - } else { - _note_lines[i]->hide(); + double y; + double prev_y = contents_height(); + uint32_t color; + + _note_lines->clear(); + + for(int i = _lowest_note; i <= _highest_note; ++i) { + y = floor(note_to_y(i)); + + _note_lines->add_line(prev_y, 1.0, ARDOUR_UI::config()->canvasvar_PianoRollBlackOutline.get()); + + switch(i % 12) { + case 1: + case 3: + case 6: + case 8: + case 10: + color = ARDOUR_UI::config()->canvasvar_PianoRollBlack.get(); + break; + default: + color = ARDOUR_UI::config()->canvasvar_PianoRollWhite.get(); + break; } + + if(i == _highest_note) { + _note_lines->add_line(y, prev_y - y, color); + } + else { + _note_lines->add_line(y + 1.0, prev_y - y - 1.0, color); + } + + prev_y = y; } } @@ -258,6 +288,21 @@ MidiStreamView::set_note_range(VisibleNoteRange r) redisplay_diskstream(); } +void +MidiStreamView::set_note_range(uint8_t lowest, uint8_t highest) { + if(_range == ContentsRange) { + _lowest_note = lowest; + _highest_note = highest; + + list<RegionView *>::iterator i; + for (i = region_views.begin(); i != region_views.end(); ++i) { + (*i)->set_y_position_and_height(0, height); // apply note range + } + } + + draw_note_lines(); + NoteRangeChanged(); +} void MidiStreamView::update_bounds(uint8_t note_num) @@ -544,12 +589,37 @@ MidiStreamView::color_handler () //case cMidiTrackBase: if (_trackview.is_midi_track()) { - canvas_rect->property_fill_color_rgba() = ARDOUR_UI::config()->canvasvar_MidiTrackBase.get(); + //canvas_rect->property_fill_color_rgba() = ARDOUR_UI::config()->canvasvar_MidiTrackBase.get(); } //case cMidiBusBase: if (!_trackview.is_midi_track()) { - canvas_rect->property_fill_color_rgba() = ARDOUR_UI::config()->canvasvar_MidiBusBase.get();; + //canvas_rect->property_fill_color_rgba() = ARDOUR_UI::config()->canvasvar_MidiBusBase.get();; } } +void +MidiStreamView::note_range_adjustment_changed() { + double sum = note_range_adjustment.get_value() + note_range_adjustment.get_page_size(); + int lowest = (int) floor(note_range_adjustment.get_value()); + int highest; + + if(sum == _range_sum_cache) { + cerr << "cached" << endl; + highest = (int) floor(sum); + } + else { + cerr << "recalc" << endl; + highest = lowest + (int) floor(note_range_adjustment.get_page_size()); + _range_sum_cache = sum; + } + + if(lowest == lowest_note() && highest == highest_note()) { + return; + } + + cerr << "note range changed: " << lowest << " " << highest << endl; + //cerr << " val=" << v_zoom_adjustment.get_value() << " page=" << v_zoom_adjustment.get_page_size() << " sum=" << v_zoom_adjustment.get_value() + v_zoom_adjustment.get_page_size() << endl; + + set_note_range(lowest, highest); +} diff --git a/gtk2_ardour/midi_streamview.h b/gtk2_ardour/midi_streamview.h index fa414cfe9c..5c0ee47c30 100644 --- a/gtk2_ardour/midi_streamview.h +++ b/gtk2_ardour/midi_streamview.h @@ -24,10 +24,10 @@ #include <ardour/location.h> #include "enums.h" -#include "simplerect.h" #include "streamview.h" #include "time_axis_view_item.h" #include "route_time_axis.h" +#include "canvas.h" namespace Gdk { class Color; @@ -66,8 +66,11 @@ class MidiStreamView : public StreamView ContentsRange }; + Gtk::Adjustment note_range_adjustment; + VisibleNoteRange note_range() { return _range; } void set_note_range(VisibleNoteRange r); + void set_note_range(uint8_t lowest, uint8_t highest); uint8_t lowest_note() const { return (_range == FullRange) ? 0 : _lowest_note; } uint8_t highest_note() const { return (_range == FullRange) ? 127 : _highest_note; } @@ -93,6 +96,8 @@ class MidiStreamView : public StreamView inline uint8_t contents_note_range() const { return _highest_note - _lowest_note + 1; } + + sigc::signal<void> NoteRangeChanged; private: void setup_rec_box (); @@ -104,15 +109,17 @@ class MidiStreamView : public StreamView void display_diskstream (boost::shared_ptr<ARDOUR::Diskstream> ds); void update_contents_y_position_and_height (); - void draw_note_separators(); + void draw_note_lines(); void color_handler (); + void note_range_adjustment_changed(); + VisibleNoteRange _range; + double _range_sum_cache; uint8_t _lowest_note; uint8_t _highest_note; - ArdourCanvas::Group* _note_line_group; - ArdourCanvas::SimpleLine* _note_lines[127]; + ArdourCanvas::Lineset* _note_lines; }; #endif /* __ardour_midi_streamview_h__ */ diff --git a/gtk2_ardour/midi_time_axis.cc b/gtk2_ardour/midi_time_axis.cc index 21848fff59..b3892d2790 100644 --- a/gtk2_ardour/midi_time_axis.cc +++ b/gtk2_ardour/midi_time_axis.cc @@ -67,6 +67,8 @@ #include "simplerect.h" #include "midi_streamview.h" #include "utils.h" +#include "midi_scroomer.h" +#include "piano_roll_header.h" #include <ardour/midi_track.h> @@ -81,6 +83,8 @@ using namespace Editing; MidiTimeAxisView::MidiTimeAxisView (PublicEditor& ed, Session& sess, boost::shared_ptr<Route> rt, Canvas& canvas) : AxisView(sess) // FIXME: won't compile without this, why?? , RouteTimeAxisView(ed, sess, rt, canvas) + , _range_scroomer(0) + , _piano_roll_header(0) , _note_mode(Sustained) , _note_mode_item(NULL) , _percussion_mode_item(NULL) @@ -112,6 +116,11 @@ MidiTimeAxisView::MidiTimeAxisView (PublicEditor& ed, Session& sess, boost::shar _route->processors_changed.connect (mem_fun(*this, &MidiTimeAxisView::processors_changed)); if (is_track()) { + _piano_roll_header = new PianoRollHeader(*midi_view()); + _range_scroomer = new MidiScroomer(midi_view()->note_range_adjustment); + + controls_hbox.pack_start(*_range_scroomer); + controls_hbox.pack_start(*_piano_roll_header); controls_ebox.set_name ("MidiTrackControlsBaseUnselected"); controls_base_selected_name = "MidiTrackControlsBaseSelected"; @@ -318,5 +327,3 @@ MidiTimeAxisView::route_active_changed () } } } - - diff --git a/gtk2_ardour/midi_time_axis.h b/gtk2_ardour/midi_time_axis.h index f7ec164edc..15e487e00e 100644 --- a/gtk2_ardour/midi_time_axis.h +++ b/gtk2_ardour/midi_time_axis.h @@ -50,6 +50,8 @@ namespace ARDOUR { class PublicEditor; class MidiStreamView; +class MidiScroomer; +class PianoRollHeader; class MidiTimeAxisView : public RouteTimeAxisView { @@ -67,7 +69,7 @@ class MidiTimeAxisView : public RouteTimeAxisView void create_automation_child (ARDOUR::Parameter param, bool show); ARDOUR::NoteMode note_mode() const { return _note_mode; } - + private: void append_extra_display_menu_items (); @@ -83,6 +85,8 @@ class MidiTimeAxisView : public RouteTimeAxisView Gtk::Menu _subplugin_menu; + MidiScroomer* _range_scroomer; + PianoRollHeader* _piano_roll_header; ARDOUR::NoteMode _note_mode; Gtk::RadioMenuItem* _note_mode_item; Gtk::RadioMenuItem* _percussion_mode_item; diff --git a/gtk2_ardour/piano_roll_header.cc b/gtk2_ardour/piano_roll_header.cc new file mode 100644 index 0000000000..8be0bc86c1 --- /dev/null +++ b/gtk2_ardour/piano_roll_header.cc @@ -0,0 +1,653 @@ +/* + Copyright (C) 2008 Paul Davis + + 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 <ardour/midi_track.h> + +#include "piano_roll_header.h" +#include "midi_time_axis.h" +#include "midi_streamview.h" + +#include <iostream> + +const int no_note = 0xff; + +using namespace std; + +PianoRollHeader::Color PianoRollHeader::white = PianoRollHeader::Color(0.77f, 0.78f, 0.76f); +PianoRollHeader::Color PianoRollHeader::white_highlight = PianoRollHeader::Color(0.87f, 0.88f, 0.86f); +PianoRollHeader::Color PianoRollHeader::white_shade_light = PianoRollHeader::Color(0.95f, 0.95f, 0.95f); +PianoRollHeader::Color PianoRollHeader::white_shade_dark = PianoRollHeader::Color(0.56f, 0.56f, 0.56f); + +PianoRollHeader::Color PianoRollHeader::black = PianoRollHeader::Color(0.24f, 0.24f, 0.24f); +PianoRollHeader::Color PianoRollHeader::black_highlight = PianoRollHeader::Color(0.30f, 0.30f, 0.30f); +PianoRollHeader::Color PianoRollHeader::black_shade_light = PianoRollHeader::Color(0.46f, 0.46f, 0.46f); +PianoRollHeader::Color PianoRollHeader::black_shade_dark = PianoRollHeader::Color(0.1f, 0.1f, 0.1f); + +PianoRollHeader::Color::Color() + : r(1.0f) + , g(1.0f) + , b(1.0f) { +} + +PianoRollHeader::Color::Color(double _r, double _g, double _b) + : r(_r) + , g(_g) + , b(_b) { +} + +inline void +PianoRollHeader::Color::set(const PianoRollHeader::Color& c) { + r = c.r; + g = c.g; + b = c.b; +} + +PianoRollHeader::PianoRollHeader(MidiStreamView& v) + : _view(v) + , _highlighted_note(no_note) + , _clicked_note(no_note) { + + add_events (Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | + Gdk::ENTER_NOTIFY_MASK | + Gdk::LEAVE_NOTIFY_MASK | + Gdk::SCROLL_MASK); + + for(int i = 0; i < 128; ++i) { + _active_notes[i] = false; + } + + _view.NoteRangeChanged.connect (mem_fun (*this, &PianoRollHeader::note_range_changed)); +} + +inline void +create_path(Cairo::RefPtr<Cairo::Context> cr, double x[], double y[], int start, int stop) { + cr->move_to(x[start], y[start]); + + for(int i = start+1; i <= stop; ++i) { + cr->line_to(x[i], y[i]); + } +} + +inline void +render_rect(Cairo::RefPtr<Cairo::Context> cr, int note, double x[], double y[], + PianoRollHeader::Color& bg, PianoRollHeader::Color& tl_shadow, PianoRollHeader::Color& br_shadow) { + cr->set_source_rgb(bg.r, bg.g, bg.b); + create_path(cr, x, y, 0, 4); + cr->fill(); + + cr->set_source_rgb(tl_shadow.r, tl_shadow.g, tl_shadow.b); + create_path(cr, x, y, 0, 2); + cr->stroke(); + + cr->set_source_rgb(br_shadow.r, br_shadow.g, br_shadow.b); + create_path(cr, x, y, 2, 4); + cr->stroke(); +} + +inline void +render_cf(Cairo::RefPtr<Cairo::Context> cr, int note, double x[], double y[], + PianoRollHeader::Color& bg, PianoRollHeader::Color& tl_shadow, PianoRollHeader::Color& br_shadow) { + cr->set_source_rgb(bg.r, bg.g, bg.b); + create_path(cr, x, y, 0, 6); + cr->fill(); + + cr->set_source_rgb(tl_shadow.r, tl_shadow.g, tl_shadow.b); + create_path(cr, x, y, 0, 4); + cr->stroke(); + + cr->set_source_rgb(br_shadow.r, br_shadow.g, br_shadow.b); + create_path(cr, x, y, 4, 6); + cr->stroke(); +} + +inline void +render_eb(Cairo::RefPtr<Cairo::Context> cr, int note, double x[], double y[], + PianoRollHeader::Color& bg, PianoRollHeader::Color& tl_shadow, PianoRollHeader::Color& br_shadow) { + cr->set_source_rgb(bg.r, bg.g, bg.b); + create_path(cr, x, y, 0, 6); + cr->fill(); + + cr->set_source_rgb(tl_shadow.r, tl_shadow.g, tl_shadow.b); + create_path(cr, x, y, 0, 2); + cr->stroke(); + create_path(cr, x, y, 4, 5); + cr->stroke(); + + cr->set_source_rgb(br_shadow.r, br_shadow.g, br_shadow.b); + create_path(cr, x, y, 2, 4); + cr->stroke(); + create_path(cr, x, y, 5, 6); + cr->stroke(); +} + +inline void +render_dga(Cairo::RefPtr<Cairo::Context> cr, int note, double x[], double y[], + PianoRollHeader::Color& bg, PianoRollHeader::Color& tl_shadow, PianoRollHeader::Color& br_shadow) { + cr->set_source_rgb(bg.r, bg.g, bg.b); + create_path(cr, x, y, 0, 8); + cr->fill(); + + cr->set_source_rgb(tl_shadow.r, tl_shadow.g, tl_shadow.b); + create_path(cr, x, y, 0, 4); + cr->stroke(); + create_path(cr, x, y, 6, 7); + cr->stroke(); + + cr->set_source_rgb(br_shadow.r, br_shadow.g, br_shadow.b); + create_path(cr, x, y, 4, 6); + cr->stroke(); + create_path(cr, x, y, 7, 8); + cr->stroke(); +} + +void +PianoRollHeader::get_path(PianoRollHeader::ItemType note_type, int note, double x[], double y[]) { + double y_pos = floor(_view.note_to_y(note)) - 0.5f; + double note_height; + double other_y1 = floor(_view.note_to_y(note+1)) + floor(_note_height / 2.0f) + 0.5f; + double other_y2 = floor(_view.note_to_y(note-1)) + floor(_note_height / 2.0f) - 1.0f; + double width = get_width(); + + if(note == 0) { + note_height = floor(_view.contents_height()) - y_pos; + } + else { + note_height = floor(_view.note_to_y(note - 1)) - y_pos; + } + + switch(note_type) { + case BLACK_SEPARATOR: + x[0] = 1.5f; + y[0] = y_pos; + x[1] = _black_note_width; + y[1] = y_pos; + break; + case BLACK_MIDDLE_SEPARATOR: + x[0] = _black_note_width; + y[0] = y_pos + floor(_note_height / 2.0f); + x[1] = width - 1.0f; + y[1] = y[0]; + break; + case BLACK: + x[0] = 1.5f; + y[0] = y_pos + note_height - 0.5f; + x[1] = 1.5f; + y[1] = y_pos + 1.0f; + x[2] = _black_note_width; + y[2] = y_pos + 1.0f; + x[3] = _black_note_width; + y[3] = y_pos + note_height - 0.5f; + x[4] = 1.5f; + y[4] = y_pos + note_height - 0.5f; + return; + case WHITE_SEPARATOR: + x[0] = 1.5f; + y[0] = y_pos; + x[1] = width - 1.5f; + y[1] = y_pos; + break; + case WHITE_RECT: + x[0] = 1.5f; + y[0] = y_pos + note_height - 0.5f; + x[1] = 1.5f; + y[1] = y_pos + 1.0f; + x[2] = width - 1.5f; + y[2] = y_pos + 1.0f; + x[3] = width - 1.5f; + y[3] = y_pos + note_height - 0.5f; + x[4] = 1.5f; + y[4] = y_pos + note_height - 0.5f; + return; + case WHITE_CF: + x[0] = 1.5f; + y[0] = y_pos + note_height - 1.5f; + x[1] = 1.5f; + y[1] = y_pos + 1.0f; + x[2] = _black_note_width + 1.0f; + y[2] = y_pos + 1.0f; + x[3] = _black_note_width + 1.0f; + y[3] = other_y1; + x[4] = width - 1.5f; + y[4] = other_y1; + x[5] = width - 1.5f; + y[5] = y_pos + note_height - 1.5f; + x[6] = 1.5f; + y[6] = y_pos + note_height - 1.5f; + return; + case WHITE_EB: + x[0] = 1.5f; + y[0] = y_pos + note_height - 1.5f; + x[1] = 1.5f; + y[1] = y_pos + 1.0f; + x[2] = width - 1.5f; + y[2] = y_pos + 1.0f; + x[3] = width - 1.5f; + y[3] = other_y2; + x[4] = _black_note_width + 1.0f; + y[4] = other_y2; + x[5] = _black_note_width + 1.0f; + y[5] = y_pos + note_height - 1.5f; + x[6] = 1.5f; + y[6] = y_pos + note_height - 1.5f; + return; + case WHITE_DGA: + x[0] = 1.5f; + y[0] = y_pos + note_height - 1.5f; + x[1] = 1.5f; + y[1] = y_pos + 1.0f; + x[2] = _black_note_width + 1.0f; + y[2] = y_pos + 1.0f; + x[3] = _black_note_width + 1.0f; + y[3] = other_y1; + x[4] = width - 1.5f; + y[4] = other_y1; + x[5] = width - 1.5f; + y[5] = other_y2; + x[6] = _black_note_width + 1.0f; + y[6] = other_y2; + x[7] = _black_note_width + 1.0f; + y[7] = y_pos + note_height - 1.5f; + x[8] = 1.5f; + y[8] = y_pos + note_height - 1.5f; + return; + default: + return; + } +} + +bool +PianoRollHeader::on_expose_event (GdkEventExpose* ev) { + GdkRectangle& rect = ev->area; + double font_size; + int lowest, highest; + Cairo::RefPtr<Cairo::Context> cr = get_window()->create_cairo_context(); + Cairo::RefPtr<Cairo::LinearGradient> pat = Cairo::LinearGradient::create(0, 0, _black_note_width, 0); + double x[9]; + double y[9]; + Color bg, tl_shadow, br_shadow; + int oct_rel; + int y1 = max(rect.y, 0); + int y2 = min(rect.y + rect.height, (int) floor(_view.contents_height() - 1.0f)); + + //Cairo::TextExtents te; + lowest = max(_view.lowest_note(), _view.y_to_note(y2)); + highest = min(_view.highest_note(), _view.y_to_note(y1)); + + if(lowest > 127) { + lowest = 0; + } + + cr->select_font_face ("Georgia", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD); + font_size = min(10.0, _note_height); + cr->set_font_size(font_size); + + /* fill the entire rect with the color for non-highlighted white notes. + * then we won't have to draw the background for those notes, + * and would only have to draw the background for the one highlighted white note*/ + //cr->rectangle(rect.x, rect.y, rect.width, rect.height); + //cr->set_source_rgb(white.r, white.g, white.b); + //cr->fill(); + + cr->set_line_width(1.0f); + + /* draw vertical lines with shade at both ends of the widget */ + cr->set_source_rgb(0.0f, 0.0f, 0.0f); + cr->move_to(0.5f, rect.y); + cr->line_to(0.5f, rect.y + rect.height); + cr->stroke(); + cr->move_to(get_width() - 0.5f, rect.y); + cr->line_to(get_width() - 0.5f, rect.y + rect.height); + cr->stroke(); + + //pat->add_color_stop_rgb(0.0, 0.33, 0.33, 0.33); + //pat->add_color_stop_rgb(0.2, 0.39, 0.39, 0.39); + //pat->add_color_stop_rgb(1.0, 0.22, 0.22, 0.22); + //cr->set_source(pat); + + for(int i = lowest; i <= highest; ++i) { + oct_rel = i % 12; + + switch(oct_rel) { + case 1: + case 3: + case 6: + case 8: + case 10: + /* black note */ + if(i == _highlighted_note) { + bg.set(black_highlight); + } + else { + bg.set(black); + } + + if(_active_notes[i]) { + tl_shadow.set(black_shade_dark); + br_shadow.set(black_shade_light); + } + else { + tl_shadow.set(black_shade_light); + br_shadow.set(black_shade_dark); + } + + /* draw black separators */ + cr->set_source_rgb(0.0f, 0.0f, 0.0f); + get_path(BLACK_SEPARATOR, i, x, y); + create_path(cr, x, y, 0, 1); + cr->stroke(); + + get_path(BLACK_MIDDLE_SEPARATOR, i, x, y); + create_path(cr, x, y, 0, 1); + cr->stroke(); + + get_path(BLACK, i, x, y); + render_rect(cr, i, x, y, bg, tl_shadow, br_shadow); + break; + + default: + /* white note */ + if(i == _highlighted_note) { + bg.set(white_highlight); + } + else { + bg.set(white); + } + + if(_active_notes[i]) { + tl_shadow.set(white_shade_dark); + br_shadow.set(white_shade_light); + } + else { + tl_shadow.set(white_shade_light); + br_shadow.set(white_shade_dark); + } + + switch(oct_rel) { + case 0: + case 5: + if(i == _view.highest_note()) { + get_path(WHITE_RECT, i, x, y); + render_rect(cr, i, x, y, bg, tl_shadow, br_shadow); + } + else { + get_path(WHITE_CF, i, x, y); + render_cf(cr, i, x, y, bg, tl_shadow, br_shadow); + } + break; + + case 2: + case 7: + case 9: + if(i == _view.highest_note()) { + get_path(WHITE_EB, i, x, y); + render_eb(cr, i, x, y, bg, tl_shadow, br_shadow); + } + else if(i == _view.lowest_note()) { + get_path(WHITE_CF, i, x, y); + render_cf(cr, i, x, y, bg, tl_shadow, br_shadow); + } + else { + get_path(WHITE_DGA, i, x, y); + render_dga(cr, i, x, y, bg, tl_shadow, br_shadow); + } + break; + + case 4: + case 11: + cr->set_source_rgb(0.0f, 0.0f, 0.0f); + get_path(WHITE_SEPARATOR, i, x, y); + create_path(cr, x, y, 0, 1); + cr->stroke(); + + if(i == _view.lowest_note()) { + get_path(WHITE_RECT, i, x, y); + render_rect(cr, i, x, y, bg, tl_shadow, br_shadow); + } + else { + get_path(WHITE_EB, i, x, y); + render_eb(cr, i, x, y, bg, tl_shadow, br_shadow); + } + break; + + default: + break; + + } + break; + + } + + /* render the name of which C this is */ + if(oct_rel == 0) { + std::stringstream s; + double y = floor(_view.note_to_y(i)) - 0.5f; + double note_height = floor(_view.note_to_y(i - 1)) - y; + + int cn = i / 12; + s << "C" << cn; + + //cr->get_text_extents(s.str(), te); + cr->set_source_rgb(0.30f, 0.30f, 0.30f); + cr->move_to(0, y + font_size + (note_height - font_size) / 2.0f); + cr->show_text(s.str()); + } + } + + return true; +} + +bool +PianoRollHeader::on_motion_notify_event (GdkEventMotion* ev) { + int note = _view.y_to_note(ev->y); + + if(_highlighted_note != no_note) { + if(note > _highlighted_note) { + invalidate_note_range(_highlighted_note, note); + } + else { + invalidate_note_range(note, _highlighted_note); + } + + _highlighted_note = note; + } + + /* redraw already taken care of above */ + if(_clicked_note != no_note && _clicked_note != note) { + _active_notes[_clicked_note] = false; + send_note_off(_clicked_note); + + _clicked_note = note; + + if(!_active_notes[note]) { + _active_notes[note] = true; + send_note_on(note); + } + } + + //win->process_updates(false); + + return true; +} + +bool +PianoRollHeader::on_button_press_event (GdkEventButton* ev) { + int note = _view.y_to_note(ev->y); + + if(ev->type == GDK_BUTTON_PRESS && note >= 0 && note < 128) { + if(!_active_notes[note]) { + _active_notes[note] = true; + _clicked_note = note; + send_note_on(note); + + invalidate_note_range(note, note); + } + else { + _clicked_note = no_note; + } + } + + return true; +} + +bool +PianoRollHeader::on_button_release_event (GdkEventButton* ev) { + int note = _view.y_to_note(ev->y); + + if(note == _clicked_note) { + _active_notes[note] = false; + _clicked_note = no_note; + send_note_off(note); + + invalidate_note_range(note, note); + } + + return true; +} + +bool +PianoRollHeader::on_enter_notify_event (GdkEventCrossing* ev) { + _highlighted_note = _view.y_to_note(ev->y); + invalidate_note_range(_highlighted_note, _highlighted_note); + return true; +} + +bool +PianoRollHeader::on_leave_notify_event (GdkEventCrossing*) { + invalidate_note_range(_highlighted_note, _highlighted_note); + + if(_clicked_note != no_note) { + _active_notes[_clicked_note] = false; + send_note_off(_clicked_note); + + if(_clicked_note != _highlighted_note) { + invalidate_note_range(_clicked_note, _clicked_note); + } + + _clicked_note = no_note; + } + + _highlighted_note = no_note; + return true; +} + +bool +PianoRollHeader::on_scroll_event (GdkEventScroll* ev) { + return true; +} + +void +PianoRollHeader::note_range_changed() { + _note_height = floor(_view.note_height()) + 0.5f; + + queue_draw(); + + Glib::RefPtr<Gdk::Window> win = get_window(); + + if(win) { + win->process_updates(false); + } +} + +void +PianoRollHeader::invalidate_note_range(int lowest, int highest) { + Glib::RefPtr<Gdk::Window> win = get_window(); + Gdk::Rectangle rect; + + // the non-rectangular geometry of some of the notes requires more + // redraws than the notes that actually changed. + switch(lowest % 12) { + case 0: + case 5: + lowest = max((int) _view.lowest_note(), lowest); + break; + default: + lowest = max((int) _view.lowest_note(), lowest - 1); + break; + } + + switch(highest % 12) { + case 4: + case 11: + highest = min((int) _view.highest_note(), highest); + break; + case 1: + case 3: + case 6: + case 8: + case 10: + highest = min((int) _view.highest_note(), highest + 1); + break; + default: + highest = min((int) _view.highest_note(), highest + 2); + break; + } + + double y = _view.note_to_y(highest); + double height = _view.note_to_y(lowest - 1) - y; + + rect.set_x(0); + rect.set_width(get_width()); + rect.set_y((int) floor(y)); + rect.set_height((int) floor(height)); + + if(win) { + win->invalidate_rect(rect, false); + } +} + +void +PianoRollHeader::on_size_request(Gtk::Requisition* r) { + r->width = 20; +} + +void +PianoRollHeader::on_size_allocate(Gtk::Allocation& a) { + DrawingArea::on_size_allocate(a); + + _black_note_width = floor(0.7 * get_width()) + 0.5f; +} + +void +PianoRollHeader::send_note_on(uint8_t note) { + boost::shared_ptr<ARDOUR::MidiTrack> track = _view.trackview().midi_track(); + + cerr << "note on: " << (int) note << endl; + + if(track) { + _event[0] = MIDI_CMD_NOTE_ON; + _event[1] = note; + _event[2] = 100; + + track->write_immediate_event(3, _event); + } +} + +void +PianoRollHeader::send_note_off(uint8_t note) { + boost::shared_ptr<ARDOUR::MidiTrack> track = _view.trackview().midi_track(); + + if(track) { + _event[0] = MIDI_CMD_NOTE_OFF; + _event[1] = note; + _event[2] = 100; + + track->write_immediate_event(3, _event); + } +} diff --git a/gtk2_ardour/piano_roll_header.h b/gtk2_ardour/piano_roll_header.h new file mode 100644 index 0000000000..141e7663f5 --- /dev/null +++ b/gtk2_ardour/piano_roll_header.h @@ -0,0 +1,104 @@ +/* + Copyright (C) 2008 Paul Davis + + 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. +*/ + +#ifndef __ardour_piano_roll_header_h__ +#define __ardour_piano_roll_header_h__ + +#include <ardour/types.h> + +#include <gtkmm/drawingarea.h> + +namespace ARDOUR { + class MidiTrack; +} + +class MidiTimeAxisView; +class MidiStreamView; + +class PianoRollHeader : public Gtk::DrawingArea { +public: + PianoRollHeader(MidiStreamView&); + + bool on_expose_event (GdkEventExpose*); + bool on_motion_notify_event (GdkEventMotion*); + bool on_button_press_event (GdkEventButton*); + bool on_button_release_event (GdkEventButton*); + bool on_scroll_event (GdkEventScroll*); + bool on_enter_notify_event (GdkEventCrossing*); + bool on_leave_notify_event (GdkEventCrossing*); + + void on_size_request(Gtk::Requisition*); + void on_size_allocate(Gtk::Allocation& a); + + void note_range_changed(); + + struct Color { + Color(); + Color(double _r, double _g, double _b); + inline void set(const Color& c); + + double r; + double g; + double b; + }; + +private: + static Color white; + static Color white_highlight; + static Color white_shade_light; + static Color white_shade_dark; + static Color black; + static Color black_highlight; + static Color black_shade_light; + static Color black_shade_dark; + + PianoRollHeader(const PianoRollHeader&); + + enum ItemType { + BLACK_SEPARATOR, + BLACK_MIDDLE_SEPARATOR, + BLACK, + WHITE_SEPARATOR, + WHITE_RECT, + WHITE_CF, + WHITE_EB, + WHITE_DGA + }; + + void invalidate_note_range(int lowest, int highest); + + void get_path(ItemType, int note, double x[], double y[]); + + void send_note_on(uint8_t note); + void send_note_off(uint8_t note); + + MidiStreamView& _view; + + ARDOUR::Byte _event[3]; + + Cairo::RefPtr<Cairo::Context> cc; + bool _active_notes[128]; + uint8_t _highlighted_note; + uint8_t _clicked_note; + double _grab_y; + + double _note_height; + double _black_note_width; +}; + +#endif /* __ardour_piano_roll_header_h__ */ diff --git a/gtk2_ardour/time_axis_view.cc b/gtk2_ardour/time_axis_view.cc index 5788daf19d..a707e36993 100644 --- a/gtk2_ardour/time_axis_view.cc +++ b/gtk2_ardour/time_axis_view.cc @@ -148,9 +148,9 @@ TimeAxisView::TimeAxisView (ARDOUR::Session& sess, PublicEditor& ed, TimeAxisVie controls_hbox.pack_start (controls_ebox,true,true); controls_hbox.show (); - controls_frame.add (controls_hbox); - controls_frame.set_name ("TimeAxisViewControlsBaseUnselected"); - controls_frame.set_shadow_type (Gtk::SHADOW_ETCHED_OUT); + //controls_frame.add (controls_hbox); + //controls_frame.set_name ("TimeAxisViewControlsBaseUnselected"); + //controls_frame.set_shadow_type (Gtk::SHADOW_ETCHED_OUT); ColorsChanged.connect (mem_fun (*this, &TimeAxisView::color_handler)); } @@ -193,13 +193,13 @@ TimeAxisView::show_at (double y, int& nth, VBox *parent) effective_height = 0; if (control_parent) { - control_parent->reorder_child (controls_frame, nth); + control_parent->reorder_child (controls_hbox, nth); } else { control_parent = parent; - parent->pack_start (controls_frame, false, false); - parent->reorder_child (controls_frame, nth); + parent->pack_start (controls_hbox, false, false); + parent->reorder_child (controls_hbox, nth); } - controls_frame.show (); + controls_hbox.show (); controls_ebox.show (); /* the coordinates used here are in the system of the @@ -299,10 +299,10 @@ TimeAxisView::hide () } canvas_display->hide(); - controls_frame.hide (); + controls_hbox.hide (); if (control_parent) { - control_parent->remove (controls_frame); + control_parent->remove (controls_hbox); control_parent = 0; } @@ -363,7 +363,7 @@ void TimeAxisView::set_height_pixels (uint32_t h) { height = h; - controls_frame.set_size_request (-1, height + ((order == 0) ? 1 : 0)); + controls_hbox.set_size_request (-1, height + ((order == 0) ? 1 : 0)); //cerr << "TimeAxisView::set_height_pixels() called h = " << h << endl;//DEBUG if (canvas_item_visible (selection_group)) { /* resize the selection rect */ @@ -551,7 +551,7 @@ TimeAxisView::set_selected (bool yn) if (_selected) { controls_ebox.set_name (controls_base_selected_name); - controls_frame.set_name (controls_base_selected_name); + controls_hbox.set_name (controls_base_selected_name); /* propagate any existing selection, if the mode is right */ @@ -561,7 +561,7 @@ TimeAxisView::set_selected (bool yn) } else { controls_ebox.set_name (controls_base_unselected_name); - controls_frame.set_name (controls_base_unselected_name); + controls_hbox.set_name (controls_base_unselected_name); hide_selection (); diff --git a/gtk2_ardour/time_axis_view.h b/gtk2_ardour/time_axis_view.h index baf8327e67..24c91e7ba4 100644 --- a/gtk2_ardour/time_axis_view.h +++ b/gtk2_ardour/time_axis_view.h @@ -309,7 +309,6 @@ class TimeAxisView : public virtual AxisView void set_height_pixels (uint32_t h); void color_handler (); - }; /* class TimeAxisView */ #endif /* __ardour_gtk_time_axis_h__ */ diff --git a/libs/gtkmm2ext/SConscript b/libs/gtkmm2ext/SConscript index 3660106b73..829182c71e 100644 --- a/libs/gtkmm2ext/SConscript +++ b/libs/gtkmm2ext/SConscript @@ -50,6 +50,7 @@ pixfader.cc pixscroller.cc popup.cc prompter.cc +scroomer.cc selector.cc slider_controller.cc stateful_button.cc diff --git a/libs/gtkmm2ext/gtkmm2ext/scroomer.h b/libs/gtkmm2ext/gtkmm2ext/scroomer.h new file mode 100644 index 0000000000..d4f2ce6aa3 --- /dev/null +++ b/libs/gtkmm2ext/gtkmm2ext/scroomer.h @@ -0,0 +1,88 @@ +/* + Copyright (C) 2008 Paul Davis + + 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. + +*/ + +#ifndef __gtkmm2ext_scroomer_h__ +#define __gtkmm2ext_scroomer_h__ + +#include <gtkmm/drawingarea.h> +#include <gtkmm/adjustment.h> +#include <gdkmm.h> + +namespace Gtkmm2ext { + +class Scroomer : public Gtk::DrawingArea +{ +public: + enum Component { + TopBase = 0, + Handle1 = 1, + Slider = 2, + Handle2 = 3, + BottomBase = 4, + Total = 5, + None = 6 + }; + + Scroomer(Gtk::Adjustment& adjustment); + ~Scroomer(); + + bool on_motion_notify_event (GdkEventMotion*); + bool on_button_press_event (GdkEventButton*); + bool on_button_release_event (GdkEventButton*); + bool on_scroll_event (GdkEventScroll*); + virtual void on_size_allocate (Gtk::Allocation&); + + void set_comp_rect(GdkRectangle&, Component) const; + + Component point_in(double point) const; + + void set_min_page_size(double page_size); + int get_handle_size() { return handle_size; } + + inline int position_of(Component comp) { return position[comp]; } + + // debug + std::string get_comp_name(Component); + +protected: + Gtk::Adjustment& adj; + +private: + struct UpdateRect { + GdkRectangle rect; + Component first_comp; + }; + + void update(); + void adjustment_changed (); + + int position[6]; + int old_pos[6]; + int handle_size; + double min_page_size; + GdkWindow* grab_window; + Component grab_comp; + double grab_y; + double unzoomed_val; + double unzoomed_page; +}; + +} // namespace + +#endif /* __gtkmm2ext_scroomer_h__ */ diff --git a/libs/gtkmm2ext/scroomer.cc b/libs/gtkmm2ext/scroomer.cc new file mode 100644 index 0000000000..50c17a46c6 --- /dev/null +++ b/libs/gtkmm2ext/scroomer.cc @@ -0,0 +1,393 @@ +/* + Copyright (C) 2008 Paul Davis + + 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 <iostream> +#include <gtkmm2ext/scroomer.h> + +using namespace Gtkmm2ext; +using namespace Gtk; +using namespace Gdk; +using namespace std; + +Scroomer::Scroomer(Gtk::Adjustment& adjustment) + : adj(adjustment) + , handle_size(0) + , grab_comp(None) { + + position[TopBase] = 0; + position[Handle1] = 0; + position[Slider] = 0; + position[Handle2] = 0; + position[BottomBase] = 0; + position[Total] = 0; + + add_events (Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | + Gdk::SCROLL_MASK); + + adjustment.signal_value_changed().connect (mem_fun (*this, &Scroomer::adjustment_changed)); + //adjustment.signal_changed().connect (mem_fun (*this, &Scroomer::adjustment_changed)); +} + +Scroomer::~Scroomer() { +} + +bool +Scroomer::on_motion_notify_event (GdkEventMotion* ev) { + double range = adj.get_upper() - adj.get_lower(); + double pixel2val = range / get_height(); + double val_at_pointer = ((get_height() - ev->y) * pixel2val) + adj.get_lower(); + double delta_y = ev->y - grab_y; + double half_min_page = min_page_size / 2; + double fract = delta_y / position[Total]; + double scale, temp, zoom; + double val, page; + + if(grab_comp == None || grab_comp == Total) { + return true; + } + + if (ev->window != grab_window) { + grab_y = ev->y; + grab_window = ev->window; + return true; + } + + grab_y = ev->y; + + if (ev->state & GDK_CONTROL_MASK) { + if (ev->state & GDK_MOD1_MASK) { + scale = 0.05; + } else { + scale = 0.1; + } + } else { + scale = 1.0; + } + + fract = min (1.0, fract); + fract = max (-1.0, fract); + fract = -fract; + + switch(grab_comp) { + case TopBase: + case BottomBase: + unzoomed_val += scale * fract * range; + unzoomed_val = min(unzoomed_val, adj.get_upper() - unzoomed_page); + unzoomed_val = max(unzoomed_val, adj.get_lower()); + break; + case Slider: + unzoomed_val += scale * fract * range; + unzoomed_val = min(unzoomed_val, adj.get_upper() - unzoomed_page); + unzoomed_val = max(unzoomed_val, adj.get_lower()); + break; + case Handle1: + unzoomed_page += scale * fract * range; + unzoomed_page = min(unzoomed_page, adj.get_upper() - unzoomed_val); + unzoomed_page = max(unzoomed_page, min_page_size); + break; + case Handle2: + temp = unzoomed_val + unzoomed_page; + unzoomed_val += scale * fract * range; + unzoomed_val = min(unzoomed_val, temp - min_page_size); + unzoomed_val = max(unzoomed_val, adj.get_lower()); + + unzoomed_page = temp - unzoomed_val; + unzoomed_page = max(unzoomed_page, min_page_size); + break; + default: + break; + } + + /* + * Then we handle zoom, which is dragging horizontally. We zoom around the area that is + * the current y pointer value, not from the area that was the start of the drag. + * the point of zoom must have the same + */ + + if(ev->x > get_width()) { + zoom = ev->x - get_width(); + + double higher = unzoomed_val + unzoomed_page - half_min_page - val_at_pointer; + double lower = val_at_pointer - (unzoomed_val + half_min_page); + + higher *= zoom / 128; + lower *= zoom / 128; + + val = unzoomed_val + lower; + page = unzoomed_page - higher - lower; + + page = max(page, min_page_size); + + if(lower < 0) { + val = max(val, val_at_pointer - half_min_page); + } + else if(lower > 0) { + val = min(val, val_at_pointer - half_min_page); + } + + val = min(val, adj.get_upper() - min_page_size); + page = min(page, adj.get_upper() - val); + } + else if (ev->x < 0) { + /* on zoom out increase the page size as well as moving the range towards the mouse pos*/ + zoom = abs(ev->x); + + /*double higher = unzoomed_val + unzoomed_page - half_min_page - val_at_pointer; + double lower = val_at_pointer - (unzoomed_val + half_min_page); + + higher *= zoom / 128; + lower *= zoom / 128; + + val = unzoomed_val + lower; + page = unzoomed_page - higher - lower; + + page = max(page, min_page_size); + + if(lower < 0) { + val = max(val, val_at_pointer - half_min_page); + } + else if(lower > 0) { + val = min(val, val_at_pointer - half_min_page); + } + + val = min(val, adj.get_upper() - min_page_size); + page = min(page, adj.get_upper() - val);*/ + + val = unzoomed_val; + page = unzoomed_page; + } + else { + val = unzoomed_val; + page = unzoomed_page; + } + + adj.set_page_size(page); + + if(val == adj.get_value()) { + adj.value_changed(); + } + if(val < adj.get_lower()) { + adj.value_changed(); + } + else if(val > adj.get_upper()) { + adj.value_changed(); + } + else { + adj.set_value(val); + } + + return true; +} + +bool +Scroomer::on_button_press_event (GdkEventButton* ev) { + if(ev->button == 1) { + Component comp = point_in(ev->y); + + cerr << get_comp_name(comp) << " pressed" << endl; + + if(comp == Total || comp == None) { + return false; + } + + add_modal_grab(); + grab_comp = comp; + grab_y = ev->y; + unzoomed_val = adj.get_value(); + unzoomed_page = adj.get_page_size(); + grab_window = ev->window; + } + return false; +} + +bool +Scroomer::on_button_release_event (GdkEventButton* ev) { + if(grab_comp == None || grab_comp == Total) { + return true; + } + + if (ev->window != grab_window) { + grab_y = ev->y; + grab_window = ev->window; + return true; + } + + if (ev->button != 1) { + return true; + } + + switch(grab_comp) { + case TopBase: + break; + case Handle1: + break; + case Slider: + break; + case Handle2: + break; + case BottomBase: + break; + default: + break; + } + + grab_comp = None; + + remove_modal_grab(); + return true; +} + +bool +Scroomer::on_scroll_event (GdkEventScroll*) { + return true; +} + +void +Scroomer::on_size_allocate (Allocation& a) { + Gtk::DrawingArea::on_size_allocate(a); + + cerr << "allocate" << endl; + + position[Total] = a.get_height(); + set_min_page_size(min_page_size); + update(); +} + +/* + * assumes that x and width are correct, and they will not be altered + */ +void +Scroomer::set_comp_rect(GdkRectangle& r, Component c) const { + int index = (int) c; + + switch(c) { + case None: + return; + case Total: + r.y = 0; + r.height = position[Total]; + break; + default: + r.y = position[index]; + r.height = position[index+1] - position[index]; + break; + } +} + +Scroomer::Component +Scroomer::point_in(double point) const { + for(int i = 0; i < Total; ++i) { + if(position[i+1] >= point) { + return (Component) i; + } + } + + return None; +} + +void +Scroomer::set_min_page_size(double ps) { + double coeff = ((double)position[Total]) / (adj.get_upper() - adj.get_lower()); + + min_page_size = ps; + handle_size = (int) floor((ps * coeff) / 2); +} + +void +Scroomer::update() { + double range = adj.get_upper() - adj.get_lower(); + double value = adj.get_value() - adj.get_lower(); + int height = position[Total]; + double coeff = ((double) height) / range; + + /* save the old positions to calculate update regions later*/ + for(int i = Handle1; i < Total; ++i) { + old_pos[i] = position[i]; + } + + position[BottomBase] = (int) floor(height - (adj.get_value() * coeff)); + position[Handle2] = position[BottomBase] - handle_size; + + position[Handle1] = (int) floor(height - ((adj.get_value() + adj.get_page_size()) * coeff)); + position[Slider] = position[Handle1] + handle_size; +} + +void +Scroomer::adjustment_changed() { + //cerr << floor(adj.get_value()) << " " << floor(adj.get_value() + adj.get_page_size()) << endl; + Gdk::Rectangle rect; + Glib::RefPtr<Gdk::Window> win = get_window(); + + update(); + + if(!win) { + return; + } + + rect.set_x(0); + rect.set_width(get_width()); + + if(position[Handle1] < old_pos[Handle1]) { + rect.set_y(position[Handle1]); + rect.set_height(old_pos[Slider] - position[Handle1]); + win->invalidate_rect(rect, false); + } + else if(position[Handle1] > old_pos[Handle1]) { + rect.set_y(old_pos[Handle1]); + rect.set_height(position[Slider] - old_pos[Handle1]); + win->invalidate_rect(rect, false); + } + + if(position[Handle2] < old_pos[Handle2]) { + rect.set_y(position[Handle2]); + rect.set_height(old_pos[BottomBase] - position[Handle2]); + win->invalidate_rect(rect, false); + } + else if(position[Handle2] > old_pos[Handle2]) { + rect.set_y(old_pos[Handle2]); + rect.set_height(position[BottomBase] - old_pos[Handle2]); + win->invalidate_rect(rect, false); + } + + win->process_updates(false); +} + +std::string +Scroomer::get_comp_name(Component c) { + switch(c) { + case TopBase: + return "TopBase"; + case Handle1: + return "Handle1"; + case Slider: + return "Slider"; + case Handle2: + return "Handle2"; + case BottomBase: + return "BottomBase"; + case Total: + return "Total"; + case None: + return "None"; + default: + return "ERROR"; + } +} |