Revision fb2e489ace36782a23246a4b3fa6655375b1c23a authored by Ulrich Pegelow on 23 November 2012, 14:58:46 UTC, committed by Ulrich Pegelow on 23 November 2012, 14:58:46 UTC
they are heavily outdated. for the time being the english draft version must do.
1 parent 8ea5e74
Raw File
bauhaus.c
/*
    This file is part of darktable,
    copyright (c) 2012 johannes hanika.

    darktable 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 3 of the License, or
    (at your option) any later version.

    darktable 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 darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "bauhaus/bauhaus.h"
#include "control/conf.h"
#include "common/darktable.h"
#include "develop/develop.h"
#include "gui/gtk.h"

#include <math.h>
#include <pango/pangocairo.h>

// define this to get boxes instead of arrow indicators, more like our previous version was:
// #define DT_BAUHAUS_OLD

// new type dt_bauhaus_widget_t, gtk functions start with dt_bh (so they don't collide with ours), we inherit from drawing area
G_DEFINE_TYPE (DtBauhausWidget, dt_bh, GTK_TYPE_DRAWING_AREA);

// fwd declare
static void dt_bauhaus_hide_popup();
static void dt_bauhaus_show_popup(dt_bauhaus_widget_t *w);
static gboolean
dt_bauhaus_popup_expose(GtkWidget *widget, GdkEventExpose *event, gpointer user_data);
static gboolean
dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data);
static void
dt_bauhaus_widget_accept(dt_bauhaus_widget_t *w);
static void
dt_bauhaus_widget_reject(dt_bauhaus_widget_t *w);
static gboolean
dt_bauhaus_root_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data);
static gboolean
dt_bauhaus_root_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data);


static int
get_line_space()
{
  return darktable.bauhaus->scale * darktable.bauhaus->line_space;
}

static int
get_line_height()
{
  return darktable.bauhaus->scale * darktable.bauhaus->line_height;
}

#ifndef DT_BAUHAUS_OLD
static float
get_marker_size()
{
  // will be fraction of the height, so doesn't depend on scale itself.
  return darktable.bauhaus->marker_size;
}
#endif

// TODO: remove / make use of the pango font size / X height
static float
get_label_font_size()
{
  return get_line_height() * darktable.bauhaus->label_font_size;
}

// TODO: remove / make use of the pango font size / X height
static float
get_value_font_size()
{
  return get_line_height() * darktable.bauhaus->value_font_size;
}

static void
set_value_font(cairo_t *cr)
{
  cairo_select_font_face (cr, darktable.bauhaus->value_font, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size (cr, get_value_font_size());
}

static void
set_bg_normal(cairo_t *cr)
{
  cairo_set_source_rgb(cr,
                       darktable.bauhaus->bg_normal,
                       darktable.bauhaus->bg_normal,
                       darktable.bauhaus->bg_normal);
}

static void
set_bg_focus(cairo_t *cr)
{
  cairo_set_source_rgb(cr,
                       darktable.bauhaus->bg_focus,
                       darktable.bauhaus->bg_focus,
                       darktable.bauhaus->bg_focus);
}

static void
set_text_color(cairo_t *cr, int sensitive)
{
  if(sensitive)
    cairo_set_source_rgb(cr,
                         darktable.bauhaus->text,
                         darktable.bauhaus->text,
                         darktable.bauhaus->text);
  else
    cairo_set_source_rgba(cr,
                          darktable.bauhaus->text,
                          darktable.bauhaus->text,
                          darktable.bauhaus->text,
                          darktable.bauhaus->insensitive);
}

static void
set_grid_color(cairo_t *cr, int sensitive)
{
  if(sensitive)
    cairo_set_source_rgb(cr,
                         darktable.bauhaus->grid,
                         darktable.bauhaus->grid,
                         darktable.bauhaus->grid);
  else
    cairo_set_source_rgba(cr,
                          darktable.bauhaus->grid,
                          darktable.bauhaus->grid,
                          darktable.bauhaus->grid,
                          darktable.bauhaus->insensitive);
}

static void
set_indicator_color(cairo_t *cr, int sensitive)
{
  if(sensitive)
    cairo_set_source_rgb(cr,
                         darktable.bauhaus->indicator,
                         darktable.bauhaus->indicator,
                         darktable.bauhaus->indicator);
  else
    cairo_set_source_rgba(cr,
                          darktable.bauhaus->indicator,
                          darktable.bauhaus->indicator,
                          darktable.bauhaus->indicator,
                          darktable.bauhaus->insensitive);
}

static int
show_pango_text(
    cairo_t *cr,
    char *text,
    float x_pos,
    float y_pos,
    float max_width,
    gboolean right_aligned,
    gboolean sensitive,
    gboolean indicator)
{
  PangoLayout *layout;

  layout = pango_cairo_create_layout(cr);
  pango_layout_set_text(layout, text, -1);
  pango_layout_set_font_description(layout, darktable.bauhaus->pango_font_desc);
  pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);

  int pango_width, pango_height;
  pango_layout_get_size (layout, &pango_width, &pango_height);
  float wd = pango_width/(float)PANGO_SCALE;
  float ht = pango_height/(float)PANGO_SCALE;

  if(right_aligned)
    x_pos -= wd;

  cairo_save(cr);
  if(sensitive)
    set_text_color(cr, sensitive);
  if(indicator)
    set_indicator_color(cr, 1);
  if(max_width > 0)
  {
    cairo_move_to(cr, x_pos+wd-max_width, ht*.5);
    cairo_line_to(cr, x_pos+wd-max_width+ht*.5f, ht+1);
    cairo_line_to(cr, x_pos+wd+1, ht+1);
    cairo_line_to(cr, x_pos+wd+1, -1);
    cairo_line_to(cr, x_pos+wd-max_width+ht*.5f, -1);
    cairo_close_path(cr);
    // cairo_rectangle(cr, x_pos+wd-max_width, -50, max_width, 100.0);
    cairo_clip(cr);
    cairo_new_path(cr);
  }
  cairo_move_to(cr, x_pos, y_pos);
  pango_cairo_show_layout(cr, layout);
  g_object_unref(layout);
  cairo_restore(cr);
  return pango_width/(float)PANGO_SCALE;
}

// -------------------------------
static void
dt_bauhaus_slider_set_normalized(dt_bauhaus_widget_t *w, float pos);

static float
get_slider_line_offset(float pos, float scale, float x, float y, float ht, const int width)
{
  // ht is in [0,1] scale here
  const float l = 4.0f/width;
  const float r = 1.0f-4.0f/width - ht;

  float offset = 0.0f;
  // handle linear startup and rescale y to fit the whole range again
  if(y < ht)
  {
    offset = (x-l)/(r-l) - pos;
  }
  else
  {
    y -= ht;
    y /= (1.0f-ht);

    // x = y^2 * .5(1+off/scale) + (1-y^2)*(l + (pos+off)*(r-l))
    // now find off given pos, y, and x:
    // x - y^2*.5 - (1-y^2)*pos = y^2*.5f*off/scale + (1-y^2)off
    //                          = off ((.5f/scale-1)*y^2 + 1)
    // return (x - y*y*.5f - (1.0f-y*y)*pos)/((.5f/scale-1.f)*y*y + 1.0f);

    offset = (x - y*y*.5f - (1.0f-y*y)*(l + pos*(r-l)))/(.5f*y*y/scale + (1.0f-y*y)*(r-l));
  }
  // clamp to result in a [0,1] range:
  if(pos + offset > 1.0f) offset = 1.0f-pos;
  if(pos + offset < 0.0f) offset = -pos;
  return offset;
}

// draw a loupe guideline for the quadratic zoom in in the slider interface:
static void
draw_slider_line(cairo_t *cr, float pos, float off, float scale, const int width, const int height, const int ht)
{
  // pos is normalized position [0,1], offset is on that scale.
  // ht is in pixels here
  const float l = 4.0f/width;
  const float r = 1.0f-(ht+4.0f)/width;

  const int steps = 64;
  cairo_move_to(cr, width*(l + (pos+off)*(r-l)), ht*.7f);
  cairo_line_to(cr, width*(l + (pos+off)*(r-l)), ht);
  for(int j=1; j<steps; j++)
  {
    const float y = j/(steps-1.0f);
    const float x = y*y*.5f*(1.f+off/scale) + (1.0f-y*y)*(l + (pos+off)*(r-l));
    cairo_line_to(cr, x*width, ht + y*(height-ht));
  }
}
// -------------------------------

// handlers on the root window, to close popup:
static gboolean
dt_bauhaus_root_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
{
  if(darktable.bauhaus->current)
  {
    const float tol = 100;
    gint wx, wy;
    GtkWidget *widget = darktable.bauhaus->popup_window;
    gdk_window_get_origin (gtk_widget_get_window (widget), &wx, &wy);
    if(event->x_root > wx + widget->allocation.width + tol || event->y_root > wy + widget->allocation.height + tol ||
        event->x_root < (int)wx - tol || event->y_root < (int)wy - tol)
    {
      dt_bauhaus_widget_reject(darktable.bauhaus->current);
      dt_bauhaus_hide_popup();
    }
    return TRUE;
  }
  // make sure to propagate the event further
  return FALSE;
}

static gboolean
dt_bauhaus_root_button_press(GtkWidget *wd, GdkEventButton *event, gpointer user_data)
{
  if(darktable.bauhaus->current)
  {
    const float tol = 100;
    gint wx, wy;
    GtkWidget *widget = darktable.bauhaus->popup_window;
    gdk_window_get_origin (gtk_widget_get_window (widget), &wx, &wy);
    if((event->x_root > wx + widget->allocation.width + tol || event->y_root > wy + widget->allocation.height + tol ||
        event->x_root < (int)wx - tol || event->y_root < (int)wy - tol))
    {
      dt_bauhaus_widget_reject(darktable.bauhaus->current);
      dt_bauhaus_hide_popup();
      return TRUE;
    }
  }
  // make sure to propagate the event further
  return FALSE;
}

static void
combobox_popup_scroll(int up)
{
  gint wx, wy;
  GtkWidget *w = GTK_WIDGET(darktable.bauhaus->current);
  const int ht = w->allocation.height;
  const int skip = ht + get_line_space();
  gdk_window_get_origin (gtk_widget_get_window (w), &wx, &wy);
  dt_bauhaus_combobox_data_t *d = &darktable.bauhaus->current->data.combobox;
  if(up)
    dt_bauhaus_combobox_set(w, CLAMP(d->active-1, 0, d->num_labels-1));
  else
    dt_bauhaus_combobox_set(w, CLAMP(d->active+1, 0, d->num_labels-1));
  gdk_window_move(gtk_widget_get_window(darktable.bauhaus->popup_window), wx, wy - d->active * skip);
  // make sure highlighted entry is updated:
  darktable.bauhaus->mouse_x = 0;
  darktable.bauhaus->mouse_y = d->active * skip + ht/2;
  gtk_widget_queue_draw(darktable.bauhaus->popup_area);
}


static gboolean
dt_bauhaus_popup_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
{
  gtk_widget_queue_draw(darktable.bauhaus->popup_area);
  dt_bauhaus_widget_t *w = darktable.bauhaus->current;
  switch(w->type)
  {
    case DT_BAUHAUS_COMBOBOX:
      combobox_popup_scroll(event->direction == GDK_SCROLL_UP);
      break;
    case DT_BAUHAUS_SLIDER:
      break;
    default:
      break;
  }
  return TRUE;
}

static gboolean
dt_bauhaus_popup_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
{
  gtk_widget_queue_draw(darktable.bauhaus->popup_area);
  dt_bauhaus_widget_t *w = darktable.bauhaus->current;
  int width = darktable.bauhaus->popup_window->allocation.width, height = darktable.bauhaus->popup_window->allocation.height;
  // coordinate transform is in vain because we're only ever called after a button release.
  // that means the system is always the one of the popup.
  // that also means that we can't have hovering combobox entries while still holding the button. :(
  const float ex = event->x;
  const float ey = event->y;

  switch(w->type)
  {
    case DT_BAUHAUS_COMBOBOX:
      darktable.bauhaus->mouse_x = ex;
      darktable.bauhaus->mouse_y = ey;
      break;
    case DT_BAUHAUS_SLIDER:
    {
      dt_bauhaus_slider_data_t *d = &w->data.slider;
      const float mouse_off = get_slider_line_offset(
                                d->oldpos, d->scale, ex/width,
                                ey/height, GTK_WIDGET(w)->allocation.height/(float)height,
                                widget->allocation.width);
      if(!darktable.bauhaus->change_active)
      {
        if((darktable.bauhaus->mouse_line_distance < 0 && mouse_off > 0) ||
            (darktable.bauhaus->mouse_line_distance > 0 && mouse_off < 0))
          darktable.bauhaus->change_active = 1;
        darktable.bauhaus->mouse_line_distance = mouse_off;
      }
      if(darktable.bauhaus->change_active)
      {
        // remember mouse position for motion effects in draw
        darktable.bauhaus->mouse_x = ex;
        darktable.bauhaus->mouse_y = ey;
        dt_bauhaus_slider_set_normalized(w, d->oldpos + mouse_off);
      }
    }
    break;
    default:
      break;
  }
  // throttling using motion hint:
  // gdk_event_request_motions(event);
  return TRUE;
}

static gboolean
dt_bauhaus_popup_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
{
  // all that doesn't seem to work (more events are created than necessary, exits at random
  // during popup already etc.
#if 0
  // if(event->x > widget->allocation.width + 20 || event->y > widget->allocation.height + 20 ||
  // event->x < -20 || event->y < -20)
  if(!event->state)
  {
    dt_bauhaus_widget_reject(darktable.bauhaus->current);
    dt_bauhaus_hide_popup();
  }
#endif
  return TRUE;
}

static gboolean
dt_bauhaus_popup_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
{
  if(darktable.bauhaus->current &&
      (darktable.bauhaus->current->type == DT_BAUHAUS_COMBOBOX) &&
      (event->button == 1) && // only accept left mouse click
      (dt_get_wtime() - darktable.bauhaus->opentime >= 0.250f)) // default gtk timeout for double-clicks
  {
    // event might be in wrong system, transform ourselves:
    gint wx, wy, x, y;
    gdk_window_get_origin (gtk_widget_get_window(darktable.bauhaus->popup_window), &wx, &wy);
    GdkDisplay *display = gdk_display_get_default();
    gdk_display_get_pointer(display, NULL, &x, &y, NULL);
    darktable.bauhaus->end_mouse_x = x - wx;
    darktable.bauhaus->end_mouse_y = y - wy;
    dt_bauhaus_widget_accept(darktable.bauhaus->current);
    dt_bauhaus_hide_popup();
  }
  return TRUE;
}

static gboolean
dt_bauhaus_popup_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
{
  if(event->button == 1)
  {
    if(darktable.bauhaus->current->type == DT_BAUHAUS_COMBOBOX &&
        dt_get_wtime() - darktable.bauhaus->opentime < 0.250f) // default gtk timeout for double-clicks
    {
      // counts as double click, reset:
      dt_bauhaus_combobox_data_t *d = &darktable.bauhaus->current->data.combobox;
      dt_bauhaus_combobox_set(GTK_WIDGET(darktable.bauhaus->current), d->defpos);
      dt_bauhaus_widget_reject(darktable.bauhaus->current);
    }
    else
    {
      // only accept left mouse click
      darktable.bauhaus->end_mouse_x = event->x;
      darktable.bauhaus->end_mouse_y = event->y;
      dt_bauhaus_widget_accept(darktable.bauhaus->current);
    }
  }
  else
  {
    dt_bauhaus_widget_reject(darktable.bauhaus->current);
  }
  dt_bauhaus_hide_popup();
  return TRUE;
}

static void
window_show(GtkWidget *w, gpointer user_data)
{
  /* grabbing might not succeed immediately... */
  if (gdk_keyboard_grab(w->window, FALSE, GDK_CURRENT_TIME) != GDK_GRAB_SUCCESS)
  {
    // never happened so far:
    /* ...wait a while and try again */
    fprintf(stderr, "[bauhaus] failed to get keyboard focus for popup window!\n");
    // struct timeval s;
    // s.tv_sec = 0;
    // s.tv_usec = 5000;
    // select(0, NULL, NULL, NULL, &s);
    // sched_yield();
  }
}

