Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions flutter_inappwebview_windows/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ list(APPEND PLUGIN_SOURCES
"cookie_manager.cpp"
"cookie_manager.h"
"platform_util.cpp"
"file_drop/file_drop_manager.h"
"file_drop/file_drop_manager.cpp"
"platform_util.h"
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#include "webview_drop_target.h"

#include <algorithm>
#include <cstring>
#include <map>
#include <shellapi.h>

#include "../in_app_webview/in_app_webview.h"
#include "../utils/log.h"
#include "../utils/strconv.h"

namespace flutter_inappwebview_plugin
{
Expand Down Expand Up @@ -37,39 +40,129 @@ namespace flutter_inappwebview_plugin
return dataObject->QueryGetData(&format) == S_OK;
}

// Real paths of a dragged file set. Virtual files (zip entries, Outlook
// attachments) carry no path and are reported as an empty list.
static std::vector<std::string> dataObjectFilePaths(IDataObject* dataObject)
{
std::vector<std::string> paths;
if (!dataObject) {
return paths;
}
FORMATETC format = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM medium = {};
if (dataObject->GetData(&format, &medium) != S_OK) {
return paths;
}
if (auto drop = static_cast<HDROP>(GlobalLock(medium.hGlobal))) {
const auto count = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0);
for (UINT i = 0; i < count; i++) {
const auto length = DragQueryFileW(drop, i, nullptr, 0);
if (length == 0) {
continue;
}
std::wstring path(length, L'\0');
if (DragQueryFileW(drop, i, path.data(), length + 1) > 0) {
paths.push_back(wide_to_utf8(path));
}
}
GlobalUnlock(medium.hGlobal);
}
ReleaseStgMedium(&medium);
return paths;
}

WebViewDropTarget::WebViewDropTarget(HWND flutterViewHwnd, bool oleInitialized)
: flutterViewHwnd_(flutterViewHwnd), oleInitialized_(oleInitialized)
{}

WebViewDropTarget* WebViewDropTarget::acquire(HWND flutterViewHwnd)
{
auto it = g_targets.find(flutterViewHwnd);
if (it != g_targets.end()) {
return it->second;
}
// RegisterDragDrop needs an STA. OleInitialize returns S_FALSE when OLE
// is already initialized on this thread (still balanced by a matching
// OleUninitialize); RPC_E_CHANGED_MODE means an MTA is active and drag
// and drop cannot work here.
const auto oleHr = OleInitialize(nullptr);
const bool oleInitialized = SUCCEEDED(oleHr);
auto target = new WebViewDropTarget(flutterViewHwnd, oleInitialized);
const auto hr = RegisterDragDrop(flutterViewHwnd, target);
if (FAILED(hr)) {
// Another plugin may already own the window's drop target
// (DRAGDROP_E_ALREADYREGISTERED) - drag and drop into webviews is
// unavailable then, but nothing else breaks.
failedLog(hr);
if (oleInitialized) {
OleUninitialize();
}
target->Release();
return nullptr;
}
return g_targets.emplace(flutterViewHwnd, target).first->second;
}

void WebViewDropTarget::releaseIfUnused(HWND flutterViewHwnd)
{
const auto it = g_targets.find(flutterViewHwnd);
if (it == g_targets.end()) {
return;
}
auto target = it->second;
if (!target->webViews_.empty() || target->fileDropSink_) {
return;
}
RevokeDragDrop(target->flutterViewHwnd_);
if (target->currentDataObject_) {
target->currentDataObject_->Release();
target->currentDataObject_ = nullptr;
}
const bool oleInitialized = target->oleInitialized_;
target->Release();
if (oleInitialized) {
OleUninitialize();
}
g_targets.erase(it);
}

void WebViewDropTarget::RegisterWebView(HWND flutterViewHwnd, InAppWebView* webView)
{
if (!flutterViewHwnd || !webView) {
return;
}
auto it = g_targets.find(flutterViewHwnd);
if (auto target = acquire(flutterViewHwnd)) {
target->webViews_.push_back(webView);
}
}

void WebViewDropTarget::SetFileDropSink(HWND flutterViewHwnd, FileDropSink sink)
{
if (!flutterViewHwnd || !sink) {
return;
}
if (auto target = acquire(flutterViewHwnd)) {
target->fileDropSink_ = std::move(sink);
}
}

