Compare commits

...

9 Commits

Author SHA1 Message Date
Matthias Clasen
c85905dfcd Add an experiment
This is a suggestion entry that does infix
matching, highlights the match, and sorts
the matches by relevance.
2020-07-10 15:40:12 -04:00
Matthias Clasen
e2eba253cb suggestionentry: Update action enabledness
Update the action enabledness when appropriate so
that an explicit button does not appear insensitive
when it shouldn't.
2020-07-10 14:47:54 -04:00
Matthias Clasen
09d1129f4f suggestionentry: Work better without a filter
Make it possible to use the suggestion entry
by doing filtering in the model yourself, and
set use-filter to FALSE. We are still getting
the popup behavior, but inserting prefixes
can't be guaranteed to work well if the
filtering isn't doing prefix matching.
2020-07-10 13:58:42 -04:00
Matthias Clasen
affa829ac2 suggestionentry: Handle no-model situations
We shouldn't throw criticals when there is no
model. Just act like a plain entry.
2020-07-10 13:56:16 -04:00
Matthias Clasen
da397664b4 docs: Mention GtkSuggestionEntry in list widget overview 2020-07-10 13:56:16 -04:00
Matthias Clasen
d3b32e9f42 gtk-demo: Add suggestion entry demos
Move the Dropdowns demo to Lists/Selections,
and make it show both GtkDropDown and
GtkSuggestionEntry, with some variations.
2020-07-10 13:56:16 -04:00
Matthias Clasen
e2d92863b2 suggestionentry: Add minimum-length
A direct copy of GtkEntryCompletion:minimmum-key-length.
2020-07-10 13:56:16 -04:00
Matthias Clasen
f9e1e7bd1a suggestionentry: Add an optional button
This is useful for comboboxtext-like use cases.
To simulate comboboxtext behavior, set
use-filter = FALSE
show-button = TRUE
insert-selection = TRUE
2020-07-10 13:56:16 -04:00
Matthias Clasen
641c068744 Add GtkSuggestionEntry
GtkSuggestionEntry is a replacement for GtkComboBoxText
and GtkEntryCompletion, very similar to GtkDropDown, but
using an entry as widget.
2020-07-10 13:56:16 -04:00
15 changed files with 2311 additions and 29 deletions

View File