static void
dt_bh_init(DtBauhausWidget *class)
{
  // not sure if we want to use this instead of our code in *_new()
  // TODO: the common code from bauhaus_widget_init() could go here.
}

static void
dt_bh_class_init(DtBauhausWidgetClass *class)
{
  darktable.bauhaus->signals[DT_BAUHAUS_VALUE_CHANGED_SIGNAL] =
    g_signal_new ("value-changed",
                  G_TYPE_FROM_CLASS (class),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
  darktable.bauhaus->signals[DT_BAUHAUS_QUAD_PRESSED_SIGNAL] =
    g_signal_new ("quad-pressed",
                  G_TYPE_FROM_CLASS (class),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);

  // TODO: could init callbacks once per class for more efficiency:
  // GtkWidgetClass *widget_class;
  // widget_class = GTK_WIDGET_CLASS (class);
  // widget_class->expose_event = dt_bauhaus_expose;
}

static int
guess_font_size()
{
  const int def = 8;
  const char *gtkrc_filename = getenv("GTK2_RC_FILES");
  if(!gtkrc_filename) return def;
  FILE *f = fopen(gtkrc_filename, "rb");
  if(!f) return def;
  char line[256];
  while(!feof(f))
  {
    int read = fscanf(f, "%[^\n]\n", line);
    if(read > 0)
    {
      char *c = line;
      while(*c == ' ' || *c == '\t') c++;
      if(!strncmp(c, "font_name", 9))
      {
        // skip all to = then second " (end)
        while(*c != '=' && *c != 0) c++;
        while(*c != '"' && *c != 0) c++;
        if(*c != 0) c++;
        while(*c != '"' && *c != 0) c++;
        // back to last space
        while(*c != ' ' && c > line) c--;
        if(*c == ' ' && c != line)
        {
          int fontsize = (int)atol(c);
          // fprintf(stderr, "[bauhaus] guessing gtk font size of %d pt\n", fontsize);
          fclose(f);
          if(fontsize > 0) return fontsize;
        }
        fclose(f);
        return def;
      }
    }
  }
  fclose(f);
  return def;
}

void
dt_bauhaus_init()
{
  darktable.bauhaus = (dt_bauhaus_t *)malloc(sizeof(dt_bauhaus_t));
  memset(darktable.bauhaus, 0, sizeof(dt_bauhaus_t));
  darktable.bauhaus->keys_cnt = 0;
  darktable.bauhaus->current = NULL;
  darktable.bauhaus->popup_area = gtk_drawing_area_new();

  // connect signal to eventually clean up our popup if we go too far away and such:
  GtkWidget *root_window = dt_ui_main_window(darktable.gui->ui);
  g_signal_connect (G_OBJECT (root_window), "motion-notify-event",
                    G_CALLBACK (dt_bauhaus_root_motion_notify), (gpointer)NULL);
  g_signal_connect (G_OBJECT (root_window), "button-press-event",
                    G_CALLBACK (dt_bauhaus_root_button_press), (gpointer)NULL);

  darktable.bauhaus->line_space = 2;
  darktable.bauhaus->line_height = 11;
  darktable.bauhaus->marker_size = 0.3f;
  darktable.bauhaus->label_font_size = 0.6f;
  darktable.bauhaus->value_font_size = 0.6f;
  strncpy(darktable.bauhaus->label_font, "sans", 256);
  strncpy(darktable.bauhaus->value_font, "sans", 256);
  darktable.bauhaus->bg_normal = 0.145098f;
  darktable.bauhaus->bg_focus = 0.207843f;
  darktable.bauhaus->text = .792f;
  darktable.bauhaus->grid = .1f;
  darktable.bauhaus->indicator = .6f;
  darktable.bauhaus->insensitive = 0.2f;

  // it seems impossible to get to the font size in a portable way (gtk3 deprecates GtkStyle..),
  // so we parse it ourselves and convert it from pt to scale (.. :( )
  int gtk_fontsize = guess_font_size();

  // now create the pango description for the strings using the font and size found above
  gchar *font = g_strdup_printf("%s %d", darktable.bauhaus->label_font, gtk_fontsize);
  darktable.bauhaus->pango_font_desc = pango_font_description_from_string(font);
  g_free(font);

  PangoLayout *layout;
  cairo_surface_t *cst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 128, 128);
  cairo_t *cr = cairo_create(cst);
  layout = pango_cairo_create_layout(cr);
  pango_layout_set_text(layout, "X", -1);
  pango_layout_set_font_description(layout, darktable.bauhaus->pango_font_desc);
  pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);
  int pango_width, pango_height;
  pango_layout_get_size (layout, &pango_width, &pango_height);
  g_object_unref(layout);
  cairo_destroy(cr);
  cairo_surface_destroy(cst);

  darktable.bauhaus->scale = (pango_height+0.0f)/PANGO_SCALE/8.5f;
  darktable.bauhaus->widget_space = 2.5f*darktable.bauhaus->scale;

  // keys are freed with g_free, values are ptrs to the widgets, these don't need to be cleaned up.
  darktable.bauhaus->keymap = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  darktable.bauhaus->key_mod = NULL;
  darktable.bauhaus->key_val = NULL;
  memset(darktable.bauhaus->key_history, 0, sizeof(darktable.bauhaus->key_history));

  // this easily gets keyboard input:
  // darktable.bauhaus->popup_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  // but this doesn't flicker, and the above hack with key input seems to work well.
  darktable.bauhaus->popup_window = gtk_window_new(GTK_WINDOW_POPUP);
  // this is needed for popup, not for toplevel.
  // since popup_area gets the focus if we show the window, this is all
  // we need.
  dt_gui_key_accel_block_on_focus(darktable.bauhaus->popup_area);

  gtk_widget_set_size_request(darktable.bauhaus->popup_area, 300, 300);
  gtk_window_set_resizable(GTK_WINDOW(darktable.bauhaus->popup_window), FALSE);
  gtk_window_set_default_size(GTK_WINDOW(darktable.bauhaus->popup_window), 260, 260);
  // gtk_window_set_modal(GTK_WINDOW(c->popup_window), TRUE);
  // gtk_window_set_decorated(GTK_WINDOW(c->popup_window), FALSE);

  // for pie menue:
  // gtk_window_set_position(GTK_WINDOW(c->popup_window), GTK_WIN_POS_MOUSE);// | GTK_WIN_POS_CENTER);

  // gtk_window_set_keep_above isn't enough on OS X
  gtk_window_set_transient_for(GTK_WINDOW(darktable.bauhaus->popup_window), GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)));
  gtk_container_add(GTK_CONTAINER(darktable.bauhaus->popup_window), darktable.bauhaus->popup_area);
  // gtk_window_set_title(GTK_WINDOW(c->popup_window), _("dtgtk control popup"));
  gtk_window_set_keep_above(GTK_WINDOW(darktable.bauhaus->popup_window), TRUE);
  gtk_window_set_gravity(GTK_WINDOW(darktable.bauhaus->popup_window), GDK_GRAVITY_STATIC);

  gtk_widget_set_can_focus(darktable.bauhaus->popup_area, TRUE);
  gtk_widget_add_events(darktable.bauhaus->popup_area,
                        GDK_POINTER_MOTION_MASK |
                        GDK_POINTER_MOTION_HINT_MASK |
                        GDK_BUTTON_PRESS_MASK |
                        GDK_BUTTON_RELEASE_MASK |
                        GDK_KEY_PRESS_MASK |
                        GDK_LEAVE_NOTIFY_MASK);

  g_signal_connect (G_OBJECT (darktable.bauhaus->popup_window), "show", G_CALLBACK(window_show), (gpointer)NULL);
  g_signal_connect (G_OBJECT (darktable.bauhaus->popup_area), "expose-event",
                    G_CALLBACK (dt_bauhaus_popup_expose), (gpointer)NULL);
  g_signal_connect (G_OBJECT (darktable.bauhaus->popup_area), "motion-notify-event",
                    G_CALLBACK (dt_bauhaus_popup_motion_notify), (gpointer)NULL);
  g_signal_connect (G_OBJECT (darktable.bauhaus->popup_area), "leave-notify-event",
                    G_CALLBACK (dt_bauhaus_popup_leave_notify), (gpointer)NULL);
  g_signal_connect (G_OBJECT (darktable.bauhaus->popup_area), "button-press-event",
                    G_CALLBACK (dt_bauhaus_popup_button_press), (gpointer)NULL);
  // this is connected to the widget itself, not the popup. we're only interested
  // in mouse release events that are initiated by a press on the original widget.
  // g_signal_connect (G_OBJECT (darktable.bauhaus->popup_area), "button-release-event",
  //                   G_CALLBACK (dt_bauhaus_popup_button_release), (gpointer)NULL);
  g_signal_connect (G_OBJECT (darktable.bauhaus->popup_area), "key-press-event",
                    G_CALLBACK (dt_bauhaus_popup_key_press), (gpointer)NULL);
  g_signal_connect (G_OBJECT (darktable.bauhaus->popup_area), "scroll-event",
                    G_CALLBACK (dt_bauhaus_popup_scroll), (gpointer)NULL);
}