void WebViewDropTarget::ClearFileDropSink(HWND flutterViewHwnd)
{
const auto it = g_targets.find(flutterViewHwnd);
if (it == g_targets.end()) {
// RegisterDragDrop needs an STA. OleInitialize returns S_FALSE when OLE
// is already initialized on this thread (still balanced by a matching
// OleUninitialize); RPC_E_CHANGED_MODE means an MTA is active and drag
// and drop cannot work here.
const auto oleHr = OleInitialize(nullptr);
const bool oleInitialized = SUCCEEDED(oleHr);
auto target = new WebViewDropTarget(flutterViewHwnd, oleInitialized);
const auto hr = RegisterDragDrop(flutterViewHwnd, target);
if (FAILED(hr)) {
// Another plugin may already own the window's drop target
// (DRAGDROP_E_ALREADYREGISTERED) - drag and drop into webviews is
// unavailable then, but nothing else breaks.
failedLog(hr);
if (oleInitialized) {
OleUninitialize();
}
target->Release();
return;
}
it = g_targets.emplace(flutterViewHwnd, target).first;
return;
}
it->second->fileDropSink_ = nullptr;
it->second->fileDropAccepted_ = false;
releaseIfUnused(flutterViewHwnd);
}

void WebViewDropTarget::SetFileDropAccepted(HWND flutterViewHwnd, bool accepted)
{
const auto it = g_targets.find(flutterViewHwnd);
if (it != g_targets.end()) {
it->second->fileDropAccepted_ = accepted;
}
it->second->webViews_.push_back(webView);
}

void WebViewDropTarget::UnregisterWebView(InAppWebView* webView)
Expand All @@ -85,19 +178,7 @@ namespace flutter_inappwebview_plugin
if (target->currentWebView_ == webView) {
target->currentWebView_ = nullptr;
}
if (webViews.empty()) {
RevokeDragDrop(target->flutterViewHwnd_);
if (target->currentDataObject_) {
target->currentDataObject_->Release();
target->currentDataObject_ = nullptr;
}
const bool oleInitialized = target->oleInitialized_;
target->Release();
if (oleInitialized) {
OleUninitialize();
}
g_targets.erase(it);
}
releaseIfUnused(target->flutterViewHwnd_);
return;
}
}
Expand Down Expand Up @@ -127,12 +208,11 @@ namespace flutter_inappwebview_plugin
return count;
}

InAppWebView* WebViewDropTarget::webViewAt(POINTL screenPoint,
POINT* webViewPoint) const
bool WebViewDropTarget::toClient(POINTL screenPoint, double* x, double* y) const
{
RECT client;
if (!GetClientRect(flutterViewHwnd_, &client) || client.right == 0) {
return nullptr;
return false;
}
// Mapping through both client corners stays correct on RTL-mirrored
// windows (WS_EX_LAYOUTRTL), where ClientToScreen(0,0) is the top-RIGHT.
Expand All @@ -141,12 +221,21 @@ namespace flutter_inappwebview_plugin
ClientToScreen(flutterViewHwnd_, &p0);
ClientToScreen(flutterViewHwnd_, &p1);
if (p1.x == p0.x || p1.y == p0.y) {
return false;
}
*x = static_cast<double>(screenPoint.x - p0.x) * client.right / (p1.x - p0.x);
*y = static_cast<double>(screenPoint.y - p0.y) * client.bottom / (p1.y - p0.y);
return true;
}

