1216 lines
34 KiB
C
1216 lines
34 KiB
C
#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 it’s 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;
|
||
}
|