void
dt_bauhaus_cleanup()
{
  // TODO: destroy popup window and resources
  // TODO: destroy keymap hash table and auto complete lists!
}

// fwd declare a few callbacks
static gboolean
dt_bauhaus_slider_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
static gboolean
dt_bauhaus_slider_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
static gboolean
dt_bauhaus_slider_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data);
static gboolean
dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data);
static gboolean
dt_bauhaus_slider_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data);


static gboolean
dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
static gboolean
dt_bauhaus_combobox_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data);
// static gboolean
// dt_bauhaus_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
static gboolean
dt_bauhaus_expose(GtkWidget *widget, GdkEventExpose *event, gpointer user_data);


// end static init/cleanup
// =================================================





// common initialization
static void
dt_bauhaus_widget_init(dt_bauhaus_widget_t* w, dt_iop_module_t *self)
{
  w->module = self;

  // no quad icon and no toggle button:
  w->quad_paint  = 0;
  w->quad_toggle = 0;

  gtk_widget_set_size_request(GTK_WIDGET(w), 260, get_line_height());

  gtk_widget_add_events(GTK_WIDGET(w),
                        GDK_POINTER_MOTION_MASK |
                        GDK_POINTER_MOTION_HINT_MASK |
                        GDK_BUTTON_PRESS_MASK |
                        GDK_BUTTON_RELEASE_MASK |
                        GDK_LEAVE_NOTIFY_MASK);

  g_signal_connect (G_OBJECT (w), "expose-event",
                    G_CALLBACK (dt_bauhaus_expose), NULL);

  // for combobox, where mouse-release triggers a selection, we need to catch this
  // event where the mouse-press occurred, which will be this widget. we just pass
  // it on though:
  // g_signal_connect (G_OBJECT (w), "button-release-event",
  //                   G_CALLBACK (dt_bauhaus_popup_button_release), (gpointer)NULL);
}

