| OLD | NEW |
| (Empty) |
| 1 // Copyright 2012 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #include "chrome/browser/ui/gtk/omnibox/omnibox_view_gtk.h" | |
| 6 | |
| 7 #include <gdk/gdkkeysyms.h> | |
| 8 #include <gtk/gtk.h> | |
| 9 | |
| 10 #include <algorithm> | |
| 11 | |
| 12 #include "base/logging.h" | |
| 13 #include "base/metrics/histogram.h" | |
| 14 #include "base/strings/string_util.h" | |
| 15 #include "base/strings/utf_string_conversion_utils.h" | |
| 16 #include "base/strings/utf_string_conversions.h" | |
| 17 #include "chrome/app/chrome_command_ids.h" | |
| 18 #include "chrome/browser/autocomplete/autocomplete_input.h" | |
| 19 #include "chrome/browser/autocomplete/autocomplete_match.h" | |
| 20 #include "chrome/browser/bookmarks/bookmark_node_data.h" | |
| 21 #include "chrome/browser/chrome_notification_types.h" | |
| 22 #include "chrome/browser/command_updater.h" | |
| 23 #include "chrome/browser/defaults.h" | |
| 24 #include "chrome/browser/platform_util.h" | |
| 25 #include "chrome/browser/search/search.h" | |
| 26 #include "chrome/browser/ui/browser.h" | |
| 27 #include "chrome/browser/ui/gtk/gtk_theme_service.h" | |
| 28 #include "chrome/browser/ui/gtk/gtk_util.h" | |
| 29 #include "chrome/browser/ui/gtk/location_bar_view_gtk.h" | |
| 30 #include "chrome/browser/ui/gtk/omnibox/omnibox_popup_view_gtk.h" | |
| 31 #include "chrome/browser/ui/gtk/view_id_util.h" | |
| 32 #include "chrome/browser/ui/omnibox/omnibox_edit_controller.h" | |
| 33 #include "chrome/browser/ui/omnibox/omnibox_edit_model.h" | |
| 34 #include "chrome/browser/ui/omnibox/omnibox_popup_model.h" | |
| 35 #include "chrome/browser/ui/tabs/tab_strip_model.h" | |
| 36 #include "chrome/browser/ui/toolbar/toolbar_model.h" | |
| 37 #include "content/public/browser/notification_source.h" | |
| 38 #include "content/public/browser/web_contents.h" | |
| 39 #include "extensions/common/constants.h" | |
| 40 #include "grit/generated_resources.h" | |
| 41 #include "net/base/escape.h" | |
| 42 #include "third_party/undoview/undo_view.h" | |
| 43 #include "ui/base/accelerators/menu_label_accelerator_util_linux.h" | |
| 44 #include "ui/base/dragdrop/drag_drop_types.h" | |
| 45 #include "ui/base/dragdrop/gtk_dnd_util.h" | |
| 46 #include "ui/base/gtk/gtk_hig_constants.h" | |
| 47 #include "ui/base/l10n/l10n_util.h" | |
| 48 #include "ui/base/resource/resource_bundle.h" | |
| 49 #include "ui/gfx/color_utils.h" | |
| 50 #include "ui/gfx/font.h" | |
| 51 #include "ui/gfx/gtk_compat.h" | |
| 52 #include "ui/gfx/skia_utils_gtk.h" | |
| 53 #include "url/gurl.h" | |
| 54 | |
| 55 using content::WebContents; | |
| 56 | |
| 57 namespace { | |
| 58 | |
| 59 const gchar* kOmniboxViewGtkKey = "__OMNIBOX_VIEW_GTK__"; | |
| 60 | |
| 61 const char kTextBaseColor[] = "#808080"; | |
| 62 const char kSecureSchemeColor[] = "#079500"; | |
| 63 const char kSecurityErrorSchemeColor[] = "#a20000"; | |
| 64 | |
| 65 const double kStrikethroughStrokeRed = 162.0 / 256.0; | |
| 66 const double kStrikethroughStrokeWidth = 2.0; | |
| 67 | |
| 68 size_t GetUTF8Offset(const base::string16& text, size_t text_offset) { | |
| 69 return base::UTF16ToUTF8(text.substr(0, text_offset)).size(); | |
| 70 } | |
| 71 | |
| 72 // Stores GTK+-specific state so it can be restored after switching tabs. | |
| 73 struct ViewState { | |
| 74 explicit ViewState(const OmniboxViewGtk::CharRange& selection_range) | |
| 75 : selection_range(selection_range) { | |
| 76 } | |
| 77 | |
| 78 // Range of selected text. | |
| 79 OmniboxViewGtk::CharRange selection_range; | |
| 80 }; | |
| 81 | |
| 82 const char kAutocompleteEditStateKey[] = "AutocompleteEditState"; | |
| 83 | |
| 84 struct AutocompleteEditState : public base::SupportsUserData::Data { | |
| 85 AutocompleteEditState(const OmniboxEditModel::State& model_state, | |
| 86 const ViewState& view_state) | |
| 87 : model_state(model_state), | |
| 88 view_state(view_state) { | |
| 89 } | |
| 90 virtual ~AutocompleteEditState() {} | |
| 91 | |
| 92 const OmniboxEditModel::State model_state; | |
| 93 const ViewState view_state; | |
| 94 }; | |
| 95 | |
| 96 // Set up style properties to override the default GtkTextView; if a theme has | |
| 97 // overridden some of these properties, an inner-line will be displayed inside | |
| 98 // the fake GtkTextEntry. | |
| 99 void SetEntryStyle() { | |
| 100 static bool style_was_set = false; | |
| 101 | |
| 102 if (style_was_set) | |
| 103 return; | |
| 104 style_was_set = true; | |
| 105 | |
| 106 gtk_rc_parse_string( | |
| 107 "style \"chrome-location-bar-entry\" {" | |
| 108 " xthickness = 0\n" | |
| 109 " ythickness = 0\n" | |
| 110 " GtkWidget::focus_padding = 0\n" | |
| 111 " GtkWidget::focus-line-width = 0\n" | |
| 112 " GtkWidget::interior_focus = 0\n" | |
| 113 " GtkWidget::internal-padding = 0\n" | |
| 114 " GtkContainer::border-width = 0\n" | |
| 115 "}\n" | |
| 116 "widget \"*chrome-location-bar-entry\" " | |
| 117 "style \"chrome-location-bar-entry\""); | |
| 118 } | |
| 119 | |
| 120 // Copied from GTK+. Called when we lose the primary selection. This will clear | |
| 121 // the selection in the text buffer. | |
| 122 void ClipboardSelectionCleared(GtkClipboard* clipboard, | |
| 123 gpointer data) { | |
| 124 GtkTextIter insert; | |
| 125 GtkTextIter selection_bound; | |
| 126 GtkTextBuffer* buffer = GTK_TEXT_BUFFER(data); | |
| 127 | |
| 128 gtk_text_buffer_get_iter_at_mark(buffer, &insert, | |
| 129 gtk_text_buffer_get_insert(buffer)); | |
| 130 gtk_text_buffer_get_iter_at_mark(buffer, &selection_bound, | |
| 131 gtk_text_buffer_get_selection_bound(buffer)); | |
| 132 | |
| 133 if (!gtk_text_iter_equal(&insert, &selection_bound)) { | |
| 134 gtk_text_buffer_move_mark(buffer, | |
| 135 gtk_text_buffer_get_selection_bound(buffer), | |
| 136 &insert); | |
| 137 } | |
| 138 } | |
| 139 | |
| 140 // Returns the |menu| item whose label matches |label|. | |
| 141 guint GetPopupMenuIndexForStockLabel(const char* label, GtkMenu* menu) { | |
| 142 GList* list = gtk_container_get_children(GTK_CONTAINER(menu)); | |
| 143 guint index = 1; | |
| 144 for (GList* item = list; item != NULL; item = item->next, ++index) { | |
| 145 if (GTK_IS_IMAGE_MENU_ITEM(item->data)) { | |
| 146 gboolean is_stock = gtk_image_menu_item_get_use_stock( | |
| 147 GTK_IMAGE_MENU_ITEM(item->data)); | |
| 148 if (is_stock) { | |
| 149 std::string menu_item_label = | |
| 150 gtk_menu_item_get_label(GTK_MENU_ITEM(item->data)); | |
| 151 if (menu_item_label == label) | |
| 152 break; | |
| 153 } | |
| 154 } | |
| 155 } | |
| 156 g_list_free(list); | |
| 157 return index; | |
| 158 } | |
| 159 | |
| 160 } // namespace | |
| 161 | |
| 162 OmniboxViewGtk::OmniboxViewGtk(OmniboxEditController* controller, | |
| 163 Browser* browser, | |
| 164 Profile* profile, | |
| 165 CommandUpdater* command_updater, | |
| 166 bool popup_window_mode, | |
| 167 GtkWidget* location_bar) | |
| 168 : OmniboxView(profile, controller, command_updater), | |
| 169 browser_(browser), | |
| 170 text_view_(NULL), | |
| 171 tag_table_(NULL), | |
| 172 text_buffer_(NULL), | |
| 173 faded_text_tag_(NULL), | |
| 174 secure_scheme_tag_(NULL), | |
| 175 security_error_scheme_tag_(NULL), | |
| 176 normal_text_tag_(NULL), | |
| 177 gray_text_anchor_tag_(NULL), | |
| 178 gray_text_view_(NULL), | |
| 179 gray_text_mark_(NULL), | |
| 180 popup_window_mode_(popup_window_mode), | |
| 181 security_level_(ToolbarModel::NONE), | |
| 182 mark_set_handler_id_(0), | |
| 183 button_1_pressed_(false), | |
| 184 theme_service_(GtkThemeService::GetFrom(profile)), | |
| 185 enter_was_pressed_(false), | |
| 186 tab_was_pressed_(false), | |
| 187 paste_clipboard_requested_(false), | |
| 188 enter_was_inserted_(false), | |
| 189 selection_suggested_(false), | |
| 190 delete_was_pressed_(false), | |
| 191 delete_at_end_pressed_(false), | |
| 192 handling_key_press_(false), | |
| 193 content_maybe_changed_by_key_press_(false), | |
| 194 update_popup_without_focus_(false), | |
| 195 supports_pre_edit_(!gtk_check_version(2, 20, 0)), | |
| 196 pre_edit_size_before_change_(0), | |
| 197 going_to_focus_(NULL), | |
| 198 font_baseline_shift_(0) { | |
| 199 OmniboxPopupViewGtk* view = new OmniboxPopupViewGtk( | |
| 200 GetFont(), this, model(), location_bar); | |
| 201 view->Init(); | |
| 202 popup_view_.reset(view); | |
| 203 } | |
| 204 | |
| 205 OmniboxViewGtk::~OmniboxViewGtk() { | |
| 206 // Explicitly teardown members which have a reference to us. Just to be safe | |
| 207 // we want them to be destroyed before destroying any other internal state. | |
| 208 popup_view_.reset(); | |
| 209 | |
| 210 // We own our widget and TextView related objects. | |
| 211 if (alignment_.get()) { // Init() has been called. | |
| 212 alignment_.Destroy(); | |
| 213 g_object_unref(text_buffer_); | |
| 214 g_object_unref(tag_table_); | |
| 215 // The tags we created are owned by the tag_table, and should be destroyed | |
| 216 // along with it. We don't hold our own reference to them. | |
| 217 } | |
| 218 } | |
| 219 | |
| 220 void OmniboxViewGtk::Init() { | |
| 221 SetEntryStyle(); | |
| 222 | |
| 223 alignment_.Own(gtk_alignment_new(0.0, 0.0, 1.0, 1.0)); | |
| 224 gtk_widget_set_name(alignment_.get(), | |
| 225 "chrome-autocomplete-edit-view"); | |
| 226 | |
| 227 // The GtkTagTable and GtkTextBuffer are not initially unowned, so we have | |
| 228 // our own reference when we create them, and we own them. Adding them to | |
| 229 // the other objects adds a reference; it doesn't adopt them. | |
| 230 tag_table_ = gtk_text_tag_table_new(); | |
| 231 text_buffer_ = gtk_text_buffer_new(tag_table_); | |
| 232 g_object_set_data(G_OBJECT(text_buffer_), kOmniboxViewGtkKey, this); | |
| 233 | |
| 234 // We need to run this two handlers before undo manager's handlers, so that | |
| 235 // text iterators modified by these handlers can be passed down to undo | |
| 236 // manager's handlers. | |
| 237 g_signal_connect(text_buffer_, "delete-range", | |
| 238 G_CALLBACK(&HandleDeleteRangeThunk), this); | |
| 239 g_signal_connect(text_buffer_, "mark-set", | |
| 240 G_CALLBACK(&HandleMarkSetAlwaysThunk), this); | |
| 241 | |
| 242 text_view_ = gtk_undo_view_new(text_buffer_); | |
| 243 if (popup_window_mode_) | |
| 244 gtk_text_view_set_editable(GTK_TEXT_VIEW(text_view_), false); | |
| 245 | |
| 246 // One pixel left margin is necessary to make the cursor visible when UI | |
| 247 // language direction is LTR but |text_buffer_|'s content direction is RTL. | |
| 248 gtk_text_view_set_left_margin(GTK_TEXT_VIEW(text_view_), 1); | |
| 249 | |
| 250 // See SetEntryStyle() comments. | |
| 251 gtk_widget_set_name(text_view_, "chrome-location-bar-entry"); | |
| 252 | |
| 253 // The text view was floating. It will now be owned by the alignment. | |
| 254 gtk_container_add(GTK_CONTAINER(alignment_.get()), text_view_); | |
| 255 | |
| 256 // Do not allow inserting tab characters when pressing Tab key, so that when | |
| 257 // Tab key is pressed, |text_view_| will emit "move-focus" signal, which will | |
| 258 // be intercepted by our own handler to trigger Tab to search feature when | |
| 259 // necessary. | |
| 260 gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(text_view_), FALSE); | |
| 261 | |
| 262 faded_text_tag_ = gtk_text_buffer_create_tag(text_buffer_, | |
| 263 NULL, "foreground", kTextBaseColor, NULL); | |
| 264 secure_scheme_tag_ = gtk_text_buffer_create_tag(text_buffer_, | |
| 265 NULL, "foreground", kSecureSchemeColor, NULL); | |
| 266 security_error_scheme_tag_ = gtk_text_buffer_create_tag(text_buffer_, | |
| 267 NULL, "foreground", kSecurityErrorSchemeColor, NULL); | |
| 268 normal_text_tag_ = gtk_text_buffer_create_tag(text_buffer_, | |
| 269 NULL, "foreground", "#000000", NULL); | |
| 270 | |
| 271 // NOTE: This code used to connect to "changed", however this was fired too | |
| 272 // often and during bad times (our own buffer changes?). It works out much | |
| 273 // better to listen to end-user-action, which should be fired whenever the | |
| 274 // user makes some sort of change to the buffer. | |
| 275 g_signal_connect(text_buffer_, "begin-user-action", | |
| 276 G_CALLBACK(&HandleBeginUserActionThunk), this); | |
| 277 g_signal_connect(text_buffer_, "end-user-action", | |
| 278 G_CALLBACK(&HandleEndUserActionThunk), this); | |
| 279 // We connect to key press and release for special handling of a few keys. | |
| 280 g_signal_connect(text_view_, "key-press-event", | |
| 281 G_CALLBACK(&HandleKeyPressThunk), this); | |
| 282 g_signal_connect(text_view_, "key-release-event", | |
| 283 G_CALLBACK(&HandleKeyReleaseThunk), this); | |
| 284 g_signal_connect(text_view_, "button-press-event", | |
| 285 G_CALLBACK(&HandleViewButtonPressThunk), this); | |
| 286 g_signal_connect(text_view_, "button-release-event", | |
| 287 G_CALLBACK(&HandleViewButtonReleaseThunk), this); | |
| 288 g_signal_connect(text_view_, "focus-in-event", | |
| 289 G_CALLBACK(&HandleViewFocusInThunk), this); | |
| 290 g_signal_connect(text_view_, "focus-out-event", | |
| 291 G_CALLBACK(&HandleViewFocusOutThunk), this); | |
| 292 // NOTE: The GtkTextView documentation asks you not to connect to this | |
| 293 // signal, but it is very convenient and clean for catching up/down. | |
| 294 g_signal_connect(text_view_, "move-cursor", | |
| 295 G_CALLBACK(&HandleViewMoveCursorThunk), this); | |
| 296 g_signal_connect(text_view_, "move-focus", | |
| 297 G_CALLBACK(&HandleViewMoveFocusThunk), this); | |
| 298 // Override the size request. We want to keep the original height request | |
| 299 // from the widget, since that's font dependent. We want to ignore the width | |
| 300 // so we don't force a minimum width based on the text length. | |
| 301 g_signal_connect(text_view_, "size-request", | |
| 302 G_CALLBACK(&HandleViewSizeRequestThunk), this); | |
| 303 g_signal_connect(text_view_, "populate-popup", | |
| 304 G_CALLBACK(&HandlePopulatePopupThunk), this); | |
| 305 mark_set_handler_id_ = g_signal_connect( | |
| 306 text_buffer_, "mark-set", G_CALLBACK(&HandleMarkSetThunk), this); | |
| 307 mark_set_handler_id2_ = g_signal_connect_after( | |
| 308 text_buffer_, "mark-set", G_CALLBACK(&HandleMarkSetAfterThunk), this); | |
| 309 g_signal_connect(text_view_, "drag-data-received", | |
| 310 G_CALLBACK(&HandleDragDataReceivedThunk), this); | |
| 311 // Override the text_view_'s default drag-data-get handler by calling our own | |
| 312 // version after the normal call has happened. | |
| 313 g_signal_connect_after(text_view_, "drag-data-get", | |
| 314 G_CALLBACK(&HandleDragDataGetThunk), this); | |
| 315 g_signal_connect_after(text_view_, "drag-begin", | |
| 316 G_CALLBACK(&HandleDragBeginThunk), this); | |
| 317 g_signal_connect_after(text_view_, "drag-end", | |
| 318 G_CALLBACK(&HandleDragEndThunk), this); | |
| 319 g_signal_connect(text_view_, "backspace", | |
| 320 G_CALLBACK(&HandleBackSpaceThunk), this); | |
| 321 g_signal_connect(text_view_, "copy-clipboard", | |
| 322 G_CALLBACK(&HandleCopyClipboardThunk), this); | |
| 323 g_signal_connect(text_view_, "cut-clipboard", | |
| 324 G_CALLBACK(&HandleCutClipboardThunk), this); | |
| 325 g_signal_connect(text_view_, "paste-clipboard", | |
| 326 G_CALLBACK(&HandlePasteClipboardThunk), this); | |
| 327 g_signal_connect(text_view_, "expose-event", | |
| 328 G_CALLBACK(&HandleExposeEventThunk), this); | |
| 329 g_signal_connect_after(text_view_, "expose-event", | |
| 330 G_CALLBACK(&HandleExposeEventAfterThunk), this); | |
| 331 g_signal_connect(text_view_, "direction-changed", | |
| 332 G_CALLBACK(&HandleWidgetDirectionChangedThunk), this); | |
| 333 g_signal_connect(text_view_, "delete-from-cursor", | |
| 334 G_CALLBACK(&HandleDeleteFromCursorThunk), this); | |
| 335 g_signal_connect(text_view_, "hierarchy-changed", | |
| 336 G_CALLBACK(&HandleHierarchyChangedThunk), this); | |
| 337 if (supports_pre_edit_) { | |
| 338 g_signal_connect(text_view_, "preedit-changed", | |
| 339 G_CALLBACK(&HandlePreEditChangedThunk), this); | |
| 340 } | |
| 341 g_signal_connect(text_view_, "undo", G_CALLBACK(&HandleUndoRedoThunk), this); | |
| 342 g_signal_connect(text_view_, "redo", G_CALLBACK(&HandleUndoRedoThunk), this); | |
| 343 g_signal_connect_after(text_view_, "undo", | |
| 344 G_CALLBACK(&HandleUndoRedoAfterThunk), this); | |
| 345 g_signal_connect_after(text_view_, "redo", | |
| 346 G_CALLBACK(&HandleUndoRedoAfterThunk), this); | |
| 347 g_signal_connect(text_view_, "destroy", | |
| 348 G_CALLBACK(>k_widget_destroyed), &text_view_); | |
| 349 | |
| 350 // Setup for the gray suggestion text view. | |
| 351 // GtkLabel is used instead of GtkTextView to get transparent background. | |
| 352 gray_text_view_ = gtk_label_new(NULL); | |
| 353 gtk_widget_set_no_show_all(gray_text_view_, TRUE); | |
| 354 gtk_label_set_selectable(GTK_LABEL(gray_text_view_), TRUE); | |
| 355 | |
| 356 GtkTextIter end_iter; | |
| 357 gtk_text_buffer_get_end_iter(text_buffer_, &end_iter); | |
| 358 | |
| 359 // Insert a Zero Width Space character just before the gray text anchor. | |
| 360 // It's a hack to workaround a bug of GtkTextView which can not align the | |
| 361 // pre-edit string and a child anchor correctly when there is no other content | |
| 362 // around the pre-edit string. | |
| 363 gtk_text_buffer_insert(text_buffer_, &end_iter, "\342\200\213", -1); | |
| 364 GtkTextChildAnchor* gray_text_anchor = | |
| 365 gtk_text_buffer_create_child_anchor(text_buffer_, &end_iter); | |
| 366 | |
| 367 gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(text_view_), | |
| 368 gray_text_view_, | |
| 369 gray_text_anchor); | |
| 370 | |
| 371 gray_text_anchor_tag_ = gtk_text_buffer_create_tag(text_buffer_, NULL, NULL); | |
| 372 | |
| 373 GtkTextIter anchor_iter; | |
| 374 gtk_text_buffer_get_iter_at_child_anchor(text_buffer_, &anchor_iter, | |
| 375 gray_text_anchor); | |
| 376 gtk_text_buffer_apply_tag(text_buffer_, gray_text_anchor_tag_, | |
| 377 &anchor_iter, &end_iter); | |
| 378 | |
| 379 GtkTextIter start_iter; | |
| 380 gtk_text_buffer_get_start_iter(text_buffer_, &start_iter); | |
| 381 gray_text_mark_ = | |
| 382 gtk_text_buffer_create_mark(text_buffer_, NULL, &start_iter, FALSE); | |
| 383 | |
| 384 // Hooking up this handler after setting up above hacks for gray text view, so | |
| 385 // that we won't filter out the special ZWP mark itself. | |
| 386 g_signal_connect(text_buffer_, "insert-text", | |
| 387 G_CALLBACK(&HandleInsertTextThunk), this); | |
| 388 | |
| 389 AdjustVerticalAlignmentOfGrayTextView(); | |
| 390 | |
| 391 registrar_.Add(this, | |
| 392 chrome::NOTIFICATION_BROWSER_THEME_CHANGED, | |
| 393 content::Source<ThemeService>(theme_service_)); | |
| 394 theme_service_->InitThemesFor(this); | |
| 395 | |
| 396 ViewIDUtil::SetID(GetNativeView(), VIEW_ID_OMNIBOX); | |
| 397 } | |
| 398 | |
| 399 void OmniboxViewGtk::HandleHierarchyChanged(GtkWidget* sender, | |
| 400 GtkWidget* old_toplevel) { | |
| 401 GtkWindow* new_toplevel = platform_util::GetTopLevel(sender); | |
| 402 if (!new_toplevel) | |
| 403 return; | |
| 404 | |
| 405 // Use |signals_| to make sure we don't get called back after destruction. | |
| 406 signals_.Connect(new_toplevel, "set-focus", | |
| 407 G_CALLBACK(&HandleWindowSetFocusThunk), this); | |
| 408 } | |
| 409 | |
| 410 void OmniboxViewGtk::SetFocus() { | |
| 411 DCHECK(text_view_); | |
| 412 gtk_widget_grab_focus(text_view_); | |
| 413 // Restore caret visibility if focus is explicitly requested. This is | |
| 414 // necessary because if we already have invisible focus, the RequestFocus() | |
| 415 // call above will short-circuit, preventing us from reaching | |
| 416 // OmniboxEditModel::OnSetFocus(), which handles restoring visibility when the | |
| 417 // omnibox regains focus after losing focus. | |
| 418 model()->SetCaretVisibility(true); | |
| 419 } | |
| 420 | |
| 421 void OmniboxViewGtk::ApplyCaretVisibility() { | |
| 422 gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(text_view_), | |
| 423 model()->is_caret_visible()); | |
| 424 } | |
| 425 | |
| 426 void OmniboxViewGtk::SaveStateToTab(WebContents* tab) { | |
| 427 DCHECK(tab); | |
| 428 // If any text has been selected, register it as the PRIMARY selection so it | |
| 429 // can still be pasted via middle-click after the text view is cleared. | |
| 430 if (!selected_text_.empty()) | |
| 431 SavePrimarySelection(selected_text_); | |
| 432 // NOTE: GetStateForTabSwitch may affect GetSelection, so order is important. | |
| 433 OmniboxEditModel::State model_state = model()->GetStateForTabSwitch(); | |
| 434 tab->SetUserData( | |
| 435 kAutocompleteEditStateKey, | |
| 436 new AutocompleteEditState(model_state, ViewState(GetSelection()))); | |
| 437 } | |
| 438 | |
| 439 void OmniboxViewGtk::OnTabChanged(const WebContents* web_contents) { | |
| 440 security_level_ = controller()->GetToolbarModel()->GetSecurityLevel(false); | |
| 441 selected_text_.clear(); | |
| 442 | |
| 443 const AutocompleteEditState* state = static_cast<AutocompleteEditState*>( | |
| 444 web_contents->GetUserData(&kAutocompleteEditStateKey)); | |
| 445 model()->RestoreState(state ? &state->model_state : NULL); | |
| 446 if (state) { | |
| 447 // Move the marks for the cursor and the other end of the selection to the | |
| 448 // previously-saved offsets (but preserve PRIMARY). | |
| 449 StartUpdatingHighlightedText(); | |
| 450 SetSelectedRange(state->view_state.selection_range); | |
| 451 FinishUpdatingHighlightedText(); | |
| 452 } | |
| 453 } | |
| 454 | |
| 455 void OmniboxViewGtk::Update() { | |
| 456 const ToolbarModel::SecurityLevel old_security_level = security_level_; | |
| 457 security_level_ = controller()->GetToolbarModel()->GetSecurityLevel(false); | |
| 458 if (model()->UpdatePermanentText()) { | |
| 459 // Something visibly changed. Re-enable URL replacement. | |
| 460 controller()->GetToolbarModel()->set_url_replacement_enabled(true); | |
| 461 model()->UpdatePermanentText(); | |
| 462 | |
| 463 RevertAll(); | |
| 464 } else if (old_security_level != security_level_) { | |
| 465 EmphasizeURLComponents(); | |
| 466 } | |
| 467 } | |
| 468 | |
| 469 base::string16 OmniboxViewGtk::GetText() const { | |
| 470 GtkTextIter start, end; | |
| 471 GetTextBufferBounds(&start, &end); | |
| 472 gchar* utf8 = gtk_text_buffer_get_text(text_buffer_, &start, &end, false); | |
| 473 base::string16 out(base::UTF8ToUTF16(utf8)); | |
| 474 g_free(utf8); | |
| 475 | |
| 476 if (supports_pre_edit_) { | |
| 477 // We need to treat the text currently being composed by the input method | |
| 478 // as part of the text content, so that omnibox can work correctly in the | |
| 479 // middle of composition. | |
| 480 if (pre_edit_.size()) { | |
| 481 GtkTextMark* mark = gtk_text_buffer_get_insert(text_buffer_); | |
| 482 gtk_text_buffer_get_iter_at_mark(text_buffer_, &start, mark); | |
| 483 out.insert(gtk_text_iter_get_offset(&start), pre_edit_); | |
| 484 } | |
| 485 } | |
| 486 return out; | |
| 487 } | |
| 488 | |
| 489 void OmniboxViewGtk::SetWindowTextAndCaretPos(const base::string16& text, | |
| 490 size_t caret_pos, | |
| 491 bool update_popup, | |
| 492 bool notify_text_changed) { | |
| 493 CharRange range(static_cast<int>(caret_pos), static_cast<int>(caret_pos)); | |
| 494 SetTextAndSelectedRange(text, range); | |
| 495 | |
| 496 if (update_popup) | |
| 497 UpdatePopup(); | |
| 498 | |
| 499 if (notify_text_changed) | |
| 500 TextChanged(); | |
| 501 } | |
| 502 | |
| 503 void OmniboxViewGtk::SetForcedQuery() { | |
| 504 const base::string16 current_text(GetText()); | |
| 505 const size_t start = current_text.find_first_not_of(base::kWhitespaceUTF16); | |
| 506 if (start == base::string16::npos || (current_text[start] != '?')) { | |
| 507 SetUserText(base::ASCIIToUTF16("?")); | |
| 508 } else { | |
| 509 StartUpdatingHighlightedText(); | |
| 510 SetSelectedRange(CharRange(current_text.size(), start + 1)); | |
| 511 FinishUpdatingHighlightedText(); | |
| 512 } | |
| 513 } | |
| 514 | |
| 515 bool OmniboxViewGtk::IsSelectAll() const { | |
| 516 GtkTextIter sel_start, sel_end; | |
| 517 gtk_text_buffer_get_selection_bounds(text_buffer_, &sel_start, &sel_end); | |
| 518 | |
| 519 GtkTextIter start, end; | |
| 520 GetTextBufferBounds(&start, &end); | |
| 521 | |
| 522 // Returns true if the |text_buffer_| is empty. | |
| 523 return gtk_text_iter_equal(&start, &sel_start) && | |
| 524 gtk_text_iter_equal(&end, &sel_end); | |
| 525 } | |
| 526 | |
| 527 bool OmniboxViewGtk::DeleteAtEndPressed() { | |
| 528 return delete_at_end_pressed_; | |
| 529 } | |
| 530 | |
| 531 void OmniboxViewGtk::GetSelectionBounds(base::string16::size_type* start, | |
| 532 base::string16::size_type* end) const { | |
| 533 CharRange selection = GetSelection(); | |
| 534 *start = static_cast<size_t>(selection.cp_min); | |
| 535 *end = static_cast<size_t>(selection.cp_max); | |
| 536 } | |
| 537 | |
| 538 void OmniboxViewGtk::SelectAll(bool reversed) { | |
| 539 // SelectAll() is invoked as a side effect of other actions (e.g. switching | |
| 540 // tabs or hitting Escape) in autocomplete_edit.cc, so we don't update the | |
| 541 // PRIMARY selection here. | |
| 542 SelectAllInternal(reversed, false); | |
| 543 } | |
| 544 | |
| 545 void OmniboxViewGtk::UpdatePopup() { | |
| 546 model()->SetInputInProgress(true); | |
| 547 if (!update_popup_without_focus_ && !model()->has_focus()) | |
| 548 return; | |
| 549 | |
| 550 // Don't inline autocomplete when the caret/selection isn't at the end of | |
| 551 // the text, or in the middle of composition. | |
| 552 CharRange sel = GetSelection(); | |
| 553 bool no_inline_autocomplete = | |
| 554 std::max(sel.cp_max, sel.cp_min) < GetOmniboxTextLength() || | |
| 555 IsImeComposing(); | |
| 556 model()->StartAutocomplete(sel.cp_min != sel.cp_max, no_inline_autocomplete); | |
| 557 } | |
| 558 | |
| 559 void OmniboxViewGtk::OnTemporaryTextMaybeChanged( | |
| 560 const base::string16& display_text, | |
| 561 bool save_original_selection, | |
| 562 bool notify_text_changed) { | |
| 563 if (save_original_selection) | |
| 564 saved_temporary_selection_ = GetSelection(); | |
| 565 | |
| 566 StartUpdatingHighlightedText(); | |
| 567 SetWindowTextAndCaretPos(display_text, display_text.length(), false, false); | |
| 568 FinishUpdatingHighlightedText(); | |
| 569 if (notify_text_changed) | |
| 570 TextChanged(); | |
| 571 } | |
| 572 | |
| 573 bool OmniboxViewGtk::OnInlineAutocompleteTextMaybeChanged( | |
| 574 const base::string16& display_text, | |
| 575 size_t user_text_length) { | |
| 576 if (display_text == GetText()) | |
| 577 return false; | |
| 578 | |
| 579 StartUpdatingHighlightedText(); | |
| 580 CharRange range(display_text.size(), user_text_length); | |
| 581 SetTextAndSelectedRange(display_text, range); | |
| 582 FinishUpdatingHighlightedText(); | |
| 583 TextChanged(); | |
| 584 return true; | |
| 585 } | |
| 586 | |
| 587 void OmniboxViewGtk::OnInlineAutocompleteTextCleared() { | |
| 588 } | |
| 589 | |
| 590 void OmniboxViewGtk::OnRevertTemporaryText() { | |
| 591 StartUpdatingHighlightedText(); | |
| 592 SetSelectedRange(saved_temporary_selection_); | |
| 593 FinishUpdatingHighlightedText(); | |
| 594 // We got here because the user hit the Escape key. We explicitly don't call | |
| 595 // TextChanged(), since OmniboxPopupModel::ResetToDefaultMatch() has already | |
| 596 // been called by now, and it would've called TextChanged() if it was | |
| 597 // warranted. | |
| 598 } | |
| 599 | |
| 600 void OmniboxViewGtk::OnBeforePossibleChange() { | |
| 601 // Record this paste, so we can do different behavior. | |
| 602 if (paste_clipboard_requested_) { | |
| 603 paste_clipboard_requested_ = false; | |
| 604 model()->OnPaste(); | |
| 605 } | |
| 606 | |
| 607 // This method will be called in HandleKeyPress() method just before | |
| 608 // handling a key press event. So we should prevent it from being called | |
| 609 // when handling the key press event. | |
| 610 if (handling_key_press_) | |
| 611 return; | |
| 612 | |
| 613 // Record our state. | |
| 614 text_before_change_ = GetText(); | |
| 615 sel_before_change_ = GetSelection(); | |
| 616 if (supports_pre_edit_) | |
| 617 pre_edit_size_before_change_ = pre_edit_.size(); | |
| 618 } | |
| 619 | |
| 620 // TODO(deanm): This is mostly stolen from Windows, and will need some work. | |
| 621 bool OmniboxViewGtk::OnAfterPossibleChange() { | |
| 622 // This method will be called in HandleKeyPress() method just after | |
| 623 // handling a key press event. So we should prevent it from being called | |
| 624 // when handling the key press event. | |
| 625 if (handling_key_press_) { | |
| 626 content_maybe_changed_by_key_press_ = true; | |
| 627 return false; | |
| 628 } | |
| 629 | |
| 630 // If the change is caused by an Enter key press event, and the event was not | |
| 631 // handled by IME, then it's an unexpected change and shall be reverted here. | |
| 632 // {Start|Finish}UpdatingHighlightedText() are called here to prevent the | |
| 633 // PRIMARY selection from being changed. | |
| 634 if (enter_was_pressed_ && enter_was_inserted_) { | |
| 635 StartUpdatingHighlightedText(); | |
| 636 SetTextAndSelectedRange(text_before_change_, sel_before_change_); | |
| 637 FinishUpdatingHighlightedText(); | |
| 638 return false; | |
| 639 } | |
| 640 | |
| 641 const CharRange new_sel = GetSelection(); | |
| 642 const int length = GetOmniboxTextLength(); | |
| 643 const bool selection_differs = | |
| 644 ((new_sel.cp_min != new_sel.cp_max) || | |
| 645 (sel_before_change_.cp_min != sel_before_change_.cp_max)) && | |
| 646 ((new_sel.cp_min != sel_before_change_.cp_min) || | |
| 647 (new_sel.cp_max != sel_before_change_.cp_max)); | |
| 648 const bool at_end_of_edit = | |
| 649 (new_sel.cp_min == length && new_sel.cp_max == length); | |
| 650 | |
| 651 // See if the text or selection have changed since OnBeforePossibleChange(). | |
| 652 const base::string16 new_text(GetText()); | |
| 653 text_changed_ = (new_text != text_before_change_) || (supports_pre_edit_ && | |
| 654 (pre_edit_.size() != pre_edit_size_before_change_)); | |
| 655 | |
| 656 if (text_changed_) | |
| 657 AdjustTextJustification(); | |
| 658 | |
| 659 // When the user has deleted text, we don't allow inline autocomplete. Make | |
| 660 // sure to not flag cases like selecting part of the text and then pasting | |
| 661 // (or typing) the prefix of that selection. (We detect these by making | |
| 662 // sure the caret, which should be after any insertion, hasn't moved | |
| 663 // forward of the old selection start.) | |
| 664 const bool just_deleted_text = | |
| 665 (text_before_change_.length() > new_text.length()) && | |
| 666 (new_sel.cp_min <= std::min(sel_before_change_.cp_min, | |
| 667 sel_before_change_.cp_max)); | |
| 668 | |
| 669 delete_at_end_pressed_ = false; | |
| 670 | |
| 671 const bool something_changed = model()->OnAfterPossibleChange( | |
| 672 text_before_change_, new_text, new_sel.selection_min(), | |
| 673 new_sel.selection_max(), selection_differs, text_changed_, | |
| 674 just_deleted_text, !IsImeComposing()); | |
| 675 | |
| 676 // If only selection was changed, we don't need to call the controller's | |
| 677 // OnChanged() method, which is called in TextChanged(). | |
| 678 // But we still need to call EmphasizeURLComponents() to make sure the text | |
| 679 // attributes are updated correctly. | |
| 680 if (something_changed && text_changed_) { | |
| 681 TextChanged(); | |
| 682 } else if (selection_differs) { | |
| 683 EmphasizeURLComponents(); | |
| 684 } else if (delete_was_pressed_ && at_end_of_edit) { | |
| 685 delete_at_end_pressed_ = true; | |
| 686 model()->OnChanged(); | |
| 687 } | |
| 688 delete_was_pressed_ = false; | |
| 689 | |
| 690 return something_changed; | |
| 691 } | |
| 692 | |
| 693 gfx::NativeView OmniboxViewGtk::GetNativeView() const { | |
| 694 return alignment_.get(); | |
| 695 } | |
| 696 | |
| 697 gfx::NativeView OmniboxViewGtk::GetRelativeWindowForPopup() const { | |
| 698 GtkWidget* toplevel = gtk_widget_get_toplevel(GetNativeView()); | |
| 699 DCHECK(gtk_widget_is_toplevel(toplevel)); | |
| 700 return toplevel; | |
| 701 } | |
| 702 | |
| 703 void OmniboxViewGtk::SetGrayTextAutocompletion( | |
| 704 const base::string16& suggestion) { | |
| 705 std::string suggestion_utf8 = base::UTF16ToUTF8(suggestion); | |
| 706 | |
| 707 gtk_label_set_text(GTK_LABEL(gray_text_view_), suggestion_utf8.c_str()); | |
| 708 | |
| 709 if (suggestion.empty()) { | |
| 710 gtk_widget_hide(gray_text_view_); | |
| 711 return; | |
| 712 } | |
| 713 | |
| 714 gtk_widget_show(gray_text_view_); | |
| 715 AdjustVerticalAlignmentOfGrayTextView(); | |
| 716 UpdateGrayTextViewColors(); | |
| 717 } | |
| 718 | |
| 719 base::string16 OmniboxViewGtk::GetGrayTextAutocompletion() const { | |
| 720 const gchar* suggestion = gtk_label_get_text(GTK_LABEL(gray_text_view_)); | |
| 721 return suggestion ? base::UTF8ToUTF16(suggestion) : base::string16(); | |
| 722 } | |
| 723 | |
| 724 int OmniboxViewGtk::GetTextWidth() const { | |
| 725 // TextWidth may be called after gtk widget tree is destroyed but | |
| 726 // before OmniboxViewGtk gets deleted. This is a safe guard | |
| 727 // to avoid accessing |text_view_| that has already been destroyed. | |
| 728 // See crbug.com/70192. | |
| 729 if (!text_view_) | |
| 730 return 0; | |
| 731 | |
| 732 int horizontal_border_size = | |
| 733 gtk_text_view_get_border_window_size(GTK_TEXT_VIEW(text_view_), | |
| 734 GTK_TEXT_WINDOW_LEFT) + | |
| 735 gtk_text_view_get_border_window_size(GTK_TEXT_VIEW(text_view_), | |
| 736 GTK_TEXT_WINDOW_RIGHT) + | |
| 737 gtk_text_view_get_left_margin(GTK_TEXT_VIEW(text_view_)) + | |
| 738 gtk_text_view_get_right_margin(GTK_TEXT_VIEW(text_view_)); | |
| 739 | |
| 740 GtkTextIter start, end; | |
| 741 GdkRectangle first_char_bounds, last_char_bounds; | |
| 742 gtk_text_buffer_get_start_iter(text_buffer_, &start); | |
| 743 | |
| 744 // Use the real end iterator here to take the width of gray suggestion text | |
| 745 // into account, so that location bar can layout its children correctly. | |
| 746 gtk_text_buffer_get_end_iter(text_buffer_, &end); | |
| 747 gtk_text_view_get_iter_location(GTK_TEXT_VIEW(text_view_), | |
| 748 &start, &first_char_bounds); | |
| 749 gtk_text_view_get_iter_location(GTK_TEXT_VIEW(text_view_), | |
| 750 &end, &last_char_bounds); | |
| 751 | |
| 752 gint first_char_start = first_char_bounds.x; | |
| 753 gint first_char_end = first_char_start + first_char_bounds.width; | |
| 754 gint last_char_start = last_char_bounds.x; | |
| 755 gint last_char_end = last_char_start + last_char_bounds.width; | |
| 756 | |
| 757 // bounds width could be negative for RTL text. | |
| 758 if (first_char_start > first_char_end) | |
| 759 std::swap(first_char_start, first_char_end); | |
| 760 if (last_char_start > last_char_end) | |
| 761 std::swap(last_char_start, last_char_end); | |
| 762 | |
| 763 gint text_width = first_char_start < last_char_start ? | |
| 764 last_char_end - first_char_start : first_char_end - last_char_start; | |
| 765 | |
| 766 return text_width + horizontal_border_size; | |
| 767 } | |
| 768 | |
| 769 int OmniboxViewGtk::GetWidth() const { | |
| 770 GtkAllocation allocation; | |
| 771 gtk_widget_get_allocation(text_view_, &allocation); | |
| 772 return allocation.width; | |
| 773 } | |
| 774 | |
| 775 bool OmniboxViewGtk::IsImeComposing() const { | |
| 776 return supports_pre_edit_ && !pre_edit_.empty(); | |
| 777 } | |
| 778 | |
| 779 void OmniboxViewGtk::Observe(int type, | |
| 780 const content::NotificationSource& source, | |
| 781 const content::NotificationDetails& details) { | |
| 782 DCHECK(type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED); | |
| 783 | |
| 784 OnBrowserThemeChanged(); | |
| 785 } | |
| 786 | |
| 787 void OmniboxViewGtk::UpdateGrayTextViewColors() { | |
| 788 GdkColor faded_text; | |
| 789 if (theme_service_->UsingNativeTheme()) { | |
| 790 GtkStyle* style = gtk_rc_get_style(gray_text_view_); | |
| 791 faded_text = gtk_util::AverageColors( | |
| 792 style->text[GTK_STATE_NORMAL], style->base[GTK_STATE_NORMAL]); | |
| 793 } else { | |
| 794 gdk_color_parse(kTextBaseColor, &faded_text); | |
| 795 } | |
| 796 gtk_widget_modify_fg(gray_text_view_, GTK_STATE_NORMAL, &faded_text); | |
| 797 } | |
| 798 | |
| 799 void OmniboxViewGtk::HandleBeginUserAction(GtkTextBuffer* sender) { | |
| 800 OnBeforePossibleChange(); | |
| 801 } | |
| 802 | |
| 803 void OmniboxViewGtk::HandleEndUserAction(GtkTextBuffer* sender) { | |
| 804 OnAfterPossibleChange(); | |
| 805 } | |
| 806 | |
| 807 gboolean OmniboxViewGtk::HandleKeyPress(GtkWidget* widget, GdkEventKey* event) { | |
| 808 // Background of this piece of complicated code: | |
| 809 // The omnibox supports several special behaviors which may be triggered by | |
| 810 // certain key events: | |
| 811 // Tab to search - triggered by Tab key | |
| 812 // Accept input - triggered by Enter key | |
| 813 // Revert input - triggered by Escape key | |
| 814 // | |
| 815 // Because we use a GtkTextView object |text_view_| for text input, we need | |
| 816 // send all key events to |text_view_| before handling them, to make sure | |
| 817 // IME works without any problem. So here, we intercept "key-press-event" | |
| 818 // signal of |text_view_| object and call its default handler to handle the | |
| 819 // key event first. | |
| 820 // | |
| 821 // Then if the key event is one of Tab, Enter and Escape, we need to trigger | |
| 822 // the corresponding special behavior if IME did not handle it. | |
| 823 // For Escape key, if the default signal handler returns FALSE, then we know | |
| 824 // it's not handled by IME. | |
| 825 // | |
| 826 // For Tab key, as "accepts-tab" property of |text_view_| is set to FALSE, | |
| 827 // if IME did not handle it then "move-focus" signal will be emitted by the | |
| 828 // default signal handler of |text_view_|. So we can intercept "move-focus" | |
| 829 // signal of |text_view_| to know if a Tab key press event was handled by IME, | |
| 830 // and trigger Tab to search or result traversal behavior when necessary in | |
| 831 // the signal handler. | |
| 832 // | |
| 833 // But for Enter key, if IME did not handle the key event, the default signal | |
| 834 // handler will delete current selection range and insert '\n' and always | |
| 835 // return TRUE. We need to prevent |text_view_| from performing this default | |
| 836 // action if IME did not handle the key event, because we don't want the | |
| 837 // content of omnibox to be changed before triggering our special behavior. | |
| 838 // Otherwise our special behavior would not be performed correctly. | |
| 839 // | |
| 840 // But there is no way for us to prevent GtkTextView from handling the key | |
| 841 // event and performing built-in operation. So in order to achieve our goal, | |
| 842 // "insert-text" signal of |text_buffer_| object is intercepted, and | |
| 843 // following actions are done in the signal handler: | |
| 844 // - If there is only one character in inserted text, and it's '\n' or '\r', | |
| 845 // then set |enter_was_inserted_| to true. | |
| 846 // - Filter out all new line and tab characters. | |
| 847 // | |
| 848 // So if |enter_was_inserted_| is true after calling |text_view_|'s default | |
| 849 // signal handler against an Enter key press event, then we know that the | |
| 850 // Enter key press event was handled by GtkTextView rather than IME, and can | |
| 851 // perform the special behavior for Enter key safely. | |
| 852 // | |
| 853 // Now the last thing is to prevent the content of omnibox from being changed | |
| 854 // by GtkTextView when Enter key is pressed. As OnBeforePossibleChange() and | |
| 855 // OnAfterPossibleChange() will be called by GtkTextView before and after | |
| 856 // changing the content, and the content is already saved in | |
| 857 // OnBeforePossibleChange(), so if the Enter key press event was not handled | |
| 858 // by IME, it's easy to restore the content in OnAfterPossibleChange(), as if | |
| 859 // it's not changed at all. | |
| 860 | |
| 861 GtkWidgetClass* klass = GTK_WIDGET_GET_CLASS(widget); | |
| 862 | |
| 863 enter_was_pressed_ = event->keyval == GDK_Return || | |
| 864 event->keyval == GDK_ISO_Enter || | |
| 865 event->keyval == GDK_KP_Enter; | |
| 866 | |
| 867 // Set |tab_was_pressed_| to true if it's a Tab key press event, so that our | |
| 868 // handler of "move-focus" signal can trigger Tab to search behavior when | |
| 869 // necessary. | |
| 870 tab_was_pressed_ = (event->keyval == GDK_Tab || | |
| 871 event->keyval == GDK_ISO_Left_Tab || | |
| 872 event->keyval == GDK_KP_Tab) && | |
| 873 !(event->state & GDK_CONTROL_MASK); | |
| 874 | |
| 875 shift_was_pressed_ = event->state & GDK_SHIFT_MASK; | |
| 876 | |
| 877 delete_was_pressed_ = event->keyval == GDK_Delete || | |
| 878 event->keyval == GDK_KP_Delete; | |
| 879 | |
| 880 // Reset |enter_was_inserted_|, which may be set in the "insert-text" signal | |
| 881 // handler, so that we'll know if an Enter key event was handled by IME. | |
| 882 enter_was_inserted_ = false; | |
| 883 | |
| 884 // Reset |paste_clipboard_requested_| to make sure we won't misinterpret this | |
| 885 // key input action as a paste action. | |
| 886 paste_clipboard_requested_ = false; | |
| 887 | |
| 888 // Reset |text_changed_| before passing the key event on to the text view. | |
| 889 text_changed_ = false; | |
| 890 | |
| 891 OnBeforePossibleChange(); | |
| 892 handling_key_press_ = true; | |
| 893 content_maybe_changed_by_key_press_ = false; | |
| 894 | |
| 895 // Call the default handler, so that IME can work as normal. | |
| 896 // New line characters will be filtered out by our "insert-text" | |
| 897 // signal handler attached to |text_buffer_| object. | |
| 898 gboolean result = klass->key_press_event(widget, event); | |
| 899 | |
| 900 handling_key_press_ = false; | |
| 901 if (content_maybe_changed_by_key_press_) | |
| 902 OnAfterPossibleChange(); | |
| 903 | |
| 904 // Set |tab_was_pressed_| to false, to make sure Tab to search behavior can | |
| 905 // only be triggered by pressing Tab key. | |
| 906 tab_was_pressed_ = false; | |
| 907 | |
| 908 if (enter_was_pressed_ && enter_was_inserted_) { | |
| 909 bool alt_held = (event->state & GDK_MOD1_MASK); | |
| 910 model()->AcceptInput(alt_held ? NEW_FOREGROUND_TAB : CURRENT_TAB, false); | |
| 911 result = TRUE; | |
| 912 } else if (!result && event->keyval == GDK_Escape && | |
| 913 (event->state & gtk_accelerator_get_default_mod_mask()) == 0) { | |
| 914 // We can handle the Escape key if |text_view_| did not handle it. | |
| 915 // If it's not handled by us, then we need to propagate it up to the parent | |
| 916 // widgets, so that Escape accelerator can still work. | |
| 917 result = model()->OnEscapeKeyPressed(); | |
| 918 } else if (event->keyval == GDK_Control_L || event->keyval == GDK_Control_R) { | |
| 919 // Omnibox2 can switch its contents while pressing a control key. To switch | |
| 920 // the contents of omnibox2, we notify the OmniboxEditModel class when the | |
| 921 // control-key state is changed. | |
| 922 model()->OnControlKeyChanged(true); | |
| 923 } else if (!text_changed_ && event->keyval == GDK_Delete && | |
| 924 event->state & GDK_SHIFT_MASK) { | |
| 925 // If shift+del didn't change the text, we let this delete an entry from | |
| 926 // the popup. We can't check to see if the IME handled it because even if | |
| 927 // nothing is selected, the IME or the TextView still report handling it. | |
| 928 if (model()->popup_model()->IsOpen()) | |
| 929 model()->popup_model()->TryDeletingCurrentItem(); | |
| 930 } | |
| 931 | |
| 932 // Set |enter_was_pressed_| to false, to make sure OnAfterPossibleChange() can | |
| 933 // act as normal for changes made by other events. | |
| 934 enter_was_pressed_ = false; | |
| 935 | |
| 936 // If the key event is not handled by |text_view_| or us, then we need to | |
| 937 // propagate the key event up to parent widgets by returning FALSE. | |
| 938 // In this case we need to stop the signal emission explicitly to prevent the | |
| 939 // default "key-press-event" handler of |text_view_| from being called again. | |
| 940 if (!result) { | |
| 941 static guint signal_id = | |
| 942 g_signal_lookup("key-press-event", GTK_TYPE_WIDGET); | |
| 943 g_signal_stop_emission(widget, signal_id, 0); | |
| 944 } | |
| 945 | |
| 946 return result; | |
| 947 } | |
| 948 | |
| 949 gboolean OmniboxViewGtk::HandleKeyRelease(GtkWidget* widget, | |
| 950 GdkEventKey* event) { | |
| 951 // Omnibox2 can switch its contents while pressing a control key. To switch | |
| 952 // the contents of omnibox2, we notify the OmniboxEditModel class when the | |
| 953 // control-key state is changed. | |
| 954 if (event->keyval == GDK_Control_L || event->keyval == GDK_Control_R) { | |
| 955 // Round trip to query the control state after the release. This allows | |
| 956 // you to release one control key while still holding another control key. | |
| 957 GdkDisplay* display = gdk_window_get_display(event->window); | |
| 958 GdkModifierType mod; | |
| 959 gdk_display_get_pointer(display, NULL, NULL, NULL, &mod); | |
| 960 if (!(mod & GDK_CONTROL_MASK)) | |
| 961 model()->OnControlKeyChanged(false); | |
| 962 } | |
| 963 | |
| 964 // Even though we handled the press ourselves, let GtkTextView handle the | |
| 965 // release. It shouldn't do anything particularly interesting, but it will | |
| 966 // handle the IME work for us. | |
| 967 return FALSE; // Propagate into GtkTextView. | |
| 968 } | |
| 969 | |
| 970 gboolean OmniboxViewGtk::HandleViewButtonPress(GtkWidget* sender, | |
| 971 GdkEventButton* event) { | |
| 972 // We don't need to care about double and triple clicks. | |
| 973 if (event->type != GDK_BUTTON_PRESS) | |
| 974 return FALSE; | |
| 975 | |
| 976 DCHECK(text_view_); | |
| 977 | |
| 978 // Restore caret visibility whenever the user clicks in the omnibox in a way | |
| 979 // that would give it focus. We must handle this case separately here because | |
| 980 // if the omnibox currently has invisible focus, the mouse event won't trigger | |
| 981 // either SetFocus() or OmniboxEditModel::OnSetFocus(). | |
| 982 if (event->button == 1 || event->button == 2) | |
| 983 model()->SetCaretVisibility(true); | |
| 984 | |
| 985 if (event->button == 1) { | |
| 986 button_1_pressed_ = true; | |
| 987 | |
| 988 // Button press event may change the selection, we need to record the change | |
| 989 // and report it to model() later when button is released. | |
| 990 OnBeforePossibleChange(); | |
| 991 } else if (event->button == 2) { | |
| 992 // GtkTextView pastes PRIMARY selection with middle click. | |
| 993 // We can't call model()->on_paste_replacing_all() here, because the actual | |
| 994 // paste clipboard action may not be performed if the clipboard is empty. | |
| 995 paste_clipboard_requested_ = true; | |
| 996 } | |
| 997 return FALSE; | |
| 998 } | |
| 999 | |
| 1000 gboolean OmniboxViewGtk::HandleViewButtonRelease(GtkWidget* sender, | |
| 1001 GdkEventButton* event) { | |
| 1002 if (event->button != 1) | |
| 1003 return FALSE; | |
| 1004 | |
| 1005 bool button_1_was_pressed = button_1_pressed_; | |
| 1006 button_1_pressed_ = false; | |
| 1007 | |
| 1008 DCHECK(text_view_); | |
| 1009 | |
| 1010 // Call the GtkTextView default handler, ignoring the fact that it will | |
| 1011 // likely have told us to stop propagating. We want to handle selection. | |
| 1012 GtkWidgetClass* klass = GTK_WIDGET_GET_CLASS(text_view_); | |
| 1013 klass->button_release_event(text_view_, event); | |
| 1014 | |
| 1015 // Inform model() about possible text selection change. We may get a button | |
| 1016 // release with no press (e.g. if the user clicks in the omnibox to dismiss a | |
| 1017 // bubble). | |
| 1018 if (button_1_was_pressed) | |
| 1019 OnAfterPossibleChange(); | |
| 1020 | |
| 1021 return TRUE; // Don't continue, we called the default handler already. | |
| 1022 } | |
| 1023 | |
| 1024 gboolean OmniboxViewGtk::HandleViewFocusIn(GtkWidget* sender, | |
| 1025 GdkEventFocus* event) { | |
| 1026 DCHECK(text_view_); | |
| 1027 update_popup_without_focus_ = false; | |
| 1028 | |
| 1029 GdkModifierType modifiers; | |
| 1030 GdkWindow* gdk_window = gtk_widget_get_window(text_view_); | |
| 1031 gdk_window_get_pointer(gdk_window, NULL, NULL, &modifiers); | |
| 1032 model()->OnSetFocus((modifiers & GDK_CONTROL_MASK) != 0); | |
| 1033 controller()->OnSetFocus(); | |
| 1034 // TODO(deanm): Some keyword hit business, etc here. | |
| 1035 | |
| 1036 g_signal_connect( | |
| 1037 gdk_keymap_get_for_display(gtk_widget_get_display(text_view_)), | |
| 1038 "direction-changed", | |
| 1039 G_CALLBACK(&HandleKeymapDirectionChangedThunk), this); | |
| 1040 | |
| 1041 AdjustTextJustification(); | |
| 1042 | |
| 1043 return FALSE; // Continue propagation. | |
| 1044 } | |
| 1045 | |
| 1046 gboolean OmniboxViewGtk::HandleViewFocusOut(GtkWidget* sender, | |
| 1047 GdkEventFocus* event) { | |
| 1048 DCHECK(text_view_); | |
| 1049 GtkWidget* view_getting_focus = NULL; | |
| 1050 GtkWindow* toplevel = platform_util::GetTopLevel(sender); | |
| 1051 if (gtk_window_is_active(toplevel)) | |
| 1052 view_getting_focus = going_to_focus_; | |
| 1053 | |
| 1054 // This must be invoked before ClosePopup. | |
| 1055 model()->OnWillKillFocus(view_getting_focus); | |
| 1056 | |
| 1057 // Close the popup. | |
| 1058 CloseOmniboxPopup(); | |
| 1059 // Tell the model to reset itself. | |
| 1060 model()->OnKillFocus(); | |
| 1061 | |
| 1062 g_signal_handlers_disconnect_by_func( | |
| 1063 gdk_keymap_get_for_display(gtk_widget_get_display(text_view_)), | |
| 1064 reinterpret_cast<gpointer>(&HandleKeymapDirectionChangedThunk), this); | |
| 1065 | |
| 1066 return FALSE; // Pass the event on to the GtkTextView. | |
| 1067 } | |
| 1068 | |
| 1069 void OmniboxViewGtk::HandleViewMoveCursor( | |
| 1070 GtkWidget* sender, | |
| 1071 GtkMovementStep step, | |
| 1072 gint count, | |
| 1073 gboolean extend_selection) { | |
| 1074 DCHECK(text_view_); | |
| 1075 GtkTextIter sel_start, sel_end; | |
| 1076 gboolean has_selection = | |
| 1077 gtk_text_buffer_get_selection_bounds(text_buffer_, &sel_start, &sel_end); | |
| 1078 bool handled = false; | |
| 1079 | |
| 1080 if (step == GTK_MOVEMENT_VISUAL_POSITIONS && !extend_selection && | |
| 1081 (count == 1 || count == -1)) { | |
| 1082 // We need to take the content direction into account when handling cursor | |
| 1083 // movement, because the behavior of Left and Right key will be inverted if | |
| 1084 // the direction is RTL. Although we should check the direction around the | |
| 1085 // input caret, it's much simpler and good enough to check whole content's | |
| 1086 // direction. | |
| 1087 PangoDirection content_dir = GetContentDirection(); | |
| 1088 gint count_towards_end = content_dir == PANGO_DIRECTION_RTL ? -1 : 1; | |
| 1089 | |
| 1090 // We want the GtkEntry behavior when you move the cursor while you have a | |
| 1091 // selection. GtkTextView just drops the selection and moves the cursor, | |
| 1092 // but instead we want to move the cursor to the appropiate end of the | |
| 1093 // selection. | |
| 1094 if (has_selection) { | |
| 1095 // We have a selection and start / end are in ascending order. | |
| 1096 // Cursor placement will remove the selection, so we need inform | |
| 1097 // model() about this change by | |
| 1098 // calling On{Before|After}PossibleChange() methods. | |
| 1099 OnBeforePossibleChange(); | |
| 1100 gtk_text_buffer_place_cursor( | |
| 1101 text_buffer_, count == count_towards_end ? &sel_end : &sel_start); | |
| 1102 OnAfterPossibleChange(); | |
| 1103 handled = true; | |
| 1104 } else if (count == count_towards_end && !IsCaretAtEnd()) { | |
| 1105 handled = model()->CommitSuggestedText(); | |
| 1106 } | |
| 1107 } else if (step == GTK_MOVEMENT_PAGES) { // Page up and down. | |
| 1108 // Multiply by count for the direction (if we move too much that's ok). | |
| 1109 model()->OnUpOrDownKeyPressed(model()->result().size() * count); | |
| 1110 handled = true; | |
| 1111 } else if (step == GTK_MOVEMENT_DISPLAY_LINES) { // Arrow up and down. | |
| 1112 model()->OnUpOrDownKeyPressed(count); | |
| 1113 handled = true; | |
| 1114 } | |
| 1115 | |
| 1116 if (!handled) { | |
| 1117 // Cursor movement may change the selection, we need to record the change | |
| 1118 // and report it to model(). | |
| 1119 if (has_selection || extend_selection) | |
| 1120 OnBeforePossibleChange(); | |
| 1121 | |
| 1122 // Propagate into GtkTextView | |
| 1123 GtkTextViewClass* klass = GTK_TEXT_VIEW_GET_CLASS(text_view_); | |
| 1124 klass->move_cursor(GTK_TEXT_VIEW(text_view_), step, count, | |
| 1125 extend_selection); | |
| 1126 | |
| 1127 if (has_selection || extend_selection) | |
| 1128 OnAfterPossibleChange(); | |
| 1129 } | |
| 1130 | |
| 1131 // move-cursor doesn't use a signal accumulator on the return value (it | |
| 1132 // just ignores then), so we have to stop the propagation. | |
| 1133 static guint signal_id = g_signal_lookup("move-cursor", GTK_TYPE_TEXT_VIEW); | |
| 1134 g_signal_stop_emission(text_view_, signal_id, 0); | |
| 1135 } | |
| 1136 | |
| 1137 void OmniboxViewGtk::HandleViewSizeRequest(GtkWidget* sender, | |
| 1138 GtkRequisition* req) { | |
| 1139 // Don't force a minimum width, but use the font-relative height. This is a | |
| 1140 // run-first handler, so the default handler was already called. | |
| 1141 req->width = 1; | |
| 1142 } | |
| 1143 | |
| 1144 void OmniboxViewGtk::HandlePopupMenuDeactivate(GtkWidget* sender) { | |
| 1145 // When the context menu appears, |text_view_|'s focus is lost. After an item | |
| 1146 // is activated, the focus comes back to |text_view_|, but only after the | |
| 1147 // check in UpdatePopup(). We set this flag to make UpdatePopup() aware that | |
| 1148 // it will be receiving focus again. | |
| 1149 if (!model()->has_focus()) | |
| 1150 update_popup_without_focus_ = true; | |
| 1151 } | |
| 1152 | |
| 1153 void OmniboxViewGtk::HandlePopulatePopup(GtkWidget* sender, GtkMenu* menu) { | |
| 1154 GtkWidget* separator = gtk_separator_menu_item_new(); | |
| 1155 gtk_menu_shell_append(GTK_MENU_SHELL(menu), separator); | |
| 1156 gtk_widget_show(separator); | |
| 1157 | |
| 1158 // Paste and Go menu item. | |
| 1159 GtkClipboard* x_clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); | |
| 1160 gchar* text = gtk_clipboard_wait_for_text(x_clipboard); | |
| 1161 sanitized_text_for_paste_and_go_ = text ? | |
| 1162 StripJavascriptSchemas( | |
| 1163 base::CollapseWhitespace(base::UTF8ToUTF16(text), true)) : | |
| 1164 base::string16(); | |
| 1165 g_free(text); | |
| 1166 GtkWidget* paste_and_go_menuitem = gtk_menu_item_new_with_mnemonic( | |
| 1167 ui::ConvertAcceleratorsFromWindowsStyle(l10n_util::GetStringUTF8( | |
| 1168 model()->IsPasteAndSearch(sanitized_text_for_paste_and_go_) ? | |
| 1169 IDS_PASTE_AND_SEARCH : IDS_PASTE_AND_GO)).c_str()); | |
| 1170 // Detect the stock Paste menu item by searching for the stock label | |
| 1171 // GTK_STOCK_PASTE. If we don't find it, the Paste and Go item will be | |
| 1172 // appended at the end of the popup menu. | |
| 1173 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), paste_and_go_menuitem, | |
| 1174 GetPopupMenuIndexForStockLabel(GTK_STOCK_PASTE, menu)); | |
| 1175 g_signal_connect(paste_and_go_menuitem, "activate", | |
| 1176 G_CALLBACK(HandlePasteAndGoThunk), this); | |
| 1177 gtk_widget_set_sensitive( | |
| 1178 paste_and_go_menuitem, | |
| 1179 model()->CanPasteAndGo(sanitized_text_for_paste_and_go_)); | |
| 1180 gtk_widget_show(paste_and_go_menuitem); | |
| 1181 | |
| 1182 // Show URL menu item. | |
| 1183 if (chrome::IsQueryExtractionEnabled()) { | |
| 1184 GtkWidget* show_url_menuitem = gtk_menu_item_new_with_mnemonic( | |
| 1185 ui::ConvertAcceleratorsFromWindowsStyle( | |
| 1186 l10n_util::GetStringUTF8(IDS_SHOW_URL)).c_str()); | |
| 1187 gtk_menu_shell_append(GTK_MENU_SHELL(menu), show_url_menuitem); | |
| 1188 g_signal_connect(show_url_menuitem, "activate", | |
| 1189 G_CALLBACK(HandleShowURLThunk), this); | |
| 1190 gtk_widget_set_sensitive( | |
| 1191 show_url_menuitem, | |
| 1192 controller()->GetToolbarModel()->WouldReplaceURL()); | |
| 1193 gtk_widget_show(show_url_menuitem); | |
| 1194 } | |
| 1195 | |
| 1196 // Edit Search Engines menu item. | |
| 1197 GtkWidget* edit_search_engines_menuitem = gtk_menu_item_new_with_mnemonic( | |
| 1198 ui::ConvertAcceleratorsFromWindowsStyle( | |
| 1199 l10n_util::GetStringUTF8(IDS_EDIT_SEARCH_ENGINES)).c_str()); | |
| 1200 gtk_menu_shell_append(GTK_MENU_SHELL(menu), edit_search_engines_menuitem); | |
| 1201 g_signal_connect(edit_search_engines_menuitem, "activate", | |
| 1202 G_CALLBACK(HandleEditSearchEnginesThunk), this); | |
| 1203 gtk_widget_set_sensitive( | |
| 1204 edit_search_engines_menuitem, | |
| 1205 command_updater()->IsCommandEnabled(IDC_EDIT_SEARCH_ENGINES)); | |
| 1206 gtk_widget_show(edit_search_engines_menuitem); | |
| 1207 | |
| 1208 g_signal_connect(menu, "deactivate", | |
| 1209 G_CALLBACK(HandlePopupMenuDeactivateThunk), this); | |
| 1210 } | |
| 1211 | |
| 1212 void OmniboxViewGtk::HandlePasteAndGo(GtkWidget* sender) { | |
| 1213 model()->PasteAndGo(sanitized_text_for_paste_and_go_); | |
| 1214 } | |
| 1215 | |
| 1216 void OmniboxViewGtk::HandleEditSearchEngines(GtkWidget* sender) { | |
| 1217 command_updater()->ExecuteCommand(IDC_EDIT_SEARCH_ENGINES); | |
| 1218 } | |
| 1219 | |
| 1220 void OmniboxViewGtk::HandleShowURL(GtkWidget* sender) { | |
| 1221 ShowURL(); | |
| 1222 } | |
| 1223 | |
| 1224 void OmniboxViewGtk::HandleMarkSet(GtkTextBuffer* buffer, | |
| 1225 GtkTextIter* location, | |
| 1226 GtkTextMark* mark) { | |
| 1227 if (!text_buffer_ || buffer != text_buffer_) | |
| 1228 return; | |
| 1229 | |
| 1230 if (mark != gtk_text_buffer_get_insert(text_buffer_) && | |
| 1231 mark != gtk_text_buffer_get_selection_bound(text_buffer_)) { | |
| 1232 return; | |
| 1233 } | |
| 1234 | |
| 1235 // If we are here, that means the user may be changing the selection | |
| 1236 selection_suggested_ = false; | |
| 1237 | |
| 1238 // Get the currently-selected text, if there is any. | |
| 1239 std::string new_selected_text = GetSelectedText(); | |
| 1240 | |
| 1241 // If we had some text selected earlier but it's no longer highlighted, we | |
| 1242 // might need to save it now... | |
| 1243 if (!selected_text_.empty() && new_selected_text.empty()) { | |
| 1244 // ... but only if we currently own the selection. We want to manually | |
| 1245 // update the selection when the text is unhighlighted because the user | |
| 1246 // clicked in a blank area of the text view, but not when it's unhighlighted | |
| 1247 // because another client or widget took the selection. (This handler gets | |
| 1248 // called before the default handler, so as long as nobody else took the | |
| 1249 // selection, the text buffer still owns it even if GTK is about to take it | |
| 1250 // away in the default handler.) | |
| 1251 GtkClipboard* clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY); | |
| 1252 if (gtk_clipboard_get_owner(clipboard) == G_OBJECT(text_buffer_)) | |
| 1253 SavePrimarySelection(selected_text_); | |
| 1254 } | |
| 1255 | |
| 1256 selected_text_ = new_selected_text; | |
| 1257 } | |
| 1258 | |
| 1259 // Override the primary selection the text buffer has set. This has to happen | |
| 1260 // after the default handler for the "mark-set" signal. | |
| 1261 void OmniboxViewGtk::HandleMarkSetAfter(GtkTextBuffer* buffer, | |
| 1262 GtkTextIter* location, | |
| 1263 GtkTextMark* mark) { | |
| 1264 if (!text_buffer_ || buffer != text_buffer_) | |
| 1265 return; | |
| 1266 | |
| 1267 // We should only update primary selection when the user changes the selection | |
| 1268 // range. | |
| 1269 if (mark != gtk_text_buffer_get_insert(text_buffer_) && | |
| 1270 mark != gtk_text_buffer_get_selection_bound(text_buffer_)) { | |
| 1271 return; | |
| 1272 } | |
| 1273 | |
| 1274 UpdatePrimarySelectionIfValidURL(); | |
| 1275 } | |
| 1276 | |
| 1277 // Just use the default behavior for DnD, except if the drop can be a PasteAndGo | |
| 1278 // then override. | |
| 1279 void OmniboxViewGtk::HandleDragDataReceived(GtkWidget* sender, | |
| 1280 GdkDragContext* context, | |
| 1281 gint x, | |
| 1282 gint y, | |
| 1283 GtkSelectionData* selection_data, | |
| 1284 guint target_type, | |
| 1285 guint time) { | |
| 1286 DCHECK(text_view_); | |
| 1287 | |
| 1288 // Reset |paste_clipboard_requested_| to make sure we won't misinterpret this | |
| 1289 // drop action as a paste action. | |
| 1290 paste_clipboard_requested_ = false; | |
| 1291 | |
| 1292 // Don't try to PasteAndGo on drops originating from this omnibox. However, do | |
| 1293 // allow default behavior for such drags. | |
| 1294 if (gdk_drag_context_get_source_window(context) == | |
| 1295 gtk_widget_get_window(text_view_)) | |
| 1296 return; | |
| 1297 | |
| 1298 guchar* text = gtk_selection_data_get_text(selection_data); | |
| 1299 if (!text) | |
| 1300 return; | |
| 1301 | |
| 1302 base::string16 possible_url = | |
| 1303 base::UTF8ToUTF16(reinterpret_cast<char*>(text)); | |
| 1304 g_free(text); | |
| 1305 if (OnPerformDropImpl(possible_url)) { | |
| 1306 gtk_drag_finish(context, TRUE, FALSE, time); | |
| 1307 | |
| 1308 static guint signal_id = | |
| 1309 g_signal_lookup("drag-data-received", GTK_TYPE_WIDGET); | |
| 1310 g_signal_stop_emission(text_view_, signal_id, 0); | |
| 1311 } | |
| 1312 } | |
| 1313 | |
| 1314 void OmniboxViewGtk::HandleDragDataGet(GtkWidget* widget, | |
| 1315 GdkDragContext* context, | |
| 1316 GtkSelectionData* selection_data, | |
| 1317 guint target_type, | |
| 1318 guint time) { | |
| 1319 DCHECK(text_view_); | |
| 1320 | |
| 1321 switch (target_type) { | |
| 1322 case GTK_TEXT_BUFFER_TARGET_INFO_TEXT: { | |
| 1323 gtk_selection_data_set_text(selection_data, dragged_text_.c_str(), -1); | |
| 1324 break; | |
| 1325 } | |
| 1326 case ui::CHROME_NAMED_URL: { | |
| 1327 WebContents* current_tab = controller()->GetWebContents(); | |
| 1328 base::string16 tab_title = current_tab->GetTitle(); | |
| 1329 // Pass an empty string if user has edited the URL. | |
| 1330 if (current_tab->GetURL().spec() != dragged_text_) | |
| 1331 tab_title = base::string16(); | |
| 1332 ui::WriteURLWithName(selection_data, GURL(dragged_text_), | |
| 1333 tab_title, target_type); | |
| 1334 break; | |
| 1335 } | |
| 1336 } | |
| 1337 } | |
| 1338 | |
| 1339 void OmniboxViewGtk::HandleDragBegin(GtkWidget* widget, | |
| 1340 GdkDragContext* context) { | |
| 1341 base::string16 text = base::UTF8ToUTF16(GetSelectedText()); | |
| 1342 | |
| 1343 if (text.empty()) | |
| 1344 return; | |
| 1345 | |
| 1346 // Use AdjustTextForCopy to make sure we prefix the text with 'http://'. | |
| 1347 CharRange selection = GetSelection(); | |
| 1348 GURL url; | |
| 1349 bool write_url; | |
| 1350 model()->AdjustTextForCopy(selection.selection_min(), IsSelectAll(), &text, | |
| 1351 &url, &write_url); | |
| 1352 if (write_url) { | |
| 1353 selected_text_ = base::UTF16ToUTF8(text); | |
| 1354 GtkTargetList* copy_targets = | |
| 1355 gtk_text_buffer_get_copy_target_list(text_buffer_); | |
| 1356 gtk_target_list_add(copy_targets, | |
| 1357 ui::GetAtomForTarget(ui::CHROME_NAMED_URL), | |
| 1358 GTK_TARGET_SAME_APP, ui::CHROME_NAMED_URL); | |
| 1359 } | |
| 1360 dragged_text_ = selected_text_; | |
| 1361 } | |
| 1362 | |
| 1363 void OmniboxViewGtk::HandleDragEnd(GtkWidget* widget, | |
| 1364 GdkDragContext* context) { | |
| 1365 GdkAtom atom = ui::GetAtomForTarget(ui::CHROME_NAMED_URL); | |
| 1366 GtkTargetList* copy_targets = | |
| 1367 gtk_text_buffer_get_copy_target_list(text_buffer_); | |
| 1368 gtk_target_list_remove(copy_targets, atom); | |
| 1369 dragged_text_.clear(); | |
| 1370 } | |
| 1371 | |
| 1372 void OmniboxViewGtk::HandleInsertText(GtkTextBuffer* buffer, | |
| 1373 GtkTextIter* location, | |
| 1374 const gchar* text, | |
| 1375 gint len) { | |
| 1376 base::string16 filtered_text; | |
| 1377 filtered_text.reserve(len); | |
| 1378 | |
| 1379 // Filter out new line and tab characters. | |
| 1380 // |text| is guaranteed to be a valid UTF-8 string, so we don't need to | |
| 1381 // validate it here. | |
| 1382 // | |
| 1383 // If there was only a single character, then it might be generated by a key | |
| 1384 // event. In this case, we save the single character to help our | |
| 1385 // "key-press-event" signal handler distinguish if an Enter key event is | |
| 1386 // handled by IME or not. | |
| 1387 if (len == 1 && (text[0] == '\n' || text[0] == '\r')) | |
| 1388 enter_was_inserted_ = true; | |
| 1389 | |
| 1390 for (const gchar* p = text; *p && (p - text) < len; | |
| 1391 p = g_utf8_next_char(p)) { | |
| 1392 gunichar c = g_utf8_get_char(p); | |
| 1393 | |
| 1394 // 0x200B is Zero Width Space, which is inserted just before the gray text | |
| 1395 // anchor for working around the GtkTextView's misalignment bug. | |
| 1396 // This character might be captured and inserted into the content by undo | |
| 1397 // manager, so we need to filter it out here. | |
| 1398 if (c != 0x200B) | |
| 1399 base::WriteUnicodeCharacter(c, &filtered_text); | |
| 1400 } | |
| 1401 | |
| 1402 if (model()->is_pasting()) { | |
| 1403 // If the user is pasting all-whitespace, paste a single space | |
| 1404 // rather than nothing, since pasting nothing feels broken. | |
| 1405 filtered_text = base::CollapseWhitespace(filtered_text, true); | |
| 1406 filtered_text = filtered_text.empty() ? base::ASCIIToUTF16(" ") : | |
| 1407 StripJavascriptSchemas(filtered_text); | |
| 1408 } | |
| 1409 | |
| 1410 if (!filtered_text.empty()) { | |
| 1411 // Avoid inserting the text after the gray text anchor. | |
| 1412 ValidateTextBufferIter(location); | |
| 1413 | |
| 1414 // Call the default handler to insert filtered text. | |
| 1415 GtkTextBufferClass* klass = GTK_TEXT_BUFFER_GET_CLASS(buffer); | |
| 1416 std::string utf8_text = base::UTF16ToUTF8(filtered_text); | |
| 1417 klass->insert_text(buffer, location, utf8_text.data(), | |
| 1418 static_cast<gint>(utf8_text.length())); | |
| 1419 } | |
| 1420 | |
| 1421 // Stop propagating the signal emission to prevent the default handler from | |
| 1422 // being called again. | |
| 1423 static guint signal_id = g_signal_lookup("insert-text", GTK_TYPE_TEXT_BUFFER); | |
| 1424 g_signal_stop_emission(buffer, signal_id, 0); | |
| 1425 } | |
| 1426 | |
| 1427 void OmniboxViewGtk::HandleBackSpace(GtkWidget* sender) { | |
| 1428 // Checks if it's currently in keyword search mode. | |
| 1429 if (model()->is_keyword_hint() || model()->keyword().empty()) | |
| 1430 return; // Propgate into GtkTextView. | |
| 1431 | |
| 1432 DCHECK(text_view_); | |
| 1433 | |
| 1434 GtkTextIter sel_start, sel_end; | |
| 1435 // Checks if there is some text selected. | |
| 1436 if (gtk_text_buffer_get_selection_bounds(text_buffer_, &sel_start, &sel_end)) | |
| 1437 return; // Propgate into GtkTextView. | |
| 1438 | |
| 1439 GtkTextIter start; | |
| 1440 gtk_text_buffer_get_start_iter(text_buffer_, &start); | |
| 1441 | |
| 1442 if (!gtk_text_iter_equal(&start, &sel_start)) | |
| 1443 return; // Propgate into GtkTextView. | |
| 1444 | |
| 1445 // We're showing a keyword and the user pressed backspace at the beginning | |
| 1446 // of the text. Delete the selected keyword. | |
| 1447 model()->ClearKeyword(GetText()); | |
| 1448 | |
| 1449 // Stop propagating the signal emission into GtkTextView. | |
| 1450 static guint signal_id = g_signal_lookup("backspace", GTK_TYPE_TEXT_VIEW); | |
| 1451 g_signal_stop_emission(text_view_, signal_id, 0); | |
| 1452 } | |
| 1453 | |
| 1454 void OmniboxViewGtk::HandleViewMoveFocus(GtkWidget* widget, | |
| 1455 GtkDirectionType direction) { | |
| 1456 if (!tab_was_pressed_) | |
| 1457 return; | |
| 1458 | |
| 1459 // If special behavior is triggered, then stop the signal emission to | |
| 1460 // prevent the focus from being moved. | |
| 1461 bool handled = false; | |
| 1462 | |
| 1463 // Trigger Tab to search behavior only when Tab key is pressed. | |
| 1464 if (model()->is_keyword_hint() && !shift_was_pressed_) { | |
| 1465 handled = model()->AcceptKeyword(ENTERED_KEYWORD_MODE_VIA_TAB); | |
| 1466 } else if (model()->popup_model()->IsOpen()) { | |
| 1467 if (shift_was_pressed_ && | |
| 1468 model()->popup_model()->selected_line_state() == | |
| 1469 OmniboxPopupModel::KEYWORD) | |
| 1470 model()->ClearKeyword(GetText()); | |
| 1471 else | |
| 1472 model()->OnUpOrDownKeyPressed(shift_was_pressed_ ? -1 : 1); | |
| 1473 | |
| 1474 handled = true; | |
| 1475 } | |
| 1476 | |
| 1477 if (supports_pre_edit_ && !handled && !pre_edit_.empty()) | |
| 1478 handled = true; | |
| 1479 | |
| 1480 if (!handled && gtk_widget_get_visible(gray_text_view_)) | |
| 1481 handled = model()->CommitSuggestedText(); | |
| 1482 | |
| 1483 if (handled) { | |
| 1484 static guint signal_id = g_signal_lookup("move-focus", GTK_TYPE_WIDGET); | |
| 1485 g_signal_stop_emission(widget, signal_id, 0); | |
| 1486 } | |
| 1487 } | |
| 1488 | |
| 1489 void OmniboxViewGtk::HandleCopyClipboard(GtkWidget* sender) { | |
| 1490 HandleCopyOrCutClipboard(true); | |
| 1491 } | |
| 1492 | |
| 1493 void OmniboxViewGtk::HandleCutClipboard(GtkWidget* sender) { | |
| 1494 HandleCopyOrCutClipboard(false); | |
| 1495 } | |
| 1496 | |
| 1497 void OmniboxViewGtk::HandleCopyOrCutClipboard(bool copy) { | |
| 1498 DCHECK(text_view_); | |
| 1499 | |
| 1500 // On copy or cut, we manually update the PRIMARY selection to contain the | |
| 1501 // highlighted text. This matches Firefox -- we highlight the URL but don't | |
| 1502 // update PRIMARY on Ctrl-L, so Ctrl-L, Ctrl-C and then middle-click is a | |
| 1503 // convenient way to paste the current URL somewhere. | |
| 1504 if (!gtk_text_buffer_get_has_selection(text_buffer_)) | |
| 1505 return; | |
| 1506 | |
| 1507 GtkClipboard* clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY); | |
| 1508 DCHECK(clipboard); | |
| 1509 | |
| 1510 CharRange selection = GetSelection(); | |
| 1511 GURL url; | |
| 1512 base::string16 text(base::UTF8ToUTF16(GetSelectedText())); | |
| 1513 bool write_url; | |
| 1514 model()->AdjustTextForCopy(selection.selection_min(), IsSelectAll(), &text, | |
| 1515 &url, &write_url); | |
| 1516 | |
| 1517 if (IsSelectAll()) | |
| 1518 UMA_HISTOGRAM_COUNTS(OmniboxEditModel::kCutOrCopyAllTextHistogram, 1); | |
| 1519 | |
| 1520 // On other platforms we write |text| to the clipboard regardless of | |
| 1521 // |write_url|. We don't need to do that here because we fall through to | |
| 1522 // the default signal handlers. | |
| 1523 if (write_url) { | |
| 1524 BookmarkNodeData data; | |
| 1525 data.ReadFromTuple(url, text); | |
| 1526 data.WriteToClipboard(ui::CLIPBOARD_TYPE_COPY_PASTE); | |
| 1527 SetSelectedRange(selection); | |
| 1528 | |
| 1529 // Stop propagating the signal. | |
| 1530 static guint copy_signal_id = | |
| 1531 g_signal_lookup("copy-clipboard", GTK_TYPE_TEXT_VIEW); | |
| 1532 static guint cut_signal_id = | |
| 1533 g_signal_lookup("cut-clipboard", GTK_TYPE_TEXT_VIEW); | |
| 1534 g_signal_stop_emission(text_view_, | |
| 1535 copy ? copy_signal_id : cut_signal_id, | |
| 1536 0); | |
| 1537 | |
| 1538 if (!copy && gtk_text_view_get_editable(GTK_TEXT_VIEW(text_view_))) | |
| 1539 gtk_text_buffer_delete_selection(text_buffer_, true, true); | |
| 1540 } | |
| 1541 | |
| 1542 OwnPrimarySelection(base::UTF16ToUTF8(text)); | |
| 1543 } | |
| 1544 | |
| 1545 int OmniboxViewGtk::GetOmniboxTextLength() const { | |
| 1546 GtkTextIter end; | |
| 1547 gtk_text_buffer_get_iter_at_mark(text_buffer_, &end, gray_text_mark_); | |
| 1548 if (supports_pre_edit_) { | |
| 1549 // We need to count the length of the text being composed, because we treat | |
| 1550 // it as part of the content in GetText(). | |
| 1551 return gtk_text_iter_get_offset(&end) + pre_edit_.size(); | |
| 1552 } | |
| 1553 return gtk_text_iter_get_offset(&end); | |
| 1554 } | |
| 1555 | |
| 1556 void OmniboxViewGtk::EmphasizeURLComponents() { | |
| 1557 if (supports_pre_edit_) { | |
| 1558 // We can't change the text style easily, if the pre-edit string (the text | |
| 1559 // being composed by the input method) is not empty, which is not treated as | |
| 1560 // a part of the text content inside GtkTextView. And it's ok to simply | |
| 1561 // return in this case, as this method will be called again when the | |
| 1562 // pre-edit string gets committed. | |
| 1563 if (pre_edit_.size()) { | |
| 1564 strikethrough_ = CharRange(); | |
| 1565 return; | |
| 1566 } | |
| 1567 } | |
| 1568 // See whether the contents are a URL with a non-empty host portion, which we | |
| 1569 // should emphasize. To check for a URL, rather than using the type returned | |
| 1570 // by Parse(), ask the model, which will check the desired page transition for | |
| 1571 // this input. This can tell us whether an UNKNOWN input string is going to | |
| 1572 // be treated as a search or a navigation, and is the same method the Paste | |
| 1573 // And Go system uses. | |
| 1574 url_parse::Component scheme, host; | |
| 1575 base::string16 text(GetText()); | |
| 1576 AutocompleteInput::ParseForEmphasizeComponents(text, &scheme, &host); | |
| 1577 | |
| 1578 // Set the baseline emphasis. | |
| 1579 GtkTextIter start, end; | |
| 1580 GetTextBufferBounds(&start, &end); | |
| 1581 gtk_text_buffer_remove_all_tags(text_buffer_, &start, &end); | |
| 1582 bool grey_out_url = text.substr(scheme.begin, scheme.len) == | |
| 1583 base::UTF8ToUTF16(extensions::kExtensionScheme); | |
| 1584 bool grey_base = model()->CurrentTextIsURL() && | |
| 1585 (host.is_nonempty() || grey_out_url); | |
| 1586 gtk_text_buffer_apply_tag( | |
| 1587 text_buffer_, grey_base ? faded_text_tag_ : normal_text_tag_ , &start, | |
| 1588 &end); | |
| 1589 | |
| 1590 if (grey_base && !grey_out_url) { | |
| 1591 // We've found a host name, give it more emphasis. | |
| 1592 gtk_text_buffer_get_iter_at_line_index( | |
| 1593 text_buffer_, &start, 0, GetUTF8Offset(text, host.begin)); | |
| 1594 gtk_text_buffer_get_iter_at_line_index( | |
| 1595 text_buffer_, &end, 0, GetUTF8Offset(text, host.end())); | |
| 1596 gtk_text_buffer_apply_tag(text_buffer_, normal_text_tag_, &start, &end); | |
| 1597 } | |
| 1598 | |
| 1599 strikethrough_ = CharRange(); | |
| 1600 // Emphasize the scheme for security UI display purposes (if necessary). | |
| 1601 if (!model()->user_input_in_progress() && model()->CurrentTextIsURL() && | |
| 1602 scheme.is_nonempty() && (security_level_ != ToolbarModel::NONE)) { | |
| 1603 CharRange scheme_range = CharRange(GetUTF8Offset(text, scheme.begin), | |
| 1604 GetUTF8Offset(text, scheme.end())); | |
| 1605 ItersFromCharRange(scheme_range, &start, &end); | |
| 1606 | |
| 1607 if (security_level_ == ToolbarModel::SECURITY_ERROR) { | |
| 1608 strikethrough_ = scheme_range; | |
| 1609 // When we draw the strikethrough, we don't want to include the ':' at the | |
| 1610 // end of the scheme. | |
| 1611 strikethrough_.cp_max--; | |
| 1612 | |
| 1613 gtk_text_buffer_apply_tag(text_buffer_, security_error_scheme_tag_, | |
| 1614 &start, &end); | |
| 1615 } else if (security_level_ == ToolbarModel::SECURITY_WARNING) { | |
| 1616 gtk_text_buffer_apply_tag(text_buffer_, faded_text_tag_, &start, &end); | |
| 1617 } else { | |
| 1618 gtk_text_buffer_apply_tag(text_buffer_, secure_scheme_tag_, &start, &end); | |
| 1619 } | |
| 1620 } | |
| 1621 } | |
| 1622 | |
| 1623 bool OmniboxViewGtk::OnPerformDropImpl(const base::string16& text) { | |
| 1624 base::string16 sanitized_string(StripJavascriptSchemas( | |
| 1625 base::CollapseWhitespace(text, true))); | |
| 1626 if (model()->CanPasteAndGo(sanitized_string)) { | |
| 1627 model()->PasteAndGo(sanitized_string); | |
| 1628 return true; | |
| 1629 } | |
| 1630 | |
| 1631 return false; | |
| 1632 } | |
| 1633 | |
| 1634 void OmniboxViewGtk::OnBrowserThemeChanged() { | |
| 1635 DCHECK(text_view_); | |
| 1636 | |
| 1637 bool use_gtk = theme_service_->UsingNativeTheme(); | |
| 1638 if (use_gtk) { | |
| 1639 gtk_widget_modify_cursor(text_view_, NULL, NULL); | |
| 1640 gtk_widget_modify_base(text_view_, GTK_STATE_NORMAL, NULL); | |
| 1641 gtk_widget_modify_base(text_view_, GTK_STATE_SELECTED, NULL); | |
| 1642 gtk_widget_modify_text(text_view_, GTK_STATE_SELECTED, NULL); | |
| 1643 gtk_widget_modify_base(text_view_, GTK_STATE_ACTIVE, NULL); | |
| 1644 gtk_widget_modify_text(text_view_, GTK_STATE_ACTIVE, NULL); | |
| 1645 | |
| 1646 gtk_util::UndoForceFontSize(text_view_); | |
| 1647 gtk_util::UndoForceFontSize(gray_text_view_); | |
| 1648 | |
| 1649 // Grab the text colors out of the style and set our tags to use them. | |
| 1650 GtkStyle* style = gtk_rc_get_style(text_view_); | |
| 1651 | |
| 1652 // style may be unrealized at this point, so calculate the halfway point | |
| 1653 // between text[] and base[] manually instead of just using text_aa[]. | |
| 1654 GdkColor average_color = gtk_util::AverageColors( | |
| 1655 style->text[GTK_STATE_NORMAL], style->base[GTK_STATE_NORMAL]); | |
| 1656 | |
| 1657 g_object_set(faded_text_tag_, "foreground-gdk", &average_color, NULL); | |
| 1658 g_object_set(normal_text_tag_, "foreground-gdk", | |
| 1659 &style->text[GTK_STATE_NORMAL], NULL); | |
| 1660 } else { | |
| 1661 const GdkColor* background_color_ptr = | |
| 1662 &LocationBarViewGtk::kBackgroundColor; | |
| 1663 gtk_widget_modify_cursor(text_view_, &ui::kGdkBlack, &ui::kGdkGray); | |
| 1664 gtk_widget_modify_base(text_view_, GTK_STATE_NORMAL, background_color_ptr); | |
| 1665 | |
| 1666 GdkColor c; | |
| 1667 // Override the selected colors so we don't leak colors from the current | |
| 1668 // gtk theme into the chrome-theme. | |
| 1669 c = gfx::SkColorToGdkColor( | |
| 1670 theme_service_->get_active_selection_bg_color()); | |
| 1671 gtk_widget_modify_base(text_view_, GTK_STATE_SELECTED, &c); | |
| 1672 | |
| 1673 c = gfx::SkColorToGdkColor( | |
| 1674 theme_service_->get_active_selection_fg_color()); | |
| 1675 gtk_widget_modify_text(text_view_, GTK_STATE_SELECTED, &c); | |
| 1676 | |
| 1677 c = gfx::SkColorToGdkColor( | |
| 1678 theme_service_->get_inactive_selection_bg_color()); | |
| 1679 gtk_widget_modify_base(text_view_, GTK_STATE_ACTIVE, &c); | |
| 1680 | |
| 1681 c = gfx::SkColorToGdkColor( | |
| 1682 theme_service_->get_inactive_selection_fg_color()); | |
| 1683 gtk_widget_modify_text(text_view_, GTK_STATE_ACTIVE, &c); | |
| 1684 | |
| 1685 // Until we switch to vector graphics, force the font size. | |
| 1686 const gfx::Font& font = GetFont(); | |
| 1687 gtk_util::ForceFontSizePixels(text_view_, font.GetFontSize()); | |
| 1688 gtk_util::ForceFontSizePixels(gray_text_view_, font.GetFontSize()); | |
| 1689 | |
| 1690 g_object_set(faded_text_tag_, "foreground", kTextBaseColor, NULL); | |
| 1691 g_object_set(normal_text_tag_, "foreground", "#000000", NULL); | |
| 1692 } | |
| 1693 | |
| 1694 const gfx::Font& font = GetFont(); | |
| 1695 const int cap_height = font.GetCapHeight(); | |
| 1696 const int internal_leading = font.GetBaseline() - cap_height; | |
| 1697 font_baseline_shift_ = | |
| 1698 (font.GetHeight() - cap_height) / 2.0 - internal_leading; | |
| 1699 | |
| 1700 AdjustVerticalAlignmentOfGrayTextView(); | |
| 1701 UpdateGrayTextViewColors(); | |
| 1702 } | |
| 1703 | |
| 1704 gfx::Font OmniboxViewGtk::GetFont() { | |
| 1705 if (!theme_service_->UsingNativeTheme()) { | |
| 1706 return gfx::Font( | |
| 1707 ui::ResourceBundle::GetSharedInstance().GetFont( | |
| 1708 ui::ResourceBundle::BaseFont).GetFontName(), | |
| 1709 browser_defaults::kOmniboxFontPixelSize); | |
| 1710 } | |
| 1711 | |
| 1712 // If we haven't initialized the text view yet, just create a temporary one | |
| 1713 // whose style we can grab. | |
| 1714 GtkWidget* widget = text_view_ ? text_view_ : gtk_text_view_new(); | |
| 1715 GtkStyle* gtk_style = gtk_widget_get_style(widget); | |
| 1716 GtkRcStyle* rc_style = gtk_widget_get_modifier_style(widget); | |
| 1717 gfx::Font font( | |
| 1718 (rc_style && rc_style->font_desc) ? | |
| 1719 rc_style->font_desc : gtk_style->font_desc); | |
| 1720 if (!text_view_) | |
| 1721 g_object_unref(g_object_ref_sink(widget)); | |
| 1722 return font; | |
| 1723 } | |
| 1724 | |
| 1725 void OmniboxViewGtk::OwnPrimarySelection(const std::string& text) { | |
| 1726 primary_selection_text_ = text; | |
| 1727 | |
| 1728 GtkTargetList* list = gtk_target_list_new(NULL, 0); | |
| 1729 gtk_target_list_add_text_targets(list, 0); | |
| 1730 gint len; | |
| 1731 GtkTargetEntry* entries = gtk_target_table_new_from_list(list, &len); | |
| 1732 | |
| 1733 // When |text_buffer_| is destroyed, it will clear the clipboard, hence | |
| 1734 // we needn't worry about calling gtk_clipboard_clear(). | |
| 1735 gtk_clipboard_set_with_owner(gtk_clipboard_get(GDK_SELECTION_PRIMARY), | |
| 1736 entries, len, | |
| 1737 ClipboardGetSelectionThunk, | |
| 1738 ClipboardSelectionCleared, | |
| 1739 G_OBJECT(text_buffer_)); | |
| 1740 | |
| 1741 gtk_target_list_unref(list); | |
| 1742 gtk_target_table_free(entries, len); | |
| 1743 } | |
| 1744 | |
| 1745 void OmniboxViewGtk::HandlePasteClipboard(GtkWidget* sender) { | |
| 1746 // We can't call model()->on_paste_replacing_all() here, because the actual | |
| 1747 // paste clipboard action may not be performed if the clipboard is empty. | |
| 1748 paste_clipboard_requested_ = true; | |
| 1749 } | |
| 1750 | |
| 1751 gfx::Rect OmniboxViewGtk::WindowBoundsFromIters(GtkTextIter* iter1, | |
| 1752 GtkTextIter* iter2) { | |
| 1753 GdkRectangle start_location, end_location; | |
| 1754 GtkTextView* text_view = GTK_TEXT_VIEW(text_view_); | |
| 1755 gtk_text_view_get_iter_location(text_view, iter1, &start_location); | |
| 1756 gtk_text_view_get_iter_location(text_view, iter2, &end_location); | |
| 1757 | |
| 1758 gint x1, x2, y1, y2; | |
| 1759 gtk_text_view_buffer_to_window_coords(text_view, GTK_TEXT_WINDOW_WIDGET, | |
| 1760 start_location.x, start_location.y, | |
| 1761 &x1, &y1); | |
| 1762 gtk_text_view_buffer_to_window_coords(text_view, GTK_TEXT_WINDOW_WIDGET, | |
| 1763 end_location.x + end_location.width, | |
| 1764 end_location.y + end_location.height, | |
| 1765 &x2, &y2); | |
| 1766 | |
| 1767 return gfx::Rect(x1, y1, x2 - x1, y2 - y1); | |
| 1768 } | |
| 1769 | |
| 1770 gboolean OmniboxViewGtk::HandleExposeEvent(GtkWidget* sender, | |
| 1771 GdkEventExpose* event) { | |
| 1772 // Adjust vertical alignment of |sender| before we start rendering it. | |
| 1773 // GtkTextView is a multi-line text edit control and scrollable. Thus we | |
| 1774 // have to adjust the amount of scroll in order to put the content text at | |
| 1775 // the center of Omnibox. | |
| 1776 | |
| 1777 GtkTextView* text_view = GTK_TEXT_VIEW(sender); | |
| 1778 GtkTextIter iter; | |
| 1779 gtk_text_view_get_iter_at_location(text_view, &iter, 0, 0); | |
| 1780 gint line_height = 0; | |
| 1781 gtk_text_view_get_line_yrange(text_view, &iter, NULL, &line_height); | |
| 1782 | |
| 1783 GtkAllocation allocation; | |
| 1784 gtk_widget_get_allocation(alignment_.get(), &allocation); | |
| 1785 | |
| 1786 const double shift = | |
| 1787 (line_height - allocation.height) / 2.0 + font_baseline_shift_; | |
| 1788 | |
| 1789 // If the desired shift is positive (upwards), we can scroll the control to | |
| 1790 // accomplish it; but if the desired shift is downwards, we need to add extra | |
| 1791 // padding atop the control. | |
| 1792 const gdouble new_scroll = std::max(shift, 0.); | |
| 1793 const guint new_top_padding = std::max(0., -shift); | |
| 1794 | |
| 1795 // Changing the amount of scroll may fire another "expose-event", so avoid | |
| 1796 // unnecessary change. | |
| 1797 GtkAdjustment* adjustment = gtk_text_view_get_vadjustment(text_view); | |
| 1798 if (new_scroll != gtk_adjustment_get_value(adjustment)) | |
| 1799 gtk_adjustment_set_value(adjustment, new_scroll); | |
| 1800 guint top_padding = 0; | |
| 1801 guint bottom_padding = 0; | |
| 1802 guint left_padding = 0; | |
| 1803 guint right_padding = 0; | |
| 1804 gtk_alignment_get_padding(GTK_ALIGNMENT(alignment_.get()), &top_padding, | |
| 1805 &bottom_padding, &left_padding, &right_padding); | |
| 1806 if (new_top_padding != top_padding) | |
| 1807 gtk_alignment_set_padding(GTK_ALIGNMENT(alignment_.get()), new_top_padding, | |
| 1808 bottom_padding, left_padding, right_padding); | |
| 1809 | |
| 1810 return FALSE; | |
| 1811 } | |
| 1812 | |
| 1813 gboolean OmniboxViewGtk::HandleExposeEventAfter(GtkWidget* sender, | |
| 1814 GdkEventExpose* expose) { | |
| 1815 if (strikethrough_.cp_min >= strikethrough_.cp_max) | |
| 1816 return FALSE; | |
| 1817 DCHECK(text_view_); | |
| 1818 | |
| 1819 gfx::Rect expose_rect(expose->area); | |
| 1820 | |
| 1821 GtkTextIter iter_min, iter_max; | |
| 1822 ItersFromCharRange(strikethrough_, &iter_min, &iter_max); | |
| 1823 gfx::Rect strikethrough_rect = WindowBoundsFromIters(&iter_min, &iter_max); | |
| 1824 | |
| 1825 if (!expose_rect.Intersects(strikethrough_rect)) | |
| 1826 return FALSE; | |
| 1827 | |
| 1828 // Finally, draw. | |
| 1829 cairo_t* cr = gdk_cairo_create(GDK_DRAWABLE(expose->window)); | |
| 1830 cairo_rectangle(cr, expose_rect.x(), expose_rect.y(), | |
| 1831 expose_rect.width(), expose_rect.height()); | |
| 1832 cairo_clip(cr); | |
| 1833 | |
| 1834 // TODO(estade): we probably shouldn't draw the strikethrough on selected | |
| 1835 // text. I started to do this, but it was way more effort than it seemed | |
| 1836 // worth. | |
| 1837 strikethrough_rect.Inset(kStrikethroughStrokeWidth, | |
| 1838 kStrikethroughStrokeWidth); | |
| 1839 cairo_set_source_rgb(cr, kStrikethroughStrokeRed, 0.0, 0.0); | |
| 1840 cairo_set_line_width(cr, kStrikethroughStrokeWidth); | |
| 1841 cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); | |
| 1842 cairo_move_to(cr, strikethrough_rect.x(), strikethrough_rect.bottom()); | |
| 1843 cairo_line_to(cr, strikethrough_rect.right(), strikethrough_rect.y()); | |
| 1844 cairo_stroke(cr); | |
| 1845 cairo_destroy(cr); | |
| 1846 | |
| 1847 return FALSE; | |
| 1848 } | |
| 1849 | |
| 1850 void OmniboxViewGtk::SelectAllInternal(bool reversed, | |
| 1851 bool update_primary_selection) { | |
| 1852 GtkTextIter start, end; | |
| 1853 if (reversed) { | |
| 1854 GetTextBufferBounds(&end, &start); | |
| 1855 } else { | |
| 1856 GetTextBufferBounds(&start, &end); | |
| 1857 } | |
| 1858 if (!update_primary_selection) | |
| 1859 StartUpdatingHighlightedText(); | |
| 1860 gtk_text_buffer_select_range(text_buffer_, &start, &end); | |
| 1861 if (!update_primary_selection) | |
| 1862 FinishUpdatingHighlightedText(); | |
| 1863 } | |
| 1864 | |
| 1865 void OmniboxViewGtk::StartUpdatingHighlightedText() { | |
| 1866 if (gtk_widget_get_realized(text_view_)) { | |
| 1867 GtkClipboard* clipboard = | |
| 1868 gtk_widget_get_clipboard(text_view_, GDK_SELECTION_PRIMARY); | |
| 1869 DCHECK(clipboard); | |
| 1870 if (clipboard) | |
| 1871 gtk_text_buffer_remove_selection_clipboard(text_buffer_, clipboard); | |
| 1872 } | |
| 1873 g_signal_handler_block(text_buffer_, mark_set_handler_id_); | |
| 1874 g_signal_handler_block(text_buffer_, mark_set_handler_id2_); | |
| 1875 } | |
| 1876 | |
| 1877 void OmniboxViewGtk::FinishUpdatingHighlightedText() { | |
| 1878 if (gtk_widget_get_realized(text_view_)) { | |
| 1879 GtkClipboard* clipboard = | |
| 1880 gtk_widget_get_clipboard(text_view_, GDK_SELECTION_PRIMARY); | |
| 1881 DCHECK(clipboard); | |
| 1882 if (clipboard) | |
| 1883 gtk_text_buffer_add_selection_clipboard(text_buffer_, clipboard); | |
| 1884 } | |
| 1885 g_signal_handler_unblock(text_buffer_, mark_set_handler_id_); | |
| 1886 g_signal_handler_unblock(text_buffer_, mark_set_handler_id2_); | |
| 1887 } | |
| 1888 | |
| 1889 OmniboxViewGtk::CharRange OmniboxViewGtk::GetSelection() const { | |
| 1890 // You can not just use get_selection_bounds here, since the order will be | |
| 1891 // ascending, and you don't know where the user's start and end of the | |
| 1892 // selection was (if the selection was forwards or backwards). Get the | |
| 1893 // actual marks so that we can preserve the selection direction. | |
| 1894 GtkTextIter start, insert; | |
| 1895 GtkTextMark* mark; | |
| 1896 | |
| 1897 mark = gtk_text_buffer_get_selection_bound(text_buffer_); | |
| 1898 gtk_text_buffer_get_iter_at_mark(text_buffer_, &start, mark); | |
| 1899 | |
| 1900 mark = gtk_text_buffer_get_insert(text_buffer_); | |
| 1901 gtk_text_buffer_get_iter_at_mark(text_buffer_, &insert, mark); | |
| 1902 | |
| 1903 gint start_offset = gtk_text_iter_get_offset(&start); | |
| 1904 gint end_offset = gtk_text_iter_get_offset(&insert); | |
| 1905 | |
| 1906 if (supports_pre_edit_) { | |
| 1907 // Nothing should be selected when we are in the middle of composition. | |
| 1908 DCHECK(pre_edit_.empty() || start_offset == end_offset); | |
| 1909 if (!pre_edit_.empty()) { | |
| 1910 start_offset += pre_edit_.size(); | |
| 1911 end_offset += pre_edit_.size(); | |
| 1912 } | |
| 1913 } | |
| 1914 | |
| 1915 return CharRange(start_offset, end_offset); | |
| 1916 } | |
| 1917 | |
| 1918 void OmniboxViewGtk::ItersFromCharRange(const CharRange& range, | |
| 1919 GtkTextIter* iter_min, | |
| 1920 GtkTextIter* iter_max) { | |
| 1921 DCHECK(!IsImeComposing()); | |
| 1922 gtk_text_buffer_get_iter_at_offset(text_buffer_, iter_min, range.cp_min); | |
| 1923 gtk_text_buffer_get_iter_at_offset(text_buffer_, iter_max, range.cp_max); | |
| 1924 } | |
| 1925 | |
| 1926 bool OmniboxViewGtk::IsCaretAtEnd() const { | |
| 1927 const CharRange selection = GetSelection(); | |
| 1928 return selection.cp_min == selection.cp_max && | |
| 1929 selection.cp_min == GetOmniboxTextLength(); | |
| 1930 } | |
| 1931 | |
| 1932 void OmniboxViewGtk::SavePrimarySelection(const std::string& selected_text) { | |
| 1933 DCHECK(text_view_); | |
| 1934 | |
| 1935 GtkClipboard* clipboard = | |
| 1936 gtk_widget_get_clipboard(text_view_, GDK_SELECTION_PRIMARY); | |
| 1937 DCHECK(clipboard); | |
| 1938 if (!clipboard) | |
| 1939 return; | |
| 1940 | |
| 1941 gtk_clipboard_set_text( | |
| 1942 clipboard, selected_text.data(), selected_text.size()); | |
| 1943 } | |
| 1944 | |
| 1945 void OmniboxViewGtk::SetTextAndSelectedRange(const base::string16& text, | |
| 1946 const CharRange& range) { | |
| 1947 if (text != GetText()) { | |
| 1948 std::string utf8 = base::UTF16ToUTF8(text); | |
| 1949 gtk_text_buffer_set_text(text_buffer_, utf8.data(), utf8.length()); | |
| 1950 } | |
| 1951 SetSelectedRange(range); | |
| 1952 AdjustTextJustification(); | |
| 1953 } | |
| 1954 | |
| 1955 void OmniboxViewGtk::SetSelectedRange(const CharRange& range) { | |
| 1956 GtkTextIter insert, bound; | |
| 1957 ItersFromCharRange(range, &bound, &insert); | |
| 1958 gtk_text_buffer_select_range(text_buffer_, &insert, &bound); | |
| 1959 | |
| 1960 // This should be set *after* setting the selection range, in case setting the | |
| 1961 // selection triggers HandleMarkSet which sets |selection_suggested_| to | |
| 1962 // false. | |
| 1963 selection_suggested_ = true; | |
| 1964 } | |
| 1965 | |
| 1966 void OmniboxViewGtk::AdjustTextJustification() { | |
| 1967 DCHECK(text_view_); | |
| 1968 | |
| 1969 PangoDirection content_dir = GetContentDirection(); | |
| 1970 | |
| 1971 // Use keymap direction if content does not have strong direction. | |
| 1972 // It matches the behavior of GtkTextView. | |
| 1973 if (content_dir == PANGO_DIRECTION_NEUTRAL) { | |
| 1974 content_dir = gdk_keymap_get_direction( | |
| 1975 gdk_keymap_get_for_display(gtk_widget_get_display(text_view_))); | |
| 1976 } | |
| 1977 | |
| 1978 GtkTextDirection widget_dir = gtk_widget_get_direction(text_view_); | |
| 1979 | |
| 1980 if ((widget_dir == GTK_TEXT_DIR_RTL && content_dir == PANGO_DIRECTION_LTR) || | |
| 1981 (widget_dir == GTK_TEXT_DIR_LTR && content_dir == PANGO_DIRECTION_RTL)) { | |
| 1982 gtk_text_view_set_justification(GTK_TEXT_VIEW(text_view_), | |
| 1983 GTK_JUSTIFY_RIGHT); | |
| 1984 } else { | |
| 1985 gtk_text_view_set_justification(GTK_TEXT_VIEW(text_view_), | |
| 1986 GTK_JUSTIFY_LEFT); | |
| 1987 } | |
| 1988 } | |
| 1989 | |
| 1990 PangoDirection OmniboxViewGtk::GetContentDirection() { | |
| 1991 GtkTextIter iter; | |
| 1992 gtk_text_buffer_get_start_iter(text_buffer_, &iter); | |
| 1993 | |
| 1994 PangoDirection dir = PANGO_DIRECTION_NEUTRAL; | |
| 1995 do { | |
| 1996 dir = pango_unichar_direction(gtk_text_iter_get_char(&iter)); | |
| 1997 if (dir != PANGO_DIRECTION_NEUTRAL) | |
| 1998 break; | |
| 1999 } while (gtk_text_iter_forward_char(&iter)); | |
| 2000 | |
| 2001 return dir; | |
| 2002 } | |
| 2003 | |
| 2004 void OmniboxViewGtk::HandleWidgetDirectionChanged( | |
| 2005 GtkWidget* sender, | |
| 2006 GtkTextDirection previous_direction) { | |
| 2007 AdjustTextJustification(); | |
| 2008 } | |
| 2009 | |
| 2010 void OmniboxViewGtk::HandleDeleteFromCursor(GtkWidget* sender, | |
| 2011 GtkDeleteType type, | |
| 2012 gint count) { | |
| 2013 // If the selected text was suggested for autocompletion, then erase those | |
| 2014 // first and then let the default handler take over. | |
| 2015 if (selection_suggested_) { | |
| 2016 gtk_text_buffer_delete_selection(text_buffer_, true, true); | |
| 2017 selection_suggested_ = false; | |
| 2018 } | |
| 2019 } | |
| 2020 | |
| 2021 void OmniboxViewGtk::HandleKeymapDirectionChanged(GdkKeymap* sender) { | |
| 2022 AdjustTextJustification(); | |
| 2023 } | |
| 2024 | |
| 2025 void OmniboxViewGtk::HandleDeleteRange(GtkTextBuffer* buffer, | |
| 2026 GtkTextIter* start, | |
| 2027 GtkTextIter* end) { | |
| 2028 // Prevent the user from deleting the gray text anchor. We can't simply set | |
| 2029 // the gray text anchor readonly by applying a tag with "editable" = FALSE, | |
| 2030 // because it'll prevent the insert caret from blinking. | |
| 2031 ValidateTextBufferIter(start); | |
| 2032 ValidateTextBufferIter(end); | |
| 2033 if (!gtk_text_iter_compare(start, end)) { | |
| 2034 static guint signal_id = | |
| 2035 g_signal_lookup("delete-range", GTK_TYPE_TEXT_BUFFER); | |
| 2036 g_signal_stop_emission(buffer, signal_id, 0); | |
| 2037 } | |
| 2038 } | |
| 2039 | |
| 2040 void OmniboxViewGtk::HandleMarkSetAlways(GtkTextBuffer* buffer, | |
| 2041 GtkTextIter* location, | |
| 2042 GtkTextMark* mark) { | |
| 2043 if (mark == gray_text_mark_ || !gray_text_mark_) | |
| 2044 return; | |
| 2045 | |
| 2046 GtkTextIter new_iter = *location; | |
| 2047 ValidateTextBufferIter(&new_iter); | |
| 2048 | |
| 2049 static guint signal_id = g_signal_lookup("mark-set", GTK_TYPE_TEXT_BUFFER); | |
| 2050 | |
| 2051 // "mark-set" signal is actually emitted after the mark's location is already | |
| 2052 // set, so if the location is beyond the gray text anchor, we need to move the | |
| 2053 // mark again, which will emit the signal again. In order to prevent other | |
| 2054 // signal handlers from being called twice, we need to stop signal emission | |
| 2055 // before moving the mark again. | |
| 2056 if (gtk_text_iter_compare(&new_iter, location)) { | |
| 2057 g_signal_stop_emission(buffer, signal_id, 0); | |
| 2058 gtk_text_buffer_move_mark(buffer, mark, &new_iter); | |
| 2059 return; | |
| 2060 } | |
| 2061 | |
| 2062 if (mark != gtk_text_buffer_get_insert(text_buffer_) && | |
| 2063 mark != gtk_text_buffer_get_selection_bound(text_buffer_)) { | |
| 2064 return; | |
| 2065 } | |
| 2066 | |
| 2067 // See issue http://crbug.com/63860 | |
| 2068 GtkTextIter insert; | |
| 2069 GtkTextIter selection_bound; | |
| 2070 gtk_text_buffer_get_iter_at_mark(buffer, &insert, | |
| 2071 gtk_text_buffer_get_insert(buffer)); | |
| 2072 gtk_text_buffer_get_iter_at_mark(buffer, &selection_bound, | |
| 2073 gtk_text_buffer_get_selection_bound(buffer)); | |
| 2074 | |
| 2075 GtkTextIter end; | |
| 2076 gtk_text_buffer_get_iter_at_mark(text_buffer_, &end, gray_text_mark_); | |
| 2077 | |
| 2078 if (gtk_text_iter_compare(&insert, &end) > 0 || | |
| 2079 gtk_text_iter_compare(&selection_bound, &end) > 0) { | |
| 2080 g_signal_stop_emission(buffer, signal_id, 0); | |
| 2081 } | |
| 2082 } | |
| 2083 | |
| 2084 // static | |
| 2085 void OmniboxViewGtk::ClipboardGetSelectionThunk( | |
| 2086 GtkClipboard* clipboard, | |
| 2087 GtkSelectionData* selection_data, | |
| 2088 guint info, | |
| 2089 gpointer object) { | |
| 2090 OmniboxViewGtk* omnibox_view = | |
| 2091 reinterpret_cast<OmniboxViewGtk*>( | |
| 2092 g_object_get_data(G_OBJECT(object), kOmniboxViewGtkKey)); | |
| 2093 omnibox_view->ClipboardGetSelection(clipboard, selection_data, info); | |
| 2094 } | |
| 2095 | |
| 2096 void OmniboxViewGtk::ClipboardGetSelection(GtkClipboard* clipboard, | |
| 2097 GtkSelectionData* selection_data, | |
| 2098 guint info) { | |
| 2099 gtk_selection_data_set_text(selection_data, primary_selection_text_.c_str(), | |
| 2100 primary_selection_text_.size()); | |
| 2101 } | |
| 2102 | |
| 2103 std::string OmniboxViewGtk::GetSelectedText() const { | |
| 2104 GtkTextIter start, end; | |
| 2105 std::string result; | |
| 2106 if (gtk_text_buffer_get_selection_bounds(text_buffer_, &start, &end)) { | |
| 2107 gchar* text = gtk_text_iter_get_text(&start, &end); | |
| 2108 size_t text_len = strlen(text); | |
| 2109 if (text_len) | |
| 2110 result = std::string(text, text_len); | |
| 2111 g_free(text); | |
| 2112 } | |
| 2113 return result; | |
| 2114 } | |
| 2115 | |
| 2116 void OmniboxViewGtk::UpdatePrimarySelectionIfValidURL() { | |
| 2117 base::string16 text = base::UTF8ToUTF16(GetSelectedText()); | |
| 2118 | |
| 2119 if (text.empty()) | |
| 2120 return; | |
| 2121 | |
| 2122 // Use AdjustTextForCopy to make sure we prefix the text with 'http://'. | |
| 2123 CharRange selection = GetSelection(); | |
| 2124 GURL url; | |
| 2125 bool write_url; | |
| 2126 model()->AdjustTextForCopy(selection.selection_min(), IsSelectAll(), &text, | |
| 2127 &url, &write_url); | |
| 2128 if (write_url) { | |
| 2129 selected_text_ = base::UTF16ToUTF8(text); | |
| 2130 OwnPrimarySelection(selected_text_); | |
| 2131 } | |
| 2132 } | |
| 2133 | |
| 2134 void OmniboxViewGtk::HandlePreEditChanged(GtkWidget* sender, | |
| 2135 const gchar* pre_edit) { | |
| 2136 // GtkTextView won't fire "begin-user-action" and "end-user-action" signals | |
| 2137 // when changing the pre-edit string, so we need to call | |
| 2138 // OnBeforePossibleChange() and OnAfterPossibleChange() by ourselves. | |
| 2139 OnBeforePossibleChange(); | |
| 2140 if (pre_edit && *pre_edit) { | |
| 2141 // GtkTextView will only delete the selection range when committing the | |
| 2142 // pre-edit string, which will cause very strange behavior, so we need to | |
| 2143 // delete the selection range here explicitly. See http://crbug.com/18808. | |
| 2144 if (pre_edit_.empty()) | |
| 2145 gtk_text_buffer_delete_selection(text_buffer_, false, true); | |
| 2146 pre_edit_ = base::UTF8ToUTF16(pre_edit); | |
| 2147 } else { | |
| 2148 pre_edit_.clear(); | |
| 2149 } | |
| 2150 OnAfterPossibleChange(); | |
| 2151 } | |
| 2152 | |
| 2153 void OmniboxViewGtk::HandleWindowSetFocus(GtkWindow* sender, | |
| 2154 GtkWidget* focus) { | |
| 2155 // This is actually a guess. If the focused widget changes in "focus-out" | |
| 2156 // event handler, then the window will respect that and won't focus | |
| 2157 // |focus|. I doubt that is likely to happen however. | |
| 2158 going_to_focus_ = focus; | |
| 2159 } | |
| 2160 | |
| 2161 void OmniboxViewGtk::HandleUndoRedo(GtkWidget* sender) { | |
| 2162 OnBeforePossibleChange(); | |
| 2163 } | |
| 2164 | |
| 2165 void OmniboxViewGtk::HandleUndoRedoAfter(GtkWidget* sender) { | |
| 2166 OnAfterPossibleChange(); | |
| 2167 } | |
| 2168 | |
| 2169 void OmniboxViewGtk::GetTextBufferBounds(GtkTextIter* start, | |
| 2170 GtkTextIter* end) const { | |
| 2171 gtk_text_buffer_get_start_iter(text_buffer_, start); | |
| 2172 gtk_text_buffer_get_iter_at_mark(text_buffer_, end, gray_text_mark_); | |
| 2173 } | |
| 2174 | |
| 2175 void OmniboxViewGtk::ValidateTextBufferIter(GtkTextIter* iter) const { | |
| 2176 if (!gray_text_mark_) | |
| 2177 return; | |
| 2178 | |
| 2179 GtkTextIter end; | |
| 2180 gtk_text_buffer_get_iter_at_mark(text_buffer_, &end, gray_text_mark_); | |
| 2181 if (gtk_text_iter_compare(iter, &end) > 0) | |
| 2182 *iter = end; | |
| 2183 } | |
| 2184 | |
| 2185 void OmniboxViewGtk::AdjustVerticalAlignmentOfGrayTextView() { | |
| 2186 // By default, GtkTextView layouts an anchored child widget just above the | |
| 2187 // baseline, so we need to move the |gray_text_view_| down to make sure it | |
| 2188 // has the same baseline as the |text_view_|. | |
| 2189 PangoLayout* layout = gtk_label_get_layout(GTK_LABEL(gray_text_view_)); | |
| 2190 int height; | |
| 2191 pango_layout_get_size(layout, NULL, &height); | |
| 2192 int baseline = pango_layout_get_baseline(layout); | |
| 2193 g_object_set(gray_text_anchor_tag_, "rise", baseline - height, NULL); | |
| 2194 } | |
| OLD | NEW |