Revision 74f2632206090a0dfd2cc7ddb91a875f22a9347b authored by houz on 19 December 2015, 21:41:39 UTC, committed by houz on 19 December 2015, 21:41:39 UTC
Possible fix for the import dialog crash.
2 parent s 6c23073 + 269ad77
Raw File
bauhaus.c
/*
    This file is part of darktable,
    copyright (c) 2012 johannes hanika.
    copyright (c) 2012--2014 tobias ellinghaus.

    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/calculator.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

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
// to be removed when we will depend on glib-2.38+
// see https://bugzilla.gnome.org/show_bug.cgi?id=723899
// 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)
#pragma GCC diagnostic pop

// fwd declare
static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *cr, 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;
}

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

// 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);
  if(text) {
    pango_layout_set_text(layout, text, -1);
  } else {
    // length of -1 is not allowed with NULL string (wtf)
    pango_layout_set_text(layout, NULL, 0);
  }

  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 gboolean _cursor_timeout_callback(gpointer user_data)
{
  if(darktable.bauhaus->cursor_blink_counter > 0) darktable.bauhaus->cursor_blink_counter--;

  darktable.bauhaus->cursor_visible = !darktable.bauhaus->cursor_visible;
  gtk_widget_queue_draw(darktable.bauhaus->popup_area);

  if(darktable.bauhaus->cursor_blink_counter
     != 0) // this can be >0 when we haven't reached the desired number or -1 when blinking forever
    return TRUE;

  darktable.bauhaus->cursor_timeout = 0; // otherwise the cursor won't come up when starting to type
  return FALSE;
}

static void _start_cursor(int max_blinks)
{
  darktable.bauhaus->cursor_blink_counter = max_blinks;
  darktable.bauhaus->cursor_visible = FALSE;
  if(darktable.bauhaus->cursor_timeout == 0)
    darktable.bauhaus->cursor_timeout = g_timeout_add(500, _cursor_timeout_callback, NULL);
}

static void _stop_cursor()
{
  if(darktable.bauhaus->cursor_timeout > 0)
  {
    g_source_remove(darktable.bauhaus->cursor_timeout);
    darktable.bauhaus->cursor_timeout = 0;
    darktable.bauhaus->cursor_visible = FALSE;
  }
}
// -------------------------------


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 = 50;
    gint wx, wy;
    GtkWidget *widget = darktable.bauhaus->popup_window;
    GtkAllocation allocation;
    gtk_widget_get_allocation(widget, &allocation);
    gdk_window_get_origin(gtk_widget_get_window(widget), &wx, &wy);
    if(event->x_root > wx + allocation.width + tol || event->y_root > wy + 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 = 0;
    gint wx, wy;
    GtkWidget *widget = darktable.bauhaus->popup_window;
    GtkAllocation allocation;
    gtk_widget_get_allocation(widget, &allocation);
    gdk_window_get_origin(gtk_widget_get_window(widget), &wx, &wy);
    if((event->x_root > wx + allocation.width + tol || event->y_root > wy + 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, px, py;
  GtkWidget *w = GTK_WIDGET(darktable.bauhaus->current);
  GtkAllocation allocation_w;
  gtk_widget_get_allocation(w, &allocation_w);
  const int ht = allocation_w.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;
  int new_value;
  if(up)
    new_value = CLAMP(d->active - 1, 0, d->num_labels - 1);
  else
    new_value = CLAMP(d->active + 1, 0, d->num_labels - 1);

  // we move the popup up or down
  gdk_window_get_origin(gtk_widget_get_window(darktable.bauhaus->popup_window), &px, &py);
  if(new_value == d->active)
    gdk_window_move(gtk_widget_get_window(darktable.bauhaus->popup_window), wx, wy - d->active * skip);
  else if(new_value > d->active)
    gdk_window_move(gtk_widget_get_window(darktable.bauhaus->popup_window), wx, py - skip);
  else
    gdk_window_move(gtk_widget_get_window(darktable.bauhaus->popup_window), wx, py + skip);

  // make sure highlighted entry is updated:
  darktable.bauhaus->mouse_x = 0;
  darktable.bauhaus->mouse_y = new_value * skip + ht / 2;
  gtk_widget_queue_draw(darktable.bauhaus->popup_area);

  // and we change the value
  dt_bauhaus_combobox_set(w, new_value);
}


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)
{
  GtkAllocation allocation_popup_window;
  gtk_widget_get_allocation(darktable.bauhaus->popup_window, &allocation_popup_window);
  gtk_widget_queue_draw(darktable.bauhaus->popup_area);
  dt_bauhaus_widget_t *w = darktable.bauhaus->current;
  GtkAllocation allocation_w;
  gtk_widget_get_allocation(GTK_WIDGET(w), &allocation_w);
  int width = allocation_popup_window.width, height = allocation_popup_window.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;
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);

  if(darktable.bauhaus->keys_cnt == 0) _stop_cursor();

  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,
                                                     allocation_w.height / (float)height, 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 > allocation.width + 20 || event->y > 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);
    gdk_device_get_position(
        gdk_device_manager_get_client_pointer(gdk_display_get_device_manager(gdk_display_get_default())),
        NULL, &x, &y);
    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)
{
  GdkDisplay *display = gtk_widget_get_display(w);
  GdkDeviceManager *mgr = gdk_display_get_device_manager(display);
  GList *devices = gdk_device_manager_list_devices(mgr, GDK_DEVICE_TYPE_MASTER);
  GList *tmp = devices;
  while(tmp)
  {
    GdkDevice *dev = tmp->data;
    if(gdk_device_get_source(dev) == GDK_SOURCE_KEYBOARD)
    {
      gdk_device_grab(dev, gtk_widget_get_window(w), GDK_OWNERSHIP_NONE, FALSE,
                      GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK, NULL, GDK_CURRENT_TIME);
    }
    tmp = tmp->next;
  }
  g_list_free(devices);
}

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->draw = dt_bauhaus_draw;
}

void dt_bauhaus_init()
{
  darktable.bauhaus = (dt_bauhaus_t *)calloc(1, 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;
  g_strlcpy(darktable.bauhaus->label_font, "sans", sizeof(darktable.bauhaus->label_font));
  g_strlcpy(darktable.bauhaus->value_font, "sans", sizeof(darktable.bauhaus->value_font));
  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;

  GtkStyleContext *ctx = gtk_style_context_new();
  GtkWidgetPath *path;
  path = gtk_widget_path_new ();
  int pos = gtk_widget_path_append_type(path, GTK_TYPE_WIDGET);
  gtk_widget_path_iter_set_name(path, pos, "iop-plugin-ui");
  gtk_style_context_set_path(ctx, path);
  gtk_style_context_set_screen (ctx, gtk_widget_get_screen(root_window));

  GdkRGBA color, selected_color, bg_color, selected_bg_color;
  gtk_style_context_get_color(ctx, GTK_STATE_FLAG_NORMAL, &color);
  {
    GdkRGBA *bc;
    gtk_style_context_get(ctx, GTK_STATE_FLAG_NORMAL, "background-color", &bc, NULL);
    bg_color = *bc;
    gdk_rgba_free(bc);
  }
  gtk_style_context_get_color(ctx, GTK_STATE_FLAG_SELECTED, &selected_color);
  {
    GdkRGBA *sbc;
    gtk_style_context_get(ctx, GTK_STATE_FLAG_SELECTED, "background-color", &sbc, NULL);
    selected_bg_color = *sbc;
    gdk_rgba_free(sbc);
  }
  PangoFontDescription *pfont = 0;
  gtk_style_context_get(ctx, GTK_STATE_FLAG_NORMAL, "font", &pfont, NULL);
  darktable.bauhaus->bg_normal = bg_color.red;
  darktable.bauhaus->bg_focus = selected_bg_color.red;
  gtk_widget_path_free(path);

  darktable.bauhaus->pango_font_desc = pfont;

  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_connect(darktable.bauhaus->popup_area);

  gtk_widget_set_size_request(darktable.bauhaus->popup_area, DT_PIXEL_APPLY_DPI(300), DT_PIXEL_APPLY_DPI(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 | GDK_SCROLL_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), "draw", G_CALLBACK(dt_bauhaus_popup_draw),
                   (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!
  g_list_free_full(darktable.bauhaus->key_mod, (GDestroyNotify)g_free);
  g_list_free_full(darktable.bauhaus->key_val, (GDestroyNotify)g_free);
}

// 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_draw(GtkWidget *widget, cairo_t *crf, 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;
  w->combo_populate = NULL;
  gtk_widget_set_size_request(GTK_WIDGET(w), -1, 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 | GDK_SCROLL_MASK);

  g_signal_connect(G_OBJECT(w), "draw", G_CALLBACK(dt_bauhaus_draw), 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_combobox_set_default(GtkWidget *widget, int def)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_combobox_data_t *d = &w->data.combobox;
  d->defpos = def;
}

void dt_bauhaus_slider_set_hard_min(GtkWidget* widget, float val)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  float pos = dt_bauhaus_slider_get(widget);
  d->hard_min = val;
  d->min = MAX(d->min, d->hard_min);
  d->soft_min = MAX(d->soft_min, d->hard_min);
  if(val > d->hard_max) dt_bauhaus_slider_set_hard_max(widget,val);
  if(pos < val)
  {
    dt_bauhaus_slider_set_soft(widget,val);
  }
  else 
  {
    dt_bauhaus_slider_set_soft(widget,pos);
  }
}

float dt_bauhaus_slider_get_hard_min(GtkWidget* widget)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  return d->hard_min;
}

void dt_bauhaus_slider_set_hard_max(GtkWidget* widget, float val)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  float pos = dt_bauhaus_slider_get(widget);
  d->hard_max = val;
  d->max = MIN(d->max, d->hard_max);
  d->soft_max = MIN(d->soft_max, d->hard_max);
  if(val < d->hard_min) dt_bauhaus_slider_set_hard_min(widget,val);
  if(pos > val) {

    dt_bauhaus_slider_set_soft(widget,val);
  } 
  else 
  {
    dt_bauhaus_slider_set_soft(widget,pos);
  }
}

float dt_bauhaus_slider_get_hard_max(GtkWidget* widget)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  return d->hard_max;
}

void dt_bauhaus_slider_set_soft_min(GtkWidget* widget, float val)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  float pos = dt_bauhaus_slider_get(widget);
  d->soft_min = val;
  d->hard_min = MIN(d->hard_min,d->soft_min);
  d->min =  d->soft_min;
  if(val > d->soft_max) dt_bauhaus_slider_set_soft_max(widget,val);
  if(val > d->hard_max) dt_bauhaus_slider_set_hard_max(widget,val);
  if(pos < val) 
  {
    dt_bauhaus_slider_set_soft(widget,val);
  } 
  else 
  {
    dt_bauhaus_slider_set_soft(widget,pos);
  }
}

float dt_bauhaus_slider_get_soft_min(GtkWidget* widget)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  return d->soft_min;
}

void dt_bauhaus_slider_set_soft_max(GtkWidget* widget, float val)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  float pos = dt_bauhaus_slider_get(widget);
  d->soft_max = val;
  d->hard_max = MAX(d->soft_max, d->hard_max);
  d->max =  d->soft_max;
  if(val < d->soft_min) dt_bauhaus_slider_set_soft_min(widget,val);
  if(val < d->hard_min) dt_bauhaus_slider_set_hard_min(widget,val);
  if(pos > val) {
    dt_bauhaus_slider_set_soft(widget,val);
  } else {
    dt_bauhaus_slider_set_soft(widget,pos);
  }
}

float dt_bauhaus_slider_get_soft_max(GtkWidget* widget)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  return d->soft_max;
}

void dt_bauhaus_slider_set_default(GtkWidget *widget, float def)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  d->defpos = (def - d->min) / (d->max - d->min);
}

void dt_bauhaus_slider_enable_soft_boundaries(GtkWidget *widget, float hard_min, float hard_max)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  dt_bauhaus_slider_data_t *d = &w->data.slider;
  d->hard_min = hard_min;
  d->hard_max = hard_max;
}

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

  if(w->module)
  {
    // construct control path name and insert into keymap:
    gchar *path;
    if(section && section[0] != '\0')
    {
      path = g_strdup_printf("%s.%s.%s", w->module->name(), section, w->label);
      gchar *section_path = g_strdup_printf("%s.%s", w->module->name(), section);
      if(!g_list_find_custom(darktable.bauhaus->key_val, section_path, (GCompareFunc)strcmp))
        darktable.bauhaus->key_val
            = g_list_insert_sorted(darktable.bauhaus->key_val, section_path, (GCompareFunc)strcmp);
      else
        g_free(section_path);
    }
    else
      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);
        else
          g_free(mod);

        // 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);
  }
}

const char* dt_bauhaus_widget_get_label(GtkWidget *widget)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  return w->label;
}

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)
{
  return dt_bauhaus_slider_new_with_range_and_feedback(self, min, max, step, defval, digits, 1);
}

GtkWidget *dt_bauhaus_slider_new_with_range_and_feedback(dt_iop_module_t *self, float min, float max,
                                                         float step, float defval, int digits, int feedback)
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(g_object_new(DT_BAUHAUS_WIDGET_TYPE, NULL));
  return dt_bauhaus_slider_from_widget(w,self, min, max, step, defval, digits, feedback);
}
GtkWidget *dt_bauhaus_slider_from_widget(dt_bauhaus_widget_t* w,dt_iop_module_t *self, float min, float max,
                                                         float step, float defval, int digits, int feedback)
{
  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 = d->soft_min = d->hard_min = min;
  d->max = d->soft_max = d->hard_max = max;
  d->step = step;
  // 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, sizeof(d->format), "%%.0%df", digits);

  d->grad_cnt = 0;

  d->fill_feedback = feedback;

  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_free_full(d->labels, g_free);
  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));
  dt_bauhaus_combobox_from_widget(w,self);
  return GTK_WIDGET(w);
}

void dt_bauhaus_combobox_from_widget(dt_bauhaus_widget_t* w,dt_iop_module_t *self)
{
  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);
}

void dt_bauhaus_combobox_add_populate_fct(GtkWidget *widget, void (*fct)(struct dt_iop_module_t **module))
{
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  if(w->type != DT_BAUHAUS_COMBOBOX) return;
  w->combo_populate = fct;
}

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;
}

int dt_bauhaus_combobox_get_editable(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->editable;
}

void dt_bauhaus_combobox_remove_at(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;

  if(pos < 0 || pos >= d->num_labels) return;

  GList *rm = g_list_nth(d->labels, pos);
  d->labels = g_list_delete_link(d->labels, rm);
  d->num_labels--;
}

void dt_bauhaus_combobox_insert(GtkWidget *widget, const char *text,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->num_labels++;
  d->labels = g_list_insert(d->labels, g_strdup(text),pos);
}

int dt_bauhaus_combobox_length(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->num_labels;
}

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_free_full(d->labels, g_free);
  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;
  g_strlcpy(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");
  }
}


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);
}

static void dt_bauhaus_draw_indicator(dt_bauhaus_widget_t *w, float pos, cairo_t *cr)
{
  // draw scale indicator
  GtkWidget *widget = GTK_WIDGET(w);
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  if(w->type != DT_BAUHAUS_SLIDER) return;
  const int wd = allocation.width;
  const int ht = 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)
  {
    if(darktable.develop->gui_module == w->module)
      set_bg_focus(cr);
    else
      set_bg_normal(cr);
  }
  else
  {
    // lib modules always with bright bg
    set_bg_focus(cr);
  }
  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);
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  int width = allocation.width;
  int height = 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);
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  if(w->type != DT_BAUHAUS_SLIDER) return;
  const int wd = allocation.width;
  const int ht = 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);
    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], .25f);
    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'
  // - but only if set (default, but might not be good for 'balance' sliders)
  if(d->fill_feedback)
  {
    // only change luminosity, useful for colored sliders to not get too faint:
    cairo_set_operator(cr, CAIRO_OPERATOR_HSL_LUMINOSITY);
    set_indicator_color(cr, 0);
    cairo_rectangle(cr, 2, htm, d->pos * (wd - 4 - ht - 2), htM);
    cairo_fill(cr);
    // change back to default cairo operator:
    cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
  }

  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);
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  GtkAllocation allocation_popup_window;
  gtk_widget_get_allocation(darktable.bauhaus->popup_window, &allocation_popup_window);
  int width = allocation_popup_window.width, height = allocation_popup_window.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 / (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));
          g_strlcpy(d->text, darktable.bauhaus->keys, 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, allocation.height / (float)height, allocation.width);
      dt_bauhaus_slider_set_normalized(w, d->oldpos + mouse_off);
      d->oldpos = d->pos;
      break;
    }
    default:
      break;
  }
}

static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
{
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  dt_bauhaus_widget_t *w = darktable.bauhaus->current;
  // dimensions of the popup
  int width = allocation.width, height = allocation.height;
  // dimensions of the original line
  GtkWidget *current = GTK_WIDGET(w);
  GtkAllocation allocation_current;
  gtk_widget_get_allocation(current, &allocation_current);
  int wd = allocation_current.width, ht = allocation_current.height;
  cairo_surface_t *cst = dt_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, sizeof(text), 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;
          }

          if(text[0] == '<')
          {
            gchar *text2 = (gchar *)(text + 1);
            show_pango_text(cr, text2, 2, (get_line_space() + ht) * k, 0, FALSE, !highlight, highlight);
          }
          else
          {
            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.
    const int line_height = get_line_height();
    cairo_set_font_size(cr, MIN(3 * 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);
  }
  if(darktable.bauhaus->cursor_visible)
  {
    // show the blinking cursor
    cairo_save(cr);
    cairo_set_source_rgb(cr, 1, 1, 1);
    const int line_height = get_line_height();
    cairo_set_font_size(cr, MIN(3 * line_height, .2 * height));
    cairo_move_to(cr, wd - ht + 3, height * 0.5 + line_height);
    cairo_line_to(cr, wd - ht + 3, height * 0.5 - 3 * line_height);
    cairo_stroke(cr);
    cairo_restore(cr);
  }

  cairo_destroy(cr);
  cairo_set_source_surface(crf, cst, 0, 0);
  cairo_paint(crf);
  cairo_surface_destroy(cst);

  return TRUE;
}

static gboolean dt_bauhaus_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
{
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
  const int width = allocation.width, height = allocation.height;
  cairo_surface_t *cst = dt_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, gtk_widget_is_sensitive(widget), 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, sizeof(text), 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, gtk_widget_is_sensitive(widget), FALSE);
    }
    break;
    default:
      break;
  }
  cairo_restore(cr);

  cairo_destroy(cr);
  cairo_set_source_surface(crf, cst, 0, 0);
  cairo_paint(crf);
  cairo_surface_destroy(cst);

  return TRUE;
}

void dt_bauhaus_hide_popup()
{
  if(darktable.bauhaus->current)
  {
    GdkDisplay *display = gdk_display_get_default();
    GdkDeviceManager *mgr = gdk_display_get_device_manager(display);
    GList *devices = gdk_device_manager_list_devices(mgr, GDK_DEVICE_TYPE_MASTER);
    GList *tmp = devices;
    while(tmp)
    {
      GdkDevice *dev = tmp->data;
      if(gdk_device_get_source(dev) == GDK_SOURCE_KEYBOARD)
      {
        gdk_device_ungrab(dev, GDK_CURRENT_TIME);
      }
      tmp = tmp->next;
    }
    g_list_free(devices);
    gtk_widget_hide(darktable.bauhaus->popup_window);
    darktable.bauhaus->current = NULL;
    // TODO: give focus to center view? do in accept() as well?
  }
  _stop_cursor();
}

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, sizeof(darktable.bauhaus->keys));
  darktable.bauhaus->change_active = 0;
  darktable.bauhaus->mouse_line_distance = 0.0f;
  _stop_cursor();

  if(w->module) 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);
      _start_cursor(6);
      break;
    }
    case DT_BAUHAUS_COMBOBOX:
    {
      // we launch the dynamic populate fct if any
      if(w->combo_populate) w->combo_populate(&w->module);
      // 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);
      GtkAllocation allocation_w;
      gtk_widget_get_allocation(GTK_WIDGET(w), &allocation_w);
      const int ht = allocation_w.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 before move, as this results in noticeable 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)
  {
    if(w->module) 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)
  {
    if(w->module) 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)
  {
    if(w->module) 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)
  {
    if(w->module) 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)
{
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
  if(w->type != DT_BAUHAUS_COMBOBOX) return FALSE;
  if(w->module) 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 > allocation.width - 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.
      // (except in weird corner cases where the popup is under the -1st entry
      dt_bauhaus_combobox_set(widget, d->defpos);
      dt_bauhaus_hide_popup();
    }
    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;
  if(d->max == d->min) {
    return d->max;
  }
  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));
}

float dt_bauhaus_slider_get_step(GtkWidget *widget)
{
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);

  if(w->type != DT_BAUHAUS_SLIDER) return 0;

  dt_bauhaus_slider_data_t *d = &w->data.slider;

  return d->step;
}

void dt_bauhaus_slider_reset(GtkWidget *widget)
{
  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;

  d->min = d->soft_min;
  d->max = d->soft_max;
  dt_bauhaus_slider_set_normalized(w, d->defpos);

  return;
}

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;
  g_strlcpy(d->format, format, sizeof(d->format));
}

void dt_bauhaus_slider_set_soft(GtkWidget *widget, float pos)
{
  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;
  float rpos = CLAMP(pos, d->hard_min, d->hard_max);
  d->min = MIN(d->min, rpos);
  d->max = MAX(d->max, rpos);
  rpos = (rpos - d->min) / (d->max - d->min);
  dt_bauhaus_slider_set_normalized(w, rpos);
}

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;

  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;

  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->keyval == GDK_KEY_space || event->keyval == GDK_KEY_KP_Space || // SPACE
             event->keyval == GDK_KEY_percent ||                                    // %
             (event->string[0] >= 40 && event->string[0] <= 57) ||                  // ()+-*/.,0-9
             event->keyval == GDK_KEY_asciicircum ||                                // ^
             event->keyval == GDK_KEY_X || event->keyval == GDK_KEY_x))             // Xx
      {
        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_KEY_BackSpace || event->keyval == GDK_KEY_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_KEY_Return || event->keyval == GDK_KEY_KP_Enter))
      {
        // accept input
        darktable.bauhaus->keys[darktable.bauhaus->keys_cnt] = 0;
        // unnormalized input, user was typing this:
        float old_value = dt_bauhaus_slider_get(GTK_WIDGET(darktable.bauhaus->current));
        float new_value = dt_calculator_solve(old_value, darktable.bauhaus->keys);
        if(isfinite(new_value)) dt_bauhaus_slider_set_soft(GTK_WIDGET(darktable.bauhaus->current), new_value);
        darktable.bauhaus->keys_cnt = 0;
        memset(darktable.bauhaus->keys, 0, sizeof(darktable.bauhaus->keys));
        dt_bauhaus_hide_popup();
      }
      else if(event->keyval == GDK_KEY_Escape)
      {
        // discard input and close popup
        darktable.bauhaus->keys_cnt = 0;
        memset(darktable.bauhaus->keys, 0, sizeof(darktable.bauhaus->keys));
        dt_bauhaus_hide_popup();
      }
      else
        return FALSE;
      if(darktable.bauhaus->keys_cnt > 0) _start_cursor(-1);
      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_KEY_BackSpace || event->keyval == GDK_KEY_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_KEY_Return || event->keyval == GDK_KEY_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, sizeof(darktable.bauhaus->keys));
        dt_bauhaus_hide_popup();
      }
      else if(event->keyval == GDK_KEY_Escape)
      {
        // discard input and close popup
        darktable.bauhaus->keys_cnt = 0;
        memset(darktable.bauhaus->keys, 0, sizeof(darktable.bauhaus->keys));
        dt_bauhaus_hide_popup();
      }
      else if(event->keyval == GDK_KEY_Up)
      {
        combobox_popup_scroll(1);
      }
      else if(event->keyval == GDK_KEY_Down)
      {
        combobox_popup_scroll(0);
      }
      else if(event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_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, sizeof(darktable.bauhaus->keys));
        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)
{
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
  if(w->module) dt_iop_request_focus(w->module);
  GtkAllocation tmp;
  gtk_widget_get_allocation(GTK_WIDGET(w), &tmp);
  if(w->quad_paint && (event->x > allocation.width - 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_reset(GTK_WIDGET(w));
    }
    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))
  {
    if(w->module) 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], value[256], *key;
  float old_value, new_value;

  sscanf(input, ":set %[^.].%[^=]=%s", module, label, value);
  fprintf(stderr, "[vimkey] setting module `%s', slider `%s' to `%s'", module, label, value);
  key = g_strjoin(".", module, label, NULL);
  dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)g_hash_table_lookup(darktable.bauhaus->keymap, key);
  g_free(key);
  if(!w) return;
  switch(w->type)
  {
    case DT_BAUHAUS_SLIDER:
      old_value = dt_bauhaus_slider_get(GTK_WIDGET(w));
      new_value = dt_calculator_solve(old_value, value);
      fprintf(stderr, " = %f\n", new_value);
      if(isfinite(new_value)) dt_bauhaus_slider_set_soft(GTK_WIDGET(w), new_value);
      break;
    case DT_BAUHAUS_COMBOBOX:
      // TODO: what about text as entry?
      old_value = dt_bauhaus_combobox_get(GTK_WIDGET(w));
      new_value = dt_calculator_solve(old_value, value);
      fprintf(stderr, " = %f\n", new_value);
      if(isfinite(new_value)) dt_bauhaus_combobox_set(GTK_WIDGET(w), new_value);
      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