void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *text)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  memset(w->label, 0, 256); // keep valgrind happy
  strncpy(w->label, text, 256);

  if(w->module)
  {
    // construct control path name and insert into keymap:
    gchar *path = g_strdup_printf("%s.%s", w->module->name(), w->label);
    if(!g_hash_table_lookup(darktable.bauhaus->keymap, path))
    {
      // also insert into sorted tab-complete list.
      // (but only if this is the first time we insert this path)
      gchar *mod = g_strdup(path);
      gchar *val = g_strstr_len(mod, strlen(mod), ".");
      if(val)
      {
        *val = 0;
        if(!g_list_find_custom(darktable.bauhaus->key_mod, mod, (GCompareFunc)strcmp))
          darktable.bauhaus->key_mod = g_list_insert_sorted(darktable.bauhaus->key_mod, mod, (GCompareFunc)strcmp);
        // unfortunately need our own string, as replace in the hashtable below might destroy this pointer.
        darktable.bauhaus->key_val = g_list_insert_sorted(darktable.bauhaus->key_val, g_strdup(path), (GCompareFunc)strcmp);
      }
    }
    // might free an old path
    g_hash_table_replace(darktable.bauhaus->keymap, path, w);
  }
}

void dt_bauhaus_widget_set_quad_paint(GtkWidget *widget, dt_bauhaus_quad_paint_f f, int paint_flags)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  w->quad_paint = f;
  w->quad_paint_flags = paint_flags;
}

// make this quad a toggle button:
void dt_bauhaus_widget_set_quad_toggle(GtkWidget *widget, int toggle)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  w->quad_toggle = toggle;
}

static void
dt_bauhaus_slider_destroy(dt_bauhaus_widget_t *widget, gpointer user_data)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_SLIDER) return;
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  if(d->timeout_handle) g_source_remove(d->timeout_handle);
  d->timeout_handle = 0;
}

GtkWidget*
dt_bauhaus_slider_new(dt_iop_module_t *self)
{
  return dt_bauhaus_slider_new_with_range(self, 0.0, 1.0, 0.1, 0.5, 3);
}

GtkWidget*
dt_bauhaus_slider_new_with_range(dt_iop_module_t *self, float min, float max, float step, float defval, int digits)
{
  dt_bauhaus_widget_t* w = DT_BAUHAUS_WIDGET(g_object_new(DT_BAUHAUS_WIDGET_TYPE, NULL));
  dt_bauhaus_widget_init(w, self);
  w->type = DT_BAUHAUS_SLIDER;
  dt_bauhaus_widget_init(w, self);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  d->min = min;
  d->max = max;
  // normalize default:
  d->defpos = (defval-min)/(max-min);
  d->pos = d->defpos;
  d->oldpos = d->defpos;
  d->scale = 5.0f*step/(max-min);
  d->digits = digits;
  snprintf(d->format, 24, "%%.0%df", digits);

  d->grad_cnt = 0;

  d->is_dragging = 0;
  d->is_changed = 0;
  d->timeout_handle = 0;

  g_signal_connect (G_OBJECT (w), "button-press-event",
                    G_CALLBACK (dt_bauhaus_slider_button_press), (gpointer)NULL);
  g_signal_connect (G_OBJECT (w), "button-release-event",
                    G_CALLBACK (dt_bauhaus_slider_button_release), (gpointer)NULL);
  g_signal_connect (G_OBJECT (w), "scroll-event",
                    G_CALLBACK (dt_bauhaus_slider_scroll), (gpointer)NULL);
  g_signal_connect (G_OBJECT (w), "motion-notify-event",
                    G_CALLBACK (dt_bauhaus_slider_motion_notify), (gpointer)NULL);
  g_signal_connect (G_OBJECT (w), "leave-notify-event",
                    G_CALLBACK (dt_bauhaus_slider_leave_notify), (gpointer)NULL);
  g_signal_connect (G_OBJECT (w), "destroy",
                    G_CALLBACK (dt_bauhaus_slider_destroy), (gpointer)NULL);
  return GTK_WIDGET(w);
}

static void
dt_bauhaus_combobox_destroy(dt_bauhaus_widget_t *widget, gpointer user_data)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_COMBOBOX) return;
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  g_list_foreach(d->labels,(GFunc)g_free,NULL);
  g_list_free(d->labels);
  d->labels = NULL;
  d->num_labels = 0;
}

GtkWidget*
dt_bauhaus_combobox_new(dt_iop_module_t *self)
{
  dt_bauhaus_widget_t* w = DT_BAUHAUS_WIDGET(g_object_new(DT_BAUHAUS_WIDGET_TYPE, NULL));
  w->type = DT_BAUHAUS_COMBOBOX;
  dt_bauhaus_widget_init(w, self);
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  d->labels = NULL;
  d->num_labels = 0;
  d->defpos = 0;
  d->active = d->defpos;
  d->editable = 0;
  memset(d->text, 0, sizeof(d->text));
  g_signal_connect (G_OBJECT (w), "button-press-event",
                    G_CALLBACK (dt_bauhaus_combobox_button_press), (gpointer)NULL);
  g_signal_connect (G_OBJECT (w), "button-release-event",
                    G_CALLBACK (dt_bauhaus_popup_button_release), (gpointer)NULL);
  g_signal_connect (G_OBJECT (w), "scroll-event",
                    G_CALLBACK (dt_bauhaus_combobox_scroll), (gpointer)NULL);
  g_signal_connect (G_OBJECT (w), "destroy",
                    G_CALLBACK (dt_bauhaus_combobox_destroy), (gpointer)NULL);
  return GTK_WIDGET(w);
}

void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_COMBOBOX) return;
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  d->num_labels++;
  d->labels = g_list_append(d->labels, g_strdup(text));
}

void dt_bauhaus_combobox_set_editable(GtkWidget *widget, int editable)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_COMBOBOX) return;
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  d->editable = editable ? 1 : 0;
}

const char* dt_bauhaus_combobox_get_text(GtkWidget *widget)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_COMBOBOX) return NULL;
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;

  if(d->editable && d->active < 0)
  {
    return d->text;
  }
  else
  {
    if(d->active < 0 || d->active >= d->num_labels) return NULL;
    return (const char *)g_list_nth_data(d->labels, d->active);
  }
  return NULL;
}

void dt_bauhaus_combobox_clear(GtkWidget *widget)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_COMBOBOX) return;
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  d->active = 0;
  g_list_foreach(d->labels,(GFunc)g_free,NULL);
  g_list_free(d->labels);
  d->labels = NULL;
  d->num_labels = 0;
}

const GList* dt_bauhaus_combobox_get_labels(GtkWidget *widget)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_COMBOBOX) return 0;
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  return d->labels;
}

void dt_bauhaus_combobox_set_text(GtkWidget *widget, const char *text)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_COMBOBOX) return;
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  if(!d->editable) return;
  strncpy(d->text, text, sizeof(d->text));
}

void dt_bauhaus_combobox_set(GtkWidget *widget, int pos)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_COMBOBOX) return;
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  d->active = CLAMP(pos, -1, d->num_labels-1);
  gtk_widget_queue_draw(GTK_WIDGET(w));
  if(!darktable.gui->reset)
    g_signal_emit_by_name(G_OBJECT(w), "value-changed");
}

int dt_bauhaus_combobox_get(GtkWidget *widget)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_COMBOBOX) return -1;
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  return d->active;
}

void dt_bauhaus_slider_set_stop(GtkWidget *widget, float stop, float r, float g, float b)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_SLIDER) return;
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  // need to replace stop?
  for(int k=0; k<d->grad_cnt; k++)
  {
    if(d->grad_pos[k] == stop)
    {
      d->grad_col[k][0] = r;
      d->grad_col[k][1] = g;
      d->grad_col[k][2] = b;
      return;
    }
  }
  // new stop:
  if(d->grad_cnt < 10)
  {
    int k = d->grad_cnt ++;
    d->grad_pos[k] = stop;
    d->grad_col[k][0] = r;
    d->grad_col[k][1] = g;
    d->grad_col[k][2] = b;
  }
  else
  {
    fprintf(stderr, "[bauhaus_slider_set_stop] only 10 stops allowed.\n");
  }
}


