/* * This file is part of YAD. * * YAD is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * YAD 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with YAD. If not, see <http://www.gnu.org/licenses/>. * * Copyright (C) 2008-2019, Victor Ananjevsky <ananasik@gmail.com> */ #include <pango/pango.h> #include "yad.h" static GtkWidget *text_view; static GObject *text_buffer; static GtkTextTag *tag; static GdkCursor *hand, *normal; static gchar *pattern = NULL; static gboolean new_search = TRUE; /* searching */ static void do_search (GtkWidget * e, GtkWidget * w) { static gchar *text = NULL; static guint offset; static GRegex *regex = NULL; GMatchInfo *match = NULL; GtkTextIter begin, end; g_free (pattern); pattern = g_strdup (gtk_entry_get_text (GTK_ENTRY (e))); gtk_widget_destroy (w); gtk_widget_queue_draw (text_view); if (new_search || gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (text_buffer))) { /* get the text */ g_free (text); gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (text_buffer), &begin, &end); text = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (text_buffer), &begin, &end, FALSE); offset = 0; /* compile new regex */ if (regex) g_regex_unref (regex); regex = g_regex_new (pattern, G_REGEX_EXTENDED | G_REGEX_OPTIMIZE, G_REGEX_MATCH_NOTEMPTY, NULL); new_search = FALSE; } /* search and select if found */ if (g_regex_match (regex, text + offset, G_REGEX_MATCH_NOTEMPTY, &match)) { gint sp, ep, spos, epos; g_match_info_fetch_pos (match, 0, &sp, &ep); /* positions are in bytes, not character, so here we must normalize it */ spos = g_utf8_pointer_to_offset (text, text + sp + offset); epos = g_utf8_pointer_to_offset (text, text + ep + offset); gtk_text_buffer_get_iter_at_offset (GTK_TEXT_BUFFER (text_buffer), &begin, spos); gtk_text_buffer_get_iter_at_offset (GTK_TEXT_BUFFER (text_buffer), &end, epos); gtk_text_buffer_select_range (GTK_TEXT_BUFFER (text_buffer), &begin, &end); gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (text_view), &begin, 0, FALSE, 0, 0); offset += epos; g_match_info_free (match); match = NULL; } else new_search = TRUE; } static gboolean search_key_cb (GtkWidget * w, GdkEventKey * key, GtkWidget * win) { if (key->keyval == GDK_KEY_Escape) { gtk_widget_destroy (win); return TRUE; } return FALSE; } static void search_changed (GtkWidget * w, gpointer d) { new_search = TRUE; } static void show_search () { GtkWidget *w, *f, *e; GdkEvent *fev; w = gtk_window_new (GTK_WINDOW_POPUP); gtk_window_set_transient_for (GTK_WINDOW (w), GTK_WINDOW (gtk_widget_get_toplevel (text_view))); gtk_window_set_position (GTK_WINDOW (w), GTK_WIN_POS_CENTER_ON_PARENT); /* next two lines needs for get focus to search window */ gtk_window_set_type_hint (GTK_WINDOW (w), GDK_WINDOW_TYPE_HINT_UTILITY); gtk_window_set_modal (GTK_WINDOW (w), TRUE); g_signal_connect (G_OBJECT (w), "key-press-event", G_CALLBACK (search_key_cb), w); f = gtk_frame_new (NULL); gtk_frame_set_shadow_type (GTK_FRAME (f), GTK_SHADOW_ETCHED_IN); gtk_container_set_border_width (GTK_CONTAINER (f), 1); gtk_container_add (GTK_CONTAINER (w), f); e = gtk_entry_new (); if (pattern) gtk_entry_set_text (GTK_ENTRY (e), pattern); gtk_container_add (GTK_CONTAINER (f), e); g_signal_connect (G_OBJECT (e), "activate", G_CALLBACK (do_search), w); g_signal_connect (G_OBJECT (e), "changed", G_CALLBACK (search_changed), NULL); g_signal_connect (G_OBJECT (e), "key-press-event", G_CALLBACK (search_key_cb), w); gtk_widget_show_all (w); /* send focus event to search entry (so complex due to popup window) */ fev = gdk_event_new (GDK_FOCUS_CHANGE); fev->focus_change.type = GDK_FOCUS_CHANGE; fev->focus_change.in = TRUE; fev->focus_change.window = gtk_widget_get_window (e); if (fev->focus_change.window != NULL) g_object_ref (fev->focus_change.window); gtk_widget_send_focus_change (e, fev); gdk_event_free (fev); } static gboolean key_press_cb (GtkWidget * w, GdkEventKey * key, gpointer data) { if ((key->state & GDK_CONTROL_MASK) && (key->keyval == GDK_KEY_S || key->keyval == GDK_KEY_s)) { show_search (); return TRUE; } return FALSE; } static gboolean tag_event_cb (GtkTextTag * tag, GObject * obj, GdkEvent * ev, GtkTextIter * iter, gpointer d) { GtkTextIter start = *iter; GtkTextIter end = *iter; gchar *url, *cmdline; if (ev->type == GDK_BUTTON_PRESS) { GdkEventButton *bev = (GdkEventButton *) ev; if (bev->button == 1) { gtk_text_iter_backward_to_tag_toggle (&start, tag); gtk_text_iter_forward_to_tag_toggle (&end, tag); url = gtk_text_iter_get_text (&start, &end); #ifndef STANDALONE cmdline = g_strdup_printf (g_settings_get_string (settings, "open-command"), url); #else cmdline = g_strdup_printf (OPEN_CMD, url); #endif g_free (url); run_command_async (cmdline); g_free (cmdline); return TRUE; } } return FALSE; } static gboolean hovering_over_link = FALSE; static gboolean motion_cb (GtkWidget * w, GdkEventMotion * ev, gpointer d) { gint x, y; GSList *tags = NULL, *tagp = NULL; GtkTextIter iter; gboolean hovering = FALSE; gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (w), GTK_TEXT_WINDOW_WIDGET, ev->x, ev->y, &x, &y); gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (w), &iter, x, y); tags = gtk_text_iter_get_tags (&iter); for (tagp = tags; tagp != NULL; tagp = tagp->next) { GtkTextTag *tag = tagp->data; gint link = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tag), "is_link")); if (link) { hovering = TRUE; break; } } if (hovering != hovering_over_link) { hovering_over_link = hovering; if (hovering_over_link) gdk_window_set_cursor (gtk_text_view_get_window (GTK_TEXT_VIEW (w), GTK_TEXT_WINDOW_TEXT), hand); else gdk_window_set_cursor (gtk_text_view_get_window (GTK_TEXT_VIEW (w), GTK_TEXT_WINDOW_TEXT), normal); } if (tags) g_slist_free (tags); return FALSE; } static void linkify_cb (GtkTextBuffer * buf, GRegex * regex) { gchar *text; GtkTextIter start, end; GMatchInfo *match; gtk_text_buffer_get_bounds (buf, &start, &end); text = gtk_text_buffer_get_text (buf, &start, &end, FALSE); gtk_text_buffer_remove_all_tags (buf, &start, &end); if (g_regex_match (regex, text, G_REGEX_MATCH_NOTEMPTY, &match)) { do { gint sp, ep, spos, epos; g_match_info_fetch_pos (match, 0, &sp, &ep); /* positions are in bytes, not character, so here we must normalize it */ spos = g_utf8_pointer_to_offset (text, text + sp); epos = g_utf8_pointer_to_offset (text, text + ep); gtk_text_buffer_get_iter_at_offset (buf, &start, spos); gtk_text_buffer_get_iter_at_offset (buf, &end, epos); gtk_text_buffer_apply_tag (buf, tag, &start, &end); } while (g_match_info_next (match, NULL)); } g_match_info_free (match); g_free (text); } static gboolean handle_stdin (GIOChannel * channel, GIOCondition condition, gpointer data) { if ((condition & G_IO_IN) || (condition & (G_IO_IN | G_IO_HUP))) { GString *string; GError *err = NULL; gint status; string = g_string_new (NULL); while (channel->is_readable != TRUE); do { status = g_io_channel_read_line_string (channel, string, NULL, &err); while (gtk_events_pending ()) gtk_main_iteration (); } while (status == G_IO_STATUS_AGAIN); if (status != G_IO_STATUS_NORMAL) { if (err) { g_printerr ("yad_text_handle_stdin(): %s\n", err->message); g_error_free (err); err = NULL; } /* stop handling */ g_io_channel_shutdown (channel, TRUE, NULL); return FALSE; } if (string->str[0] == '\014') { GtkTextIter start, end; /* clear text if ^L received */ gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (text_buffer), &start); gtk_text_buffer_get_end_iter (GTK_TEXT_BUFFER (text_buffer), &end); gtk_text_buffer_delete (GTK_TEXT_BUFFER (text_buffer), &start, &end); } else if (string->len > 0) { GtkTextIter end; gtk_text_buffer_get_end_iter (GTK_TEXT_BUFFER (text_buffer), &end); if (!g_utf8_validate (string->str, string->len, NULL)) { gchar *utftext = g_convert_with_fallback (string->str, string->len, "UTF-8", "ISO-8859-1", NULL, NULL, NULL, NULL); if (options.text_data.formatted && !options.common_data.editable) gtk_text_buffer_insert_markup (GTK_TEXT_BUFFER (text_buffer), &end, utftext, -1); else gtk_text_buffer_insert (GTK_TEXT_BUFFER (text_buffer), &end, utftext, -1); g_free (utftext); } else { if (options.text_data.formatted && !options.common_data.editable) gtk_text_buffer_insert_markup (GTK_TEXT_BUFFER (text_buffer), &end, string->str, string->len); else gtk_text_buffer_insert (GTK_TEXT_BUFFER (text_buffer), &end, string->str, string->len); } if (options.common_data.tail) { while (gtk_events_pending ()) gtk_main_iteration (); gtk_text_buffer_get_end_iter (GTK_TEXT_BUFFER (text_buffer), &end); gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (text_view), &end, 0, FALSE, 0, 0); } } g_string_free (string, TRUE); } #ifdef HAVE_SOURCEVIEW if (options.source_data.lang) { GtkSourceLanguage *lang = gtk_source_language_manager_get_language (gtk_source_language_manager_get_default (), options.source_data.lang); gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (text_buffer), lang); } #endif return TRUE; } static void fill_buffer_from_file () { GtkTextIter iter; #ifdef HAVE_SOURCEVIEW GtkSourceLanguage *lang; #endif gchar *buf; gsize len; GError *err = NULL; if (options.common_data.uri == NULL) return; if (!g_file_get_contents (options.common_data.uri, &buf, &len, &err)) { g_printerr (_("Cannot open file '%s': %s\n"), options.common_data.uri, err->message); return; } if (len <= 0) return; gtk_text_buffer_get_iter_at_offset (GTK_TEXT_BUFFER (text_buffer), &iter, 0); if (!g_utf8_validate (buf, -1, NULL)) { gchar *utftext = g_convert_with_fallback (buf, -1, "UTF-8", "ISO-8859-1", NULL, NULL, NULL, NULL); if (options.text_data.formatted && !options.common_data.editable) gtk_text_buffer_insert_markup (GTK_TEXT_BUFFER (text_buffer), &iter, utftext, -1); else gtk_text_buffer_insert (GTK_TEXT_BUFFER (text_buffer), &iter, utftext, -1); g_free (utftext); } else { if (options.text_data.formatted && !options.common_data.editable) gtk_text_buffer_insert_markup (GTK_TEXT_BUFFER (text_buffer), &iter, buf, -1); else gtk_text_buffer_insert (GTK_TEXT_BUFFER (text_buffer), &iter, buf, -1); } g_free (buf); #ifdef HAVE_SOURCEVIEW if (options.source_data.lang) lang = gtk_source_language_manager_get_language (gtk_source_language_manager_get_default (), options.source_data.lang); else lang = gtk_source_language_manager_guess_language (gtk_source_language_manager_get_default (), options.common_data.uri, NULL); gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (text_buffer), lang); #endif } static void fill_buffer_from_stdin () { GIOChannel *channel; channel = g_io_channel_unix_new (0); g_io_channel_set_encoding (channel, NULL, NULL); g_io_channel_set_flags (channel, G_IO_FLAG_NONBLOCK, NULL); g_io_add_watch (channel, G_IO_IN | G_IO_HUP, handle_stdin, NULL); } GtkWidget * text_create_widget (GtkWidget * dlg) { GtkWidget *w, *tv; w = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (w), GTK_SHADOW_ETCHED_IN); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (w), options.hscroll_policy, options.vscroll_policy); #ifdef HAVE_SOURCEVIEW text_buffer = (GObject *) gtk_source_buffer_new (NULL); tv = text_view = gtk_source_view_new_with_buffer (GTK_SOURCE_BUFFER (text_buffer)); #else text_buffer = (GObject *) gtk_text_buffer_new (NULL); tv = text_view = gtk_text_view_new_with_buffer (GTK_TEXT_BUFFER (text_buffer)); #endif gtk_widget_set_name (text_view, "yad-text-widget"); gtk_text_view_set_justification (GTK_TEXT_VIEW (text_view), options.text_data.justify); gtk_text_view_set_left_margin (GTK_TEXT_VIEW (text_view), options.text_data.margins); gtk_text_view_set_right_margin (GTK_TEXT_VIEW (text_view), options.text_data.margins); gtk_text_view_set_monospace (GTK_TEXT_VIEW (text_view), TRUE); gtk_text_view_set_editable (GTK_TEXT_VIEW (text_view), options.common_data.editable); if (!options.common_data.editable && options.text_data.hide_cursor) gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (text_view), FALSE); if (options.text_data.wrap) gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (text_view), GTK_WRAP_WORD_CHAR); if (options.common_data.font || options.text_data.fore || options.text_data.back) { GtkCssProvider *provider; GtkStyleContext *context; GString *css; css = g_string_new ("textview, textview text {\n"); if (options.common_data.font) g_string_append_printf (css, " font: %s;\n", options.common_data.font); if (options.text_data.fore) g_string_append_printf (css, " color: %s;\n", options.text_data.fore); if (options.text_data.back) g_string_append_printf (css, " background-color: %s;\n", options.text_data.back); g_string_append (css, "}\n"); provider = gtk_css_provider_new (); gtk_css_provider_load_from_data (provider, css->str, -1, NULL); context = gtk_widget_get_style_context (text_view); gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_string_free (css, TRUE); } #ifdef HAVE_SOURCEVIEW if (options.source_data.theme) { GtkSourceStyleScheme *scheme = NULL; GtkSourceStyleSchemeManager *mgr; const gchar **ids; mgr = gtk_source_style_scheme_manager_get_default (); ids = (const gchar **) gtk_source_style_scheme_manager_get_scheme_ids (mgr); if (ids) { gint i; gboolean found = FALSE; for (i = 0; ids[i]; i++) { const gchar *name; scheme = gtk_source_style_scheme_manager_get_scheme (mgr, ids[i]); name = gtk_source_style_scheme_get_name (scheme); if (strcmp (name, options.source_data.theme) == 0) { found = TRUE; break; } } if (!found) scheme = NULL; } if (scheme) gtk_source_buffer_set_style_scheme (GTK_SOURCE_BUFFER (text_buffer), scheme); else g_printerr (_("Theme %s not found\n"), options.source_data.theme); } #endif #ifdef HAVE_SPELL if (options.common_data.enable_spell) { GspellTextView *spell_view = gspell_text_view_get_from_gtk_text_view (GTK_TEXT_VIEW (text_view)); gspell_text_view_basic_setup (spell_view);; } #endif /* Add submit on ctrl+enter */ g_signal_connect (text_view, "key-press-event", G_CALLBACK (key_press_cb), dlg); /* Initialize linkifying */ if (options.text_data.uri) { GRegex *regex; regex = g_regex_new (YAD_URL_REGEX, G_REGEX_CASELESS | G_REGEX_OPTIMIZE | G_REGEX_EXTENDED, G_REGEX_MATCH_NOTEMPTY, NULL); /* Create text tag for URI */ tag = gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (text_buffer), NULL, "foreground", options.text_data.uri_color, "underline", PANGO_UNDERLINE_SINGLE, NULL); g_object_set_data (G_OBJECT (tag), "is_link", GINT_TO_POINTER (1)); g_signal_connect (G_OBJECT (tag), "event", G_CALLBACK (tag_event_cb), NULL); /* Create cursors */ hand = gdk_cursor_new_for_display (gdk_display_get_default (), GDK_HAND2); normal = gdk_cursor_new_for_display (gdk_display_get_default (), GDK_XTERM); g_signal_connect (G_OBJECT (text_view), "motion-notify-event", G_CALLBACK (motion_cb), NULL); g_signal_connect_after (G_OBJECT (text_buffer), "changed", G_CALLBACK (linkify_cb), regex); } gtk_container_add (GTK_CONTAINER (w), tv); if (options.common_data.uri) fill_buffer_from_file (); if (options.common_data.listen || options.common_data.uri == NULL) fill_buffer_from_stdin (); else { /* place cursor at start of file */ GtkTextIter iter; gtk_text_buffer_get_iter_at_line (GTK_TEXT_BUFFER (text_buffer), &iter, 0); gtk_text_buffer_place_cursor (GTK_TEXT_BUFFER (text_buffer), &iter); } return w; } void text_print_result (void) { GtkTextIter start, end; gchar *text; if (!options.common_data.editable) return; gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (text_buffer), &start, &end); text = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (text_buffer), &start, &end, 0); g_print ("%s", text); g_free (text); }