Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions src/control/control.c
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,9 @@ void dt_control_cursor_debug(const char *owner,

static void _change_cursor_with_fallback(const char *cursor_name,
gboolean is_temp,
const char *owner)
const char *owner,
GtkWidget *widget)
{
GtkWidget *widget = dt_ui_main_window(darktable.gui->ui);
if(!widget) return;
GdkCursor *cursor = dt_gui_cursor_new_for_name(gtk_widget_get_display(widget), cursor_name);

Expand Down Expand Up @@ -327,7 +327,7 @@ void dt_control_set_temp_cursor(const char *cursor_name)
if(_prev_cursor)
g_object_ref(_prev_cursor);
}
_change_cursor_with_fallback(cursor_name, TRUE, "control/temp");
_change_cursor_with_fallback(cursor_name, TRUE, "control/temp", widget);
}

void dt_control_clear_temp_cursor()
Expand All @@ -352,9 +352,31 @@ void dt_control_clear_temp_cursor()
_prev_cursor = NULL;
}

/* This is the shared, application-wide cursor: it has to stay on the toplevel.
*
* Its callers are of three kinds, and only the toplevel window is under the
* pointer for all three: whole-application states (view switching, startup,
* leaving shortcut-mapping mode), on-canvas interaction (crop, ashift,
* clipping, the darkroom view itself) and a handful of panel widgets that have
* not been migrated yet.
*
* Aiming it at dt_ui_center() instead splits the target in two, because the
* code that sets a cursor and the code that clears it then write different
* GdkWindows: _set_mapping_mode_cursor() and dt_control_set_temp_cursor()
* apply to the toplevel, so their cursor is never cleared and stays stuck.
* There is no rectangle where the retargeted clear is even visible in
* lighttable, since the thumbtable is an overlay sibling of the centre canvas
* rather than a child of it, so the pointer is never over dt_ui_center().
*
* A widget that owns its own interaction should call dt_gui_cursor_set() with
* its own widget rather than come through here -- that is what the panel
* handles, the range selector, the timeline and the resize wrappers now do,
* and it is also what stops a child control from claiming the toplevel cursor.
*/
void dt_control_change_cursor(const char *cursor_name)
{
_change_cursor_with_fallback(cursor_name, FALSE, "control/change");
_change_cursor_with_fallback(cursor_name, FALSE, "control/change",
dt_ui_main_window(darktable.gui->ui));
}

