blob: b6302711e78336912a9177f3aae9521b611acedc [file] [log] [blame]
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/autocomplete/autocomplete_edit_view_win.h"
#include <algorithm>
#include <locale>
#include <string>
#include <richedit.h>
#include <textserv.h>
#include "app/win/iat_patch_function.h"
#include "base/auto_reset.h"
#include "base/basictypes.h"
#include "base/i18n/rtl.h"
#include "base/lazy_instance.h"
#include "base/memory/ref_counted.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/autocomplete/autocomplete_accessibility.h"
#include "chrome/browser/autocomplete/autocomplete_match.h"
#include "chrome/browser/autocomplete/autocomplete_popup_model.h"
#include "chrome/browser/autocomplete/keyword_provider.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/command_updater.h"
#include "chrome/browser/metrics/user_metrics.h"
#include "chrome/browser/net/url_fixer_upper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url.h"
#include "chrome/browser/search_engines/template_url_model.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/common/notification_service.h"
#include "googleurl/src/url_util.h"
#include "grit/generated_resources.h"
#include "net/base/escape.h"
#include "skia/ext/skia_utils_win.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/drag_source.h"
#include "ui/base/dragdrop/drop_target.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/base/dragdrop/os_exchange_data_provider_win.h"
#include "ui/base/keycodes/keyboard_codes.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_win.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/canvas_skia.h"
#include "views/controls/textfield/native_textfield_win.h"
#include "views/drag_utils.h"
#include "views/events/event_utils_win.h"
#include "views/focus/focus_util_win.h"
#include "views/widget/widget.h"
#pragma comment(lib, "oleacc.lib") // Needed for accessibility support.
#pragma comment(lib, "riched20.lib") // Needed for the richedit control.
///////////////////////////////////////////////////////////////////////////////
// AutocompleteEditModel
namespace {
// A helper method for determining a valid DROPEFFECT given the allowed
// DROPEFFECTS. We prefer copy over link.
DWORD CopyOrLinkDropEffect(DWORD effect) {
if (effect & DROPEFFECT_COPY)
return DROPEFFECT_COPY;
if (effect & DROPEFFECT_LINK)
return DROPEFFECT_LINK;
return DROPEFFECT_NONE;
}
// A helper method for determining a valid drag operation given the allowed
// operation. We prefer copy over link.
int CopyOrLinkDragOperation(int drag_operation) {
if (drag_operation & ui::DragDropTypes::DRAG_COPY)
return ui::DragDropTypes::DRAG_COPY;
if (drag_operation & ui::DragDropTypes::DRAG_LINK)
return ui::DragDropTypes::DRAG_LINK;
return ui::DragDropTypes::DRAG_NONE;
}
// The AutocompleteEditState struct contains enough information about the
// AutocompleteEditModel and AutocompleteEditViewWin to save/restore a user's
// typing, caret position, etc. across tab changes. We explicitly don't
// preserve things like whether the popup was open as this might be weird.
struct AutocompleteEditState {
AutocompleteEditState(const AutocompleteEditModel::State& model_state,
const AutocompleteEditViewWin::State& view_state)
: model_state(model_state),
view_state(view_state) {
}
const AutocompleteEditModel::State model_state;
const AutocompleteEditViewWin::State view_state;
};
// Returns true if the current point is far enough from the origin that it
// would be considered a drag.
bool IsDrag(const POINT& origin, const POINT& current) {
return views::View::ExceededDragThreshold(current.x - origin.x,
current.y - origin.y);
}
} // namespace
// EditDropTarget is the IDropTarget implementation installed on
// AutocompleteEditViewWin. EditDropTarget prefers URL over plain text. A drop
// of a URL replaces all the text of the edit and navigates immediately to the
// URL. A drop of plain text from the same edit either copies or moves the
// selected text, and a drop of plain text from a source other than the edit
// does a paste and go.
class AutocompleteEditViewWin::EditDropTarget : public ui::DropTarget {
public:
explicit EditDropTarget(AutocompleteEditViewWin* edit);
protected:
virtual DWORD OnDragEnter(IDataObject* data_object,
DWORD key_state,
POINT cursor_position,
DWORD effect);
virtual DWORD OnDragOver(IDataObject* data_object,
DWORD key_state,
POINT cursor_position,
DWORD effect);
virtual void OnDragLeave(IDataObject* data_object);
virtual DWORD OnDrop(IDataObject* data_object,
DWORD key_state,
POINT cursor_position,
DWORD effect);
private:
// If dragging a string, the drop highlight position of the edit is reset
// based on the mouse position.
void UpdateDropHighlightPosition(const POINT& cursor_screen_position);
// Resets the visual drop indicates we install on the edit.
void ResetDropHighlights();
// The edit we're the drop target for.
AutocompleteEditViewWin* edit_;
// If true, the drag session contains a URL.
bool drag_has_url_;
// If true, the drag session contains a string. If drag_has_url_ is true,
// this is false regardless of whether the clipboard has a string.
bool drag_has_string_;
DISALLOW_COPY_AND_ASSIGN(EditDropTarget);
};
AutocompleteEditViewWin::EditDropTarget::EditDropTarget(
AutocompleteEditViewWin* edit)
: ui::DropTarget(edit->m_hWnd),
edit_(edit),
drag_has_url_(false),
drag_has_string_(false) {
}
DWORD AutocompleteEditViewWin::EditDropTarget::OnDragEnter(
IDataObject* data_object,
DWORD key_state,
POINT cursor_position,
DWORD effect) {
ui::OSExchangeData os_data(new ui::OSExchangeDataProviderWin(data_object));
drag_has_url_ = os_data.HasURL();
drag_has_string_ = !drag_has_url_ && os_data.HasString();
if (drag_has_url_) {
if (edit_->in_drag()) {
// The edit we're associated with originated the drag. No point in
// allowing the user to drop back on us.
drag_has_url_ = false;
}
// NOTE: it would be nice to visually show all the text is going to
// be replaced by selecting all, but this caused painting problems. In
// particular the flashing caret would appear outside the edit! For now
// we stick with no visual indicator other than that shown own the mouse
// cursor.
}
return OnDragOver(data_object, key_state, cursor_position, effect);
}
DWORD AutocompleteEditViewWin::EditDropTarget::OnDragOver(
IDataObject* data_object,
DWORD key_state,
POINT cursor_position,
DWORD effect) {
if (drag_has_url_)
return CopyOrLinkDropEffect(effect);
if (drag_has_string_) {
UpdateDropHighlightPosition(cursor_position);
if (edit_->drop_highlight_position() == -1 && edit_->in_drag())
return DROPEFFECT_NONE;
if (edit_->in_drag()) {
// The edit we're associated with originated the drag. Do the normal drag
// behavior.
DCHECK((effect & DROPEFFECT_COPY) && (effect & DROPEFFECT_MOVE));
return (key_state & MK_CONTROL) ? DROPEFFECT_COPY : DROPEFFECT_MOVE;
}
// Our edit didn't originate the drag, only allow link or copy.
return CopyOrLinkDropEffect(effect);
}
return DROPEFFECT_NONE;
}
void AutocompleteEditViewWin::EditDropTarget::OnDragLeave(
IDataObject* data_object) {
ResetDropHighlights();
}
DWORD AutocompleteEditViewWin::EditDropTarget::OnDrop(
IDataObject* data_object,
DWORD key_state,
POINT cursor_position,
DWORD effect) {
effect = OnDragOver(data_object, key_state, cursor_position, effect);
ui::OSExchangeData os_data(new ui::OSExchangeDataProviderWin(data_object));
views::DropTargetEvent event(os_data, cursor_position.x, cursor_position.y,
ui::DragDropTypes::DropEffectToDragOperation(effect));
int drag_operation = edit_->OnPerformDropImpl(event, edit_->in_drag());
if (!drag_has_url_)
ResetDropHighlights();
return ui::DragDropTypes::DragOperationToDropEffect(drag_operation);
}
void AutocompleteEditViewWin::EditDropTarget::UpdateDropHighlightPosition(
const POINT& cursor_screen_position) {
if (drag_has_string_) {
POINT client_position = cursor_screen_position;
::ScreenToClient(edit_->m_hWnd, &client_position);
int drop_position = edit_->CharFromPos(client_position);
if (edit_->in_drag()) {
// Our edit originated the drag, don't allow a drop if over the selected
// region.
LONG sel_start, sel_end;
edit_->GetSel(sel_start, sel_end);
if ((sel_start != sel_end) && (drop_position >= sel_start) &&
(drop_position <= sel_end))
drop_position = -1;
} else {
// A drop from a source other than the edit replaces all the text, so
// we don't show the drop location. See comment in OnDragEnter as to why
// we don't try and select all here.
drop_position = -1;
}
edit_->SetDropHighlightPosition(drop_position);
}
}
void AutocompleteEditViewWin::EditDropTarget::ResetDropHighlights() {
if (drag_has_string_)
edit_->SetDropHighlightPosition(-1);
}
///////////////////////////////////////////////////////////////////////////////
// Helper classes
AutocompleteEditViewWin::ScopedFreeze::ScopedFreeze(
AutocompleteEditViewWin* edit,
ITextDocument* text_object_model)
: edit_(edit),
text_object_model_(text_object_model) {
// Freeze the screen.
if (text_object_model_) {
long count;
text_object_model_->Freeze(&count);
}
}
AutocompleteEditViewWin::ScopedFreeze::~ScopedFreeze() {
// Unfreeze the screen.
// NOTE: If this destructor is reached while the edit is being destroyed (for
// example, because we double-clicked the edit of a popup and caused it to
// transform to an unconstrained window), it will no longer have an HWND, and
// text_object_model_ may point to a destroyed object, so do nothing here.
if (edit_->IsWindow() && text_object_model_) {
long count;
text_object_model_->Unfreeze(&count);
if (count == 0) {
// We need to UpdateWindow() here in addition to InvalidateRect() because,
// as far as I can tell, the edit likes to synchronously erase its
// background when unfreezing, thus requiring us to synchronously redraw
// if we don't want flicker.
edit_->InvalidateRect(NULL, false);
edit_->UpdateWindow();
}
}
}
AutocompleteEditViewWin::ScopedSuspendUndo::ScopedSuspendUndo(
ITextDocument* text_object_model)
: text_object_model_(text_object_model) {
// Suspend Undo processing.
if (text_object_model_)
text_object_model_->Undo(tomSuspend, NULL);
}
AutocompleteEditViewWin::ScopedSuspendUndo::~ScopedSuspendUndo() {
// Resume Undo processing.
if (text_object_model_)
text_object_model_->Undo(tomResume, NULL);
}
///////////////////////////////////////////////////////////////////////////////
// AutocompleteEditViewWin
namespace {
// These are used to hook the CRichEditCtrl's calls to BeginPaint() and
// EndPaint() and provide a memory DC instead. See OnPaint().
HWND edit_hwnd = NULL;
PAINTSTRUCT paint_struct;
// Intercepted method for BeginPaint(). Must use __stdcall convention.
HDC WINAPI BeginPaintIntercept(HWND hWnd, LPPAINTSTRUCT lpPaint) {
if (!edit_hwnd || (hWnd != edit_hwnd))
return ::BeginPaint(hWnd, lpPaint);
*lpPaint = paint_struct;
return paint_struct.hdc;
}
// Intercepted method for EndPaint(). Must use __stdcall convention.
BOOL WINAPI EndPaintIntercept(HWND hWnd, const PAINTSTRUCT* lpPaint) {
return (edit_hwnd && (hWnd == edit_hwnd)) || ::EndPaint(hWnd, lpPaint);
}
// Returns a lazily initialized property bag accessor for saving our state in a
// TabContents.
PropertyAccessor<AutocompleteEditState>* GetStateAccessor() {
static PropertyAccessor<AutocompleteEditState> state;
return &state;
}
class PaintPatcher {
public:
PaintPatcher();
~PaintPatcher();
void RefPatch();
void DerefPatch();
private:
size_t refcount_;
app::win::IATPatchFunction begin_paint_;
app::win::IATPatchFunction end_paint_;
DISALLOW_COPY_AND_ASSIGN(PaintPatcher);
};
PaintPatcher::PaintPatcher() : refcount_(0) {
}
PaintPatcher::~PaintPatcher() {
DCHECK_EQ(0U, refcount_);
}
void PaintPatcher::RefPatch() {
if (refcount_ == 0) {
DCHECK(!begin_paint_.is_patched());
DCHECK(!end_paint_.is_patched());
begin_paint_.Patch(L"riched20.dll", "user32.dll", "BeginPaint",
&BeginPaintIntercept);
end_paint_.Patch(L"riched20.dll", "user32.dll", "EndPaint",
&EndPaintIntercept);
}
++refcount_;
}
void PaintPatcher::DerefPatch() {
DCHECK(begin_paint_.is_patched());
DCHECK(end_paint_.is_patched());
--refcount_;
if (refcount_ == 0) {
begin_paint_.Unpatch();
end_paint_.Unpatch();
}
}
base::LazyInstance<PaintPatcher> g_paint_patcher(base::LINKER_INITIALIZED);
// twips are a unit of type measurement, and RichEdit controls use them
// to set offsets.
const int kTwipsPerInch = 1440;
} // namespace
AutocompleteEditViewWin::AutocompleteEditViewWin(
const gfx::Font& font,
AutocompleteEditController* controller,
ToolbarModel* toolbar_model,
LocationBarView* parent_view,
HWND hwnd,
Profile* profile,
CommandUpdater* command_updater,
bool popup_window_mode,
const views::View* location_bar)
: model_(new AutocompleteEditModel(this, controller, profile)),
popup_view_(new AutocompletePopupContentsView(font, this, model_.get(),
profile, location_bar)),
controller_(controller),
parent_view_(parent_view),
toolbar_model_(toolbar_model),
command_updater_(command_updater),
popup_window_mode_(popup_window_mode),
force_hidden_(false),
tracking_click_(),
tracking_double_click_(false),
double_click_time_(0),
can_discard_mousemove_(false),
ignore_ime_messages_(false),
delete_at_end_pressed_(false),
font_(font),
possible_drag_(false),
in_drag_(false),
initiated_drag_(false),
drop_highlight_position_(-1),
background_color_(skia::SkColorToCOLORREF(LocationBarView::GetColor(
ToolbarModel::NONE, LocationBarView::BACKGROUND))),
security_level_(ToolbarModel::NONE),
text_object_model_(NULL) {
// Dummy call to a function exported by riched20.dll to ensure it sets up an
// import dependency on the dll.
CreateTextServices(NULL, NULL, NULL);
saved_selection_for_focus_change_.cpMin = -1;
g_paint_patcher.Pointer()->RefPatch();
Create(hwnd, 0, 0, 0, l10n_util::GetExtendedStyles());
SetReadOnly(popup_window_mode_);
SetFont(font_.GetNativeFont());
// NOTE: Do not use SetWordBreakProcEx() here, that is no longer supported as
// of Rich Edit 2.0 onward.
SendMessage(m_hWnd, EM_SETWORDBREAKPROC, 0,
reinterpret_cast<LPARAM>(&WordBreakProc));
// Get the metrics for the font.
HDC dc = ::GetDC(NULL);
SelectObject(dc, font_.GetNativeFont());
TEXTMETRIC tm = {0};
GetTextMetrics(dc, &tm);
const float kXHeightRatio = 0.7f; // The ratio of a font's x-height to its
// cap height. Sadly, Windows doesn't
// provide a true value for a font's
// x-height in its text metrics, so we
// approximate.
font_x_height_ = static_cast<int>((static_cast<float>(font_.GetBaseline() -
tm.tmInternalLeading) * kXHeightRatio) + 0.5);
// The distance from the top of the field to the desired baseline of the
// rendered text.
const int kTextBaseline = popup_window_mode_ ? 15 : 18;
font_y_adjustment_ = kTextBaseline - font_.GetBaseline();
// Get the number of twips per pixel, which we need below to offset our text
// by the desired number of pixels.
const long kTwipsPerPixel = kTwipsPerInch / GetDeviceCaps(dc, LOGPIXELSY);
::ReleaseDC(NULL, dc);
// Set the default character style -- adjust to our desired baseline.
CHARFORMAT cf = {0};
cf.dwMask = CFM_OFFSET;
cf.yOffset = -font_y_adjustment_ * kTwipsPerPixel;
SetDefaultCharFormat(cf);
SetBackgroundColor(background_color_);
// By default RichEdit has a drop target. Revoke it so that we can install our
// own. Revoke takes care of deleting the existing one.
RevokeDragDrop(m_hWnd);
// Register our drop target. RichEdit appears to invoke RevokeDropTarget when
// done so that we don't have to explicitly.
if (!popup_window_mode_) {
scoped_refptr<EditDropTarget> drop_target = new EditDropTarget(this);
RegisterDragDrop(m_hWnd, drop_target.get());
}
}
AutocompleteEditViewWin::~AutocompleteEditViewWin() {
NotificationService::current()->Notify(
NotificationType::AUTOCOMPLETE_EDIT_DESTROYED,
Source<AutocompleteEditViewWin>(this),
NotificationService::NoDetails());
// Explicitly release the text object model now that we're done with it, and
// before we free the library. If the library gets unloaded before this
// released, it becomes garbage.
text_object_model_->Release();
// We balance our reference count and unpatch when the last instance has
// been destroyed. This prevents us from relying on the AtExit or static
// destructor sequence to do our unpatching, which is generally fragile.
g_paint_patcher.Pointer()->DerefPatch();
}
views::View* AutocompleteEditViewWin::parent_view() const {
return parent_view_;
}
int AutocompleteEditViewWin::WidthOfTextAfterCursor() {
CHARRANGE selection;
GetSelection(selection);
const int start = std::max(0, static_cast<int>(selection.cpMax - 1));
return WidthNeededToDisplay(GetText().substr(start));
}
gfx::Font AutocompleteEditViewWin::GetFont() {
return font_;
}
void AutocompleteEditViewWin::SaveStateToTab(TabContents* tab) {
DCHECK(tab);
const AutocompleteEditModel::State model_state(
model_->GetStateForTabSwitch());
CHARRANGE selection;
GetSelection(selection);
GetStateAccessor()->SetProperty(tab->property_bag(),
AutocompleteEditState(
model_state,
State(selection, saved_selection_for_focus_change_)));
}
void AutocompleteEditViewWin::Update(
const TabContents* tab_for_state_restoring) {
const bool visibly_changed_permanent_text =
model_->UpdatePermanentText(toolbar_model_->GetText());
const ToolbarModel::SecurityLevel security_level =
toolbar_model_->GetSecurityLevel();
const bool changed_security_level = (security_level != security_level_);
// Bail early when no visible state will actually change (prevents an
// unnecessary ScopedFreeze, and thus UpdateWindow()).
if (!changed_security_level && !visibly_changed_permanent_text &&
!tab_for_state_restoring)
return;
// Update our local state as desired. We set security_level_ here so it will
// already be correct before we get to any RevertAll()s below and use it.
security_level_ = security_level;
// When we're switching to a new tab, restore its state, if any.
ScopedFreeze freeze(this, GetTextObjectModel());
if (tab_for_state_restoring) {
// Make sure we reset our own state first. The new tab may not have any
// saved state, or it may not have had input in progress, in which case we
// won't overwrite all our local state.
RevertAll();
const AutocompleteEditState* state = GetStateAccessor()->GetProperty(
tab_for_state_restoring->property_bag());
if (state) {
model_->RestoreState(state->model_state);
// Restore user's selection. We do this after restoring the user_text
// above so we're selecting in the correct string.
SetSelectionRange(state->view_state.selection);
saved_selection_for_focus_change_ =
state->view_state.saved_selection_for_focus_change;
}
} else if (visibly_changed_permanent_text) {
// Not switching tabs, just updating the permanent text. (In the case where
// we _were_ switching tabs, the RevertAll() above already drew the new
// permanent text.)
// Tweak: if the edit was previously nonempty and had all the text selected,
// select all the new text. This makes one particular case better: the
// user clicks in the box to change it right before the permanent URL is
// changed. Since the new URL is still fully selected, the user's typing
// will replace the edit contents as they'd intended.
//
// NOTE: The selection can be longer than the text length if the edit is in
// in rich text mode and the user has selected the "phantom newline" at the
// end, so use ">=" instead of "==" to see if all the text is selected. In
// theory we prevent this case from ever occurring, but this is still safe.
CHARRANGE sel;
GetSelection(sel);
const bool was_reversed = (sel.cpMin > sel.cpMax);
const bool was_sel_all = (sel.cpMin != sel.cpMax) &&
IsSelectAllForRange(sel);
RevertAll();
if (was_sel_all)
SelectAll(was_reversed);
} else if (changed_security_level) {
// Only the security style changed, nothing else. Redraw our text using it.
EmphasizeURLComponents();
}
}
void AutocompleteEditViewWin::OpenURL(const GURL& url,
WindowOpenDisposition disposition,
PageTransition::Type transition,
const GURL& alternate_nav_url,
size_t selected_line,
const string16& keyword) {
if (!url.is_valid())
return;
// When we navigate, we first revert to the unedited state, then if necessary
// synchronously change the permanent text to the new URL. If we don't freeze
// here, the user could potentially see a flicker of the current URL before
// the new one reappears, which would look glitchy.
ScopedFreeze freeze(this, GetTextObjectModel());
model_->OpenURL(url, disposition, transition, alternate_nav_url,
selected_line, keyword);
}
string16 AutocompleteEditViewWin::GetText() const {
const int len = GetTextLength() + 1;
string16 str;
GetWindowText(WriteInto(&str, len), len);
return str;
}
bool AutocompleteEditViewWin::IsEditingOrEmpty() const {
return model_->user_input_in_progress() || (GetTextLength() == 0);
}
int AutocompleteEditViewWin::GetIcon() const {
return IsEditingOrEmpty() ?
AutocompleteMatch::TypeToIcon(model_->CurrentTextType()) :
toolbar_model_->GetIcon();
}
void AutocompleteEditViewWin::SetUserText(const string16& text) {
SetUserText(text, text, true);
}
void AutocompleteEditViewWin::SetUserText(const string16& text,
const string16& display_text,
bool update_popup) {
ScopedFreeze freeze(this, GetTextObjectModel());
model_->SetUserText(text);
saved_selection_for_focus_change_.cpMin = -1;
SetWindowTextAndCaretPos(display_text, display_text.length());
if (update_popup)
UpdatePopup();
TextChanged();
}
void AutocompleteEditViewWin::SetWindowTextAndCaretPos(const string16& text,
size_t caret_pos) {
SetWindowText(text.c_str());
PlaceCaretAt(caret_pos);
}
void AutocompleteEditViewWin::SetForcedQuery() {
const string16 current_text(GetText());
const size_t start = current_text.find_first_not_of(kWhitespaceWide);
if (start == string16::npos || (current_text[start] != '?'))
SetUserText(L"?");
else
SetSelection(current_text.length(), start + 1);
}
bool AutocompleteEditViewWin::IsSelectAll() {
CHARRANGE selection;
GetSel(selection);
return IsSelectAllForRange(selection);
}
bool AutocompleteEditViewWin::DeleteAtEndPressed() {
return delete_at_end_pressed_;
}
void AutocompleteEditViewWin::GetSelectionBounds(string16::size_type* start,
string16::size_type* end) {
CHARRANGE selection;
GetSel(selection);
*start = static_cast<size_t>(selection.cpMin);
*end = static_cast<size_t>(selection.cpMax);
}
void AutocompleteEditViewWin::SelectAll(bool reversed) {
if (reversed)
SetSelection(GetTextLength(), 0);
else
SetSelection(0, GetTextLength());
}
void AutocompleteEditViewWin::RevertAll() {
ScopedFreeze freeze(this, GetTextObjectModel());
ClosePopup();
model_->Revert();
saved_selection_for_focus_change_.cpMin = -1;
TextChanged();
}
void AutocompleteEditViewWin::UpdatePopup() {
ScopedFreeze freeze(this, GetTextObjectModel());
model_->SetInputInProgress(true);
if (!model_->has_focus()) {
// When we're in the midst of losing focus, don't rerun autocomplete. This
// can happen when losing focus causes the IME to cancel/finalize a
// composition. We still want to note that user input is in progress, we
// just don't want to do anything else.
//
// Note that in this case the ScopedFreeze above was unnecessary; however,
// we're inside the callstack of OnKillFocus(), which has already frozen the
// edit, so this will never result in an unnecessary UpdateWindow() call.
return;
}
// Don't inline autocomplete when:
// * The user is deleting text
// * The caret/selection isn't at the end of the text
// * The user has just pasted in something that replaced all the text
// * The user is trying to compose something in an IME
CHARRANGE sel;
GetSel(sel);
model_->StartAutocomplete(sel.cpMax != sel.cpMin,
(sel.cpMax < GetTextLength()) || IsImeComposing());
}
void AutocompleteEditViewWin::ClosePopup() {
model_->StopAutocomplete();
}
void AutocompleteEditViewWin::SetFocus() {
::SetFocus(m_hWnd);
parent_view_->GetWidget()->NotifyAccessibilityEvent(
parent_view_,
ui::AccessibilityTypes::EVENT_FOCUS,
false);
}
IAccessible* AutocompleteEditViewWin::GetIAccessible() {
if (!autocomplete_accessibility_) {
CComObject<AutocompleteAccessibility>* accessibility = NULL;
if (!SUCCEEDED(CComObject<AutocompleteAccessibility>::CreateInstance(
&accessibility)) || !accessibility)
return NULL;
// Wrap the created object in a smart pointer so it won't leak.
base::win::ScopedComPtr<IAccessible> accessibility_comptr(accessibility);
if (!SUCCEEDED(accessibility->Initialize(this)))
return NULL;
// Copy to the class smart pointer, and notify that an instance of
// IAccessible was allocated for m_hWnd.
autocomplete_accessibility_ = accessibility_comptr;
NotifyWinEvent(EVENT_OBJECT_CREATE, m_hWnd, OBJID_CLIENT, CHILDID_SELF);
}
// Detach to leave ref counting to the caller.
return autocomplete_accessibility_.Detach();
}
void AutocompleteEditViewWin::SetDropHighlightPosition(int position) {
if (drop_highlight_position_ != position) {
RepaintDropHighlight(drop_highlight_position_);
drop_highlight_position_ = position;
RepaintDropHighlight(drop_highlight_position_);
}
}
void AutocompleteEditViewWin::MoveSelectedText(int new_position) {
const string16 selected_text(GetSelectedText());
CHARRANGE sel;
GetSel(sel);
DCHECK((sel.cpMax != sel.cpMin) && (new_position >= 0) &&
(new_position <= GetTextLength()));
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
// Nuke the selected text.
ReplaceSel(L"", TRUE);
// And insert it into the new location.
if (new_position >= sel.cpMin)
new_position -= (sel.cpMax - sel.cpMin);
PlaceCaretAt(new_position);
ReplaceSel(selected_text.c_str(), TRUE);
OnAfterPossibleChange();
}
void AutocompleteEditViewWin::InsertText(int position,
const string16& text) {
DCHECK((position >= 0) && (position <= GetTextLength()));
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
SetSelection(position, position);
ReplaceSel(text.c_str());
OnAfterPossibleChange();
}
void AutocompleteEditViewWin::OnTemporaryTextMaybeChanged(
const string16& display_text,
bool save_original_selection) {
if (save_original_selection)
GetSelection(original_selection_);
// Set new text and cursor position. Sometimes this does extra work (e.g.
// when the new text and the old text are identical), but it's only called
// when the user manually changes the selected line in the popup, so that's
// not really a problem. Also, even when the text hasn't changed we'd want to
// update the caret, because if the user had the cursor in the middle of the
// text and then arrowed to another entry with the same text, we'd still want
// to move the caret.
ScopedFreeze freeze(this, GetTextObjectModel());
SetWindowTextAndCaretPos(display_text, display_text.length());
TextChanged();
}
bool AutocompleteEditViewWin::OnInlineAutocompleteTextMaybeChanged(
const string16& display_text,
size_t user_text_length) {
// Update the text and selection. Because this can be called repeatedly while
// typing, we've careful not to freeze the edit unless we really need to.
// Also, unlike in the temporary text case above, here we don't want to update
// the caret/selection unless we have to, since this might make the user's
// caret position change without warning during typing.
if (display_text == GetText())
return false;
ScopedFreeze freeze(this, GetTextObjectModel());
SetWindowText(display_text.c_str());
// Set a reversed selection to keep the caret in the same position, which
// avoids scrolling the user's text.
SetSelection(static_cast<LONG>(display_text.length()),
static_cast<LONG>(user_text_length));
TextChanged();
return true;
}
void AutocompleteEditViewWin::OnRevertTemporaryText() {
SetSelectionRange(original_selection_);
TextChanged();
}
void AutocompleteEditViewWin::OnBeforePossibleChange() {
// Record our state.
text_before_change_ = GetText();
GetSelection(sel_before_change_);
}
bool AutocompleteEditViewWin::OnAfterPossibleChange() {
return OnAfterPossibleChangeInternal(false);
}
bool AutocompleteEditViewWin::OnAfterPossibleChangeInternal(
bool force_text_changed) {
// Prevent the user from selecting the "phantom newline" at the end of the
// edit. If they try, we just silently move the end of the selection back to
// the end of the real text.
CHARRANGE new_sel;
GetSelection(new_sel);
const int length = GetTextLength();
if ((new_sel.cpMin > length) || (new_sel.cpMax > length)) {
if (new_sel.cpMin > length)
new_sel.cpMin = length;
if (new_sel.cpMax > length)
new_sel.cpMax = length;
SetSelectionRange(new_sel);
}
const bool selection_differs =
((new_sel.cpMin != new_sel.cpMax) ||
(sel_before_change_.cpMin != sel_before_change_.cpMax)) &&
((new_sel.cpMin != sel_before_change_.cpMin) ||
(new_sel.cpMax != sel_before_change_.cpMax));
// See if the text or selection have changed since OnBeforePossibleChange().
const string16 new_text(GetText());
const bool text_differs = (new_text != text_before_change_) ||
force_text_changed;
// When the user has deleted text, we don't allow inline autocomplete. Make
// sure to not flag cases like selecting part of the text and then pasting
// (or typing) the prefix of that selection. (We detect these by making
// sure the caret, which should be after any insertion, hasn't moved
// forward of the old selection start.)
const bool just_deleted_text =
(text_before_change_.length() > new_text.length()) &&
(new_sel.cpMin <= std::min(sel_before_change_.cpMin,
sel_before_change_.cpMax));
const bool something_changed = model_->OnAfterPossibleChange(
new_text, new_sel.cpMin, new_sel.cpMax, selection_differs,
text_differs, just_deleted_text, !IsImeComposing());
if (selection_differs)
controller_->OnSelectionBoundsChanged();
if (something_changed && text_differs)
TextChanged();
if (text_differs) {
// Note that a TEXT_CHANGED event implies that the cursor/selection
// probably changed too, so we don't need to send both.
parent_view_->GetWidget()->NotifyAccessibilityEvent(
parent_view_, ui::AccessibilityTypes::EVENT_TEXT_CHANGED, true);
} else if (selection_differs) {
// Notify assistive technology that the cursor or selection changed.
parent_view_->GetWidget()->NotifyAccessibilityEvent(
parent_view_, ui::AccessibilityTypes::EVENT_SELECTION_CHANGED, true);
} else if (delete_at_end_pressed_) {
model_->OnChanged();
}
return something_changed;
}
gfx::NativeView AutocompleteEditViewWin::GetNativeView() const {
return m_hWnd;
}
CommandUpdater* AutocompleteEditViewWin::GetCommandUpdater() {
return command_updater_;
}
void AutocompleteEditViewWin::SetInstantSuggestion(const string16& suggestion,
bool animate_to_complete) {
parent_view_->SetInstantSuggestion(suggestion, animate_to_complete);
}
int AutocompleteEditViewWin::TextWidth() const {
return WidthNeededToDisplay(GetText());
}
string16 AutocompleteEditViewWin::GetInstantSuggestion() const {
return parent_view_->GetInstantSuggestion();
}
bool AutocompleteEditViewWin::IsImeComposing() const {
bool ime_composing = false;
HIMC context = ImmGetContext(m_hWnd);
if (context) {
ime_composing = !!ImmGetCompositionString(context, GCS_COMPSTR, NULL, 0);
ImmReleaseContext(m_hWnd, context);
}
return ime_composing;
}
views::View* AutocompleteEditViewWin::AddToView(views::View* parent) {
views::NativeViewHost* host = new views::NativeViewHost;
parent->AddChildView(host);
host->set_focus_view(parent);
host->Attach(GetNativeView());
return host;
}
int AutocompleteEditViewWin::OnPerformDrop(
const views::DropTargetEvent& event) {
return OnPerformDropImpl(event, false);
}
int AutocompleteEditViewWin::OnPerformDropImpl(
const views::DropTargetEvent& event,
bool in_drag) {
const ui::OSExchangeData& data = event.data();
if (data.HasURL()) {
GURL url;
string16 title;
if (data.GetURLAndTitle(&url, &title)) {
SetUserText(UTF8ToWide(url.spec()));
model()->AcceptInput(CURRENT_TAB, true);
return CopyOrLinkDragOperation(event.source_operations());
}
} else if (data.HasString()) {
int string_drop_position = drop_highlight_position();
string16 text;
if ((string_drop_position != -1 || !in_drag) && data.GetString(&text)) {
DCHECK(string_drop_position == -1 ||
((string_drop_position >= 0) &&
(string_drop_position <= GetTextLength())));
if (in_drag) {
if (event.source_operations()== ui::DragDropTypes::DRAG_MOVE)
MoveSelectedText(string_drop_position);
else
InsertText(string_drop_position, text);
} else {
PasteAndGo(CollapseWhitespace(text, true));
}
return CopyOrLinkDragOperation(event.source_operations());
}
}
return ui::DragDropTypes::DRAG_NONE;
}
void AutocompleteEditViewWin::PasteAndGo(const string16& text) {
if (CanPasteAndGo(text))
model_->PasteAndGo();
}
bool AutocompleteEditViewWin::SkipDefaultKeyEventProcessing(
const views::KeyEvent& event) {
ui::KeyboardCode key = event.key_code();
// We don't process ALT + numpad digit as accelerators, they are used for
// entering special characters. We do translate alt-home.
if (event.IsAltDown() && (key != ui::VKEY_HOME) &&
views::NativeTextfieldWin::IsNumPadDigit(key,
views::IsExtendedKey(event)))
return true;
// Skip accelerators for key combinations omnibox wants to crack. This list
// should be synced with OnKeyDownOnlyWritable() (but for tab which is dealt
// with above in LocationBarView::SkipDefaultKeyEventProcessing).
//
// We cannot return true for all keys because we still need to handle some
// accelerators (e.g., F5 for reload the page should work even when the
// Omnibox gets focused).
switch (key) {
case ui::VKEY_ESCAPE: {
ScopedFreeze freeze(this, GetTextObjectModel());
return model_->OnEscapeKeyPressed();
}
case ui::VKEY_RETURN:
return true;
case ui::VKEY_UP:
case ui::VKEY_DOWN:
return !event.IsAltDown();
case ui::VKEY_DELETE:
case ui::VKEY_INSERT:
return !event.IsAltDown() && event.IsShiftDown() &&
!event.IsControlDown();
case ui::VKEY_X:
case ui::VKEY_V:
return !event.IsAltDown() && event.IsControlDown();
case ui::VKEY_BACK:
case ui::VKEY_OEM_PLUS:
return true;
default:
return false;
}
}
void AutocompleteEditViewWin::HandleExternalMsg(UINT msg,
UINT flags,
const CPoint& screen_point) {
if (msg == WM_CAPTURECHANGED) {
SendMessage(msg, 0, NULL);
return;
}
CPoint client_point(screen_point);
::MapWindowPoints(NULL, m_hWnd, &client_point, 1);
SendMessage(msg, flags, MAKELPARAM(client_point.x, client_point.y));
}
bool AutocompleteEditViewWin::IsCommandIdChecked(int command_id) const {
return false;
}
bool AutocompleteEditViewWin::IsCommandIdEnabled(int command_id) const {
switch (command_id) {
case IDS_UNDO: return !!CanUndo();
case IDC_CUT: return !!CanCut();
case IDC_COPY: return !!CanCopy();
case IDC_PASTE: return !!CanPaste();
case IDS_PASTE_AND_GO: return CanPasteAndGo(GetClipboardText());
case IDS_SELECT_ALL: return !!CanSelectAll();
case IDS_EDIT_SEARCH_ENGINES:
return command_updater_->IsCommandEnabled(IDC_EDIT_SEARCH_ENGINES);
default:
NOTREACHED();
return false;
}
}
bool AutocompleteEditViewWin::GetAcceleratorForCommandId(
int command_id,
ui::Accelerator* accelerator) {
return parent_view_->GetWidget()->GetAccelerator(command_id, accelerator);
}
bool AutocompleteEditViewWin::IsItemForCommandIdDynamic(int command_id) const {
// No need to change the default IDS_PASTE_AND_GO label unless this is a
// search.
return command_id == IDS_PASTE_AND_GO;
}
string16 AutocompleteEditViewWin::GetLabelForCommandId(
int command_id) const {
DCHECK_EQ(IDS_PASTE_AND_GO, command_id);
return l10n_util::GetStringUTF16(model_->is_paste_and_search() ?
IDS_PASTE_AND_SEARCH : IDS_PASTE_AND_GO);
}
void AutocompleteEditViewWin::ExecuteCommand(int command_id) {
ScopedFreeze freeze(this, GetTextObjectModel());
if (command_id == IDS_PASTE_AND_GO) {
// This case is separate from the switch() below since we don't want to wrap
// it in OnBefore/AfterPossibleChange() calls.
model_->PasteAndGo();
return;
}
OnBeforePossibleChange();
switch (command_id) {
case IDS_UNDO:
Undo();
break;
case IDC_CUT:
Cut();
break;
case IDC_COPY:
Copy();
break;
case IDC_PASTE:
Paste();
break;
case IDS_SELECT_ALL:
SelectAll(false);
break;
case IDS_EDIT_SEARCH_ENGINES:
command_updater_->ExecuteCommand(IDC_EDIT_SEARCH_ENGINES);
break;
default:
NOTREACHED();
break;
}
OnAfterPossibleChange();
}
// static
int CALLBACK AutocompleteEditViewWin::WordBreakProc(LPTSTR edit_text,
int current_pos,
int num_bytes,
int action) {
// TODO(pkasting): http://b/1111308 We should let other people, like ICU and
// GURL, do the work for us here instead of writing all this ourselves.
// Sadly, even though the MSDN docs claim that the third parameter here is a
// number of characters, they lie. It's a number of bytes.
const int length = num_bytes / sizeof(wchar_t);
// With no clear guidance from the MSDN docs on how to handle "not found" in
// the "find the nearest xxx..." cases below, I cap the return values at
// [0, length]. Since one of these (0) is also a valid position, the return
// values are thus ambiguous :(
switch (action) {
// Find nearest character before current position that begins a word.
case WB_LEFT:
case WB_MOVEWORDLEFT: {
if (current_pos < 2) {
// Either current_pos == 0, so we have a "not found" case and return 0,
// or current_pos == 1, and the only character before this position is
// at 0.
return 0;
}
// Look for a delimiter before the previous character; the previous word
// starts immediately after. (If we looked for a delimiter before the
// current character, we could stop on the immediate prior character,
// which would mean we'd return current_pos -- which isn't "before the
// current position".)
const int prev_delim =
WordBreakProc(edit_text, current_pos - 1, num_bytes, WB_LEFTBREAK);
if ((prev_delim == 0) &&
!WordBreakProc(edit_text, 0, num_bytes, WB_ISDELIMITER)) {
// Got back 0, but position 0 isn't a delimiter. This was a "not
// found" 0, so return one of our own.
return 0;
}
return prev_delim + 1;
}
// Find nearest character after current position that begins a word.
case WB_RIGHT:
case WB_MOVEWORDRIGHT: {
if (WordBreakProc(edit_text, current_pos, num_bytes, WB_ISDELIMITER)) {
// The current character is a delimiter, so the next character starts
// a new word. Done.
return current_pos + 1;
}
// Look for a delimiter after the current character; the next word starts
// immediately after.
const int next_delim =
WordBreakProc(edit_text, current_pos, num_bytes, WB_RIGHTBREAK);
if (next_delim == length) {
// Didn't find a delimiter. Return length to signal "not found".
return length;
}
return next_delim + 1;
}
// Determine if the current character delimits words.
case WB_ISDELIMITER:
return !!(WordBreakProc(edit_text, current_pos, num_bytes, WB_CLASSIFY) &
WBF_BREAKLINE);
// Return the classification of the current character.
case WB_CLASSIFY:
if (IsWhitespace(edit_text[current_pos])) {
// Whitespace normally breaks words, but the MSDN docs say that we must
// not break on the CRs in a "CR, LF" or a "CR, CR, LF" sequence. Just
// check for an arbitrarily long sequence of CRs followed by LF and
// report "not a delimiter" for the current CR in that case.
while ((current_pos < (length - 1)) &&
(edit_text[current_pos] == 0x13)) {
if (edit_text[++current_pos] == 0x10)
return WBF_ISWHITE;
}
return WBF_BREAKLINE | WBF_ISWHITE;
}
// Punctuation normally breaks words, but the first two characters in
// "://" (end of scheme) should not be breaks, so that "http://" will be
// treated as one word.
if (ispunct(edit_text[current_pos], std::locale()) &&
!SchemeEnd(edit_text, current_pos, length) &&
!SchemeEnd(edit_text, current_pos - 1, length))
return WBF_BREAKLINE;
// Normal character, no flags.
return 0;
// Finds nearest delimiter before current position.
case WB_LEFTBREAK:
for (int i = current_pos - 1; i >= 0; --i) {
if (WordBreakProc(edit_text, i, num_bytes, WB_ISDELIMITER))
return i;
}
return 0;
// Finds nearest delimiter after current position.
case WB_RIGHTBREAK:
for (int i = current_pos + 1; i < length; ++i) {
if (WordBreakProc(edit_text, i, num_bytes, WB_ISDELIMITER))
return i;
}
return length;
}
NOTREACHED();
return 0;
}
// static
bool AutocompleteEditViewWin::SchemeEnd(LPTSTR edit_text,
int current_pos,
int length) {
return (current_pos >= 0) &&
((length - current_pos) > 2) &&
(edit_text[current_pos] == ':') &&
(edit_text[current_pos + 1] == '/') &&
(edit_text[current_pos + 2] == '/');
}
void AutocompleteEditViewWin::OnChar(TCHAR ch, UINT repeat_count, UINT flags) {
// Don't let alt-enter beep. Not sure this is necessary, as the standard
// alt-enter will hit DiscardWMSysChar() and get thrown away, and
// ctrl-alt-enter doesn't seem to reach here for some reason? At least not on
// my system... still, this is harmless and maybe necessary in other locales.
if (ch == VK_RETURN && (flags & KF_ALTDOWN))
return;
// Escape is processed in OnKeyDown. Don't let any WM_CHAR messages propagate
// as we don't want the RichEdit to do anything funky.
if (ch == VK_ESCAPE && !(flags & KF_ALTDOWN))
return;
if (ch == VK_TAB) {
// Don't add tabs to the input.
return;
}
HandleKeystroke(GetCurrentMessage()->message, ch, repeat_count, flags);
}
void AutocompleteEditViewWin::OnContextMenu(HWND window, const CPoint& point) {
BuildContextMenu();
if (point.x == -1 || point.y == -1) {
POINT p;
GetCaretPos(&p);
MapWindowPoints(HWND_DESKTOP, &p, 1);
context_menu_->RunContextMenuAt(gfx::Point(p));
} else {
context_menu_->RunContextMenuAt(gfx::Point(point));
}
}
void AutocompleteEditViewWin::OnCopy() {
string16 text(GetSelectedText());
if (text.empty())
return;
CHARRANGE sel;
GURL url;
bool write_url = false;
GetSel(sel);
// GetSel() doesn't preserve selection direction, so sel.cpMin will always be
// the smaller value.
model_->AdjustTextForCopy(sel.cpMin, IsSelectAll(), &text, &url, &write_url);
ui::ScopedClipboardWriter scw(g_browser_process->clipboard());
scw.WriteText(text);
if (write_url) {
scw.WriteBookmark(text, url.spec());
scw.WriteHyperlink(EscapeForHTML(text), url.spec());
}
}
void AutocompleteEditViewWin::OnCut() {
OnCopy();
// This replace selection will have no effect (even on the undo stack) if the
// current selection is empty.
ReplaceSel(L"", true);
}
LRESULT AutocompleteEditViewWin::OnGetObject(UINT uMsg,
WPARAM wparam,
LPARAM lparam) {
// Accessibility readers will send an OBJID_CLIENT message.
if (lparam == OBJID_CLIENT) {
// Re-attach for internal re-usage of accessibility pointer.
autocomplete_accessibility_.Attach(GetIAccessible());
if (autocomplete_accessibility_) {
return LresultFromObject(IID_IAccessible, wparam,
autocomplete_accessibility_);
}
}
return 0;
}
LRESULT AutocompleteEditViewWin::OnImeComposition(UINT message,
WPARAM wparam,
LPARAM lparam) {
if (ignore_ime_messages_) {
// This message was sent while we're in the middle of meddling with the
// underlying edit control. If we handle it below, OnAfterPossibleChange()
// can get bogus text for the edit, and rerun autocomplete, destructively
// modifying the result set that we're in the midst of using. For example,
// if SetWindowTextAndCaretPos() was called due to the user clicking an
// entry in the popup, we're in the middle of executing SetSelectedLine(),
// and changing the results can cause checkfailures.
return DefWindowProc(message, wparam, lparam);
}
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
LRESULT result = DefWindowProc(message, wparam, lparam);
// Force an IME composition confirmation operation to trigger the text_changed
// code in OnAfterPossibleChange(), even if identical contents are confirmed,
// to make sure the model can update its internal states correctly.
OnAfterPossibleChangeInternal((lparam & GCS_RESULTSTR) != 0);
return result;
}
void AutocompleteEditViewWin::OnKeyDown(TCHAR key,
UINT repeat_count,
UINT flags) {
delete_at_end_pressed_ = false;
if (OnKeyDownAllModes(key, repeat_count, flags))
return;
// Make sure that we handle system key events like Alt-F4.
if (popup_window_mode_) {
DefWindowProc(GetCurrentMessage()->message, key, MAKELPARAM(repeat_count,
flags));
return;
}
if (OnKeyDownOnlyWritable(key, repeat_count, flags))
return;
// CRichEditCtrl changes its text on WM_KEYDOWN instead of WM_CHAR for many
// different keys (backspace, ctrl-v, ...), so we call this in both cases.
HandleKeystroke(GetCurrentMessage()->message, key, repeat_count, flags);
}
void AutocompleteEditViewWin::OnKeyUp(TCHAR key,
UINT repeat_count,
UINT flags) {
if (key == VK_CONTROL)
model_->OnControlKeyChanged(false);
// On systems with RTL input languages, ctrl+shift toggles the reading order
// (depending on which shift key is pressed). But by default the CRichEditCtrl
// only changes the current reading order, and as soon as the user deletes all
// the text, or we call SetWindowText(), it reverts to the "default" order.
// To work around this, if the user hits ctrl+shift, we pass it to
// DefWindowProc() while the edit is empty, which toggles the default reading
// order; then we restore the user's input.
if (!(flags & KF_ALTDOWN) &&
(((key == VK_CONTROL) && (GetKeyState(VK_SHIFT) < 0)) ||
((key == VK_SHIFT) && (GetKeyState(VK_CONTROL) < 0)))) {
ScopedFreeze freeze(this, GetTextObjectModel());
string16 saved_text(GetText());
CHARRANGE saved_sel;
GetSelection(saved_sel);
SetWindowText(L"");
DefWindowProc(WM_KEYUP, key, MAKELPARAM(repeat_count, flags));
SetWindowText(saved_text.c_str());
SetSelectionRange(saved_sel);
return;
}
SetMsgHandled(false);
}
void AutocompleteEditViewWin::OnKillFocus(HWND focus_wnd) {
if (m_hWnd == focus_wnd) {
// Focus isn't actually leaving.
SetMsgHandled(false);
return;
}
// This must be invoked before ClosePopup.
model_->OnWillKillFocus(focus_wnd);
// Close the popup.
ClosePopup();
// Save the user's existing selection to restore it later.
GetSelection(saved_selection_for_focus_change_);
// Tell the model to reset itself.
model_->OnKillFocus();
// Let the CRichEditCtrl do its default handling. This will complete any
// in-progress IME composition. We must do this after setting has_focus_ to
// false so that UpdatePopup() will know not to rerun autocomplete.
ScopedFreeze freeze(this, GetTextObjectModel());
DefWindowProc(WM_KILLFOCUS, reinterpret_cast<WPARAM>(focus_wnd), 0);
// Cancel any user selection and scroll the text back to the beginning of the
// URL. We have to do this after calling DefWindowProc() because otherwise
// an in-progress IME composition will be completed at the new caret position,
// resulting in the string jumping unexpectedly to the front of the edit.
PlaceCaretAt(0);
}
void AutocompleteEditViewWin::OnLButtonDblClk(UINT keys, const CPoint& point) {
// Save the double click info for later triple-click detection.
tracking_double_click_ = true;
double_click_point_ = point;
double_click_time_ = GetCurrentMessage()->time;
possible_drag_ = false;
// Modifying the selection counts as accepting any inline autocompletion, so
// track "changes" made by clicking the mouse button.
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
DefWindowProc(WM_LBUTTONDBLCLK, keys,
MAKELPARAM(ClipXCoordToVisibleText(point.x, false), point.y));
OnAfterPossibleChange();
gaining_focus_.reset(); // See NOTE in OnMouseActivate().
}
void AutocompleteEditViewWin::OnLButtonDown(UINT keys, const CPoint& point) {
TrackMousePosition(kLeft, point);
if (gaining_focus_.get()) {
// When Chrome was already the activated app, we haven't reached
// OnSetFocus() yet. When we get there, don't restore the saved selection,
// since it will just screw up the user's interaction with the edit.
saved_selection_for_focus_change_.cpMin = -1;
// Crazy hack: In this particular case, the CRichEditCtrl seems to have an
// internal flag that discards the next WM_LBUTTONDOWN without processing
// it, so that clicks on the edit when its owning app is not activated are
// eaten rather than processed (despite whatever the return value of
// DefWindowProc(WM_MOUSEACTIVATE, ...) may say). This behavior is
// confusing and we want the click to be treated normally. So, to reset the
// CRichEditCtrl's internal flag, we pass it an extra WM_LBUTTONDOWN here
// (as well as a matching WM_LBUTTONUP, just in case we'd be confusing some
// kind of state tracking otherwise).
DefWindowProc(WM_LBUTTONDOWN, keys, MAKELPARAM(point.x, point.y));
DefWindowProc(WM_LBUTTONUP, keys, MAKELPARAM(point.x, point.y));
}
// Check for triple click, then reset tracker. Should be safe to subtract
// double_click_time_ from the current message's time even if the timer has
// wrapped in between.
const bool is_triple_click = tracking_double_click_ &&
views::NativeTextfieldWin::IsDoubleClick(double_click_point_, point,
GetCurrentMessage()->time - double_click_time_);
tracking_double_click_ = false;
if (!gaining_focus_.get() && !is_triple_click)
OnPossibleDrag(point);
// Modifying the selection counts as accepting any inline autocompletion, so
// track "changes" made by clicking the mouse button.
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
DefWindowProc(WM_LBUTTONDOWN, keys,
MAKELPARAM(ClipXCoordToVisibleText(point.x, is_triple_click),
point.y));
OnAfterPossibleChange();
gaining_focus_.reset();
}
void AutocompleteEditViewWin::OnLButtonUp(UINT keys, const CPoint& point) {
// default processing should happen first so we can see the result of the
// selection
ScopedFreeze freeze(this, GetTextObjectModel());
DefWindowProc(WM_LBUTTONUP, keys,
MAKELPARAM(ClipXCoordToVisibleText(point.x, false), point.y));
SelectAllIfNecessary(kLeft, point);
tracking_click_[kLeft] = false;
possible_drag_ = false;
}
void AutocompleteEditViewWin::OnMButtonDblClk(UINT /*keys*/,
const CPoint& /*point*/) {
gaining_focus_.reset(); // See NOTE in OnMouseActivate().
// By default, the edit responds to middle-clicks by capturing the mouse and
// ignoring all subsequent events until it receives another click (of any of
// the left, middle, or right buttons). This bizarre behavior is not only
// useless but can cause the UI to appear unresponsive if a user accidentally
// middle-clicks the edit (instead of a tab above it), so we purposefully eat
// this message (instead of calling SetMsgHandled(false)) to avoid triggering
// this.
}
void AutocompleteEditViewWin::OnMButtonDown(UINT /*keys*/,
const CPoint& /*point*/) {
tracking_double_click_ = false;
// See note in OnMButtonDblClk above.
}
void AutocompleteEditViewWin::OnMButtonUp(UINT /*keys*/,
const CPoint& /*point*/) {
possible_drag_ = false;
// See note in OnMButtonDblClk above.
}
LRESULT AutocompleteEditViewWin::OnMouseActivate(HWND window,
UINT hit_test,
UINT mouse_message) {
// First, give other handlers a chance to handle the message to see if we are
// actually going to activate and gain focus.
LRESULT result = DefWindowProc(WM_MOUSEACTIVATE,
reinterpret_cast<WPARAM>(window),
MAKELPARAM(hit_test, mouse_message));
// Check if we're getting focus from a click. We have to do this here rather
// than in OnXButtonDown() since in many scenarios OnSetFocus() will be
// reached before OnXButtonDown(), preventing us from detecting this properly
// there. Also in those cases, we need to already know in OnSetFocus() that
// we should not restore the saved selection.
if (!model_->has_focus() &&
((mouse_message == WM_LBUTTONDOWN || mouse_message == WM_RBUTTONDOWN)) &&
(result == MA_ACTIVATE)) {
DCHECK(!gaining_focus_.get());
gaining_focus_.reset(new ScopedFreeze(this, GetTextObjectModel()));
// NOTE: Despite |mouse_message| being WM_XBUTTONDOWN here, we're not
// guaranteed to call OnXButtonDown() later! Specifically, if this is the
// second click of a double click, we'll reach here but later call
// OnXButtonDblClk(). Make sure |gaining_focus_| gets reset both places,
// or we'll have visual glitchiness and then DCHECK failures.
// Don't restore saved selection, it will just screw up our interaction
// with this edit.
saved_selection_for_focus_change_.cpMin = -1;
}
return result;
}
void AutocompleteEditViewWin::OnMouseMove(UINT keys, const CPoint& point) {
if (possible_drag_) {
StartDragIfNecessary(point);
// Don't fall through to default mouse handling, otherwise a second
// drag session may start.
return;
}
if (tracking_click_[kLeft] && !IsDrag(click_point_[kLeft], point))
return;
tracking_click_[kLeft] = false;
// Return quickly if this can't change the selection/cursor, so we don't
// create a ScopedFreeze (and thus do an UpdateWindow()) on every
// WM_MOUSEMOVE.
if (!(keys & MK_LBUTTON)) {
DefWindowProc(WM_MOUSEMOVE, keys, MAKELPARAM(point.x, point.y));
return;
}
// Clamp the selection to the visible text so the user can't drag to select
// the "phantom newline". In theory we could achieve this by clipping the X
// coordinate, but in practice the edit seems to behave nondeterministically
// with similar sequences of clipped input coordinates fed to it. Maybe it's
// reading the mouse cursor position directly?
//
// This solution has a minor visual flaw, however: if there's a visible cursor
// at the edge of the text (only true when there's no selection), dragging the
// mouse around outside that edge repaints the cursor on every WM_MOUSEMOVE
// instead of allowing it to blink normally. To fix this, we special-case
// this exact case and discard the WM_MOUSEMOVE messages instead of passing
// them along.
//
// But even this solution has a flaw! (Argh.) In the case where the user has
// a selection that starts at the edge of the edit, and proceeds to the middle
// of the edit, and the user is dragging back past the start edge to remove
// the selection, there's a redraw problem where the change between having the
// last few bits of text still selected and having nothing selected can be
// slow to repaint (which feels noticeably strange). This occurs if you only
// let the edit receive a single WM_MOUSEMOVE past the edge of the text. I
// think on each WM_MOUSEMOVE the edit is repainting its previous state, then
// updating its internal variables to the new state but not repainting. To
// fix this, we allow one more WM_MOUSEMOVE through after the selection has
// supposedly been shrunk to nothing; this makes the edit redraw the selection
// quickly so it feels smooth.
CHARRANGE selection;
GetSel(selection);
const bool possibly_can_discard_mousemove =
(selection.cpMin == selection.cpMax) &&
(((selection.cpMin == 0) &&
(ClipXCoordToVisibleText(point.x, false) > point.x)) ||
((selection.cpMin == GetTextLength()) &&
(ClipXCoordToVisibleText(point.x, false) < point.x)));
if (!can_discard_mousemove_ || !possibly_can_discard_mousemove) {
can_discard_mousemove_ = possibly_can_discard_mousemove;
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
// Force the Y coordinate to the center of the clip rect. The edit
// behaves strangely when the cursor is dragged vertically: if the cursor
// is in the middle of the text, drags inside the clip rect do nothing,
// and drags outside the clip rect act as if the cursor jumped to the
// left edge of the text. When the cursor is at the right edge, drags of
// just a few pixels vertically end up selecting the "phantom newline"...
// sometimes.
RECT r;
GetRect(&r);
DefWindowProc(WM_MOUSEMOVE, keys,
MAKELPARAM(point.x, (r.bottom - r.top) / 2));
OnAfterPossibleChange();
}
}
void AutocompleteEditViewWin::OnPaint(HDC bogus_hdc) {
// We need to paint over the top of the edit. If we simply let the edit do
// its default painting, then do ours into the window DC, the screen is
// updated in between and we can get flicker. To avoid this, we force the
// edit to paint into a memory DC, which we also paint onto, then blit the
// whole thing to the screen.
// Don't paint if not necessary.
CRect paint_clip_rect;
if (!GetUpdateRect(&paint_clip_rect, true))
return;
// Begin painting, and create a memory DC for the edit to paint into.
CPaintDC paint_dc(m_hWnd);
CDC memory_dc(CreateCompatibleDC(paint_dc));
CRect rect;
GetClientRect(&rect);
// NOTE: This next call uses |paint_dc| instead of |memory_dc| because
// |memory_dc| contains a 1x1 monochrome bitmap by default, which will cause
// |memory_bitmap| to be monochrome, which isn't what we want.
CBitmap memory_bitmap(CreateCompatibleBitmap(paint_dc, rect.Width(),
rect.Height()));
HBITMAP old_bitmap = memory_dc.SelectBitmap(memory_bitmap);
// Tell our intercept functions to supply our memory DC to the edit when it
// tries to call BeginPaint().
//
// The sane way to do this would be to use WM_PRINTCLIENT to ask the edit to
// paint into our desired DC. Unfortunately, the Rich Edit 3.0 that ships
// with Windows 2000/XP/Vista doesn't handle WM_PRINTCLIENT correctly; it
// treats it just like WM_PAINT and calls BeginPaint(), ignoring our provided
// DC. The Rich Edit 6.0 that ships with Office 2007 handles this better, but
// has other issues, and we can't redistribute that DLL anyway. So instead,
// we use this scary hack.
//
// NOTE: It's possible to get nested paint calls (!) (try setting the
// permanent URL to something longer than the edit width, then selecting the
// contents of the edit, typing a character, and hitting <esc>), so we can't
// DCHECK(!edit_hwnd_) here. Instead, just save off the old HWND, which most
// of the time will be NULL.
HWND old_edit_hwnd = edit_hwnd;
edit_hwnd = m_hWnd;
paint_struct = paint_dc.m_ps;
paint_struct.hdc = memory_dc;
DefWindowProc(WM_PAINT, reinterpret_cast<WPARAM>(bogus_hdc), 0);
// Make the selection look better.
EraseTopOfSelection(&memory_dc, rect, paint_clip_rect);
// Draw a slash through the scheme if this is insecure.
if (insecure_scheme_component_.is_nonempty())
DrawSlashForInsecureScheme(memory_dc, rect, paint_clip_rect);
// Draw the drop highlight.
if (drop_highlight_position_ != -1)
DrawDropHighlight(memory_dc, rect, paint_clip_rect);
// Blit the memory DC to the actual paint DC and clean up.
BitBlt(paint_dc, rect.left, rect.top, rect.Width(), rect.Height(), memory_dc,
rect.left, rect.top, SRCCOPY);
memory_dc.SelectBitmap(old_bitmap);
edit_hwnd = old_edit_hwnd;
}
void AutocompleteEditViewWin::OnPaste() {
// Replace the selection if we have something to paste.
const string16 text(GetClipboardText());
if (!text.empty()) {
// Record this paste, so we can do different behavior.
model_->on_paste();
// Force a Paste operation to trigger the text_changed code in
// OnAfterPossibleChange(), even if identical contents are pasted into the
// text box.
text_before_change_.clear();
ReplaceSel(text.c_str(), true);
}
}
void AutocompleteEditViewWin::OnRButtonDblClk(UINT /*keys*/,
const CPoint& /*point*/) {
gaining_focus_.reset(); // See NOTE in OnMouseActivate().
SetMsgHandled(false);
}
void AutocompleteEditViewWin::OnRButtonDown(UINT /*keys*/,
const CPoint& point) {
TrackMousePosition(kRight, point);
tracking_double_click_ = false;
possible_drag_ = false;
gaining_focus_.reset();
SetMsgHandled(false);
}
void AutocompleteEditViewWin::OnRButtonUp(UINT /*keys*/, const CPoint& point) {
SelectAllIfNecessary(kRight, point);
tracking_click_[kRight] = false;
SetMsgHandled(false);
}
void AutocompleteEditViewWin::OnSetFocus(HWND focus_wnd) {
views::FocusManager* focus_manager = parent_view_->GetFocusManager();
if (focus_manager) {
// Notify the FocusManager that the focused view is now the location bar
// (our parent view).
focus_manager->SetFocusedView(parent_view_);
} else {
NOTREACHED();
}
model_->OnSetFocus(GetKeyState(VK_CONTROL) < 0);
// Restore saved selection if available.
if (saved_selection_for_focus_change_.cpMin != -1) {
SetSelectionRange(saved_selection_for_focus_change_);
saved_selection_for_focus_change_.cpMin = -1;
}
SetMsgHandled(false);
}
LRESULT AutocompleteEditViewWin::OnSetText(const wchar_t* text) {
// Ignore all IME messages while we process this WM_SETTEXT message.
// When SetWindowText() is called while an IME is composing text, the IME
// calls SendMessage() to send a WM_IME_COMPOSITION message. When we receive
// this WM_IME_COMPOSITION message, we update the omnibox and may call
// SetWindowText() again. To stop this recursive message-handler call, we
// stop updating the omnibox while we process a WM_SETTEXT message.
// We wouldn't need to do this update anyway, because either we're in the
// middle of updating the omnibox already or the caller of SetWindowText()
// is going to update the omnibox next.
AutoReset<bool> auto_reset_ignore_ime_messages(&ignore_ime_messages_, true);
return DefWindowProc(WM_SETTEXT, 0, reinterpret_cast<LPARAM>(text));
}
void AutocompleteEditViewWin::OnSysChar(TCHAR ch,
UINT repeat_count,
UINT flags) {
// Nearly all alt-<xxx> combos result in beeping rather than doing something
// useful, so we discard most. Exceptions:
// * ctrl-alt-<xxx>, which is sometimes important, generates WM_CHAR instead
// of WM_SYSCHAR, so it doesn't need to be handled here.
// * alt-space gets translated by the default WM_SYSCHAR handler to a
// WM_SYSCOMMAND to open the application context menu, so we need to allow
// it through.
if (ch == VK_SPACE)
SetMsgHandled(false);
}
void AutocompleteEditViewWin::OnWindowPosChanging(WINDOWPOS* window_pos) {
if (force_hidden_)
window_pos->flags &= ~SWP_SHOWWINDOW;
SetMsgHandled(true);
}
BOOL AutocompleteEditViewWin::OnMouseWheel(UINT flags,
short delta,
CPoint point) {
// Forward the mouse-wheel message to the window under the mouse.
if (!views::RerouteMouseWheel(m_hWnd, MAKEWPARAM(flags, delta),
MAKELPARAM(point.x, point.y)))
SetMsgHandled(false);
return 0;
}
void AutocompleteEditViewWin::HandleKeystroke(UINT message,
TCHAR key,
UINT repeat_count,
UINT flags) {
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
if (key == ui::VKEY_HOME || key == ui::VKEY_END) {
// DefWindowProc() might reset the keyboard layout when it receives a
// keydown event for VKEY_HOME or VKEY_END. When the window was created
// with WS_EX_LAYOUTRTL and the current keyboard layout is not a RTL one,
// if the input text is pure LTR text, the layout changes to the first RTL
// keyboard layout in keyboard layout queue; if the input text is
// bidirectional text, the layout changes to the keyboard layout of the
// first RTL character in input text. When the window was created without
// WS_EX_LAYOUTRTL and the current keyboard layout is not a LTR one, if the
// input text is pure RTL text, the layout changes to English; if the input
// text is bidirectional text, the layout changes to the keyboard layout of
// the first LTR character in input text. Such keyboard layout change
// behavior is surprising and inconsistent with keyboard behavior
// elsewhere, so reset the layout in this case.
HKL layout = GetKeyboardLayout(0);
DefWindowProc(message, key, MAKELPARAM(repeat_count, flags));
ActivateKeyboardLayout(layout, KLF_REORDER);
} else {
DefWindowProc(message, key, MAKELPARAM(repeat_count, flags));
}
// CRichEditCtrl automatically turns on IMF_AUTOKEYBOARD when the user
// inputs an RTL character, making it difficult for the user to control
// what language is set as they type. Force this off to make the edit's
// behavior more stable.
const int lang_options = SendMessage(EM_GETLANGOPTIONS, 0, 0);
if (lang_options & IMF_AUTOKEYBOARD)
SendMessage(EM_SETLANGOPTIONS, 0, lang_options & ~IMF_AUTOKEYBOARD);
OnAfterPossibleChange();
}
bool AutocompleteEditViewWin::OnKeyDownOnlyWritable(TCHAR key,
UINT repeat_count,
UINT flags) {
// NOTE: Annoyingly, ctrl-alt-<key> generates WM_KEYDOWN rather than
// WM_SYSKEYDOWN, so we need to check (flags & KF_ALTDOWN) in various places
// in this function even with a WM_SYSKEYDOWN handler.
// If adding a new key that could possibly be an accelerator then you'll need
// to update LocationBarView::SkipDefaultKeyEventProcessing() as well.
int count = repeat_count;
switch (key) {
case VK_RIGHT:
// TODO(sky): figure out RTL.
if (base::i18n::IsRTL())
return false;
{
CHARRANGE selection;
GetSel(selection);
return (selection.cpMin == selection.cpMax) &&
(selection.cpMin == GetTextLength()) &&
model_->CommitSuggestedText(true);
}
case VK_RETURN:
model_->AcceptInput((flags & KF_ALTDOWN) ?
NEW_FOREGROUND_TAB : CURRENT_TAB, false);
return true;
case VK_PRIOR:
case VK_NEXT:
count = model_->result().size();
// FALL THROUGH
case VK_UP:
case VK_DOWN:
// Ignore alt + numpad, but treat alt + (non-numpad) like (non-numpad).
if ((flags & KF_ALTDOWN) && !(flags & KF_EXTENDED))
return false;
model_->OnUpOrDownKeyPressed(((key == VK_PRIOR) || (key == VK_UP)) ?
-count : count);
return true;
// Hijacking Editing Commands
//
// We hijack the keyboard short-cuts for Cut, Copy, and Paste here so that
// they go through our clipboard routines. This allows us to be smarter
// about how we interact with the clipboard and avoid bugs in the
// CRichEditCtrl. If we didn't hijack here, the edit control would handle
// these internally with sending the WM_CUT, WM_COPY, or WM_PASTE messages.
//
// Cut: Shift-Delete and Ctrl-x are treated as cut. Ctrl-Shift-Delete and
// Ctrl-Shift-x are not treated as cut even though the underlying
// CRichTextEdit would treat them as such. Also note that we bring
// up 'clear browsing data' on control-shift-delete.
// Copy: Ctrl-Insert and Ctrl-c is treated as copy. Shift-Ctrl-c is not.
// (This is handled in OnKeyDownAllModes().)
// Paste: Shift-Insert and Ctrl-v are treated as paste. Ctrl-Shift-Insert
// and Ctrl-Shift-v are not.
//
// This behavior matches most, but not all Windows programs, and largely
// conforms to what users expect.
case VK_DELETE:
if (flags & KF_ALTDOWN)
return false;
if (GetKeyState(VK_SHIFT) >= 0) {
if (GetKeyState(VK_CONTROL) >= 0) {
CHARRANGE selection;
GetSel(selection);
delete_at_end_pressed_ = ((selection.cpMin == selection.cpMax) &&
(selection.cpMin == GetTextLength()));
}
return false;
}
if (GetKeyState(VK_CONTROL) >= 0) {
// Cut text if possible.
CHARRANGE selection;
GetSel(selection);
if (selection.cpMin != selection.cpMax) {
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
Cut();
OnAfterPossibleChange();
} else {
if (model_->popup_model()->IsOpen()) {
// This is a bit overloaded, but we hijack Shift-Delete in this
// case to delete the current item from the pop-up. We prefer
// cutting to this when possible since that's the behavior more
// people expect from Shift-Delete, and it's more commonly useful.
model_->popup_model()->TryDeletingCurrentItem();
}
}
}
return true;
case 'X':
if ((flags & KF_ALTDOWN) || (GetKeyState(VK_CONTROL) >= 0))
return false;
if (GetKeyState(VK_SHIFT) >= 0) {
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
Cut();
OnAfterPossibleChange();
}
return true;
case VK_INSERT:
// Ignore insert by itself, so we don't turn overtype mode on/off.
if (!(flags & KF_ALTDOWN) && (GetKeyState(VK_SHIFT) >= 0) &&
(GetKeyState(VK_CONTROL) >= 0))
return true;
// FALL THROUGH
case 'V':
if ((flags & KF_ALTDOWN) ||
(GetKeyState((key == 'V') ? VK_CONTROL : VK_SHIFT) >= 0))
return false;
if (GetKeyState((key == 'V') ? VK_SHIFT : VK_CONTROL) >= 0) {
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
Paste();
OnAfterPossibleChange();
}
return true;
case VK_BACK: {
if ((flags & KF_ALTDOWN) || model_->is_keyword_hint() ||
model_->keyword().empty())
return false;
{
CHARRANGE selection;
GetSel(selection);
if ((selection.cpMin != selection.cpMax) || (selection.cpMin != 0))
return false;
}
// We're showing a keyword and the user pressed backspace at the beginning
// of the text. Delete the selected keyword.
ScopedFreeze freeze(this, GetTextObjectModel());
model_->ClearKeyword(GetText());
return true;
}
case VK_TAB: {
if (model_->is_keyword_hint()) {
// Accept the keyword.
ScopedFreeze freeze(this, GetTextObjectModel());
model_->AcceptKeyword();
} else if (!IsCaretAtEnd()) {
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
PlaceCaretAt(GetTextLength());
OnAfterPossibleChange();
} else {
model_->CommitSuggestedText(true);
}
return true;
}
case 0xbb: // Ctrl-'='. Triggers subscripting (even in plain text mode).
// We don't use VK_OEM_PLUS in case the macro isn't defined.
// (e.g., we don't have this symbol in embeded environment).
return true;
default:
return false;
}
}
bool AutocompleteEditViewWin::OnKeyDownAllModes(TCHAR key,
UINT repeat_count,
UINT flags) {
// See KF_ALTDOWN comment atop OnKeyDownOnlyWritable().
switch (key) {
case VK_CONTROL:
model_->OnControlKeyChanged(true);
return false;
case VK_INSERT:
case 'C':
// See more detailed comments in OnKeyDownOnlyWritable().
if ((flags & KF_ALTDOWN) || (GetKeyState(VK_CONTROL) >= 0))
return false;
if (GetKeyState(VK_SHIFT) >= 0)
Copy();
return true;
default:
return false;
}
}
void AutocompleteEditViewWin::GetSelection(CHARRANGE& sel) const {
GetSel(sel);
// See if we need to reverse the direction of the selection.
ITextDocument* const text_object_model = GetTextObjectModel();
if (!text_object_model)
return;
base::win::ScopedComPtr<ITextSelection> selection;
const HRESULT hr = text_object_model->GetSelection(selection.Receive());
DCHECK_EQ(S_OK, hr);
long flags;
selection->GetFlags(&flags);
if (flags & tomSelStartActive)
std::swap(sel.cpMin, sel.cpMax);
}
string16 AutocompleteEditViewWin::GetSelectedText() const {
// Figure out the length of the selection.
CHARRANGE sel;
GetSel(sel);
// Grab the selected text.
string16 str;
GetSelText(WriteInto(&str, sel.cpMax - sel.cpMin + 1));
return str;
}
void AutocompleteEditViewWin::SetSelection(LONG start, LONG end) {
SetSel(start, end);
if (start <= end)
return;
// We need to reverse the direction of the selection.
ITextDocument* const text_object_model = GetTextObjectModel();
if (!text_object_model)
return;
base::win::ScopedComPtr<ITextSelection> selection;
const HRESULT hr = text_object_model->GetSelection(selection.Receive());
DCHECK_EQ(S_OK, hr);
selection->SetFlags(tomSelStartActive);
}
void AutocompleteEditViewWin::PlaceCaretAt(string16::size_type pos) {
SetSelection(static_cast<LONG>(pos), static_cast<LONG>(pos));
}
bool AutocompleteEditViewWin::IsSelectAllForRange(const CHARRANGE& sel) const {
const int text_length = GetTextLength();
return ((sel.cpMin == 0) && (sel.cpMax >= text_length)) ||
((sel.cpMax == 0) && (sel.cpMin >= text_length));
}
LONG AutocompleteEditViewWin::ClipXCoordToVisibleText(
LONG x, bool is_triple_click) const {
// Clip the X coordinate to the left edge of the text. Careful:
// PosFromChar(0) may return a negative X coordinate if the beginning of the
// text has scrolled off the edit, so don't go past the clip rect's edge.
PARAFORMAT2 pf2;
GetParaFormat(pf2);
// Calculation of the clipped coordinate is more complicated if the paragraph
// layout is RTL layout, or if there is RTL characters inside the LTR layout
// paragraph.
const bool ltr_text_in_ltr_layout = !(pf2.wEffects & PFE_RTLPARA) &&
!base::i18n::StringContainsStrongRTLChars(GetText());
const int length = GetTextLength();
RECT r;
GetRect(&r);
// The values returned by PosFromChar() seem to refer always
// to the left edge of the character's bounding box.
const LONG first_position_x = PosFromChar(0).x;
LONG min_x = first_position_x;
if (!ltr_text_in_ltr_layout) {
for (int i = 1; i < length; ++i)
min_x = std::min(min_x, PosFromChar(i).x);
}
const LONG left_bound = std::max(r.left, min_x);
// PosFromChar(length) is a phantom character past the end of the text. It is
// not necessarily a right bound; in RTL controls it may be a left bound. So
// treat it as a right bound only if it is to the right of the first
// character.
LONG right_bound = r.right;
LONG end_position_x = PosFromChar(length).x;
if (end_position_x >= first_position_x) {
right_bound = std::min(right_bound, end_position_x); // LTR case.
}
// For trailing characters that are 2 pixels wide or less (like "l" in some
// fonts), we have a problem:
// * Clicks on any pixel within the character will place the cursor before
// the character.
// * Clicks on the pixel just after the character will not allow triple-
// click to work properly (true for any last character width).
// So, we move to the last pixel of the character when this is a
// triple-click, and moving to one past the last pixel in all other
// scenarios. This way, all clicks that can move the cursor will place it at
// the end of the text, but triple-click will still work.
if (x < left_bound) {
return (is_triple_click && ltr_text_in_ltr_layout) ? left_bound - 1 :
left_bound;
}
if ((length == 0) || (x < right_bound))
return x;
return is_triple_click ? (right_bound - 1) : right_bound;
}
void AutocompleteEditViewWin::EmphasizeURLComponents() {
ITextDocument* const text_object_model = GetTextObjectModel();
ScopedFreeze freeze(this, text_object_model);
ScopedSuspendUndo suspend_undo(text_object_model);
// Save the selection.
CHARRANGE saved_sel;
GetSelection(saved_sel);
// See whether the contents are a URL with a non-empty host portion, which we
// should emphasize. To check for a URL, rather than using the type returned
// by Parse(), ask the model, which will check the desired page transition for
// this input. This can tell us whether an UNKNOWN input string is going to
// be treated as a search or a navigation, and is the same method the Paste
// And Go system uses.
url_parse::Component scheme, host;
AutocompleteInput::ParseForEmphasizeComponents(
GetText(), model_->GetDesiredTLD(), &scheme, &host);
const bool emphasize = model_->CurrentTextIsURL() && (host.len > 0);
// Set the baseline emphasis.
CHARFORMAT cf = {0};
cf.dwMask = CFM_COLOR;
// If we're going to emphasize parts of the text, then the baseline state
// should be "de-emphasized". If not, then everything should be rendered in
// the standard text color.
cf.crTextColor = skia::SkColorToCOLORREF(LocationBarView::GetColor(
security_level_,
emphasize ? LocationBarView::DEEMPHASIZED_TEXT : LocationBarView::TEXT));
// NOTE: Don't use SetDefaultCharFormat() instead of the below; that sets the
// format that will get applied to text added in the future, not to text
// already in the edit.
SelectAll(false);
SetSelectionCharFormat(cf);
if (emphasize) {
// We've found a host name, give it more emphasis.
cf.crTextColor = skia::SkColorToCOLORREF(LocationBarView::GetColor(
security_level_, LocationBarView::TEXT));
SetSelection(host.begin, host.end());
SetSelectionCharFormat(cf);
}
// Emphasize the scheme for security UI display purposes (if necessary).
insecure_scheme_component_.reset();
if (!model_->user_input_in_progress() && scheme.is_nonempty() &&
(security_level_ != ToolbarModel::NONE)) {
if (security_level_ == ToolbarModel::SECURITY_ERROR) {
insecure_scheme_component_.begin = scheme.begin;
insecure_scheme_component_.len = scheme.len;
}
cf.crTextColor = skia::SkColorToCOLORREF(LocationBarView::GetColor(
security_level_, LocationBarView::SECURITY_TEXT));
SetSelection(scheme.begin, scheme.end());
SetSelectionCharFormat(cf);
}
// Restore the selection.
SetSelectionRange(saved_sel);
}
void AutocompleteEditViewWin::EraseTopOfSelection(
CDC* dc, const CRect& client_rect, const CRect& paint_clip_rect) {
// Find the area we care about painting. We could calculate the rect
// containing just the selected portion, but there's no harm in simply erasing
// the whole top of the client area, and at least once I saw us manage to
// select the "phantom newline" briefly, which looks very weird if not clipped
// off at the same height.
CRect erase_rect(client_rect.left, client_rect.top, client_rect.right,
client_rect.top + font_y_adjustment_);
erase_rect.IntersectRect(erase_rect, paint_clip_rect);
// Erase to the background color.
if (!erase_rect.IsRectNull())
dc->FillSolidRect(&erase_rect, background_color_);
}
void AutocompleteEditViewWin::DrawSlashForInsecureScheme(
HDC hdc,
const CRect& client_rect,
const CRect& paint_clip_rect) {
DCHECK(insecure_scheme_component_.is_nonempty());
// Calculate the rect, in window coordinates, containing the portion of the
// scheme where we'll be drawing the slash. Vertically, we draw across one
// x-height of text, plus an additional 3 stroke diameters (the stroke width
// plus a half-stroke width of space between the stroke and the text, both
// above and below the text).
const int font_top = client_rect.top + font_y_adjustment_;
const SkScalar kStrokeWidthPixels = SkIntToScalar(2);
const int kAdditionalSpaceOutsideFont =
static_cast<int>(ceil(kStrokeWidthPixels * 1.5f));
const CRect scheme_rect(PosFromChar(insecure_scheme_component_.begin).x,
font_top + font_.GetBaseline() - font_x_height_ -
kAdditionalSpaceOutsideFont,
PosFromChar(insecure_scheme_component_.end()).x,
font_top + font_.GetBaseline() +
kAdditionalSpaceOutsideFont);
// Clip to the portion we care about and translate to canvas coordinates
// (see the canvas creation below) for use later.
CRect canvas_clip_rect, canvas_paint_clip_rect;
canvas_clip_rect.IntersectRect(scheme_rect, client_rect);
canvas_paint_clip_rect.IntersectRect(canvas_clip_rect, paint_clip_rect);
if (canvas_paint_clip_rect.IsRectNull())
return; // We don't need to paint any of this region, so just bail early.
canvas_clip_rect.OffsetRect(-scheme_rect.left, -scheme_rect.top);
canvas_paint_clip_rect.OffsetRect(-scheme_rect.left, -scheme_rect.top);
// Create a paint context for drawing the antialiased stroke.
SkPaint paint;
paint.setAntiAlias(true);
paint.setStrokeWidth(kStrokeWidthPixels);
paint.setStrokeCap(SkPaint::kRound_Cap);
// Create a canvas as large as |scheme_rect| to do our drawing, and initialize
// it to fully transparent so any antialiasing will look nice when painted
// atop the edit.
gfx::CanvasSkia canvas(scheme_rect.Width(), scheme_rect.Height(), false);
canvas.getDevice()->accessBitmap(true).eraseARGB(0, 0, 0, 0);
// Calculate the start and end of the stroke, which are just the lower left
// and upper right corners of the canvas, inset by the radius of the endcap
// so we don't clip the endcap off.
const SkScalar kEndCapRadiusPixels = kStrokeWidthPixels / SkIntToScalar(2);
const SkPoint start_point = {
kEndCapRadiusPixels,
SkIntToScalar(scheme_rect.Height()) - kEndCapRadiusPixels };
const SkPoint end_point = {
SkIntToScalar(scheme_rect.Width()) - kEndCapRadiusPixels,
kEndCapRadiusPixels };
// Calculate the selection rectangle in canvas coordinates, which we'll use
// to clip the stroke so we can draw the unselected and selected portions.
CHARRANGE sel;
GetSel(sel);
const SkRect selection_rect = {
SkIntToScalar(PosFromChar(sel.cpMin).x - scheme_rect.left),
SkIntToScalar(0),
SkIntToScalar(PosFromChar(sel.cpMax).x - scheme_rect.left),
SkIntToScalar(scheme_rect.Height()) };
// Draw the unselected portion of the stroke.
canvas.save();
if (selection_rect.isEmpty() ||
canvas.clipRect(selection_rect, SkRegion::kDifference_Op)) {
paint.setColor(LocationBarView::GetColor(security_level_,
LocationBarView::SECURITY_TEXT));
canvas.drawLine(start_point.fX, start_point.fY,
end_point.fX, end_point.fY, paint);
}
canvas.restore();
// Draw the selected portion of the stroke.
if (!selection_rect.isEmpty() && canvas.clipRect(selection_rect)) {
paint.setColor(LocationBarView::GetColor(security_level_,
LocationBarView::SELECTED_TEXT));
canvas.drawLine(start_point.fX, start_point.fY,
end_point.fX, end_point.fY, paint);
}
// Now copy what we drew to the target HDC.
canvas.getTopPlatformDevice().drawToHDC(hdc,
scheme_rect.left + canvas_paint_clip_rect.left - canvas_clip_rect.left,
std::max(scheme_rect.top, client_rect.top) + canvas_paint_clip_rect.top -
canvas_clip_rect.top, &canvas_paint_clip_rect);
}
void AutocompleteEditViewWin::DrawDropHighlight(HDC hdc,
const CRect& client_rect,
const CRect& paint_clip_rect) {
DCHECK_NE(-1, drop_highlight_position_);
const int highlight_y = client_rect.top + font_y_adjustment_;
const int highlight_x = PosFromChar(drop_highlight_position_).x - 1;
const CRect highlight_rect(highlight_x,
highlight_y,
highlight_x + 1,
highlight_y + font_.GetHeight());
// Clip the highlight to the region being painted.
CRect clip_rect;
clip_rect.IntersectRect(highlight_rect, paint_clip_rect);
if (clip_rect.IsRectNull())
return;
HGDIOBJ last_pen = SelectObject(hdc, CreatePen(PS_SOLID, 1, RGB(0, 0, 0)));
MoveToEx(hdc, clip_rect.left, clip_rect.top, NULL);
LineTo(hdc, clip_rect.left, clip_rect.bottom);
DeleteObject(SelectObject(hdc, last_pen));
}
void AutocompleteEditViewWin::TextChanged() {
ScopedFreeze freeze(this, GetTextObjectModel());
EmphasizeURLComponents();
model_->OnChanged();
}
string16 AutocompleteEditViewWin::GetClipboardText() const {
// Try text format.
ui::Clipboard* clipboard = g_browser_process->clipboard();
if (clipboard->IsFormatAvailable(ui::Clipboard::GetPlainTextWFormatType(),
ui::Clipboard::BUFFER_STANDARD)) {
string16 text;
clipboard->ReadText(ui::Clipboard::BUFFER_STANDARD, &text);
// Note: Unlike in the find popup and textfield view, here we completely
// remove whitespace strings containing newlines. We assume users are
// most likely pasting in URLs that may have been split into multiple
// lines in terminals, email programs, etc., and so linebreaks indicate
// completely bogus whitespace that would just cause the input to be
// invalid.
return CollapseWhitespace(text, true);
}
// Try bookmark format.
//
// It is tempting to try bookmark format first, but the URL we get out of a
// bookmark has been cannonicalized via GURL. This means if a user copies
// and pastes from the URL bar to itself, the text will get fixed up and
// cannonicalized, which is not what the user expects. By pasting in this
// order, we are sure to paste what the user copied.
if (clipboard->IsFormatAvailable(ui::Clipboard::GetUrlWFormatType(),
ui::Clipboard::BUFFER_STANDARD)) {
std::string url_str;
clipboard->ReadBookmark(NULL, &url_str);
// pass resulting url string through GURL to normalize
GURL url(url_str);
if (url.is_valid())
return UTF8ToWide(url.spec());
}
return string16();
}
bool AutocompleteEditViewWin::CanPasteAndGo(const string16& text) const {
return !popup_window_mode_ && model_->CanPasteAndGo(text);
}
ITextDocument* AutocompleteEditViewWin::GetTextObjectModel() const {
if (!text_object_model_) {
// This is lazily initialized, instead of being initialized in the
// constructor, in order to avoid hurting startup performance.
base::win::ScopedComPtr<IRichEditOle, NULL> ole_interface;
ole_interface.Attach(GetOleInterface());
if (ole_interface) {
ole_interface.QueryInterface(
__uuidof(ITextDocument),
reinterpret_cast<void**>(&text_object_model_));
}
}
return text_object_model_;
}
void AutocompleteEditViewWin::StartDragIfNecessary(const CPoint& point) {
if (initiated_drag_ || !IsDrag(click_point_[kLeft], point))
return;
ui::OSExchangeData data;
DWORD supported_modes = DROPEFFECT_COPY;
CHARRANGE sel;
GetSelection(sel);
// We're about to start a drag session, but the edit is expecting a mouse up
// that it uses to reset internal state. If we don't send a mouse up now,
// when the mouse moves back into the edit the edit will reset the selection.
// So, we send the event now which resets the selection. We then restore the
// selection and start the drag. We always send lbuttonup as otherwise we
// might trigger a context menu (right up). This seems scary, but doesn't
// seem to cause problems.
{
ScopedFreeze freeze(this, GetTextObjectModel());
DefWindowProc(WM_LBUTTONUP, 0,
MAKELPARAM(click_point_[kLeft].x, click_point_[kLeft].y));
SetSelectionRange(sel);
}
const string16 start_text(GetText());
string16 text_to_write(GetSelectedText());
GURL url;
bool write_url;
const bool is_all_selected = IsSelectAllForRange(sel);
// |sel| was set by GetSelection(), which preserves selection direction, so
// sel.cpMin may not be the smaller value.
model()->AdjustTextForCopy(std::min(sel.cpMin, sel.cpMax), is_all_selected,
&text_to_write, &url, &write_url);
if (write_url) {
string16 title;
SkBitmap favicon;
if (is_all_selected)
model_->GetDataForURLExport(&url, &title, &favicon);
drag_utils::SetURLAndDragImage(url, title, favicon, &data);
supported_modes |= DROPEFFECT_LINK;
UserMetrics::RecordAction(UserMetricsAction("Omnibox_DragURL"),
model_->profile());
} else {
supported_modes |= DROPEFFECT_MOVE;
UserMetrics::RecordAction(UserMetricsAction("Omnibox_DragString"),
model_->profile());
}
data.SetString(text_to_write);
scoped_refptr<ui::DragSource> drag_source(new ui::DragSource);
DWORD dropped_mode;
AutoReset<bool> auto_reset_in_drag(&in_drag_, true);
if (DoDragDrop(ui::OSExchangeDataProviderWin::GetIDataObject(data),
drag_source, supported_modes, &dropped_mode) ==
DRAGDROP_S_DROP) {
if ((dropped_mode == DROPEFFECT_MOVE) && (start_text == GetText())) {
ScopedFreeze freeze(this, GetTextObjectModel());
OnBeforePossibleChange();
SetSelectionRange(sel);
ReplaceSel(L"", true);
OnAfterPossibleChange();
}
// else case, not a move or it was a move and the drop was on us.
// If the drop was on us, EditDropTarget took care of the move so that
// we don't have to delete the text.
possible_drag_ = false;
} else {
// Drag was canceled or failed. The mouse may still be down and
// over us, in which case we need possible_drag_ to remain true so
// that we don't forward mouse move events to the edit which will
// start another drag.
//
// NOTE: we didn't use mouse capture during the mouse down as DoDragDrop
// does its own capture.
CPoint cursor_location;
GetCursorPos(&cursor_location);
CRect client_rect;
GetClientRect(&client_rect);
CPoint client_origin_on_screen(client_rect.left, client_rect.top);
ClientToScreen(&client_origin_on_screen);
client_rect.MoveToXY(client_origin_on_screen.x,
client_origin_on_screen.y);
possible_drag_ = (client_rect.PtInRect(cursor_location) &&
((GetKeyState(VK_LBUTTON) != 0) ||
(GetKeyState(VK_MBUTTON) != 0) ||
(GetKeyState(VK_RBUTTON) != 0)));
}
initiated_drag_ = true;
tracking_click_[kLeft] = false;
}
void AutocompleteEditViewWin::OnPossibleDrag(const CPoint& point) {
if (possible_drag_)
return;
click_point_[kLeft] = point;
initiated_drag_ = false;
CHARRANGE selection;
GetSel(selection);
if (selection.cpMin != selection.cpMax) {
const POINT min_sel_location(PosFromChar(selection.cpMin));
const POINT max_sel_location(PosFromChar(selection.cpMax));
// NOTE: we don't consider the y location here as we always pass a
// y-coordinate in the middle to the default handler which always triggers
// a drag regardless of the y-coordinate.
possible_drag_ = (point.x >= min_sel_location.x) &&
(point.x < max_sel_location.x);
}
}
void AutocompleteEditViewWin::RepaintDropHighlight(int position) {
if ((position != -1) && (position <= GetTextLength())) {
const POINT min_loc(PosFromChar(position));
const RECT highlight_bounds = {min_loc.x - 1, font_y_adjustment_,
min_loc.x + 2, font_.GetHeight() + font_y_adjustment_};
InvalidateRect(&highlight_bounds, false);
}
}
void AutocompleteEditViewWin::BuildContextMenu() {
if (context_menu_contents_.get())
return;
context_menu_contents_.reset(new ui::SimpleMenuModel(this));
// Set up context menu.
if (popup_window_mode_) {
context_menu_contents_->AddItemWithStringId(IDC_COPY, IDS_COPY);
} else {
context_menu_contents_->AddItemWithStringId(IDS_UNDO, IDS_UNDO);
context_menu_contents_->AddSeparator();
context_menu_contents_->AddItemWithStringId(IDC_CUT, IDS_CUT);
context_menu_contents_->AddItemWithStringId(IDC_COPY, IDS_COPY);
context_menu_contents_->AddItemWithStringId(IDC_PASTE, IDS_PASTE);
// GetContextualLabel() will override this next label with the
// IDS_PASTE_AND_SEARCH label as needed.
context_menu_contents_->AddItemWithStringId(IDS_PASTE_AND_GO,
IDS_PASTE_AND_GO);
context_menu_contents_->AddSeparator();
context_menu_contents_->AddItemWithStringId(IDS_SELECT_ALL, IDS_SELECT_ALL);
context_menu_contents_->AddSeparator();
context_menu_contents_->AddItemWithStringId(IDS_EDIT_SEARCH_ENGINES,
IDS_EDIT_SEARCH_ENGINES);
}
context_menu_.reset(new views::Menu2(context_menu_contents_.get()));
}
void AutocompleteEditViewWin::SelectAllIfNecessary(MouseButton button,
const CPoint& point) {
// When the user has clicked and released to give us focus, select all.
if (tracking_click_[button] &&
!IsDrag(click_point_[button], point)) {
// Select all in the reverse direction so as not to scroll the caret
// into view and shift the contents jarringly.
SelectAll(true);
possible_drag_ = false;
}
}
void AutocompleteEditViewWin::TrackMousePosition(MouseButton button,
const CPoint& point) {
if (gaining_focus_.get()) {
// This click is giving us focus, so we need to track how much the mouse
// moves to see if it's a drag or just a click. Clicks should select all
// the text.
tracking_click_[button] = true;
click_point_[button] = point;
}
}
int AutocompleteEditViewWin::GetHorizontalMargin() const {
RECT rect;
GetRect(&rect);
RECT client_rect;
GetClientRect(&client_rect);
return (rect.left - client_rect.left) + (client_rect.right - rect.right);
}
int AutocompleteEditViewWin::WidthNeededToDisplay(
const string16& text) const {
// Use font_.GetStringWidth() instead of
// PosFromChar(location_entry_->GetTextLength()) because PosFromChar() is
// apparently buggy. In both LTR UI and RTL UI with left-to-right layout,
// PosFromChar(i) might return 0 when i is greater than 1.
return font_.GetStringWidth(text) + GetHorizontalMargin();
}
bool AutocompleteEditViewWin::IsCaretAtEnd() const {
long length = GetTextLength();
CHARRANGE sel;
GetSelection(sel);
return sel.cpMin == sel.cpMax && sel.cpMin == length;
}