/*
* Copyright (C) 2010 Apple Inc. All rights reserved.
* Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved.
* Copyright (C) 2011 Igalia S.L.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "WebView.h"
#include "ChunkedUpdateDrawingAreaProxy.h"
#include "NativeWebKeyboardEvent.h"
#include "NativeWebMouseEvent.h"
#include "NotImplemented.h"
#include "WebContext.h"
#include "WebContextMenuProxy.h"
#include "WebEventFactory.h"
#include "WebViewWidget.h"
#include "WebPageProxy.h"
#include <wtf/text/WTFString.h>
typedef HashMap<int, const char*> IntConstCharHashMap;
using namespace WebCore;
namespace WebKit {
void WebView::handleFocusInEvent(GtkWidget* widget)
{
if (!(m_isPageActive)) {
m_isPageActive = true;
m_page->viewStateDidChange(WebPageProxy::ViewWindowIsActive);
}
m_page->viewStateDidChange(WebPageProxy::ViewIsFocused);
}
void WebView::handleFocusOutEvent(GtkWidget* widget)
{
m_isPageActive = false;
m_page->viewStateDidChange(WebPageProxy::ViewWindowIsActive);
}
static void backspaceCallback(GtkWidget* widget, WebView* client)
{
g_signal_stop_emission_by_name(widget, "backspace");
client->addPendingEditorCommand("DeleteBackward");
}
static void selectAllCallback(GtkWidget* widget, gboolean select, WebView* client)
{
g_signal_stop_emission_by_name(widget, "select-all");
client->addPendingEditorCommand(select ? "SelectAll" : "Unselect");
}
static void cutClipboardCallback(GtkWidget* widget, WebView* client)
{
g_signal_stop_emission_by_name(widget, "cut-clipboard");
client->addPendingEditorCommand("Cut");
}
static void copyClipboardCallback(GtkWidget* widget, WebView* client)
{
g_signal_stop_emission_by_name(widget, "copy-clipboard");
client->addPendingEditorCommand("Copy");
}
static void pasteClipboardCallback(GtkWidget* widget, WebView* client)
{
g_signal_stop_emission_by_name(widget, "paste-clipboard");
client->addPendingEditorCommand("Paste");
}
static void toggleOverwriteCallback(GtkWidget* widget, EditorClient*)
{
// We don't support toggling the overwrite mode, but the default callback expects
// the GtkTextView to have a layout, so we handle this signal just to stop it.
g_signal_stop_emission_by_name(widget, "toggle-overwrite");
}
// GTK+ will still send these signals to the web view. So we can safely stop signal
// emission without breaking accessibility.
static void popupMenuCallback(GtkWidget* widget, EditorClient*)
{
g_signal_stop_emission_by_name(widget, "popup-menu");
}
static void showHelpCallback(GtkWidget* widget, EditorClient*)
{
g_signal_stop_emission_by_name(widget, "show-help");
}
static const char* const gtkDeleteCommands[][2] = {
{ "DeleteBackward", "DeleteForward" }, // Characters
{ "DeleteWordBackward", "DeleteWordForward" }, // Word ends
{ "DeleteWordBackward", "DeleteWordForward" }, // Words
{ "DeleteToBeginningOfLine", "DeleteToEndOfLine" }, // Lines
{ "DeleteToBeginningOfLine", "DeleteToEndOfLine" }, // Line ends
{ "DeleteToBeginningOfParagraph", "DeleteToEndOfParagraph" }, // Paragraph ends
{ "DeleteToBeginningOfParagraph", "DeleteToEndOfParagraph" }, // Paragraphs
{ 0, 0 } // Whitespace (M-\ in Emacs)
};
static void deleteFromCursorCallback(GtkWidget* widget, GtkDeleteType deleteType, gint count, WebView* client)
{
g_signal_stop_emission_by_name(widget, "delete-from-cursor");
int direction = count > 0 ? 1 : 0;
// Ensuring that deleteType <= G_N_ELEMENTS here results in a compiler warning
// that the condition is always true.
if (deleteType == GTK_DELETE_WORDS) {
if (!direction) {
client->addPendingEditorCommand("MoveWordForward");
client->addPendingEditorCommand("MoveWordBackward");
} else {
client->addPendingEditorCommand("MoveWordBackward");
client->addPendingEditorCommand("MoveWordForward");
}
} else if (deleteType == GTK_DELETE_DISPLAY_LINES) {
if (!direction)
client->addPendingEditorCommand("MoveToBeginningOfLine");
else
client->addPendingEditorCommand("MoveToEndOfLine");
} else if (deleteType == GTK_DELETE_PARAGRAPHS) {
if (!direction)
client->addPendingEditorCommand("MoveToBeginningOfParagraph");
else
client->addPendingEditorCommand("MoveToEndOfParagraph");
}
const char* rawCommand = gtkDeleteCommands[deleteType][direction];
if (!rawCommand)
return;
for (int i = 0; i < abs(count); i++)
client->addPendingEditorCommand(rawCommand);
}
static const char* const gtkMoveCommands[][4] = {
{ "MoveBackward", "MoveForward",
"MoveBackwardAndModifySelection", "MoveForwardAndModifySelection" }, // Forward/backward grapheme
{ "MoveLeft", "MoveRight",
"MoveBackwardAndModifySelection", "MoveForwardAndModifySelection" }, // Left/right grapheme
{ "MoveWordBackward", "MoveWordForward",
"MoveWordBackwardAndModifySelection", "MoveWordForwardAndModifySelection" }, // Forward/backward word
{ "MoveUp", "MoveDown",
"MoveUpAndModifySelection", "MoveDownAndModifySelection" }, // Up/down line
{ "MoveToBeginningOfLine", "MoveToEndOfLine",
"MoveToBeginningOfLineAndModifySelection", "MoveToEndOfLineAndModifySelection" }, // Up/down line ends
{ "MoveParagraphForward", "MoveParagraphBackward",
"MoveParagraphForwardAndModifySelection", "MoveParagraphBackwardAndModifySelection" }, // Up/down paragraphs
{ "MoveToBeginningOfParagraph", "MoveToEndOfParagraph",
"MoveToBeginningOfParagraphAndModifySelection", "MoveToEndOfParagraphAndModifySelection" }, // Up/down paragraph ends.
{ "MovePageUp", "MovePageDown",
"MovePageUpAndModifySelection", "MovePageDownAndModifySelection" }, // Up/down page
{ "MoveToBeginningOfDocument", "MoveToEndOfDocument",
"MoveToBeginningOfDocumentAndModifySelection", "MoveToEndOfDocumentAndModifySelection" }, // Begin/end of buffer
{ 0, 0,
0, 0 } // Horizontal page movement
};
static void moveCursorCallback(GtkWidget* widget, GtkMovementStep step, gint count, gboolean extendSelection, WebView* client)
{
g_signal_stop_emission_by_name(widget, "move-cursor");
int direction = count > 0 ? 1 : 0;
if (extendSelection)
direction += 2;
if (static_cast<unsigned>(step) >= G_N_ELEMENTS(gtkMoveCommands))
return;
const char* rawCommand = gtkMoveCommands[step][direction];
if (!rawCommand)
return;
for (int i = 0; i < abs(count); i++)
client->addPendingEditorCommand(rawCommand);
}
static const unsigned CtrlKey = 1 << 0;
static const unsigned AltKey = 1 << 1;
static const unsigned ShiftKey = 1 << 2;
struct KeyDownEntry {
unsigned virtualKey;
unsigned modifiers;
const char* name;
};
struct KeyPressEntry {
unsigned charCode;
unsigned modifiers;
const char* name;
};
static const KeyDownEntry keyDownEntries[] = {
{ 'B', CtrlKey, "ToggleBold" },
{ 'I', CtrlKey, "ToggleItalic" },
{ VK_ESCAPE, 0, "Cancel" },
{ VK_OEM_PERIOD, CtrlKey, "Cancel" },
{ VK_TAB, 0, "InsertTab" },
{ VK_TAB, ShiftKey, "InsertBacktab" },
{ VK_RETURN, 0, "InsertNewline" },
{ VK_RETURN, CtrlKey, "InsertNewline" },
{ VK_RETURN, AltKey, "InsertNewline" },
{ VK_RETURN, AltKey | ShiftKey, "InsertNewline" },
};
static const KeyPressEntry keyPressEntries[] = {
{ '\t', 0, "InsertTab" },
{ '\t', ShiftKey, "InsertBacktab" },
{ '\r', 0, "InsertNewline" },
{ '\r', CtrlKey, "InsertNewline" },
{ '\r', AltKey, "InsertNewline" },
{ '\r', AltKey | ShiftKey, "InsertNewline" },
};
WebView::WebView(WebContext* context, WebPageGroup* pageGroup)
: m_isPageActive(true)
, m_nativeWidget(gtk_text_view_new())
{
m_page = context->createWebPage(this, pageGroup);
m_viewWidget = static_cast<GtkWidget*>(g_object_new(WEB_VIEW_TYPE_WIDGET, NULL));
ASSERT(m_viewWidget);
m_page->initializeWebPage();
WebViewWidget* webViewWidget = WEB_VIEW_WIDGET(m_viewWidget);
webViewWidgetSetWebViewInstance(webViewWidget, this);
g_signal_connect(m_nativeWidget.get(), "backspace", G_CALLBACK(backspaceCallback), this);
g_signal_connect(m_nativeWidget.get(), "cut-clipboard", G_CALLBACK(cutClipboardCallback), this);
g_signal_connect(m_nativeWidget.get(), "copy-clipboard", G_CALLBACK(copyClipboardCallback), this);
g_signal_connect(m_nativeWidget.get(), "paste-clipboard", G_CALLBACK(pasteClipboardCallback), this);
g_signal_connect(m_nativeWidget.get(), "select-all", G_CALLBACK(selectAllCallback), this);
g_signal_connect(m_nativeWidget.get(), "move-cursor", G_CALLBACK(moveCursorCallback), this);
g_signal_connect(m_nativeWidget.get(), "delete-from-cursor", G_CALLBACK(deleteFromCursorCallback), this);
g_signal_connect(m_nativeWidget.get(), "toggle-overwrite", G_CALLBACK(toggleOverwriteCallback), this);
g_signal_connect(m_nativeWidget.get(), "popup-menu", G_CALLBACK(popupMenuCallback), this);
g_signal_connect(m_nativeWidget.get(), "show-help", G_CALLBACK(showHelpCallback), this);
}
WebView::~WebView()
{
}
GdkWindow* WebView::getWebViewWindow()
{
return gtk_widget_get_window(m_viewWidget);
}
void WebView::paint(GtkWidget* widget, GdkRectangle rect, cairo_t* cr)
{
m_page->drawingArea()->paint(IntRect(rect), cr);
}
void WebView::setSize(GtkWidget*, IntSize windowSize)
{
m_page->drawingArea()->setSize(windowSize, IntSize());
}
void WebView::handleKeyboardEvent(GdkEventKey* event)
{
m_page->handleKeyboardEvent(NativeWebKeyboardEvent(reinterpret_cast<GdkEvent*>(event)));
}
void WebView::handleMouseEvent(GdkEvent* event, int currentClickCount)
{
m_page->handleMouseEvent(NativeWebMouseEvent(event, currentClickCount));
}
void WebView::handleWheelEvent(GdkEventScroll* event)
{
m_page->handleWheelEvent(WebEventFactory::createWebWheelEvent(event));
}
void WebView::getEditorCommandsForKeyEvent(const NativeWebKeyboardEvent& event, Vector<WTF::String>& commandList)
{
m_pendingEditorCommands.clear();
#ifdef GTK_API_VERSION_2
gtk_bindings_activate_event(GTK_OBJECT(m_nativeWidget.get()), const_cast<GdkEventKey*>(&event.nativeEvent()->key));
#else
gtk_bindings_activate_event(G_OBJECT(m_nativeWidget.get()), const_cast<GdkEventKey*>(&event.nativeEvent()->key));
#endif
if (m_pendingEditorCommands.isEmpty()) {
commandList.append(m_pendingEditorCommands);
return;
}
DEFINE_STATIC_LOCAL(IntConstCharHashMap, keyDownCommandsMap, ());
DEFINE_STATIC_LOCAL(IntConstCharHashMap, keyPressCommandsMap, ());
if (keyDownCommandsMap.isEmpty()) {
for (unsigned i = 0; i < G_N_ELEMENTS(keyDownEntries); i++)
keyDownCommandsMap.set(keyDownEntries[i].modifiers << 16 | keyDownEntries[i].virtualKey, keyDownEntries[i].name);
for (unsigned i = 0; i < G_N_ELEMENTS(keyPressEntries); i++)
keyPressCommandsMap.set(keyPressEntries[i].modifiers << 16 | keyPressEntries[i].charCode, keyPressEntries[i].name);
}
unsigned modifiers = 0;
if (event.shiftKey())
modifiers |= ShiftKey;
if (event.altKey())
modifiers |= AltKey;
if (event.controlKey())
modifiers |= CtrlKey;
// For keypress events, we want charCode(), but keyCode() does that.
int mapKey = modifiers << 16 | event.nativeVirtualKeyCode();
if (mapKey) {
HashMap<int, const char*>* commandMap = event.type() == WebEvent::KeyDown ?
&keyDownCommandsMap : &keyPressCommandsMap;
if (const char* commandString = commandMap->get(mapKey))
m_pendingEditorCommands.append(commandString);
}
commandList.append(m_pendingEditorCommands);
}
bool WebView::isActive()
{
return m_isPageActive;
}
void WebView::close()
{
m_page->close();
}
// PageClient's pure virtual functions
PassOwnPtr<DrawingAreaProxy> WebView::createDrawingAreaProxy()
{
return ChunkedUpdateDrawingAreaProxy::create(this, m_page.get());
}
void WebView::setViewNeedsDisplay(const WebCore::IntRect&)
{
notImplemented();
}
void WebView::displayView()
{
notImplemented();
}
void WebView::scrollView(const WebCore::IntRect& scrollRect, const WebCore::IntSize& scrollOffset)
{
notImplemented();
}
WebCore::IntSize WebView::viewSize()
{
GtkAllocation allocation;
gtk_widget_get_allocation(m_viewWidget, &allocation);
return IntSize(allocation.width, allocation.height);
}
bool WebView::isViewWindowActive()
{
notImplemented();
return true;
}
bool WebView::isViewFocused()
{
notImplemented();
return true;
}
bool WebView::isViewVisible()
{
notImplemented();
return true;
}
bool WebView::isViewInWindow()
{
notImplemented();
return true;
}
void WebView::WebView::processDidCrash()
{
notImplemented();
}
void WebView::didRelaunchProcess()
{
notImplemented();
}
void WebView::takeFocus(bool)
{
notImplemented();
}
void WebView::toolTipChanged(const String&, const String&)
{
notImplemented();
}
void WebView::setCursor(const Cursor& cursor)
{
// [GTK] Widget::setCursor() gets called frequently
// http://bugs.webkit.org/show_bug.cgi?id=16388
// Setting the cursor may be an expensive operation in some backends,
// so don't re-set the cursor if it's already set to the target value.
GdkWindow* window = gtk_widget_get_window(m_viewWidget);
GdkCursor* currentCursor = gdk_window_get_cursor(window);
GdkCursor* newCursor = cursor.platformCursor().get();
if (currentCursor != newCursor)
gdk_window_set_cursor(window, newCursor);
}
void WebView::setViewportArguments(const WebCore::ViewportArguments&)
{
notImplemented();
}
void WebView::registerEditCommand(PassRefPtr<WebEditCommandProxy>, WebPageProxy::UndoOrRedo)
{
notImplemented();
}
void WebView::clearAllEditCommands()
{
notImplemented();
}
bool WebView::canUndoRedo(WebPageProxy::UndoOrRedo)
{
notImplemented();
return false;
}
void WebView::executeUndoRedo(WebPageProxy::UndoOrRedo)
{
notImplemented();
}
FloatRect WebView::convertToDeviceSpace(const FloatRect& viewRect)
{
notImplemented();
return viewRect;
}
FloatRect WebView::convertToUserSpace(const FloatRect& viewRect)
{
notImplemented();
return viewRect;
}
IntRect WebView::windowToScreen(const IntRect& rect)
{
notImplemented();
return IntRect();
}
void WebView::doneWithKeyEvent(const NativeWebKeyboardEvent&, bool wasEventHandled)
{
notImplemented();
}
void WebView::didNotHandleKeyEvent(const NativeWebKeyboardEvent& event)
{
notImplemented();
}
PassRefPtr<WebPopupMenuProxy> WebView::createPopupMenuProxy(WebPageProxy*)
{
notImplemented();
return 0;
}
PassRefPtr<WebContextMenuProxy> WebView::createContextMenuProxy(WebPageProxy*)
{
notImplemented();
return 0;
}
void WebView::setFindIndicator(PassRefPtr<FindIndicator>, bool fadeOut)
{
notImplemented();
}
#if USE(ACCELERATED_COMPOSITING)
void WebView::pageDidEnterAcceleratedCompositing()
{
notImplemented();
}
void WebView::pageDidLeaveAcceleratedCompositing()
{
notImplemented();
}
#endif // USE(ACCELERATED_COMPOSITING)
void WebView::didCommitLoadForMainFrame(bool useCustomRepresentation)
{
}
void WebView::didFinishLoadingDataForCustomRepresentation(const String& suggestedFilename, const CoreIPC::DataReference&)
{
}
double WebView::customRepresentationZoomFactor()
{
notImplemented();
return 0;
}
void WebView::setCustomRepresentationZoomFactor(double)
{
notImplemented();
}
void WebView::pageClosed()
{
notImplemented();
}
void WebView::didChangeScrollbarsForMainFrame() const
{
}
void WebView::flashBackingStoreUpdates(const Vector<IntRect>&)
{
notImplemented();
}
void WebView::findStringInCustomRepresentation(const String&, FindOptions, unsigned)
{
notImplemented();
}
void WebView::countStringMatchesInCustomRepresentation(const String&, FindOptions, unsigned)
{
notImplemented();
}
} // namespace WebKit