/* Some implementation and how-to use notes about control->running
Expand Down
11 changes: 6 additions & 5 deletions src/dtgtk/range.c
Original file line number Diff line number Diff line change
Expand Up @@ -1509,14 +1509,15 @@ void dtgtk_range_select_redraw(GtkDarktableRangeSelect *range)

static void _event_band_motion_cb(GtkEventControllerMotion *controller, double x, double y, GtkDarktableRangeSelect *range)
{
GtkWidget *widget = dt_gui_get_widget(controller);
range->current_x_px = x - range->alloc_padding.x;

// if we are outside the graph, don't go further
const gboolean inside = (range->current_x_px >= 0 && range->current_x_px <= range->alloc_padding.width);
if(!inside)
{
range->mouse_inside = HOVER_OUTSIDE;
dt_control_change_cursor("default");
dt_gui_cursor_set(widget, NULL, "range-select");
_current_hide_popup(range);
return;
}
Expand All @@ -1538,27 +1539,27 @@ static void _event_band_motion_cb(GtkEventControllerMotion *controller, double x
&& fabs(range->current_x_px - smin_px) <= SNAP_SIZE)
{
range->mouse_inside = HOVER_MIN;
dt_control_change_cursor("w-resize");
dt_gui_cursor_set(widget, "w-resize", "range-select");
}
else if(range->allow_resize
&& !range->set_selection
&& fabs(range->current_x_px - smax_px) <= SNAP_SIZE)
{
range->mouse_inside = HOVER_MAX;
dt_control_change_cursor("e-resize");
dt_gui_cursor_set(widget, "e-resize", "range-select");
}
else
{
range->mouse_inside = HOVER_INSIDE;
dt_control_change_cursor("default");
dt_gui_cursor_set(widget, NULL, "range-select");
}
gtk_widget_queue_draw(range->band);
}

static void _event_band_leave_cb(GtkEventControllerMotion *controller, GtkDarktableRangeSelect *range)
{
range->mouse_inside = HOVER_OUTSIDE;
dt_control_change_cursor("default");
dt_gui_cursor_set(dt_gui_get_widget(controller), NULL, "range-select");
_current_hide_popup(range);

gtk_widget_queue_draw(range->band);
Expand Down
121 changes: 83 additions & 38 deletions src/gui/gtk.c
Original file line number Diff line number Diff line change
Expand Up @@ -3306,13 +3306,15 @@ static void _panel_handle_button_released(GtkGestureSingle *gesture,
static void _panel_handle_cursor_set(GtkWidget *handle, const gboolean entering)
{
// GTK produces a lot of GDK_NOTIFY_ANCESTOR when dragging handle,
// but we only care about events when enter/leave the drag region
// but we only care about events when enter/leave the drag region.
if(darktable.gui->widgets.panel_handle_dragging)
return;
if(strcmp(gtk_widget_get_name(handle), "panel-handle-bottom") == 0)
dt_control_change_cursor(entering ? "ns-resize" : "default");
else
dt_control_change_cursor(entering ? "ew-resize" : "default");

const gboolean bottom =
strcmp(gtk_widget_get_name(handle), "panel-handle-bottom") == 0;
dt_gui_cursor_set(handle,
entering ? (bottom ? "ns-resize" : "ew-resize") : NULL,
"panel-handle");
}

static void _panel_handle_cursor_enter(GtkEventControllerMotion *controller,
Expand Down Expand Up @@ -4898,21 +4900,43 @@ static gboolean _scroll_wrap_height_controller(GtkEventControllerScroll *control
}
#endif

static gboolean _resize_wrap_dragging = FALSE;
static gboolean _resize_wrap_handle_hover = FALSE;
static GtkWidget *_resize_wrap_hovered = NULL;
#define DT_RESIZE_WRAP_STATE_KEY "dt-resize-wrap-state"

typedef struct dt_resize_wrap_state_t
{
gboolean dragging;
gboolean handle_hover;
gboolean pointer_inside;
} dt_resize_wrap_state_t;

static dt_resize_wrap_state_t *_resize_wrap_state(GtkWidget *widget)
{
return g_object_get_data(G_OBJECT(widget), DT_RESIZE_WRAP_STATE_KEY);
}

static void _resize_wrap_set_handle_hover(GtkWidget *widget,
dt_resize_wrap_state_t *state,
const gboolean handle_hover)
{
if(state->handle_hover == handle_hover) return;
state->handle_hover = handle_hover;
dt_gui_cursor_set(widget,
handle_hover ? "ns-resize" : NULL,
"resize-wrap/handle");
}

static gboolean _resize_wrap_draw_handle(GtkWidget *w,
void *cr,
gpointer user_data)
{
if(w != _resize_wrap_hovered)
dt_resize_wrap_state_t *state = _resize_wrap_state(w);
if(!state || !state->pointer_inside)
return FALSE;

GtkAllocation allocation;
gtk_widget_get_allocation(w, &allocation);

set_color(cr, _resize_wrap_handle_hover
set_color(cr, state->handle_hover
? darktable.bauhaus->color_fg_hover
: darktable.bauhaus->color_fg_insensitive);
cairo_move_to(cr, allocation.width / 8 * 3,
Expand All @@ -4925,18 +4949,20 @@ static gboolean _resize_wrap_draw_handle(GtkWidget *w,
return FALSE;
}

/* controller version: the drag uses the controller-relative y, the hover
* check keeps the old window comparison via the current event (BUBBLE phase
* delivers motions over child widgets too, as the old signal did) */
/* Keep resize state per wrapper. Multiple metadata rows can contain resize
* wrappers at once; shared hover/drag state lets one row clear another row's
* cursor. */
static void _resize_wrap_motion_controller(GtkEventControllerMotion *controller,
gdouble x,
gdouble y,
gpointer user_data)
{
GtkWidget *widget = dt_gui_get_widget(controller);
dt_resize_wrap_state_t *state = _resize_wrap_state(widget);
const char *config_str = user_data;
if(!state) return;

if(_resize_wrap_dragging)
if(state->dragging)
{
// keeps resize box from shrinking when user clicks above very
// bottom of handle
Expand All @@ -4956,20 +4982,15 @@ static void _resize_wrap_motion_controller(GtkEventControllerMotion *controller,
return;
}

const gboolean prior = _resize_wrap_handle_hover;
GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller));
if(!(dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)) & GDK_BUTTON1_MASK)
&& (!event || dt_gdk_event_get_window(event) == gtk_widget_get_window(widget)))
{
_resize_wrap_handle_hover =
const gboolean handle_hover =
y >= gtk_widget_get_allocated_height(widget) - DT_RESIZE_HANDLE_SIZE;
if(_resize_wrap_handle_hover != prior)
if(handle_hover != state->handle_hover)
{
if(_resize_wrap_handle_hover)
dt_control_set_temp_cursor("ns-resize");
else
dt_control_clear_temp_cursor();
// draw changed handle hover state
_resize_wrap_set_handle_hover(widget, state, handle_hover);
gtk_widget_queue_draw(widget);
}
}
Expand All @@ -4985,10 +5006,19 @@ static void _resize_wrap_button_pressed(GtkGestureSingle *gesture,
gpointer user_data)
{
GtkWidget *widget = dt_gui_get_widget(gesture);
dt_resize_wrap_state_t *state = _resize_wrap_state(widget);
if(!state) return;
if(y >= gtk_widget_get_allocated_height(widget) - DT_RESIZE_HANDLE_SIZE
&& gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY)
{
_resize_wrap_dragging = TRUE;
state->dragging = TRUE;
// go through the hover setter rather than setting the cursor directly, so
// that handle_hover records the cursor now on the widget. The release path
// only clears it when it sees handle_hover change back to FALSE, so a
// press that arrives without a preceding hover motion -- the motion
// handler ignores events coming from a child window -- would otherwise
// leave "ns-resize" on the widget for good.
_resize_wrap_set_handle_hover(widget, state, TRUE);
}
}

