blob: 2ecd259d1683bf39dd6b9193cf6ee5847192ba7f [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/omnibox/omnibox_view_win.h"
#include <algorithm>
#include <locale>
#include <string>
#include <richedit.h>
#include <textserv.h>
#include "base/auto_reset.h"
#include "base/basictypes.h"
#include "base/bind.h"
#include "base/i18n/rtl.h"
#include "base/lazy_instance.h"
#include "base/memory/ref_counted.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/iat_patch_function.h"
#include "base/win/metro.h"
#include "base/win/scoped_hdc.h"
#include "base/win/scoped_select_object.h"
#include "base/win/windows_version.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/autocomplete/autocomplete_input.h"
#include "chrome/browser/autocomplete/autocomplete_match.h"
#include "chrome/browser/autocomplete/keyword_provider.h"
#include "chrome/browser/bookmarks/bookmark_node_data.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/command_updater.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/omnibox/omnibox_edit_controller.h"
#include "chrome/browser/ui/omnibox/omnibox_edit_model.h"
#include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_view_views.h"
#include "chrome/common/net/url_fixer_upper.h"
#include "content/public/browser/user_metrics.h"
#include "content/public/browser/web_contents.h"
#include "extensions/common/constants.h"
#include "grit/generated_resources.h"
#include "net/base/escape.h"
#include "skia/ext/skia_utils_win.h"
#include "ui/base/accessibility/accessible_view_state.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_win.h"
#include "ui/base/dragdrop/drop_target_win.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/base/dragdrop/os_exchange_data_provider_win.h"
#include "ui/base/events/event.h"
#include "ui/base/events/event_constants.h"
#include "ui/base/ime/win/tsf_bridge.h"
#include "ui/base/ime/win/tsf_event_router.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/base/touch/touch_enabled.h"
#include "ui/base/win/hwnd_util.h"
#include "ui/base/win/mouse_wheel_util.h"
#include "ui/base/win/touch_input.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/image/image.h"
#include "ui/views/button_drag_utils.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/controls/textfield/native_textfield_win.h"
#include "ui/views/widget/widget.h"
#include "url/url_util.h"
#include "win8/util/win8_util.h"
#pragma comment(lib, "oleacc.lib") // Needed for accessibility support.
using content::UserMetricsAction;
using content::WebContents;
namespace {
const char kAutocompleteEditStateKey[] = "AutocompleteEditState";
// msftedit.dll is RichEdit ver 4.1.
// This version is available from WinXP SP1 and has TSF support.
const wchar_t* kRichEditDLLName = L"msftedit.dll";
// 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
// OmniboxEditModel and OmniboxViewWin 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 : public base::SupportsUserData::Data {
AutocompleteEditState(const OmniboxEditModel::State& model_state,
const OmniboxViewWin::State& view_state)
: model_state(model_state),
view_state(view_state) {
}
virtual ~AutocompleteEditState() {}
const OmniboxEditModel::State model_state;
const OmniboxViewWin::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(
gfx::Point(current) - gfx::Point(origin));
}
// Copies |selected_text| as text to the primary clipboard.
void DoCopyText(const string16& selected_text) {
ui::ScopedClipboardWriter scw(ui::Clipboard::GetForCurrentThread(),
ui::Clipboard::BUFFER_STANDARD);
scw.WriteText(selected_text);
}
// Writes |url| and |text| to the clipboard as a well-formed URL.
void DoCopyURL(const GURL& url, const string16& text) {
BookmarkNodeData data;
data.ReadFromTuple(url, text);
data.WriteToClipboard();
}
} // namespace
// EditDropTarget is the IDropTarget implementation installed on
// OmniboxViewWin. 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 OmniboxViewWin::EditDropTarget : public ui::DropTargetWin {
public:
explicit EditDropTarget(OmniboxViewWin* 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.
OmniboxViewWin* 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);
};
OmniboxViewWin::EditDropTarget::EditDropTarget(OmniboxViewWin* edit)
: ui::DropTargetWin(edit->m_hWnd),
edit_(edit),
drag_has_url_(false),
drag_has_string_(false) {
}
DWORD OmniboxViewWin::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 OmniboxViewWin::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 OmniboxViewWin::EditDropTarget::OnDragLeave(IDataObject* data_object) {
ResetDropHighlights();
}
DWORD OmniboxViewWin::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));
gfx::Point point(cursor_position.x, cursor_position.y);
ui::DropTargetEvent event(
os_data, point, point,
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 OmniboxViewWin::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 OmniboxViewWin::EditDropTarget::ResetDropHighlights() {
if (drag_has_string_)
edit_->SetDropHighlightPosition(-1);
}
///////////////////////////////////////////////////////////////////////////////
// Helper classes
OmniboxViewWin::ScopedFreeze::ScopedFreeze(OmniboxViewWin* 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);
}
}
OmniboxViewWin::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();
}
}
}
OmniboxViewWin::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);
}
OmniboxViewWin::ScopedSuspendUndo::~ScopedSuspendUndo() {
// Resume Undo processing.
if (text_object_model_)
text_object_model_->Undo(tomResume, NULL);
}
// A subclass of NativeViewHost that provides accessibility info for the
// underlying Omnibox view.
class OmniboxViewWrapper : public views::NativeViewHost {
public:
explicit OmniboxViewWrapper(OmniboxViewWin* omnibox_view_win)
: omnibox_view_win_(omnibox_view_win) {}
gfx::NativeViewAccessible GetNativeViewAccessible() {
// This forces it to use NativeViewAccessibilityWin rather than
// any accessibility provided natively by the HWND.
return View::GetNativeViewAccessible();
}
// views::View
virtual void GetAccessibleState(ui::AccessibleViewState* state) {
views::NativeViewHost::GetAccessibleState(state);
state->name = l10n_util::GetStringUTF16(IDS_ACCNAME_LOCATION);
state->role = ui::AccessibilityTypes::ROLE_TEXT;
state->value = omnibox_view_win_->GetText();
state->state = ui::AccessibilityTypes::STATE_EDITABLE;
size_t sel_start;
size_t sel_end;
omnibox_view_win_->GetSelectionBounds(&sel_start, &sel_end);
state->selection_start = sel_start;
state->selection_end = sel_end;
}
private:
OmniboxViewWin* omnibox_view_win_;
DISALLOW_COPY_AND_ASSIGN(OmniboxViewWrapper);
};
///////////////////////////////////////////////////////////////////////////////
// OmniboxViewWin
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);
}
class PaintPatcher {
public:
PaintPatcher();
~PaintPatcher();
void RefPatch();
void DerefPatch();
private:
size_t refcount_;
base::win::IATPatchFunction begin_paint_;
base::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(kRichEditDLLName, "user32.dll", "BeginPaint",
&BeginPaintIntercept);
end_paint_.Patch(kRichEditDLLName, "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 = LAZY_INSTANCE_INITIALIZER;
// twips are a unit of type measurement, and RichEdit controls use them
// to set offsets.
const int kTwipsPerInch = 1440;
} // namespace
HMODULE OmniboxViewWin::loaded_library_module_ = NULL;
OmniboxViewWin::OmniboxViewWin(OmniboxEditController* controller,
LocationBarView* location_bar,
CommandUpdater* command_updater,
bool popup_window_mode,
const gfx::FontList& font_list,
int font_y_offset)
: OmniboxView(location_bar->profile(), controller, command_updater),
popup_view_(OmniboxPopupContentsView::Create(
font_list, this, model(), location_bar)),
location_bar_(location_bar),
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_list_(font_list),
font_y_adjustment_(font_y_offset),
possible_drag_(false),
in_drag_(false),
initiated_drag_(false),
drop_highlight_position_(-1),
ime_candidate_window_open_(false),
background_color_(skia::SkColorToCOLORREF(location_bar->GetColor(
ToolbarModel::NONE, LocationBarView::BACKGROUND))),
security_level_(ToolbarModel::NONE),
text_object_model_(NULL),
tsf_event_router_(base::win::IsTSFAwareRequired() ?
new ui::TSFEventRouter(this) : NULL) {
if (!loaded_library_module_)
loaded_library_module_ = LoadLibrary(kRichEditDLLName);
// RichEdit should be available; rare exceptions should use the Views omnibox.
DCHECK(loaded_library_module_);
saved_selection_for_focus_change_.cpMin = -1;
g_paint_patcher.Pointer()->RefPatch();
Create(location_bar->GetWidget()->GetNativeView(), 0, 0, 0,
l10n_util::GetExtendedStyles());
SetReadOnly(popup_window_mode_);
gfx::NativeFont native_font(font_list_.GetPrimaryFont().GetNativeFont());
SetFont(native_font);
// IMF_DUALFONT (on by default) is supposed to use one font for ASCII text
// and a different one for Asian text. In some cases, ASCII characters may
// be mis-marked as Asian, e.g. because input from the keyboard may be
// auto-stamped with the keyboard language. As a result adjacent characters
// can render in different fonts, which looks bizarre. To fix this we
// disable dual-font mode, which forces the control to use a single font for
// everything.
// Note that we should not disable the very similar IMF_AUTOFONT flag, which
// allows the control to hunt for fonts that can display all the current
// characters; doing this results in "missing glyph" boxes when the user
// enters characters not available in the currently-chosen font.
const LRESULT lang_options = SendMessage(m_hWnd, EM_GETLANGOPTIONS, 0, 0);
SendMessage(m_hWnd, EM_SETLANGOPTIONS, 0, lang_options & ~IMF_DUALFONT);
// 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.
base::win::ScopedGetDC screen_dc(NULL);
base::win::ScopedSelectObject font_in_dc(screen_dc, native_font);
TEXTMETRIC tm = {0};
GetTextMetrics(screen_dc, &tm);
int cap_height = font_list_.GetBaseline() - tm.tmInternalLeading;
// 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.
const float kXHeightRatio = 0.7f;
font_x_height_ = static_cast<int>(
(static_cast<float>(cap_height) * kXHeightRatio) + 0.5);
// 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(screen_dc, LOGPIXELSY);
// 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_);
if (!popup_window_mode_) {
// Non-read-only edit controls have a drop target. Revoke it so that we can
// install our own. Revoking automatically deletes the existing one.
HRESULT hr = RevokeDragDrop(m_hWnd);
DCHECK_EQ(S_OK, hr);
// Register our drop target. The scoped_refptr here will delete the drop
// target if it fails to register itself correctly on |m_hWnd|. Otherwise,
// the edit control will invoke RevokeDragDrop when it's being destroyed, so
// we don't have to do so.
scoped_refptr<EditDropTarget> drop_target(new EditDropTarget(this));
}
}
OmniboxViewWin::~OmniboxViewWin() {
// 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. Note that since text_object_model_ is lazy
// initialized, it may still be null.
if (text_object_model_)
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* OmniboxViewWin::parent_view() const {
return location_bar_;
}
void OmniboxViewWin::SaveStateToTab(WebContents* tab) {
DCHECK(tab);
const OmniboxEditModel::State model_state(model()->GetStateForTabSwitch());
CHARRANGE selection;
GetSelection(selection);
tab->SetUserData(
kAutocompleteEditStateKey,
new AutocompleteEditState(
model_state,
State(selection, saved_selection_for_focus_change_)));
}
void OmniboxViewWin::Update(const WebContents* tab_for_state_restoring) {
const bool visibly_changed_permanent_text = model()->UpdatePermanentText(
controller()->GetToolbarModel()->GetText(true));
const ToolbarModel::SecurityLevel security_level =
controller()->GetToolbarModel()->GetSecurityLevel(false);
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 = static_cast<AutocompleteEditState*>(
tab_for_state_restoring->GetUserData(&kAutocompleteEditStateKey));
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 user 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.
CHARRANGE sel;
GetSelection(sel);
const bool was_select_all = IsSelectAllForRange(sel);
RevertAll();
// Only select all when we have focus. If we don't have focus, selecting
// all is unnecessary since the selection will change on regaining focus,
// and can in fact cause artifacts, e.g. if the user is on the NTP and
// clicks a link to navigate, causing |was_select_all| to be vacuously true
// for the empty omnibox, and we then select all here, leading to the
// trailing portion of a long URL being scrolled into view. We could try
// and address cases like this, but it seems better to just not muck with
// things when the omnibox isn't focused to begin with.
if (was_select_all && model()->has_focus())
SelectAll(sel.cpMin > sel.cpMax);
} else if (changed_security_level) {
// Only the security style changed, nothing else. Redraw our text using it.
EmphasizeURLComponents();
}
}
void OmniboxViewWin::OpenMatch(const AutocompleteMatch& match,
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
size_t selected_line) {
// 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());
OmniboxView::OpenMatch(match, disposition, alternate_nav_url, selected_line);
}
string16 OmniboxViewWin::GetText() const {
const int len = GetTextLength() + 1;
string16 str;
if (len > 1)
GetWindowText(WriteInto(&str, len), len);
return str;
}
void OmniboxViewWin::SetUserText(const string16& text,
const string16& display_text,
bool update_popup) {
ScopedFreeze freeze(this, GetTextObjectModel());
saved_selection_for_focus_change_.cpMin = -1;
OmniboxView::SetUserText(text, display_text, update_popup);
}
void OmniboxViewWin::SetWindowTextAndCaretPos(const string16& text,
size_t caret_pos,
bool update_popup,
bool notify_text_changed) {
SetWindowText(text.c_str());
PlaceCaretAt(caret_pos);
if (update_popup)
UpdatePopup();
if (notify_text_changed)
TextChanged();
}
void OmniboxViewWin::SetForcedQuery() {
const string16 current_text(GetText());
const size_t start = current_text.find_first_not_of(kWhitespaceWide);
if (start == string16::npos || (current_text[start] != '?'))
OmniboxView::SetUserText(L"?");
else
SetSelection(current_text.length(), start + 1);
}
bool OmniboxViewWin::IsSelectAll() const {
CHARRANGE selection;
GetSel(selection);
return IsSelectAllForRange(selection);
}
bool OmniboxViewWin::DeleteAtEndPressed() {
return delete_at_end_pressed_;
}
void OmniboxViewWin::GetSelectionBounds(string16::size_type* start,
string16::size_type* end) const {
CHARRANGE selection;
GetSel(selection);
*start = static_cast<size_t>(selection.cpMin);
*end = static_cast<size_t>(selection.cpMax);
}
void OmniboxViewWin::SelectAll(bool reversed) {
if (reversed)
SetSelection(GetTextLength(), 0);
else
SetSelection(0, GetTextLength());
}
void OmniboxViewWin::RevertAll() {
ScopedFreeze freeze(this, GetTextObjectModel());
saved_selection_for_focus_change_.cpMin = -1;
OmniboxView::RevertAll();
}
void OmniboxViewWin::UpdatePopup() {
ScopedFreeze freeze(this, GetTextObjectModel());
model()->SetInputInProgress(true);
// Don't allow the popup to open while the candidate window is open, so
// they don't overlap.
if (ime_candidate_window_open_)
return;
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 OmniboxViewWin::SetFocus() {
::SetFocus(m_hWnd);
// Restore caret visibility if focus is explicitly requested. This is
// necessary because if we already have invisible focus, the ::SetFocus()
// call above will short-circuit, preventing us from reaching
// OmniboxEditModel::OnSetFocus(), which handles restoring visibility when the
// omnibox regains focus after losing focus.
model()->SetCaretVisibility(true);
}
void OmniboxViewWin::ApplyCaretVisibility() {
// We hide the caret just before destroying it, since destroying a caret that
// is in the "solid" phase of its blinking will leave a solid vertical bar.
// We even hide and destroy the caret if we're going to create it again below.
// If the caret was already visible on entry to this function, the
// CreateCaret() call (which first destroys the old caret) might leave a solid
// vertical bar for the same reason as above. Unconditionally hiding prevents
// this. The caret could be visible on entry to this function if the
// underlying edit control had re-created it automatically (see comments in
// OnPaint()).
HideCaret();
// We use DestroyCaret()/CreateCaret() instead of simply HideCaret()/
// ShowCaret() because HideCaret() is not sticky across paint events, e.g. a
// window resize will effectively restore caret visibility, regardless of
// whether HideCaret() was called before. While we do catch and handle these
// paint events (see OnPaint()), it doesn't seem to be enough to simply call
// HideCaret() while handling them because of the unpredictability of this
// Windows API. According to the documentation, it should be a cumulative call
// e.g. 5 hide calls should be balanced by 5 show calls. We have not found
// this to be true, which may be explained by the fact that this API is called
// internally in Windows, as well.
::DestroyCaret();
if (model()->is_caret_visible()) {
::CreateCaret(m_hWnd, (HBITMAP) NULL, 1, font_list_.GetHeight());
// According to the Windows API documentation, a newly created caret needs
// ShowCaret to be visible.
ShowCaret();
}
}
void OmniboxViewWin::SetDropHighlightPosition(int position) {
if (drop_highlight_position_ != position) {
RepaintDropHighlight(drop_highlight_position_);
drop_highlight_position_ = position;
RepaintDropHighlight(drop_highlight_position_);
}
}
void OmniboxViewWin::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 OmniboxViewWin::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 OmniboxViewWin::OnTemporaryTextMaybeChanged(const string16& display_text,
bool save_original_selection,
bool notify_text_changed) {
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(), false,
notify_text_changed);
}
bool OmniboxViewWin::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 OmniboxViewWin::OnRevertTemporaryText() {
SetSelectionRange(original_selection_);
// We got here because the user hit the Escape key. We explicitly don't call
// TextChanged(), since OmniboxPopupModel::ResetToDefaultMatch() has already
// been called by now, and it would've called TextChanged() if it was
// warranted.
}
void OmniboxViewWin::OnBeforePossibleChange() {
// Record our state.
text_before_change_ = GetText();
GetSelection(sel_before_change_);
}
bool OmniboxViewWin::OnAfterPossibleChange() {
return OnAfterPossibleChangeInternal(false);
}
bool OmniboxViewWin::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(
text_before_change_, 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.
native_view_host_->NotifyAccessibilityEvent(
ui::AccessibilityTypes::EVENT_TEXT_CHANGED, true);
} else if (selection_differs) {
// Notify assistive technology that the cursor or selection changed.
native_view_host_->NotifyAccessibilityEvent(
ui::AccessibilityTypes::EVENT_SELECTION_CHANGED, true);
} else if (delete_at_end_pressed_) {
model()->OnChanged();
}
return something_changed;
}
void OmniboxViewWin::OnCandidateWindowCountChanged(size_t window_count) {
ime_candidate_window_open_ = (window_count != 0);
if (ime_candidate_window_open_) {
CloseOmniboxPopup();
} else if (model()->user_input_in_progress()) {
// UpdatePopup assumes user input is in progress, so only call it if
// that's the case. Otherwise, autocomplete may run on an empty user
// text.
UpdatePopup();
}
}
void OmniboxViewWin::OnTextUpdated(const ui::Range& /*composition_range*/) {
if (ignore_ime_messages_)
return;
OnAfterPossibleChangeInternal(true);
// Call OnBeforePossibleChange function here to get correct diff in next IME
// update. The Text Services Framework does not provide any notification
// before entering edit session, therefore we don't have good place to call
// OnBeforePossibleChange.
OnBeforePossibleChange();
}
gfx::NativeView OmniboxViewWin::GetNativeView() const {
return m_hWnd;
}
// static
gfx::NativeView OmniboxViewWin::GetRelativeWindowForNativeView(
gfx::NativeView edit_native_view) {
// When an IME is attached to the rich-edit control, retrieve its window
// handle, and the popup window of OmniboxPopupView will be shown under the
// IME windows.
// Otherwise, the popup window will be shown under top-most windows.
// TODO(hbono): http://b/1111369 if we exclude this popup window from the
// display area of IME windows, this workaround becomes unnecessary.
HWND ime_window = ImmGetDefaultIMEWnd(edit_native_view);
return ime_window ? ime_window : HWND_NOTOPMOST;
}
gfx::NativeView OmniboxViewWin::GetRelativeWindowForPopup() const {
return GetRelativeWindowForNativeView(GetNativeView());
}
void OmniboxViewWin::SetGrayTextAutocompletion(const string16& suggestion) {
location_bar_->SetGrayTextAutocompletion(suggestion);
}
int OmniboxViewWin::TextWidth() const {
return WidthNeededToDisplay(GetText());
}
string16 OmniboxViewWin::GetGrayTextAutocompletion() const {
return location_bar_->GetGrayTextAutocompletion();
}
bool OmniboxViewWin::IsImeComposing() const {
if (tsf_event_router_)
return tsf_event_router_->IsImeComposing();
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;
}
int OmniboxViewWin::GetMaxEditWidth(int entry_width) const {
RECT formatting_rect;
GetRect(&formatting_rect);
RECT edit_bounds;
GetClientRect(&edit_bounds);
return entry_width - formatting_rect.left -
(edit_bounds.right - formatting_rect.right);
}
views::View* OmniboxViewWin::AddToView(views::View* parent) {
native_view_host_ = new OmniboxViewWrapper(this);
parent->AddChildView(native_view_host_);
native_view_host_->set_focus_view(parent);
native_view_host_->Attach(GetNativeView());
return native_view_host_;
}
int OmniboxViewWin::OnPerformDrop(const ui::DropTargetEvent& event) {
return OnPerformDropImpl(event, false);
}
int OmniboxViewWin::OnPerformDropImpl(const ui::DropTargetEvent& event,
bool in_drag) {
const ui::OSExchangeData& data = event.data();
if (data.HasURL()) {
GURL url;
string16 title;
if (data.GetURLAndTitle(&url, &title)) {
string16 text(StripJavascriptSchemas(UTF8ToUTF16(url.spec())));
OmniboxView::SetUserText(text);
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 {
string16 collapsed_text(CollapseWhitespace(text, true));
if (model()->CanPasteAndGo(collapsed_text))
model()->PasteAndGo(collapsed_text);
}
return CopyOrLinkDragOperation(event.source_operations());
}
}
return ui::DragDropTypes::DRAG_NONE;
}
void OmniboxViewWin::CopyURL() {
DoCopyURL(controller()->GetToolbarModel()->GetURL(),
controller()->GetToolbarModel()->GetText(false));
}
bool OmniboxViewWin::SkipDefaultKeyEventProcessing(const ui::KeyEvent& event) {
// Skip processing of [Alt]+<num-pad digit> Unicode alt key codes.
if (event.IsUnicodeKeyCode())
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 (event.key_code()) {
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:
return true;
default:
return false;
}
}
void OmniboxViewWin::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 OmniboxViewWin::IsCommandIdChecked(int command_id) const {
return false;
}
bool OmniboxViewWin::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_COPY_URL:
return !!CanCopy() &&
controller()->GetToolbarModel()->WouldReplaceSearchURLWithSearchTerms(
false);
case IDC_PASTE:
return !!CanPaste();
case IDS_PASTE_AND_GO:
return model()->CanPasteAndGo(GetClipboardText());
case IDS_SELECT_ALL:
return !!CanSelectAll();
case IDC_EDIT_SEARCH_ENGINES:
return command_updater()->IsCommandEnabled(command_id);
default:
NOTREACHED();
return false;
}
}
bool OmniboxViewWin::GetAcceleratorForCommandId(
int command_id,
ui::Accelerator* accelerator) {
return location_bar_->GetWidget()->GetAccelerator(command_id, accelerator);
}
bool OmniboxViewWin::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 OmniboxViewWin::GetLabelForCommandId(int command_id) const {
DCHECK_EQ(IDS_PASTE_AND_GO, command_id);
return l10n_util::GetStringUTF16(
model()->IsPasteAndSearch(GetClipboardText()) ?
IDS_PASTE_AND_SEARCH : IDS_PASTE_AND_GO);
}
void OmniboxViewWin::ExecuteCommand(int command_id, int event_flags) {
ScopedFreeze freeze(this, GetTextObjectModel());
// These commands don't invoke the popup via OnBefore/AfterPossibleChange().
if (command_id == IDS_PASTE_AND_GO) {
model()->PasteAndGo(GetClipboardText());
return;
} else if (command_id == IDC_EDIT_SEARCH_ENGINES) {
command_updater()->ExecuteCommand(command_id);
return;
} else if (command_id == IDC_COPY) {
Copy();
return;
} else if (command_id == IDC_COPY_URL) {
CopyURL();
return;
}
OnBeforePossibleChange();
switch (command_id) {
case IDS_UNDO:
Undo();
break;
case IDC_CUT:
Cut();
break;
case IDC_PASTE:
Paste();
break;
case IDS_SELECT_ALL:
SelectAll(false);
break;
default:
NOTREACHED();
break;
}
OnAfterPossibleChange();
}
// static
int CALLBACK OmniboxViewWin::WordBreakProc(LPTSTR edit_text,
int current_pos,
int length,
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.
// 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, length, WB_LEFTBREAK);
if ((prev_delim == 0) &&
!WordBreakProc(edit_text, 0, length, 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, length, 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, length, 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, length, 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, length, 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, length, WB_ISDELIMITER))
return i;
}
return length;
}
NOTREACHED();
return 0;
}
// static
bool OmniboxViewWin::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 OmniboxViewWin::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 OmniboxViewWin::OnContextMenu(HWND window, const CPoint& point) {
BuildContextMenu();
context_menu_runner_.reset(
new views::MenuRunner(context_menu_contents_.get()));
gfx::Point location(point);
if (point.x == -1 || point.y == -1) {
POINT p;
GetCaretPos(&p);
MapWindowPoints(HWND_DESKTOP, &p, 1);
location.SetPoint(p.x, p.y);
}
ignore_result(context_menu_runner_->RunMenuAt(native_view_host_->GetWidget(),
NULL, gfx::Rect(location, gfx::Size()), views::MenuItemView::TOPLEFT,
ui::MENU_SOURCE_MOUSE, views::MenuRunner::HAS_MNEMONICS));
}
void OmniboxViewWin::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);
if (write_url)
DoCopyURL(url, text);
else
DoCopyText(text);
}
LRESULT OmniboxViewWin::OnCreate(const CREATESTRUCTW* /*create_struct*/) {
if (base::win::IsTSFAwareRequired()) {
// Enable TSF support of RichEdit.
SetEditStyle(SES_USECTF, SES_USECTF);
}
if ((base::win::GetVersion() >= base::win::VERSION_WIN8) &&
ui::AreTouchEventsEnabled()) {
BOOL touch_mode = RegisterTouchWindow(m_hWnd, TWF_WANTPALM);
DCHECK(touch_mode);
}
SetMsgHandled(FALSE);
// When TSF is enabled, OnTextUpdated() may be called without any previous
// call that would have indicated the start of an editing session. In order
// to guarantee we've always called OnBeforePossibleChange() before
// OnAfterPossibleChange(), we therefore call that here. Note that multiple
// (i.e. unmatched) calls to this function in a row are safe.
if (base::win::IsTSFAwareRequired())
OnBeforePossibleChange();
return 0;
}
void OmniboxViewWin::OnCut() {
OnCopy();
// This replace selection will have no effect (even on the undo stack) if the
// current selection is empty.
ReplaceSel(L"", true);
}
LRESULT OmniboxViewWin::OnGetObject(UINT message,
WPARAM wparam,
LPARAM lparam) {
// This is a request for the native accessibility object.
if (lparam == OBJID_CLIENT) {
return LresultFromObject(IID_IAccessible, wparam,
native_view_host_->GetNativeViewAccessible());
}
return 0;
}
LRESULT OmniboxViewWin::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;
}
LRESULT OmniboxViewWin::OnImeEndComposition(UINT message, WPARAM wparam,
LPARAM lparam) {
// The edit control auto-clears the selection on WM_IME_ENDCOMPOSITION, which
// means any inline autocompletion we were showing will no longer be
// selected, and therefore no longer replaced by further user typing. To
// avoid this we manually restore the original selection after the edit
// handles the message.
CHARRANGE range;
GetSel(range);
LRESULT result = DefWindowProc(message, wparam, lparam);
SetSel(range);
return result;
}
LRESULT OmniboxViewWin::OnImeNotify(UINT message,
WPARAM wparam,
LPARAM lparam) {
// Close the popup when the IME composition window is open, so they don't
// overlap.
switch (wparam) {
case IMN_OPENCANDIDATE:
ime_candidate_window_open_ = true;
CloseOmniboxPopup();
break;
case IMN_CLOSECANDIDATE:
ime_candidate_window_open_ = false;
// UpdatePopup assumes user input is in progress, so only call it if
// that's the case. Otherwise, autocomplete may run on an empty user
// text. For example, Baidu Japanese IME sends IMN_CLOSECANDIDATE when
// composition mode is entered, but the user may not have input anything
// yet.
if (model()->user_input_in_progress())
UpdatePopup();
break;
default:
break;
}
return DefWindowProc(message, wparam, lparam);
}
LRESULT OmniboxViewWin::OnTouchEvent(UINT message,
WPARAM wparam,
LPARAM lparam) {
// There is a bug in Windows 8 where in the generated mouse messages
// after touch go to the window which previously had focus. This means that
// if a user taps the omnibox to give it focus, we don't get the simulated
// WM_LBUTTONDOWN, and thus don't properly select all the text. To ensure
// that we get this message, we capture the mouse when the user is doing a
// single-point tap on an unfocused model.
if ((wparam == 1) && !model()->has_focus()) {
TOUCHINPUT point = {0};
if (ui::GetTouchInputInfoWrapper(reinterpret_cast<HTOUCHINPUT>(lparam), 1,
&point, sizeof(TOUCHINPUT))) {
if (point.dwFlags & TOUCHEVENTF_DOWN)
SetCapture();
else if (point.dwFlags & TOUCHEVENTF_UP)
ReleaseCapture();
}
}
SetMsgHandled(false);
return 0;
}
void OmniboxViewWin::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 OmniboxViewWin::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 OmniboxViewWin::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.
CloseOmniboxPopup();
// 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.
//
// Crazy hack: If we just do PlaceCaretAt(0), and the beginning of the text is
// currently scrolled out of view, we can wind up with a blinking cursor in
// the toolbar at the current X coordinate of the beginning of the text. By
// first doing a reverse-select-all to scroll the beginning of the text into
// view, we work around this CRichEditCtrl bug.
SelectAll(true);
PlaceCaretAt(0);
if (tsf_event_router_)
tsf_event_router_->SetManager(NULL);
}
void OmniboxViewWin::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));
// Rich Edit 4.1 doesn't select the last word when the user double clicks
// past the text. Do it manually.
CHARRANGE selection;
GetSelection(selection);
// The default window proc for Rich Edit 4.1 seems to select the CHARRANGE
// {text_length, text_length + 1} after a double click past the text.
int length = GetTextLength();
if (selection.cpMin == length && selection.cpMax == length + 1) {
string16 text = GetText();
int word_break = WordBreakProc(&text[0], length, length, WB_LEFT);
selection.cpMin = word_break;
selection.cpMax = length;
SetSelectionRange(selection);
}
OnAfterPossibleChange();
gaining_focus_.reset(); // See NOTE in OnMouseActivate().
}
void OmniboxViewWin::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 OmniboxViewWin::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 OmniboxViewWin::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 OmniboxViewWin::OnMButtonDown(UINT /*keys*/, const CPoint& /*point*/) {
tracking_double_click_ = false;
// See note in OnMButtonDblClk above.
}
void OmniboxViewWin::OnMButtonUp(UINT /*keys*/, const CPoint& /*point*/) {
possible_drag_ = false;
// See note in OnMButtonDblClk above.
}
LRESULT OmniboxViewWin::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() ||
(model()->focus_state() == OMNIBOX_FOCUS_INVISIBLE)) &&
((mouse_message == WM_LBUTTONDOWN || mouse_message == WM_RBUTTONDOWN)) &&
(result == MA_ACTIVATE)) {
if (gaining_focus_) {
// On Windows 8 in metro mode, we get two WM_MOUSEACTIVATE messages when
// we click on the omnibox with the mouse.
DCHECK(win8::IsSingleWindowMetroMode());
return result;
}
gaining_focus_.reset(new ScopedFreeze(this, GetTextObjectModel()));
// Restore caret visibility whenever the user clicks in the omnibox in a
// way that would give it focus. We must handle this case separately here
// because if the omnibox currently has invisible focus, the mouse event
// won't trigger either SetFocus() or OmniboxEditModel::OnSetFocus().
model()->SetCaretVisibility(true);
// 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 OmniboxViewWin::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 OmniboxViewWin::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;
// If textfield has focus, reaffirm its caret visibility (without focus, a new
// caret could be created and confuse the user as to where the focus is). This
// needs to be called regardless of the current visibility of the caret. This
// is because the underlying edit control will automatically re-create the
// caret when it receives certain events that trigger repaints, e.g. window
// resize events. This also checks for the existence of selected text, in
// which case there shouldn't be a recreated caret since this would create
// both a highlight and a blinking caret.
if (model()->has_focus()) {
CHARRANGE sel;
GetSel(sel);
if (sel.cpMin == sel.cpMax)
ApplyCaretVisibility();
}
}
void OmniboxViewWin::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 OmniboxViewWin::OnRButtonDblClk(UINT /*keys*/, const CPoint& /*point*/) {
gaining_focus_.reset(); // See NOTE in OnMouseActivate().
SetMsgHandled(false);
}
void OmniboxViewWin::OnRButtonDown(UINT /*keys*/, const CPoint& point) {
TrackMousePosition(kRight, point);
tracking_double_click_ = false;
possible_drag_ = false;
gaining_focus_.reset();
SetMsgHandled(false);
}
void OmniboxViewWin::OnRButtonUp(UINT /*keys*/, const CPoint& point) {
SelectAllIfNecessary(kRight, point);
tracking_click_[kRight] = false;
SetMsgHandled(false);
}
void OmniboxViewWin::OnSetFocus(HWND focus_wnd) {
views::FocusManager* focus_manager = location_bar_->GetFocusManager();
if (focus_manager) {
// Notify the FocusManager that the focused view is now the location bar
// (our parent view).
focus_manager->SetFocusedView(location_bar_);
} 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;
}
if (!tsf_event_router_) {
SetMsgHandled(false);
} else {
DefWindowProc();
// Document manager created by RichEdit can be obtained only after
// WM_SETFOCUS event is handled.
tsf_event_router_->SetManager(
ui::TSFBridge::GetInstance()->GetThreadManager());
SetMsgHandled(true);
}
}
LRESULT OmniboxViewWin::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.
base::AutoReset<bool> auto_reset_ignore_ime_messages(
&ignore_ime_messages_, true);
return DefWindowProc(WM_SETTEXT, 0, reinterpret_cast<LPARAM>(text));
}
void OmniboxViewWin::OnSysChar(TCHAR ch,
UINT repeat_count,
UINT flags) {
DCHECK(flags & KF_ALTDOWN);
// Explicitly show the system menu at a good location on [Alt]+[Space].
// Nearly all other [Alt]+<xxx> combos result in beeping rather than doing
// something useful, so discard those. Note that [Ctrl]+[Alt]+<xxx> generates
// WM_CHAR instead of WM_SYSCHAR, so it is not handled here.
if (ch == VK_SPACE) {
ui::ShowSystemMenu(
native_view_host_->GetWidget()->GetTopLevelWidget()->GetNativeWindow());
}
}
void OmniboxViewWin::OnWindowPosChanging(WINDOWPOS* window_pos) {
if (force_hidden_)
window_pos->flags &= ~SWP_SHOWWINDOW;
SetMsgHandled(true);
}
BOOL OmniboxViewWin::OnMouseWheel(UINT flags, short delta, CPoint point) {
// Forward the mouse-wheel message to the window under the mouse.
if (!ui::RerouteMouseWheel(m_hWnd, MAKEWPARAM(flags, delta),
MAKELPARAM(point.x, point.y)))
SetMsgHandled(false);
return 0;
}
void OmniboxViewWin::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 OmniboxViewWin::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();
}
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: {
const bool shift_pressed = GetKeyState(VK_SHIFT) < 0;
if (model()->is_keyword_hint() && !shift_pressed) {
// Accept the keyword.
ScopedFreeze freeze(this, GetTextObjectModel());
model()->AcceptKeyword(ENTERED_KEYWORD_MODE_VIA_TAB);
} else if (shift_pressed &&
model()->popup_model()->selected_line_state() ==
OmniboxPopupModel::KEYWORD) {
model()->ClearKeyword(GetText());
} else {
model()->OnUpOrDownKeyPressed(shift_pressed ? -count : count);
}
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 OmniboxViewWin::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 OmniboxViewWin::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 OmniboxViewWin::GetSelectedText() const {
CHARRANGE sel;
GetSel(sel);
string16 str;
if (sel.cpMin != sel.cpMax)
GetSelText(WriteInto(&str, sel.cpMax - sel.cpMin + 1));
return str;
}
void OmniboxViewWin::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 OmniboxViewWin::PlaceCaretAt(string16::size_type pos) {
SetSelection(static_cast<LONG>(pos), static_cast<LONG>(pos));
}
bool OmniboxViewWin::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 OmniboxViewWin::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;
}
int OmniboxViewWin::GetOmniboxTextLength() const {
return static_cast<int>(GetTextLength());
}
void OmniboxViewWin::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;
string16 text(GetText());
AutocompleteInput::ParseForEmphasizeComponents(text, &scheme, &host);
// 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 unless we should grey out the entire URL.
bool grey_out_url = text.substr(scheme.begin, scheme.len) ==
UTF8ToUTF16(extensions::kExtensionScheme);
bool grey_base = model()->CurrentTextIsURL() &&
(host.is_nonempty() || grey_out_url);
cf.crTextColor = skia::SkColorToCOLORREF(location_bar_->GetColor(
security_level_,
grey_base ? 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 (host.is_nonempty() && !grey_out_url) {
// We've found a host name and we should provide emphasis to host names,
// so emphasize it.
cf.crTextColor = skia::SkColorToCOLORREF(location_bar_->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() && model()->CurrentTextIsURL() &&
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(location_bar_->GetColor(
security_level_, LocationBarView::SECURITY_TEXT));
SetSelection(scheme.begin, scheme.end());
SetSelectionCharFormat(cf);
}
// Restore the selection.
SetSelectionRange(saved_sel);
}
void OmniboxViewWin::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 OmniboxViewWin::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 int font_ascent = font_list_.GetBaseline();
const CRect scheme_rect(
PosFromChar(insecure_scheme_component_.begin).x,
font_top + font_ascent - font_x_height_ - kAdditionalSpaceOutsideFont,
PosFromChar(insecure_scheme_component_.end()).x,
font_top + font_ascent + 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::Canvas canvas(gfx::Size(scheme_rect.Width(), scheme_rect.Height()),
ui::SCALE_FACTOR_100P, false);
SkCanvas* sk_canvas = canvas.sk_canvas();
sk_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.
sk_canvas->save();
if (selection_rect.isEmpty() ||
sk_canvas->clipRect(selection_rect, SkRegion::kDifference_Op)) {
paint.setColor(location_bar_->GetColor(security_level_,
LocationBarView::SECURITY_TEXT));
sk_canvas->drawLine(start_point.fX, start_point.fY,
end_point.fX, end_point.fY, paint);
}
sk_canvas->restore();
// Draw the selected portion of the stroke.
if (!selection_rect.isEmpty() && sk_canvas->clipRect(selection_rect)) {
paint.setColor(location_bar_->GetColor(security_level_,
LocationBarView::SELECTED_TEXT));
sk_canvas->drawLine(start_point.fX, start_point.fY,
end_point.fX, end_point.fY, paint);
}
// Now copy what we drew to the target HDC.
skia::DrawToNativeContext(sk_canvas, 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 OmniboxViewWin::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_list_.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 OmniboxViewWin::TextChanged() {
ScopedFreeze freeze(this, GetTextObjectModel());
OmniboxView::TextChanged();
}
ITextDocument* OmniboxViewWin::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 OmniboxViewWin::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;
gfx::Image favicon;
if (is_all_selected)
model()->GetDataForURLExport(&url, &title, &favicon);
button_drag_utils::SetURLAndDragImage(url, title, favicon.AsImageSkia(),
&data, native_view_host_->GetWidget());
supported_modes |= DROPEFFECT_LINK;
content::RecordAction(UserMetricsAction("Omnibox_DragURL"));
} else {
supported_modes |= DROPEFFECT_MOVE;
content::RecordAction(UserMetricsAction("Omnibox_DragString"));
}
data.SetString(text_to_write);
scoped_refptr<ui::DragSourceWin> drag_source(new ui::DragSourceWin);
DWORD dropped_mode;
base::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 OmniboxViewWin::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 OmniboxViewWin::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_list_.GetHeight() + font_y_adjustment_};
InvalidateRect(&highlight_bounds, false);
}
}
void OmniboxViewWin::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(ui::NORMAL_SEPARATOR);
context_menu_contents_->AddItemWithStringId(IDC_CUT, IDS_CUT);
context_menu_contents_->AddItemWithStringId(IDC_COPY, IDS_COPY);
if (chrome::IsQueryExtractionEnabled())
context_menu_contents_->AddItemWithStringId(IDC_COPY_URL, IDS_COPY_URL);
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(ui::NORMAL_SEPARATOR);
context_menu_contents_->AddItemWithStringId(IDS_SELECT_ALL, IDS_SELECT_ALL);
context_menu_contents_->AddSeparator(ui::NORMAL_SEPARATOR);
context_menu_contents_->AddItemWithStringId(IDC_EDIT_SEARCH_ENGINES,
IDS_EDIT_SEARCH_ENGINES);
}
}
void OmniboxViewWin::SelectAllIfNecessary(MouseButton button,
const CPoint& point) {
// When the user has clicked and released to give us focus, select all unless
// we're doing search term replacement (in which case refining the existing
// query is common enough that we do click-to-place-cursor).
if (tracking_click_[button] &&
!IsDrag(click_point_[button], point) &&
!controller()->GetToolbarModel()->WouldReplaceSearchURLWithSearchTerms(
false)) {
// 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 OmniboxViewWin::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 OmniboxViewWin::GetHorizontalMargin() const {
RECT rect;
GetRect(&rect);
RECT client_rect;
GetClientRect(&client_rect);
return (rect.left - client_rect.left) + (client_rect.right - rect.right);
}
int OmniboxViewWin::WidthNeededToDisplay(const string16& text) const {
// Use font_list_.GetPrimaryFont().GetStringWidth() instead of
// PosFromChar(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_list_.GetPrimaryFont().GetStringWidth(text) +
GetHorizontalMargin();
}