OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2009 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/autocomplete/autocomplete_edit_view_gtk.h" |
| 6 |
| 7 #include <gtk/gtk.h> |
| 8 |
| 9 #include "base/logging.h" |
| 10 #include "base/string_util.h" |
| 11 #include "chrome/browser/autocomplete/autocomplete_edit.h" |
| 12 #include "chrome/browser/autocomplete/autocomplete_popup_model.h" |
| 13 #include "chrome/browser/autocomplete/autocomplete_popup_view_gtk.h" |
| 14 #include "chrome/browser/tab_contents/tab_contents.h" |
| 15 #include "chrome/browser/toolbar_model.h" |
| 16 #include "chrome/common/notification_service.h" |
| 17 #include "googleurl/src/gurl.h" |
| 18 |
| 19 namespace { |
| 20 |
| 21 const char kTextBaseColor[] = "#808080"; |
| 22 const char kSecureSchemeColor[] = "#009614"; |
| 23 const char kInsecureSchemeColor[] = "#009614"; |
| 24 const GdkColor kSecureBackgroundColor = {0, 65535, 62965, 50115}; // #fff5c3 |
| 25 const GdkColor kInsecureBackgroundColor = {0, 65535, 65535, 65535}; // #ffffff |
| 26 |
| 27 } // namespace |
| 28 |
| 29 AutocompleteEditViewGtk::AutocompleteEditViewGtk( |
| 30 AutocompleteEditController* controller, |
| 31 ToolbarModel* toolbar_model, |
| 32 Profile* profile, |
| 33 CommandUpdater* command_updater) |
| 34 : text_view_(NULL), |
| 35 tag_table_(NULL), |
| 36 text_buffer_(NULL), |
| 37 base_tag_(NULL), |
| 38 secure_scheme_tag_(NULL), |
| 39 insecure_scheme_tag_(NULL), |
| 40 model_(new AutocompleteEditModel(this, controller, profile)), |
| 41 popup_view_(new AutocompletePopupViewGtk(this, model_.get(), profile)), |
| 42 controller_(controller), |
| 43 toolbar_model_(toolbar_model), |
| 44 command_updater_(command_updater), |
| 45 popup_window_mode_(false), // TODO(deanm) |
| 46 scheme_security_level_(ToolbarModel::NORMAL) { |
| 47 model_->set_popup_model(popup_view_->model()); |
| 48 } |
| 49 |
| 50 AutocompleteEditViewGtk::~AutocompleteEditViewGtk() { |
| 51 NotificationService::current()->Notify( |
| 52 NotificationType::AUTOCOMPLETE_EDIT_DESTROYED, |
| 53 Source<AutocompleteEditViewGtk>(this), |
| 54 NotificationService::NoDetails()); |
| 55 } |
| 56 |
| 57 void AutocompleteEditViewGtk::Init() { |
| 58 tag_table_ = gtk_text_tag_table_new(); |
| 59 text_buffer_ = gtk_text_buffer_new(tag_table_); |
| 60 text_view_ = gtk_text_view_new_with_buffer(text_buffer_); |
| 61 |
| 62 gtk_text_view_set_left_margin(GTK_TEXT_VIEW(text_view_), 4); |
| 63 gtk_text_view_set_right_margin(GTK_TEXT_VIEW(text_view_), 4); |
| 64 |
| 65 // TODO(deanm): This is a super lame attempt to vertically center our single |
| 66 // line of text in a multiline edit control. Mannnn. |
| 67 gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(text_view_), 4); |
| 68 |
| 69 // TODO(deanm): This will probably have to be handled differently with the |
| 70 // tab to search business. Maybe we should just eat the tab characters. |
| 71 // We want the tab key to move focus, not insert a tab. |
| 72 gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(text_view_), false); |
| 73 |
| 74 base_tag_ = gtk_text_buffer_create_tag(text_buffer_, |
| 75 NULL, "foreground", kTextBaseColor, NULL); |
| 76 secure_scheme_tag_ = gtk_text_buffer_create_tag(text_buffer_, |
| 77 NULL, "foreground", kSecureSchemeColor, NULL); |
| 78 insecure_scheme_tag_ = gtk_text_buffer_create_tag(text_buffer_, |
| 79 NULL, "foreground", kInsecureSchemeColor, NULL); |
| 80 |
| 81 // NOTE: This code used to connect to "changed", however this was fired too |
| 82 // often and during bad times (our own buffer changes?). It works out much |
| 83 // better to listen to end-user-action, which should be fired whenever the |
| 84 // user makes some sort of change to the buffer. |
| 85 g_signal_connect(text_buffer_, "begin-user-action", |
| 86 G_CALLBACK(&HandleBeginUserActionThunk), this); |
| 87 g_signal_connect(text_buffer_, "end-user-action", |
| 88 G_CALLBACK(&HandleEndUserActionThunk), this); |
| 89 g_signal_connect(text_view_, "size-request", |
| 90 G_CALLBACK(&HandleViewSizeRequest), this); |
| 91 g_signal_connect(text_view_, "button-press-event", |
| 92 G_CALLBACK(&HandleViewButtonPressThunk), this); |
| 93 g_signal_connect(text_view_, "focus-in-event", |
| 94 G_CALLBACK(&HandleViewFocusInThunk), this); |
| 95 g_signal_connect(text_view_, "focus-out-event", |
| 96 G_CALLBACK(&HandleViewFocusOutThunk), this); |
| 97 // NOTE: The GtkTextView documentation asks you not to connect to this |
| 98 // signal, but it is very convenient and clean for catching up/down. |
| 99 g_signal_connect(text_view_, "move-cursor", |
| 100 G_CALLBACK(&HandleViewMoveCursorThunk), this); |
| 101 } |
| 102 |
| 103 void AutocompleteEditViewGtk::FocusLocation() { |
| 104 gtk_widget_grab_focus(text_view_); |
| 105 SelectAll(false); |
| 106 } |
| 107 |
| 108 void AutocompleteEditViewGtk::SaveStateToTab(TabContents* tab) { |
| 109 NOTIMPLEMENTED(); |
| 110 } |
| 111 |
| 112 void AutocompleteEditViewGtk::Update(const TabContents* contents) { |
| 113 // NOTE: We're getting the URL text here from the ToolbarModel. |
| 114 bool visibly_changed_permanent_text = |
| 115 model_->UpdatePermanentText(toolbar_model_->GetText()); |
| 116 |
| 117 ToolbarModel::SecurityLevel security_level = |
| 118 toolbar_model_->GetSchemeSecurityLevel(); |
| 119 bool changed_security_level = (security_level != scheme_security_level_); |
| 120 scheme_security_level_ = security_level; |
| 121 |
| 122 if (contents) { |
| 123 RevertAll(); |
| 124 // TODO(deanm): Tab switching. The Windows code puts some state in a |
| 125 // PropertyBag on the tab contents, and restores state from there. |
| 126 } else if (visibly_changed_permanent_text) { |
| 127 RevertAll(); |
| 128 // TODO(deanm): There should be code to restore select all here. |
| 129 } else if(changed_security_level) { |
| 130 EmphasizeURLComponents(); |
| 131 } |
| 132 } |
| 133 |
| 134 void AutocompleteEditViewGtk::OpenURL(const GURL& url, |
| 135 WindowOpenDisposition disposition, |
| 136 PageTransition::Type transition, |
| 137 const GURL& alternate_nav_url, |
| 138 size_t selected_line, |
| 139 const std::wstring& keyword) { |
| 140 if (!url.is_valid()) |
| 141 return; |
| 142 |
| 143 model_->SendOpenNotification(selected_line, keyword); |
| 144 |
| 145 if (disposition != NEW_BACKGROUND_TAB) |
| 146 RevertAll(); // Revert the box to its unedited state |
| 147 controller_->OnAutocompleteAccept(url, disposition, transition, |
| 148 alternate_nav_url); |
| 149 } |
| 150 |
| 151 std::wstring AutocompleteEditViewGtk::GetText() const { |
| 152 GtkTextIter start, end; |
| 153 gtk_text_buffer_get_bounds(text_buffer_, &start, &end); |
| 154 gchar* utf8 = gtk_text_buffer_get_text(text_buffer_, &start, &end, false); |
| 155 std::wstring out(UTF8ToWide(utf8)); |
| 156 g_free(utf8); |
| 157 return out; |
| 158 } |
| 159 |
| 160 void AutocompleteEditViewGtk::SetUserText(const std::wstring& text, |
| 161 const std::wstring& display_text, |
| 162 bool update_popup) { |
| 163 NOTIMPLEMENTED(); |
| 164 } |
| 165 |
| 166 void AutocompleteEditViewGtk::SetWindowTextAndCaretPos(const std::wstring& text, |
| 167 size_t caret_pos) { |
| 168 std::string utf8 = WideToUTF8(text); |
| 169 gtk_text_buffer_set_text(text_buffer_, utf8.data(), utf8.length()); |
| 170 EmphasizeURLComponents(); |
| 171 |
| 172 GtkTextIter cur_pos; |
| 173 gtk_text_buffer_get_iter_at_offset(text_buffer_, &cur_pos, caret_pos); |
| 174 gtk_text_buffer_place_cursor(text_buffer_, &cur_pos); |
| 175 } |
| 176 |
| 177 bool AutocompleteEditViewGtk::IsSelectAll() { |
| 178 NOTIMPLEMENTED(); |
| 179 return false; |
| 180 } |
| 181 |
| 182 void AutocompleteEditViewGtk::SelectAll(bool reversed) { |
| 183 GtkTextIter start, end; |
| 184 if (reversed) { |
| 185 gtk_text_buffer_get_bounds(text_buffer_, &end, &start); |
| 186 } else { |
| 187 gtk_text_buffer_get_bounds(text_buffer_, &start, &end); |
| 188 } |
| 189 gtk_text_buffer_place_cursor(text_buffer_, &start); |
| 190 gtk_text_buffer_select_range(text_buffer_, &start, &end); |
| 191 } |
| 192 |
| 193 void AutocompleteEditViewGtk::RevertAll() { |
| 194 ClosePopup(); |
| 195 model_->Revert(); |
| 196 TextChanged(); |
| 197 } |
| 198 |
| 199 void AutocompleteEditViewGtk::UpdatePopup() { |
| 200 model_->SetInputInProgress(true); |
| 201 if (!model_->has_focus()) |
| 202 return; |
| 203 |
| 204 // Don't inline autocomplete when the caret/selection isn't at the end of |
| 205 // the text. |
| 206 CharRange sel = GetSelection(); |
| 207 model_->StartAutocomplete(sel.cp_max < GetTextLength()); |
| 208 } |
| 209 |
| 210 void AutocompleteEditViewGtk::ClosePopup() { |
| 211 popup_view_->model()->StopAutocomplete(); |
| 212 } |
| 213 |
| 214 void AutocompleteEditViewGtk::OnTemporaryTextMaybeChanged( |
| 215 const std::wstring& display_text, |
| 216 bool save_original_selection) { |
| 217 // TODO(deanm): Ignoring save_original_selection here, etc. |
| 218 SetWindowTextAndCaretPos(display_text, display_text.length()); |
| 219 TextChanged(); |
| 220 } |
| 221 |
| 222 bool AutocompleteEditViewGtk::OnInlineAutocompleteTextMaybeChanged( |
| 223 const std::wstring& display_text, |
| 224 size_t user_text_length) { |
| 225 if (display_text == GetText()) |
| 226 return false; |
| 227 |
| 228 SetWindowTextAndCaretPos(display_text, 0); |
| 229 |
| 230 // Select the part of the text that was inline autocompleted. |
| 231 GtkTextIter bound, insert; |
| 232 gtk_text_buffer_get_bounds(text_buffer_, &insert, &bound); |
| 233 gtk_text_buffer_get_iter_at_offset(text_buffer_, &insert, user_text_length); |
| 234 gtk_text_buffer_select_range(text_buffer_, &insert, &bound); |
| 235 |
| 236 TextChanged(); |
| 237 return true; |
| 238 } |
| 239 |
| 240 void AutocompleteEditViewGtk::OnRevertTemporaryText() { |
| 241 NOTIMPLEMENTED(); |
| 242 } |
| 243 |
| 244 void AutocompleteEditViewGtk::OnBeforePossibleChange() { |
| 245 // Record our state. |
| 246 text_before_change_ = GetText(); |
| 247 sel_before_change_ = GetSelection(); |
| 248 } |
| 249 |
| 250 // TODO(deanm): This is mostly stolen from Windows, and will need some work. |
| 251 bool AutocompleteEditViewGtk::OnAfterPossibleChange() { |
| 252 CharRange new_sel = GetSelection(); |
| 253 int length = GetTextLength(); |
| 254 bool selection_differs = (new_sel.cp_min != sel_before_change_.cp_min) || |
| 255 (new_sel.cp_max != sel_before_change_.cp_max); |
| 256 bool at_end_of_edit = (new_sel.cp_min == length && new_sel.cp_max == length); |
| 257 |
| 258 // See if the text or selection have changed since OnBeforePossibleChange(). |
| 259 std::wstring new_text(GetText()); |
| 260 bool text_differs = (new_text != text_before_change_); |
| 261 |
| 262 // When the user has deleted text, we don't allow inline autocomplete. Make |
| 263 // sure to not flag cases like selecting part of the text and then pasting |
| 264 // (or typing) the prefix of that selection. (We detect these by making |
| 265 // sure the caret, which should be after any insertion, hasn't moved |
| 266 // forward of the old selection start.) |
| 267 bool just_deleted_text = |
| 268 (text_before_change_.length() > new_text.length()) && |
| 269 (new_sel.cp_min <= std::min(sel_before_change_.cp_min, |
| 270 sel_before_change_.cp_max)); |
| 271 |
| 272 bool something_changed = model_->OnAfterPossibleChange(new_text, |
| 273 selection_differs, text_differs, just_deleted_text, at_end_of_edit); |
| 274 |
| 275 if (something_changed && text_differs) |
| 276 TextChanged(); |
| 277 |
| 278 return something_changed; |
| 279 } |
| 280 |
| 281 void AutocompleteEditViewGtk::BottomLeftPosWidth(int* x, int* y, int* width) { |
| 282 gdk_window_get_origin(text_view_->window, x, y); |
| 283 *y += text_view_->allocation.height; |
| 284 *width = text_view_->allocation.width; |
| 285 } |
| 286 |
| 287 void AutocompleteEditViewGtk::HandleBeginUserAction() { |
| 288 OnBeforePossibleChange(); |
| 289 } |
| 290 |
| 291 void AutocompleteEditViewGtk::HandleEndUserAction() { |
| 292 bool had_newline = false; |
| 293 |
| 294 // TODO(deanm): obviously super inefficient. |
| 295 for(;;) { |
| 296 GtkTextIter cur, end; |
| 297 gtk_text_buffer_get_bounds(text_buffer_, &cur, &end); |
| 298 |
| 299 while (!gtk_text_iter_equal(&cur, &end)) { |
| 300 if (gtk_text_iter_ends_line(&cur)) { |
| 301 had_newline = true; |
| 302 GtkTextIter next = cur; |
| 303 gtk_text_iter_forward_char(&next); |
| 304 gtk_text_buffer_delete(text_buffer_, &cur, &next); |
| 305 |
| 306 // We've invalidated our iterators, gotta start again. |
| 307 break; |
| 308 } |
| 309 |
| 310 gtk_text_iter_forward_char(&cur); |
| 311 } |
| 312 |
| 313 // We've exhausted the whole input and there is now only 1 line, good. |
| 314 if (gtk_text_iter_equal(&cur, &end)) |
| 315 break; |
| 316 } |
| 317 |
| 318 OnAfterPossibleChange(); |
| 319 |
| 320 if (had_newline) |
| 321 model_->AcceptInput(CURRENT_TAB, false); |
| 322 } |
| 323 |
| 324 // static |
| 325 void AutocompleteEditViewGtk::HandleViewSizeRequest(GtkWidget* view, |
| 326 GtkRequisition* req, |
| 327 gpointer unused) { |
| 328 // Don't force a minimum size, allow our embedder to size us better. |
| 329 req->height = req->width = 1; |
| 330 } |
| 331 |
| 332 gboolean AutocompleteEditViewGtk::HandleViewButtonPress(GdkEventButton* event) { |
| 333 // When the GtkTextView is clicked, it will call gtk_widget_grab_focus. |
| 334 // I believe this causes the focus-in event to be fired before the main |
| 335 // clicked handling code. If we were to try to set the selection from |
| 336 // the focus-in event, it's just going to be undone by the click handler. |
| 337 // This is a bit ugly. We shim in to get the click before the GtkTextView, |
| 338 // then if we don't have focus, we (hopefully safely) assume that the click |
| 339 // will cause us to become focused. We call GtkTextView's default handler |
| 340 // and then stop propagation. This allows us to run our code after the |
| 341 // default handler, even if that handler stopped propagation. |
| 342 if (GTK_WIDGET_HAS_FOCUS(text_view_)) |
| 343 return FALSE; // Continue to propagate into the GtkTextView handler. |
| 344 |
| 345 // Call the GtkTextView default handler, ignoring the fact that it will |
| 346 // likely have told us to stop propagating. We want to handle selection. |
| 347 GtkWidgetClass* klass = GTK_WIDGET_GET_CLASS(text_view_); |
| 348 klass->button_press_event(text_view_, event); |
| 349 |
| 350 // Select the full input when we get focus. |
| 351 SelectAll(false); |
| 352 // So we told the buffer where the cursor should be, but make sure to tell |
| 353 // the view so it can scroll it to be visible if needed. |
| 354 // NOTE: This function doesn't seem to like a count of 0, looking at the |
| 355 // code it will skip an important loop. Use -1 to achieve the same. |
| 356 GtkTextIter start, end; |
| 357 gtk_text_buffer_get_bounds(text_buffer_, &start, &end); |
| 358 gtk_text_view_move_visually(GTK_TEXT_VIEW(text_view_), &start, -1); |
| 359 |
| 360 return TRUE; // Don't continue, we called the default handler already. |
| 361 } |
| 362 |
| 363 gboolean AutocompleteEditViewGtk::HandleViewFocusIn() { |
| 364 model_->OnSetFocus(false); |
| 365 // TODO(deanm): Some keyword hit business, etc here. |
| 366 |
| 367 return FALSE; // Continue propagation. |
| 368 } |
| 369 |
| 370 gboolean AutocompleteEditViewGtk::HandleViewFocusOut() { |
| 371 // Close the popup. |
| 372 ClosePopup(); |
| 373 |
| 374 // Tell the model to reset itself. |
| 375 model_->OnKillFocus(); |
| 376 |
| 377 // TODO(deanm): This probably isn't right, and doesn't match Windows. We |
| 378 // don't really want to match Windows though, because it probably feels |
| 379 // wrong on Linux. Firefox doesn't have great behavior here also, imo. |
| 380 // Deselect any selection and make sure the input is at the beginning. |
| 381 GtkTextIter start, end; |
| 382 gtk_text_buffer_get_bounds(text_buffer_, &start, &end); |
| 383 gtk_text_buffer_place_cursor(text_buffer_, &start); |
| 384 return FALSE; // Pass the event on to the GtkTextView. |
| 385 } |
| 386 |
| 387 void AutocompleteEditViewGtk::HandleViewMoveCursor( |
| 388 GtkMovementStep step, |
| 389 gint count, |
| 390 gboolean extendion_selection) { |
| 391 // Handle up / down cursor movement on our own. |
| 392 if (step == GTK_MOVEMENT_DISPLAY_LINES) { |
| 393 model_->OnUpOrDownKeyPressed(count); |
| 394 // move-cursor doesn't use a signal accumulator on the return value (it |
| 395 // just ignores them), so we have to stop the propagation. |
| 396 g_signal_stop_emission_by_name(text_view_, "move-cursor"); |
| 397 return; |
| 398 } |
| 399 // Propagate into GtkTextView. |
| 400 } |
| 401 |
| 402 AutocompleteEditViewGtk::CharRange AutocompleteEditViewGtk::GetSelection() { |
| 403 // You can not just use get_selection_bounds here, since the order will be |
| 404 // ascending, and you don't know where the user's start and end of the |
| 405 // selection was (if the selection was forwards or backwards). Get the |
| 406 // actual marks so that we can preserve the selection direction. |
| 407 GtkTextIter start, insert; |
| 408 GtkTextMark* mark; |
| 409 |
| 410 mark = gtk_text_buffer_get_selection_bound(text_buffer_); |
| 411 gtk_text_buffer_get_iter_at_mark(text_buffer_, &start, mark); |
| 412 |
| 413 mark = gtk_text_buffer_get_insert(text_buffer_); |
| 414 gtk_text_buffer_get_iter_at_mark(text_buffer_, &insert, mark); |
| 415 |
| 416 return CharRange(gtk_text_iter_get_offset(&start), |
| 417 gtk_text_iter_get_offset(&insert)); |
| 418 } |
| 419 |
| 420 void AutocompleteEditViewGtk::ItersFromCharRange(const CharRange& range, |
| 421 GtkTextIter* iter_min, |
| 422 GtkTextIter* iter_max) { |
| 423 gtk_text_buffer_get_iter_at_offset(text_buffer_, iter_min, range.cp_min); |
| 424 gtk_text_buffer_get_iter_at_offset(text_buffer_, iter_max, range.cp_max); |
| 425 } |
| 426 |
| 427 int AutocompleteEditViewGtk::GetTextLength() { |
| 428 GtkTextIter start, end; |
| 429 gtk_text_buffer_get_bounds(text_buffer_, &start, &end); |
| 430 return gtk_text_iter_get_offset(&end); |
| 431 } |
| 432 |
| 433 void AutocompleteEditViewGtk::EmphasizeURLComponents() { |
| 434 // See whether the contents are a URL with a non-empty host portion, which we |
| 435 // should emphasize. To check for a URL, rather than using the type returned |
| 436 // by Parse(), ask the model, which will check the desired page transition for |
| 437 // this input. This can tell us whether an UNKNOWN input string is going to |
| 438 // be treated as a search or a navigation, and is the same method the Paste |
| 439 // And Go system uses. |
| 440 url_parse::Parsed parts; |
| 441 AutocompleteInput::Parse(GetText(), model_->GetDesiredTLD(), &parts, NULL); |
| 442 bool emphasize = model_->CurrentTextIsURL() && (parts.host.len > 0); |
| 443 |
| 444 // Set the baseline emphasis. |
| 445 GtkTextIter start, end; |
| 446 gtk_text_buffer_get_bounds(text_buffer_, &start, &end); |
| 447 gtk_text_buffer_remove_all_tags(text_buffer_, &start, &end); |
| 448 if (emphasize) { |
| 449 gtk_text_buffer_apply_tag(text_buffer_, base_tag_, &start, &end); |
| 450 |
| 451 // We've found a host name, give it more emphasis. |
| 452 gtk_text_buffer_get_iter_at_line_index(text_buffer_, &start, 0, |
| 453 parts.host.begin); |
| 454 gtk_text_buffer_get_iter_at_line_index(text_buffer_, &end, 0, |
| 455 parts.host.end()); |
| 456 gtk_text_buffer_remove_all_tags(text_buffer_, &start, &end); |
| 457 } |
| 458 |
| 459 // Emphasize the scheme for security UI display purposes (if necessary). |
| 460 if (!model_->user_input_in_progress() && parts.scheme.is_nonempty() && |
| 461 (scheme_security_level_ != ToolbarModel::NORMAL)) { |
| 462 gtk_text_buffer_get_iter_at_line_index(text_buffer_, &start, 0, |
| 463 parts.scheme.begin); |
| 464 gtk_text_buffer_get_iter_at_line_index(text_buffer_, &end, 0, |
| 465 parts.scheme.end()); |
| 466 const GdkColor* background; |
| 467 if (scheme_security_level_ == ToolbarModel::SECURE) { |
| 468 background = &kSecureBackgroundColor; |
| 469 gtk_text_buffer_apply_tag(text_buffer_, secure_scheme_tag_, |
| 470 &start, &end); |
| 471 } else { |
| 472 background = &kInsecureBackgroundColor; |
| 473 gtk_text_buffer_apply_tag(text_buffer_, insecure_scheme_tag_, |
| 474 &start, &end); |
| 475 } |
| 476 gtk_widget_modify_base(text_view_, GTK_STATE_NORMAL, background); |
| 477 } |
| 478 } |
| 479 |
| 480 void AutocompleteEditViewGtk::TextChanged() { |
| 481 EmphasizeURLComponents(); |
| 482 controller_->OnChanged(); |
| 483 } |
OLD | NEW |