diff --git a/flutter_inappwebview_windows/windows/CMakeLists.txt b/flutter_inappwebview_windows/windows/CMakeLists.txt index 3842ad86c..a5cee625e 100644 --- a/flutter_inappwebview_windows/windows/CMakeLists.txt +++ b/flutter_inappwebview_windows/windows/CMakeLists.txt @@ -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" ) diff --git a/flutter_inappwebview_windows/windows/custom_platform_view/webview_drop_target.cc b/flutter_inappwebview_windows/windows/custom_platform_view/webview_drop_target.cc index a2859c4a2..1c01f0ca8 100644 --- a/flutter_inappwebview_windows/windows/custom_platform_view/webview_drop_target.cc +++ b/flutter_inappwebview_windows/windows/custom_platform_view/webview_drop_target.cc @@ -1,10 +1,13 @@ #include "webview_drop_target.h" #include +#include #include +#include #include "../in_app_webview/in_app_webview.h" #include "../utils/log.h" +#include "../utils/strconv.h" namespace flutter_inappwebview_plugin { @@ -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 dataObjectFilePaths(IDataObject* dataObject) + { + std::vector 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(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) @@ -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; } } @@ -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. @@ -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(screenPoint.x - p0.x) * client.right / (p1.x - p0.x); + *y = static_cast(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(screenPoint.x - p0.x) * - client.right / (p1.x - p0.x); - const auto localY = static_cast(screenPoint.y - p0.y) * - client.bottom / (p1.y - p0.y); for (const auto webView : webViews_) { const auto offset = webView->widgetOffset(); @@ -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(), 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_) { @@ -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); } @@ -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); @@ -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; @@ -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; diff --git a/flutter_inappwebview_windows/windows/custom_platform_view/webview_drop_target.h b/flutter_inappwebview_windows/windows/custom_platform_view/webview_drop_target.h index c71c03d21..7567d35f9 100644 --- a/flutter_inappwebview_windows/windows/custom_platform_view/webview_drop_target.h +++ b/flutter_inappwebview_windows/windows/custom_platform_view/webview_drop_target.h @@ -2,6 +2,8 @@ #define FLUTTER_INAPPWEBVIEW_PLUGIN_WEBVIEW_DROP_TARGET_H_ #include +#include +#include #include namespace flutter_inappwebview_plugin @@ -23,6 +25,11 @@ 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& 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); @@ -30,6 +37,19 @@ namespace flutter_inappwebview_plugin // 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; @@ -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 @@ -60,6 +90,8 @@ namespace flutter_inappwebview_plugin InAppWebView* currentWebView_ = nullptr; IDataObject* currentDataObject_ = nullptr; bool currentDragHasFiles_ = false; + FileDropSink fileDropSink_; + bool fileDropAccepted_ = false; }; } diff --git a/flutter_inappwebview_windows/windows/file_drop/file_drop_manager.cpp b/flutter_inappwebview_windows/windows/file_drop/file_drop_manager.cpp new file mode 100644 index 000000000..bc40cf720 --- /dev/null +++ b/flutter_inappwebview_windows/windows/file_drop/file_drop_manager.cpp @@ -0,0 +1,76 @@ +#include "file_drop_manager.h" + +#include "../custom_platform_view/webview_drop_target.h" +#include "../utils/flutter.h" + +namespace flutter_inappwebview_plugin +{ + FileDropManager::FileDropManager(const FlutterInappwebviewWindowsPlugin* plugin) + : plugin(plugin), ChannelDelegate(plugin->registrar->messenger(), FileDropManager::METHOD_CHANNEL_NAME_PREFIX) + {} + + HWND FileDropManager::flutterViewHwnd() const + { + const auto view = plugin && plugin->registrar ? plugin->registrar->GetView() : nullptr; + return view ? view->GetNativeWindow() : nullptr; + } + + void FileDropManager::setEnabled(bool enabled) + { + const auto hwnd = flutterViewHwnd(); + if (!hwnd || enabled == enabled_) { + return; + } + enabled_ = enabled; + if (!enabled) { + WebViewDropTarget::ClearFileDropSink(hwnd); + return; + } + WebViewDropTarget::SetFileDropSink(hwnd, + [this](const std::string& event, const std::vector& paths, + double x, double y) + { + if (channel == nullptr) { + return; + } + channel->InvokeMethod("onFileDrop", std::make_unique( + flutter::EncodableMap{ + {make_fl_value("event"), make_fl_value(event)}, + {make_fl_value("paths"), make_fl_value(paths)}, + {make_fl_value("x"), make_fl_value(x)}, + {make_fl_value("y"), make_fl_value(y)}, + })); + }); + } + + void FileDropManager::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) + { + auto* arguments = std::get_if(method_call.arguments()); + if (method_call.method_name().compare("setEnabled") == 0) { + setEnabled(arguments && get_fl_map_value(*arguments, "enabled", false)); + result->Success(); + return; + } + if (method_call.method_name().compare("setAccepted") == 0) { + if (const auto hwnd = flutterViewHwnd()) { + WebViewDropTarget::SetFileDropAccepted(hwnd, + arguments && get_fl_map_value(*arguments, "accepted", false)); + } + result->Success(); + return; + } + result->NotImplemented(); + } + + FileDropManager::~FileDropManager() + { + if (enabled_) { + if (const auto hwnd = flutterViewHwnd()) { + WebViewDropTarget::ClearFileDropSink(hwnd); + } + } + plugin = nullptr; + } +} diff --git a/flutter_inappwebview_windows/windows/file_drop/file_drop_manager.h b/flutter_inappwebview_windows/windows/file_drop/file_drop_manager.h new file mode 100644 index 000000000..de34ed784 --- /dev/null +++ b/flutter_inappwebview_windows/windows/file_drop/file_drop_manager.h @@ -0,0 +1,39 @@ +#ifndef FLUTTER_INAPPWEBVIEW_PLUGIN_FILE_DROP_MANAGER_H_ +#define FLUTTER_INAPPWEBVIEW_PLUGIN_FILE_DROP_MANAGER_H_ + +#include +#include +#include +#include + +#include "../flutter_inappwebview_windows_plugin.h" +#include "../types/channel_delegate.h" + +namespace flutter_inappwebview_plugin +{ + // Reports OS file drags over the Flutter window to the host app. The webview + // drop target owns the window's only IDropTarget, so a host that needs file + // drops has to receive them from here rather than registering its own. + class FileDropManager : public ChannelDelegate + { + public: + static inline const std::string METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_filedrop"; + + const FlutterInappwebviewWindowsPlugin* plugin; + + FileDropManager(const FlutterInappwebviewWindowsPlugin* plugin); + ~FileDropManager(); + + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) override; + + private: + void setEnabled(bool enabled); + HWND flutterViewHwnd() const; + + bool enabled_ = false; + }; +} + +#endif //FLUTTER_INAPPWEBVIEW_PLUGIN_FILE_DROP_MANAGER_H_ diff --git a/flutter_inappwebview_windows/windows/flutter_inappwebview_windows_plugin.cpp b/flutter_inappwebview_windows/windows/flutter_inappwebview_windows_plugin.cpp index 9864d3a0e..92d649ac7 100644 --- a/flutter_inappwebview_windows/windows/flutter_inappwebview_windows_plugin.cpp +++ b/flutter_inappwebview_windows/windows/flutter_inappwebview_windows_plugin.cpp @@ -3,6 +3,7 @@ #include #include "cookie_manager.h" +#include "file_drop/file_drop_manager.h" #include "headless_in_app_webview/headless_in_app_webview_manager.h" #include "in_app_browser/in_app_browser_manager.h" #include "in_app_webview/in_app_webview_manager.h" @@ -35,6 +36,7 @@ namespace flutter_inappwebview_plugin headlessInAppWebViewManager = std::make_unique(this); cookieManager = std::make_unique(this); platformUtil = std::make_unique(this); + fileDropManager = std::make_unique(this); window_proc_id = registrar->RegisterTopLevelWindowProcDelegate( [this](HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) @@ -54,6 +56,7 @@ namespace flutter_inappwebview_plugin headlessInAppWebViewManager = nullptr; cookieManager = nullptr; platformUtil = nullptr; + fileDropManager = nullptr; } diff --git a/flutter_inappwebview_windows/windows/flutter_inappwebview_windows_plugin.h b/flutter_inappwebview_windows/windows/flutter_inappwebview_windows_plugin.h index b195a5b34..f8e0382a8 100644 --- a/flutter_inappwebview_windows/windows/flutter_inappwebview_windows_plugin.h +++ b/flutter_inappwebview_windows/windows/flutter_inappwebview_windows_plugin.h @@ -11,6 +11,7 @@ namespace flutter_inappwebview_plugin class HeadlessInAppWebViewManager; class CookieManager; class PlatformUtil; + class FileDropManager; class FlutterInappwebviewWindowsPlugin : public flutter::Plugin { public: @@ -21,6 +22,7 @@ namespace flutter_inappwebview_plugin std::unique_ptr headlessInAppWebViewManager; std::unique_ptr cookieManager; std::unique_ptr platformUtil; + std::unique_ptr fileDropManager; static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar);