gtk/demos/gtk-demo/suggestionentry.c

1216 lines
34 KiB
C
Raw Normal View History

#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_width (GTK_WIDGET (self)), -1);
gtk_widget_queue_resize (self->popup);
gtk_popover_present (GTK_POPOVER (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_list_view_scroll_to (GTK_LIST_VIEW (self->list), selected, GTK_LIST_SCROLL_SELECT, NULL);
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, "/listview_selections/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;
}