From 4bd4dac27293030c95a49e03421afbd072611c11 Mon Sep 17 00:00:00 2001 From: Reuben Green Date: Thu, 12 Mar 2020 17:28:34 +0000 Subject: [PATCH] Add a widget for filename input Adds a new widget, XfceFilenameInput, which essentially provides a smart version of the GtkEntry widget especially for entering filenames for creating or renaming files. In such situations it is necessary to check the filename the user has entered for validity (for example, it cannot be too long or contain directory separator characters). The new widget provides this, with as-you-type error messages to tell the user if the filename they have entered is not valid. --- libxfce4ui/Makefile.am | 2 + libxfce4ui/libxfce4ui.h | 1 + libxfce4ui/libxfce4ui.symbols | 14 ++ libxfce4ui/xfce-filename-input.c | 349 +++++++++++++++++++++++++++++++ libxfce4ui/xfce-filename-input.h | 61 ++++++ 5 files changed, 427 insertions(+) create mode 100644 libxfce4ui/xfce-filename-input.c create mode 100644 libxfce4ui/xfce-filename-input.h diff --git a/libxfce4ui/Makefile.am b/libxfce4ui/Makefile.am index 3daa0ab..49edf47 100644 --- a/libxfce4ui/Makefile.am +++ b/libxfce4ui/Makefile.am @@ -24,6 +24,7 @@ libxfce4ui_headers = \ xfce-gdk-extensions.h \ xfce-gtk-extensions.h \ xfce-spawn.h \ + xfce-filename-input.h \ xfce-titled-dialog.h \ $(libxfce4ui_enum_headers) @@ -47,6 +48,7 @@ libxfce4ui_sources = \ xfce-gtk-extensions.c \ xfce-sm-client.c \ xfce-spawn.c \ + xfce-filename-input.c \ xfce-titled-dialog.c libxfce4ui_includedir = \ diff --git a/libxfce4ui/libxfce4ui.h b/libxfce4ui/libxfce4ui.h index 3e0b536..6dc15f8 100644 --- a/libxfce4ui/libxfce4ui.h +++ b/libxfce4ui/libxfce4ui.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include diff --git a/libxfce4ui/libxfce4ui.symbols b/libxfce4ui/libxfce4ui.symbols index f8b55d5..e4017b1 100644 --- a/libxfce4ui/libxfce4ui.symbols +++ b/libxfce4ui/libxfce4ui.symbols @@ -101,6 +101,20 @@ xfce_spawn_command_line_on_screen #endif #endif +/* xfce-filename-input functions */ +#if IN_HEADER(__XFCE_FILENAME_INPUT_H__) +#if IN_SOURCE(__XFCE_FILENAME_INPUT_C__) +xfce_filename_input_get_type +xfce_filename_input_get_text +xfce_filename_input_sensitise_widget +xfce_filename_input_desensitise_widget +xfce_filename_input_set_original_filename +xfce_filename_input_set_max_length +xfce_filename_input_check +xfce_filename_input_get_entry +#endif +#endif + /* xfce-sm-client functions */ #if IN_HEADER(__XFCE_SM_CLIENT_H__) #if IN_SOURCE(__XFCE_SM_CLIENT_C__) diff --git a/libxfce4ui/xfce-filename-input.c b/libxfce4ui/xfce-filename-input.c new file mode 100644 index 0000000..7d5dad1 --- /dev/null +++ b/libxfce4ui/xfce-filename-input.c @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2020 The Xfce Development Team + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +/** + * SECTION:xfce-filename-input + * @title: XfceFilenameInput + * @short_description: widget for filename input + * @stability: Stable + * @include: libxfce4ui/libxfce4ui.h + * + * A widget to allow filename input for creating or renaming files, + * with as-you-type checking for invalid filenames. + **/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#ifdef HAVE_STRING_H +#include +#endif +#ifdef HAVE_STDARG_H +#include +#endif +#ifdef HAVE_LOCALE_H +#include +#endif + +#include +#include + +#include +#include +#include + + +/* Signal identifiers */ +enum +{ + SIG_TEXT_VALID = 0, + SIG_TEXT_INVALID, + N_SIGS +}; + + +static void xfce_filename_input_changed (GtkEditable *editable, + gpointer data); + +static void xfce_filename_input_finalize (GObject *object); + +static gboolean xfce_filename_input_entry_undo (GtkWidget *widget, + GdkEvent *event, + gpointer data); + + +struct _XfceFilenameInputClass +{ + GtkBoxClass parent; + + /* signals */ + void (*text_valid) (XfceFilenameInput *filename_input); + void (*text_invalid) (XfceFilenameInput *filename_input); +}; + +struct _XfceFilenameInput +{ + GtkBox parent; + + GtkEntry *entry; + GtkLabel *label; + + GRegex *whitespace_regex; + GRegex *dir_sep_regex; + + guint max_text_length; + gchar *original_filename; +}; + +static guint signals[N_SIGS]; + +G_DEFINE_TYPE (XfceFilenameInput, xfce_filename_input, GTK_TYPE_BOX) + + +static void +xfce_filename_input_class_init (XfceFilenameInputClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *)klass; + + gobject_class->finalize = xfce_filename_input_finalize; + + /** + * XfceFilenameInput::text-valid: + * @filename_input: An #XfceFilenameInput + * + * Signals that the current text is a valid filename. This signal is + * emitted whenever the user changes the text and the result is a valid + * filename. + **/ + signals[SIG_TEXT_VALID] = g_signal_new ("text-valid", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (XfceFilenameInputClass, + text_valid), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /** + * XfceFilenameInput::text-invalid: + * @filename_input: An #XfceFilenameInput + * + * Signals that the current text is not a valid filename. This signal is + * emitted whenever the user changes the text and the result is not a valid + * filename. + **/ + signals[SIG_TEXT_VALID] = g_signal_new ("text-invalid", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (XfceFilenameInputClass, + text_invalid), + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + + +static void +xfce_filename_input_init (XfceFilenameInput *filename_input) +{ + GError *err = NULL; + + /* by default there is no maximum length for the filename and no original filename */ + filename_input->max_text_length = -1; + filename_input->original_filename = NULL; + + /* compile the regular expressions used to check the input */ + /* the pattern for whitespace_regex matches if the text starts or ends with whitespace */ + filename_input->whitespace_regex = g_regex_new ("^\\s|\\s$", 0, 0, &err); + filename_input->dir_sep_regex = g_regex_new (G_DIR_SEPARATOR_S, 0, 0, &err); + + gtk_orientable_set_orientation (GTK_ORIENTABLE (filename_input), GTK_ORIENTATION_VERTICAL); + gtk_container_set_border_width (GTK_CONTAINER (filename_input), 2); + + /* set up the GtkEntry for the input */ + filename_input->entry = GTK_ENTRY (gtk_entry_new()); + gtk_widget_set_hexpand (GTK_WIDGET (filename_input->entry), TRUE); + gtk_widget_set_valign (GTK_WIDGET (filename_input->entry), GTK_ALIGN_CENTER); + gtk_box_pack_start (GTK_BOX (filename_input), GTK_WIDGET (filename_input->entry), FALSE, FALSE, 0); + + /* set up the GtkLabel to display any error or warning messages */ + filename_input->label = GTK_LABEL (gtk_label_new("")); + gtk_label_set_xalign (filename_input->label, 0.0f); + gtk_widget_set_hexpand (GTK_WIDGET (filename_input->label), TRUE); + gtk_box_pack_start (GTK_BOX (filename_input), GTK_WIDGET (filename_input->label), FALSE, FALSE, 0); + + /* allow reverting the filename with ctrl + z */ + g_signal_connect (filename_input->entry, "key-press-event", + G_CALLBACK (xfce_filename_input_entry_undo), filename_input); + + /* set up a callback to check the input text whenever it is changed*/ + g_signal_connect (filename_input->entry, "changed", + G_CALLBACK (xfce_filename_input_changed), filename_input); +} + +static void +xfce_filename_input_finalize (GObject *object) +{ + XfceFilenameInput *filename_input = XFCE_FILENAME_INPUT (object); + + g_regex_unref (filename_input->whitespace_regex); + g_regex_unref (filename_input->dir_sep_regex); + + g_free (filename_input->original_filename); + + (*G_OBJECT_CLASS (xfce_filename_input_parent_class)->finalize) (object); +} + +/* + * xfce_filename_input_check is used to force a check of the current input text + * even when it has not changed. This is useful to force the appropriate signal + * to be sent to indicate whether the text is a valid filename or not, so that + * for example any GtkWidgets whose sensitivity is controlled by this can be + * correctly updated. + */ +void +xfce_filename_input_check (XfceFilenameInput *filename_input) +{ + g_signal_emit_by_name (filename_input->entry, "changed", 0); +} + +void +xfce_filename_input_set_original_filename (XfceFilenameInput *filename_input, gchar *filename) +{ + /* we only allow the original filename to be set once */ + if (filename == NULL || filename_input->original_filename != NULL) + return; + + filename_input->original_filename = g_strdup (filename); + gtk_entry_set_text (filename_input->entry, filename_input->original_filename); +} + +void +xfce_filename_input_set_max_length (XfceFilenameInput *filename_input, gint max_length) +{ + filename_input->max_text_length = max_length; +} + +GtkEntry* +xfce_filename_input_get_entry (XfceFilenameInput *filename_input) +{ + return filename_input->entry; +} + +const gchar* +xfce_filename_input_get_text (XfceFilenameInput *filename_input) +{ + return gtk_entry_get_text (filename_input->entry); +} + +static void +xfce_filename_input_changed (GtkEditable *editable, + gpointer data) +{ + XfceFilenameInput *filename_input = XFCE_FILENAME_INPUT (data); + GtkEntry *entry = GTK_ENTRY (editable); + GtkLabel *label = filename_input->label; + + gint text_length; + const gchar *text; + const gchar *label_text = ""; + const gchar *icon_name = NULL; + gboolean new_text_valid = TRUE; + + GMatchInfo *matchInfo; + gboolean match_ws, match_ds; + + text_length = gtk_entry_get_text_length (entry); + text = gtk_entry_get_text (entry); /* NB this string must not be modified or freed, + as it belongs to the GtkEntry */ + + /* + * check whether the string starts or ends with whitespace, or contains the directory + * separator + */ + match_ws = g_regex_match (filename_input->whitespace_regex, text, 0, NULL); + match_ds = g_regex_match (filename_input->dir_sep_regex, text, 0, NULL); + + if (text_length == 0) /* an empty string is not a valid filename */ + { + icon_name = NULL; + label_text = ""; + new_text_valid = FALSE; + } + else if (match_ds) + { + label_text = _("Directory separator illegal in file name"); + icon_name = "dialog-error"; + new_text_valid = FALSE; + } + else if (filename_input->max_text_length != -1 && /* max_text_length = -1 means no maximum */ + text_length > filename_input->max_text_length) + { + label_text = _("Filename is too long"); + icon_name = "dialog-error"; + new_text_valid = FALSE; + } + else if (match_ws) + { + label_text = _("Filenames should not start or end with a space"); + icon_name = "dialog-warning"; + new_text_valid = TRUE; + } + + /* update the icon in the GtkEntry and the message in the GtkLabel */ + gtk_entry_set_icon_from_icon_name (entry, + GTK_ENTRY_ICON_SECONDARY, + icon_name); + gtk_label_set_text (label,label_text); + + /* send a signal to indicate whether the filename is valid */ + gtk_entry_set_activates_default (entry, new_text_valid); + if (new_text_valid) + g_signal_emit_by_name (filename_input, "text-valid", 0); + else + g_signal_emit_by_name (filename_input, "text-invalid", 0); +} + +static gboolean +xfce_filename_input_entry_undo (GtkWidget *widget, + GdkEvent *event, + gpointer data) +{ + guint keyval; + GdkModifierType state; + XfceFilenameInput *filename_input = XFCE_FILENAME_INPUT (data); + + if (filename_input->original_filename == NULL) + return GDK_EVENT_PROPAGATE; + + if (G_UNLIKELY (!gdk_event_get_keyval (event, &keyval) || + !gdk_event_get_state (event, &state))) + return GDK_EVENT_PROPAGATE; + + if ((state & GDK_CONTROL_MASK) != 0 && keyval == GDK_KEY_z) + { + gtk_entry_set_text (GTK_ENTRY (widget), + filename_input->original_filename); + return GDK_EVENT_STOP; + } + + return GDK_EVENT_PROPAGATE; +} + +/* + * xfce_filename_input_desensitise_widget and xfce_filename_input_sensitise_widget + * are convenince functions to be connected as callbacks for the "text-valid" and + * "text-invalid" signals (using g_connect_swapped) for the simple case where the + * desired effect of these signals is to set the sensitivity of a single GtkWidget + * (for example, a GtkButton). + */ + +void +xfce_filename_input_desensitise_widget( GtkWidget *widget) +{ + gtk_widget_set_sensitive (widget, FALSE); +} + +void +xfce_filename_input_sensitise_widget( GtkWidget *widget ) +{ + gtk_widget_set_sensitive (widget, TRUE); +} + +#define __XFCE_FILENAME_INPUT_C__ +#include diff --git a/libxfce4ui/xfce-filename-input.h b/libxfce4ui/xfce-filename-input.h new file mode 100644 index 0000000..f53da33 --- /dev/null +++ b/libxfce4ui/xfce-filename-input.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 The Xfce Development Team + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#if !defined (LIBXFCE4UI_INSIDE_LIBXFCE4UI_H) && !defined (LIBXFCE4UI_COMPILATION) +#error "Only can be included directly, this file is not part of the public API." +#endif + +#ifndef __XFCE_FILENAME_INPUT_H__ +#define __XFCE_FILENAME_INPUT_H__ + +#include + +G_BEGIN_DECLS + +typedef struct _XfceFilenameInputClass XfceFilenameInputClass; +typedef struct _XfceFilenameInput XfceFilenameInput; + +#define XFCE_TYPE_FILENAME_INPUT (xfce_filename_input_get_type()) +#define XFCE_FILENAME_INPUT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), XFCE_TYPE_FILENAME_INPUT, XfceFilenameInput)) +#define XFCE_IS_FILENAME_INPUT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), XFCE_TYPE_FILENAME_INPUT)) +#define XFCE_FILENAME_INPUT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), XFCE_TYPE_FILENAME_INPUT, XfceFilenameInputClass)) +#define XFCE_IS_FILENAME_INPUT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), XFCE_TYPE_FILENAME_INPUT)) +#define XFCE_FILENAME_INPUT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), XFCE_TYPE_FILENAME_INPUT, XfceFilenameInputClass)) + +GType xfce_filename_input_get_type (void) G_GNUC_CONST; + +const gchar *xfce_filename_input_get_text (XfceFilenameInput *filename_input); + +void xfce_filename_input_sensitise_widget (GtkWidget *widget); + +void xfce_filename_input_desensitise_widget (GtkWidget *widget); + +void xfce_filename_input_set_original_filename (XfceFilenameInput *filename_input, + gchar *filename); + +void xfce_filename_input_set_max_length (XfceFilenameInput *filename_input, + gint max_length); + +void xfce_filename_input_check (XfceFilenameInput *filename_input); + +GtkEntry *xfce_filename_input_get_entry (XfceFilenameInput *filename_input); + +G_END_DECLS + +#endif /* !__XFCE_FILENAME_INPUT_H__ */ -- 2.25.1