diff options
author | tfarina@chromium.org <tfarina@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-04-26 22:16:14 +0000 |
---|---|---|
committer | tfarina@chromium.org <tfarina@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-04-26 22:16:14 +0000 |
commit | 7dcc272d07e2dc3fb5394439edc2975498c9ce98 (patch) | |
tree | 07b3c6a293d7d80c6e91899bfc19c00756c72cd8 /chrome/browser/ui | |
parent | 51c84ee35f03c61d35c30290d46df332c8ddeae5 (diff) | |
download | chromium_src-7dcc272d07e2dc3fb5394439edc2975498c9ce98.zip chromium_src-7dcc272d07e2dc3fb5394439edc2975498c9ce98.tar.gz chromium_src-7dcc272d07e2dc3fb5394439edc2975498c9ce98.tar.bz2 |
gtk: Move AutocompleteEditViewGtk/autocomplete_edit_view_gtk.* to ui/gtk/omnibox directory.
- Rename AutocompleteEditViewGtk to OmniboxViewGtk.
- Move autocomplete_edit_view_gtk.* to omnibox_view_gtk.*
BUG=80186
TEST=None
R=pkasting@chromium.org,evan@chromium.org
Review URL: http://codereview.chromium.org/6905030
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@83086 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/ui')
-rw-r--r-- | chrome/browser/ui/gtk/location_bar_view_gtk.cc | 18 | ||||
-rw-r--r-- | chrome/browser/ui/gtk/location_bar_view_gtk.h | 6 | ||||
-rw-r--r-- | chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.cc | 2346 | ||||
-rw-r--r-- | chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.h | 546 | ||||
-rw-r--r-- | chrome/browser/ui/views/location_bar/location_bar_view.cc | 6 | ||||
-rw-r--r-- | chrome/browser/ui/views/location_bar/location_bar_view.h | 2 |
6 files changed, 2907 insertions, 17 deletions
diff --git a/chrome/browser/ui/gtk/location_bar_view_gtk.cc b/chrome/browser/ui/gtk/location_bar_view_gtk.cc index 500626f..0b73891 100644 --- a/chrome/browser/ui/gtk/location_bar_view_gtk.cc +++ b/chrome/browser/ui/gtk/location_bar_view_gtk.cc @@ -17,7 +17,6 @@ #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/accessibility_events.h" #include "chrome/browser/alternate_nav_url_fetcher.h" -#include "chrome/browser/autocomplete/autocomplete_edit_view_gtk.h" #include "chrome/browser/autocomplete/autocomplete_popup_model.h" #include "chrome/browser/command_updater.h" #include "chrome/browser/defaults.h" @@ -41,6 +40,7 @@ #include "chrome/browser/ui/gtk/gtk_theme_service.h" #include "chrome/browser/ui/gtk/gtk_util.h" #include "chrome/browser/ui/gtk/nine_box.h" +#include "chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.h" #include "chrome/browser/ui/gtk/rounded_window.h" #include "chrome/browser/ui/gtk/view_id_util.h" #include "chrome/browser/ui/omnibox/location_bar_util.h" @@ -184,7 +184,7 @@ LocationBarViewGtk::~LocationBarViewGtk() { void LocationBarViewGtk::Init(bool popup_window_mode) { popup_window_mode_ = popup_window_mode; - // Create the widget first, so we can pass it to the AutocompleteEditViewGtk. + // Create the widget first, so we can pass it to the OmniboxViewGtk. hbox_.Own(gtk_hbox_new(FALSE, kInnerPadding)); gtk_container_set_border_width(GTK_CONTAINER(hbox_.get()), kHboxBorder); // We will paint for the alignment, to paint the background and border. @@ -193,13 +193,13 @@ void LocationBarViewGtk::Init(bool popup_window_mode) { // the home button on/off. gtk_widget_set_redraw_on_allocate(hbox_.get(), TRUE); - // Now initialize the AutocompleteEditViewGtk. - location_entry_.reset(new AutocompleteEditViewGtk(this, - toolbar_model_, - profile_, - command_updater_, - popup_window_mode_, - hbox_.get())); + // Now initialize the OmniboxViewGtk. + location_entry_.reset(new OmniboxViewGtk(this, + toolbar_model_, + profile_, + command_updater_, + popup_window_mode_, + hbox_.get())); location_entry_->Init(); g_signal_connect(hbox_.get(), "expose-event", diff --git a/chrome/browser/ui/gtk/location_bar_view_gtk.h b/chrome/browser/ui/gtk/location_bar_view_gtk.h index b07df3b..ec20f2d 100644 --- a/chrome/browser/ui/gtk/location_bar_view_gtk.h +++ b/chrome/browser/ui/gtk/location_bar_view_gtk.h @@ -16,7 +16,6 @@ #include "base/memory/scoped_ptr.h" #include "base/memory/scoped_vector.h" #include "chrome/browser/autocomplete/autocomplete_edit.h" -#include "chrome/browser/autocomplete/autocomplete_edit_view_gtk.h" #include "chrome/browser/extensions/extension_context_menu_model.h" #include "chrome/browser/extensions/image_loading_tracker.h" #include "chrome/browser/first_run/first_run.h" @@ -30,11 +29,12 @@ #include "content/common/notification_registrar.h" #include "content/common/page_transition_types.h" #include "third_party/skia/include/core/SkBitmap.h" +#include "ui/base/animation/animation_delegate.h" #include "ui/base/animation/slide_animation.h" #include "ui/base/gtk/gtk_signal.h" #include "webkit/glue/window_open_disposition.h" -class AutocompleteEditViewGtk; +class OmniboxViewGtk; class Browser; class CommandUpdater; class ContentSettingImageModel; @@ -403,7 +403,7 @@ class LocationBarViewGtk : public AutocompleteEditController, GtkWidget* tab_to_search_hint_icon_; GtkWidget* tab_to_search_hint_trailing_label_; - scoped_ptr<AutocompleteEditViewGtk> location_entry_; + scoped_ptr<OmniboxViewGtk> location_entry_; // Alignment used to wrap |location_entry_|. GtkWidget* location_entry_alignment_; diff --git a/chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.cc b/chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.cc new file mode 100644 index 0000000..c2cb83a --- /dev/null +++ b/chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.cc @@ -0,0 +1,2346 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.h" + +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> + +#include <algorithm> + +#include "base/logging.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/autocomplete/autocomplete_edit.h" +#include "chrome/browser/autocomplete/autocomplete_match.h" +#include "chrome/browser/autocomplete/autocomplete_popup_model.h" +#include "chrome/browser/bookmarks/bookmark_node_data.h" +#include "chrome/browser/command_updater.h" +#include "chrome/browser/defaults.h" +#include "chrome/browser/instant/instant_controller.h" +#include "chrome/browser/platform_util.h" +#include "chrome/browser/ui/gtk/gtk_util.h" +#include "chrome/browser/ui/gtk/view_id_util.h" +#include "chrome/browser/ui/toolbar/toolbar_model.h" +#include "content/browser/tab_contents/tab_contents.h" +#include "content/common/notification_service.h" +#include "googleurl/src/gurl.h" +#include "grit/generated_resources.h" +#include "net/base/escape.h" +#include "third_party/undoview/undo_view.h" +#include "ui/base/animation/multi_animation.h" +#include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/resource/resource_bundle.h" +#include "ui/gfx/color_utils.h" +#include "ui/gfx/font.h" +#include "ui/gfx/gtk_util.h" +#include "ui/gfx/skia_utils_gtk.h" + +#if defined(TOOLKIT_VIEWS) +#include "chrome/browser/autocomplete/autocomplete_edit_view_views.h" +#include "chrome/browser/ui/views/autocomplete/autocomplete_popup_contents_view.h" +#include "chrome/browser/ui/views/location_bar/location_bar_view.h" +#include "views/controls/textfield/native_textfield_views.h" +#include "views/events/event.h" +#else +#include "chrome/browser/autocomplete/autocomplete_popup_view_gtk.h" +#include "chrome/browser/ui/gtk/gtk_theme_service.h" +#include "chrome/browser/ui/gtk/location_bar_view_gtk.h" +#endif + +namespace { + +const gchar* kOmniboxViewGtkKey = "__OMNIBOX_VIEW_GTK__"; + +const char kTextBaseColor[] = "#808080"; +const char kSecureSchemeColor[] = "#079500"; +const char kSecurityErrorSchemeColor[] = "#a20000"; + +const double kStrikethroughStrokeRed = 162.0 / 256.0; +const double kStrikethroughStrokeWidth = 2.0; + +size_t GetUTF8Offset(const string16& text, size_t text_offset) { + return UTF16ToUTF8(text.substr(0, text_offset)).size(); +} + +// A helper method for determining a valid drag operation given the allowed +// operation. We prefer copy over link. +int CopyOrLinkDragOperation(int drag_operation) { + if (drag_operation & ui::DragDropTypes::DRAG_COPY) + return ui::DragDropTypes::DRAG_COPY; + if (drag_operation & ui::DragDropTypes::DRAG_LINK) + return ui::DragDropTypes::DRAG_LINK; + return ui::DragDropTypes::DRAG_NONE; +} + +// Stores GTK+-specific state so it can be restored after switching tabs. +struct ViewState { + explicit ViewState(const OmniboxViewGtk::CharRange& selection_range) + : selection_range(selection_range) { + } + + // Range of selected text. + OmniboxViewGtk::CharRange selection_range; +}; + +struct AutocompleteEditState { + AutocompleteEditState(const AutocompleteEditModel::State& model_state, + const ViewState& view_state) + : model_state(model_state), + view_state(view_state) { + } + + const AutocompleteEditModel::State model_state; + const ViewState view_state; +}; + +// Returns a lazily initialized property bag accessor for saving our state in a +// TabContents. +PropertyAccessor<AutocompleteEditState>* GetStateAccessor() { + static PropertyAccessor<AutocompleteEditState> state; + return &state; +} + +// Set up style properties to override the default GtkTextView; if a theme has +// overridden some of these properties, an inner-line will be displayed inside +// the fake GtkTextEntry. +void SetEntryStyle() { + static bool style_was_set = false; + + if (style_was_set) + return; + style_was_set = true; + + gtk_rc_parse_string( + "style \"chrome-location-bar-entry\" {" + " xthickness = 0\n" + " ythickness = 0\n" + " GtkWidget::focus_padding = 0\n" + " GtkWidget::focus-line-width = 0\n" + " GtkWidget::interior_focus = 0\n" + " GtkWidget::internal-padding = 0\n" + " GtkContainer::border-width = 0\n" + "}\n" + "widget \"*chrome-location-bar-entry\" " + "style \"chrome-location-bar-entry\""); +} + +// Copied from GTK+. Called when we lose the primary selection. This will clear +// the selection in the text buffer. +void ClipboardSelectionCleared(GtkClipboard* clipboard, + gpointer data) { + GtkTextIter insert; + GtkTextIter selection_bound; + GtkTextBuffer* buffer = GTK_TEXT_BUFFER(data); + + gtk_text_buffer_get_iter_at_mark(buffer, &insert, + gtk_text_buffer_get_insert(buffer)); + gtk_text_buffer_get_iter_at_mark(buffer, &selection_bound, + gtk_text_buffer_get_selection_bound(buffer)); + + if (!gtk_text_iter_equal(&insert, &selection_bound)) { + gtk_text_buffer_move_mark(buffer, + gtk_text_buffer_get_selection_bound(buffer), + &insert); + } +} + +} // namespace + +OmniboxViewGtk::OmniboxViewGtk( + AutocompleteEditController* controller, + ToolbarModel* toolbar_model, + Profile* profile, + CommandUpdater* command_updater, + bool popup_window_mode, +#if defined(TOOLKIT_VIEWS) + views::View* location_bar) +#else + GtkWidget* location_bar) +#endif + : text_view_(NULL), + tag_table_(NULL), + text_buffer_(NULL), + faded_text_tag_(NULL), + secure_scheme_tag_(NULL), + security_error_scheme_tag_(NULL), + normal_text_tag_(NULL), + instant_anchor_tag_(NULL), + instant_view_(NULL), + instant_mark_(NULL), + model_(new AutocompleteEditModel(this, controller, profile)), + controller_(controller), + toolbar_model_(toolbar_model), + command_updater_(command_updater), + popup_window_mode_(popup_window_mode), + security_level_(ToolbarModel::NONE), + mark_set_handler_id_(0), +#if defined(OS_CHROMEOS) + button_1_pressed_(false), + text_selected_during_click_(false), + text_view_focused_before_button_press_(false), +#endif +#if defined(TOOLKIT_VIEWS) + location_bar_view_(location_bar), +#else + theme_service_(GtkThemeService::GetFrom(profile)), +#endif + enter_was_pressed_(false), + tab_was_pressed_(false), + paste_clipboard_requested_(false), + enter_was_inserted_(false), + selection_suggested_(false), + delete_was_pressed_(false), + delete_at_end_pressed_(false), + handling_key_press_(false), + content_maybe_changed_by_key_press_(false), + update_popup_without_focus_(false), +#if GTK_CHECK_VERSION(2, 20, 0) + preedit_size_before_change_(0), +#endif + going_to_focus_(NULL) { + popup_view_.reset( +#if defined(TOOLKIT_VIEWS) + new AutocompletePopupContentsView +#else + new AutocompletePopupViewGtk +#endif + (GetFont(), this, model_.get(), profile, location_bar)); +} + +OmniboxViewGtk::~OmniboxViewGtk() { + NotificationService::current()->Notify( + NotificationType::AUTOCOMPLETE_EDIT_DESTROYED, + Source<OmniboxViewGtk>(this), + NotificationService::NoDetails()); + + // Explicitly teardown members which have a reference to us. Just to be safe + // we want them to be destroyed before destroying any other internal state. + popup_view_.reset(); + model_.reset(); + + // We own our widget and TextView related objects. + if (alignment_.get()) { // Init() has been called. + alignment_.Destroy(); + g_object_unref(text_buffer_); + g_object_unref(tag_table_); + // The tags we created are owned by the tag_table, and should be destroyed + // along with it. We don't hold our own reference to them. + } +} + +void OmniboxViewGtk::Init() { + SetEntryStyle(); + + // The height of the text view is going to change based on the font used. We + // don't want to stretch the height, and we want it vertically centered. + alignment_.Own(gtk_alignment_new(0., 0.5, 1.0, 0.0)); + gtk_widget_set_name(alignment_.get(), + "chrome-autocomplete-edit-view"); + + // The GtkTagTable and GtkTextBuffer are not initially unowned, so we have + // our own reference when we create them, and we own them. Adding them to + // the other objects adds a reference; it doesn't adopt them. + tag_table_ = gtk_text_tag_table_new(); + text_buffer_ = gtk_text_buffer_new(tag_table_); + g_object_set_data(G_OBJECT(text_buffer_), kOmniboxViewGtkKey, this); + + // We need to run this two handlers before undo manager's handlers, so that + // text iterators modified by these handlers can be passed down to undo + // manager's handlers. + g_signal_connect(text_buffer_, "delete-range", + G_CALLBACK(&HandleDeleteRangeThunk), this); + g_signal_connect(text_buffer_, "mark-set", + G_CALLBACK(&HandleMarkSetAlwaysThunk), this); + + text_view_ = gtk_undo_view_new(text_buffer_); + if (popup_window_mode_) + gtk_text_view_set_editable(GTK_TEXT_VIEW(text_view_), false); + + // One pixel left margin is necessary to make the cursor visible when UI + // language direction is LTR but |text_buffer_|'s content direction is RTL. + gtk_text_view_set_left_margin(GTK_TEXT_VIEW(text_view_), 1); + + // See SetEntryStyle() comments. + gtk_widget_set_name(text_view_, "chrome-location-bar-entry"); + + // The text view was floating. It will now be owned by the alignment. + gtk_container_add(GTK_CONTAINER(alignment_.get()), text_view_); + + // Do not allow inserting tab characters when pressing Tab key, so that when + // Tab key is pressed, |text_view_| will emit "move-focus" signal, which will + // be intercepted by our own handler to trigger Tab to search feature when + // necessary. + gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(text_view_), FALSE); + + faded_text_tag_ = gtk_text_buffer_create_tag(text_buffer_, + NULL, "foreground", kTextBaseColor, NULL); + secure_scheme_tag_ = gtk_text_buffer_create_tag(text_buffer_, + NULL, "foreground", kSecureSchemeColor, NULL); + security_error_scheme_tag_ = gtk_text_buffer_create_tag(text_buffer_, + NULL, "foreground", kSecurityErrorSchemeColor, NULL); + normal_text_tag_ = gtk_text_buffer_create_tag(text_buffer_, + NULL, "foreground", "#000000", NULL); + + // NOTE: This code used to connect to "changed", however this was fired too + // often and during bad times (our own buffer changes?). It works out much + // better to listen to end-user-action, which should be fired whenever the + // user makes some sort of change to the buffer. + g_signal_connect(text_buffer_, "begin-user-action", + G_CALLBACK(&HandleBeginUserActionThunk), this); + g_signal_connect(text_buffer_, "end-user-action", + G_CALLBACK(&HandleEndUserActionThunk), this); + // We connect to key press and release for special handling of a few keys. + g_signal_connect(text_view_, "key-press-event", + G_CALLBACK(&HandleKeyPressThunk), this); + g_signal_connect(text_view_, "key-release-event", + G_CALLBACK(&HandleKeyReleaseThunk), this); + g_signal_connect(text_view_, "button-press-event", + G_CALLBACK(&HandleViewButtonPressThunk), this); + g_signal_connect(text_view_, "button-release-event", + G_CALLBACK(&HandleViewButtonReleaseThunk), this); + g_signal_connect(text_view_, "focus-in-event", + G_CALLBACK(&HandleViewFocusInThunk), this); + g_signal_connect(text_view_, "focus-out-event", + G_CALLBACK(&HandleViewFocusOutThunk), this); + // NOTE: The GtkTextView documentation asks you not to connect to this + // signal, but it is very convenient and clean for catching up/down. + g_signal_connect(text_view_, "move-cursor", + G_CALLBACK(&HandleViewMoveCursorThunk), this); + g_signal_connect(text_view_, "move-focus", + G_CALLBACK(&HandleViewMoveFocusThunk), this); + // Override the size request. We want to keep the original height request + // from the widget, since that's font dependent. We want to ignore the width + // so we don't force a minimum width based on the text length. + g_signal_connect(text_view_, "size-request", + G_CALLBACK(&HandleViewSizeRequestThunk), this); + g_signal_connect(text_view_, "populate-popup", + G_CALLBACK(&HandlePopulatePopupThunk), this); + mark_set_handler_id_ = g_signal_connect( + text_buffer_, "mark-set", G_CALLBACK(&HandleMarkSetThunk), this); + mark_set_handler_id2_ = g_signal_connect_after( + text_buffer_, "mark-set", G_CALLBACK(&HandleMarkSetAfterThunk), this); + g_signal_connect(text_view_, "drag-data-received", + G_CALLBACK(&HandleDragDataReceivedThunk), this); + // Override the text_view_'s default drag-data-get handler by calling our own + // version after the normal call has happened. + g_signal_connect_after(text_view_, "drag-data-get", + G_CALLBACK(&HandleDragDataGetThunk), this); + g_signal_connect(text_view_, "backspace", + G_CALLBACK(&HandleBackSpaceThunk), this); + g_signal_connect(text_view_, "copy-clipboard", + G_CALLBACK(&HandleCopyClipboardThunk), this); + g_signal_connect(text_view_, "cut-clipboard", + G_CALLBACK(&HandleCutClipboardThunk), this); + g_signal_connect(text_view_, "paste-clipboard", + G_CALLBACK(&HandlePasteClipboardThunk), this); + g_signal_connect_after(text_view_, "expose-event", + G_CALLBACK(&HandleExposeEventThunk), this); + g_signal_connect(text_view_, "direction-changed", + G_CALLBACK(&HandleWidgetDirectionChangedThunk), this); + g_signal_connect(text_view_, "delete-from-cursor", + G_CALLBACK(&HandleDeleteFromCursorThunk), this); + g_signal_connect(text_view_, "hierarchy-changed", + G_CALLBACK(&HandleHierarchyChangedThunk), this); +#if GTK_CHECK_VERSION(2, 20, 0) + g_signal_connect(text_view_, "preedit-changed", + G_CALLBACK(&HandlePreeditChangedThunk), this); +#endif + g_signal_connect(text_view_, "undo", G_CALLBACK(&HandleUndoRedoThunk), this); + g_signal_connect(text_view_, "redo", G_CALLBACK(&HandleUndoRedoThunk), this); + g_signal_connect_after(text_view_, "undo", + G_CALLBACK(&HandleUndoRedoAfterThunk), this); + g_signal_connect_after(text_view_, "redo", + G_CALLBACK(&HandleUndoRedoAfterThunk), this); + g_signal_connect(text_view_, "destroy", + G_CALLBACK(>k_widget_destroyed), &text_view_); + + // Setup for the Instant suggestion text view. + // GtkLabel is used instead of GtkTextView to get transparent background. + instant_view_ = gtk_label_new(NULL); + gtk_widget_set_no_show_all(instant_view_, TRUE); + gtk_label_set_selectable(GTK_LABEL(instant_view_), TRUE); + + GtkTextIter end_iter; + gtk_text_buffer_get_end_iter(text_buffer_, &end_iter); + + // Insert a Zero Width Space character just before the instant anchor. + // It's a hack to workaround a bug of GtkTextView which can not align the + // preedit string and a child anchor correctly when there is no other content + // around the preedit string. + gtk_text_buffer_insert(text_buffer_, &end_iter, "\342\200\213", -1); + GtkTextChildAnchor* instant_anchor = + gtk_text_buffer_create_child_anchor(text_buffer_, &end_iter); + + gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(text_view_), + instant_view_, + instant_anchor); + + instant_anchor_tag_ = gtk_text_buffer_create_tag(text_buffer_, NULL, NULL); + + GtkTextIter anchor_iter; + gtk_text_buffer_get_iter_at_child_anchor(text_buffer_, &anchor_iter, + instant_anchor); + gtk_text_buffer_apply_tag(text_buffer_, instant_anchor_tag_, + &anchor_iter, &end_iter); + + GtkTextIter start_iter; + gtk_text_buffer_get_start_iter(text_buffer_, &start_iter); + instant_mark_ = + gtk_text_buffer_create_mark(text_buffer_, NULL, &start_iter, FALSE); + + // Hooking up this handler after setting up above hacks for Instant view, so + // that we won't filter out the special ZWP mark itself. + g_signal_connect(text_buffer_, "insert-text", + G_CALLBACK(&HandleInsertTextThunk), this); + + AdjustVerticalAlignmentOfInstantView(); + + ui::MultiAnimation::Parts parts; + parts.push_back(ui::MultiAnimation::Part( + InstantController::kAutoCommitPauseTimeMS, ui::Tween::ZERO)); + parts.push_back(ui::MultiAnimation::Part( + InstantController::kAutoCommitFadeInTimeMS, ui::Tween::EASE_IN)); + instant_animation_.reset(new ui::MultiAnimation(parts)); + instant_animation_->set_continuous(false); + +#if !defined(TOOLKIT_VIEWS) + registrar_.Add(this, + NotificationType::BROWSER_THEME_CHANGED, + NotificationService::AllSources()); + theme_service_->InitThemesFor(this); +#else + // Manually invoke SetBaseColor() because TOOLKIT_VIEWS doesn't observe + // themes. + SetBaseColor(); +#endif + + ViewIDUtil::SetID(GetNativeView(), VIEW_ID_AUTOCOMPLETE); +} + +void OmniboxViewGtk::HandleHierarchyChanged(GtkWidget* sender, + GtkWidget* old_toplevel) { + GtkWindow* new_toplevel = platform_util::GetTopLevel(sender); + if (!new_toplevel) + return; + + // Use |signals_| to make sure we don't get called back after destruction. + signals_.Connect(new_toplevel, "set-focus", + G_CALLBACK(&HandleWindowSetFocusThunk), this); +} + +void OmniboxViewGtk::SetFocus() { + DCHECK(text_view_); + gtk_widget_grab_focus(text_view_); +} + +int OmniboxViewGtk::WidthOfTextAfterCursor() { + // Not used. + return -1; +} + +AutocompleteEditModel* OmniboxViewGtk::model() { + return model_.get(); +} + +const AutocompleteEditModel* OmniboxViewGtk::model() const { + return model_.get(); +} + +void OmniboxViewGtk::SaveStateToTab(TabContents* tab) { + DCHECK(tab); + // If any text has been selected, register it as the PRIMARY selection so it + // can still be pasted via middle-click after the text view is cleared. + if (!selected_text_.empty()) + SavePrimarySelection(selected_text_); + // NOTE: GetStateForTabSwitch may affect GetSelection, so order is important. + AutocompleteEditModel::State model_state = model_->GetStateForTabSwitch(); + GetStateAccessor()->SetProperty( + tab->property_bag(), + AutocompleteEditState(model_state, ViewState(GetSelection()))); +} + +void OmniboxViewGtk::Update(const TabContents* contents) { + // NOTE: We're getting the URL text here from the ToolbarModel. + bool visibly_changed_permanent_text = + model_->UpdatePermanentText(WideToUTF16Hack(toolbar_model_->GetText())); + + ToolbarModel::SecurityLevel security_level = + toolbar_model_->GetSecurityLevel(); + bool changed_security_level = (security_level != security_level_); + security_level_ = security_level; + + if (contents) { + selected_text_.clear(); + RevertAll(); + const AutocompleteEditState* state = + GetStateAccessor()->GetProperty(contents->property_bag()); + if (state) { + model_->RestoreState(state->model_state); + + // Move the marks for the cursor and the other end of the selection to + // the previously-saved offsets (but preserve PRIMARY). + StartUpdatingHighlightedText(); + SetSelectedRange(state->view_state.selection_range); + FinishUpdatingHighlightedText(); + } + } else if (visibly_changed_permanent_text) { + RevertAll(); + // TODO(deanm): There should be code to restore select all here. + } else if (changed_security_level) { + EmphasizeURLComponents(); + } +} + +void OmniboxViewGtk::OpenURL(const GURL& url, + WindowOpenDisposition disposition, + PageTransition::Type transition, + const GURL& alternate_nav_url, + size_t selected_line, + const string16& keyword) { + if (!url.is_valid()) + return; + + model_->OpenURL(url, disposition, transition, alternate_nav_url, + selected_line, keyword); +} + +string16 OmniboxViewGtk::GetText() const { + GtkTextIter start, end; + GetTextBufferBounds(&start, &end); + gchar* utf8 = gtk_text_buffer_get_text(text_buffer_, &start, &end, false); + string16 out(UTF8ToUTF16(utf8)); + g_free(utf8); + +#if GTK_CHECK_VERSION(2, 20, 0) + // We need to treat the text currently being composed by the input method as + // part of the text content, so that omnibox can work correctly in the middle + // of composition. + if (preedit_.size()) { + GtkTextMark* mark = gtk_text_buffer_get_insert(text_buffer_); + gtk_text_buffer_get_iter_at_mark(text_buffer_, &start, mark); + out.insert(gtk_text_iter_get_offset(&start), preedit_); + } +#endif + return out; +} + +bool OmniboxViewGtk::IsEditingOrEmpty() const { + return model_->user_input_in_progress() || (GetTextLength() == 0); +} + +int OmniboxViewGtk::GetIcon() const { + return IsEditingOrEmpty() ? + AutocompleteMatch::TypeToIcon(model_->CurrentTextType()) : + toolbar_model_->GetIcon(); +} + +void OmniboxViewGtk::SetUserText(const string16& text) { + SetUserText(text, text, true); +} + +void OmniboxViewGtk::SetUserText(const string16& text, + const string16& display_text, + bool update_popup) { + model_->SetUserText(text); + // TODO(deanm): something about selection / focus change here. + SetWindowTextAndCaretPos(display_text, display_text.length()); + if (update_popup) + UpdatePopup(); + TextChanged(); +} + +void OmniboxViewGtk::SetWindowTextAndCaretPos(const string16& text, + size_t caret_pos) { + CharRange range(static_cast<int>(caret_pos), static_cast<int>(caret_pos)); + SetTextAndSelectedRange(text, range); +} + +void OmniboxViewGtk::SetForcedQuery() { + const string16 current_text(GetText()); + const size_t start = current_text.find_first_not_of(kWhitespaceUTF16); + if (start == string16::npos || (current_text[start] != '?')) { + SetUserText(ASCIIToUTF16("?")); + } else { + StartUpdatingHighlightedText(); + SetSelectedRange(CharRange(current_text.size(), start + 1)); + FinishUpdatingHighlightedText(); + } +} + +bool OmniboxViewGtk::IsSelectAll() { + GtkTextIter sel_start, sel_end; + gtk_text_buffer_get_selection_bounds(text_buffer_, &sel_start, &sel_end); + + GtkTextIter start, end; + GetTextBufferBounds(&start, &end); + + // Returns true if the |text_buffer_| is empty. + return gtk_text_iter_equal(&start, &sel_start) && + gtk_text_iter_equal(&end, &sel_end); +} + +bool OmniboxViewGtk::DeleteAtEndPressed() { + return delete_at_end_pressed_; +} + +void OmniboxViewGtk::GetSelectionBounds(string16::size_type* start, + string16::size_type* end) { + CharRange selection = GetSelection(); + *start = static_cast<size_t>(selection.cp_min); + *end = static_cast<size_t>(selection.cp_max); +} + +void OmniboxViewGtk::SelectAll(bool reversed) { + // SelectAll() is invoked as a side effect of other actions (e.g. switching + // tabs or hitting Escape) in autocomplete_edit.cc, so we don't update the + // PRIMARY selection here. + SelectAllInternal(reversed, false); +} + +void OmniboxViewGtk::RevertAll() { + ClosePopup(); + model_->Revert(); + TextChanged(); +} + +void OmniboxViewGtk::UpdatePopup() { + model_->SetInputInProgress(true); + if (!update_popup_without_focus_ && !model_->has_focus()) + return; + + // Don't inline autocomplete when the caret/selection isn't at the end of + // the text, or in the middle of composition. + CharRange sel = GetSelection(); + bool no_inline_autocomplete = + std::max(sel.cp_max, sel.cp_min) < GetTextLength() || IsImeComposing(); + model_->StartAutocomplete(sel.cp_min != sel.cp_max, no_inline_autocomplete); +} + +void OmniboxViewGtk::ClosePopup() { + model_->StopAutocomplete(); +} + +void OmniboxViewGtk::OnTemporaryTextMaybeChanged( + const string16& display_text, + bool save_original_selection) { + if (save_original_selection) + saved_temporary_selection_ = GetSelection(); + + StartUpdatingHighlightedText(); + SetWindowTextAndCaretPos(display_text, display_text.length()); + FinishUpdatingHighlightedText(); + TextChanged(); +} + +bool OmniboxViewGtk::OnInlineAutocompleteTextMaybeChanged( + const string16& display_text, + size_t user_text_length) { + if (display_text == GetText()) + return false; + + StartUpdatingHighlightedText(); + CharRange range(display_text.size(), user_text_length); + SetTextAndSelectedRange(display_text, range); + FinishUpdatingHighlightedText(); + TextChanged(); + return true; +} + +void OmniboxViewGtk::OnRevertTemporaryText() { + StartUpdatingHighlightedText(); + SetSelectedRange(saved_temporary_selection_); + FinishUpdatingHighlightedText(); + TextChanged(); +} + +void OmniboxViewGtk::OnBeforePossibleChange() { + // Record this paste, so we can do different behavior. + if (paste_clipboard_requested_) { + paste_clipboard_requested_ = false; + model_->on_paste(); + } + + // This method will be called in HandleKeyPress() method just before + // handling a key press event. So we should prevent it from being called + // when handling the key press event. + if (handling_key_press_) + return; + + // Record our state. + text_before_change_ = GetText(); + sel_before_change_ = GetSelection(); +#if GTK_CHECK_VERSION(2, 20, 0) + preedit_size_before_change_ = preedit_.size(); +#endif +} + +// TODO(deanm): This is mostly stolen from Windows, and will need some work. +bool OmniboxViewGtk::OnAfterPossibleChange() { + // This method will be called in HandleKeyPress() method just after + // handling a key press event. So we should prevent it from being called + // when handling the key press event. + if (handling_key_press_) { + content_maybe_changed_by_key_press_ = true; + return false; + } + + // If the change is caused by an Enter key press event, and the event was not + // handled by IME, then it's an unexpected change and shall be reverted here. + // {Start|Finish}UpdatingHighlightedText() are called here to prevent the + // PRIMARY selection from being changed. + if (enter_was_pressed_ && enter_was_inserted_) { + StartUpdatingHighlightedText(); + SetTextAndSelectedRange(text_before_change_, sel_before_change_); + FinishUpdatingHighlightedText(); + return false; + } + + const CharRange new_sel = GetSelection(); + const int length = GetTextLength(); + const bool selection_differs = + ((new_sel.cp_min != new_sel.cp_max) || + (sel_before_change_.cp_min != sel_before_change_.cp_max)) && + ((new_sel.cp_min != sel_before_change_.cp_min) || + (new_sel.cp_max != sel_before_change_.cp_max)); + const bool at_end_of_edit = + (new_sel.cp_min == length && new_sel.cp_max == length); + + // See if the text or selection have changed since OnBeforePossibleChange(). + const string16 new_text(GetText()); + text_changed_ = (new_text != text_before_change_); +#if GTK_CHECK_VERSION(2, 20, 0) + text_changed_ = + text_changed_ || (preedit_.size() != preedit_size_before_change_); +#endif + + if (text_changed_) + AdjustTextJustification(); + + // When the user has deleted text, we don't allow inline autocomplete. Make + // sure to not flag cases like selecting part of the text and then pasting + // (or typing) the prefix of that selection. (We detect these by making + // sure the caret, which should be after any insertion, hasn't moved + // forward of the old selection start.) + const bool just_deleted_text = + (text_before_change_.length() > new_text.length()) && + (new_sel.cp_min <= std::min(sel_before_change_.cp_min, + sel_before_change_.cp_max)); + + delete_at_end_pressed_ = false; + + const bool something_changed = model_->OnAfterPossibleChange( + new_text, new_sel.selection_min(), new_sel.selection_max(), + selection_differs, text_changed_, just_deleted_text, + !IsImeComposing()); + + // If only selection was changed, we don't need to call |controller_|'s + // OnChanged() method, which is called in TextChanged(). + // But we still need to call EmphasizeURLComponents() to make sure the text + // attributes are updated correctly. + if (something_changed && text_changed_) { + TextChanged(); + } else if (selection_differs) { + EmphasizeURLComponents(); + } else if (delete_was_pressed_ && at_end_of_edit) { + delete_at_end_pressed_ = true; + model_->OnChanged(); + } + delete_was_pressed_ = false; + + return something_changed; +} + +gfx::NativeView OmniboxViewGtk::GetNativeView() const { + return alignment_.get(); +} + +CommandUpdater* OmniboxViewGtk::GetCommandUpdater() { + return command_updater_; +} + +void OmniboxViewGtk::SetInstantSuggestion(const string16& suggestion, + bool animate_to_complete) { + std::string suggestion_utf8 = UTF16ToUTF8(suggestion); + + gtk_label_set_text(GTK_LABEL(instant_view_), suggestion_utf8.c_str()); + + StopAnimation(); + + if (suggestion.empty()) { + gtk_widget_hide(instant_view_); + return; + } + if (animate_to_complete +#if GTK_CHECK_VERSION(2, 20, 0) + && preedit_.empty() +#endif + ) { + instant_animation_->set_delegate(this); + instant_animation_->Start(); + } + + gtk_widget_show(instant_view_); + AdjustVerticalAlignmentOfInstantView(); + UpdateInstantViewColors(); +} + +string16 OmniboxViewGtk::GetInstantSuggestion() const { + const gchar* suggestion = gtk_label_get_text(GTK_LABEL(instant_view_)); + return suggestion ? UTF8ToUTF16(suggestion) : string16(); +} + +int OmniboxViewGtk::TextWidth() const { + // TextWidth may be called after gtk widget tree is destroyed but + // before OmniboxViewGtk gets deleted. This is a safe guard + // to avoid accessing |text_view_| that has already been destroyed. + // See crbug.com/70192. + if (!text_view_) + return 0; + + int horizontal_border_size = + gtk_text_view_get_border_window_size(GTK_TEXT_VIEW(text_view_), + GTK_TEXT_WINDOW_LEFT) + + gtk_text_view_get_border_window_size(GTK_TEXT_VIEW(text_view_), + GTK_TEXT_WINDOW_RIGHT) + + gtk_text_view_get_left_margin(GTK_TEXT_VIEW(text_view_)) + + gtk_text_view_get_right_margin(GTK_TEXT_VIEW(text_view_)); + + GtkTextIter start, end; + GdkRectangle first_char_bounds, last_char_bounds; + gtk_text_buffer_get_start_iter(text_buffer_, &start); + + // Use the real end iterator here to take the width of instant suggestion + // text into account, so that location bar can layout its children correctly. + gtk_text_buffer_get_end_iter(text_buffer_, &end); + gtk_text_view_get_iter_location(GTK_TEXT_VIEW(text_view_), + &start, &first_char_bounds); + gtk_text_view_get_iter_location(GTK_TEXT_VIEW(text_view_), + &end, &last_char_bounds); + + gint first_char_start = first_char_bounds.x; + gint first_char_end = first_char_start + first_char_bounds.width; + gint last_char_start = last_char_bounds.x; + gint last_char_end = last_char_start + last_char_bounds.width; + + // bounds width could be negative for RTL text. + if (first_char_start > first_char_end) + std::swap(first_char_start, first_char_end); + if (last_char_start > last_char_end) + std::swap(last_char_start, last_char_end); + + gint text_width = first_char_start < last_char_start ? + last_char_end - first_char_start : first_char_end - last_char_start; + + return text_width + horizontal_border_size; +} + +bool OmniboxViewGtk::IsImeComposing() const { +#if GTK_CHECK_VERSION(2, 20, 0) + return !preedit_.empty(); +#else + return false; +#endif +} + +#if defined(TOOLKIT_VIEWS) +views::View* OmniboxViewGtk::AddToView(views::View* parent) { + views::NativeViewHost* host = new views::NativeViewHost; + parent->AddChildView(host); + host->set_focus_view(parent); + host->Attach(GetNativeView()); + return host; +} + +int OmniboxViewGtk::OnPerformDrop( + const views::DropTargetEvent& event) { + string16 text; + const ui::OSExchangeData& data = event.data(); + if (data.HasURL()) { + GURL url; + string16 title; + if (data.GetURLAndTitle(&url, &title)) + text = UTF8ToUTF16(url.spec()); + } else { + string16 data_string; + if (data.GetString(&data_string)) + text = CollapseWhitespace(data_string, true); + } + + if (!text.empty() && OnPerformDropImpl(text)) + return CopyOrLinkDragOperation(event.source_operations()); + + return ui::DragDropTypes::DRAG_NONE; +} + +// static +AutocompleteEditView* OmniboxViewGtk::Create( + AutocompleteEditController* controller, + ToolbarModel* toolbar_model, + Profile* profile, + CommandUpdater* command_updater, + bool popup_window_mode, + views::View* location_bar) { + if (views::NativeTextfieldViews::IsTextfieldViewsEnabled()) { + AutocompleteEditViewViews* autocomplete = + new AutocompleteEditViewViews(controller, + toolbar_model, + profile, + command_updater, + popup_window_mode, + location_bar); + autocomplete->Init(); + return autocomplete; + } + + OmniboxViewGtk* autocomplete = new OmniboxViewGtk(controller, + toolbar_model, + profile, + command_updater, + popup_window_mode, + location_bar); + autocomplete->Init(); + + // Make all the children of the widget visible. NOTE: this won't display + // anything, it just toggles the visible flag. + gtk_widget_show_all(autocomplete->GetNativeView()); + // Hide the widget. NativeViewHostGtk will make it visible again as + // necessary. + gtk_widget_hide(autocomplete->GetNativeView()); + + return autocomplete; +} +#endif + +void OmniboxViewGtk::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + DCHECK(type == NotificationType::BROWSER_THEME_CHANGED); + + SetBaseColor(); +} + +void OmniboxViewGtk::AnimationEnded(const ui::Animation* animation) { + model_->CommitSuggestedText(false); +} + +void OmniboxViewGtk::AnimationProgressed(const ui::Animation* animation) { + UpdateInstantViewColors(); +} + +void OmniboxViewGtk::AnimationCanceled(const ui::Animation* animation) { + UpdateInstantViewColors(); +} + +void OmniboxViewGtk::SetBaseColor() { + DCHECK(text_view_); + +#if defined(TOOLKIT_VIEWS) + bool use_gtk = false; +#else + bool use_gtk = theme_service_->UseGtkTheme(); +#endif + if (use_gtk) { + gtk_widget_modify_cursor(text_view_, NULL, NULL); + gtk_widget_modify_base(text_view_, GTK_STATE_NORMAL, NULL); + gtk_widget_modify_base(text_view_, GTK_STATE_SELECTED, NULL); + gtk_widget_modify_text(text_view_, GTK_STATE_SELECTED, NULL); + gtk_widget_modify_base(text_view_, GTK_STATE_ACTIVE, NULL); + gtk_widget_modify_text(text_view_, GTK_STATE_ACTIVE, NULL); + + gtk_util::UndoForceFontSize(text_view_); + gtk_util::UndoForceFontSize(instant_view_); + + // Grab the text colors out of the style and set our tags to use them. + GtkStyle* style = gtk_rc_get_style(text_view_); + + // style may be unrealized at this point, so calculate the halfway point + // between text[] and base[] manually instead of just using text_aa[]. + GdkColor average_color = gtk_util::AverageColors( + style->text[GTK_STATE_NORMAL], style->base[GTK_STATE_NORMAL]); + + g_object_set(faded_text_tag_, "foreground-gdk", &average_color, NULL); + g_object_set(normal_text_tag_, "foreground-gdk", + &style->text[GTK_STATE_NORMAL], NULL); + } else { + const GdkColor* background_color_ptr; +#if defined(TOOLKIT_VIEWS) + const GdkColor background_color = gfx::SkColorToGdkColor( + LocationBarView::GetColor(ToolbarModel::NONE, + LocationBarView::BACKGROUND)); + background_color_ptr = &background_color; +#else + background_color_ptr = &LocationBarViewGtk::kBackgroundColor; +#endif + gtk_widget_modify_cursor( + text_view_, >k_util::kGdkBlack, >k_util::kGdkGray); + gtk_widget_modify_base(text_view_, GTK_STATE_NORMAL, background_color_ptr); + +#if !defined(TOOLKIT_VIEWS) + GdkColor c; + // Override the selected colors so we don't leak colors from the current + // gtk theme into the chrome-theme. + c = gfx::SkColorToGdkColor( + theme_service_->get_active_selection_bg_color()); + gtk_widget_modify_base(text_view_, GTK_STATE_SELECTED, &c); + + c = gfx::SkColorToGdkColor( + theme_service_->get_active_selection_fg_color()); + gtk_widget_modify_text(text_view_, GTK_STATE_SELECTED, &c); + + c = gfx::SkColorToGdkColor( + theme_service_->get_inactive_selection_bg_color()); + gtk_widget_modify_base(text_view_, GTK_STATE_ACTIVE, &c); + + c = gfx::SkColorToGdkColor( + theme_service_->get_inactive_selection_fg_color()); + gtk_widget_modify_text(text_view_, GTK_STATE_ACTIVE, &c); +#endif + + // Until we switch to vector graphics, force the font size. + gtk_util::ForceFontSizePixels(text_view_, GetFont().GetFontSize()); + gtk_util::ForceFontSizePixels(instant_view_, GetFont().GetFontSize()); + + g_object_set(faded_text_tag_, "foreground", kTextBaseColor, NULL); + g_object_set(normal_text_tag_, "foreground", "#000000", NULL); + } + + AdjustVerticalAlignmentOfInstantView(); + UpdateInstantViewColors(); +} + +void OmniboxViewGtk::UpdateInstantViewColors() { + SkColor selection_text, selection_bg; + GdkColor faded_text, normal_bg; + +#if defined(TOOLKIT_VIEWS) + bool use_gtk = false; +#else + bool use_gtk = theme_service_->UseGtkTheme(); +#endif + + if (use_gtk) { + GtkStyle* style = gtk_rc_get_style(instant_view_); + + faded_text = gtk_util::AverageColors( + style->text[GTK_STATE_NORMAL], style->base[GTK_STATE_NORMAL]); + normal_bg = style->base[GTK_STATE_NORMAL]; + + selection_text = gfx::GdkColorToSkColor(style->text[GTK_STATE_SELECTED]); + selection_bg = gfx::GdkColorToSkColor(style->base[GTK_STATE_SELECTED]); + } else { + gdk_color_parse(kTextBaseColor, &faded_text); + +#if defined(TOOLKIT_VIEWS) + normal_bg = gfx::SkColorToGdkColor( + LocationBarView::GetColor(ToolbarModel::NONE, + LocationBarView::BACKGROUND)); + selection_text = LocationBarView::GetColor( + ToolbarModel::NONE, LocationBarView::SELECTED_TEXT); + + GtkStyle* style = gtk_rc_get_style(instant_view_); + selection_bg = gfx::GdkColorToSkColor(style->base[GTK_STATE_SELECTED]); +#else + normal_bg = LocationBarViewGtk::kBackgroundColor; + selection_text = + theme_service_->get_active_selection_fg_color(); + selection_bg = + theme_service_->get_active_selection_bg_color(); +#endif + } + + double alpha = instant_animation_->is_animating() ? + instant_animation_->GetCurrentValue() : 0.0; + GdkColor text = gfx::SkColorToGdkColor(color_utils::AlphaBlend( + selection_text, + gfx::GdkColorToSkColor(faded_text), + alpha * 0xff)); + GdkColor bg = gfx::SkColorToGdkColor(color_utils::AlphaBlend( + selection_bg, + gfx::GdkColorToSkColor(normal_bg), + alpha * 0xff)); + + if (alpha > 0.0) { + gtk_label_select_region(GTK_LABEL(instant_view_), 0, -1); + // ACTIVE is the state for text that is selected, but not focused. + gtk_widget_modify_text(instant_view_, GTK_STATE_ACTIVE, &text); + gtk_widget_modify_base(instant_view_, GTK_STATE_ACTIVE, &bg); + } else { + // When the text is unselected, fg is used for text color, the state + // is NORMAL, and the background is transparent. + gtk_widget_modify_fg(instant_view_, GTK_STATE_NORMAL, &text); + } +} + +void OmniboxViewGtk::HandleBeginUserAction(GtkTextBuffer* sender) { + OnBeforePossibleChange(); +} + +void OmniboxViewGtk::HandleEndUserAction(GtkTextBuffer* sender) { + OnAfterPossibleChange(); +} + +gboolean OmniboxViewGtk::HandleKeyPress(GtkWidget* widget, GdkEventKey* event) { + // Background of this piece of complicated code: + // The omnibox supports several special behaviors which may be triggered by + // certain key events: + // Tab to search - triggered by Tab key + // Accept input - triggered by Enter key + // Revert input - triggered by Escape key + // + // Because we use a GtkTextView object |text_view_| for text input, we need + // send all key events to |text_view_| before handling them, to make sure + // IME works without any problem. So here, we intercept "key-press-event" + // signal of |text_view_| object and call its default handler to handle the + // key event first. + // + // Then if the key event is one of Tab, Enter and Escape, we need to trigger + // the corresponding special behavior if IME did not handle it. + // For Escape key, if the default signal handler returns FALSE, then we know + // it's not handled by IME. + // + // For Tab key, as "accepts-tab" property of |text_view_| is set to FALSE, + // if IME did not handle it then "move-focus" signal will be emitted by the + // default signal handler of |text_view_|. So we can intercept "move-focus" + // signal of |text_view_| to know if a Tab key press event was handled by IME, + // and trigger Tab to search behavior when necessary in the signal handler. + // + // But for Enter key, if IME did not handle the key event, the default signal + // handler will delete current selection range and insert '\n' and always + // return TRUE. We need to prevent |text_view_| from performing this default + // action if IME did not handle the key event, because we don't want the + // content of omnibox to be changed before triggering our special behavior. + // Otherwise our special behavior would not be performed correctly. + // + // But there is no way for us to prevent GtkTextView from handling the key + // event and performing built-in operation. So in order to achieve our goal, + // "insert-text" signal of |text_buffer_| object is intercepted, and + // following actions are done in the signal handler: + // - If there is only one character in inserted text, and it's '\n' or '\r', + // then set |enter_was_inserted_| to true. + // - Filter out all new line and tab characters. + // + // So if |enter_was_inserted_| is true after calling |text_view_|'s default + // signal handler against an Enter key press event, then we know that the + // Enter key press event was handled by GtkTextView rather than IME, and can + // perform the special behavior for Enter key safely. + // + // Now the last thing is to prevent the content of omnibox from being changed + // by GtkTextView when Enter key is pressed. As OnBeforePossibleChange() and + // OnAfterPossibleChange() will be called by GtkTextView before and after + // changing the content, and the content is already saved in + // OnBeforePossibleChange(), so if the Enter key press event was not handled + // by IME, it's easy to restore the content in OnAfterPossibleChange(), as if + // it's not changed at all. + + GtkWidgetClass* klass = GTK_WIDGET_GET_CLASS(widget); + + enter_was_pressed_ = event->keyval == GDK_Return || + event->keyval == GDK_ISO_Enter || + event->keyval == GDK_KP_Enter; + + // Set |tab_was_pressed_| to true if it's a Tab key press event, so that our + // handler of "move-focus" signal can trigger Tab to search behavior when + // necessary. + tab_was_pressed_ = (event->keyval == GDK_Tab || + event->keyval == GDK_ISO_Left_Tab || + event->keyval == GDK_KP_Tab) && + !(event->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)); + + delete_was_pressed_ = event->keyval == GDK_Delete || + event->keyval == GDK_KP_Delete; + + // Reset |enter_was_inserted_|, which may be set in the "insert-text" signal + // handler, so that we'll know if an Enter key event was handled by IME. + enter_was_inserted_ = false; + + // Reset |paste_clipboard_requested_| to make sure we won't misinterpret this + // key input action as a paste action. + paste_clipboard_requested_ = false; + + // Reset |text_changed_| before passing the key event on to the text view. + text_changed_ = false; + + OnBeforePossibleChange(); + handling_key_press_ = true; + content_maybe_changed_by_key_press_ = false; + + // Call the default handler, so that IME can work as normal. + // New line characters will be filtered out by our "insert-text" + // signal handler attached to |text_buffer_| object. + gboolean result = klass->key_press_event(widget, event); + + handling_key_press_ = false; + if (content_maybe_changed_by_key_press_) + OnAfterPossibleChange(); + + // Set |tab_was_pressed_| to false, to make sure Tab to search behavior can + // only be triggered by pressing Tab key. + tab_was_pressed_ = false; + + if (enter_was_pressed_ && enter_was_inserted_) { + bool alt_held = (event->state & GDK_MOD1_MASK); + model_->AcceptInput(alt_held ? NEW_FOREGROUND_TAB : CURRENT_TAB, false); + result = TRUE; + } else if (!result && event->keyval == GDK_Escape && + (event->state & gtk_accelerator_get_default_mod_mask()) == 0) { + // We can handle the Escape key if |text_view_| did not handle it. + // If it's not handled by us, then we need to propagate it up to the parent + // widgets, so that Escape accelerator can still work. + result = model_->OnEscapeKeyPressed(); + } else if (event->keyval == GDK_Control_L || event->keyval == GDK_Control_R) { + // Omnibox2 can switch its contents while pressing a control key. To switch + // the contents of omnibox2, we notify the AutocompleteEditModel class when + // the control-key state is changed. + model_->OnControlKeyChanged(true); + } else if (!text_changed_ && event->keyval == GDK_Delete && + event->state & GDK_SHIFT_MASK) { + // If shift+del didn't change the text, we let this delete an entry from + // the popup. We can't check to see if the IME handled it because even if + // nothing is selected, the IME or the TextView still report handling it. + if (model_->popup_model()->IsOpen()) + model_->popup_model()->TryDeletingCurrentItem(); + } + + // Set |enter_was_pressed_| to false, to make sure OnAfterPossibleChange() can + // act as normal for changes made by other events. + enter_was_pressed_ = false; + + // If the key event is not handled by |text_view_| or us, then we need to + // propagate the key event up to parent widgets by returning FALSE. + // In this case we need to stop the signal emission explicitly to prevent the + // default "key-press-event" handler of |text_view_| from being called again. + if (!result) { + static guint signal_id = + g_signal_lookup("key-press-event", GTK_TYPE_WIDGET); + g_signal_stop_emission(widget, signal_id, 0); + } + +#if defined(TOOLKIT_VIEWS) + location_bar_view_->GetWidget()->NotifyAccessibilityEvent( + location_bar_view_, ui::AccessibilityTypes::EVENT_TEXT_CHANGED, true); +#endif + + return result; +} + +gboolean OmniboxViewGtk::HandleKeyRelease(GtkWidget* widget, + GdkEventKey* event) { + // Omnibox2 can switch its contents while pressing a control key. To switch + // the contents of omnibox2, we notify the AutocompleteEditModel class when + // the control-key state is changed. + if (event->keyval == GDK_Control_L || event->keyval == GDK_Control_R) { + // Round trip to query the control state after the release. This allows + // you to release one control key while still holding another control key. + GdkDisplay* display = gdk_drawable_get_display(event->window); + GdkModifierType mod; + gdk_display_get_pointer(display, NULL, NULL, NULL, &mod); + if (!(mod & GDK_CONTROL_MASK)) + model_->OnControlKeyChanged(false); + } + + // Even though we handled the press ourselves, let GtkTextView handle the + // release. It shouldn't do anything particularly interesting, but it will + // handle the IME work for us. + return FALSE; // Propagate into GtkTextView. +} + +gboolean OmniboxViewGtk::HandleViewButtonPress(GtkWidget* sender, + GdkEventButton* event) { + // We don't need to care about double and triple clicks. + if (event->type != GDK_BUTTON_PRESS) + return FALSE; + + DCHECK(text_view_); + + if (event->button == 1) { +#if defined(OS_CHROMEOS) + // When the first button is pressed, track some stuff that will help us + // determine whether we should select all of the text when the button is + // released. + button_1_pressed_ = true; + text_view_focused_before_button_press_ = GTK_WIDGET_HAS_FOCUS(text_view_); + text_selected_during_click_ = false; +#endif + + // Button press event may change the selection, we need to record the change + // and report it to |model_| later when button is released. + OnBeforePossibleChange(); + } else if (event->button == 2) { + // GtkTextView pastes PRIMARY selection with middle click. + // We can't call model_->on_paste_replacing_all() here, because the actual + // paste clipboard action may not be performed if the clipboard is empty. + paste_clipboard_requested_ = true; + } + return FALSE; +} + +gboolean OmniboxViewGtk::HandleViewButtonRelease(GtkWidget* sender, + GdkEventButton* event) { + if (event->button != 1) + return FALSE; + + DCHECK(text_view_); + +#if defined(OS_CHROMEOS) + button_1_pressed_ = false; +#endif + + // Call the GtkTextView default handler, ignoring the fact that it will + // likely have told us to stop propagating. We want to handle selection. + GtkWidgetClass* klass = GTK_WIDGET_GET_CLASS(text_view_); + klass->button_release_event(text_view_, event); + +#if defined(OS_CHROMEOS) + if (!text_view_focused_before_button_press_ && !text_selected_during_click_) { + // If this was a focusing click and the user didn't drag to highlight any + // text, select the full input and update the PRIMARY selection. + SelectAllInternal(false, true); + + // So we told the buffer where the cursor should be, but make sure to tell + // the view so it can scroll it to be visible if needed. + // NOTE: This function doesn't seem to like a count of 0, looking at the + // code it will skip an important loop. Use -1 to achieve the same. + GtkTextIter start, end; + GetTextBufferBounds(&start, &end); + gtk_text_view_move_visually(GTK_TEXT_VIEW(text_view_), &start, -1); + } +#endif + + // Inform |model_| about possible text selection change. + OnAfterPossibleChange(); + + return TRUE; // Don't continue, we called the default handler already. +} + +gboolean OmniboxViewGtk::HandleViewFocusIn(GtkWidget* sender, + GdkEventFocus* event) { + DCHECK(text_view_); + update_popup_without_focus_ = false; + + GdkModifierType modifiers; + gdk_window_get_pointer(text_view_->window, NULL, NULL, &modifiers); + model_->OnSetFocus((modifiers & GDK_CONTROL_MASK) != 0); + controller_->OnSetFocus(); + // TODO(deanm): Some keyword hit business, etc here. + + g_signal_connect( + gdk_keymap_get_for_display(gtk_widget_get_display(text_view_)), + "direction-changed", + G_CALLBACK(&HandleKeymapDirectionChangedThunk), this); + + AdjustTextJustification(); + + return FALSE; // Continue propagation. +} + +gboolean OmniboxViewGtk::HandleViewFocusOut(GtkWidget* sender, + GdkEventFocus* event) { + DCHECK(text_view_); + GtkWidget* view_getting_focus = NULL; + GtkWindow* toplevel = platform_util::GetTopLevel(sender); + if (gtk_window_is_active(toplevel)) + view_getting_focus = going_to_focus_; + + // This must be invoked before ClosePopup. + model_->OnWillKillFocus(view_getting_focus); + + // Close the popup. + ClosePopup(); + // Tell the model to reset itself. + model_->OnKillFocus(); + controller_->OnKillFocus(); + + g_signal_handlers_disconnect_by_func( + gdk_keymap_get_for_display(gtk_widget_get_display(text_view_)), + reinterpret_cast<gpointer>(&HandleKeymapDirectionChangedThunk), this); + + return FALSE; // Pass the event on to the GtkTextView. +} + +void OmniboxViewGtk::HandleViewMoveCursor( + GtkWidget* sender, + GtkMovementStep step, + gint count, + gboolean extend_selection) { + DCHECK(text_view_); + GtkTextIter sel_start, sel_end; + gboolean has_selection = + gtk_text_buffer_get_selection_bounds(text_buffer_, &sel_start, &sel_end); + bool handled = false; + + if (step == GTK_MOVEMENT_VISUAL_POSITIONS && !extend_selection && + (count == 1 || count == -1)) { + // We need to take the content direction into account when handling cursor + // movement, because the behavior of Left and Right key will be inverted if + // the direction is RTL. Although we should check the direction around the + // input caret, it's much simpler and good enough to check whole content's + // direction. + PangoDirection content_dir = GetContentDirection(); + gint count_towards_end = content_dir == PANGO_DIRECTION_RTL ? -1 : 1; + + // We want the GtkEntry behavior when you move the cursor while you have a + // selection. GtkTextView just drops the selection and moves the cursor, + // but instead we want to move the cursor to the appropiate end of the + // selection. + if (has_selection) { + // We have a selection and start / end are in ascending order. + // Cursor placement will remove the selection, so we need inform + // |model_| about this change by + // calling On{Before|After}PossibleChange() methods. + OnBeforePossibleChange(); + gtk_text_buffer_place_cursor( + text_buffer_, count == count_towards_end ? &sel_end : &sel_start); + OnAfterPossibleChange(); + handled = true; + } else if (count == count_towards_end && !IsCaretAtEnd()) { + handled = model_->CommitSuggestedText(true); + } + } else if (step == GTK_MOVEMENT_PAGES) { // Page up and down. + // Multiply by count for the direction (if we move too much that's ok). + model_->OnUpOrDownKeyPressed(model_->result().size() * count); + handled = true; + } else if (step == GTK_MOVEMENT_DISPLAY_LINES) { // Arrow up and down. + model_->OnUpOrDownKeyPressed(count); + handled = true; + } + + if (!handled) { + // Cursor movement may change the selection, we need to record the change + // and report it to |model_|. + if (has_selection || extend_selection) + OnBeforePossibleChange(); + + // Propagate into GtkTextView + GtkTextViewClass* klass = GTK_TEXT_VIEW_GET_CLASS(text_view_); + klass->move_cursor(GTK_TEXT_VIEW(text_view_), step, count, + extend_selection); + + if (has_selection || extend_selection) + OnAfterPossibleChange(); + } + + // move-cursor doesn't use a signal accumulator on the return value (it + // just ignores then), so we have to stop the propagation. + static guint signal_id = g_signal_lookup("move-cursor", GTK_TYPE_TEXT_VIEW); + g_signal_stop_emission(text_view_, signal_id, 0); +} + +void OmniboxViewGtk::HandleViewSizeRequest(GtkWidget* sender, + GtkRequisition* req) { + // Don't force a minimum width, but use the font-relative height. This is a + // run-first handler, so the default handler was already called. + req->width = 1; +} + +void OmniboxViewGtk::HandlePopupMenuDeactivate(GtkWidget* sender) { + // When the context menu appears, |text_view_|'s focus is lost. After an item + // is activated, the focus comes back to |text_view_|, but only after the + // check in UpdatePopup(). We set this flag to make UpdatePopup() aware that + // it will be receiving focus again. + if (!model_->has_focus()) + update_popup_without_focus_ = true; +} + +void OmniboxViewGtk::HandlePopulatePopup(GtkWidget* sender, GtkMenu* menu) { + GtkWidget* separator = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), separator); + gtk_widget_show(separator); + + // Search Engine menu item. + GtkWidget* search_engine_menuitem = gtk_menu_item_new_with_mnemonic( + gfx::ConvertAcceleratorsFromWindowsStyle( + l10n_util::GetStringUTF8(IDS_EDIT_SEARCH_ENGINES)).c_str()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), search_engine_menuitem); + g_signal_connect(search_engine_menuitem, "activate", + G_CALLBACK(HandleEditSearchEnginesThunk), this); + gtk_widget_set_sensitive(search_engine_menuitem, + command_updater_->IsCommandEnabled(IDC_EDIT_SEARCH_ENGINES)); + gtk_widget_show(search_engine_menuitem); + + // We need to update the paste and go controller before we know what text + // to show. We could do this all asynchronously, but it would be elaborate + // because we'd have to account for multiple menus showing, getting called + // back after shutdown, and similar issues. + GtkClipboard* x_clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); + gchar* text = gtk_clipboard_wait_for_text(x_clipboard); + string16 text_wstr = UTF8ToUTF16(text ? text : ""); + g_free(text); + + // Paste and Go menu item. + GtkWidget* paste_go_menuitem = gtk_menu_item_new_with_mnemonic( + gfx::ConvertAcceleratorsFromWindowsStyle( + l10n_util::GetStringUTF8(model_->is_paste_and_search() ? + IDS_PASTE_AND_SEARCH : IDS_PASTE_AND_GO)).c_str()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), paste_go_menuitem); + g_signal_connect(paste_go_menuitem, "activate", + G_CALLBACK(HandlePasteAndGoThunk), this); + gtk_widget_set_sensitive(paste_go_menuitem, + model_->CanPasteAndGo(text_wstr)); + gtk_widget_show(paste_go_menuitem); + + g_signal_connect(menu, "deactivate", + G_CALLBACK(HandlePopupMenuDeactivateThunk), this); +} + +void OmniboxViewGtk::HandleEditSearchEngines(GtkWidget* sender) { + command_updater_->ExecuteCommand(IDC_EDIT_SEARCH_ENGINES); +} + +void OmniboxViewGtk::HandlePasteAndGo(GtkWidget* sender) { + model_->PasteAndGo(); +} + +void OmniboxViewGtk::HandleMarkSet(GtkTextBuffer* buffer, + GtkTextIter* location, + GtkTextMark* mark) { + if (!text_buffer_ || buffer != text_buffer_) + return; + + if (mark != gtk_text_buffer_get_insert(text_buffer_) && + mark != gtk_text_buffer_get_selection_bound(text_buffer_)) { + return; + } + + StopAnimation(); + + // If we are here, that means the user may be changing the selection + selection_suggested_ = false; + + // Get the currently-selected text, if there is any. + std::string new_selected_text = GetSelectedText(); + +#if defined(OS_CHROMEOS) + // If the user just selected some text with the mouse (or at least while the + // mouse button was down), make sure that we won't blow their selection away + // later by selecting all of the text when the button is released. + if (button_1_pressed_ && !new_selected_text.empty()) + text_selected_during_click_ = true; +#endif + + // If we had some text selected earlier but it's no longer highlighted, we + // might need to save it now... + if (!selected_text_.empty() && new_selected_text.empty()) { + // ... but only if we currently own the selection. We want to manually + // update the selection when the text is unhighlighted because the user + // clicked in a blank area of the text view, but not when it's unhighlighted + // because another client or widget took the selection. (This handler gets + // called before the default handler, so as long as nobody else took the + // selection, the text buffer still owns it even if GTK is about to take it + // away in the default handler.) + GtkClipboard* clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY); + if (gtk_clipboard_get_owner(clipboard) == G_OBJECT(text_buffer_)) + SavePrimarySelection(selected_text_); + } + + selected_text_ = new_selected_text; +} + +// Override the primary selection the text buffer has set. This has to happen +// after the default handler for the "mark-set" signal. +void OmniboxViewGtk::HandleMarkSetAfter(GtkTextBuffer* buffer, + GtkTextIter* location, + GtkTextMark* mark) { + if (!text_buffer_ || buffer != text_buffer_) + return; + + // We should only update primary selection when the user changes the selection + // range. + if (mark != gtk_text_buffer_get_insert(text_buffer_) && + mark != gtk_text_buffer_get_selection_bound(text_buffer_)) { + return; + } + + UpdatePrimarySelectionIfValidURL(); +} + +// Just use the default behavior for DnD, except if the drop can be a PasteAndGo +// then override. +void OmniboxViewGtk::HandleDragDataReceived(GtkWidget* sender, + GdkDragContext* context, + gint x, + gint y, + GtkSelectionData* selection_data, + guint target_type, + guint time) { + DCHECK(text_view_); + + // Reset |paste_clipboard_requested_| to make sure we won't misinterpret this + // drop action as a paste action. + paste_clipboard_requested_ = false; + + // Don't try to PasteAndGo on drops originating from this omnibox. However, do + // allow default behavior for such drags. + if (context->source_window == text_view_->window) + return; + + guchar* text = gtk_selection_data_get_text(selection_data); + if (!text) + return; + + string16 possible_url = UTF8ToUTF16(reinterpret_cast<char*>(text)); + g_free(text); + if (OnPerformDropImpl(possible_url)) { + gtk_drag_finish(context, TRUE, TRUE, time); + + static guint signal_id = + g_signal_lookup("drag-data-received", GTK_TYPE_WIDGET); + g_signal_stop_emission(text_view_, signal_id, 0); + } +} + +void OmniboxViewGtk::HandleDragDataGet(GtkWidget* widget, + GdkDragContext* context, + GtkSelectionData* selection_data, + guint target_type, + guint time) { + DCHECK(text_view_); + // If GTK put the normal textual version of the selection in our drag data, + // put our doctored selection that might have the 'http://' prefix. Also, GTK + // is confused about signedness of its datatypes, leading to the weird switch + // statement (no set of casts fixes this). + switch (target_type) { + case GTK_TEXT_BUFFER_TARGET_INFO_TEXT: { + gtk_selection_data_set_text(selection_data, selected_text_.c_str(), -1); + } + } +} + +void OmniboxViewGtk::HandleInsertText(GtkTextBuffer* buffer, + GtkTextIter* location, + const gchar* text, + gint len) { + std::string filtered_text; + filtered_text.reserve(len); + + // Filter out new line and tab characters. + // |text| is guaranteed to be a valid UTF-8 string, so we don't need to + // validate it here. + // + // If there was only a single character, then it might be generated by a key + // event. In this case, we save the single character to help our + // "key-press-event" signal handler distinguish if an Enter key event is + // handled by IME or not. + if (len == 1 && (text[0] == '\n' || text[0] == '\r')) + enter_was_inserted_ = true; + + const gchar* p = text; + while (*p && (p - text) < len) { + gunichar c = g_utf8_get_char(p); + const gchar* next = g_utf8_next_char(p); + + // 0x200B is Zero Width Space, which is inserted just before the instant + // anchor for working around the GtkTextView's misalignment bug. + // This character might be captured and inserted into the content by undo + // manager, so we need to filter it out here. + if (c != L'\n' && c != L'\r' && c != L'\t' && c != 0x200B) + filtered_text.append(p, next); + + p = next; + } + + if (filtered_text.length()) { + // Avoid inserting the text after the instant anchor. + ValidateTextBufferIter(location); + + // Call the default handler to insert filtered text. + GtkTextBufferClass* klass = GTK_TEXT_BUFFER_GET_CLASS(buffer); + klass->insert_text(buffer, location, filtered_text.data(), + static_cast<gint>(filtered_text.length())); + } + + // Stop propagating the signal emission to prevent the default handler from + // being called again. + static guint signal_id = g_signal_lookup("insert-text", GTK_TYPE_TEXT_BUFFER); + g_signal_stop_emission(buffer, signal_id, 0); +} + +void OmniboxViewGtk::HandleBackSpace(GtkWidget* sender) { + // Checks if it's currently in keyword search mode. + if (model_->is_keyword_hint() || model_->keyword().empty()) + return; // Propgate into GtkTextView. + + DCHECK(text_view_); + + GtkTextIter sel_start, sel_end; + // Checks if there is some text selected. + if (gtk_text_buffer_get_selection_bounds(text_buffer_, &sel_start, &sel_end)) + return; // Propgate into GtkTextView. + + GtkTextIter start; + gtk_text_buffer_get_start_iter(text_buffer_, &start); + + if (!gtk_text_iter_equal(&start, &sel_start)) + return; // Propgate into GtkTextView. + + // We're showing a keyword and the user pressed backspace at the beginning + // of the text. Delete the selected keyword. + model_->ClearKeyword(GetText()); + + // Stop propagating the signal emission into GtkTextView. + static guint signal_id = g_signal_lookup("backspace", GTK_TYPE_TEXT_VIEW); + g_signal_stop_emission(text_view_, signal_id, 0); +} + +void OmniboxViewGtk::HandleViewMoveFocus(GtkWidget* widget, + GtkDirectionType direction) { + if (!tab_was_pressed_) + return; + + // If special behavior is triggered, then stop the signal emission to + // prevent the focus from being moved. + bool handled = false; + + // Trigger Tab to search behavior only when Tab key is pressed. + if (model_->is_keyword_hint()) + handled = model_->AcceptKeyword(); + +#if GTK_CHECK_VERSION(2, 20, 0) + if (!handled && !preedit_.empty()) + handled = true; +#endif + + if (!handled && GTK_WIDGET_VISIBLE(instant_view_)) + handled = model_->CommitSuggestedText(true); + + if (!handled) { + if (!IsCaretAtEnd()) { + OnBeforePossibleChange(); + PlaceCaretAt(GetTextLength()); + OnAfterPossibleChange(); + handled = true; + } + } + + if (!handled) + handled = model_->AcceptCurrentInstantPreview(); + + if (handled) { + static guint signal_id = g_signal_lookup("move-focus", GTK_TYPE_WIDGET); + g_signal_stop_emission(widget, signal_id, 0); + } +} + +void OmniboxViewGtk::HandleCopyClipboard(GtkWidget* sender) { + HandleCopyOrCutClipboard(true); +} + +void OmniboxViewGtk::HandleCutClipboard(GtkWidget* sender) { + HandleCopyOrCutClipboard(false); +} + +void OmniboxViewGtk::HandleCopyOrCutClipboard(bool copy) { + DCHECK(text_view_); + + // On copy or cut, we manually update the PRIMARY selection to contain the + // highlighted text. This matches Firefox -- we highlight the URL but don't + // update PRIMARY on Ctrl-L, so Ctrl-L, Ctrl-C and then middle-click is a + // convenient way to paste the current URL somewhere. + if (!gtk_text_buffer_get_has_selection(text_buffer_)) + return; + + GtkClipboard* clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY); + DCHECK(clipboard); + if (!clipboard) + return; + + CharRange selection = GetSelection(); + GURL url; + string16 text(UTF8ToUTF16(GetSelectedText())); + bool write_url; + model_->AdjustTextForCopy(selection.selection_min(), IsSelectAll(), &text, + &url, &write_url); + + if (write_url) { + BookmarkNodeData data; + data.ReadFromTuple(url, text); + data.WriteToClipboard(NULL); + + // Stop propagating the signal. + static guint copy_signal_id = + g_signal_lookup("copy-clipboard", GTK_TYPE_TEXT_VIEW); + static guint cut_signal_id = + g_signal_lookup("cut-clipboard", GTK_TYPE_TEXT_VIEW); + g_signal_stop_emission(text_view_, + copy ? copy_signal_id : cut_signal_id, + 0); + + if (!copy && gtk_text_view_get_editable(GTK_TEXT_VIEW(text_view_))) + gtk_text_buffer_delete_selection(text_buffer_, true, true); + } + + OwnPrimarySelection(UTF16ToUTF8(text)); +} + +bool OmniboxViewGtk::OnPerformDropImpl(const string16& text) { + if (model_->CanPasteAndGo(CollapseWhitespace(text, true))) { + model_->PasteAndGo(); + return true; + } + + return false; +} + +gfx::Font OmniboxViewGtk::GetFont() { +#if defined(TOOLKIT_VIEWS) + bool use_gtk = false; +#else + bool use_gtk = theme_service_->UseGtkTheme(); +#endif + + if (use_gtk) { + // If we haven't initialized the text view yet, just create a temporary one + // whose style we can grab. + GtkWidget* widget = text_view_ ? text_view_ : gtk_text_view_new(); + GtkRcStyle* rc_style = gtk_widget_get_modifier_style(widget); + gfx::Font font((rc_style && rc_style->font_desc) ? + rc_style->font_desc : + widget->style->font_desc); + if (!text_view_) + g_object_unref(g_object_ref_sink(widget)); + + // Scaling the font down for popup windows doesn't help here, since we just + // use the normal unforced font size when using the GTK theme. + return font; + } else { + return gfx::Font( + ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::BaseFont). + GetFontName(), + popup_window_mode_ ? + browser_defaults::kAutocompleteEditFontPixelSizeInPopup : + browser_defaults::kAutocompleteEditFontPixelSize); + } +} + +void OmniboxViewGtk::OwnPrimarySelection(const std::string& text) { + primary_selection_text_ = text; + + GtkTargetList* list = gtk_target_list_new(NULL, 0); + gtk_target_list_add_text_targets(list, 0); + gint len; + GtkTargetEntry* entries = gtk_target_table_new_from_list(list, &len); + + // When |text_buffer_| is destroyed, it will clear the clipboard, hence + // we needn't worry about calling gtk_clipboard_clear(). + gtk_clipboard_set_with_owner(gtk_clipboard_get(GDK_SELECTION_PRIMARY), + entries, len, + ClipboardGetSelectionThunk, + ClipboardSelectionCleared, + G_OBJECT(text_buffer_)); + + gtk_target_list_unref(list); + gtk_target_table_free(entries, len); +} + +void OmniboxViewGtk::HandlePasteClipboard(GtkWidget* sender) { + // We can't call model_->on_paste_replacing_all() here, because the actual + // paste clipboard action may not be performed if the clipboard is empty. + paste_clipboard_requested_ = true; +} + +gfx::Rect OmniboxViewGtk::WindowBoundsFromIters(GtkTextIter* iter1, + GtkTextIter* iter2) { + GdkRectangle start_location, end_location; + GtkTextView* text_view = GTK_TEXT_VIEW(text_view_); + gtk_text_view_get_iter_location(text_view, iter1, &start_location); + gtk_text_view_get_iter_location(text_view, iter2, &end_location); + + gint x1, x2, y1, y2; + gtk_text_view_buffer_to_window_coords(text_view, GTK_TEXT_WINDOW_WIDGET, + start_location.x, start_location.y, + &x1, &y1); + gtk_text_view_buffer_to_window_coords(text_view, GTK_TEXT_WINDOW_WIDGET, + end_location.x + end_location.width, + end_location.y + end_location.height, + &x2, &y2); + + return gfx::Rect(x1, y1, x2 - x1, y2 - y1); +} + +gboolean OmniboxViewGtk::HandleExposeEvent(GtkWidget* sender, + GdkEventExpose* expose) { + if (strikethrough_.cp_min >= strikethrough_.cp_max) + return FALSE; + DCHECK(text_view_); + + gfx::Rect expose_rect(expose->area); + + GtkTextIter iter_min, iter_max; + ItersFromCharRange(strikethrough_, &iter_min, &iter_max); + gfx::Rect strikethrough_rect = WindowBoundsFromIters(&iter_min, &iter_max); + + if (!expose_rect.Intersects(strikethrough_rect)) + return FALSE; + + // Finally, draw. + cairo_t* cr = gdk_cairo_create(GDK_DRAWABLE(expose->window)); + cairo_rectangle(cr, expose_rect.x(), expose_rect.y(), + expose_rect.width(), expose_rect.height()); + cairo_clip(cr); + + // TODO(estade): we probably shouldn't draw the strikethrough on selected + // text. I started to do this, but it was way more effort than it seemed + // worth. + strikethrough_rect.Inset(kStrikethroughStrokeWidth, + kStrikethroughStrokeWidth); + cairo_set_source_rgb(cr, kStrikethroughStrokeRed, 0.0, 0.0); + cairo_set_line_width(cr, kStrikethroughStrokeWidth); + cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); + cairo_move_to(cr, strikethrough_rect.x(), strikethrough_rect.bottom()); + cairo_line_to(cr, strikethrough_rect.right(), strikethrough_rect.y()); + cairo_stroke(cr); + cairo_destroy(cr); + + return FALSE; +} + +void OmniboxViewGtk::SelectAllInternal(bool reversed, + bool update_primary_selection) { + GtkTextIter start, end; + if (reversed) { + GetTextBufferBounds(&end, &start); + } else { + GetTextBufferBounds(&start, &end); + } + if (!update_primary_selection) + StartUpdatingHighlightedText(); + gtk_text_buffer_select_range(text_buffer_, &start, &end); + if (!update_primary_selection) + FinishUpdatingHighlightedText(); +} + +void OmniboxViewGtk::StartUpdatingHighlightedText() { + if (GTK_WIDGET_REALIZED(text_view_)) { + GtkClipboard* clipboard = + gtk_widget_get_clipboard(text_view_, GDK_SELECTION_PRIMARY); + DCHECK(clipboard); + if (clipboard) + gtk_text_buffer_remove_selection_clipboard(text_buffer_, clipboard); + } + g_signal_handler_block(text_buffer_, mark_set_handler_id_); + g_signal_handler_block(text_buffer_, mark_set_handler_id2_); +} + +void OmniboxViewGtk::FinishUpdatingHighlightedText() { + if (GTK_WIDGET_REALIZED(text_view_)) { + GtkClipboard* clipboard = + gtk_widget_get_clipboard(text_view_, GDK_SELECTION_PRIMARY); + DCHECK(clipboard); + if (clipboard) + gtk_text_buffer_add_selection_clipboard(text_buffer_, clipboard); + } + g_signal_handler_unblock(text_buffer_, mark_set_handler_id_); + g_signal_handler_unblock(text_buffer_, mark_set_handler_id2_); +} + +OmniboxViewGtk::CharRange OmniboxViewGtk::GetSelection() const { + // You can not just use get_selection_bounds here, since the order will be + // ascending, and you don't know where the user's start and end of the + // selection was (if the selection was forwards or backwards). Get the + // actual marks so that we can preserve the selection direction. + GtkTextIter start, insert; + GtkTextMark* mark; + + mark = gtk_text_buffer_get_selection_bound(text_buffer_); + gtk_text_buffer_get_iter_at_mark(text_buffer_, &start, mark); + + mark = gtk_text_buffer_get_insert(text_buffer_); + gtk_text_buffer_get_iter_at_mark(text_buffer_, &insert, mark); + + gint start_offset = gtk_text_iter_get_offset(&start); + gint end_offset = gtk_text_iter_get_offset(&insert); + +#if GTK_CHECK_VERSION(2, 20, 0) + // Nothing should be selected when we are in the middle of composition. + DCHECK(preedit_.empty() || start_offset == end_offset); + if (!preedit_.empty()) { + start_offset += preedit_.size(); + end_offset += preedit_.size(); + } +#endif + + return CharRange(start_offset, end_offset); +} + +void OmniboxViewGtk::ItersFromCharRange(const CharRange& range, + GtkTextIter* iter_min, + GtkTextIter* iter_max) { + DCHECK(!IsImeComposing()); + gtk_text_buffer_get_iter_at_offset(text_buffer_, iter_min, range.cp_min); + gtk_text_buffer_get_iter_at_offset(text_buffer_, iter_max, range.cp_max); +} + +int OmniboxViewGtk::GetTextLength() const { + GtkTextIter end; + gtk_text_buffer_get_iter_at_mark(text_buffer_, &end, instant_mark_); +#if GTK_CHECK_VERSION(2, 20, 0) + // We need to count the length of the text being composed, because we treat + // it as part of the content in GetText(). + return gtk_text_iter_get_offset(&end) + preedit_.size(); +#else + return gtk_text_iter_get_offset(&end); +#endif +} + +void OmniboxViewGtk::PlaceCaretAt(int pos) { + GtkTextIter cursor; + gtk_text_buffer_get_iter_at_offset(text_buffer_, &cursor, pos); + gtk_text_buffer_place_cursor(text_buffer_, &cursor); +} + +bool OmniboxViewGtk::IsCaretAtEnd() const { + const CharRange selection = GetSelection(); + return selection.cp_min == selection.cp_max && + selection.cp_min == GetTextLength(); +} + +void OmniboxViewGtk::EmphasizeURLComponents() { +#if GTK_CHECK_VERSION(2, 20, 0) + // We can't change the text style easily, if the preedit string (the text + // being composed by the input method) is not empty, which is not treated as + // a part of the text content inside GtkTextView. And it's ok to simply return + // in this case, as this method will be called again when the preedit string + // gets committed. + if (preedit_.size()) { + strikethrough_ = CharRange(); + return; + } +#endif + // See whether the contents are a URL with a non-empty host portion, which we + // should emphasize. To check for a URL, rather than using the type returned + // by Parse(), ask the model, which will check the desired page transition for + // this input. This can tell us whether an UNKNOWN input string is going to + // be treated as a search or a navigation, and is the same method the Paste + // And Go system uses. + url_parse::Component scheme, host; + string16 text(GetText()); + AutocompleteInput::ParseForEmphasizeComponents( + text, model_->GetDesiredTLD(), &scheme, &host); + const bool emphasize = model_->CurrentTextIsURL() && (host.len > 0); + + // Set the baseline emphasis. + GtkTextIter start, end; + GetTextBufferBounds(&start, &end); + gtk_text_buffer_remove_all_tags(text_buffer_, &start, &end); + if (emphasize) { + gtk_text_buffer_apply_tag(text_buffer_, faded_text_tag_, &start, &end); + + // We've found a host name, give it more emphasis. + gtk_text_buffer_get_iter_at_line_index(text_buffer_, &start, 0, + GetUTF8Offset(text, + host.begin)); + gtk_text_buffer_get_iter_at_line_index(text_buffer_, &end, 0, + GetUTF8Offset(text, + host.end())); + + gtk_text_buffer_apply_tag(text_buffer_, normal_text_tag_, &start, &end); + } else { + gtk_text_buffer_apply_tag(text_buffer_, normal_text_tag_, &start, &end); + } + + strikethrough_ = CharRange(); + // Emphasize the scheme for security UI display purposes (if necessary). + if (!model_->user_input_in_progress() && scheme.is_nonempty() && + (security_level_ != ToolbarModel::NONE)) { + CharRange scheme_range = CharRange(GetUTF8Offset(text, scheme.begin), + GetUTF8Offset(text, scheme.end())); + ItersFromCharRange(scheme_range, &start, &end); + + if (security_level_ == ToolbarModel::SECURITY_ERROR) { + strikethrough_ = scheme_range; + // When we draw the strikethrough, we don't want to include the ':' at the + // end of the scheme. + strikethrough_.cp_max--; + + gtk_text_buffer_apply_tag(text_buffer_, security_error_scheme_tag_, + &start, &end); + } else if (security_level_ == ToolbarModel::SECURITY_WARNING) { + gtk_text_buffer_apply_tag(text_buffer_, faded_text_tag_, &start, &end); + } else { + gtk_text_buffer_apply_tag(text_buffer_, secure_scheme_tag_, &start, &end); + } + } +} + +void OmniboxViewGtk::StopAnimation() { + // Clear the animation delegate so we don't get an AnimationEnded() callback. + instant_animation_->set_delegate(NULL); + instant_animation_->Stop(); + UpdateInstantViewColors(); +} + +void OmniboxViewGtk::TextChanged() { + EmphasizeURLComponents(); + model_->OnChanged(); +} + +void OmniboxViewGtk::SavePrimarySelection(const std::string& selected_text) { + DCHECK(text_view_); + + GtkClipboard* clipboard = + gtk_widget_get_clipboard(text_view_, GDK_SELECTION_PRIMARY); + DCHECK(clipboard); + if (!clipboard) + return; + + gtk_clipboard_set_text( + clipboard, selected_text.data(), selected_text.size()); +} + +void OmniboxViewGtk::SetTextAndSelectedRange(const string16& text, + const CharRange& range) { + if (text != GetText()) { + std::string utf8 = UTF16ToUTF8(text); + gtk_text_buffer_set_text(text_buffer_, utf8.data(), utf8.length()); + } + SetSelectedRange(range); + AdjustTextJustification(); +} + +void OmniboxViewGtk::SetSelectedRange(const CharRange& range) { + GtkTextIter insert, bound; + ItersFromCharRange(range, &bound, &insert); + gtk_text_buffer_select_range(text_buffer_, &insert, &bound); + + // This should be set *after* setting the selection range, in case setting the + // selection triggers HandleMarkSet which sets |selection_suggested_| to + // false. + selection_suggested_ = true; +} + +void OmniboxViewGtk::AdjustTextJustification() { + DCHECK(text_view_); + + PangoDirection content_dir = GetContentDirection(); + + // Use keymap direction if content does not have strong direction. + // It matches the behavior of GtkTextView. + if (content_dir == PANGO_DIRECTION_NEUTRAL) { + content_dir = gdk_keymap_get_direction( + gdk_keymap_get_for_display(gtk_widget_get_display(text_view_))); + } + + GtkTextDirection widget_dir = gtk_widget_get_direction(text_view_); + + if ((widget_dir == GTK_TEXT_DIR_RTL && content_dir == PANGO_DIRECTION_LTR) || + (widget_dir == GTK_TEXT_DIR_LTR && content_dir == PANGO_DIRECTION_RTL)) { + gtk_text_view_set_justification(GTK_TEXT_VIEW(text_view_), + GTK_JUSTIFY_RIGHT); + } else { + gtk_text_view_set_justification(GTK_TEXT_VIEW(text_view_), + GTK_JUSTIFY_LEFT); + } +} + +PangoDirection OmniboxViewGtk::GetContentDirection() { + GtkTextIter iter; + gtk_text_buffer_get_start_iter(text_buffer_, &iter); + + PangoDirection dir = PANGO_DIRECTION_NEUTRAL; + do { + dir = pango_unichar_direction(gtk_text_iter_get_char(&iter)); + if (dir != PANGO_DIRECTION_NEUTRAL) + break; + } while (gtk_text_iter_forward_char(&iter)); + + return dir; +} + +void OmniboxViewGtk::HandleWidgetDirectionChanged( + GtkWidget* sender, + GtkTextDirection previous_direction) { + AdjustTextJustification(); +} + +void OmniboxViewGtk::HandleDeleteFromCursor(GtkWidget* sender, + GtkDeleteType type, + gint count) { + // If the selected text was suggested for autocompletion, then erase those + // first and then let the default handler take over. + if (selection_suggested_) { + gtk_text_buffer_delete_selection(text_buffer_, true, true); + selection_suggested_ = false; + } +} + +void OmniboxViewGtk::HandleKeymapDirectionChanged(GdkKeymap* sender) { + AdjustTextJustification(); +} + +void OmniboxViewGtk::HandleDeleteRange(GtkTextBuffer* buffer, + GtkTextIter* start, + GtkTextIter* end) { + // Prevent the user from deleting the instant anchor. We can't simply set the + // instant anchor readonly by applying a tag with "editable" = FALSE, because + // it'll prevent the insert caret from blinking. + ValidateTextBufferIter(start); + ValidateTextBufferIter(end); + if (!gtk_text_iter_compare(start, end)) { + static guint signal_id = + g_signal_lookup("delete-range", GTK_TYPE_TEXT_BUFFER); + g_signal_stop_emission(buffer, signal_id, 0); + } +} + +void OmniboxViewGtk::HandleMarkSetAlways(GtkTextBuffer* buffer, + GtkTextIter* location, + GtkTextMark* mark) { + if (mark == instant_mark_ || !instant_mark_) + return; + + GtkTextIter new_iter = *location; + ValidateTextBufferIter(&new_iter); + + static guint signal_id = g_signal_lookup("mark-set", GTK_TYPE_TEXT_BUFFER); + + // "mark-set" signal is actually emitted after the mark's location is already + // set, so if the location is beyond the instant anchor, we need to move the + // mark again, which will emit the signal again. In order to prevent other + // signal handlers from being called twice, we need to stop signal emission + // before moving the mark again. + if (gtk_text_iter_compare(&new_iter, location)) { + g_signal_stop_emission(buffer, signal_id, 0); + gtk_text_buffer_move_mark(buffer, mark, &new_iter); + return; + } + + if (mark != gtk_text_buffer_get_insert(text_buffer_) && + mark != gtk_text_buffer_get_selection_bound(text_buffer_)) { + return; + } + + // See issue http://crbug.com/63860 + GtkTextIter insert; + GtkTextIter selection_bound; + gtk_text_buffer_get_iter_at_mark(buffer, &insert, + gtk_text_buffer_get_insert(buffer)); + gtk_text_buffer_get_iter_at_mark(buffer, &selection_bound, + gtk_text_buffer_get_selection_bound(buffer)); + + GtkTextIter end; + gtk_text_buffer_get_iter_at_mark(text_buffer_, &end, instant_mark_); + + if (gtk_text_iter_compare(&insert, &end) > 0 || + gtk_text_iter_compare(&selection_bound, &end) > 0) { + g_signal_stop_emission(buffer, signal_id, 0); + } +} + +// static +void OmniboxViewGtk::ClipboardGetSelectionThunk( + GtkClipboard* clipboard, + GtkSelectionData* selection_data, + guint info, + gpointer object) { + OmniboxViewGtk* edit_view = + reinterpret_cast<OmniboxViewGtk*>( + g_object_get_data(G_OBJECT(object), kOmniboxViewGtkKey)); + edit_view->ClipboardGetSelection(clipboard, selection_data, info); +} + +void OmniboxViewGtk::ClipboardGetSelection(GtkClipboard* clipboard, + GtkSelectionData* selection_data, + guint info) { + gtk_selection_data_set_text(selection_data, primary_selection_text_.c_str(), + primary_selection_text_.size()); +} + +std::string OmniboxViewGtk::GetSelectedText() const { + GtkTextIter start, end; + std::string result; + if (gtk_text_buffer_get_selection_bounds(text_buffer_, &start, &end)) { + gchar* text = gtk_text_iter_get_text(&start, &end); + size_t text_len = strlen(text); + if (text_len) + result = std::string(text, text_len); + g_free(text); + } + return result; +} + +void OmniboxViewGtk::UpdatePrimarySelectionIfValidURL() { + string16 text = UTF8ToUTF16(GetSelectedText()); + + if (text.empty()) + return; + + // Use AdjustTextForCopy to make sure we prefix the text with 'http://'. + CharRange selection = GetSelection(); + GURL url; + bool write_url; + model_->AdjustTextForCopy(selection.selection_min(), IsSelectAll(), &text, + &url, &write_url); + if (write_url) { + selected_text_ = UTF16ToUTF8(text); + OwnPrimarySelection(selected_text_); + } +} + +#if GTK_CHECK_VERSION(2, 20, 0) +void OmniboxViewGtk::HandlePreeditChanged(GtkWidget* sender, + const gchar* preedit) { + // GtkTextView won't fire "begin-user-action" and "end-user-action" signals + // when changing the preedit string, so we need to call + // OnBeforePossibleChange() and OnAfterPossibleChange() by ourselves. + OnBeforePossibleChange(); + if (preedit && *preedit) { + // GtkTextView will only delete the selection range when committing the + // preedit string, which will cause very strange behavior, so we need to + // delete the selection range here explicitly. See http://crbug.com/18808. + if (preedit_.empty()) + gtk_text_buffer_delete_selection(text_buffer_, false, true); + preedit_ = UTF8ToUTF16(preedit); + } else { + preedit_.clear(); + } + OnAfterPossibleChange(); +} +#endif + +void OmniboxViewGtk::HandleWindowSetFocus(GtkWindow* sender, + GtkWidget* focus) { + // This is actually a guess. If the focused widget changes in "focus-out" + // event handler, then the window will respect that and won't focus + // |focus|. I doubt that is likely to happen however. + going_to_focus_ = focus; +} + +void OmniboxViewGtk::HandleUndoRedo(GtkWidget* sender) { + OnBeforePossibleChange(); +} + +void OmniboxViewGtk::HandleUndoRedoAfter(GtkWidget* sender) { + OnAfterPossibleChange(); +} + +void OmniboxViewGtk::GetTextBufferBounds(GtkTextIter* start, + GtkTextIter* end) const { + gtk_text_buffer_get_start_iter(text_buffer_, start); + gtk_text_buffer_get_iter_at_mark(text_buffer_, end, instant_mark_); +} + +void OmniboxViewGtk::ValidateTextBufferIter(GtkTextIter* iter) const { + if (!instant_mark_) + return; + + GtkTextIter end; + gtk_text_buffer_get_iter_at_mark(text_buffer_, &end, instant_mark_); + if (gtk_text_iter_compare(iter, &end) > 0) + *iter = end; +} + +void OmniboxViewGtk::AdjustVerticalAlignmentOfInstantView() { + // By default, GtkTextView layouts an anchored child widget just above the + // baseline, so we need to move the |instant_view_| down to make sure it + // has the same baseline as the |text_view_|. + PangoLayout* layout = gtk_label_get_layout(GTK_LABEL(instant_view_)); + int height; + pango_layout_get_size(layout, NULL, &height); + PangoLayoutIter* iter = pango_layout_get_iter(layout); + int baseline = pango_layout_iter_get_baseline(iter); + pango_layout_iter_free(iter); + g_object_set(instant_anchor_tag_, "rise", baseline - height, NULL); +} diff --git a/chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.h b/chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.h new file mode 100644 index 0000000..cbda4c7 --- /dev/null +++ b/chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.h @@ -0,0 +1,546 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_UI_GTK_OMNIBOX_OMNIBOX_VIEW_GTK_H_ +#define CHROME_BROWSER_UI_GTK_OMNIBOX_OMNIBOX_VIEW_GTK_H_ +#pragma once + +#include <gtk/gtk.h> + +#include <algorithm> +#include <string> + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "base/string_util.h" +#include "chrome/browser/autocomplete/autocomplete_edit_view.h" +#include "chrome/browser/ui/gtk/owned_widget_gtk.h" +#include "chrome/browser/ui/toolbar/toolbar_model.h" +#include "content/common/notification_observer.h" +#include "content/common/notification_registrar.h" +#include "content/common/page_transition_types.h" +#include "ui/base/animation/animation_delegate.h" +#include "ui/base/gtk/gtk_signal.h" +#include "ui/base/gtk/gtk_signal_registrar.h" +#include "ui/gfx/rect.h" +#include "webkit/glue/window_open_disposition.h" + +class AutocompleteEditController; +class AutocompleteEditModel; +class AutocompletePopupView; +class Profile; +class TabContents; + +namespace gfx { +class Font; +} + +namespace ui { +class MultiAnimation; +} + +namespace views { +class View; +} + +#if !defined(TOOLKIT_VIEWS) +class GtkThemeService; +#endif + +class OmniboxViewGtk : public AutocompleteEditView, + public NotificationObserver, + public ui::AnimationDelegate { + public: + // Modeled like the Windows CHARRANGE. Represent a pair of cursor position + // offsets. Since GtkTextIters are invalid after the buffer is changed, we + // work in character offsets (not bytes). + struct CharRange { + CharRange() : cp_min(0), cp_max(0) { } + CharRange(int n, int x) : cp_min(n), cp_max(x) { } + + // Returns the start/end of the selection. + int selection_min() const { return std::min(cp_min, cp_max); } + int selection_max() const { return std::max(cp_min, cp_max); } + + // Work in integers to match the gint GTK APIs. + int cp_min; // For a selection: Represents the start. + int cp_max; // For a selection: Represents the end (insert position). + }; + + OmniboxViewGtk(AutocompleteEditController* controller, + ToolbarModel* toolbar_model, + Profile* profile, + CommandUpdater* command_updater, + bool popup_window_mode, +#if defined(TOOLKIT_VIEWS) + views::View* location_bar +#else + GtkWidget* location_bar +#endif + ); + virtual ~OmniboxViewGtk(); + + // Initialize, create the underlying widgets, etc. + void Init(); + // Returns the width in pixels needed to display the text from one character + // before the caret to the end of the string. See comments in + // LocationBarView::Layout as to why this uses -1. + int WidthOfTextAfterCursor(); + + // Implement the AutocompleteEditView interface. + virtual AutocompleteEditModel* model(); + virtual const AutocompleteEditModel* model() const; + + virtual void SaveStateToTab(TabContents* tab); + + virtual void Update(const TabContents* tab_for_state_restoring); + + virtual void OpenURL(const GURL& url, + WindowOpenDisposition disposition, + PageTransition::Type transition, + const GURL& alternate_nav_url, + size_t selected_line, + const string16& keyword); + + virtual string16 GetText() const; + + virtual bool IsEditingOrEmpty() const; + virtual int GetIcon() const; + + virtual void SetUserText(const string16& text); + virtual void SetUserText(const string16& text, + const string16& display_text, + bool update_popup); + + virtual void SetWindowTextAndCaretPos(const string16& text, + size_t caret_pos); + + virtual void SetForcedQuery(); + + virtual bool IsSelectAll(); + virtual bool DeleteAtEndPressed(); + virtual void GetSelectionBounds(string16::size_type* start, + string16::size_type* end); + virtual void SelectAll(bool reversed); + virtual void RevertAll(); + + virtual void UpdatePopup(); + virtual void ClosePopup(); + + virtual void SetFocus(); + + virtual void OnTemporaryTextMaybeChanged(const string16& display_text, + bool save_original_selection); + virtual bool OnInlineAutocompleteTextMaybeChanged( + const string16& display_text, size_t user_text_length); + virtual void OnRevertTemporaryText(); + virtual void OnBeforePossibleChange(); + virtual bool OnAfterPossibleChange(); + virtual gfx::NativeView GetNativeView() const; + virtual CommandUpdater* GetCommandUpdater(); + virtual void SetInstantSuggestion(const string16& suggestion, + bool animate_to_complete); + virtual string16 GetInstantSuggestion() const; + virtual int TextWidth() const; + virtual bool IsImeComposing() const; + +#if defined(TOOLKIT_VIEWS) + virtual views::View* AddToView(views::View* parent); + virtual int OnPerformDrop(const views::DropTargetEvent& event); + + // A factory method to create an AutocompleteEditView instance initialized for + // linux_views. This currently returns an instance of OmniboxViewGtk only, + // but AutocompleteEditViewViews will be added as an option when + // TextfieldViews is enabled. + static AutocompleteEditView* Create(AutocompleteEditController* controller, + ToolbarModel* toolbar_model, + Profile* profile, + CommandUpdater* command_updater, + bool popup_window_mode, + views::View* location_bar); +#endif + + // Overridden from NotificationObserver: + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details); + + // Overridden from ui::AnimationDelegate. + virtual void AnimationEnded(const ui::Animation* animation); + virtual void AnimationProgressed(const ui::Animation* animation); + virtual void AnimationCanceled(const ui::Animation* animation); + + // Sets the colors of the text view according to the theme. + void SetBaseColor(); + // Sets the colors of the instant suggestion view according to the theme and + // the animation state. + void UpdateInstantViewColors(); + + // Returns the text view gtk widget. May return NULL if the widget + // has already been destroyed. + GtkWidget* text_view() { + return text_view_; + } + + private: + CHROMEG_CALLBACK_0(OmniboxViewGtk, void, HandleBeginUserAction, + GtkTextBuffer*); + CHROMEG_CALLBACK_0(OmniboxViewGtk, void, HandleEndUserAction, GtkTextBuffer*); + CHROMEG_CALLBACK_2(OmniboxViewGtk, void, HandleMarkSet, GtkTextBuffer*, + GtkTextIter*, GtkTextMark*); + // As above, but called after the default handler. + CHROMEG_CALLBACK_2(OmniboxViewGtk, void, HandleMarkSetAfter, GtkTextBuffer*, + GtkTextIter*, GtkTextMark*); + CHROMEG_CALLBACK_3(OmniboxViewGtk, void, HandleInsertText, GtkTextBuffer*, + GtkTextIter*, const gchar*, gint); + CHROMEG_CALLBACK_0(OmniboxViewGtk, void, HandleKeymapDirectionChanged, + GdkKeymap*); + CHROMEG_CALLBACK_2(OmniboxViewGtk, void, HandleDeleteRange, GtkTextBuffer*, + GtkTextIter*, GtkTextIter*); + // Unlike above HandleMarkSet and HandleMarkSetAfter, this handler will always + // be connected to the signal. + CHROMEG_CALLBACK_2(OmniboxViewGtk, void, HandleMarkSetAlways, GtkTextBuffer*, + GtkTextIter*, GtkTextMark*); + + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, gboolean, HandleKeyPress, GdkEventKey*); + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, gboolean, HandleKeyRelease, + GdkEventKey*); + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, gboolean, HandleViewButtonPress, + GdkEventButton*); + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, gboolean, HandleViewButtonRelease, + GdkEventButton*); + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, gboolean, HandleViewFocusIn, + GdkEventFocus*); + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, gboolean, HandleViewFocusOut, + GdkEventFocus*); + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, void, HandleViewMoveFocus, + GtkDirectionType); + CHROMEGTK_CALLBACK_3(OmniboxViewGtk, void, HandleViewMoveCursor, + GtkMovementStep, gint, gboolean); + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, void, HandleViewSizeRequest, + GtkRequisition*); + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, void, HandlePopulatePopup, GtkMenu*); + CHROMEGTK_CALLBACK_0(OmniboxViewGtk, void, HandleEditSearchEngines); + CHROMEGTK_CALLBACK_0(OmniboxViewGtk, void, HandlePasteAndGo); + CHROMEGTK_CALLBACK_6(OmniboxViewGtk, void, HandleDragDataReceived, + GdkDragContext*, gint, gint, GtkSelectionData*, + guint, guint); + CHROMEGTK_CALLBACK_4(OmniboxViewGtk, void, HandleDragDataGet, + GdkDragContext*, GtkSelectionData*, guint, guint); + CHROMEGTK_CALLBACK_0(OmniboxViewGtk, void, HandleBackSpace); + CHROMEGTK_CALLBACK_0(OmniboxViewGtk, void, HandleCopyClipboard); + CHROMEGTK_CALLBACK_0(OmniboxViewGtk, void, HandleCutClipboard); + CHROMEGTK_CALLBACK_0(OmniboxViewGtk, void, HandlePasteClipboard); + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, gboolean, HandleExposeEvent, + GdkEventExpose*); + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, void, HandleWidgetDirectionChanged, + GtkTextDirection); + CHROMEGTK_CALLBACK_2(OmniboxViewGtk, void, HandleDeleteFromCursor, + GtkDeleteType, gint); + // We connect to this so we can determine our toplevel window, so we can + // listen to focus change events on it. + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, void, HandleHierarchyChanged, + GtkWidget*); +#if GTK_CHECK_VERSION(2, 20, 0) + CHROMEGTK_CALLBACK_1(OmniboxViewGtk, void, HandlePreeditChanged, + const gchar*); +#endif + // Undo/redo operations won't trigger "begin-user-action" and + // "end-user-action" signals, so we need to hook into "undo" and "redo" + // signals and call OnBeforePossibleChange()/OnAfterPossibleChange() by + // ourselves. + CHROMEGTK_CALLBACK_0(OmniboxViewGtk, void, HandleUndoRedo); + CHROMEGTK_CALLBACK_0(OmniboxViewGtk, void, HandleUndoRedoAfter); + + CHROMEG_CALLBACK_1(OmniboxViewGtk, void, HandleWindowSetFocus, + GtkWindow*, GtkWidget*); + + // Callback function called after context menu is closed. + CHROMEGTK_CALLBACK_0(OmniboxViewGtk, void, HandlePopupMenuDeactivate); + + // Callback for the PRIMARY selection clipboard. + static void ClipboardGetSelectionThunk(GtkClipboard* clipboard, + GtkSelectionData* selection_data, + guint info, + gpointer object); + void ClipboardGetSelection(GtkClipboard* clipboard, + GtkSelectionData* selection_data, + guint info); + + void HandleCopyOrCutClipboard(bool copy); + + // Common implementation for performing a drop on the edit view. + bool OnPerformDropImpl(const string16& text); + + // Returns the font used in |text_view_|. + gfx::Font GetFont(); + + // Take control of the PRIMARY selection clipboard with |text|. Use + // |text_buffer_| as the owner, so that this doesn't remove the selection on + // it. This makes use of the above callbacks. + void OwnPrimarySelection(const std::string& text); + + // Gets the GTK_TEXT_WINDOW_WIDGET coordinates for |text_view_| that bound the + // given iters. + gfx::Rect WindowBoundsFromIters(GtkTextIter* iter1, GtkTextIter* iter2); + + // Actual implementation of SelectAll(), but also provides control over + // whether the PRIMARY selection is set to the selected text (in SelectAll(), + // it isn't, but we want set the selection when the user clicks in the entry). + void SelectAllInternal(bool reversed, bool update_primary_selection); + + // Get ready to update |text_buffer_|'s highlighting without making changes to + // the PRIMARY selection. Removes the clipboard from |text_buffer_| and + // blocks the "mark-set" signal handler. + void StartUpdatingHighlightedText(); + + // Finish updating |text_buffer_|'s highlighting such that future changes will + // automatically update the PRIMARY selection. Undoes + // StartUpdatingHighlightedText()'s changes. + void FinishUpdatingHighlightedText(); + + // Get the character indices of the current selection. This honors + // direction, cp_max is the insertion point, and cp_min is the bound. + CharRange GetSelection() const; + + // Translate from character positions to iterators for the current buffer. + void ItersFromCharRange(const CharRange& range, + GtkTextIter* iter_min, + GtkTextIter* iter_max); + + // Return the number of characers in the current buffer. + int GetTextLength() const; + + // Places the caret at the given position. This clears any selection. + void PlaceCaretAt(int pos); + + // Returns true if the caret is at the end of the content. + bool IsCaretAtEnd() const; + + // Try to parse the current text as a URL and colorize the components. + void EmphasizeURLComponents(); + + // Internally invoked whenever the text changes in some way. + void TextChanged(); + + // Save |selected_text| as the PRIMARY X selection. Unlike + // OwnPrimarySelection(), this won't set an owner or use callbacks. + void SavePrimarySelection(const std::string& selected_text); + + // Update the field with |text| and set the selection. + void SetTextAndSelectedRange(const string16& text, + const CharRange& range); + + // Set the selection to |range|. + void SetSelectedRange(const CharRange& range); + + // Adjust the text justification according to the text direction of the widget + // and |text_buffer_|'s content, to make sure the real text justification is + // always in sync with the UI language direction. + void AdjustTextJustification(); + + // Get the text direction of |text_buffer_|'s content, by searching the first + // character that has a strong direction. + PangoDirection GetContentDirection(); + + // Returns the selected text. + std::string GetSelectedText() const; + + // If the selected text parses as a URL OwnPrimarySelection is invoked. + void UpdatePrimarySelectionIfValidURL(); + + // Retrieves the first and last iterators in the |text_buffer_|, but excludes + // the anchor holding the |instant_view_| widget. + void GetTextBufferBounds(GtkTextIter* start, GtkTextIter* end) const; + + // Validates an iterator in the |text_buffer_|, to make sure it doesn't go + // beyond the anchor for holding the |instant_view_| widget. + void ValidateTextBufferIter(GtkTextIter* iter) const; + + // Adjusts vertical alignment of the |instant_view_| in the |text_view_|, to + // make sure they have the same baseline. + void AdjustVerticalAlignmentOfInstantView(); + + // Stop showing the instant suggest auto-commit animation. + void StopAnimation(); + + // The widget we expose, used for vertically centering the real text edit, + // since the height will change based on the font / font size, etc. + OwnedWidgetGtk alignment_; + + // The actual text entry which will be owned by the alignment_. The + // reference will be set to NULL upon destruction to tell if the gtk + // widget tree has been destroyed. This is because gtk destroies child + // widgets if the parent (alignemtn_)'s refcount does not go down to 0. + GtkWidget* text_view_; + + GtkTextTagTable* tag_table_; + GtkTextBuffer* text_buffer_; + GtkTextTag* faded_text_tag_; + GtkTextTag* secure_scheme_tag_; + GtkTextTag* security_error_scheme_tag_; + GtkTextTag* normal_text_tag_; + + // Objects for the instant suggestion text view. + GtkTextTag* instant_anchor_tag_; + + // A widget for displaying instant suggestion text. It'll be attached to a + // child anchor in the |text_buffer_| object. + GtkWidget* instant_view_; + // Animation from instant suggest (faded text) to autocomplete (selected + // text). + scoped_ptr<ui::MultiAnimation> instant_animation_; + + // A mark to split the content and the instant anchor. Wherever the end + // iterator of the text buffer is required, the iterator to this mark should + // be used. + GtkTextMark* instant_mark_; + + scoped_ptr<AutocompleteEditModel> model_; + scoped_ptr<AutocompletePopupView> popup_view_; + AutocompleteEditController* controller_; + ToolbarModel* toolbar_model_; + + // The object that handles additional command functionality exposed on the + // edit, such as invoking the keyword editor. + CommandUpdater* command_updater_; + + // When true, the location bar view is read only and also is has a slightly + // different presentation (smaller font size). This is used for popups. + bool popup_window_mode_; + + ToolbarModel::SecurityLevel security_level_; + + // Selection at the point where the user started using the + // arrows to move around in the popup. + CharRange saved_temporary_selection_; + + // Tracking state before and after a possible change. + string16 text_before_change_; + CharRange sel_before_change_; + + // The most-recently-selected text from the entry that was copied to the + // clipboard. This is updated on-the-fly as the user selects text. This may + // differ from the actual selected text, such as when 'http://' is prefixed to + // the text. It is used in cases where we need to make the PRIMARY selection + // persist even after the user has unhighlighted the text in the view + // (e.g. when they highlight some text and then click to unhighlight it, we + // pass this string to SavePrimarySelection()). + std::string selected_text_; + + // When we own the X clipboard, this is the text for it. + std::string primary_selection_text_; + + // IDs of the signal handlers for "mark-set" on |text_buffer_|. + gulong mark_set_handler_id_; + gulong mark_set_handler_id2_; + +#if defined(OS_CHROMEOS) + // The following variables are used to implement select-all-on-mouse-up, which + // is disabled in the standard Linux build due to poor interaction with the + // PRIMARY X selection. + + // Is the first mouse button currently down? When selection marks get moved, + // we use this to determine if the user was highlighting text with the mouse + // -- if so, we avoid selecting all the text on mouse-up. + bool button_1_pressed_; + + // Did the user change the selected text in the middle of the current click? + // If so, we don't select all of the text when the button is released -- we + // don't want to blow away their selection. + bool text_selected_during_click_; + + // Was the text view already focused before the user clicked in it? We use + // this to figure out whether we should select all of the text when the button + // is released (we only do so if the view was initially unfocused). + bool text_view_focused_before_button_press_; +#endif + +#if defined(TOOLKIT_VIEWS) + views::View* location_bar_view_; +#else + // Supplies colors, et cetera. + GtkThemeService* theme_service_; + + NotificationRegistrar registrar_; +#endif + + // Indicates if Enter key was pressed. + // + // It's used in the key press handler to detect an Enter key press event + // during sync dispatch of "end-user-action" signal so that an unexpected + // change caused by the event can be ignored in OnAfterPossibleChange(). + bool enter_was_pressed_; + + // Indicates if Tab key was pressed. + // + // It's only used in the key press handler to detect a Tab key press event + // during sync dispatch of "move-focus" signal. + bool tab_was_pressed_; + + // Indicates that user requested to paste clipboard. + // The actual paste clipboard action might be performed later if the + // clipboard is not empty. + bool paste_clipboard_requested_; + + // Indicates if an Enter key press is inserted as text. + // It's used in the key press handler to determine if an Enter key event is + // handled by IME or not. + bool enter_was_inserted_; + + // Indicates whether the IME changed the text. It's possible for the IME to + // handle a key event but not change the text contents (e.g., when pressing + // shift+del with no selection). + bool text_changed_; + + // Contains the character range that should have a strikethrough (used for + // insecure schemes). If the range is size one or less, no strikethrough + // is needed. + CharRange strikethrough_; + + // Indicates if the selected text is suggested text or not. If the selection + // is not suggested text, that means the user manually made the selection. + bool selection_suggested_; + + // Was delete pressed? + bool delete_was_pressed_; + + // Was the delete key pressed with an empty selection at the end of the edit? + bool delete_at_end_pressed_; + + // Indicates if we are handling a key press event. + bool handling_key_press_; + + // Indicates if omnibox's content maybe changed by a key press event, so that + // we need to call OnAfterPossibleChange() after handling the event. + // This flag should be set for changes directly caused by a key press event, + // including changes to content text, selection range and preedit string. + // Changes caused by function calls like SetUserText() should not affect this + // flag. + bool content_maybe_changed_by_key_press_; + + // Set this flag to call UpdatePopup() in lost focus and need to update. + // Because context menu might take the focus, before setting the flag, check + // the focus with model_->has_focus(). + bool update_popup_without_focus_; + +#if GTK_CHECK_VERSION(2, 20, 0) + // Stores the text being composed by the input method. + string16 preedit_; + + // Tracking preedit state before and after a possible change. We don't need to + // track preedit_'s content, as it'll be treated as part of text content. + size_t preedit_size_before_change_; +#endif + + // The view that is going to be focused next. Only valid while handling + // "focus-out" events. + GtkWidget* going_to_focus_; + + ui::GtkSignalRegistrar signals_; + + DISALLOW_COPY_AND_ASSIGN(OmniboxViewGtk); +}; + +#endif // CHROME_BROWSER_UI_GTK_OMNIBOX_OMNIBOX_VIEW_GTK_H_ diff --git a/chrome/browser/ui/views/location_bar/location_bar_view.cc b/chrome/browser/ui/views/location_bar/location_bar_view.cc index a3f4b82..77296d8 100644 --- a/chrome/browser/ui/views/location_bar/location_bar_view.cc +++ b/chrome/browser/ui/views/location_bar/location_bar_view.cc @@ -171,10 +171,8 @@ void LocationBarView::Init() { GetWidget()->GetNativeView(), profile_, command_updater_, mode_ == POPUP, this)); #else - location_entry_.reset( - AutocompleteEditViewGtk::Create( - this, model_, profile_, - command_updater_, mode_ == POPUP, this)); + location_entry_.reset(OmniboxViewGtk::Create(this, model_, profile_, + command_updater_, mode_ == POPUP, this)); #endif location_entry_view_ = location_entry_->AddToView(this); diff --git a/chrome/browser/ui/views/location_bar/location_bar_view.h b/chrome/browser/ui/views/location_bar/location_bar_view.h index 8017a6e..cd39837 100644 --- a/chrome/browser/ui/views/location_bar/location_bar_view.h +++ b/chrome/browser/ui/views/location_bar/location_bar_view.h @@ -26,7 +26,7 @@ #if defined(OS_WIN) #include "chrome/browser/autocomplete/autocomplete_edit_view_win.h" #elif defined(OS_LINUX) -#include "chrome/browser/autocomplete/autocomplete_edit_view_gtk.h" +#include "chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.h" #endif class CommandUpdater; |