/* ide-editor-page.c * * Copyright 2017-2019 Christian Hergert * * This program 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. * * This program 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 this program. If not, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "ide-editor-page" #include "config.h" #include #include #include #include #include "ide-editor-page.h" #include "ide-editor-page-addin.h" #include "ide-editor-private.h" #include "ide-line-change-gutter-renderer.h" #define AUTO_HIDE_TIMEOUT_SECONDS 5 enum { PROP_0, PROP_AUTO_HIDE_MAP, PROP_BUFFER, PROP_BUFFER_FILE, PROP_SEARCH, PROP_SHOW_MAP, PROP_VIEW, N_PROPS }; static void ide_editor_page_update_reveal_timer (IdeEditorPage *self); G_DEFINE_FINAL_TYPE (IdeEditorPage, ide_editor_page, IDE_TYPE_PAGE) DZL_DEFINE_COUNTER (instances, "Editor", "N Views", "Number of editor views"); static GParamSpec *properties [N_PROPS]; static FcConfig *localFontConfig; static void ide_editor_page_load_fonts (IdeEditorPage *self) { PangoFontMap *font_map; PangoFontDescription *font_desc; if (g_once_init_enter (&localFontConfig)) { const gchar *font_path = PACKAGE_DATADIR "/gnome-builder/fonts/BuilderBlocks.ttf"; FcConfig *config = FcInitLoadConfigAndFonts (); if (g_getenv ("GB_IN_TREE_FONTS") != NULL) font_path = "data/fonts/BuilderBlocks.ttf"; if (!g_file_test (font_path, G_FILE_TEST_IS_REGULAR)) g_warning ("Failed to locate \"%s\"", font_path); FcConfigAppFontAddFile (config, (const FcChar8 *)font_path); g_once_init_leave (&localFontConfig, config); } font_map = pango_cairo_font_map_new_for_font_type (CAIRO_FONT_TYPE_FT); pango_fc_font_map_set_config (PANGO_FC_FONT_MAP (font_map), localFontConfig); gtk_widget_set_font_map (GTK_WIDGET (self->map), font_map); font_desc = pango_font_description_from_string ("BuilderBlocks"); pango_font_description_set_absolute_size (font_desc, (96.0/72.0) * 2 * PANGO_SCALE); g_assert (localFontConfig != NULL); g_assert (font_map != NULL); g_assert (font_desc != NULL); g_object_set (self->map, "font-desc", font_desc, NULL); pango_font_description_free (font_desc); g_object_unref (font_map); } static void ide_editor_page_update_icon (IdeEditorPage *self) { g_autofree gchar *name = NULL; g_autofree gchar *content_type = NULL; g_autofree gchar *sniff = NULL; g_autoptr(GIcon) icon = NULL; GtkTextIter begin, end; GFile *file; g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_BUFFER (self->buffer)); /* Get first 1024 bytes to help determine content type */ gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end); if (gtk_text_iter_get_offset (&end) > 1024) gtk_text_iter_set_offset (&end, 1024); sniff = gtk_text_iter_get_slice (&begin, &end); /* Now get basename for content type */ file = ide_buffer_get_file (self->buffer); name = g_file_get_basename (file); /* Guess content type */ content_type = g_content_type_guess (name, (const guchar *)sniff, strlen (sniff), NULL); /* Update icon to match guess */ icon = ide_g_content_type_get_symbolic_icon (content_type, name); ide_page_set_icon (IDE_PAGE (self), icon); } static void ide_editor_page_buffer_notify_failed (IdeEditorPage *self, GParamSpec *pspec, IdeBuffer *buffer) { gboolean failed; g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_BUFFER (buffer)); failed = ide_buffer_get_failed (buffer); ide_page_set_failed (IDE_PAGE (self), failed); } static void ide_editor_page_stop_search (IdeEditorPage *self, IdeEditorSearchBar *search_bar) { g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_EDITOR_SEARCH_BAR (search_bar)); gtk_revealer_set_reveal_child (self->search_revealer, FALSE); gtk_widget_grab_focus (GTK_WIDGET (self->source_view)); } static void ide_editor_page_notify_child_revealed (IdeEditorPage *self, GParamSpec *pspec, GtkRevealer *revealer) { g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (GTK_IS_REVEALER (revealer)); if (gtk_revealer_get_child_revealed (revealer)) { GtkWidget *toplevel = gtk_widget_get_ancestor (GTK_WIDGET (revealer), GTK_TYPE_WINDOW); GtkWidget *focus = gtk_window_get_focus (GTK_WINDOW (toplevel)); /* Only focus the search bar if it doesn't already have focus, * as it can reselect the search text. */ if (focus == NULL || !gtk_widget_is_ancestor (focus, GTK_WIDGET (revealer))) gtk_widget_grab_focus (GTK_WIDGET (self->search_bar)); } } static gboolean ide_editor_page_focus_in_event (IdeEditorPage *self, GdkEventFocus *focus, IdeSourceView *source_view) { g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_SOURCE_VIEW (source_view)); ide_page_mark_used (IDE_PAGE (self)); return GDK_EVENT_PROPAGATE; } static void ide_editor_page_buffer_loaded (IdeEditorPage *self, IdeBuffer *buffer) { g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_BUFFER (buffer)); ide_editor_page_update_icon (self); /* Scroll to the insertion location once the buffer * has loaded. This is useful if it is not onscreen. */ ide_source_view_scroll_to_insert (self->source_view); } static void ide_editor_page_buffer_modified_changed (IdeEditorPage *self, IdeBuffer *buffer) { gboolean modified = FALSE; g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_BUFFER (buffer)); if (!ide_buffer_get_loading (buffer)) modified = gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (buffer)); ide_page_set_modified (IDE_PAGE (self), modified); } static void ide_editor_page_buffer_notify_language_cb (IdeExtensionSetAdapter *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { const gchar *language_id = user_data; g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_EDITOR_PAGE_ADDIN (exten)); ide_editor_page_addin_language_changed (IDE_EDITOR_PAGE_ADDIN (exten), language_id); } static void ide_editor_page_buffer_notify_language (IdeEditorPage *self, GParamSpec *pspec, IdeBuffer *buffer) { const gchar *lang_id = NULL; g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_BUFFER (buffer)); if (self->addins == NULL) return; lang_id = ide_buffer_get_language_id (buffer); /* Update extensions that change based on language */ ide_extension_set_adapter_set_value (self->addins, lang_id); ide_extension_set_adapter_foreach (self->addins, ide_editor_page_buffer_notify_language_cb, (gpointer)lang_id); ide_editor_page_update_icon (self); } static void ide_editor_page_buffer_notify_style_scheme (IdeEditorPage *self, GParamSpec *pspec, IdeBuffer *buffer) { g_autofree gchar *background = NULL; g_autofree gchar *foreground = NULL; GtkSourceStyleScheme *scheme; GtkSourceStyle *style; gboolean background_set = FALSE; gboolean foreground_set = FALSE; GdkRGBA rgba; g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_BUFFER (buffer)); if (NULL == (scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer))) || NULL == (style = gtk_source_style_scheme_get_style (scheme, "text"))) goto unset_primary_color; g_object_get (style, "background-set", &background_set, "background", &background, "foreground-set", &foreground_set, "foreground", &foreground, NULL); if (!background_set || background == NULL || !gdk_rgba_parse (&rgba, background)) goto unset_primary_color; if (background_set && background != NULL && gdk_rgba_parse (&rgba, background)) ide_page_set_primary_color_bg (IDE_PAGE (self), &rgba); else goto unset_primary_color; if (foreground_set && foreground != NULL && gdk_rgba_parse (&rgba, foreground)) ide_page_set_primary_color_fg (IDE_PAGE (self), &rgba); else ide_page_set_primary_color_fg (IDE_PAGE (self), NULL); return; unset_primary_color: ide_page_set_primary_color_bg (IDE_PAGE (self), NULL); ide_page_set_primary_color_fg (IDE_PAGE (self), NULL); } static void ide_editor_page__buffer_notify_changed_on_volume (IdeEditorPage *self, GParamSpec *pspec, IdeBuffer *buffer) { g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_BUFFER (buffer)); gtk_revealer_set_reveal_child (self->modified_revealer, ide_buffer_get_changed_on_volume (buffer)); } static void ide_editor_page_hide_reload_bar (IdeEditorPage *self, GtkWidget *button) { g_assert (IDE_IS_EDITOR_PAGE (self)); gtk_revealer_set_reveal_child (self->modified_revealer, FALSE); } static gboolean ide_editor_page_source_view_event (IdeEditorPage *self, GdkEvent *event, IdeSourceView *source_view) { g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (event != NULL); g_assert (IDE_IS_SOURCE_VIEW (source_view) || GTK_SOURCE_IS_MAP (source_view)); if (self->auto_hide_map) { ide_editor_page_update_reveal_timer (self); gtk_revealer_set_reveal_child (self->map_revealer, TRUE); } return GDK_EVENT_PROPAGATE; } static void ide_editor_page_bind_signals (IdeEditorPage *self, IdeBuffer *buffer, DzlSignalGroup *buffer_signals) { g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_BUFFER (buffer)); g_assert (DZL_IS_SIGNAL_GROUP (buffer_signals)); ide_editor_page_buffer_modified_changed (self, buffer); ide_editor_page_buffer_notify_language (self, NULL, buffer); ide_editor_page_buffer_notify_style_scheme (self, NULL, buffer); ide_editor_page_buffer_notify_failed (self, NULL, buffer); } static void ide_editor_page_set_buffer (IdeEditorPage *self, IdeBuffer *buffer) { g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (!buffer || IDE_IS_BUFFER (buffer)); if (g_set_object (&self->buffer, buffer)) { dzl_signal_group_set_target (self->buffer_signals, buffer); dzl_binding_group_set_source (self->buffer_bindings, buffer); gtk_text_view_set_buffer (GTK_TEXT_VIEW (self->source_view), GTK_TEXT_BUFFER (buffer)); gtk_drag_dest_unset (GTK_WIDGET (self->source_view)); ide_editor_page_update_icon (self); } } static IdePage * ide_editor_page_create_split (IdePage *view) { IdeEditorPage *self = (IdeEditorPage *)view; g_assert (IDE_IS_EDITOR_PAGE (self)); return g_object_new (IDE_TYPE_EDITOR_PAGE, "buffer", self->buffer, "visible", TRUE, NULL); } static void ide_editor_page_notify_frame_set (IdeExtensionSetAdapter *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeFrame *frame = user_data; IdeEditorPageAddin *addin = (IdeEditorPageAddin *)exten; g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin)); g_assert (IDE_IS_FRAME (frame)); ide_editor_page_addin_frame_set (addin, frame); } static void ide_editor_page_addin_added (IdeExtensionSetAdapter *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeEditorPage *self = user_data; IdeEditorPageAddin *addin = (IdeEditorPageAddin *)exten; g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin)); g_assert (IDE_IS_EDITOR_PAGE (self)); ide_editor_page_addin_load (addin, self); /* * Notify of the current frame, but refetch the frame pointer just * to be sure we aren't re-using an old pointer in case we're racing * with a finalizer. */ if (self->last_frame_ptr != NULL) { GtkWidget *frame = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME); if (frame != NULL) ide_editor_page_addin_frame_set (addin, IDE_FRAME (frame)); } } static void ide_editor_page_addin_removed (IdeExtensionSetAdapter *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeEditorPage *self = user_data; IdeEditorPageAddin *addin = (IdeEditorPageAddin *)exten; g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin)); g_assert (IDE_IS_EDITOR_PAGE (self)); ide_editor_page_addin_unload (addin, self); } static void ide_editor_page_hierarchy_changed (GtkWidget *widget, GtkWidget *old_toplevel) { IdeEditorPage *self = (IdeEditorPage *)widget; IdeFrame *frame; IdeContext *context; g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel)); /* * We don't need to chain up today, but if IdePage starts * using the hierarchy_changed signal to handle anything, we want * to make sure we aren't surprised. */ if (GTK_WIDGET_CLASS (ide_editor_page_parent_class)->hierarchy_changed) GTK_WIDGET_CLASS (ide_editor_page_parent_class)->hierarchy_changed (widget, old_toplevel); context = ide_widget_get_context (GTK_WIDGET (self)); frame = (IdeFrame *)gtk_widget_get_ancestor (widget, IDE_TYPE_FRAME); /* * We don't want to create addins until the widget has been placed into * the widget tree. That way the addins can get access to the context * or other useful details. */ if (context != NULL && self->addins == NULL) { self->addins = ide_extension_set_adapter_new (IDE_OBJECT (context), peas_engine_get_default (), IDE_TYPE_EDITOR_PAGE_ADDIN, "Editor-Page-Languages", ide_editor_page_get_language_id (self)); g_signal_connect (self->addins, "extension-added", G_CALLBACK (ide_editor_page_addin_added), self); g_signal_connect (self->addins, "extension-removed", G_CALLBACK (ide_editor_page_addin_removed), self); ide_extension_set_adapter_foreach (self->addins, ide_editor_page_addin_added, self); } /* * If we have been moved into a new frame, notify the addins of the * hierarchy change. */ if (frame != NULL && frame != self->last_frame_ptr && self->addins != NULL) { self->last_frame_ptr = frame; ide_extension_set_adapter_foreach (self->addins, ide_editor_page_notify_frame_set, frame); } } static void ide_editor_page_update_map (IdeEditorPage *self) { GtkWidget *parent; g_assert (IDE_IS_EDITOR_PAGE (self)); parent = gtk_widget_get_parent (GTK_WIDGET (self->map)); g_object_ref (self->map); gtk_container_remove (GTK_CONTAINER (parent), GTK_WIDGET (self->map)); if (self->auto_hide_map) gtk_container_add (GTK_CONTAINER (self->map_revealer), GTK_WIDGET (self->map)); else gtk_container_add (GTK_CONTAINER (self->scroller_box), GTK_WIDGET (self->map)); gtk_widget_set_visible (GTK_WIDGET (self->map_revealer), self->show_map && self->auto_hide_map); gtk_widget_set_visible (GTK_WIDGET (self->map), self->show_map); gtk_revealer_set_reveal_child (self->map_revealer, self->show_map); ide_editor_page_update_reveal_timer (self); g_object_unref (self->map); } static void ide_editor_page_buffer_notify_file (IdeEditorPage *self, GParamSpec *pspec, gpointer user_data) { g_assert (IDE_IS_EDITOR_PAGE (self)); } static void search_revealer_notify_reveal_child (IdeEditorPage *self, GParamSpec *pspec, GtkRevealer *revealer) { IdeCompletion *completion; g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); g_return_if_fail (pspec != NULL); g_return_if_fail (GTK_IS_REVEALER (revealer)); completion = ide_source_view_get_completion (IDE_SOURCE_VIEW (self->source_view)); if (!gtk_revealer_get_reveal_child (revealer)) { ide_editor_search_end_interactive (self->search); /* Restore completion that we blocked below. */ ide_completion_unblock_interactive (completion); } else { ide_editor_search_begin_interactive (self->search); /* * Block the completion while the search bar is set. It only * slows things down like replace functionality. We'll * restore it above when we clear state. */ ide_completion_block_interactive (completion); } } static void ide_editor_page_focus_location (IdeEditorPage *self, IdeLocation *location, IdeSourceView *source_view) { GtkWidget *editor; g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (location != NULL); g_assert (IDE_IS_SOURCE_VIEW (source_view)); editor = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_EDITOR_SURFACE); ide_editor_surface_focus_location (IDE_EDITOR_SURFACE (editor), location); } static void ide_editor_page_clear_search (IdeEditorPage *self, IdeSourceView *view) { g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_EDITOR_SEARCH (self->search)); g_assert (IDE_IS_SOURCE_VIEW (view)); ide_editor_search_set_search_text (self->search, NULL); ide_editor_search_set_visible (self->search, FALSE); gtk_revealer_set_reveal_child (self->search_revealer, FALSE); } static void ide_editor_page_move_search (IdeEditorPage *self, GtkDirectionType dir, gboolean extend_selection, gboolean select_match, gboolean exclusive, gboolean apply_count, gboolean at_word_boundaries, IdeSourceView *view) { IdeEditorSearchSelect sel = 0; g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_EDITOR_SEARCH (self->search)); g_assert (IDE_IS_SOURCE_VIEW (view)); if (extend_selection && select_match) sel = IDE_EDITOR_SEARCH_SELECT_WITH_RESULT; else if (extend_selection) sel = IDE_EDITOR_SEARCH_SELECT_TO_RESULT; ide_editor_search_set_extend_selection (self->search, sel); ide_editor_search_set_visible (self->search, TRUE); if (apply_count) { ide_editor_search_set_repeat (self->search, ide_source_view_get_count (view)); g_signal_emit_by_name (view, "clear-count"); } ide_editor_search_set_at_word_boundaries (self->search, at_word_boundaries); switch (dir) { case GTK_DIR_DOWN: case GTK_DIR_RIGHT: ide_editor_search_set_reverse (self->search, FALSE); ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT); break; case GTK_DIR_TAB_FORWARD: if (extend_selection) ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_FORWARD); else ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT); break; case GTK_DIR_UP: case GTK_DIR_LEFT: ide_editor_search_set_reverse (self->search, TRUE); ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT); break; case GTK_DIR_TAB_BACKWARD: if (extend_selection) ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_BACKWARD); else ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_PREVIOUS); break; default: break; } } static void ide_editor_page_set_search_text (IdeEditorPage *self, const gchar *search_text, gboolean from_selection, IdeSourceView *view) { g_autofree gchar *freeme = NULL; GtkTextIter begin; GtkTextIter end; g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_EDITOR_SEARCH (self->search)); g_assert (search_text != NULL || from_selection); g_assert (IDE_IS_SOURCE_VIEW (view)); /* Use interactive mode if we're copying from the clipboard, because that * is usually going to be followed by focusing the search box and we want * to make sure the occurrance count is updated. */ if (from_selection) ide_editor_search_begin_interactive (self->search); if (from_selection) { if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end)) search_text = freeme = gtk_text_iter_get_slice (&begin, &end); } ide_editor_search_set_search_text (self->search, search_text); ide_editor_search_set_regex_enabled (self->search, FALSE); if (from_selection) ide_editor_search_end_interactive (self->search); } static void ide_editor_page_constructed (GObject *object) { IdeEditorPage *self = (IdeEditorPage *)object; GtkSourceGutterRenderer *renderer; GtkSourceGutter *gutter; g_assert (IDE_IS_EDITOR_PAGE (self)); G_OBJECT_CLASS (ide_editor_page_parent_class)->constructed (object); gutter = gtk_source_view_get_gutter (GTK_SOURCE_VIEW (self->map), GTK_TEXT_WINDOW_LEFT); renderer = g_object_new (IDE_TYPE_LINE_CHANGE_GUTTER_RENDERER, "size", 1, "visible", TRUE, NULL); gtk_source_gutter_insert (gutter, renderer, 0); _ide_editor_page_init_actions (self); _ide_editor_page_init_shortcuts (self); _ide_editor_page_init_settings (self); g_signal_connect_swapped (self->source_view, "focus-in-event", G_CALLBACK (ide_editor_page_focus_in_event), self); g_signal_connect_swapped (self->source_view, "motion-notify-event", G_CALLBACK (ide_editor_page_source_view_event), self); g_signal_connect_swapped (self->source_view, "scroll-event", G_CALLBACK (ide_editor_page_source_view_event), self); g_signal_connect_swapped (self->source_view, "focus-location", G_CALLBACK (ide_editor_page_focus_location), self); g_signal_connect_swapped (self->source_view, "set-search-text", G_CALLBACK (ide_editor_page_set_search_text), self); g_signal_connect_swapped (self->source_view, "clear-search", G_CALLBACK (ide_editor_page_clear_search), self); g_signal_connect_swapped (self->source_view, "move-search", G_CALLBACK (ide_editor_page_move_search), self); g_signal_connect_swapped (self->map, "motion-notify-event", G_CALLBACK (ide_editor_page_source_view_event), self); /* * We want to track when the search revealer is visible. We will discard * the search context when the revealer is not visible so that we don't * continue performing expensive buffer operations. */ g_signal_connect_swapped (self->search_revealer, "notify::reveal-child", G_CALLBACK (search_revealer_notify_reveal_child), self); self->search = ide_editor_search_new (GTK_SOURCE_VIEW (self->source_view)); ide_editor_search_bar_set_search (self->search_bar, self->search); gtk_widget_insert_action_group (GTK_WIDGET (self), "editor-search", G_ACTION_GROUP (self->search)); ide_editor_page_load_fonts (self); ide_editor_page_update_map (self); } static void ide_editor_page_destroy (GtkWidget *widget) { IdeEditorPage *self = (IdeEditorPage *)widget; g_assert (IDE_IS_EDITOR_PAGE (self)); /* * WORKAROUND: We need to reset the drag dest to avoid warnings by Gtk * reseting the target list for the source view. */ if (self->source_view != NULL) gtk_drag_dest_set (GTK_WIDGET (self->source_view), GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY); dzl_clear_source (&self->toggle_map_source); ide_clear_and_destroy_object (&self->addins); gtk_widget_insert_action_group (widget, "editor-search", NULL); gtk_widget_insert_action_group (widget, "editor-page", NULL); g_cancellable_cancel (self->destroy_cancellable); g_clear_object (&self->destroy_cancellable); g_clear_object (&self->search); g_clear_object (&self->editor_settings); g_clear_object (&self->insight_settings); g_clear_object (&self->buffer); if (self->buffer_bindings != NULL) { dzl_binding_group_set_source (self->buffer_bindings, NULL); g_clear_object (&self->buffer_bindings); } if (self->buffer_signals != NULL) { dzl_signal_group_set_target (self->buffer_signals, NULL); g_clear_object (&self->buffer_signals); } GTK_WIDGET_CLASS (ide_editor_page_parent_class)->destroy (widget); } static GFile * ide_editor_page_get_file_or_directory (IdePage *page) { GFile *ret = ide_editor_page_get_file (IDE_EDITOR_PAGE (page)); return ret ? g_object_ref (ret) : NULL; } static void ide_editor_page_finalize (GObject *object) { G_OBJECT_CLASS (ide_editor_page_parent_class)->finalize (object); DZL_COUNTER_DEC (instances); } static void ide_editor_page_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { IdeEditorPage *self = IDE_EDITOR_PAGE (object); switch (prop_id) { case PROP_AUTO_HIDE_MAP: g_value_set_boolean (value, ide_editor_page_get_auto_hide_map (self)); break; case PROP_BUFFER: g_value_set_object (value, ide_editor_page_get_buffer (self)); break; case PROP_BUFFER_FILE: g_value_set_object (value, ide_buffer_get_file (self->buffer)); break; case PROP_VIEW: g_value_set_object (value, ide_editor_page_get_view (self)); break; case PROP_SEARCH: g_value_set_object (value, ide_editor_page_get_search (self)); break; case PROP_SHOW_MAP: g_value_set_boolean (value, ide_editor_page_get_show_map (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_editor_page_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { IdeEditorPage *self = IDE_EDITOR_PAGE (object); switch (prop_id) { case PROP_AUTO_HIDE_MAP: ide_editor_page_set_auto_hide_map (self, g_value_get_boolean (value)); break; case PROP_BUFFER: ide_editor_page_set_buffer (self, g_value_get_object (value)); break; case PROP_SHOW_MAP: ide_editor_page_set_show_map (self, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_editor_page_class_init (IdeEditorPageClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); IdePageClass *page_class = IDE_PAGE_CLASS (klass); object_class->finalize = ide_editor_page_finalize; object_class->constructed = ide_editor_page_constructed; object_class->get_property = ide_editor_page_get_property; object_class->set_property = ide_editor_page_set_property; widget_class->destroy = ide_editor_page_destroy; widget_class->hierarchy_changed = ide_editor_page_hierarchy_changed; page_class->create_split = ide_editor_page_create_split; page_class->get_file_or_directory = ide_editor_page_get_file_or_directory; properties [PROP_BUFFER] = g_param_spec_object ("buffer", "Buffer", "The buffer for the view", IDE_TYPE_BUFFER, (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); /* It's really just there to get notify:: support for the buffer's file property * but through the page, for the session addin. */ properties [PROP_BUFFER_FILE] = g_param_spec_object ("buffer-file", "Buffer file", "The buffer file for the view's buffer", G_TYPE_FILE, (G_PARAM_STATIC_STRINGS | G_PARAM_READABLE)); properties [PROP_SEARCH] = g_param_spec_object ("search", "Search", "An search helper for the document", IDE_TYPE_EDITOR_SEARCH, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); properties [PROP_SHOW_MAP] = g_param_spec_boolean ("show-map", "Show Map", "If the overview map should be shown", FALSE, (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); properties [PROP_AUTO_HIDE_MAP] = g_param_spec_boolean ("auto-hide-map", "Auto Hide Map", "If the overview map should be auto-hidden", FALSE, (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); properties [PROP_VIEW] = g_param_spec_object ("view", "View", "The view for editing the buffer", IDE_TYPE_SOURCE_VIEW, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, N_PROPS, properties); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-editor/ui/ide-editor-page.ui"); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, map); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, map_revealer); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, overlay); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, progress_bar); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, scroller); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, scroller_box); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, search_bar); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, search_revealer); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, modified_revealer); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, modified_cancel_button); gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, source_view); gtk_widget_class_bind_template_callback (widget_class, ide_editor_page_notify_child_revealed); gtk_widget_class_bind_template_callback (widget_class, ide_editor_page_stop_search); g_type_ensure (IDE_TYPE_SOURCE_VIEW); g_type_ensure (IDE_TYPE_EDITOR_SEARCH_BAR); } static void ide_editor_page_init (IdeEditorPage *self) { DZL_COUNTER_INC (instances); gtk_widget_init_template (GTK_WIDGET (self)); ide_page_set_can_split (IDE_PAGE (self), TRUE); ide_page_set_menu_id (IDE_PAGE (self), "ide-editor-page-document-menu"); self->destroy_cancellable = g_cancellable_new (); /* Setup signals to monitor on the buffer. */ self->buffer_signals = dzl_signal_group_new (IDE_TYPE_BUFFER); dzl_signal_group_connect_swapped (self->buffer_signals, "loaded", G_CALLBACK (ide_editor_page_buffer_loaded), self); dzl_signal_group_connect_swapped (self->buffer_signals, "modified-changed", G_CALLBACK (ide_editor_page_buffer_modified_changed), self); dzl_signal_group_connect_swapped (self->buffer_signals, "notify::file", G_CALLBACK (ide_editor_page_buffer_notify_file), self); dzl_signal_group_connect_swapped (self->buffer_signals, "notify::failed", G_CALLBACK (ide_editor_page_buffer_notify_failed), self); dzl_signal_group_connect_swapped (self->buffer_signals, "notify::language", G_CALLBACK (ide_editor_page_buffer_notify_language), self); dzl_signal_group_connect_swapped (self->buffer_signals, "notify::style-scheme", G_CALLBACK (ide_editor_page_buffer_notify_style_scheme), self); dzl_signal_group_connect_swapped (self->buffer_signals, "notify::changed-on-volume", G_CALLBACK (ide_editor_page__buffer_notify_changed_on_volume), self); g_signal_connect_swapped (self->buffer_signals, "bind", G_CALLBACK (ide_editor_page_bind_signals), self); g_signal_connect_object (self->modified_cancel_button, "clicked", G_CALLBACK (ide_editor_page_hide_reload_bar), self, G_CONNECT_SWAPPED); /* Setup bindings for the buffer. */ self->buffer_bindings = dzl_binding_group_new (); dzl_binding_group_bind (self->buffer_bindings, "title", self, "title", 0); /* Load our custom font for the overview map. */ gtk_source_map_set_view (self->map, GTK_SOURCE_VIEW (self->source_view)); } /** * ide_editor_page_get_buffer: * @self: a #IdeEditorPage * * Gets the underlying buffer for the view. * * Returns: (transfer none): An #IdeBuffer * * Since: 3.32 */ IdeBuffer * ide_editor_page_get_buffer (IdeEditorPage *self) { g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL); return self->buffer; } /** * ide_editor_page_get_view: * @self: a #IdeEditorPage * * Gets the #IdeSourceView that is part of the #IdeEditorPage. * * Returns: (transfer none): An #IdeSourceView * * Since: 3.32 */ IdeSourceView * ide_editor_page_get_view (IdeEditorPage *self) { g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL); return self->source_view; } /** * ide_editor_page_get_language_id: * @self: a #IdeEditorPage * * This is a helper to get the language-id of the underlying buffer. * * Returns: (nullable): the language-id as a string, or %NULL * * Since: 3.32 */ const gchar * ide_editor_page_get_language_id (IdeEditorPage *self) { g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL); if (self->buffer != NULL) { GtkSourceLanguage *language; language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (self->buffer)); if (language != NULL) return gtk_source_language_get_id (language); } return NULL; } /** * ide_editor_page_scroll_to_line: * @self: a #IdeEditorPage * @line: the line to scroll to * * This is a helper to quickly jump to a given line without all the frills. It * will also ensure focus on the editor view, so that refocusing the view * afterwards does not cause the view to restore the cursor to the previous * location. * * This will move the insert cursor. * * Lines start from 0. * * Since: 3.32 */ void ide_editor_page_scroll_to_line (IdeEditorPage *self, guint line) { ide_editor_page_scroll_to_line_offset (self, line, 0); } /** * ide_editor_page_scroll_to_line_offset: * @self: a #IdeEditorPage * @line: the line to scroll to * @line_offset: the line offset * * Like ide_editor_page_scroll_to_line() but allows specifying the * line offset (column) to place the cursor on. * * This will move the insert cursor. * * Lines and offsets start from 0. * * If @line_offset is zero, the first non-space character of @line will be * used instead. * * Since: 3.32 */ void ide_editor_page_scroll_to_line_offset (IdeEditorPage *self, guint line, guint line_offset) { GtkTextIter iter; g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); g_return_if_fail (self->buffer != NULL); g_return_if_fail (line <= G_MAXINT); gtk_widget_grab_focus (GTK_WIDGET (self->source_view)); gtk_text_buffer_get_iter_at_line_offset (GTK_TEXT_BUFFER (self->buffer), &iter, line, line_offset); if (line_offset == 0) { while (!gtk_text_iter_ends_line (&iter) && g_unichar_isspace (gtk_text_iter_get_char (&iter))) { if (!gtk_text_iter_forward_char (&iter)) break; } } gtk_text_buffer_select_range (GTK_TEXT_BUFFER (self->buffer), &iter, &iter); ide_source_view_scroll_to_insert (self->source_view); } gboolean ide_editor_page_get_auto_hide_map (IdeEditorPage *self) { g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), FALSE); return self->auto_hide_map; } static gboolean ide_editor_page_auto_hide_cb (gpointer user_data) { IdeEditorPage *self = user_data; g_assert (IDE_IS_EDITOR_PAGE (self)); self->toggle_map_source = 0; gtk_revealer_set_reveal_child (self->map_revealer, FALSE); return G_SOURCE_REMOVE; } static void ide_editor_page_update_reveal_timer (IdeEditorPage *self) { g_assert (IDE_IS_EDITOR_PAGE (self)); dzl_clear_source (&self->toggle_map_source); if (self->auto_hide_map && gtk_revealer_get_reveal_child (self->map_revealer)) { self->toggle_map_source = gdk_threads_add_timeout_seconds_full (G_PRIORITY_LOW, AUTO_HIDE_TIMEOUT_SECONDS, ide_editor_page_auto_hide_cb, g_object_ref (self), g_object_unref); } } void ide_editor_page_set_auto_hide_map (IdeEditorPage *self, gboolean auto_hide_map) { g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); auto_hide_map = !!auto_hide_map; if (auto_hide_map != self->auto_hide_map) { self->auto_hide_map = auto_hide_map; ide_editor_page_update_map (self); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_AUTO_HIDE_MAP]); } } gboolean ide_editor_page_get_show_map (IdeEditorPage *self) { g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), FALSE); return self->show_map; } void ide_editor_page_set_show_map (IdeEditorPage *self, gboolean show_map) { g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); show_map = !!show_map; if (show_map != self->show_map) { self->show_map = show_map; g_object_set (self->scroller, "vscrollbar-policy", show_map ? GTK_POLICY_EXTERNAL : GTK_POLICY_AUTOMATIC, NULL); ide_editor_page_update_map (self); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_AUTO_HIDE_MAP]); } } /** * ide_editor_page_set_language: * @self: a #IdeEditorPage * * This is a convenience function to set the language on the underlying * #IdeBuffer text buffer. * * Since: 3.32 */ void ide_editor_page_set_language (IdeEditorPage *self, GtkSourceLanguage *language) { g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); g_return_if_fail (!language || GTK_SOURCE_IS_LANGUAGE (language)); gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (self->buffer), language); } /** * ide_editor_page_get_language: * @self: a #IdeEditorPage * * Gets the #GtkSourceLanguage that is used by the underlying buffer. * * Returns: (transfer none) (nullable): a #GtkSourceLanguage or %NULL. * * Since: 3.32 */ GtkSourceLanguage * ide_editor_page_get_language (IdeEditorPage *self) { g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL); return gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (self->buffer)); } /** * ide_editor_page_move_next_error: * @self: a #IdeEditorPage * * Moves to the next error, if any. * * If there is no error, the insertion cursor is not moved. * * Since: 3.32 */ void ide_editor_page_move_next_error (IdeEditorPage *self) { g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); g_signal_emit_by_name (self->source_view, "move-error", GTK_DIR_DOWN); } /** * ide_editor_page_move_previous_error: * @self: a #IdeEditorPage * * Moves the insertion cursor to the previous error. * * If there is no error, the insertion cursor is not moved. * * Since: 3.32 */ void ide_editor_page_move_previous_error (IdeEditorPage *self) { g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); g_signal_emit_by_name (self->source_view, "move-error", GTK_DIR_UP); } /** * ide_editor_page_move_next_search_result: * @self: a #IdeEditorPage * * Moves the insertion cursor to the next search result. * * If there is no search result, the insertion cursor is not moved. * * Since: 3.32 */ void ide_editor_page_move_next_search_result (IdeEditorPage *self) { g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); g_return_if_fail (self->destroy_cancellable != NULL); g_return_if_fail (self->buffer != NULL); ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT); } /** * ide_editor_page_move_previous_search_result: * @self: a #IdeEditorPage * * Moves the insertion cursor to the previous search result. * * If there is no search result, the insertion cursor is not moved. * * Since: 3.32 */ void ide_editor_page_move_previous_search_result (IdeEditorPage *self) { g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); g_return_if_fail (self->destroy_cancellable != NULL); g_return_if_fail (self->buffer != NULL); ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_PREVIOUS); } /** * ide_editor_page_get_search: * @self: a #IdeEditorPage * * Gets the #IdeEditorSearch used to search within the document. * * Returns: (transfer none): An #IdeEditorSearch * * Since: 3.32 */ IdeEditorSearch * ide_editor_page_get_search (IdeEditorPage *self) { g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL); return self->search; } /** * ide_editor_page_get_file: * @self: a #IdeEditorPage * * Gets the #GFile that represents the current file. This may be a temporary * file, but a #GFile will still be used for the temporary file. * * Returns: (transfer none): a #GFile for the current buffer * * Since: 3.32 */ GFile * ide_editor_page_get_file (IdeEditorPage *self) { IdeBuffer *buffer; g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL); if ((buffer = ide_editor_page_get_buffer (self))) return ide_buffer_get_file (buffer); return NULL; }