#ifndef DT_BAUHAUS_OLD
static void
draw_equilateral_triangle(cairo_t *cr, float radius)
{
  const float sin = 0.866025404 * radius;
  const float cos = 0.5f * radius;
  cairo_move_to(cr, 0.0, radius);
  cairo_line_to(cr, -sin, -cos);
  cairo_line_to(cr,  sin, -cos);
  cairo_line_to(cr, 0.0, radius);
}
#endif

static void
dt_bauhaus_draw_indicator(dt_bauhaus_widget_t *w, float pos, cairo_t *cr)
{
  // draw scale indicator
  GtkWidget *widget = GTK_WIDGET(w);
  if(w->type != DT_BAUHAUS_SLIDER) return;
  const int wd = widget->allocation.width;
  const int ht = widget->allocation.height;
  cairo_save(cr);
  // dt_bauhaus_slider_data_t *d = &w->data.slider;

  const float l = 4.0f/wd;
  const float r = 1.0f-(ht+4.0f)/wd;
  set_indicator_color(cr, 1);
#ifndef DT_BAUHAUS_OLD
  cairo_translate(cr, (l + pos*(r-l))*wd, get_line_height() * (darktable.bauhaus->label_font_size + 0.25f));
  cairo_scale(cr, 1.0f, -1.0f);
  draw_equilateral_triangle(cr, ht*get_marker_size());
  cairo_fill_preserve(cr);
  cairo_set_line_width(cr, 1.);
  set_grid_color(cr, 1);
  cairo_stroke(cr);
#else
  cairo_translate(cr, (l + pos*(r-l))*wd, 0);
  cairo_set_line_width(cr, 1.);
  cairo_move_to(cr, 0.0f, 0.0f);
  cairo_line_to(cr, 0.0f, ht);
  set_grid_color(cr, 1);
  cairo_stroke(cr);
#endif
  cairo_restore(cr);
}


static void
dt_bauhaus_clear(dt_bauhaus_widget_t *w, cairo_t *cr)
{
  // clear bg with background color
  cairo_save(cr);
  if(w->module && darktable.develop->gui_module == w->module)
    set_bg_focus(cr);
  else
    set_bg_normal(cr);
#if 0
  GtkWidget *topwidget = dt_iop_gui_get_pluginui(w->module);
  GtkStyle *style = gtk_widget_get_style(topwidget);
  if(style)
  {
    cairo_set_source_rgb (cr,
                          style->bg[gtk_widget_get_state(topwidget)].red/65535.0f,
                          style->bg[gtk_widget_get_state(topwidget)].green/65535.0f,
                          style->bg[gtk_widget_get_state(topwidget)].blue/65535.0f);
  }
#endif
  cairo_paint(cr);
  cairo_restore(cr);
}

static void
dt_bauhaus_draw_quad(dt_bauhaus_widget_t *w, cairo_t *cr)
{
  GtkWidget *widget = GTK_WIDGET(w);
  int width  = widget->allocation.width;
  int height = widget->allocation.height;
  if(w->quad_paint)
  {
    cairo_save(cr);
    set_grid_color(cr, gtk_widget_is_sensitive(GTK_WIDGET(w)));
    w->quad_paint(cr, width-height-1, -1, height+2, get_label_font_size()+2, w->quad_paint_flags);
    set_indicator_color(cr, gtk_widget_is_sensitive(GTK_WIDGET(w)));
    w->quad_paint(cr, width-height, 0, height, get_label_font_size(), w->quad_paint_flags);
    cairo_restore(cr);
  }
  else
  {
    // TODO: decide if we need this at all.
    //       there is a chance it only introduces clutter.
#if 1
    // draw active area square:
    cairo_save(cr);
    set_indicator_color(cr, gtk_widget_is_sensitive(GTK_WIDGET(w)));
    switch(w->type)
    {
      case DT_BAUHAUS_COMBOBOX:
        cairo_translate(cr, width -height*.5f, get_label_font_size()*.5f);
        draw_equilateral_triangle(cr, height*get_marker_size());
        cairo_fill_preserve(cr);
        cairo_set_line_width(cr, 1.);
        set_grid_color(cr, 1);
        cairo_stroke(cr);
        break;
      case DT_BAUHAUS_SLIDER:
        break;
      default:
        cairo_rectangle(cr, width - height, 0, height, height);
        cairo_fill(cr);
        break;
    }
    cairo_restore(cr);
#endif
  }
}

static void
dt_bauhaus_draw_baseline(dt_bauhaus_widget_t *w, cairo_t *cr)
{
  // draw line for orientation in slider
  GtkWidget *widget = GTK_WIDGET(w);
  if(w->type != DT_BAUHAUS_SLIDER) return;
  const int wd = widget->allocation.width;
  const int ht = widget->allocation.height;
  cairo_save(cr);
  dt_bauhaus_slider_data_t *d = &w->data.slider;

#ifdef DT_BAUHAUS_OLD
  const float htm = 0.0f, htM = ht;
#else
  // pos of baseline
  const float htm = ht*(darktable.bauhaus->label_font_size + 0.15f);
  const float htM = ht*0.2f; // thickness of baseline
#endif

  cairo_pattern_t *gradient = NULL;
  if(d->grad_cnt > 0)
  {
    gradient = cairo_pattern_create_linear(0, 0, wd-4-ht-2, ht);
    cairo_pattern_reference(gradient);
    for(int k=0; k<d->grad_cnt; k++)
      cairo_pattern_add_color_stop_rgba(gradient, d->grad_pos[k],
                                        d->grad_col[k][0],
                                        d->grad_col[k][1],
                                        d->grad_col[k][2],
                                        .2f);
    cairo_set_source(cr, gradient);
  }
  else
  {
    // regular baseline
    set_grid_color(cr, 0);
  }

  cairo_rectangle(cr, 2, htm, wd-4-ht-2, htM);
  cairo_fill(cr);

  // have a `fill ratio feel'
  set_indicator_color(cr, 0);
  cairo_rectangle(cr, 2, htm, d->pos*(wd-4-ht-2), htM);
  cairo_fill(cr);

  cairo_rectangle(cr, 2, htm, wd-4-ht-2, htM);
  cairo_set_line_width(cr, 1.);
  set_grid_color(cr, 1);
  cairo_stroke(cr);

  cairo_restore(cr);

  if(d->grad_cnt > 0)
    cairo_pattern_destroy(gradient);
}

static void
dt_bauhaus_widget_reject(dt_bauhaus_widget_t *w)
{
  switch(w->type)
  {
    case DT_BAUHAUS_COMBOBOX:
      break;
    case DT_BAUHAUS_SLIDER:
    {
      dt_bauhaus_slider_data_t *d = &w->data.slider;
      dt_bauhaus_slider_set_normalized(w, d->oldpos);
    }
    break;
    default:
      break;
  }
}

static void
dt_bauhaus_widget_accept(dt_bauhaus_widget_t *w)
{
  GtkWidget *widget = GTK_WIDGET(w);
  int width = darktable.bauhaus->popup_window->allocation.width, height = darktable.bauhaus->popup_window->allocation.height;
  switch(w->type)
  {
    case DT_BAUHAUS_COMBOBOX:
    {
      // only set to what's in the filtered list.
      dt_bauhaus_combobox_data_t *d = &w->data.combobox;
      int active = darktable.bauhaus->end_mouse_y >= 0
        ? (darktable.bauhaus->end_mouse_y / (widget->allocation.height + get_line_space()))
        : d->active;
      GList *it = d->labels;
      int k = 0, i = 0, kk = 0, match = 1;

      gchar *keys = g_utf8_casefold(darktable.bauhaus->keys, -1);
      while(it)
      {
        gchar *text = (gchar *)it->data;
        gchar *text_cmp = g_utf8_casefold(text, -1);
        if(!strncmp(text_cmp, keys, darktable.bauhaus->keys_cnt))
        {
          if(active == k)
          {
            dt_bauhaus_combobox_set(widget, i);
            g_free(keys);
            g_free(text_cmp);
            return;
          }
          kk = i; // remember for down there
          // editable should only snap to perfect matches, not prefixes:
          if(d->editable && strcmp(text, darktable.bauhaus->keys)) match = 0;
          k++;
        }
        i++;
        it = g_list_next(it);
        g_free(text_cmp);
      }
      // if list is short (2 entries could be: typed something similar, and one similar)
      if(k < 3)
      {
        // didn't find it, but had only one matching choice?
        if(k == 1 && match) dt_bauhaus_combobox_set(widget, kk);
        else if(d->editable)
        {
          // had no close match (k == 1 && !match) or no match at all (k == 0)
          memset(d->text, 0, sizeof(d->text));
          strncpy(d->text, darktable.bauhaus->keys, MIN(darktable.bauhaus->keys_cnt, sizeof(d->text)));
          // select custom entry
          dt_bauhaus_combobox_set(widget, -1);
        }
      }
      g_free(keys);
      break;
    }
    case DT_BAUHAUS_SLIDER:
    {
      dt_bauhaus_slider_data_t *d = &w->data.slider;
      const float mouse_off = get_slider_line_offset(
                                d->oldpos, d->scale, darktable.bauhaus->end_mouse_x/width,
                                darktable.bauhaus->end_mouse_y/height, widget->allocation.height/(float)height,
                                widget->allocation.width);
      dt_bauhaus_slider_set_normalized(w, d->oldpos + mouse_off);
      d->oldpos = d->pos;
      break;
    }
    default:
      break;
  }
}