Expand All @@ -4998,11 +5028,15 @@ static void _resize_wrap_button_released(GtkGestureSingle *gesture,
gdouble y,
gpointer user_data)
{
if(_resize_wrap_dragging)
{
_resize_wrap_dragging = FALSE;
dt_control_clear_temp_cursor();
}
GtkWidget *widget = dt_gui_get_widget(gesture);
dt_resize_wrap_state_t *state = _resize_wrap_state(widget);
if(!state || !state->dragging) return;

state->dragging = FALSE;
_resize_wrap_set_handle_hover
(widget, state,
y >= gtk_widget_get_allocated_height(widget) - DT_RESIZE_HANDLE_SIZE);
gtk_widget_queue_draw(widget);
}

static void _resize_wrap_enter_leave_controller(GtkEventControllerMotion *controller,
Expand All @@ -5028,21 +5062,29 @@ static void _resize_wrap_enter_leave_controller(GtkEventControllerMotion *contro
#endif
}

_resize_wrap_hovered =
is_enter || detail == GDK_NOTIFY_INFERIOR || _resize_wrap_dragging ? widget : NULL;

// When leave handle and widget, remove temp resize cursor. When
// enter widget, motion event will handle cursor change for handle.
if(!is_enter && !_resize_wrap_dragging && _resize_wrap_handle_hover)
dt_resize_wrap_state_t *state = _resize_wrap_state(widget);
if(!state)
{
dt_control_clear_temp_cursor();
_resize_wrap_handle_hover = FALSE;
#if !GTK_CHECK_VERSION(4, 0, 0)
if(event) gdk_event_free(event);
#endif
return;
}

state->pointer_inside =
is_enter || detail == GDK_NOTIFY_INFERIOR || state->dragging;

if(!is_enter && !state->dragging)
_resize_wrap_set_handle_hover(widget, state, FALSE);

gtk_widget_queue_draw(widget);

if(mode == GDK_CROSSING_GTK_UNGRAB)
_resize_wrap_dragging = FALSE;
if(mode == GDK_CROSSING_GTK_UNGRAB && state->dragging)
{
state->dragging = FALSE;
state->pointer_inside = FALSE;
_resize_wrap_set_handle_hover(widget, state, FALSE);
}
#if !GTK_CHECK_VERSION(4, 0, 0)
if(event) gdk_event_free(event);
#endif
Expand Down Expand Up @@ -5111,6 +5153,9 @@ GtkWidget *dt_ui_resize_wrap(GtkWidget *w,
gtk_container_add(GTK_CONTAINER(w), sw);
}

g_object_set_data_full(G_OBJECT(w), DT_RESIZE_WRAP_STATE_KEY,
g_new0(dt_resize_wrap_state_t, 1), g_free);

gtk_widget_add_events(w, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
| GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
| GDK_POINTER_MOTION_MASK | darktable.gui->scroll_mask);
Expand Down
10 changes: 9 additions & 1 deletion src/gui/gtk.h
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,15 @@ gboolean dt_gui_osx_edit_command(GtkWidget *focus, GdkEvent *event);

/* Cursor compatibility boundary. GTK3 applies cursors to the widget's
* backing window (using a TreeView's bin window when needed); GTK4 applies
* them to the widget itself. The owner string is used by -d input tracing. */
* them to the widget itself. The owner string is used by -d input tracing.
*
* A NULL cursor_name means "inherit whatever the parent shows", in both GTK3
* and GTK4, and that is how a widget-local cursor should be undone. Passing
* "default" instead pins a real arrow on the widget, which then hides the
* cursor the toplevel is showing -- the busy/wait cursor, or shortcut-mapping
* and help mode. Pass "default" only where overriding the toplevel is the
* point, as src/libs/backgroundjobs.c does so that the busy cursor does not
* cover the job's cancel box. */
GdkCursor *dt_gui_cursor_new_for_name(GdkDisplay *display, const char *cursor_name);
GdkCursor *dt_gui_cursor_get(GtkWidget *widget);
void dt_gui_cursor_apply(GtkWidget *widget, GdkCursor *cursor);
Expand Down
2 changes: 0 additions & 2 deletions src/iop/toneequal.c
Original file line number Diff line number Diff line change
Expand Up @@ -1977,7 +1977,6 @@ static void switch_cursors(dt_iop_module_t *self)
{
// display default cursor
dt_control_change_cursor("default");

return;
}

Expand Down Expand Up @@ -2009,7 +2008,6 @@ static void switch_cursors(dt_iop_module_t *self)
// if module is active and opened but cursor is out of the preview,
// display default cursor
dt_control_change_cursor("default");

dt_control_queue_redraw_center();
}
else
Expand Down
10 changes: 6 additions & 4 deletions src/libs/tools/timeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,7 @@ static void _lib_timeline_motion_notify_cb(GtkEventControllerMotion *controller,
gdouble y,
dt_lib_module_t *self)
{
GtkWidget *widget = dt_gui_get_widget(controller);
dt_lib_timeline_t *strip = self->data;

strip->in = TRUE;
Expand All @@ -1289,22 +1290,22 @@ static void _lib_timeline_motion_notify_cb(GtkEventControllerMotion *controller,
{
strip->stop_x = x;
strip->stop_t = _time_get_from_pos(x, strip);
dt_control_change_cursor("default");
dt_gui_cursor_set(widget, NULL, "timeline");
}
else
{
// we change the cursor if we are close enough of a selection limit
if(x - strip->start_x < 2 && x - strip->start_x > -2)
{
dt_control_change_cursor("w-resize");
dt_gui_cursor_set(widget, "w-resize", "timeline");
}
else if(x - strip->stop_x < 2 && x - strip->stop_x > -2)
{
dt_control_change_cursor("e-resize");
dt_gui_cursor_set(widget, "e-resize", "timeline");
}
else
{
dt_control_change_cursor("default");
dt_gui_cursor_set(widget, NULL, "timeline");
}
}
gtk_widget_queue_draw(strip->timeline);
Expand Down Expand Up @@ -1374,6 +1375,7 @@ static void _lib_timeline_mouse_leave_cb(GtkEventControllerMotion *controller,
dt_lib_timeline_t *strip = self->data;

strip->in = FALSE;
dt_gui_cursor_set(dt_gui_get_widget(controller), NULL, "timeline");

gtk_widget_queue_draw(strip->timeline);
}
Expand Down
Loading