InAppWebView* WebViewDropTarget::webViewAt(POINTL screenPoint,
POINT* webViewPoint) const
{
double localX = 0;
double localY = 0;
if (!toClient(screenPoint, &localX, &localY)) {
return nullptr;
}
const auto localX = static_cast<double>(screenPoint.x - p0.x) *
client.right / (p1.x - p0.x);
const auto localY = static_cast<double>(screenPoint.y - p0.y) *
client.bottom / (p1.y - p0.y);

for (const auto webView : webViews_) {
const auto offset = webView->widgetOffset();
Expand All @@ -161,6 +250,28 @@ namespace flutter_inappwebview_plugin
return nullptr;
}

HRESULT WebViewDropTarget::reportFileDrag(const char* event,
IDataObject* dataObject, POINTL point, DWORD* effect)
{
// Files never reach WebView2: a file dropped on browser UI (e.g. print
// preview) bypasses every page-level and navigation guard.
forwardLeave();
if (!fileDropSink_) {
*effect = DROPEFFECT_NONE;
return S_OK;
}
double x = 0;
double y = 0;
toClient(point, &x, &y);
const bool needsPaths = strcmp(event, "over") != 0;
fileDropSink_(event, needsPaths ? dataObjectFilePaths(dataObject)
: std::vector<std::string>(), x, y);
// The host answers asynchronously, so this reflects the previous report -
// one drag event of lag, which the continuous DragOver stream absorbs.
*effect = fileDropAccepted_ ? DROPEFFECT_COPY : DROPEFFECT_NONE;
return S_OK;
}

void WebViewDropTarget::forwardLeave()
{
if (currentWebView_) {
Expand All @@ -185,6 +296,10 @@ namespace flutter_inappwebview_plugin
}
currentDragHasFiles_ = dataObjectContainsFiles(dataObject);
currentWebView_ = nullptr;
if (currentDragHasFiles_) {
fileDropAccepted_ = false;
return reportFileDrag("enter", dataObject, point, effect);
}
return DragOver(keyState, point, effect);
}

Expand All @@ -196,9 +311,7 @@ namespace flutter_inappwebview_plugin
// and OLE would never call Drop.
const DWORD allowedEffects = *effect;
if (currentDragHasFiles_) {
forwardLeave();
*effect = DROPEFFECT_NONE;
return S_OK;
return reportFileDrag("over", currentDataObject_, point, effect);
}
POINT webViewPoint;
const auto webView = webViewAt(point, &webViewPoint);
Expand Down Expand Up @@ -228,7 +341,11 @@ namespace flutter_inappwebview_plugin
HRESULT WebViewDropTarget::DragLeave()
{
forwardLeave();
if (currentDragHasFiles_ && fileDropSink_) {
fileDropSink_("leave", {}, 0, 0);
}
currentDragHasFiles_ = false;
fileDropAccepted_ = false;
if (currentDataObject_) {
currentDataObject_->Release();
currentDataObject_ = nullptr;
Expand All @@ -240,14 +357,13 @@ namespace flutter_inappwebview_plugin
POINTL point, DWORD* effect)
{
if (currentDragHasFiles_ || dataObjectContainsFiles(dataObject)) {
forwardLeave();
const auto hr = reportFileDrag("drop", dataObject, point, effect);
currentDragHasFiles_ = false;
if (currentDataObject_) {
currentDataObject_->Release();
currentDataObject_ = nullptr;
}
*effect = DROPEFFECT_NONE;
return S_OK;
return hr;
}
const DWORD allowedEffects = *effect;
POINT webViewPoint;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#define FLUTTER_INAPPWEBVIEW_PLUGIN_WEBVIEW_DROP_TARGET_H_

#include <ole2.h>
#include <functional>
#include <string>
#include <vector>

namespace flutter_inappwebview_plugin
Expand All @@ -23,13 +25,31 @@ namespace flutter_inappwebview_plugin
class WebViewDropTarget : public IDropTarget
{
public:
// Reported to the host for a drag carrying OS files. [event] is one of
// "enter" / "over" / "leave" / "drop"; [x],[y] are client coordinates.
using FileDropSink = std::function<void(const std::string& event,
const std::vector<std::string>& paths, double x, double y)>;

// Registers [webView] for drag routing under [flutterViewHwnd], creating
// and installing a drop target for that window on first use.
static void RegisterWebView(HWND flutterViewHwnd, InAppWebView* webView);
// Removes [webView] from whichever window owns it; revokes and destroys
// that window's drop target once it has no webviews left.
static void UnregisterWebView(InAppWebView* webView);

// Claims OS file drags on [flutterViewHwnd] for the host. Only one IDropTarget
// may exist per window, so the host cannot register its own alongside this
// one - it routes them through here instead. Keeps the target alive even
// while no webview exists.
static void SetFileDropSink(HWND flutterViewHwnd, FileDropSink sink);
// Stops reporting file drags, restoring the plain-refusal behaviour.
static void ClearFileDropSink(HWND flutterViewHwnd);

// The host's answer to the last reported drag: whether it would accept a
// drop here. Drives the drag cursor, so the host must answer while the
// drag is still moving. Resets to refused on every new drag.
static void SetFileDropAccepted(HWND flutterViewHwnd, bool accepted);

// IUnknown
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override;
ULONG STDMETHODCALLTYPE AddRef() override;
Expand All @@ -48,8 +68,18 @@ namespace flutter_inappwebview_plugin
explicit WebViewDropTarget(HWND flutterViewHwnd, bool oleInitialized);
~WebViewDropTarget() = default;

// Creates and registers the window's target on first use, or returns the
// existing one. Null when RegisterDragDrop failed.
static WebViewDropTarget* acquire(HWND flutterViewHwnd);
// Revokes and destroys the window's target once nothing needs it.
static void releaseIfUnused(HWND flutterViewHwnd);

bool toClient(POINTL screenPoint, double* x, double* y) const;
InAppWebView* webViewAt(POINTL screenPoint, POINT* webViewPoint) const;
void forwardLeave();
// Emits [event] to the file drop sink; paths are read for "enter"/"drop" only.
HRESULT reportFileDrag(const char* event, IDataObject* dataObject,
POINTL point, DWORD* effect);

HWND flutterViewHwnd_;
// Whether this target's own OleInitialize succeeded and must be balanced
Expand All @@ -60,6 +90,8 @@ namespace flutter_inappwebview_plugin
InAppWebView* currentWebView_ = nullptr;
IDataObject* currentDataObject_ = nullptr;
bool currentDragHasFiles_ = false;
FileDropSink fileDropSink_;
bool fileDropAccepted_ = false;
};
}

Expand Down
Loading
Loading