static gboolean
dt_bauhaus_popup_expose(GtkWidget *widget, GdkEventExpose *event, gpointer user_data)
{
  dt_bauhaus_widget_t *w = darktable.bauhaus->current;
  // dimensions of the popup
  int width = widget->allocation.width, height = widget->allocation.height;
  // dimensions of the original line
  GtkWidget *current = GTK_WIDGET(w);
  int wd = current->allocation.width, ht = current->allocation.height;
  cairo_surface_t *cst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
  cairo_t *cr = cairo_create(cst);

  // draw same things as original widget, for visual consistency:
  dt_bauhaus_clear(w, cr);

  // draw line around popup
  cairo_set_line_width(cr, 1.0);
  cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
  if(w->type == DT_BAUHAUS_COMBOBOX)
  {
    // separate more clearly (looks terrible in a way but might help separate text
    // from other widgets above and below)
    cairo_move_to(cr, 1.0, height-1.0);
    cairo_line_to(cr, width-1.0, height-1.0);
    cairo_line_to(cr, width-1.0, 1.0);
    cairo_stroke(cr);
    cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.4);
    cairo_move_to(cr, 1.0, height-1.0);
    cairo_line_to(cr, 1.0, 1.0);
    cairo_line_to(cr, width-1.0, 1.0);
    cairo_stroke(cr);
  }
  else
  {
    // blend in
    cairo_move_to(cr, 1.0, 3.0*ht);
    cairo_line_to(cr, 1.0, height-1);
    cairo_line_to(cr, width-1, height-1);
    cairo_line_to(cr, width-1, 3.0*ht);
    cairo_stroke(cr);
    // fade in line around popup:
    for(int k=0; k<4; k++)
    {
      cairo_set_line_width(cr, (k+1)/4.0f);
      cairo_move_to(cr, 1.0, ht*(2.f + k/4.0f));
      cairo_line_to(cr, 1.0, ht*(2.f + (k+1)/4.0f));
      cairo_move_to(cr, width-1.0, ht*(2.f + k/4.0f));
      cairo_line_to(cr, width-1.0, ht*(2.f + (k+1)/4.0f));
      cairo_stroke(cr);
    }
  }

  // switch on bauhaus widget type (so we only need one static window)
  switch(w->type)
  {
    case DT_BAUHAUS_SLIDER:
    {
      dt_bauhaus_slider_data_t *d = &w->data.slider;

      dt_bauhaus_draw_baseline(w, cr);

      cairo_save(cr);
      cairo_set_line_width(cr, 1.);
      set_grid_color(cr, 1);
      const int num_scales = 1.f/d->scale;
      // don't draw over orientation line
      cairo_rectangle(cr, 0.0f, 0.9*ht, width, height);
      cairo_clip(cr);
      for(int k=0; k<num_scales; k++)
      {
        const float off = k*d->scale - d->oldpos;
        cairo_set_source_rgba(cr, darktable.bauhaus->grid, darktable.bauhaus->grid, darktable.bauhaus->grid, d->scale/fabsf(off));
        draw_slider_line(cr, d->oldpos, off, d->scale, width, height, ht);
        cairo_stroke(cr);
      }
      cairo_restore(cr);

      show_pango_text(cr, w->label, 2, 0, 0, FALSE, TRUE, FALSE);

      // draw mouse over indicator line
      cairo_save(cr);
      set_indicator_color(cr, 1);
      cairo_set_line_width(cr, 2.);
      const float mouse_off = darktable.bauhaus->change_active ? get_slider_line_offset(d->oldpos, d->scale,
                              darktable.bauhaus->mouse_x/width, darktable.bauhaus->mouse_y/height, ht/(float)height,
                              width) : 0.0f;
      draw_slider_line(cr, d->oldpos, mouse_off, d->scale, width, height, ht);
      cairo_stroke(cr);
      cairo_restore(cr);

      // draw indicator
      dt_bauhaus_draw_indicator(w, d->oldpos+mouse_off, cr);

      // draw numerical value:
      cairo_save(cr);
      char text[256];
      const float f = d->min + (d->oldpos+mouse_off)*(d->max-d->min);
      snprintf(text, 256, d->format, f);
      show_pango_text(cr, text, wd-4-ht, 0, 0, TRUE, TRUE, FALSE);

      cairo_restore(cr);
    }
    break;
    case DT_BAUHAUS_COMBOBOX:
    {
      show_pango_text(cr, w->label, 2, 0, 0, FALSE, TRUE, FALSE);

      dt_bauhaus_combobox_data_t *d = &w->data.combobox;
      cairo_save(cr);
      set_text_color(cr, 1);
      set_value_font(cr);
      GList *it = d->labels;
      int k = 0, i = 0;
      int hovered = darktable.bauhaus->mouse_y / (ht + get_line_space());
      gchar *keys = g_utf8_casefold(darktable.bauhaus->keys, -1);
      while(it)
      {
        gchar *text = (gchar *)it->data;
        gchar *text_cmp = g_utf8_casefold(text, -1);
        if(!strncmp(text_cmp, keys, darktable.bauhaus->keys_cnt))
        {
          gboolean highlight = FALSE;
          if(i == hovered)
          {
            highlight = TRUE;
          }
          show_pango_text(cr, text, wd-4-ht, (get_line_space()+ht)*k, 0, TRUE, !highlight, highlight);
          k++;
        }
        i++;
        it = g_list_next(it);
        g_free(text_cmp);
      }
      cairo_restore(cr);
      g_free(keys);
    }
    break;
    default:
      // yell
      break;
  }

  // draw currently typed text. if a type doesn't want this, it should not
  // allow stuff to be written here in the key callback.
  if(darktable.bauhaus->keys_cnt)
  {
    cairo_save(cr);
    cairo_text_extents_t ext;
    set_text_color(cr, 1);
    set_value_font(cr);
    // make extra large, but without dependency on popup window height
    // (that might differ for comboboxes for example). only fall back
    // to height dependency if the popup is really small.
    cairo_set_font_size (cr, MIN(3*get_line_height(), .2*height));
    cairo_text_extents (cr, darktable.bauhaus->keys, &ext);
    cairo_move_to (cr, wd-4-ht-ext.width, height*0.5);
    cairo_show_text(cr, darktable.bauhaus->keys); // FIXME: pango
    cairo_restore(cr);
  }

  cairo_destroy(cr);
  cairo_t *cr_pixmap = gdk_cairo_create(gtk_widget_get_window(widget));
  cairo_set_source_surface (cr_pixmap, cst, 0, 0);
  cairo_paint(cr_pixmap);
  cairo_destroy(cr_pixmap);
  cairo_surface_destroy(cst);

  return TRUE;
}

static gboolean
dt_bauhaus_expose(GtkWidget *widget, GdkEventExpose *event, gpointer user_data)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  const int width = widget->allocation.width, height = widget->allocation.height;
  cairo_surface_t *cst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
  cairo_t *cr = cairo_create(cst);

  dt_bauhaus_clear(w, cr);

  // draw type specific content:
  cairo_save(cr);
  cairo_set_line_width(cr, 1.0);
  switch(w->type)
  {
    case DT_BAUHAUS_COMBOBOX:
    {
      // draw label and quad area at right end
      float label_width = show_pango_text(cr, w->label, 2, 0, 0, FALSE, TRUE, FALSE);
      dt_bauhaus_draw_quad(w, cr);

      if(gtk_widget_is_sensitive(widget))
      {
        dt_bauhaus_combobox_data_t *d = &w->data.combobox;
        gchar *text = d->text;
        if(d->active >= 0)
          text = (gchar *)g_list_nth_data(d->labels, d->active);
        show_pango_text(cr, text, width-4-height, 0, width-4-height-label_width-height, TRUE, TRUE, FALSE);
      }
      break;
    }
    case DT_BAUHAUS_SLIDER:
    {
      dt_bauhaus_slider_data_t *d = &w->data.slider;

      // line for orientation
      dt_bauhaus_draw_baseline(w, cr);
      dt_bauhaus_draw_quad(w, cr);

      if(gtk_widget_is_sensitive(widget))
      {
        dt_bauhaus_draw_indicator(w, d->pos, cr);

        // TODO: merge that text with combo
        char text[256];
        const float f = d->min + d->pos*(d->max-d->min);
        snprintf(text, 256, d->format, f);
        show_pango_text(cr, text, width-4-height, 0, 0, TRUE, TRUE, FALSE);
      }
      // label on top of marker:
      show_pango_text(cr, w->label, 2, 0, 0, FALSE, TRUE, FALSE);
    }
    break;
    default:
      break;
  }
  cairo_restore(cr);

  cairo_destroy(cr);
  cairo_t *cr_pixmap = gdk_cairo_create(gtk_widget_get_window(widget));
  cairo_set_source_surface (cr_pixmap, cst, 0, 0);
  cairo_paint(cr_pixmap);
  cairo_destroy(cr_pixmap);
  cairo_surface_destroy(cst);

  return TRUE;
}

