1101 lines
34 KiB
C
1101 lines
34 KiB
C
/*
|
|
* Copyright 2020 Mihai Bazon
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software
|
|
* for any purpose with or without fee is hereby granted, provided that
|
|
* the above copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
|
* OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
|
|
* BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR
|
|
* ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
* WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
|
* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
#include <gdk/gdkkeysyms.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <dirent.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include "config.h"
|
|
|
|
#include "config_prefs.h"
|
|
#include "gtkcompletionline.h"
|
|
|
|
#define HISTORY_FILE ".gmrun_history"
|
|
|
|
static int on_cursor_changed_handler = 0;
|
|
static int on_key_press_handler = 0;
|
|
static guint timeout_id = 0;
|
|
|
|
/* history search - backwards / fordwards -- see history.c */
|
|
const char * (*history_search_first_func) (HistoryFile *);
|
|
const char * (*history_search_next_func) (HistoryFile *);
|
|
static gboolean searching_history = FALSE;
|
|
|
|
/* GLOBALS */
|
|
|
|
/* signals */
|
|
enum {
|
|
UNIQUE,
|
|
NOTUNIQUE,
|
|
INCOMPLETE,
|
|
RUNWITHTERM,
|
|
SEARCH_MODE,
|
|
SEARCH_LETTER,
|
|
SEARCH_NOT_FOUND,
|
|
EXT_HANDLER,
|
|
CANCEL,
|
|
LAST_SIGNAL
|
|
};
|
|
|
|
static guint gtk_completion_line_signals[LAST_SIGNAL];
|
|
|
|
static gchar ** path_gc = NULL; /* string list (gchar *) containing each directory in PATH */
|
|
static gchar * prefix = NULL;
|
|
static int g_show_dot_files;
|
|
|
|
/* callbacks */
|
|
static gboolean on_key_press (GtkCompletionLine *cl, GdkEventKey *event, gpointer data);
|
|
static gboolean on_scroll (GtkCompletionLine *cl, GdkEventScroll *event, gpointer data);
|
|
|
|
// https://developer.gnome.org/gobject/stable/gobject-Type-Information.html#G-DEFINE-TYPE-EXTENDED:CAPS
|
|
G_DEFINE_TYPE_EXTENDED (GtkCompletionLine, /* type name */
|
|
gtk_completion_line, /* type name in lowercase, separated by '_' */
|
|
GTK_TYPE_ENTRY, /* GType of the parent type */
|
|
(GTypeFlags)0,
|
|
NULL);
|
|
static void gtk_completion_line_dispose (GObject *object);
|
|
static void gtk_completion_line_finalize (GObject *object);
|
|
// see also https://developer.gnome.org/gobject/stable/GTypeModule.html#G-DEFINE-DYNAMIC-TYPE:CAPS
|
|
|
|
/* class_init */
|
|
static void gtk_completion_line_class_init (GtkCompletionLineClass *klass)
|
|
{
|
|
GtkWidgetClass * object_class = GTK_WIDGET_CLASS (klass);
|
|
guint s;
|
|
|
|
GObjectClass * basic_class = G_OBJECT_CLASS (klass);
|
|
basic_class->dispose = gtk_completion_line_dispose;
|
|
basic_class->finalize = gtk_completion_line_finalize;
|
|
|
|
s = g_signal_new ("unique",
|
|
G_TYPE_FROM_CLASS (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkCompletionLineClass, unique),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__VOID,
|
|
G_TYPE_NONE, /* return_type */
|
|
0); /* n_params */
|
|
gtk_completion_line_signals[UNIQUE] = s;
|
|
|
|
s = g_signal_new ("notunique",
|
|
G_TYPE_FROM_CLASS (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkCompletionLineClass, notunique),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__VOID,
|
|
G_TYPE_NONE, /* return_type */
|
|
0); /* n_params */
|
|
gtk_completion_line_signals[NOTUNIQUE] = s;
|
|
|
|
s = g_signal_new ("incomplete",
|
|
G_TYPE_FROM_CLASS (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkCompletionLineClass, incomplete),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__VOID,
|
|
G_TYPE_NONE, /* return_type */
|
|
0); /* n_params */
|
|
gtk_completion_line_signals[INCOMPLETE] = s;
|
|
|
|
s = g_signal_new ("runwithterm",
|
|
G_TYPE_FROM_CLASS (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkCompletionLineClass, runwithterm),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__VOID,
|
|
G_TYPE_NONE, /* return_type */
|
|
0); /* n_params */
|
|
gtk_completion_line_signals[RUNWITHTERM] = s;
|
|
|
|
s = g_signal_new ("search_mode",
|
|
G_TYPE_FROM_CLASS (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkCompletionLineClass, search_mode),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__VOID,
|
|
G_TYPE_NONE, /* return_type */
|
|
0); /* n_params */
|
|
gtk_completion_line_signals[SEARCH_MODE] = s;
|
|
|
|
s = g_signal_new ("search_not_found",
|
|
G_TYPE_FROM_CLASS (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkCompletionLineClass, search_not_found),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__VOID,
|
|
G_TYPE_NONE, /* return_type */
|
|
0); /* n_params */
|
|
gtk_completion_line_signals[SEARCH_NOT_FOUND] = s;
|
|
|
|
s = g_signal_new ("search_letter",
|
|
G_TYPE_FROM_CLASS (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkCompletionLineClass, search_letter),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__VOID,
|
|
G_TYPE_NONE, /* return_type */
|
|
0); /* n_params */
|
|
gtk_completion_line_signals[SEARCH_LETTER] = s;
|
|
|
|
s = g_signal_new ("ext_handler",
|
|
G_TYPE_FROM_CLASS (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkCompletionLineClass, ext_handler),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__POINTER,
|
|
G_TYPE_NONE, /* return_type */
|
|
1, /* n_params */
|
|
G_TYPE_POINTER);
|
|
gtk_completion_line_signals[EXT_HANDLER] = s;
|
|
|
|
s = g_signal_new ("cancel",
|
|
G_TYPE_FROM_CLASS (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkCompletionLineClass, cancel),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__POINTER,
|
|
G_TYPE_NONE, /* return_type */
|
|
1, /* n_params */
|
|
G_TYPE_POINTER);
|
|
gtk_completion_line_signals[CANCEL] = s;
|
|
|
|
klass->unique = NULL;
|
|
klass->notunique = NULL;
|
|
klass->incomplete = NULL;
|
|
klass->runwithterm = NULL;
|
|
klass->search_mode = NULL;
|
|
klass->search_letter = NULL;
|
|
klass->search_not_found = NULL;
|
|
klass->ext_handler = NULL;
|
|
klass->cancel = NULL;
|
|
}
|
|
|
|
/* init */
|
|
static void gtk_completion_line_init (GtkCompletionLine *self)
|
|
{
|
|
/* Add object initialization / creation stuff here */
|
|
self->win_compl = NULL;
|
|
self->list_compl = NULL;
|
|
self->sort_list_compl = NULL;
|
|
self->tree_compl = NULL;
|
|
self->hist_search_mode = FALSE;
|
|
self->hist_word[0] = 0;
|
|
self->hist_word_count = 0;
|
|
self->tabtimeout = 0;
|
|
self->show_dot_files = 0;
|
|
|
|
// required for gtk3+
|
|
gtk_widget_add_events(GTK_WIDGET(self), GDK_SCROLL_MASK);
|
|
|
|
on_key_press_handler = g_signal_connect(G_OBJECT(self),
|
|
"key_press_event",
|
|
G_CALLBACK(on_key_press), NULL);
|
|
g_signal_connect(G_OBJECT(self), "scroll-event",
|
|
G_CALLBACK(on_scroll), NULL);
|
|
|
|
char * HOME = getenv ("HOME");
|
|
char history_file[512] = "";
|
|
if (HOME) {
|
|
snprintf (history_file, sizeof (history_file), "%s/%s", HOME, HISTORY_FILE);
|
|
}
|
|
|
|
int HIST_MAX_SIZE;
|
|
if (!config_get_int ("History", &HIST_MAX_SIZE))
|
|
HIST_MAX_SIZE = 20;
|
|
|
|
self->hist = history_new (history_file, HIST_MAX_SIZE);
|
|
// hacks for prev/next will be applied
|
|
history_unset_current (self->hist);
|
|
}
|
|
|
|
static void gtk_completion_line_dispose (GObject *object)
|
|
{
|
|
GtkCompletionLine * self = GTK_COMPLETION_LINE (object);
|
|
// GTK3: Pango-CRITICAL **: pango_layout_get_cursor_pos: assertion 'index >= 0 && index <= layout->length' failed
|
|
// -- for some reason there's an error when the object is destroyed
|
|
// -- The GtkCompletionLine 'cancel' signal makes gmrun destroy the object and exit
|
|
// -- The current fix is to set an empty text
|
|
gtk_entry_set_text (GTK_ENTRY (self), "");
|
|
// --
|
|
if (path_gc) {
|
|
g_strfreev (path_gc);
|
|
path_gc = NULL;
|
|
}
|
|
G_OBJECT_CLASS (gtk_completion_line_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void gtk_completion_line_finalize (GObject *object)
|
|
{
|
|
GtkCompletionLine * self = GTK_COMPLETION_LINE (object);
|
|
if (self->hist) {
|
|
// 0 = save | 1 = if changed
|
|
history_save (self->hist, HISTORY_SAVE_IF_CHANGED);
|
|
//history_print (self->hist); //debug
|
|
history_destroy (self->hist);
|
|
self->hist = NULL;
|
|
}
|
|
G_OBJECT_CLASS (gtk_completion_line_parent_class)->finalize (object);
|
|
}
|
|
|
|
// ====================================================================
|
|
|
|
void gtk_completion_line_last_history_item (GtkCompletionLine* object) {
|
|
const char *last_item = history_last (object->hist);
|
|
if (last_item) {
|
|
gtk_entry_set_text (GTK_ENTRY(object), last_item);
|
|
}
|
|
}
|
|
|
|
/* Get one word of a string: separator is space, but only unescaped spaces */
|
|
static const gchar *get_token (const gchar *str, char *out_buf, int out_buf_len)
|
|
{
|
|
gboolean escaped = FALSE;
|
|
*out_buf = 0;
|
|
int x = 0;
|
|
while (*str != '\0')
|
|
{
|
|
if (escaped) {
|
|
escaped = FALSE;
|
|
out_buf[x++] = *str;
|
|
} else if (*str == '\\') {
|
|
escaped = TRUE;
|
|
} else if (isspace(*str)) {
|
|
while (isspace(*str)) str++;
|
|
break;
|
|
} else {
|
|
out_buf[x++] = *str;
|
|
}
|
|
if (x >= out_buf_len) {
|
|
break;
|
|
}
|
|
str++;
|
|
}
|
|
out_buf[x] = 0;
|
|
return str;
|
|
}
|
|
|
|
/* get words before current edit position */
|
|
int get_words (GtkCompletionLine * object, GList ** words)
|
|
{
|
|
#ifdef DEBUG
|
|
printf (" get_words\n");
|
|
#endif
|
|
const gchar * content = gtk_entry_get_text (GTK_ENTRY(object));
|
|
const gchar * i = content;
|
|
int pos = gtk_editable_get_position (GTK_EDITABLE(object));
|
|
int n_w = 0;
|
|
char tmp[2048] = "";
|
|
|
|
while (*i != '\0')
|
|
{
|
|
i = get_token (i, tmp, 2000);
|
|
*words = g_list_append (*words, g_strdup(tmp));
|
|
if (*i && (i - content < pos) && (i != content))
|
|
n_w++;
|
|
}
|
|
|
|
if (!*words) { // must add empty string otherwise a segfault awaits
|
|
*words = g_list_append (*words, strdup (""));
|
|
}
|
|
|
|
return n_w;
|
|
}
|
|
|
|
/* Replace words in the entry fields, return position of char after first 'pos' words */
|
|
int set_words (GtkCompletionLine *object, GList *words, int pos)
|
|
{
|
|
#ifdef DEBUG
|
|
printf (" set_words\n");
|
|
#endif
|
|
GList * igl;
|
|
char * word;
|
|
char * tmp;
|
|
int tmp_len = 0;
|
|
|
|
// determine buffer length
|
|
for (igl = words; igl; igl = igl->next) {
|
|
word = (char*) (igl->data);
|
|
tmp_len += strlen (word) + 5;
|
|
for (tmp = word; *tmp; tmp++)
|
|
if (*tmp == ' ')
|
|
tmp_len += 5;
|
|
}
|
|
tmp = (char*) g_malloc0 (sizeof(char) * tmp_len);
|
|
|
|
if (pos == -1)
|
|
pos = g_list_length (words) - 1;
|
|
int cur = 0;
|
|
int i = 0;
|
|
|
|
while (words)
|
|
{
|
|
// replace ' ' with '\ ' [escape]
|
|
word = (char *)(words->data);
|
|
while (*word) {
|
|
if (*word == ' ') {
|
|
tmp[i++] = '\\';
|
|
tmp[i++] = ' ';
|
|
} else {
|
|
tmp[i++] = *word;
|
|
}
|
|
word++;
|
|
}
|
|
|
|
// add space if not the last word
|
|
if (words != g_list_last (words)) {
|
|
tmp[i++] = ' ';
|
|
}
|
|
if (!pos && !cur) {
|
|
cur = strlen (tmp); /* cur: length of string after inserting pos words */
|
|
} else {
|
|
--pos;
|
|
}
|
|
words = words->next;
|
|
}
|
|
tmp[i] = 0;
|
|
|
|
if (g_list_length(words) == 1) {
|
|
g_signal_emit_by_name (G_OBJECT(object), "ext_handler", NULL);
|
|
}
|
|
|
|
gtk_entry_set_text (GTK_ENTRY(object), tmp);
|
|
gtk_editable_set_position (GTK_EDITABLE(object), cur);
|
|
g_free (tmp);
|
|
return cur;
|
|
}
|
|
|
|
/* Filter callback for scandir */
|
|
static int select_executables_only(const struct dirent* dent)
|
|
{
|
|
int len = strlen(dent->d_name);
|
|
int lenp = prefix ? strlen (prefix) : 0;
|
|
|
|
if (dent->d_name[0] == '.') {
|
|
if (!g_show_dot_files)
|
|
return 0;
|
|
if (dent->d_name[1] == '\0')
|
|
return 0;
|
|
if ((dent->d_name[1] == '.') && (dent->d_name[2] == '\0'))
|
|
return 0;
|
|
}
|
|
if (dent->d_name[len - 1] == '~')
|
|
return 0;
|
|
if (lenp == 0)
|
|
return 1;
|
|
if (lenp > len)
|
|
return 0;
|
|
|
|
if (strncmp(dent->d_name, prefix, lenp) == 0)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Iterates though PATH and list all executables */
|
|
static GList * generate_execs_list (char * pfix)
|
|
{
|
|
// generate_path_list
|
|
if (!path_gc) {
|
|
char *path_cstr = (char*) getenv("PATH");
|
|
path_gc = g_strsplit (path_cstr, ":", -1);
|
|
}
|
|
|
|
GList * execs_gc = NULL;
|
|
if (prefix) g_free (prefix);
|
|
prefix = g_strdup (pfix);
|
|
|
|
gchar ** path_gc_i = path_gc;
|
|
while (*path_gc_i)
|
|
{
|
|
struct dirent **eps;
|
|
int n = scandir (*path_gc_i, &eps, select_executables_only, alphasort);
|
|
if (n >= 0) {
|
|
for (int j = 0; j < n; j++) {
|
|
execs_gc = g_list_prepend (execs_gc, g_strdup (eps[j]->d_name));
|
|
free (eps[j]);
|
|
}
|
|
free (eps);
|
|
}
|
|
path_gc_i++;
|
|
}
|
|
if (execs_gc && execs_gc->next) {
|
|
execs_gc = g_list_reverse (execs_gc);
|
|
}
|
|
if (prefix) {
|
|
g_free (prefix);
|
|
prefix = NULL;
|
|
}
|
|
return (execs_gc);
|
|
}
|
|
|
|
/* list all subdirs in what, return if ok or not */
|
|
static GList * generate_dirlist (const char * path)
|
|
{
|
|
#ifdef DEBUG
|
|
printf ("generate_dirlist\n");
|
|
#endif
|
|
char * str = strdup (path);
|
|
char * filename = strrchr (str, '/');
|
|
GList * dirlist_gc = NULL;
|
|
|
|
char * p = str;
|
|
int slashes = 0;
|
|
while (*p) {
|
|
if (*p == '/') slashes++;
|
|
p++;
|
|
}
|
|
|
|
char * dir;
|
|
if (slashes == 1) {
|
|
dir = "/";
|
|
} else {
|
|
dir = str;
|
|
}
|
|
|
|
*filename = '\0';
|
|
filename++;
|
|
if (prefix) g_free (prefix);
|
|
prefix = g_strdup(filename);
|
|
|
|
struct dirent **eps;
|
|
char * file;
|
|
int n, len;
|
|
|
|
n = scandir (dir, &eps, select_executables_only, alphasort);
|
|
if (n >= 0) {
|
|
for (int j = 0; j < n; j++)
|
|
{
|
|
file = g_strconcat (str, "/", eps[j]->d_name, "/", NULL);
|
|
len = strlen (file);
|
|
file[len-1] = 0;
|
|
struct stat filestatus;
|
|
stat (file, &filestatus);
|
|
if (S_ISDIR (filestatus.st_mode)) {
|
|
file[len-1] = '/';
|
|
}
|
|
dirlist_gc = g_list_prepend (dirlist_gc, file);
|
|
free(eps[j]);
|
|
}
|
|
free(eps);
|
|
}
|
|
if (dirlist_gc && dirlist_gc->next) {
|
|
dirlist_gc = g_list_reverse (dirlist_gc);
|
|
}
|
|
|
|
if (prefix) {
|
|
g_free (prefix);
|
|
prefix = NULL;
|
|
}
|
|
free (str);
|
|
return (dirlist_gc);
|
|
}
|
|
|
|
/* Expand tilde */
|
|
static int parse_tilda (GtkCompletionLine *object)
|
|
{
|
|
const gchar *text = gtk_entry_get_text(GTK_ENTRY(object));
|
|
const gchar *match = g_strstr_len(text, -1, "~");
|
|
if (match) {
|
|
gint cur = match - text;
|
|
if ((cur > 0) && (text[cur - 1] != ' '))
|
|
return 0;
|
|
if ((guint)cur < strlen(text) - 1 && text[cur + 1] != '/') {
|
|
// FIXME: Parse another user's home
|
|
} else {
|
|
gtk_editable_insert_text(GTK_EDITABLE(object),
|
|
g_get_home_dir(), strlen(g_get_home_dir()), &cur);
|
|
gtk_editable_delete_text(GTK_EDITABLE(object), cur, cur + 1);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void complete_from_list (GtkCompletionLine *object, char *cword) /* can be NULL */
|
|
{
|
|
#ifdef DEBUG
|
|
printf ("\ncomplete_from_list\n");
|
|
#endif
|
|
if (on_cursor_changed_handler) {
|
|
g_signal_handler_block (G_OBJECT(object->tree_compl), on_cursor_changed_handler);
|
|
}
|
|
|
|
parse_tilda(object);
|
|
GList *words = NULL, *word_i;
|
|
int pos = get_words (object, &words);
|
|
word_i = g_list_nth (words, pos);
|
|
char * word = NULL;
|
|
|
|
/* Completion list is opened */
|
|
if (object->win_compl != NULL) {
|
|
/* word will point to a dynamycally allocated string */
|
|
gtk_tree_model_get (object->sort_list_compl, &(object->list_compl_it), 0, &word, -1);
|
|
|
|
GtkTreePath *path = gtk_tree_model_get_path(object->sort_list_compl, &(object->list_compl_it));
|
|
gtk_tree_view_set_cursor(GTK_TREE_VIEW(object->tree_compl), path, NULL, FALSE);
|
|
gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(object->tree_compl), path, NULL, TRUE, 0.5, 0.5);
|
|
gtk_tree_path_free(path);
|
|
} else {
|
|
word = cword ? strdup (cword) : NULL;
|
|
}
|
|
|
|
if (word) {
|
|
g_free (word_i->data);
|
|
word_i->data = word;
|
|
}
|
|
int current_pos = set_words (object, words, pos);
|
|
gtk_editable_select_region (GTK_EDITABLE(object), object->pos_in_text, current_pos);
|
|
|
|
g_list_free_full (words, g_free);
|
|
|
|
if (on_cursor_changed_handler) {
|
|
g_signal_handler_unblock (G_OBJECT(object->tree_compl), on_cursor_changed_handler);
|
|
}
|
|
}
|
|
|
|
static void on_cursor_changed(GtkTreeView *tree, gpointer data)
|
|
{
|
|
#ifdef DEBUG
|
|
printf ("on_cusor_changed\n");
|
|
#endif
|
|
GtkCompletionLine *object = GTK_COMPLETION_LINE(data);
|
|
|
|
GtkTreeSelection *selection;
|
|
selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(object->tree_compl));
|
|
gtk_tree_selection_get_selected (selection, &(object->sort_list_compl), &(object->list_compl_it));
|
|
|
|
complete_from_list (object, NULL);
|
|
}
|
|
|
|
static void clear_selection (GtkCompletionLine* cl)
|
|
{
|
|
int pos = gtk_editable_get_position (GTK_EDITABLE (cl));
|
|
gtk_editable_select_region (GTK_EDITABLE(cl), pos, pos);
|
|
}
|
|
|
|
|
|
// called by tab_pressed() only if completion window doesn't exist
|
|
static void complete_line (GtkCompletionLine *object)
|
|
{
|
|
#ifdef DEBUG
|
|
printf ("complete_line\n");
|
|
#endif
|
|
parse_tilda(object);
|
|
|
|
GList * WordList = NULL, * witem = NULL;
|
|
GList * FileList = NULL;
|
|
int pos = get_words (object, &WordList);
|
|
witem = g_list_nth (WordList, pos);
|
|
char * word = (gchar *)(witem->data);
|
|
|
|
g_show_dot_files = object->show_dot_files;
|
|
|
|
/* populate FileList */
|
|
if (word[0] != '/') { /* exec list */
|
|
FileList = generate_execs_list (word);
|
|
} else { /* dirlist */
|
|
FileList = generate_dirlist (word);
|
|
}
|
|
|
|
guint num_items = FileList ? g_list_length (FileList) : 0;
|
|
|
|
if (num_items == 1) { // only 1 item
|
|
complete_from_list (object, (char*)(FileList->data));
|
|
g_signal_emit_by_name(G_OBJECT(object), "unique");
|
|
clear_selection (object);
|
|
g_list_free_full (WordList, g_free);
|
|
g_list_free_full (FileList, g_free);
|
|
return;
|
|
} else if (num_items == 0) {
|
|
g_signal_emit_by_name(G_OBJECT(object), "incomplete");
|
|
g_list_free_full (WordList, g_free);
|
|
g_list_free_full (FileList, g_free);
|
|
return;
|
|
}
|
|
|
|
/*** num_items > 1 ***/
|
|
g_signal_emit_by_name (G_OBJECT(object), "notunique");
|
|
|
|
object->win_compl = gtk_window_new (GTK_WINDOW_POPUP);
|
|
gtk_widget_set_name (object->win_compl, "gmrun_completion_window");
|
|
|
|
GtkWidget * main_win = gtk_widget_get_toplevel (GTK_WIDGET (object));
|
|
gtk_window_set_transient_for (GTK_WINDOW (object->win_compl), GTK_WINDOW (main_win));
|
|
|
|
/* attemp to silence warning: Gtk-WARNING **: Allocating size to Window ...
|
|
https://git.eclipse.org/c/platform/eclipse.platform.swt.git/commit/?id=61a598af4dfda586b27d87537bb2d015bd614ba1
|
|
https://sources.debian.org/src/clutter-gtk/1.8.2-2/clutter-gtk/gtk-clutter-actor.c/?hl=325#L325
|
|
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=867427
|
|
*/
|
|
#if GTK_MAJOR_VERSION == 3 && GTK_MINOR_VERSION >= 20 && GTK_MINOR_VERSION < 24
|
|
// not a proper fix
|
|
GtkRequisition r, r2;
|
|
gtk_widget_get_preferred_size (GTK_WIDGET (object->win_compl), &r, &r2);
|
|
GtkAllocation wal = { 0, 0, r2.width, r.height };
|
|
gtk_widget_size_allocate (GTK_WIDGET (object->win_compl), &wal);
|
|
#endif
|
|
|
|
object->list_compl = gtk_list_store_new (1, G_TYPE_STRING);
|
|
object->sort_list_compl = GTK_TREE_MODEL (object->list_compl);
|
|
object->tree_compl = gtk_tree_view_new_with_model (object->sort_list_compl);
|
|
g_object_unref (object->list_compl);
|
|
|
|
/* fill ListStore before sorting */
|
|
GtkTreeIter iter;
|
|
GList *p = FileList;
|
|
while (p) {
|
|
gtk_list_store_append (object->list_compl, &iter); /* modifies iter */
|
|
gtk_list_store_set (object->list_compl, &iter,
|
|
0, (char*) p->data, -1);
|
|
p = g_list_next (p);
|
|
}
|
|
|
|
/* sort ListStore, column 0 */
|
|
GtkTreeSortable * sorted = GTK_TREE_SORTABLE (object->list_compl);
|
|
gtk_tree_sortable_set_sort_column_id (sorted, 0, GTK_SORT_ASCENDING);
|
|
|
|
GtkTreeViewColumn *col;
|
|
GtkCellRenderer *renderer;
|
|
col = gtk_tree_view_column_new ();
|
|
renderer = gtk_cell_renderer_text_new ();
|
|
gtk_tree_view_column_pack_start (col, renderer, TRUE);
|
|
gtk_tree_view_column_add_attribute (col, renderer, "text", 0);
|
|
gtk_tree_view_append_column (GTK_TREE_VIEW (object->tree_compl), col);
|
|
|
|
GtkTreeSelection *selection;
|
|
selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (object->tree_compl));
|
|
gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE);
|
|
gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (object->tree_compl), FALSE);
|
|
|
|
on_cursor_changed_handler = g_signal_connect (GTK_TREE_VIEW (object->tree_compl),
|
|
"cursor-changed",
|
|
G_CALLBACK (on_cursor_changed), object);
|
|
g_signal_handler_block (G_OBJECT (object->tree_compl),
|
|
on_cursor_changed_handler);
|
|
|
|
GtkWidget *scroll = gtk_scrolled_window_new (NULL, NULL);
|
|
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW(scroll), GTK_SHADOW_OUT);
|
|
gtk_widget_show (scroll);
|
|
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
|
|
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
|
gtk_container_set_border_width (GTK_CONTAINER (object->tree_compl), 2);
|
|
gtk_container_add (GTK_CONTAINER (scroll), object->tree_compl);
|
|
gtk_container_add (GTK_CONTAINER (object->win_compl), scroll);
|
|
|
|
GdkWindow *top = gtk_widget_get_parent_window (GTK_WIDGET (object));
|
|
int x, y;
|
|
gdk_window_get_position(top, &x, &y);
|
|
GtkAllocation al;
|
|
gtk_widget_get_allocation( GTK_WIDGET(object), &al );
|
|
x += al.x;
|
|
y += al.y + al.height;
|
|
|
|
// gtk_widget_popup(object->win_compl, x, y);
|
|
gtk_window_move (GTK_WINDOW (object->win_compl), x, y);
|
|
gtk_widget_show_all (object->win_compl);
|
|
|
|
gtk_tree_view_columns_autosize (GTK_TREE_VIEW (object->tree_compl));
|
|
gtk_widget_get_allocation (object->tree_compl, &al);
|
|
gtk_widget_set_size_request (scroll, al.width + 40, 150);
|
|
|
|
gtk_tree_model_get_iter_first (object->sort_list_compl, &(object->list_compl_it));
|
|
gtk_tree_selection_select_iter (selection, &(object->list_compl_it));
|
|
|
|
g_signal_handler_unblock (G_OBJECT(object->tree_compl),
|
|
on_cursor_changed_handler);
|
|
|
|
// completion has been created, now use the 1st item from TreeView
|
|
object->pos_in_text = gtk_editable_get_position (GTK_EDITABLE (object));
|
|
complete_from_list (object, NULL);
|
|
|
|
g_list_free_full (WordList, g_free);
|
|
g_list_free_full (FileList, g_free);
|
|
return;
|
|
}
|
|
|
|
GtkWidget *
|
|
gtk_completion_line_new()
|
|
{
|
|
return GTK_WIDGET(g_object_new(gtk_completion_line_get_type(), NULL));
|
|
}
|
|
|
|
static void
|
|
up_history(GtkCompletionLine* cl)
|
|
{
|
|
static int pause = 0;
|
|
const char * text_up;
|
|
if (pause == 1) {
|
|
text_up = history_last (cl->hist);
|
|
pause = 0;
|
|
} else {
|
|
text_up = history_prev (cl->hist);
|
|
if (!text_up) { // empty, set a flag, next time we'll get something
|
|
pause = 1;
|
|
text_up = "";
|
|
}
|
|
}
|
|
if (text_up) {
|
|
gtk_entry_set_text (GTK_ENTRY(cl), text_up);
|
|
}
|
|
}
|
|
|
|
static void
|
|
down_history(GtkCompletionLine* cl)
|
|
{
|
|
static int pause = 0;
|
|
const char * text_down;
|
|
if (pause == 1) {
|
|
text_down = history_first (cl->hist);
|
|
pause = 0;
|
|
} else {
|
|
text_down = history_next (cl->hist);
|
|
if (!text_down) { // empty, set a flag, next time we'll get something
|
|
pause = 1;
|
|
text_down = "";
|
|
}
|
|
}
|
|
if (text_down) {
|
|
gtk_entry_set_text (GTK_ENTRY(cl), text_down);
|
|
}
|
|
}
|
|
|
|
static void search_off (GtkCompletionLine* cl)
|
|
{
|
|
cl->hist_search_mode = FALSE;
|
|
g_signal_emit_by_name (G_OBJECT(cl), "search_mode");
|
|
history_unset_current (cl->hist);
|
|
}
|
|
|
|
|
|
static int
|
|
search_history (GtkCompletionLine* cl)
|
|
{ // must only be called if cl->hist_search_mode = TRUE
|
|
/* a key is pressed and added to cl->hist_word */
|
|
searching_history = TRUE;
|
|
if (cl->hist_word[0])
|
|
{
|
|
const char * history_current_item;
|
|
const char * search_str;
|
|
history_current_item = history_search_first_func (cl->hist);
|
|
search_str = cl->hist_word;
|
|
|
|
while (1)
|
|
{
|
|
const char * s;
|
|
s = strstr (history_current_item, search_str);
|
|
if (s) {
|
|
if (strstr (history_current_item, search_str)) {
|
|
gtk_entry_set_text (GTK_ENTRY(cl), history_current_item);
|
|
g_signal_emit_by_name (G_OBJECT(cl), "search_letter");
|
|
return 1;
|
|
}
|
|
}
|
|
history_current_item = history_search_next_func (cl->hist);
|
|
if (history_current_item == NULL) {
|
|
g_signal_emit_by_name (G_OBJECT(cl), "search_not_found");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
g_signal_emit_by_name (G_OBJECT(cl), "search_letter");
|
|
searching_history = FALSE;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static guint tab_pressed(GtkCompletionLine* cl)
|
|
{
|
|
#ifdef DEBUG
|
|
printf ("tab_pressed\n");
|
|
#endif
|
|
if (cl->hist_search_mode == TRUE) {
|
|
search_off(cl);
|
|
}
|
|
if (cl->win_compl) {
|
|
// completion window exists, avoid calling complete_line()
|
|
gboolean valid = gtk_tree_model_iter_next (cl->sort_list_compl, &(cl->list_compl_it));
|
|
if(!valid) {
|
|
gtk_tree_model_get_iter_first (cl->sort_list_compl, &(cl->list_compl_it));
|
|
}
|
|
complete_from_list (cl, NULL);
|
|
} else { // complete_line ()
|
|
complete_line (cl);
|
|
}
|
|
timeout_id = 0;
|
|
return FALSE;
|
|
}
|
|
|
|
static void destroy_completion_window (GtkCompletionLine *cl)
|
|
{
|
|
if (on_cursor_changed_handler) {
|
|
g_signal_handler_block (G_OBJECT (cl->tree_compl), on_cursor_changed_handler);
|
|
}
|
|
gtk_list_store_clear (cl->list_compl);
|
|
gtk_widget_destroy (cl->tree_compl);
|
|
gtk_widget_destroy (cl->win_compl);
|
|
cl->list_compl = NULL;
|
|
cl->tree_compl = NULL;
|
|
cl->win_compl = NULL;
|
|
on_cursor_changed_handler = 0;
|
|
}
|
|
|
|
|
|
static gboolean
|
|
on_scroll(GtkCompletionLine *cl, GdkEventScroll *event, gpointer data)
|
|
{
|
|
// https://developer.gnome.org/gdk2/stable/gdk2-Event-Structures.html#GdkEventScroll
|
|
// https://developer.gnome.org/gdk3/stable/gdk3-Event-Structures.html#GdkEventScroll
|
|
if (event->type != GDK_SCROLL) {
|
|
return FALSE;
|
|
}
|
|
GdkScrollDirection direction;
|
|
direction = event->direction;
|
|
if (direction == GDK_SCROLL_UP) {
|
|
if (cl->win_compl != NULL) {
|
|
gboolean valid;
|
|
#if GTK_CHECK_VERSION(3, 0, 0)
|
|
valid = gtk_tree_model_iter_previous(cl->sort_list_compl, &(cl->list_compl_it));
|
|
#else
|
|
GtkTreePath * path = gtk_tree_model_get_path (cl->sort_list_compl, &(cl->list_compl_it));
|
|
valid = gtk_tree_path_prev (path);
|
|
if (valid) {
|
|
gtk_tree_model_get_iter (cl->sort_list_compl, &(cl->list_compl_it), path);
|
|
}
|
|
gtk_tree_path_free (path);
|
|
#endif
|
|
if(!valid) {
|
|
int rowCount = gtk_tree_model_iter_n_children (cl->sort_list_compl, NULL);
|
|
gtk_tree_model_iter_nth_child(cl->sort_list_compl, &(cl->list_compl_it), NULL, rowCount - 1);
|
|
}
|
|
complete_from_list (cl, NULL);
|
|
} else {
|
|
up_history(cl);
|
|
}
|
|
if (cl->hist_search_mode == TRUE) {
|
|
search_off(cl);
|
|
}
|
|
return TRUE;
|
|
} else if (direction == GDK_SCROLL_DOWN) {
|
|
if (cl->win_compl != NULL) {
|
|
gboolean valid = gtk_tree_model_iter_next(cl->sort_list_compl, &(cl->list_compl_it));
|
|
if(!valid) {
|
|
gtk_tree_model_get_iter_first(cl->sort_list_compl, &(cl->list_compl_it));
|
|
}
|
|
complete_from_list (cl, NULL);
|
|
} else {
|
|
down_history(cl);
|
|
}
|
|
if (cl->hist_search_mode == TRUE) {
|
|
search_off(cl);
|
|
}
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
on_key_press(GtkCompletionLine *cl, GdkEventKey *event, gpointer data)
|
|
{ // https://developer.gnome.org/gtk2/stable/GtkWidget.html#GtkWidget-key-press-event
|
|
int key = event->keyval;
|
|
switch (key)
|
|
{
|
|
case GDK_KEY_Control_R:
|
|
case GDK_KEY_Control_L:
|
|
case GDK_KEY_Shift_R:
|
|
case GDK_KEY_Shift_L:
|
|
case GDK_KEY_Alt_R:
|
|
case GDK_KEY_Alt_L:
|
|
break;
|
|
|
|
case GDK_KEY_Tab:
|
|
if (timeout_id != 0) {
|
|
g_source_remove(timeout_id);
|
|
timeout_id = 0;
|
|
}
|
|
tab_pressed(cl);
|
|
return TRUE; /* stop signal emission */
|
|
|
|
case GDK_KEY_Up:
|
|
if (cl->win_compl != NULL) {
|
|
gboolean valid;
|
|
#if GTK_CHECK_VERSION(3, 0, 0)
|
|
valid = gtk_tree_model_iter_previous(cl->sort_list_compl, &(cl->list_compl_it));
|
|
#else
|
|
GtkTreePath * path = gtk_tree_model_get_path (cl->sort_list_compl, &(cl->list_compl_it));
|
|
valid = gtk_tree_path_prev (path);
|
|
if (valid) {
|
|
gtk_tree_model_get_iter (cl->sort_list_compl, &(cl->list_compl_it), path);
|
|
}
|
|
gtk_tree_path_free (path);
|
|
#endif
|
|
if(!valid) {
|
|
int rowCount = gtk_tree_model_iter_n_children (cl->sort_list_compl, NULL);
|
|
gtk_tree_model_iter_nth_child(cl->sort_list_compl, &(cl->list_compl_it), NULL, rowCount - 1);
|
|
}
|
|
complete_from_list (cl, NULL);
|
|
} else {
|
|
up_history(cl);
|
|
}
|
|
if (cl->hist_search_mode == TRUE) {
|
|
search_off(cl);
|
|
}
|
|
return TRUE; /* stop signal emission */
|
|
|
|
case GDK_KEY_space:
|
|
{
|
|
if (cl->hist_search_mode) {
|
|
search_off (cl);
|
|
}
|
|
if (cl->win_compl != NULL) {
|
|
destroy_completion_window (cl);
|
|
}
|
|
}
|
|
return FALSE;
|
|
|
|
case GDK_KEY_Down:
|
|
if (cl->win_compl != NULL) {
|
|
gboolean valid = gtk_tree_model_iter_next(cl->sort_list_compl, &(cl->list_compl_it));
|
|
if(!valid) {
|
|
gtk_tree_model_get_iter_first(cl->sort_list_compl, &(cl->list_compl_it));
|
|
}
|
|
complete_from_list (cl, NULL);
|
|
} else {
|
|
down_history(cl);
|
|
}
|
|
if (cl->hist_search_mode == TRUE) {
|
|
search_off(cl);
|
|
}
|
|
return TRUE; /* stop signal emission */
|
|
|
|
case GDK_KEY_Return:
|
|
if (cl->win_compl != NULL) {
|
|
destroy_completion_window (cl);
|
|
}
|
|
if (event->state & GDK_CONTROL_MASK) {
|
|
g_signal_emit_by_name(G_OBJECT(cl), "runwithterm");
|
|
} else {
|
|
g_signal_emit_by_name(G_OBJECT(cl), "activate");
|
|
}
|
|
return TRUE; /* stop signal emission */
|
|
|
|
case GDK_KEY_S:
|
|
case GDK_KEY_s:
|
|
case GDK_KEY_R:
|
|
case GDK_KEY_r:
|
|
if (event->state & GDK_CONTROL_MASK) {
|
|
if (searching_history == FALSE) {
|
|
/* set proper funcs for forward/backward search */
|
|
if (key == GDK_KEY_R || key == GDK_KEY_r) { /* reverse - backward */
|
|
history_search_first_func = history_last;
|
|
history_search_next_func = history_prev;
|
|
} else { /* from start - forward */
|
|
history_search_first_func = history_first;
|
|
history_search_next_func = history_next;
|
|
}
|
|
}
|
|
if (cl->hist_search_mode == FALSE) {
|
|
gtk_entry_set_text (GTK_ENTRY (cl), "");
|
|
cl->hist_search_mode = TRUE;
|
|
cl->hist_word[0] = 0;
|
|
cl->hist_word_count = 0;
|
|
g_signal_emit_by_name(G_OBJECT(cl), "search_mode");
|
|
}
|
|
return TRUE; /* stop signal emission */
|
|
} else goto ordinary;
|
|
|
|
case GDK_KEY_BackSpace:
|
|
if (cl->hist_search_mode == TRUE) {
|
|
if (cl->hist_word[0]) {
|
|
cl->hist_word_count--;
|
|
cl->hist_word[cl->hist_word_count] = 0;
|
|
search_history(cl);
|
|
g_signal_emit_by_name(G_OBJECT(cl), "search_letter");
|
|
}
|
|
return TRUE; /* stop signal emission */
|
|
}
|
|
return FALSE;
|
|
|
|
case GDK_KEY_Home:
|
|
case GDK_KEY_End:
|
|
clear_selection(cl);
|
|
goto ordinary;
|
|
|
|
case GDK_KEY_Escape:
|
|
if (cl->hist_search_mode == TRUE) {
|
|
search_off(cl);
|
|
} else if (cl->win_compl != NULL) {
|
|
destroy_completion_window (cl);
|
|
} else {
|
|
// user cancelled
|
|
g_signal_emit_by_name(G_OBJECT(cl), "cancel");
|
|
}
|
|
return TRUE; /* stop signal emission */
|
|
|
|
ordinary:
|
|
default:
|
|
if (cl->win_compl != NULL) {
|
|
destroy_completion_window (cl);
|
|
}
|
|
if (cl->hist_search_mode == TRUE) {
|
|
// https://developer.gnome.org/gdk2/stable/gdk2-Event-Structures.html#GdkEventKey
|
|
if (event->state & GDK_CONTROL_MASK)
|
|
return TRUE; /* stop signal emission */
|
|
if (event->length > 0) {
|
|
// event->string = char: 'c' 'd'
|
|
cl->hist_word[cl->hist_word_count] = event->string[0];
|
|
cl->hist_word_count++;
|
|
if (cl->hist_word_count <= 1000) {
|
|
search_history(cl);
|
|
}
|
|
return TRUE; /* stop signal emission */
|
|
} else
|
|
search_off(cl);
|
|
}
|
|
if (cl->tabtimeout != 0) {
|
|
if (timeout_id != 0) {
|
|
g_source_remove(timeout_id);
|
|
timeout_id = 0;
|
|
}
|
|
if (isprint (*event->string)) {
|
|
timeout_id = g_timeout_add (cl->tabtimeout,
|
|
(GSourceFunc) tab_pressed, cl);
|
|
}
|
|
}
|
|
break;
|
|
} //switch
|
|
return FALSE;
|
|
}
|
|
|