@@ -1,13 +1,15 @@
/* Drop Downs
/* Lists/Selections
*
* The GtkDropDown widget is a modern alternative to GtkComboBox.
* It uses list models instead of tree models, and the content is
* displayed using widgets instead of cell renderers.
* The GtkDropDown and GtkSuggestionEntry widgets are modern
* alternatives to GtkComboBox and GtkEntryCompletion.
*
* They use list models instead of tree models, and the content
* is displayed using widgets instead of cell renderers.
*
* The examples here demonstrate how to use different kinds of
* list models with GtkDropDown, how to use search and how to
* display the selected item differently from the presentation
* in the popup.
* list models with GtkDropDown and GtkSuggestionEntry, how to
* use search and how to display the selected item differently
* from the presentation in the popup.
*/
#include <gtk/gtk.h>
@@ -218,13 +220,50 @@ get_title (gpointer item)
return g_strdup (STRING_HOLDER (item)->title);
}
static char *
get_file_name (gpointer item)
{
return g_strdup (g_file_info_get_display_name (G_FILE_INFO (item)));
}
static void
setup_item (GtkSignalListItemFactory *factory,
GtkListItem *item)
{
GtkWidget *box;
GtkWidget *icon;
GtkWidget *label;
box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
icon = gtk_image_new ();
label = gtk_label_new ("");
gtk_label_set_xalign (GTK_LABEL (label), 0);
gtk_box_append (GTK_BOX (box), icon);
gtk_box_append (GTK_BOX (box), label);
gtk_list_item_set_child (item, box);
}
static void
bind_item (GtkSignalListItemFactory *factory,
GtkListItem *item)
{
GFileInfo *info = G_FILE_INFO (gtk_list_item_get_item (item));
GtkWidget *box = gtk_list_item_get_child (item);
GtkWidget *icon = gtk_widget_get_first_child (box);
GtkWidget *label = gtk_widget_get_last_child (box);
gtk_image_set_from_gicon (GTK_IMAGE (icon), g_file_info_get_icon (info));
gtk_label_set_label (GTK_LABEL (label), g_file_info_get_display_name (info));
}
GtkWidget *
do_dropdown (GtkWidget *do_widget)
{
static GtkWidget *window = NULL;
GtkWidget *button, *box, *spin, *check;
GtkWidget *button, *box, *spin, *check, *hbox, *label, *entry;
GListModel *model;
GtkExpression *expression;
GtkListItemFactory *factory;
const char * const times[] = { "1 minute", "2 minutes", "5 minutes", "20 minutes", NULL };
const char * const many_times[] = {
"1 minute", "2 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes",
@@ -237,23 +276,78 @@ do_dropdown (GtkWidget *do_widget)
const char * const device_descriptions[] = {
"Built-in Audio", "Built-in audio", "Thinkpad Tunderbolt 3 Dock USB Audio", "Thinkpad Tunderbolt 3 Dock USB Audio", NULL
};
const char *words[] = {
"GNOME",
"gnominious",
"Gnomonic projection",
"total",
"totally",
"toto",
"tottery",
"totterer",
"Totten trust",
"totipotent",
"totipotency",
"totemism",
"totem pole",
"Totara",
"totalizer",
"totalizator",
"totalitarianism",
"total parenteral nutrition",
"total hysterectomy",
"total eclipse",
"Totipresence",
"Totipalmi",
"Tomboy",
"zombie",
NULL
};
char *cwd;
GFile *file;
GListModel *dir;
if (!window)
{
window = gtk_window_new ();
gtk_window_set_display (GTK_WINDOW (window),
gtk_widget_get_display (do_widget));
gtk_window_set_title (GTK_WINDOW (window), "Drop Downs");
gtk_window_set_title (GTK_WINDOW (window), "Selections");
gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
g_object_add_weak_pointer (G_OBJECT (window), (gpointer *)&window);
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start (box, 10);
gtk_widget_set_margin_end (box, 10);
gtk_widget_set_margin_top (box, 10);
gtk_widget_set_margin_bottom (box, 10);
gtk_window_set_child (GTK_WINDOW (window), box);
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 20);
gtk_widget_set_margin_start (hbox, 20);
gtk_widget_set_margin_end (hbox, 20);
gtk_widget_set_margin_top (hbox, 20);
gtk_widget_set_margin_bottom (hbox, 20);
gtk_window_set_child (GTK_WINDOW (window), hbox);
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
gtk_box_append (GTK_BOX (hbox), box);
label = gtk_label_new ("Dropdowns");
gtk_widget_add_css_class (label, "title-4");
gtk_box_append (GTK_BOX (box), label);
/* A basic dropdown */
button = drop_down_new_from_strings (times, NULL, NULL);
gtk_box_append (GTK_BOX (box), button);
/* A dropdown using an expression to obtain strings */
button = drop_down_new_from_strings (many_times, NULL, NULL);
gtk_drop_down_set_enable_search (GTK_DROP_DOWN (button), TRUE);
expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL,
0, NULL,
(GCallback)get_title,
NULL, NULL);
gtk_drop_down_set_expression (GTK_DROP_DOWN (button), expression);
gtk_expression_unref (expression);
gtk_box_append (GTK_BOX (box), button);
/* A dropdown using a non-trivial model, and search */
button = gtk_drop_down_new ();
model = G_LIST_MODEL (pango_cairo_font_map_get_default ());
@@ -270,30 +364,86 @@ do_dropdown (GtkWidget *do_widget)
spin = gtk_spin_button_new_with_range (-1, g_list_model_get_n_items (G_LIST_MODEL (model)), 1);
gtk_widget_set_halign (spin, GTK_ALIGN_START);
gtk_widget_set_margin_start (spin, 20);
g_object_bind_property (button, "selected", spin, "value", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
gtk_box_append (GTK_BOX (box), spin);
check = gtk_check_button_new_with_label ("Enable search");
gtk_widget_set_margin_start (check, 20);
g_object_bind_property (button, "enable-search", check, "active", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
gtk_box_append (GTK_BOX (box), check);
g_object_unref (model);
button = drop_down_new_from_strings (times, NULL, NULL);
gtk_box_append (GTK_BOX (box), button);
button = drop_down_new_from_strings (many_times, NULL, NULL);
gtk_drop_down_set_enable_search (GTK_DROP_DOWN (button), TRUE);
expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL,
0, NULL,
(GCallback)get_title,
NULL, NULL);
gtk_drop_down_set_expression (GTK_DROP_DOWN (button), expression);
gtk_expression_unref (expression);
gtk_box_append (GTK_BOX (box), button);
/* A dropdown with a separate list factory */
button = drop_down_new_from_strings (device_titles, device_icons, device_descriptions);
gtk_box_append (GTK_BOX (box), button);
gtk_box_append (GTK_BOX (hbox), gtk_separator_new (GTK_ORIENTATION_VERTICAL));
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
gtk_box_append (GTK_BOX (hbox), box);
label = gtk_label_new ("Suggestions");
gtk_widget_add_css_class (label, "title-4");
gtk_box_append (GTK_BOX (box), label);
/* A basic suggestion entry */
entry = gtk_suggestion_entry_new ();
g_object_set (entry, "placeholder-text", "Words with T or G…", NULL);
gtk_suggestion_entry_set_from_strings (GTK_SUGGESTION_ENTRY (entry), words);
gtk_box_append (GTK_BOX (box), entry);
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
gtk_widget_set_halign (hbox, GTK_ALIGN_START);
gtk_widget_set_margin_start (hbox, 20);
spin = gtk_spin_button_new_with_range (0, 10, 1);
g_object_bind_property (entry, "minimum-length", spin, "value", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
gtk_box_append (GTK_BOX (hbox), gtk_label_new ("Min. length"));
gtk_box_append (GTK_BOX (hbox), spin);
gtk_box_append (GTK_BOX (box), hbox);
check = gtk_check_button_new_with_label ("Auto-Insert");
gtk_widget_set_margin_start (check, 20);
g_object_bind_property (entry, "insert-prefix", check, "active", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
gtk_box_append (GTK_BOX (box), check);
check = gtk_check_button_new_with_label ("Auto-Select");
gtk_widget_set_margin_start (check, 20);
g_object_bind_property (entry, "insert-selection", check, "active", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
gtk_box_append (GTK_BOX (box), check);
/* A suggestion entry using a custom model, and no filtering */
entry = gtk_suggestion_entry_new ();
cwd = g_get_current_dir ();
file = g_file_new_for_path (cwd);
dir = G_LIST_MODEL (gtk_directory_list_new ("standard::display-name,standard::content-type,standard::icon,standard::size", file));
gtk_suggestion_entry_set_model (GTK_SUGGESTION_ENTRY (entry), dir);
g_object_unref (dir);
g_object_unref (file);
g_free (cwd);
expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL,
0, NULL,
(GCallback)get_file_name,
NULL, NULL);
gtk_suggestion_entry_set_expression (GTK_SUGGESTION_ENTRY (entry), expression);
gtk_expression_unref (expression);
factory = gtk_signal_list_item_factory_new ();
g_signal_connect (factory, "setup", G_CALLBACK (setup_item), NULL);
g_signal_connect (factory, "bind", G_CALLBACK (bind_item), NULL);
gtk_suggestion_entry_set_factory (GTK_SUGGESTION_ENTRY (entry), factory);
g_object_unref (factory);
gtk_suggestion_entry_set_use_filter (GTK_SUGGESTION_ENTRY (entry), FALSE);
gtk_suggestion_entry_set_show_button (GTK_SUGGESTION_ENTRY (entry), TRUE);
gtk_suggestion_entry_set_insert_selection (GTK_SUGGESTION_ENTRY (entry), TRUE);
gtk_box_append (GTK_BOX (box), entry);
}
if (!gtk_widget_get_visible (window))

View File

@@ -18,7 +18,6 @@ demos = files([
'cursors.c',
'dialog.c',
'drawingarea.c',
'dropdown.c',
'dnd.c',
'editable_cells.c',
'entry_completion.c',
@@ -47,6 +46,7 @@ demos = files([
'listview_colors.c',
'listview_filebrowser.c',
'listview_minesweeper.c',
'dropdown.c',
'listview_settings.c',
'listview_weather.c',
'listview_words.c',

View File

@@ -221,6 +221,7 @@
<xi:include href="xml/gtksearchentry.xml" />
<xi:include href="xml/gtksearchbar.xml" />
<xi:include href="xml/gtkeditablelabel.xml" />
<xi:include href="xml/gtksuggestionentry.xml" />
</chapter>
<chapter id="TextWidgetObjects">

View File

@@ -7622,3 +7622,27 @@ gtk_selection_filter_model_new_for_type
gtk_selection_filter_model_set_model
gtk_selection_filter_model_get_model
</SECTION>
<SECTION>
<FILE>gtksuggestionentry</FILE>
<TITLE>GtkSuggestionEntry</TITLE>
GtkSuggestionEntry
gtk_suggestion_entry_new
gtk_suggestion_entry_set_model
gtk_suggestion_entry_get_model
gtk_suggestion_entry_set_from_strings
gtk_suggestion_entry_set_factory
gtk_suggestion_entry_get_factory
gtk_suggestion_entry_set_expression
gtk_suggestion_entry_get_expression
gtk_suggestion_entry_set_use_filter
gtk_suggestion_entry_get_use_filter
gtk_suggestion_entry_set_insert_selection
gtk_suggestion_entry_get_insert_selection
gtk_suggestion_entry_set_insert_prefix
gtk_suggestion_entry_get_insert_prefix
gtk_suggestion_entry_set_show_button
gtk_suggestion_entry_get_show_button
gtk_suggestion_entry_set_minimum_length
gtk_suggestion_entry_get_minimum_length
</SECTION>

View File

@@ -214,6 +214,7 @@ gtk_string_filter_get_type
gtk_string_list_get_type
gtk_string_object_get_type
gtk_string_sorter_get_type
gtk_suggestion_entry_get_type
gtk_switch_get_type
gtk_level_bar_get_type
gtk_style_context_get_type

View File

@@ -198,4 +198,6 @@ transitioning code for easy lookup:
| #GtkCellLayout | #GtkListItemFactory |
| #GtkCellArea | #GtkWidget |
| #GtkCellRenderer | #GtkWidget |
| #GtkComboBoxText | #GtkSuggestionEntry |
| #GtkEntryCompletion | #GtkSuggestionEntry |

View File

@@ -247,6 +247,7 @@
#include <gtk/gtkstringsorter.h>
#include <gtk/gtkstylecontext.h>
#include <gtk/gtkstyleprovider.h>
#include <gtk/gtksuggestionentry.h>
#include <gtk/gtkswitch.h>
#include <gtk/gtktext.h>
#include <gtk/gtktextbuffer.h>

1596
gtk/gtksuggestionentry.c Normal file

File diff suppressed because it is too large Load Diff

104
gtk/gtksuggestionentry.h Normal file
View File

@@ -0,0 +1,104 @@
/* GTK - The GIMP Toolkit
* Copyright (C) 2020 Red Hat, Inc.
*
* Author: Matthias Clasen <mclasen@redhat.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __GTK_SUGGESTION_ENTRY_H__
#define __GTK_SUGGESTION_ENTRY_H__
#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
#error "Only <gtk/gtk.h> can be included directly."
#endif
#include <gtk/gtkfilter.h>
#include <gtk/gtkexpression.h>
#include <gtk/gtklistitemfactory.h>
#include <gtk/gtkwidget.h>
G_BEGIN_DECLS
#define GTK_TYPE_SUGGESTION_ENTRY (gtk_suggestion_entry_get_type ())
#define GTK_SUGGESTION_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_SUGGESTION_ENTRY, GtkSuggestionEntry))
#define GTK_IS_SUGGESTION_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_SUGGESTION_ENTRY))
typedef struct _GtkSuggestionEntry GtkSuggestionEntry;
GDK_AVAILABLE_IN_ALL
GType gtk_suggestion_entry_get_type (void) G_GNUC_CONST;
GDK_AVAILABLE_IN_ALL
GtkWidget* gtk_suggestion_entry_new (void);
GDK_AVAILABLE_IN_ALL
void gtk_suggestion_entry_set_model (GtkSuggestionEntry *self,
GListModel *model);
GDK_AVAILABLE_IN_ALL
GListModel * gtk_suggestion_entry_get_model (GtkSuggestionEntry *self);
GDK_AVAILABLE_IN_ALL
void gtk_suggestion_entry_set_from_strings (GtkSuggestionEntry *self,
const char **strings);
GDK_AVAILABLE_IN_ALL
void gtk_suggestion_entry_set_factory (GtkSuggestionEntry *self,
GtkListItemFactory *factory);
GDK_AVAILABLE_IN_ALL
GtkListItemFactory *
gtk_suggestion_entry_get_factory (GtkSuggestionEntry *self);
GDK_AVAILABLE_IN_ALL
void gtk_suggestion_entry_set_use_filter (GtkSuggestionEntry *self,
gboolean use_ilter);
GDK_AVAILABLE_IN_ALL
gboolean gtk_suggestion_entry_get_use_filter (GtkSuggestionEntry *self);
GDK_AVAILABLE_IN_ALL
void gtk_suggestion_entry_set_expression (GtkSuggestionEntry *self,
GtkExpression *expression);
GDK_AVAILABLE_IN_ALL
GtkExpression * gtk_suggestion_entry_get_expression (GtkSuggestionEntry *self);
GDK_AVAILABLE_IN_ALL
void gtk_suggestion_entry_set_insert_selection (GtkSuggestionEntry *self,
gboolean insert_selection);
GDK_AVAILABLE_IN_ALL
gboolean gtk_suggestion_entry_get_insert_selection (GtkSuggestionEntry *self);
GDK_AVAILABLE_IN_ALL
void gtk_suggestion_entry_set_insert_prefix (GtkSuggestionEntry *self,
gboolean insert_prefix);
GDK_AVAILABLE_IN_ALL
gboolean gtk_suggestion_entry_get_insert_prefix (GtkSuggestionEntry *self);
GDK_AVAILABLE_IN_ALL
void gtk_suggestion_entry_set_show_button (GtkSuggestionEntry *self,
gboolean show_button);
GDK_AVAILABLE_IN_ALL
gboolean gtk_suggestion_entry_get_show_button (GtkSuggestionEntry *self);
GDK_AVAILABLE_IN_ALL
void gtk_suggestion_entry_set_minimum_length (GtkSuggestionEntry *self,
guint minimum_length);
GDK_AVAILABLE_IN_ALL
guint gtk_suggestion_entry_get_minimum_length (GtkSuggestionEntry *self);
G_END_DECLS
#endif /* __GTK_SUGGESTION_ENTRY_H__ */

View File

@@ -383,6 +383,7 @@ gtk_public_sources = files([
'gtkstringsorter.c',
'gtkstylecontext.c',
'gtkstyleprovider.c',
'gtksuggestionentry.c',
'gtkswitch.c',
'gtktestutils.c',
'gtktext.c',
@@ -652,6 +653,7 @@ gtk_public_headers = files([
'gtkstringsorter.h',
'gtkstylecontext.h',
'gtkstyleprovider.h',
'gtksuggestionentry.h',
'gtkswitch.h',
'gtktestutils.h',
'gtktext.h',

View File

@@ -1153,8 +1153,10 @@ spinbutton {
/**************
* ComboBoxes *
**************/
entry.suggestion > popover.menu.background > contents,
dropdown > popover.menu.background > contents { padding: 0; } //allow search entries with no margin
entry.suggestion,
dropdown,
combobox {
arrow {

View File

@@ -67,6 +67,7 @@ gtk_tests = [
['testselectionmode'],
['testsounds'],
['testspinbutton'],
['testsuggestions'],
['testtreechanging'],
['testtreednd'],
['testtreeedit'],

390
tests/testsuggestions.c Normal file
View File

@@ -0,0 +1,390 @@
#include <gtk/gtk.h>
#include <ctype.h>
#define GTK_TYPE_MATCH_OBJECT (gtk_match_object_get_type ())
GDK_AVAILABLE_IN_ALL
G_DECLARE_FINAL_TYPE (GtkMatchObject, gtk_match_object, GTK, MATCH_OBJECT, GObject)
struct _GtkMatchObject
{
GObject parent_instance;
char *string;
int start;
int end;
int score;
};
enum {
PROP_STRING = 1,
PROP_START,
PROP_END,
PROP_SCORE
};
G_DEFINE_TYPE (GtkMatchObject, gtk_match_object, G_TYPE_OBJECT);
static void
gtk_match_object_init (GtkMatchObject *object)
{
object->start = -1;
object->end = -1;
object->score = 0;
}
static void
gtk_match_object_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GtkMatchObject *self = GTK_MATCH_OBJECT (object);
switch (property_id)
{
case PROP_STRING:
g_value_set_string (value, self->string);
break;
case PROP_START:
g_value_set_int (value, self->start);
break;
case PROP_END:
g_value_set_int (value, self->end);
break;
case PROP_SCORE:
g_value_set_int (value, self->score);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gtk_match_object_finalize (GObject *object)
{
GtkMatchObject *self = GTK_MATCH_OBJECT (object);
g_free (self->string);
G_OBJECT_CLASS (gtk_match_object_parent_class)->finalize (object);
}
static void
gtk_match_object_class_init (GtkMatchObjectClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS (class);
GParamSpec *pspec;
object_class->finalize = gtk_match_object_finalize;
object_class->get_property = gtk_match_object_get_property;
pspec = g_param_spec_string ("string", "String", "String",
NULL,
G_PARAM_READABLE |
G_PARAM_STATIC_STRINGS);
g_object_class_install_property (object_class, PROP_STRING, pspec);
pspec = g_param_spec_int ("start", "Start", "Match Start",
-1, G_MAXINT, -1,
G_PARAM_READABLE |
G_PARAM_STATIC_STRINGS);
g_object_class_install_property (object_class, PROP_START, pspec);
pspec = g_param_spec_int ("end", "End", "Match End",
-1, G_MAXINT, -1,
G_PARAM_READABLE |
G_PARAM_STATIC_STRINGS);
g_object_class_install_property (object_class, PROP_END, pspec);
pspec = g_param_spec_int ("score", "Score", "Score",
0, G_MAXINT, 0,
G_PARAM_READABLE |
G_PARAM_STATIC_STRINGS);
g_object_class_install_property (object_class, PROP_SCORE, pspec);
}
static GtkMatchObject *
gtk_match_object_new (const char *string)
{
GtkMatchObject *obj;
obj = g_object_new (GTK_TYPE_MATCH_OBJECT, NULL);
obj->string = g_strdup (string);
return obj;
}
static void
gtk_match_object_set_match (GtkMatchObject *obj,
int start,
int end)
{
g_object_freeze_notify (G_OBJECT (obj));
if (obj->start != start)
{
obj->start = start;
g_object_notify (G_OBJECT (obj), "start");
}
if (obj->end != end)
{
obj->end = end;
g_object_notify (G_OBJECT (obj), "end");
}
g_object_thaw_notify (G_OBJECT (obj));
}
static void
gtk_match_object_set_score (GtkMatchObject *obj,
int score)
{
if (obj->score == score)
return;
obj->score = score;
g_object_notify (G_OBJECT (obj), "score");
}
static const char *
gtk_match_object_get_string (GtkMatchObject *obj)
{
return obj->string;
}
static int
gtk_match_object_get_start (GtkMatchObject *obj)
{
return obj->start;
}
static int
gtk_match_object_get_end (GtkMatchObject *obj)
{
return obj->end;
}
static int
gtk_match_object_get_score (GtkMatchObject *obj)
{
return obj->score;
}
static GListModel *
load_words (const char *file)
{
char *contents;
gsize size;
char **lines;
GError *error = NULL;
GListStore *store;
int i;
if (!g_file_get_contents (file, &contents, &size, &error))
{
g_printerr ("%s", error->message);
exit (1);
}
store = g_list_store_new (GTK_TYPE_MATCH_OBJECT);
lines = g_strsplit (contents, "\n", -1);
for (i = 0; lines[i]; i++)
{
char *s = g_strstrip (lines[i]);
if (s[0] != '\0')
{
GtkMatchObject *obj = gtk_match_object_new (s);
g_list_store_append (store, obj);
g_object_unref (obj);
}
}
g_strfreev (lines);
g_free (contents);
return G_LIST_MODEL (store);
}
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)
{
GtkMatchObject *obj;
GtkWidget *label;
PangoAttrList *attrs;
PangoAttribute *attr;
const char *str;
int score;
char *text;
obj = GTK_MATCH_OBJECT (gtk_list_item_get_item (list_item));
label = gtk_list_item_get_child (list_item);
str = gtk_match_object_get_string (obj);
score = gtk_match_object_get_score (obj);
text = g_strdup_printf ("%s - %d", str, score);
gtk_label_set_label (GTK_LABEL (label), text);
g_free (text);
attrs = pango_attr_list_new ();
attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD);
attr->start_index = gtk_match_object_get_start (obj);
attr->end_index = gtk_match_object_get_end (obj);
pango_attr_list_insert (attrs, attr);
gtk_label_set_attributes (GTK_LABEL (label), attrs);
pango_attr_list_unref (attrs);
}
static void
text_changed (GObject *object,
GParamSpec *pspec,
GListModel *model)
{
const char *text;
guint i, n;
int len;
text = gtk_editable_get_text (GTK_EDITABLE (object));
len = strlen (text);
n = g_list_model_get_n_items (model);
for (i = 0; i < n; i++)
{
GtkMatchObject *obj = GTK_MATCH_OBJECT (g_list_model_get_item (model, i));
char *p;
int start, end;
int score;
#ifdef G_OS_WIN32
p = strstr (obj->string, text);
#else
p = strcasestr (obj->string, text);
#endif
if (p)
{
start = p - obj->string;
end = start + len;
/* prefer matches close to the start */
score = 100 - start;
/* prefer exact matches */
if (strncmp (p, text, len) == 0)
score += 10;
/* prefer words */
if ((start == 0 || isspace (p[-1])) &&
(p[len] == '\0' || isspace (p[len]) || ispunct (p[len])))
score += 20;
}
else
{
start = end = -1;
score = 0;
}
gtk_match_object_set_match (obj, start, end);
gtk_match_object_set_score (obj, score);
g_object_unref (obj);
}
g_list_model_items_changed (model, 0, n, n);
}
static gboolean
filter_func (gpointer item, gpointer data)
{
return GTK_MATCH_OBJECT (item)->score > 0;
}
static int
compare_func (gconstpointer a,
gconstpointer b,
gpointer data)
{
const GtkMatchObject *ao = a;
const GtkMatchObject *bo = b;
return bo->score - ao->score;
}
int
main (int argc, char *argv[])
{
GtkWidget *window;
GtkWidget *entry;
GListModel *words;
GListModel *sorted;
GListModel *filtered;
GtkExpression *expression;
GtkListItemFactory *factory;
GtkFilter *filter;
GtkSorter *sorter;
words = load_words (argv[1]);
sorter = gtk_custom_sorter_new (compare_func, NULL, NULL);
sorted = G_LIST_MODEL (gtk_sort_list_model_new (words, sorter));
g_object_unref (sorter);
filter = gtk_custom_filter_new (filter_func, NULL, NULL);
filtered = G_LIST_MODEL (gtk_filter_list_model_new (sorted, filter));
g_object_unref (filter);
gtk_init ();
window = gtk_window_new ();
entry = gtk_suggestion_entry_new ();
gtk_suggestion_entry_set_model (GTK_SUGGESTION_ENTRY (entry), filtered);
gtk_suggestion_entry_set_use_filter (GTK_SUGGESTION_ENTRY (entry), FALSE);
expression = gtk_property_expression_new (GTK_TYPE_MATCH_OBJECT, NULL, "string");
gtk_suggestion_entry_set_expression (GTK_SUGGESTION_ENTRY (entry), expression);
gtk_expression_unref (expression);
factory = gtk_signal_list_item_factory_new ();
g_signal_connect (factory, "setup", G_CALLBACK (setup_item), NULL);
g_signal_connect (factory, "bind", G_CALLBACK (bind_item), NULL);
gtk_suggestion_entry_set_factory (GTK_SUGGESTION_ENTRY (entry), factory);
g_object_unref (factory);
g_signal_connect (entry, "notify::text", G_CALLBACK (text_changed), words);
gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
gtk_window_set_child (GTK_WINDOW (window), entry);
gtk_window_present (GTK_WINDOW (window));
while (g_list_model_get_n_items (gtk_window_get_toplevels ()) > 0)
g_main_context_iteration (NULL, TRUE);
g_object_unref (words);
g_object_unref (sorted);
g_object_unref (filtered);
return 0;
}

View File

@@ -417,6 +417,10 @@ G_GNUC_END_IGNORE_DEPRECATIONS
strcmp (pspec->name, "factory") == 0)
continue;
if (g_type_is_a (type, GTK_TYPE_SUGGESTION_ENTRY) &&
strcmp (pspec->name, "factory") == 0)
continue;
if (g_type_is_a (type, GTK_TYPE_BOOKMARK_LIST) &&
(strcmp (pspec->name, "filename") == 0 ||
strcmp (pspec->name, "loading") == 0))

View File

@@ -653,6 +653,10 @@ test_type (gconstpointer data)
g_str_equal (pspec->name, "selected"))
continue;
if (g_type_is_a (type, GTK_TYPE_SUGGESTION_ENTRY) &&
g_str_equal (pspec->name, "popup-visible"))
continue;
/* can't set position without a notebook */
if (g_type_is_a (type, GTK_TYPE_NOTEBOOK_PAGE) &&
g_str_equal (pspec->name, "position"))