static void
dt_bauhaus_hide_popup()
{
  gdk_keyboard_ungrab(GDK_CURRENT_TIME);
  gtk_widget_hide(darktable.bauhaus->popup_window);
  darktable.bauhaus->current = NULL;
  // TODO: give focus to center view? do in accept() as well?
}

static void
dt_bauhaus_show_popup(dt_bauhaus_widget_t *w)
{
  if(darktable.bauhaus->current)
    dt_bauhaus_hide_popup();
  darktable.bauhaus->current = w;
  darktable.bauhaus->keys_cnt = 0;
  memset(darktable.bauhaus->keys, 0, 64);
  darktable.bauhaus->change_active = 0;
  darktable.bauhaus->mouse_line_distance = 0.0f;

  dt_iop_request_focus(w->module);
  int offset = 0;

  switch(darktable.bauhaus->current->type)
  {
    case DT_BAUHAUS_SLIDER:
    {
      dt_bauhaus_slider_data_t *d = &w->data.slider;
      d->oldpos = d->pos;

      GtkAllocation tmp;
      gtk_widget_get_allocation(GTK_WIDGET(w), &tmp);
      gtk_widget_set_size_request(darktable.bauhaus->popup_area, tmp.width, tmp.width);
      break;
    }
    case DT_BAUHAUS_COMBOBOX:
    {
      // comboboxes change immediately
      darktable.bauhaus->change_active = 1;
      GtkAllocation tmp;
      gtk_widget_get_allocation(GTK_WIDGET(w), &tmp);
      dt_bauhaus_combobox_data_t *d = &w->data.combobox;
      gtk_widget_set_size_request(darktable.bauhaus->popup_area, tmp.width, (tmp.height+get_line_space()) * d->num_labels);
      const int ht = GTK_WIDGET(w)->allocation.height;
      const int skip = ht + get_line_space();
      offset = - d->active * skip;
      darktable.bauhaus->mouse_x = 0;
      darktable.bauhaus->mouse_y = d->active * skip + ht/2;
      break;
    }
    default:
      break;
  }

  gint wx, wy;
  gdk_window_get_origin (gtk_widget_get_window (GTK_WIDGET(w)), &wx, &wy);

  // move popup so mouse is over currently active item, to minimize confusion with scroll wheel:
  if(darktable.bauhaus->current->type == DT_BAUHAUS_COMBOBOX)
    wy += offset;

  // gtk_widget_get_window will return null if not shown yet.
  // it is needed for gdk_window_move, and gtk_window move will
  // sometimes be ignored. this is why we always call both...
  // we also don't want to show befor move, as this results in noticable flickering.
  GdkWindow *window = gtk_widget_get_window(darktable.bauhaus->popup_window);
  if(window) gdk_window_move(window, wx, wy);
  gtk_window_move(GTK_WINDOW(darktable.bauhaus->popup_window), wx, wy);
  gtk_widget_show_all(darktable.bauhaus->popup_window);
  gtk_widget_grab_focus(darktable.bauhaus->popup_area);
}

static gboolean
dt_bauhaus_slider_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
{
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
  if(w->type != DT_BAUHAUS_SLIDER) return FALSE;
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  if(event->direction == GDK_SCROLL_UP)
  {
    dt_iop_request_focus(w->module);
    dt_bauhaus_slider_set_normalized(w, d->pos+d->scale/5.0f);
    return TRUE;
  }
  else if(event->direction == GDK_SCROLL_DOWN)
  {
    dt_iop_request_focus(w->module);
    dt_bauhaus_slider_set_normalized(w, d->pos-d->scale/5.0f);
    return TRUE;
  }
  return FALSE;
}


static gboolean
dt_bauhaus_combobox_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
{
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
  if(w->type != DT_BAUHAUS_COMBOBOX) return FALSE;
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  if(event->direction == GDK_SCROLL_UP)
  {
    dt_iop_request_focus(w->module);
    dt_bauhaus_combobox_set(widget, CLAMP(d->active-1, 0, d->num_labels-1));
    return TRUE;
  }
  else if(event->direction == GDK_SCROLL_DOWN)
  {
    dt_iop_request_focus(w->module);
    dt_bauhaus_combobox_set(widget, CLAMP(d->active+1, 0, d->num_labels-1));
    return TRUE;
  }
  return FALSE;
}

static gboolean
dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
{
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
  if(w->type != DT_BAUHAUS_COMBOBOX) return FALSE;
  if(w->module) // tethering uses these with module == NULL
    dt_iop_request_focus(w->module);
  GtkAllocation tmp;
  gtk_widget_get_allocation(GTK_WIDGET(w), &tmp);
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  if(w->quad_paint && (event->x > widget->allocation.width - widget->allocation.height))
  {
    g_signal_emit_by_name(G_OBJECT(w), "quad-pressed");
    return TRUE;
  }
  else if(event->button == 3)
  {
    darktable.bauhaus->mouse_x = event->x;
    darktable.bauhaus->mouse_y = event->y;
    dt_bauhaus_show_popup(w);
    return TRUE;
  }
  else if(event->button == 1)
  {
    // reset to default.
    if(event->type == GDK_2BUTTON_PRESS)
    {
      // never called, as we popup the other window under your cursor before.
      dt_bauhaus_combobox_set(widget, d->defpos);
    }
    else
    {
      // single click, show options
      darktable.bauhaus->opentime = dt_get_wtime();
      darktable.bauhaus->mouse_x = event->x;
      darktable.bauhaus->mouse_y = event->y;
      dt_bauhaus_show_popup(w);
    }
    return TRUE;
  }
  return FALSE;
}

float
dt_bauhaus_slider_get(GtkWidget *widget)
{
  // first cast to bh widget, to check that type:
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
  if(!w->type == DT_BAUHAUS_SLIDER) return -1.0f;
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  return d->min + d->pos*(d->max-d->min);
}

void
dt_bauhaus_slider_set(GtkWidget *widget, float pos)
{
  // this is the public interface function, translate by bounds and call set_normalized
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_SLIDER) return;
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  dt_bauhaus_slider_set_normalized(w, (pos-d->min)/(d->max-d->min));
}

void
dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
{
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_SLIDER) return;
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  strncpy(d->format, format, 24);
}

static void
dt_bauhaus_slider_set_normalized(dt_bauhaus_widget_t *w, float pos)
{
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  float rpos = CLAMP(pos, 0.0f, 1.0f);
  rpos = d->min + (d->max - d->min)*rpos;
  const float base = powf(10.0f, d->digits);
  rpos = roundf(base * rpos) / base;
  d->pos = (rpos-d->min)/(d->max-d->min);
  gtk_widget_queue_draw(GTK_WIDGET(w));
  d->is_changed = 1;
  if(!darktable.gui->reset && !d->is_dragging)
  {
    g_signal_emit_by_name(G_OBJECT(w), "value-changed");
    d->is_changed = 0;
  }
}

static gboolean
dt_bauhaus_slider_postponed_value_change(gpointer data)
{
  if(!GTK_IS_WIDGET(data)) return 0;

  gdk_threads_enter();
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)data;
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  if(d->is_changed)
  {
    g_signal_emit_by_name(G_OBJECT(w),"value-changed");
    d->is_changed = 0;
  }

  if(!d->is_dragging) d->timeout_handle = 0;

  gdk_threads_leave();

  return d->is_dragging;
}

