/* ide-editor-page-actions.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-actions" #include "config.h" #include #include #include #include "ide-editor-surface.h" #include "ide-editor-private.h" #include "ide-editor-print-operation.h" #include "ide-editor-settings-dialog.h" typedef struct { IdeEditorPage *self; guint line; guint line_offset; } ReloadState; static void reload_state_free (ReloadState *state) { g_clear_object (&state->self); g_slice_free (ReloadState, state); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (ReloadState, reload_state_free) static void ide_editor_page_actions_reload_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeBufferManager *bufmgr = (IdeBufferManager *)object; g_autoptr(ReloadState) state = user_data; g_autoptr(IdeBuffer) buffer = NULL; g_autoptr(GError) error = NULL; g_assert (IDE_IS_BUFFER_MANAGER (bufmgr)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (state != NULL); g_assert (IDE_IS_EDITOR_PAGE (state->self)); if (state->self->progress_bar != NULL) dzl_gtk_widget_hide_with_fade (GTK_WIDGET (state->self->progress_bar)); if (!(buffer = ide_buffer_manager_load_file_finish (bufmgr, result, &error))) { g_warning ("%s", error->message); ide_page_report_error (IDE_PAGE (state->self), /* translators: %s is the error message */ _("Failed to load file: %s"), error->message); ide_page_set_failed (IDE_PAGE (state->self), TRUE); } else { IdeSourceView *view; GtkTextIter iter; view = ide_editor_page_get_view (state->self); gtk_text_buffer_get_iter_at_line_offset (GTK_TEXT_BUFFER (buffer), &iter, state->line, state->line_offset); gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &iter, &iter); ide_source_view_scroll_to_iter (view, &iter, .25, IDE_SOURCE_SCROLL_BOTH, 1.0, 0.5, FALSE); } gtk_revealer_set_reveal_child (state->self->modified_revealer, FALSE); } static void ide_editor_page_actions_reload (GSimpleAction *action, GVariant *param, gpointer user_data) { IdeEditorPage *self = user_data; g_autoptr(IdeNotification) notif = NULL; g_autoptr(IdeContext) context = NULL; IdeBufferManager *bufmgr; ReloadState *state; GtkTextIter iter; IdeBuffer *buffer; GFile *file; g_assert (IDE_IS_EDITOR_PAGE (self)); buffer = ide_editor_page_get_buffer (self); context = ide_buffer_ref_context (buffer); bufmgr = ide_buffer_manager_from_context (context); file = ide_buffer_get_file (buffer); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->progress_bar), 0.0); gtk_widget_show (GTK_WIDGET (self->progress_bar)); notif = ide_notification_new (); ide_buffer_get_selection_bounds (buffer, &iter, NULL); state = g_slice_new0 (ReloadState); state->self = g_object_ref (self); state->line = gtk_text_iter_get_line (&iter); state->line_offset = gtk_text_iter_get_line_offset (&iter); ide_buffer_manager_load_file_async (bufmgr, file, IDE_BUFFER_OPEN_FLAGS_FORCE_RELOAD, notif, NULL, ide_editor_page_actions_reload_cb, g_steal_pointer (&state)); g_object_bind_property (notif, "progress", self->progress_bar, "fraction", G_BINDING_SYNC_CREATE); } static void handle_print_result (IdeEditorPage *self, GtkPrintOperation *operation, GtkPrintOperationResult result) { g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (GTK_IS_PRINT_OPERATION (operation)); if (result == GTK_PRINT_OPERATION_RESULT_ERROR) { g_autoptr(GError) error = NULL; gtk_print_operation_get_error (operation, &error); g_warning ("%s", error->message); ide_page_report_error (IDE_PAGE (self), /* translators: %s is the error message */ _("Print failed: %s"), error->message); } } static void print_done (GtkPrintOperation *operation, GtkPrintOperationResult result, gpointer user_data) { IdeEditorPage *self = user_data; g_assert (GTK_IS_PRINT_OPERATION (operation)); g_assert (IDE_IS_EDITOR_PAGE (self)); handle_print_result (self, operation, result); g_object_unref (operation); g_object_unref (self); } static void ide_editor_page_actions_print (GSimpleAction *action, GVariant *param, gpointer user_data) { g_autoptr(IdeEditorPrintOperation) operation = NULL; IdeEditorPage *self = user_data; IdeSourceView *source_view; GtkWidget *toplevel; GtkPrintOperationResult result; g_assert (IDE_IS_EDITOR_PAGE (self)); toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW); source_view = ide_editor_page_get_view (self); operation = ide_editor_print_operation_new (source_view); /* keep a ref until "done" is emitted */ g_object_ref (operation); g_signal_connect_after (g_object_ref (operation), "done", G_CALLBACK (print_done), g_object_ref (self)); result = gtk_print_operation_run (GTK_PRINT_OPERATION (operation), GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, GTK_WINDOW (toplevel), NULL); handle_print_result (self, GTK_PRINT_OPERATION (operation), result); } static void ide_editor_page_actions_save_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeBuffer *buffer = (IdeBuffer *)object; g_autoptr(IdeEditorPage) self = user_data; g_autoptr(GError) error = NULL; g_assert (IDE_IS_BUFFER (buffer)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_EDITOR_PAGE (self)); if (!ide_buffer_save_file_finish (buffer, result, &error)) { g_warning ("%s", error->message); ide_page_report_error (IDE_PAGE (self), /* translators: %s is the error message */ _("Failed to save file: %s"), error->message); ide_page_set_failed (IDE_PAGE (self), TRUE); } if (self->progress_bar != NULL) dzl_gtk_widget_hide_with_fade (GTK_WIDGET (self->progress_bar)); } static void ide_editor_page_actions_save (GSimpleAction *action, GVariant *variant, gpointer user_data) { IdeEditorPage *self = user_data; g_autoptr(IdeNotification) notif = NULL; g_autoptr(IdeContext) context = NULL; g_autoptr(GFile) local_file = NULL; g_autoptr(GFile) workdir = NULL; IdeBuffer *buffer; GFile *file; g_assert (G_IS_SIMPLE_ACTION (action)); g_assert (IDE_IS_EDITOR_PAGE (self)); buffer = ide_editor_page_get_buffer (self); g_return_if_fail (IDE_IS_BUFFER (buffer)); context = ide_buffer_ref_context (buffer); g_return_if_fail (IDE_IS_CONTEXT (context)); file = ide_buffer_get_file (buffer); g_return_if_fail (G_IS_FILE (file)); workdir = ide_context_ref_workdir (context); g_assert (G_IS_FILE (workdir)); if (ide_buffer_get_is_temporary (buffer)) { GtkFileChooserNative *dialog; GtkWidget *toplevel; gint ret; toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW); dialog = gtk_file_chooser_native_new (_("Save File"), GTK_WINDOW (toplevel), GTK_FILE_CHOOSER_ACTION_SAVE, _("Save"), _("Cancel")); g_object_set (dialog, "do-overwrite-confirmation", TRUE, "local-only", FALSE, "modal", TRUE, "select-multiple", FALSE, "show-hidden", FALSE, NULL); gtk_file_chooser_set_current_folder_file (GTK_FILE_CHOOSER (dialog), workdir, NULL); ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (dialog)); if (ret == GTK_RESPONSE_ACCEPT) file = local_file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (dialog)); if (local_file == NULL) return; } ide_buffer_save_file_async (buffer, file, NULL, ¬if, ide_editor_page_actions_save_cb, g_object_ref (self)); g_object_bind_property (notif, "progress", self->progress_bar, "fraction", G_BINDING_SYNC_CREATE); gtk_widget_show (GTK_WIDGET (self->progress_bar)); } static void ide_editor_page_actions_save_as_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeBuffer *buffer = (IdeBuffer *)object; g_autoptr(IdeEditorPage) self = user_data; g_autoptr(GError) error = NULL; g_assert (IDE_IS_BUFFER (buffer)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_EDITOR_PAGE (self)); if (!ide_buffer_save_file_finish (buffer, result, &error)) { /* In this case, the editor page hasn't failed since this is for an * alternate file (which maybe we just don't have access to on the * network or something). * * But we do still need to notify the user of the error. */ g_warning ("%s", error->message); ide_page_report_error (IDE_PAGE (self), /* translators: %s is the underlying error message */ _("Failed to save file: %s"), error->message); } dzl_gtk_widget_hide_with_fade (GTK_WIDGET (self->progress_bar)); } static void ide_editor_page_actions_save_as (GSimpleAction *action, GVariant *param, gpointer user_data) { IdeEditorPage *self = user_data; GtkFileChooserNative *dialog; IdeBuffer *buffer; GtkWidget *toplevel; GFile *file; gint ret; g_assert (IDE_IS_EDITOR_PAGE (self)); buffer = ide_editor_page_get_buffer (self); file = ide_buffer_get_file (buffer); /* Just redirect to the save flow if we have a temporary * file currently. That way we can avoid splitting the * flow to handle both cases here. */ if (ide_buffer_get_is_temporary (buffer)) { ide_editor_page_actions_save (action, NULL, user_data); return; } toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW); dialog = gtk_file_chooser_native_new (_("Save a Copy"), GTK_WINDOW (toplevel), GTK_FILE_CHOOSER_ACTION_SAVE, _("Save"), _("Cancel")); gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE); gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (dialog), FALSE); gtk_file_chooser_set_show_hidden (GTK_FILE_CHOOSER (dialog), FALSE); if (file != NULL) gtk_file_chooser_set_file (GTK_FILE_CHOOSER (dialog), file, NULL); ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (dialog)); if (ret == GTK_RESPONSE_ACCEPT) { g_autoptr(GFile) save_as = NULL; g_autoptr(IdeNotification) notif = NULL; save_as = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); ide_buffer_save_file_async (buffer, save_as, NULL, ¬if, ide_editor_page_actions_save_as_cb, g_object_ref (self)); g_object_bind_property (notif, "progress", self->progress_bar, "fraction", G_BINDING_SYNC_CREATE); gtk_widget_show (GTK_WIDGET (self->progress_bar)); } gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (dialog)); } static void ide_editor_page_actions_find (GSimpleAction *action, GVariant *variant, gpointer user_data) { IdeEditorPage *self = user_data; GtkTextIter begin; GtkTextIter end; g_assert (G_IS_SIMPLE_ACTION (action)); g_assert (IDE_IS_EDITOR_PAGE (self)); if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end)) { g_autofree gchar *word = gtk_text_iter_get_slice (&begin, &end); ide_editor_search_set_search_text (self->search, word); } ide_editor_search_bar_set_replace_mode (self->search_bar, FALSE); gtk_revealer_set_reveal_child (self->search_revealer, TRUE); gtk_widget_grab_focus (GTK_WIDGET (self->search_bar)); } static void ide_editor_page_actions_find_replace (GSimpleAction *action, GVariant *variant, gpointer user_data) { IdeEditorPage *self = user_data; GtkTextIter begin; GtkTextIter end; g_assert (G_IS_SIMPLE_ACTION (action)); g_assert (IDE_IS_EDITOR_PAGE (self)); if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end)) { g_autofree gchar *word = gtk_text_iter_get_slice (&begin, &end); ide_editor_search_set_search_text (self->search, word); } ide_editor_search_bar_set_replace_mode (self->search_bar, TRUE); gtk_revealer_set_reveal_child (self->search_revealer, TRUE); gtk_widget_grab_focus (GTK_WIDGET (self->search_bar)); } static void ide_editor_page_actions_hide_search (GSimpleAction *action, GVariant *variant, gpointer user_data) { IdeEditorPage *self = user_data; g_assert (G_IS_SIMPLE_ACTION (action)); g_assert (IDE_IS_EDITOR_PAGE (self)); gtk_revealer_set_reveal_child (self->search_revealer, FALSE); gtk_widget_grab_focus (GTK_WIDGET (self->source_view)); } static void ide_editor_page_actions_notify_file_settings (IdeEditorPage *self, GParamSpec *pspec, IdeSourceView *source_view) { IdeFileSettings *file_settings; GActionGroup *group; g_assert (IDE_IS_EDITOR_PAGE (self)); g_assert (IDE_IS_SOURCE_VIEW (source_view)); group = gtk_widget_get_action_group (GTK_WIDGET (self), "file-settings"); g_assert (DZL_IS_PROPERTIES_GROUP (group)); file_settings = ide_source_view_get_file_settings (source_view); g_assert (!file_settings || IDE_IS_FILE_SETTINGS (file_settings)); g_object_set (group, "object", file_settings, NULL); } static void ide_editor_page_actions_move_next_error (GSimpleAction *action, GVariant *variant, gpointer user_data) { ide_editor_page_move_next_error (user_data); } static void ide_editor_page_actions_move_previous_error (GSimpleAction *action, GVariant *variant, gpointer user_data) { ide_editor_page_move_previous_error (user_data); } static void ide_editor_page_actions_activate_next_search_result (GSimpleAction *action, GVariant *variant, gpointer user_data) { IdeEditorPage *self = user_data; GtkTextIter begin; GtkTextIter end; g_assert (IDE_IS_EDITOR_PAGE (self)); ide_editor_page_move_next_search_result (self); gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end); gtk_widget_grab_focus (GTK_WIDGET (self->source_view)); gtk_text_buffer_select_range (GTK_TEXT_BUFFER (self->buffer), &begin, &end); ide_source_view_scroll_to_insert (self->source_view); } static void ide_editor_page_actions_move_next_search_result (GSimpleAction *action, GVariant *variant, gpointer user_data) { ide_editor_page_move_next_search_result (user_data); } static void ide_editor_page_actions_move_previous_search_result (GSimpleAction *action, GVariant *variant, gpointer user_data) { ide_editor_page_move_previous_search_result (user_data); } static void ide_editor_page_actions_properties (GSimpleAction *action, GVariant *variant, gpointer user_data) { IdeEditorPage *self = user_data; IdeEditorSettingsDialog *dialog; g_assert (IDE_IS_EDITOR_PAGE (self)); dialog = ide_editor_settings_dialog_new (self); g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL); ide_gtk_window_present (GTK_WINDOW (dialog)); } static void ide_editor_page_actions_toggle_map (GSimpleAction *action, GVariant *variant, gpointer user_data) { IdeEditorPage *self = user_data; g_assert (G_IS_SIMPLE_ACTION (action)); g_assert (IDE_IS_EDITOR_PAGE (self)); ide_editor_page_set_show_map (self, !ide_editor_page_get_show_map (self)); } static const GActionEntry editor_view_entries[] = { { "activate-next-search-result", ide_editor_page_actions_activate_next_search_result }, { "find", ide_editor_page_actions_find }, { "find-replace", ide_editor_page_actions_find_replace }, { "hide-search", ide_editor_page_actions_hide_search }, { "move-next-error", ide_editor_page_actions_move_next_error }, { "move-next-search-result", ide_editor_page_actions_move_next_search_result }, { "move-previous-error", ide_editor_page_actions_move_previous_error }, { "move-previous-search-result", ide_editor_page_actions_move_previous_search_result }, { "properties", ide_editor_page_actions_properties }, { "print", ide_editor_page_actions_print }, { "reload", ide_editor_page_actions_reload }, { "save", ide_editor_page_actions_save }, { "save-as", ide_editor_page_actions_save_as }, { "toggle-map", ide_editor_page_actions_toggle_map }, }; void _ide_editor_page_init_actions (IdeEditorPage *self) { g_autoptr(GSimpleActionGroup) group = NULL; g_autoptr(DzlPropertiesGroup) sv_props = NULL; g_autoptr(DzlPropertiesGroup) file_props = NULL; IdeSourceView *source_view; g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); source_view = ide_editor_page_get_view (self); /* Setup our user-facing actions */ group = g_simple_action_group_new (); g_action_map_add_action_entries (G_ACTION_MAP (group), editor_view_entries, G_N_ELEMENTS (editor_view_entries), self); gtk_widget_insert_action_group (GTK_WIDGET (self), "editor-page", G_ACTION_GROUP (group)); /* We want to access some settings properties as stateful GAction so they * manipulated using regular Gtk widgets from the properties panel. */ sv_props = dzl_properties_group_new (G_OBJECT (source_view)); dzl_properties_group_add_all_properties (sv_props); dzl_properties_group_add_property_full (sv_props, "use-spaces", "insert-spaces-instead-of-tabs", DZL_PROPERTIES_FLAGS_STATEFUL_BOOLEANS); gtk_widget_insert_action_group (GTK_WIDGET (self), "source-view", G_ACTION_GROUP (sv_props)); /* * We want to bind our file-settings, used to tweak values in the * source-view, to a GActionGroup that can be manipulated by the properties * editor. Make sure we get notified of changes and sink the current state. */ file_props = dzl_properties_group_new_for_type (IDE_TYPE_FILE_SETTINGS); dzl_properties_group_add_all_properties (file_props); g_signal_connect_swapped (source_view, "notify::file-settings", G_CALLBACK (ide_editor_page_actions_notify_file_settings), self); gtk_widget_insert_action_group (GTK_WIDGET (self), "file-settings", G_ACTION_GROUP (file_props)); ide_editor_page_actions_notify_file_settings (self, NULL, source_view); } void _ide_editor_page_update_actions (IdeEditorPage *self) { g_return_if_fail (IDE_IS_EDITOR_PAGE (self)); }