13
0
livetrax/libs/gtkmm2ext/pixfader.cc
David Robillard 587dc283ea Fix stuck faders on very fine adjustments.
I attempted to preserve the "don't draw unless different" by ditching rounding for more
precise display_span, but that didn't work.  An alternative solution would be
to draw on adjustment change if there's text, since then we need to redraw
regardless of slider position, but it seemed weird even just with respect to
the slider, so I opted for this, which really definitely redraws when the
adjustment changes, period.

If this proves to be a performance issue we'll have to figure that out.
2014-12-05 15:19:43 -05:00

720 lines
19 KiB
C++

/*
Copyright (C) 2006 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.
$Id: fastmeter.h 570 2006-06-07 21:21:21Z sampo $
*/
#include <iostream>
#include <assert.h>
#include "pbd/stacktrace.h"
#include "gtkmm2ext/cairo_widget.h"
#include "gtkmm2ext/keyboard.h"
#include "gtkmm2ext/pixfader.h"
#include "gtkmm2ext/utils.h"
using namespace Gtkmm2ext;
using namespace Gtk;
using namespace std;
#define CORNER_RADIUS 2.5
#define CORNER_SIZE 2
#define CORNER_OFFSET 1
#define FADER_RESERVE 6
std::list<PixFader::FaderImage*> PixFader::_patterns;
PixFader::PixFader (Gtk::Adjustment& adj, int orientation, int fader_length, int fader_girth)
: _layout (0)
, _tweaks (Tweaks(0))
, _adjustment (adj)
, _text_width (0)
, _text_height (0)
, _span (fader_length)
, _girth (fader_girth)
, _min_span (fader_length)
, _min_girth (fader_girth)
, _orien (orientation)
, _pattern (0)
, _hovering (false)
, _dragging (false)
, _centered_text (true)
, _current_parent (0)
{
_default_value = _adjustment.get_value();
update_unity_position ();
add_events (
Gdk::BUTTON_PRESS_MASK
| Gdk::BUTTON_RELEASE_MASK
| Gdk::POINTER_MOTION_MASK
| Gdk::SCROLL_MASK
| Gdk::ENTER_NOTIFY_MASK
| Gdk::LEAVE_NOTIFY_MASK
);
_adjustment.signal_value_changed().connect (mem_fun (*this, &PixFader::adjustment_changed));
_adjustment.signal_changed().connect (mem_fun (*this, &PixFader::adjustment_changed));
if (_orien == VERT) {
CairoWidget::set_size_request(_girth, _span);
} else {
CairoWidget::set_size_request(_span, _girth);
}
}
PixFader::~PixFader ()
{
if (_parent_style_change) _parent_style_change.disconnect();
if (_layout) _layout.clear (); // drop reference to existing layout
}
cairo_pattern_t*
PixFader::find_pattern (double afr, double afg, double afb,
double abr, double abg, double abb,
int w, int h)
{
for (list<FaderImage*>::iterator f = _patterns.begin(); f != _patterns.end(); ++f) {
if ((*f)->matches (afr, afg, afb, abr, abg, abb, w, h)) {
return (*f)->pattern;
}
}
return 0;
}
void
PixFader::create_patterns ()
{
Gdk::Color c = get_style()->get_fg (get_state());
float fr, fg, fb;
float br, bg, bb;
fr = c.get_red_p ();
fg = c.get_green_p ();
fb = c.get_blue_p ();
c = get_style()->get_bg (get_state());
br = c.get_red_p ();
bg = c.get_green_p ();
bb = c.get_blue_p ();
cairo_surface_t* surface;
cairo_t* tc = 0;
if (get_width() <= 1 || get_height() <= 1) {
return;
}
if ((_pattern = find_pattern (fr, fg, fb, br, bg, bb, get_width(), get_height())) != 0) {
/* found it - use it */
return;
}
if (_orien == VERT) {
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, get_width(), get_height() * 2.0);
tc = cairo_create (surface);
/* paint background + border */
cairo_pattern_t* shade_pattern = cairo_pattern_create_linear (0.0, 0.0, get_width(), 0);
cairo_pattern_add_color_stop_rgba (shade_pattern, 0, br*0.4,bg*0.4,bb*0.4, 1.0);
cairo_pattern_add_color_stop_rgba (shade_pattern, 0.25, br*0.6,bg*0.6,bb*0.6, 1.0);
cairo_pattern_add_color_stop_rgba (shade_pattern, 1, br*0.8,bg*0.8,bb*0.8, 1.0);
cairo_set_source (tc, shade_pattern);
cairo_rectangle (tc, 0, 0, get_width(), get_height() * 2.0);
cairo_fill (tc);
cairo_pattern_destroy (shade_pattern);
/* paint lower shade */
shade_pattern = cairo_pattern_create_linear (0.0, 0.0, get_width() - 2 - CORNER_OFFSET , 0);
cairo_pattern_add_color_stop_rgba (shade_pattern, 0, fr*0.8,fg*0.8,fb*0.8, 1.0);
cairo_pattern_add_color_stop_rgba (shade_pattern, 1, fr*0.6,fg*0.6,fb*0.6, 1.0);
cairo_set_source (tc, shade_pattern);
Gtkmm2ext::rounded_top_half_rectangle (tc, CORNER_OFFSET, get_height() + CORNER_OFFSET,
get_width() - CORNER_SIZE, get_height(), CORNER_RADIUS);
cairo_fill (tc);
cairo_pattern_destroy (shade_pattern);
_pattern = cairo_pattern_create_for_surface (surface);
} else {
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, get_width() * 2.0, get_height());
tc = cairo_create (surface);
/* paint right shade (background section)*/
cairo_pattern_t* shade_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, get_height());
cairo_pattern_add_color_stop_rgba (shade_pattern, 0, br*0.4,bg*0.4,bb*0.4, 1.0);
cairo_pattern_add_color_stop_rgba (shade_pattern, 0.25, br*0.6,bg*0.6,bb*0.6, 1.0);
cairo_pattern_add_color_stop_rgba (shade_pattern, 1, br*0.8,bg*0.8,bb*0.8, 1.0);
cairo_set_source (tc, shade_pattern);
cairo_rectangle (tc, 0, 0, get_width() * 2.0, get_height());
cairo_fill (tc);
/* paint left shade (active section/foreground) */
shade_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, get_height());
cairo_pattern_add_color_stop_rgba (shade_pattern, 0, fr*0.8,fg*0.8,fb*0.8, 1.0);
cairo_pattern_add_color_stop_rgba (shade_pattern, 1, fr*0.6,fg*0.6,fb*0.6, 1.0);
cairo_set_source (tc, shade_pattern);
Gtkmm2ext::rounded_right_half_rectangle (tc, CORNER_OFFSET, CORNER_OFFSET,
get_width() - CORNER_OFFSET, get_height() - CORNER_SIZE, CORNER_RADIUS);
cairo_fill (tc);
cairo_pattern_destroy (shade_pattern);
_pattern = cairo_pattern_create_for_surface (surface);
}
/* cache it for others to use */
_patterns.push_back (new FaderImage (_pattern, fr, fg, fb, br, bg, bb, get_width(), get_height()));
cairo_destroy (tc);
cairo_surface_destroy (surface);
}
void
PixFader::render (cairo_t *cr, cairo_rectangle_t* area)
{
if (!_pattern) {
create_patterns();
}
if (!_pattern) {
/* this isn't supposed to be happen, but some wackiness whereby
* the pixfader ends up with a 1xN or Nx1 size allocation
* leads to it. the basic wackiness needs fixing but we
* shouldn't crash. just fill in the expose area with
* our bg color.
*/
CairoWidget::set_source_rgb_a (cr, get_style()->get_bg (get_state()), 1);
cairo_rectangle (cr, area->x, area->y, area->width, area->height);
cairo_fill (cr);
return;
}
OnExpose();
int ds = display_span ();
const float w = get_width();
const float h = get_height();
CairoWidget::set_source_rgb_a (cr, get_parent_bg(), 1);
cairo_rectangle (cr, 0, 0, w, h);
cairo_fill(cr);
cairo_set_line_width (cr, 2);
cairo_set_source_rgba (cr, 0, 0, 0, 1.0);
cairo_matrix_t matrix;
Gtkmm2ext::rounded_rectangle (cr, CORNER_OFFSET, CORNER_OFFSET, w-CORNER_SIZE, h-CORNER_SIZE, CORNER_RADIUS);
// we use a 'trick' here: The stoke is off by .5px but filling the interior area
// after a stroke of 2px width results in an outline of 1px
cairo_stroke_preserve(cr);
if (_orien == VERT) {
if (ds > h - FADER_RESERVE - CORNER_OFFSET) {
ds = h - FADER_RESERVE - CORNER_OFFSET;
}
if (!CairoWidget::flat_buttons() ) {
cairo_set_source (cr, _pattern);
cairo_matrix_init_translate (&matrix, 0, (h - ds));
cairo_pattern_set_matrix (_pattern, &matrix);
} else {
CairoWidget::set_source_rgb_a (cr, get_style()->get_bg (get_state()), 1);
cairo_fill (cr);
CairoWidget::set_source_rgb_a (cr, get_style()->get_fg (get_state()), 1);
Gtkmm2ext::rounded_rectangle (cr, CORNER_OFFSET, ds + CORNER_OFFSET,
w - CORNER_SIZE, h - ds - CORNER_SIZE, CORNER_RADIUS);
}
cairo_fill (cr);
} else {
if (ds < FADER_RESERVE) {
ds = FADER_RESERVE;
}
assert(ds <= w);
/*
* if ds == w, the pattern does not need to be translated
* if ds == 0 (or FADER_RESERVE), the pattern needs to be moved
* w to the left, which is -w in pattern space, and w in user space
* if ds == 10, then the pattern needs to be moved w - 10
* to the left, which is -(w-10) in pattern space, which
* is (w - 10) in user space
* thus: translation = (w - ds)
*/
if (!CairoWidget::flat_buttons() ) {
cairo_set_source (cr, _pattern);
cairo_matrix_init_translate (&matrix, w - ds, 0);
cairo_pattern_set_matrix (_pattern, &matrix);
} else {
CairoWidget::set_source_rgb_a (cr, get_style()->get_bg (get_state()), 1);
cairo_fill (cr);
CairoWidget::set_source_rgb_a (cr, get_style()->get_fg (get_state()), 1);
Gtkmm2ext::rounded_rectangle (cr, CORNER_OFFSET, CORNER_OFFSET,
ds - CORNER_SIZE, h - CORNER_SIZE, CORNER_RADIUS);
}
cairo_fill (cr);
}
/* draw the unity-position line if it's not at either end*/
if (!(_tweaks & NoShowUnityLine) && _unity_loc > CORNER_RADIUS) {
cairo_set_line_width(cr, 1);
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
Gdk::Color c = get_style()->get_fg (Gtk::STATE_ACTIVE);
cairo_set_source_rgba (cr, c.get_red_p() * 1.5, c.get_green_p() * 1.5, c.get_blue_p() * 1.5, 0.85);
if (_orien == VERT) {
if (_unity_loc < h - CORNER_RADIUS) {
cairo_move_to (cr, 1.5, _unity_loc + CORNER_OFFSET + .5);
cairo_line_to (cr, _girth - 1.5, _unity_loc + CORNER_OFFSET + .5);
cairo_stroke (cr);
}
} else {
if (_unity_loc < w - CORNER_RADIUS) {
cairo_move_to (cr, _unity_loc - CORNER_OFFSET + .5, 1.5);
cairo_line_to (cr, _unity_loc - CORNER_OFFSET + .5, _girth - 1.5);
cairo_stroke (cr);
}
}
}
if (_layout && !_text.empty() && _orien == HORIZ) {
cairo_save (cr);
if (_centered_text) {
/* center text */
cairo_move_to (cr, (w - _text_width)/2.0, h/2.0 - _text_height/2.0);
} else if (ds > .5 * w) {
cairo_move_to (cr, CORNER_OFFSET + 3, h/2.0 - _text_height/2.0);
cairo_set_operator(cr, CAIRO_OPERATOR_XOR);
} else {
cairo_move_to (cr, w - _text_width - CORNER_OFFSET - 3, h/2.0 - _text_height/2.0);
}
CairoWidget::set_source_rgb_a (cr, get_style()->get_text (get_state()), 1);
pango_cairo_show_layout (cr, _layout->gobj());
cairo_restore (cr);
}
if (!get_sensitive()) {
Gtkmm2ext::rounded_rectangle (cr, CORNER_OFFSET, CORNER_OFFSET, w-CORNER_SIZE, h-CORNER_SIZE, CORNER_RADIUS);
cairo_set_source_rgba (cr, 0.505, 0.517, 0.525, 0.4);
cairo_fill (cr);
} else if (_hovering) {
Gtkmm2ext::rounded_rectangle (cr, CORNER_OFFSET, CORNER_OFFSET, w-CORNER_SIZE, h-CORNER_SIZE, CORNER_RADIUS);
cairo_set_source_rgba (cr, 0.905, 0.917, 0.925, 0.1);
cairo_fill (cr);
}
}
void
PixFader::on_size_request (GtkRequisition* req)
{
if (_orien == VERT) {
req->width = (_min_girth ? _min_girth : -1);
req->height = (_min_span ? _min_span : -1);
} else {
req->height = (_min_girth ? _min_girth : -1);
req->width = (_min_span ? _min_span : -1);
}
}
void
PixFader::on_size_allocate (Gtk::Allocation& alloc)
{
CairoWidget::on_size_allocate(alloc);
if (_orien == VERT) {
_girth = alloc.get_width ();
_span = alloc.get_height ();
} else {
_girth = alloc.get_height ();
_span = alloc.get_width ();
}
if (is_realized()) {
/* recreate patterns in case we've changed size */
create_patterns ();
}
update_unity_position ();
}
bool
PixFader::on_button_press_event (GdkEventButton* ev)
{
if (ev->type != GDK_BUTTON_PRESS) {
if (_dragging) {
remove_modal_grab();
_dragging = false;
gdk_pointer_ungrab (GDK_CURRENT_TIME);
StopGesture ();
}
return (_tweaks & NoButtonForward) ? true : false;
}
if (ev->button != 1 && ev->button != 2) {
return false;
}
add_modal_grab ();
StartGesture ();
_grab_loc = (_orien == VERT) ? ev->y : ev->x;
_grab_start = (_orien == VERT) ? ev->y : ev->x;
_grab_window = ev->window;
_dragging = true;
gdk_pointer_grab(ev->window,false,
GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
NULL,NULL,ev->time);
if (ev->button == 2) {
set_adjustment_from_event (ev);
}
return (_tweaks & NoButtonForward) ? true : false;
}
bool
PixFader::on_button_release_event (GdkEventButton* ev)
{
double ev_pos = (_orien == VERT) ? ev->y : ev->x;
switch (ev->button) {
case 1:
if (_dragging) {
remove_modal_grab();
_dragging = false;
gdk_pointer_ungrab (GDK_CURRENT_TIME);
StopGesture ();
if (!_hovering) {
if (!(_tweaks & NoVerticalScroll)) {
Keyboard::magic_widget_drop_focus();
}
queue_draw ();
}
if (ev_pos == _grab_start) {
/* no motion - just a click */
ev_pos = rint(ev_pos);
if (ev->state & Keyboard::TertiaryModifier) {
_adjustment.set_value (_default_value);
} else if (ev->state & Keyboard::GainFineScaleModifier) {
_adjustment.set_value (_adjustment.get_lower());
#if 0 // ignore clicks
} else if (ev_pos == slider_pos) {
; // click on current position, no move.
} else if ((_orien == VERT && ev_pos < slider_pos) || (_orien == HORIZ && ev_pos > slider_pos)) {
/* above the current display height, remember X Window coords */
_adjustment.set_value (_adjustment.get_value() + _adjustment.get_step_increment());
} else {
_adjustment.set_value (_adjustment.get_value() - _adjustment.get_step_increment());
#endif
}
}
return true;
}
break;
case 2:
if (_dragging) {
remove_modal_grab();
_dragging = false;
StopGesture ();
set_adjustment_from_event (ev);
gdk_pointer_ungrab (GDK_CURRENT_TIME);
return true;
}
break;
default:
break;
}
return false;
}
bool
PixFader::on_scroll_event (GdkEventScroll* ev)
{
double scale;
bool ret = false;
if (ev->state & Keyboard::GainFineScaleModifier) {
if (ev->state & Keyboard::GainExtraFineScaleModifier) {
scale = 0.005;
} else {
scale = 0.1;
}
} else {
scale = 1.0;
}
if (_orien == VERT) {
switch (ev->direction) {
case GDK_SCROLL_UP:
_adjustment.set_value (_adjustment.get_value() + (_adjustment.get_page_increment() * scale));
ret = true;
break;
case GDK_SCROLL_DOWN:
_adjustment.set_value (_adjustment.get_value() - (_adjustment.get_page_increment() * scale));
ret = true;
break;
default:
break;
}
} else {
int dir = ev->direction;
if (ev->state & Keyboard::ScrollHorizontalModifier || !(_tweaks & NoVerticalScroll)) {
if (ev->direction == GDK_SCROLL_UP) dir = GDK_SCROLL_RIGHT;
if (ev->direction == GDK_SCROLL_DOWN) dir = GDK_SCROLL_LEFT;
}
switch (dir) {
case GDK_SCROLL_RIGHT:
_adjustment.set_value (_adjustment.get_value() + (_adjustment.get_page_increment() * scale));
ret = true;
break;
case GDK_SCROLL_LEFT:
_adjustment.set_value (_adjustment.get_value() - (_adjustment.get_page_increment() * scale));
ret = true;
break;
default:
break;
}
}
return ret;
}
bool
PixFader::on_motion_notify_event (GdkEventMotion* ev)
{
if (_dragging) {
double scale = 1.0;
double const ev_pos = (_orien == VERT) ? ev->y : ev->x;
if (ev->window != _grab_window) {
_grab_loc = ev_pos;
_grab_window = ev->window;
return true;
}
if (ev->state & Keyboard::GainFineScaleModifier) {
if (ev->state & Keyboard::GainExtraFineScaleModifier) {
scale = 0.005;
} else {
scale = 0.1;
}
}
double const delta = ev_pos - _grab_loc;
_grab_loc = ev_pos;
const double off = FADER_RESERVE + ((_orien == VERT) ? CORNER_OFFSET : 0);
const double span = _span - off;
double fract = (delta / span);
fract = min (1.0, fract);
fract = max (-1.0, fract);
// X Window is top->bottom for 0..Y
if (_orien == VERT) {
fract = -fract;
}
_adjustment.set_value (_adjustment.get_value() + scale * fract * (_adjustment.get_upper() - _adjustment.get_lower()));
}
return true;
}
void
PixFader::adjustment_changed ()
{
queue_draw ();
}
/** @return pixel offset of the current value from the right or bottom of the fader */
int
PixFader::display_span ()
{
float fract = (_adjustment.get_value () - _adjustment.get_lower()) / ((_adjustment.get_upper() - _adjustment.get_lower()));
int ds;
if (_orien == VERT) {
const double off = FADER_RESERVE + CORNER_OFFSET;
const double span = _span - off;
ds = (int)rint (span * (1.0 - fract));
} else {
const double off = FADER_RESERVE;
const double span = _span - off;
ds = (int)rint (span * fract + off);
}
return ds;
}
void
PixFader::update_unity_position ()
{
if (_orien == VERT) {
const double span = _span - FADER_RESERVE - CORNER_OFFSET;
_unity_loc = (int) rint (span * (1 - ((_default_value - _adjustment.get_lower()) / (_adjustment.get_upper() - _adjustment.get_lower())))) - 1;
} else {
const double span = _span - FADER_RESERVE;
_unity_loc = (int) rint (FADER_RESERVE + (_default_value - _adjustment.get_lower()) * span / (_adjustment.get_upper() - _adjustment.get_lower()));
}
queue_draw ();
}
bool
PixFader::on_enter_notify_event (GdkEventCrossing*)
{
_hovering = true;
if (!(_tweaks & NoVerticalScroll)) {
Keyboard::magic_widget_grab_focus ();
}
queue_draw ();
return false;
}
bool
PixFader::on_leave_notify_event (GdkEventCrossing*)
{
if (!_dragging) {
_hovering = false;
if (!(_tweaks & NoVerticalScroll)) {
Keyboard::magic_widget_drop_focus();
}
queue_draw ();
}
return false;
}
void
PixFader::set_adjustment_from_event (GdkEventButton* ev)
{
const double off = FADER_RESERVE + ((_orien == VERT) ? CORNER_OFFSET : 0);
const double span = _span - off;
double fract = (_orien == VERT) ? (1.0 - ((ev->y - off) / span)) : ((ev->x - off) / span);
fract = min (1.0, fract);
fract = max (0.0, fract);
_adjustment.set_value (fract * (_adjustment.get_upper () - _adjustment.get_lower ()));
}
void
PixFader::set_default_value (float d)
{
_default_value = d;
update_unity_position ();
}
void
PixFader::set_tweaks (Tweaks t)
{
bool need_redraw = false;
if ((_tweaks & NoShowUnityLine) ^ (t & NoShowUnityLine)) {
need_redraw = true;
}
_tweaks = t;
if (need_redraw) {
queue_draw();
}
}
void
PixFader::set_text (const std::string& str, bool centered, bool expose)
{
if (_layout && _text == str) {
return;
}
if (!_layout && !str.empty()) {
_layout = Pango::Layout::create (get_pango_context());
}
_text = str;
_centered_text = centered;
if (_layout) {
_layout->set_text (str);
_layout->get_pixel_size (_text_width, _text_height);
// queue_resize ();
if (expose) queue_draw ();
}
}
void
PixFader::on_state_changed (Gtk::StateType old_state)
{
Widget::on_state_changed (old_state);
create_patterns ();
queue_draw ();
}
void
PixFader::on_style_changed (const Glib::RefPtr<Gtk::Style>&)
{
if (_layout) {
std::string txt = _layout->get_text();
_layout.clear (); // drop reference to existing layout
_text = "";
set_text (txt, _centered_text, false);
}
/* patterns are cached and re-created as needed
* during 'expose' in the GUI thread */
_pattern = 0;
queue_draw ();
}
Gdk::Color
PixFader::get_parent_bg ()
{
Widget* parent = get_parent ();
while (parent) {
if (parent->get_has_window()) {
break;
}
parent = parent->get_parent();
}
if (parent && parent->get_has_window()) {
if (_current_parent != parent) {
if (_parent_style_change) _parent_style_change.disconnect();
_current_parent = parent;
_parent_style_change = parent->signal_style_changed().connect (mem_fun (*this, &PixFader::on_style_changed));
}
return parent->get_style ()->get_bg (parent->get_state());
}
return get_style ()->get_bg (get_state());
}