static gboolean
dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
{
  switch(darktable.bauhaus->current->type)
  {
    case DT_BAUHAUS_SLIDER:
    {
      // hack to do screenshots from popup:
      // if(event->string[0] == 'p') return system("scrot");
      // else
      if(darktable.bauhaus->keys_cnt + 2 < 64 &&
          ((event->string[0] >= 43 && event->string[0] <= 46) ||  // .,-+
           (event->string[0] >= 48 && event->string[0] <= 57)))   // 0-9
      {
        darktable.bauhaus->keys[darktable.bauhaus->keys_cnt++] = event->string[0];
        gtk_widget_queue_draw(darktable.bauhaus->popup_area);
      }
      else if(darktable.bauhaus->keys_cnt > 0 &&
              (event->keyval == GDK_BackSpace || event->keyval == GDK_Delete))
      {
        darktable.bauhaus->keys[--darktable.bauhaus->keys_cnt] = 0;
        gtk_widget_queue_draw(darktable.bauhaus->popup_area);
      }
      else if(darktable.bauhaus->keys_cnt > 0 && darktable.bauhaus->keys_cnt + 1 < 64 &&
              (event->keyval == GDK_Return || event->keyval == GDK_KP_Enter))
      {
        // accept input
        darktable.bauhaus->keys[darktable.bauhaus->keys_cnt] = 0;
        // unnormalized input, user was typing this:
        dt_bauhaus_slider_set(GTK_WIDGET(darktable.bauhaus->current), atof(darktable.bauhaus->keys));
        darktable.bauhaus->keys_cnt = 0;
        memset(darktable.bauhaus->keys, 0, 64);
        dt_bauhaus_hide_popup();
      }
      else if(event->keyval == GDK_Escape)
      {
        // discard input and close popup
        darktable.bauhaus->keys_cnt = 0;
        memset(darktable.bauhaus->keys, 0, 64);
        dt_bauhaus_hide_popup();
      }
      else return FALSE;
      return TRUE;
    }
    case DT_BAUHAUS_COMBOBOX:
    {
      if(!g_utf8_validate(event->string, -1, NULL)) return FALSE;
      gunichar c = g_utf8_get_char(event->string);
      long int char_width = g_utf8_next_char(event->string) - event->string;
      // if(event->string[0] == 'p') return system("scrot");
      // else
      if(darktable.bauhaus->keys_cnt + 1 + char_width < 64 && g_unichar_isprint(c))
      {
        // only accept key input if still valid or editable?
        g_utf8_strncpy(darktable.bauhaus->keys+darktable.bauhaus->keys_cnt, event->string, 1);
        darktable.bauhaus->keys_cnt += char_width;
        gtk_widget_queue_draw(darktable.bauhaus->popup_area);
      }
      else if(darktable.bauhaus->keys_cnt > 0 &&
              (event->keyval == GDK_BackSpace || event->keyval == GDK_Delete))
      {
        darktable.bauhaus->keys_cnt -= (darktable.bauhaus->keys+darktable.bauhaus->keys_cnt) -
                                        g_utf8_prev_char(darktable.bauhaus->keys+darktable.bauhaus->keys_cnt);
        darktable.bauhaus->keys[darktable.bauhaus->keys_cnt] = 0;
        gtk_widget_queue_draw(darktable.bauhaus->popup_area);
      }
      else if(darktable.bauhaus->keys_cnt > 0 && darktable.bauhaus->keys_cnt + 1 < 64 &&
              (event->keyval == GDK_Return || event->keyval == GDK_KP_Enter))
      {
        // accept unique matches only for editable:
        if(darktable.bauhaus->current->data.combobox.editable)
          darktable.bauhaus->end_mouse_y = FLT_MAX;
        else
          darktable.bauhaus->end_mouse_y = 0;
        darktable.bauhaus->keys[darktable.bauhaus->keys_cnt] = 0;
        dt_bauhaus_widget_accept(darktable.bauhaus->current);
        darktable.bauhaus->keys_cnt = 0;
        memset(darktable.bauhaus->keys, 0, 64);
        dt_bauhaus_hide_popup();
      }
      else if(event->keyval == GDK_Escape)
      {
        // discard input and close popup
        darktable.bauhaus->keys_cnt = 0;
        memset(darktable.bauhaus->keys, 0, 64);
        dt_bauhaus_hide_popup();
      }
      else if(event->keyval == GDK_Up)
      {
        combobox_popup_scroll(1);
      }
      else if(event->keyval == GDK_Down)
      {
        combobox_popup_scroll(0);
      }
      else if(event->keyval == GDK_Return || event->keyval == GDK_KP_Enter)
      {
        // return pressed, but didn't type anything
        darktable.bauhaus->end_mouse_y = -1; // negative will use currently highlighted instead.
        darktable.bauhaus->keys[darktable.bauhaus->keys_cnt] = 0;
        darktable.bauhaus->keys_cnt = 0;
        memset(darktable.bauhaus->keys, 0, 64);
        dt_bauhaus_widget_accept(darktable.bauhaus->current);
        dt_bauhaus_hide_popup();
      }
      else return FALSE;
      return TRUE;
    }
    default:
      return FALSE;
  }
}

static gboolean
dt_bauhaus_slider_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
{
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
  dt_iop_request_focus(w->module);
  GtkAllocation tmp;
  gtk_widget_get_allocation(GTK_WIDGET(w), &tmp);
  if(w->quad_paint && (event->x > widget->allocation.width - widget->allocation.height))
  {
    g_signal_emit_by_name(G_OBJECT(w), "quad-pressed");
    return TRUE;
  }
  else if(event->button == 3)
  {
    dt_bauhaus_show_popup(w);
    return TRUE;
  }
  else if(event->button == 1)
  {
    // reset to default.
    if(event->type == GDK_2BUTTON_PRESS)
    {
      dt_bauhaus_slider_data_t *d = &w->data.slider;
      d->is_dragging = 0;
      dt_bauhaus_slider_set_normalized(w, d->defpos);
    }
    else
    {
      const float l = 4.0f/tmp.width;
      const float r = 1.0f-(tmp.height+4.0f)/tmp.width;
      dt_bauhaus_slider_set_normalized(w, (event->x/tmp.width - l)/(r-l));
      dt_bauhaus_slider_data_t *d = &w->data.slider;
      d->is_dragging = 1;
      int delay = CLAMP(darktable.develop->average_delay*3/2, DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MIN, DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MAX);
      // timeout_handle should always be zero here, but check just in case
      if(!d->timeout_handle)
        d->timeout_handle = g_timeout_add(delay, dt_bauhaus_slider_postponed_value_change, widget);
    }
    return TRUE;
  }
  return FALSE;
}

static gboolean
dt_bauhaus_slider_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
{
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
  dt_bauhaus_slider_data_t *d = &w->data.slider;

  if((event->button == 1) && (d->is_dragging))
  {
    dt_iop_request_focus(w->module);
    GtkAllocation tmp;
    gtk_widget_get_allocation(GTK_WIDGET(w), &tmp);
    d->is_dragging = 0;
    if(d->timeout_handle) g_source_remove(d->timeout_handle);
    d->timeout_handle = 0;
    const float l = 4.0f/tmp.width;
    const float r = 1.0f-(tmp.height+4.0f)/tmp.width;
    dt_bauhaus_slider_set_normalized(w, (event->x/tmp.width - l)/(r-l));

    return TRUE;
  }
  return FALSE;
}

static gboolean
dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
{
  // remember mouse position for motion effects in draw
  if(event->state & GDK_BUTTON1_MASK && event->type != GDK_2BUTTON_PRESS)
  {
    dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
    GtkAllocation tmp;
    gtk_widget_get_allocation(GTK_WIDGET(w), &tmp);
    const float l = 4.0f/tmp.width;
    const float r = 1.0f-(tmp.height+4.0f)/tmp.width;
    dt_bauhaus_slider_set_normalized(w, (event->x/tmp.width - l)/(r-l));
  }
  // not sure if needed:
  // gdk_event_request_motions(event);
  return TRUE;
}

static gboolean
dt_bauhaus_slider_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
{
  // TODO: highlight?
  return TRUE;
}

void dt_bauhaus_vimkey_exec(const char *input)
{
  char module[64], label[64];
  float num;
  sscanf(input, ":set %[^.].%[^=]=%f", module, label, &num);
  fprintf(stderr, "[vimkey] setting module `%s', slider `%s' to %f\n", module, label, num);
  sscanf(input, ":set %[^=]=%f", label, &num);
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)g_hash_table_lookup(darktable.bauhaus->keymap, label);
  if(!w) return;
  switch(w->type)
  {
    case DT_BAUHAUS_SLIDER:
      dt_bauhaus_slider_set(GTK_WIDGET(w), num);
      break;
    case DT_BAUHAUS_COMBOBOX:
      // TODO: what about text as entry?
      dt_bauhaus_combobox_set(GTK_WIDGET(w), num);
      break;
    default:
      break;
  }
}

// give autocomplete suggestions
GList* dt_bauhaus_vimkey_complete(const char *input)
{
  GList *cmp = darktable.bauhaus->key_mod;
  char *point = strstr(input, ".");
  if(point)
    cmp = darktable.bauhaus->key_val;
  int prefix = strlen(input);
  GList *res = NULL;
  int after = 0;
  while(cmp)
  {
    char *path = (char *)cmp->data;
    if(strncasecmp(path, input, prefix))
    {
      if(after) break; // sorted, so we're done
      // else loop till we find the start of it
    }
    else
    {
      // append:
      res = g_list_insert_sorted(res, path, (GCompareFunc)strcmp);
      after = 1;
    }
    cmp = g_list_next(cmp);
  }
  return res;
}

// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-space on;
back to top