gtk2/demos/gtk-demo/suggestionentry.c
Matthias Clasen a2897e1868 gtk-demo: Add suggestion entry demos
Add a possible replacement for GtkEntryCompletion
as a demo.

Move the Dropdowns demo to Lists/Selections, and make
it show both GtkDropDown and the suggestion entry, with
some variations.
2020-11-11 15:54:43 -05:00

1216 lines
34 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "suggestionentry.h"
struct _MatchObject
{
GObject parent_instance;
GObject *item;
char *string;
guint match_start;
guint match_end;
guint score;
};
typedef struct
{
GObjectClass parent_class;
} MatchObjectClass;
enum
{
PROP_ITEM = 1,
PROP_STRING,
PROP_MATCH_START,
PROP_MATCH_END,
PROP_SCORE,
N_MATCH_PROPERTIES
};
static GParamSpec *match_properties[N_MATCH_PROPERTIES];
G_DEFINE_TYPE (MatchObject, match_object, G_TYPE_OBJECT)
static void
match_object_init (MatchObject *object)
{
}
static void
match_object_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
MatchObject *self = MATCH_OBJECT (object);
switch (property_id)
{
case PROP_ITEM:
g_value_set_object (value, self->item);
break;
case PROP_STRING:
g_value_set_string (value, self->string);
break;
case PROP_MATCH_START:
g_value_set_uint (value, self->match_start);
break;
case PROP_MATCH_END:
g_value_set_uint (value, self->match_end);
break;
case PROP_SCORE:
g_value_set_uint (value, self->score);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
match_object_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
MatchObject *self = MATCH_OBJECT (object);
switch (property_id)
{
case PROP_ITEM:
self->item = g_value_get_object (value);
break;
case PROP_STRING:
self->string = g_value_dup_string (value);
break;
case PROP_MATCH_START:
if (self->match_start != g_value_get_uint (value))
{
self->match_start = g_value_get_uint (value);
g_object_notify_by_pspec (object, pspec);
}
break;
case PROP_MATCH_END:
if (self->match_end != g_value_get_uint (value))
{
self->match_end = g_value_get_uint (value);
g_object_notify_by_pspec (object, pspec);
}
break;
case PROP_SCORE:
if (self->score != g_value_get_uint (value))
{
self->score = g_value_get_uint (value);
g_object_notify_by_pspec (object, pspec);
}
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
match_object_dispose (GObject *object)
{
MatchObject *self = MATCH_OBJECT (object);
g_clear_object (&self->item);
g_clear_pointer (&self->string, g_free);
G_OBJECT_CLASS (match_object_parent_class)->dispose (object);
}
static void
match_object_class_init (MatchObjectClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS (class);
object_class->dispose = match_object_dispose;
object_class->get_property = match_object_get_property;
object_class->set_property = match_object_set_property;
match_properties[PROP_ITEM]
= g_param_spec_object ("item", "Item", "Item",
G_TYPE_OBJECT,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
match_properties[PROP_STRING]
= g_param_spec_string ("string", "String", "String",
NULL,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
match_properties[PROP_MATCH_START]
= g_param_spec_uint ("match-start", "Match Start", "Match Start",
0, G_MAXUINT, 0,
G_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY |
G_PARAM_STATIC_STRINGS);
match_properties[PROP_MATCH_END]
= g_param_spec_uint ("match-end", "Match End", "Match End",
0, G_MAXUINT, 0,
G_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY |
G_PARAM_STATIC_STRINGS);
match_properties[PROP_SCORE]
= g_param_spec_uint ("score", "Score", "Score",
0, G_MAXUINT, 0,
G_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY |
G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, N_MATCH_PROPERTIES, match_properties);
}
static MatchObject *
match_object_new (gpointer item,
const char *string)
{
return g_object_new (MATCH_TYPE_OBJECT,
"item", item,
"string", string,
NULL);
}
gpointer
match_object_get_item (MatchObject *object)
{
return object->item;
}
const char *
match_object_get_string (MatchObject *object)
{
return object->string;
}
guint
match_object_get_match_start (MatchObject *object)
{
return object->match_start;
}
guint
match_object_get_match_end (MatchObject *object)
{
return object->match_end;
}
guint
match_object_get_score (MatchObject *object)
{
return object->score;
}
void
match_object_set_match (MatchObject *object,
guint start,
guint end,
guint score)
{
g_object_freeze_notify (G_OBJECT (object));
g_object_set (object,
"match-start", start,
"match-end", end,
"score", score,
NULL);
g_object_thaw_notify (G_OBJECT (object));
}
/* ---- */
struct _SuggestionEntry
{
GtkWidget parent_instance;
GListModel *model;
GtkListItemFactory *factory;
GtkExpression *expression;
GtkFilter *filter;
GtkMapListModel *map_model;
GtkSingleSelection *selection;
GtkWidget *entry;
GtkWidget *arrow;
GtkWidget *popup;
GtkWidget *list;
char *search;
SuggestionEntryMatchFunc match_func;
gpointer match_data;
GDestroyNotify destroy;
gulong changed_id;
guint use_filter : 1;
guint show_arrow : 1;
};
typedef struct _SuggestionEntryClass SuggestionEntryClass;
struct _SuggestionEntryClass
{
GtkWidgetClass parent_class;
};
enum
{
PROP_0,
PROP_MODEL,
PROP_FACTORY,
PROP_EXPRESSION,
PROP_PLACEHOLDER_TEXT,
PROP_POPUP_VISIBLE,
PROP_USE_FILTER,
PROP_SHOW_ARROW,
N_PROPERTIES,
};
static void suggestion_entry_set_popup_visible (SuggestionEntry *self,
gboolean visible);
static GtkEditable *
suggestion_entry_get_delegate (GtkEditable *editable)
{
return GTK_EDITABLE (SUGGESTION_ENTRY (editable)->entry);
}
static void
suggestion_entry_editable_init (GtkEditableInterface *iface)
{
iface->get_delegate = suggestion_entry_get_delegate;
}
G_DEFINE_TYPE_WITH_CODE (SuggestionEntry, suggestion_entry, GTK_TYPE_WIDGET,
G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE,
suggestion_entry_editable_init))
static GParamSpec *properties[N_PROPERTIES] = { NULL, };
static void
suggestion_entry_dispose (GObject *object)
{
SuggestionEntry *self = SUGGESTION_ENTRY (object);
if (self->changed_id)
{
g_signal_handler_disconnect (self->entry, self->changed_id);
self->changed_id = 0;
}
g_clear_pointer (&self->entry, gtk_widget_unparent);
g_clear_pointer (&self->arrow, gtk_widget_unparent);
g_clear_pointer (&self->popup, gtk_widget_unparent);
g_clear_pointer (&self->expression, gtk_expression_unref);
g_clear_object (&self->factory);
g_clear_object (&self->model);
g_clear_object (&self->map_model);
g_clear_object (&self->selection);
g_clear_pointer (&self->search, g_free);
if (self->destroy)
self->destroy (self->match_data);
G_OBJECT_CLASS (suggestion_entry_parent_class)->dispose (object);
}
static void
suggestion_entry_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
SuggestionEntry *self = SUGGESTION_ENTRY (object);
if (gtk_editable_delegate_get_property (object, property_id, value, pspec))
return;
switch (property_id)
{
case PROP_MODEL:
g_value_set_object (value, suggestion_entry_get_model (self));
break;
case PROP_FACTORY:
g_value_set_object (value, suggestion_entry_get_factory (self));
break;
case PROP_EXPRESSION:
gtk_value_set_expression (value, suggestion_entry_get_expression (self));
break;
case PROP_PLACEHOLDER_TEXT:
g_value_set_string (value, gtk_text_get_placeholder_text (GTK_TEXT (self->entry)));
break;
case PROP_POPUP_VISIBLE:
g_value_set_boolean (value, self->popup && gtk_widget_get_visible (self->popup));
break;
case PROP_USE_FILTER:
g_value_set_boolean (value, suggestion_entry_get_use_filter (self));
break;
case PROP_SHOW_ARROW:
g_value_set_boolean (value, suggestion_entry_get_show_arrow (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
suggestion_entry_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
SuggestionEntry *self = SUGGESTION_ENTRY (object);
if (gtk_editable_delegate_set_property (object, property_id, value, pspec))
return;
switch (property_id)
{
case PROP_MODEL:
suggestion_entry_set_model (self, g_value_get_object (value));
break;
case PROP_FACTORY:
suggestion_entry_set_factory (self, g_value_get_object (value));
break;
case PROP_EXPRESSION:
suggestion_entry_set_expression (self, gtk_value_get_expression (value));
break;
case PROP_PLACEHOLDER_TEXT:
gtk_text_set_placeholder_text (GTK_TEXT (self->entry), g_value_get_string (value));
break;
case PROP_POPUP_VISIBLE:
suggestion_entry_set_popup_visible (self, g_value_get_boolean (value));
break;
case PROP_USE_FILTER:
suggestion_entry_set_use_filter (self, g_value_get_boolean (value));
break;
case PROP_SHOW_ARROW:
suggestion_entry_set_show_arrow (self, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
suggestion_entry_measure (GtkWidget *widget,
GtkOrientation orientation,
int for_size,
int *minimum,
int *natural,
int *minimum_baseline,
int *natural_baseline)
{
SuggestionEntry *self = SUGGESTION_ENTRY (widget);
int arrow_min = 0, arrow_nat = 0;
gtk_widget_measure (self->entry, orientation, for_size,
minimum, natural,
minimum_baseline, natural_baseline);
if (self->arrow && gtk_widget_get_visible (self->arrow))
gtk_widget_measure (self->arrow, orientation, for_size,
&arrow_min, &arrow_nat,
NULL, NULL);
}
static void
suggestion_entry_size_allocate (GtkWidget *widget,
int width,
int height,
int baseline)
{
SuggestionEntry *self = SUGGESTION_ENTRY (widget);
int arrow_min = 0, arrow_nat = 0;
if (self->arrow && gtk_widget_get_visible (self->arrow))
gtk_widget_measure (self->arrow, GTK_ORIENTATION_HORIZONTAL, -1,
&arrow_min, &arrow_nat,
NULL, NULL);
gtk_widget_size_allocate (self->entry,
&(GtkAllocation) { 0, 0, width - arrow_nat, height },
baseline);
if (self->arrow && gtk_widget_get_visible (self->arrow))
gtk_widget_size_allocate (self->arrow,
&(GtkAllocation) { width - arrow_nat, 0, arrow_nat, height },
baseline);
gtk_widget_set_size_request (self->popup, gtk_widget_get_allocated_width (GTK_WIDGET (self)), -1);
gtk_widget_queue_resize (self->popup);
gtk_native_check_resize (GTK_NATIVE (self->popup));
}
static gboolean
suggestion_entry_grab_focus (GtkWidget *widget)
{
SuggestionEntry *self = SUGGESTION_ENTRY (widget);
return gtk_widget_grab_focus (self->entry);
}
static void
suggestion_entry_class_init (SuggestionEntryClass *klass)
{
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->dispose = suggestion_entry_dispose;
object_class->get_property = suggestion_entry_get_property;
object_class->set_property = suggestion_entry_set_property;
widget_class->measure = suggestion_entry_measure;
widget_class->size_allocate = suggestion_entry_size_allocate;
widget_class->grab_focus = suggestion_entry_grab_focus;
properties[PROP_MODEL] =
g_param_spec_object ("model",
"Model",
"Model for the displayed items",
G_TYPE_LIST_MODEL,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
properties[PROP_FACTORY] =
g_param_spec_object ("factory",
"Factory",
"Factory for populating list items",
GTK_TYPE_LIST_ITEM_FACTORY,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
properties[PROP_EXPRESSION] =
gtk_param_spec_expression ("expression",
"Expression",
"Expression to determine strings to search for",
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
properties[PROP_PLACEHOLDER_TEXT] =
g_param_spec_string ("placeholder-text",
"Placeholder text",
"Show text in the entry when its empty and unfocused",
NULL,
G_PARAM_READWRITE);
properties[PROP_POPUP_VISIBLE] =
g_param_spec_boolean ("popup-visible",
"Popup visible",
"Whether the popup with suggestions is currently visible",
FALSE,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
properties[PROP_USE_FILTER] =
g_param_spec_boolean ("use-filter",
"Use filter",
"Whether to filter the list for matches",
TRUE,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
properties[PROP_SHOW_ARROW] =
g_param_spec_boolean ("show-arrow",
"Show arrow",
"Whether to show a clickable arrow for presenting the popup",
FALSE,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, N_PROPERTIES, properties);
gtk_editable_install_properties (object_class, N_PROPERTIES);
gtk_widget_class_install_property_action (widget_class, "popup.show", "popup-visible");
gtk_widget_class_add_binding_action (widget_class,
GDK_KEY_Down, GDK_ALT_MASK,
"popup.show", NULL);
gtk_widget_class_set_css_name (widget_class, "entry");
}
static void
setup_item (GtkSignalListItemFactory *factory,
GtkListItem *list_item,
gpointer data)
{
GtkWidget *label;
label = gtk_label_new (NULL);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_list_item_set_child (list_item, label);
}
static void
bind_item (GtkSignalListItemFactory *factory,
GtkListItem *list_item,
gpointer data)
{
gpointer item;
GtkWidget *label;
GValue value = G_VALUE_INIT;
item = gtk_list_item_get_item (list_item);
label = gtk_list_item_get_child (list_item);
gtk_label_set_label (GTK_LABEL (label), match_object_get_string (MATCH_OBJECT (item)));
g_value_unset (&value);
}
static void
suggestion_entry_set_popup_visible (SuggestionEntry *self,
gboolean visible)
{
if (gtk_widget_get_visible (self->popup) == visible)
return;
if (g_list_model_get_n_items (G_LIST_MODEL (self->selection)) == 0)
return;
if (visible)
{
if (!gtk_widget_has_focus (self->entry))
gtk_text_grab_focus_without_selecting (GTK_TEXT (self->entry));
gtk_single_selection_set_selected (self->selection, GTK_INVALID_LIST_POSITION);
gtk_popover_popup (GTK_POPOVER (self->popup));
}
else
{
gtk_popover_popdown (GTK_POPOVER (self->popup));
}
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_POPUP_VISIBLE]);
}
static void update_map (SuggestionEntry *self);
static gboolean
text_changed_idle (gpointer data)
{
SuggestionEntry *self = data;
const char *text;
guint matches;
if (!self->map_model)
return G_SOURCE_REMOVE;
text = gtk_editable_get_text (GTK_EDITABLE (self->entry));
g_free (self->search);
self->search = g_strdup (text);
update_map (self);
matches = g_list_model_get_n_items (G_LIST_MODEL (self->selection));
suggestion_entry_set_popup_visible (self, matches > 0);
return G_SOURCE_REMOVE;
}
static void
text_changed (GtkEditable *editable,
GParamSpec *pspec,
SuggestionEntry *self)
{
/* We need to defer to an idle since GtkText sets selection bounds
* after notify::text
*/
g_idle_add (text_changed_idle, self);
}
static void
accept_current_selection (SuggestionEntry *self)
{
gpointer item;
item = gtk_single_selection_get_selected_item (self->selection);
if (!item)
return;
g_signal_handler_block (self->entry, self->changed_id);
gtk_editable_set_text (GTK_EDITABLE (self->entry),
match_object_get_string (MATCH_OBJECT (item)));
gtk_editable_set_position (GTK_EDITABLE (self->entry), -1);
g_signal_handler_unblock (self->entry, self->changed_id);
}
static void
suggestion_entry_row_activated (GtkListView *listview,
guint position,
SuggestionEntry *self)
{
suggestion_entry_set_popup_visible (self, FALSE);
accept_current_selection (self);
}
static inline gboolean
keyval_is_cursor_move (guint keyval)
{
if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up)
return TRUE;
if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down)
return TRUE;
if (keyval == GDK_KEY_Page_Up || keyval == GDK_KEY_Page_Down)
return TRUE;
return FALSE;
}
#define PAGE_STEP 10
static gboolean
suggestion_entry_key_pressed (GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
SuggestionEntry *self)
{
guint matches;
guint selected;
if (state & (GDK_SHIFT_MASK | GDK_ALT_MASK | GDK_CONTROL_MASK))
return FALSE;
if (keyval == GDK_KEY_Return ||
keyval == GDK_KEY_KP_Enter ||
keyval == GDK_KEY_ISO_Enter)
{
suggestion_entry_set_popup_visible (self, FALSE);
accept_current_selection (self);
g_free (self->search);
self->search = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->entry)));
update_map (self);
return TRUE;
}
else if (keyval == GDK_KEY_Escape)
{
if (gtk_widget_get_mapped (self->popup))
{
suggestion_entry_set_popup_visible (self, FALSE);
g_signal_handler_block (self->entry, self->changed_id);
gtk_editable_set_text (GTK_EDITABLE (self->entry), self->search ? self->search : "");
gtk_editable_set_position (GTK_EDITABLE (self->entry), -1);
g_signal_handler_unblock (self->entry, self->changed_id);
return TRUE;
}
}
else if (keyval == GDK_KEY_Right ||
keyval == GDK_KEY_KP_Right)
{
gtk_editable_set_position (GTK_EDITABLE (self->entry), -1);
return TRUE;
}
else if (keyval == GDK_KEY_Left ||
keyval == GDK_KEY_KP_Left)
{
return FALSE;
}
else if (keyval == GDK_KEY_Tab ||
keyval == GDK_KEY_KP_Tab ||
keyval == GDK_KEY_ISO_Left_Tab)
{
suggestion_entry_set_popup_visible (self, FALSE);
return FALSE; /* don't disrupt normal focus handling */
}
matches = g_list_model_get_n_items (G_LIST_MODEL (self->selection));
selected = gtk_single_selection_get_selected (self->selection);
if (keyval_is_cursor_move (keyval))
{
if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up)
{
if (selected == 0)
selected = GTK_INVALID_LIST_POSITION;
else if (selected == GTK_INVALID_LIST_POSITION)
selected = matches - 1;
else
selected--;
}
else if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down)
{
if (selected == matches - 1)
selected = GTK_INVALID_LIST_POSITION;
else if (selected == GTK_INVALID_LIST_POSITION)
selected = 0;
else
selected++;
}
else if (keyval == GDK_KEY_Page_Up)
{
if (selected == 0)
selected = GTK_INVALID_LIST_POSITION;
else if (selected == GTK_INVALID_LIST_POSITION)
selected = matches - 1;
else if (selected >= PAGE_STEP)
selected -= PAGE_STEP;
else
selected = 0;
}
else if (keyval == GDK_KEY_Page_Down)
{
if (selected == matches - 1)
selected = GTK_INVALID_LIST_POSITION;
else if (selected == GTK_INVALID_LIST_POSITION)
selected = 0;
else if (selected + PAGE_STEP < matches)
selected += PAGE_STEP;
else
selected = matches - 1;
}
gtk_single_selection_set_selected (self->selection, selected);
return TRUE;
}
return FALSE;
}
static void
suggestion_entry_focus_out (GtkEventController *controller,
SuggestionEntry *self)
{
if (!gtk_widget_get_mapped (self->popup))
return;
suggestion_entry_set_popup_visible (self, FALSE);
accept_current_selection (self);
}
static void
set_default_factory (SuggestionEntry *self)
{
GtkListItemFactory *factory;
factory = gtk_signal_list_item_factory_new ();
g_signal_connect (factory, "setup", G_CALLBACK (setup_item), self);
g_signal_connect (factory, "bind", G_CALLBACK (bind_item), self);
suggestion_entry_set_factory (self, factory);
g_object_unref (factory);
}
static void default_match_func (MatchObject *object,
const char *search,
gpointer data);
static void
suggestion_entry_init (SuggestionEntry *self)
{
GtkWidget *sw;
GtkEventController *controller;
if (!g_object_get_data (G_OBJECT (gdk_display_get_default ()), "suggestion-style"))
{
GtkCssProvider *provider;
provider = gtk_css_provider_new ();
gtk_css_provider_load_from_resource (provider, "/dropdown/suggestionentry.css");
gtk_style_context_add_provider_for_display (gdk_display_get_default (),
GTK_STYLE_PROVIDER (provider),
800);
g_object_set_data (G_OBJECT (gdk_display_get_default ()), "suggestion-style", provider);
g_object_unref (provider);
}
self->use_filter = TRUE;
self->show_arrow = FALSE;
self->match_func = default_match_func;
self->match_data = NULL;
self->destroy = NULL;
gtk_widget_add_css_class (GTK_WIDGET (self), "suggestion");
self->entry = gtk_text_new ();
gtk_widget_set_parent (self->entry, GTK_WIDGET (self));
gtk_widget_set_hexpand (self->entry, TRUE);
gtk_editable_init_delegate (GTK_EDITABLE (self));
self->changed_id = g_signal_connect (self->entry, "notify::text", G_CALLBACK (text_changed), self);
self->popup = gtk_popover_new ();
gtk_popover_set_position (GTK_POPOVER (self->popup), GTK_POS_BOTTOM);
gtk_popover_set_autohide (GTK_POPOVER (self->popup), FALSE);
gtk_popover_set_has_arrow (GTK_POPOVER (self->popup), FALSE);
gtk_widget_set_halign (self->popup, GTK_ALIGN_START);
gtk_widget_add_css_class (self->popup, "menu");
gtk_widget_set_parent (self->popup, GTK_WIDGET (self));
sw = gtk_scrolled_window_new ();
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
GTK_POLICY_NEVER,
GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (sw), 400);
gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (sw), TRUE);
gtk_popover_set_child (GTK_POPOVER (self->popup), sw);
self->list = gtk_list_view_new (NULL, NULL);
gtk_list_view_set_single_click_activate (GTK_LIST_VIEW (self->list), TRUE);
g_signal_connect (self->list, "activate",
G_CALLBACK (suggestion_entry_row_activated), self);
gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), self->list);
set_default_factory (self);
controller = gtk_event_controller_key_new ();
gtk_event_controller_set_name (controller, "gtk-suggestion-entry");
g_signal_connect (controller, "key-pressed",
G_CALLBACK (suggestion_entry_key_pressed), self);
gtk_widget_add_controller (self->entry, controller);
controller = gtk_event_controller_focus_new ();
gtk_event_controller_set_name (controller, "gtk-suggestion-entry");
g_signal_connect (controller, "leave",
G_CALLBACK (suggestion_entry_focus_out), self);
gtk_widget_add_controller (self->entry, controller);
}
GtkWidget *
suggestion_entry_new (void)
{
return g_object_new (SUGGESTION_TYPE_ENTRY, NULL);
}
GListModel *
suggestion_entry_get_model (SuggestionEntry *self)
{
g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), NULL);
return self->model;
}
static void
selection_changed (GtkSingleSelection *selection,
GParamSpec *pspec,
SuggestionEntry *self)
{
accept_current_selection (self);
}
static gboolean
filter_func (gpointer item, gpointer user_data)
{
SuggestionEntry *self = SUGGESTION_ENTRY (user_data);
guint min_score;
if (self->use_filter)
min_score = 1;
else
min_score = 0;
return match_object_get_score (MATCH_OBJECT (item)) >= min_score;
}
static void
default_match_func (MatchObject *object,
const char *search,
gpointer data)
{
char *tmp1, *tmp2, *tmp3, *tmp4;
tmp1 = g_utf8_normalize (match_object_get_string (object), -1, G_NORMALIZE_ALL);
tmp2 = g_utf8_casefold (tmp1, -1);
tmp3 = g_utf8_normalize (search, -1, G_NORMALIZE_ALL);
tmp4 = g_utf8_casefold (tmp3, -1);
if (g_str_has_prefix (tmp2, tmp4))
match_object_set_match (object, 0, g_utf8_strlen (search, -1), 1);
else
match_object_set_match (object, 0, 0, 0);
g_free (tmp1);
g_free (tmp2);
g_free (tmp3);
g_free (tmp4);
}
static gpointer
map_func (gpointer item, gpointer user_data)
{
SuggestionEntry *self = SUGGESTION_ENTRY (user_data);
GValue value = G_VALUE_INIT;
gpointer obj;
if (self->expression)
{
gtk_expression_evaluate (self->expression, item, &value);
}
else if (GTK_IS_STRING_OBJECT (item))
{
g_object_get_property (G_OBJECT (item), "string", &value);
}
else
{
g_critical ("Either SuggestionEntry:expression must be set "
"or SuggestionEntry:model must be a GtkStringList");
g_value_set_string (&value, "No value");
}
obj = match_object_new (item, g_value_get_string (&value));
g_value_unset (&value);
if (self->search && self->search[0])
self->match_func (obj, self->search, self->match_data);
else
match_object_set_match (obj, 0, 0, 1);
return obj;
}
static void
update_map (SuggestionEntry *self)
{
gtk_map_list_model_set_map_func (self->map_model, map_func, self, NULL);
}
void
suggestion_entry_set_model (SuggestionEntry *self,
GListModel *model)
{
g_return_if_fail (SUGGESTION_IS_ENTRY (self));
g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
if (!g_set_object (&self->model, model))
return;
if (self->selection)
g_signal_handlers_disconnect_by_func (self->selection, selection_changed, self);
if (model == NULL)
{
gtk_list_view_set_model (GTK_LIST_VIEW (self->list), NULL);
g_clear_object (&self->selection);
g_clear_object (&self->map_model);
g_clear_object (&self->filter);
}
else
{
GtkMapListModel *map_model;
GtkFilterListModel *filter_model;
GtkFilter *filter;
GtkSortListModel *sort_model;
GtkSingleSelection *selection;
GtkSorter *sorter;
map_model = gtk_map_list_model_new (g_object_ref (model), NULL, NULL, NULL);
g_set_object (&self->map_model, map_model);
update_map (self);
filter = GTK_FILTER (gtk_custom_filter_new (filter_func, self, NULL));
filter_model = gtk_filter_list_model_new (G_LIST_MODEL (self->map_model), filter);
g_set_object (&self->filter, filter);
sorter = GTK_SORTER (gtk_numeric_sorter_new (gtk_property_expression_new (MATCH_TYPE_OBJECT, NULL, "score")));
gtk_numeric_sorter_set_sort_order (GTK_NUMERIC_SORTER (sorter), GTK_SORT_DESCENDING);
sort_model = gtk_sort_list_model_new (G_LIST_MODEL (filter_model), sorter);
update_map (self);
selection = gtk_single_selection_new (G_LIST_MODEL (sort_model));
gtk_single_selection_set_autoselect (selection, FALSE);
gtk_single_selection_set_can_unselect (selection, TRUE);
gtk_single_selection_set_selected (selection, GTK_INVALID_LIST_POSITION);
g_set_object (&self->selection, selection);
gtk_list_view_set_model (GTK_LIST_VIEW (self->list), GTK_SELECTION_MODEL (selection));
g_object_unref (selection);
}
if (self->selection)
{
g_signal_connect (self->selection, "notify::selected",
G_CALLBACK (selection_changed), self);
selection_changed (self->selection, NULL, self);
}
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]);
}
GtkListItemFactory *
suggestion_entry_get_factory (SuggestionEntry *self)
{
g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), NULL);
return self->factory;
}
void
suggestion_entry_set_factory (SuggestionEntry *self,
GtkListItemFactory *factory)
{
g_return_if_fail (SUGGESTION_IS_ENTRY (self));
g_return_if_fail (factory == NULL || GTK_LIST_ITEM_FACTORY (factory));
if (!g_set_object (&self->factory, factory))
return;
if (self->list)
gtk_list_view_set_factory (GTK_LIST_VIEW (self->list), factory);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FACTORY]);
}
void
suggestion_entry_set_expression (SuggestionEntry *self,
GtkExpression *expression)
{
g_return_if_fail (SUGGESTION_IS_ENTRY (self));
g_return_if_fail (expression == NULL ||
gtk_expression_get_value_type (expression) == G_TYPE_STRING);
if (self->expression == expression)
return;
if (self->expression)
gtk_expression_unref (self->expression);
self->expression = expression;
if (self->expression)
gtk_expression_ref (self->expression);
update_map (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_EXPRESSION]);
}
GtkExpression *
suggestion_entry_get_expression (SuggestionEntry *self)
{
g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), NULL);
return self->expression;
}
void
suggestion_entry_set_use_filter (SuggestionEntry *self,
gboolean use_filter)
{
g_return_if_fail (SUGGESTION_IS_ENTRY (self));
if (self->use_filter == use_filter)
return;
self->use_filter = use_filter;
gtk_filter_changed (self->filter, GTK_FILTER_CHANGE_DIFFERENT);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USE_FILTER]);
}
gboolean
suggestion_entry_get_use_filter (SuggestionEntry *self)
{
g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), TRUE);
return self->use_filter;
}
static void
suggestion_entry_arrow_clicked (SuggestionEntry *self)
{
gboolean visible;
visible = gtk_widget_get_visible (self->popup);
suggestion_entry_set_popup_visible (self, !visible);
}
void
suggestion_entry_set_show_arrow (SuggestionEntry *self,
gboolean show_arrow)
{
g_return_if_fail (SUGGESTION_IS_ENTRY (self));
if (self->show_arrow == show_arrow)
return;
self->show_arrow = show_arrow;
if (show_arrow)
{
GtkGesture *press;
self->arrow = gtk_image_new_from_icon_name ("pan-down-symbolic");
gtk_widget_set_tooltip_text (self->arrow, "Show suggestions");
gtk_widget_set_parent (self->arrow, GTK_WIDGET (self));
press = gtk_gesture_click_new ();
g_signal_connect_swapped (press, "released",
G_CALLBACK (suggestion_entry_arrow_clicked), self);
gtk_widget_add_controller (self->arrow, GTK_EVENT_CONTROLLER (press));
}
else
{
g_clear_pointer (&self->arrow, gtk_widget_unparent);
}
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_ARROW]);
}
gboolean
suggestion_entry_get_show_arrow (SuggestionEntry *self)
{
g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), FALSE);
return self->show_arrow;
}
void
suggestion_entry_set_match_func (SuggestionEntry *self,
SuggestionEntryMatchFunc match_func,
gpointer user_data,
GDestroyNotify destroy)
{
if (self->destroy)
self->destroy (self->match_data);
self->match_func = match_func;
self->match_data = user_data;
self->destroy = destroy;
}