Files
stransky f221d1f0da Bug 2047140 [Linux] Keep D&D source GtkWidget live until D&D operations is finished r=emilio
Recently we delete source GtkWidget while the D&D is still live. That may lead to various issues/crashes as
Gtk still renders and uses the widget surface etc.

The early delete was introduced as a hack for mem leak in Bug 219521. Let's fix that better.

Also fix logging build errors.

Differential Revision: https://phabricator.services.mozilla.com/D312259
2026-07-22 11:44:53 +00:00

1227 lines
42 KiB
C++

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsDragService.h"
#include "nsDragServiceGtk.h"
#include "nsDragSessionSource.h"
#ifdef MOZ_WAYLAND
# include "nsDragServiceWayland.h"
#endif
#include <dlfcn.h>
#include <gtk/gtk.h>
#include <mutex>
#include "mozilla/AutoRestore.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPrefs_widget.h"
#include "mozilla/WidgetUtils.h"
#include "mozilla/WidgetUtilsGtk.h"
#include "mozilla/widget/nsGtkHtmlUtils.h"
#include "nsArrayUtils.h"
#include "nsGtkKeyUtils.h"
#include "nsIFileURL.h"
#include "nsPrimitiveHelpers.h"
#include "nsString.h"
#include "nsStringStream.h"
#include "nsWindow.h"
using namespace mozilla;
using namespace mozilla::widget;
using namespace mozilla::gfx;
#undef LOGDRAG
#undef LOGDRAGSTATIC
#ifdef MOZ_LOGGING
extern mozilla::LazyLogModule gWidgetDragLog;
# define LOGDRAG(str, ...) \
MOZ_LOG( \
gWidgetDragLog, mozilla::LogLevel::Debug, \
("[D %d]%s %*s" str, nsDragSession::GetLoopDepth(), \
GetDebugTag().get(), \
nsDragSession::GetLoopDepth() > 1 ? nsDragSession::GetLoopDepth() * 2 \
: 0, \
"", ##__VA_ARGS__))
# define LOGDRAGSTATIC(str, ...) \
MOZ_LOG(gWidgetDragLog, mozilla::LogLevel::Debug, (str, ##__VA_ARGS__))
#else
# define LOGDRAG(...)
# define LOGDRAGSTATIC(...)
#endif
const char nsDragSession::gMozUrlType[] = "_NETSCAPE_URL";
const char nsDragSession::gMimeListType[] =
"application/x-moz-internal-item-list";
const char nsDragSession::gTextUriListType[] = "text/uri-list";
const char nsDragSession::gTextPlainUTF8Type[] = "text/plain; charset=utf-8";
const char nsDragSession::gXdndDirectSaveType[] = "XdndDirectSave0";
const char nsDragSession::gTabDropType[] = "application/x-moz-tabbrowser-tab";
const char nsDragSession::gPortalFile[] = "application/vnd.portal.files";
const char nsDragSession::gPortalFileTransfer[] =
"application/vnd.portal.filetransfer";
const char nsDragSession::gUTF8STRINGType[] = "UTF8_STRING";
const char nsDragSession::gSTRINGType[] = "STRING";
GdkAtom nsDragSession::sJPEGImageMimeAtom;
GdkAtom nsDragSession::sJPGImageMimeAtom;
GdkAtom nsDragSession::sPNGImageMimeAtom;
GdkAtom nsDragSession::sGIFImageMimeAtom;
GdkAtom nsDragSession::sCustomTypesMimeAtom;
GdkAtom nsDragSession::sURLMimeAtom;
GdkAtom nsDragSession::sRTFMimeAtom;
GdkAtom nsDragSession::sTextMimeAtom;
GdkAtom nsDragSession::sMozUrlTypeAtom;
GdkAtom nsDragSession::sMimeListTypeAtom;
GdkAtom nsDragSession::sTextUriListTypeAtom;
GdkAtom nsDragSession::sTextPlainUTF8TypeAtom;
GdkAtom nsDragSession::sXdndDirectSaveTypeAtom;
GdkAtom nsDragSession::sTabDropTypeAtom;
GdkAtom nsDragSession::sFileMimeAtom;
GdkAtom nsDragSession::sPortalFileAtom;
GdkAtom nsDragSession::sPortalFileTransferAtom;
GdkAtom nsDragSession::sFilePromiseURLMimeAtom;
GdkAtom nsDragSession::sFilePromiseMimeAtom;
GdkAtom nsDragSession::sNativeImageMimeAtom;
GdkAtom nsDragSession::sUTF8STRINGMimeAtom;
GdkAtom nsDragSession::sSTRINGMimeAtom;
static nsString UTF8ToNewString(const char* aUTF8, uint32_t aUTF8DataLen = 0) {
nsDependentCSubstring utf8(aUTF8,
aUTF8DataLen ? aUTF8DataLen : strlen(aUTF8));
nsString ret;
uint32_t convertedTextLen = 0;
char16_t* convertedText = UTF8ToNewUnicode(utf8, &convertedTextLen);
if (!convertedText) {
return ret;
}
convertedTextLen *= 2;
// Strip CRLF which might be present in the string.
// Not sure where it's added, maybe Gtk?
nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(
/* aIsSingleByteChars */ false, (void**)&convertedText,
(int32_t*)&convertedTextLen);
ret.Adopt(convertedText, convertedTextLen / 2);
return ret;
}
static bool GetFileFromUri(const nsCString& aUri, nsCOMPtr<nsIFile>& aFile) {
nsresult rv;
nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
nsCOMPtr<nsIURI> fileURI;
if (NS_SUCCEEDED(
ioService->NewURI(aUri, nullptr, nullptr, getter_AddRefs(fileURI)))) {
nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(fileURI, &rv);
if (NS_SUCCEEDED(rv)) {
if (NS_SUCCEEDED(fileURL->GetFile(getter_AddRefs(aFile)))) {
return true;
}
}
}
LOGDRAGSTATIC("GetFileFromUri() failed");
return false;
}
bool DragData::IsImageFlavor() const {
return mDataFlavor == nsDragSession::sJPEGImageMimeAtom ||
mDataFlavor == nsDragSession::sJPGImageMimeAtom ||
mDataFlavor == nsDragSession::sPNGImageMimeAtom ||
mDataFlavor == nsDragSession::sGIFImageMimeAtom;
}
bool DragData::IsFileFlavor() const {
return mDataFlavor == nsDragSession::sFileMimeAtom ||
mDataFlavor == nsDragSession::sPortalFileAtom ||
mDataFlavor == nsDragSession::sPortalFileTransferAtom;
}
bool DragData::IsTextFlavor() const {
return nsDragSession::IsTextFlavor(mDataFlavor);
}
bool DragData::IsURIFlavor() const {
// We support x-moz-url URL MIME type only
return mDataFlavor == nsDragSession::sURLMimeAtom;
}
int DragData::GetURIsNum() const {
int urlNum = 1;
if (mDragUris) {
urlNum = g_strv_length(mDragUris.get());
} else if (IsURIFlavor()) {
urlNum = mUris.Length();
}
LOGDRAGSTATIC("DragData::GetURIsNum() %d", urlNum);
return urlNum;
}
bool DragData::Export(nsITransferable* aTransferable, uint32_t aItemIndex) {
GUniquePtr<gchar> flavorName(gdk_atom_name(mDataFlavor));
LOGDRAGSTATIC("DragData::Export() MIME %s index %d", flavorName.get(),
aItemIndex);
if (IsFileFlavor()) {
MOZ_ASSERT(mDragUris.get());
char** list = mDragUris.get();
if (aItemIndex >= g_strv_length(list)) {
NS_WARNING(
nsPrintfCString(
"DragData::Export(): Index %d is overflow file list len %d",
aItemIndex, g_strv_length(list))
.get());
return false;
}
bool fileExists = false;
nsCOMPtr<nsIFile> file;
if (GetFileFromUri(nsDependentCString(list[aItemIndex]), file)) {
file->Exists(&fileExists);
}
if (!fileExists) {
LOGDRAGSTATIC(" uri %s not reachable/not found\n", list[aItemIndex]);
return false;
}
LOGDRAGSTATIC(" export file %s (flavor: %s) as %s", list[aItemIndex],
flavorName.get(), kFileMime);
aTransferable->SetTransferData(kFileMime, file);
return true;
}
if (IsURIFlavor()) {
MOZ_ASSERT(mAsURIData);
if (aItemIndex >= mUris.Length()) {
NS_WARNING(nsPrintfCString(
"DragData::Export(): Index %d is overflow uri list len %d",
aItemIndex, (int)mUris.Length())
.get());
return false;
}
LOGDRAGSTATIC("%d URI:\n%s", (int)aItemIndex,
NS_ConvertUTF16toUTF8(mUris[aItemIndex]).get());
// put it into the transferable.
nsCOMPtr<nsISupports> genericDataWrapper;
nsPrimitiveHelpers::CreatePrimitiveForData(
nsAutoCString(kURLMime), mUris[aItemIndex].get(),
mUris[aItemIndex].Length() * 2, getter_AddRefs(genericDataWrapper));
return NS_SUCCEEDED(
aTransferable->SetTransferData(kURLMime, genericDataWrapper));
}
if (IsImageFlavor()) {
LOGDRAGSTATIC(" export image %s", flavorName.get());
nsCOMPtr<nsIInputStream> byteStream;
NS_NewByteInputStream(getter_AddRefs(byteStream),
mozilla::Span((char*)mDragData.get(), mDragDataLen),
NS_ASSIGNMENT_COPY);
return NS_SUCCEEDED(
aTransferable->SetTransferData(flavorName.get(), byteStream));
}
if (IsTextFlavor()) {
LOGDRAGSTATIC(" export text %s", kTextMime);
// We get text flavors as UTF8 but we export them as UTF16.
if (mData.IsEmpty() && mDragDataLen) {
mData = UTF8ToNewString(static_cast<const char*>(mDragData.get()),
mDragDataLen);
}
// put it into the transferable.
nsCOMPtr<nsISupports> genericDataWrapper;
nsPrimitiveHelpers::CreatePrimitiveForData(
nsAutoCString(kTextMime), mData.get(), mData.Length() * 2,
getter_AddRefs(genericDataWrapper));
return NS_SUCCEEDED(
aTransferable->SetTransferData(kTextMime, genericDataWrapper));
}
// text/html from external apps arrives as raw bytes in a charset that may
// not be UTF-16. Detect and decode it just as the clipboard code does.
if (nsDependentCString(flavorName.get()).EqualsLiteral(kHTMLMime) &&
mDragData && mDragDataLen) {
LOGDRAGSTATIC(" export HTML, decoding charset");
mozilla::Span<const char> span(static_cast<const char*>(mDragData.get()),
mDragDataLen);
nsAutoString unicodeData;
if (!mozilla::widget::DecodeHTMLData(span, unicodeData)) {
LOGDRAGSTATIC(" failed to decode HTML data");
return false;
}
nsCOMPtr<nsISupports> genericDataWrapper;
nsPrimitiveHelpers::CreatePrimitiveForData(
nsLiteralCString(kHTMLMime), (const char*)unicodeData.BeginReading(),
unicodeData.Length() * sizeof(char16_t),
getter_AddRefs(genericDataWrapper));
return NS_SUCCEEDED(
aTransferable->SetTransferData(kHTMLMime, genericDataWrapper));
}
// We export obtained data directly from Gtk. In such case only
// update line endings to DOM format.
if (!mDragDataDOMEndings &&
mDataFlavor != nsDragSession::sCustomTypesMimeAtom) {
mDragDataDOMEndings = true;
void* tmpData = mDragData.release();
nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(
mDataFlavor == nsDragSession::sRTFMimeAtom, &tmpData,
(int32_t*)&mDragDataLen);
mDragData.reset(tmpData);
}
// put it into the transferable.
nsCOMPtr<nsISupports> genericDataWrapper;
nsPrimitiveHelpers::CreatePrimitiveForData(
nsDependentCString(flavorName.get()), mDragData.get(), mDragDataLen,
getter_AddRefs(genericDataWrapper));
return NS_SUCCEEDED(
aTransferable->SetTransferData(flavorName.get(), genericDataWrapper));
}
RefPtr<DragData> DragData::ConvertToMozURL() const {
// "text/uri-list" is exported as URI mime type by Gtk, perhaps in UTF8.
// We convert it to "text/x-moz-url" which is UTF16 with line breaks.
if (mDataFlavor == nsDragSession::sTextUriListTypeAtom) {
MOZ_ASSERT(mAsURIData && mDragUris);
LOGDRAGSTATIC("ConvertToMozURL(): text/uri-list => text/x-moz-url");
RefPtr<DragData> data = new DragData(nsDragSession::sURLMimeAtom);
data->mAsURIData = true;
int len = g_strv_length(mDragUris.get());
for (int i = 0; i < len; i++) {
data->mUris.AppendElement(UTF8ToNewString(mDragUris.get()[i]));
}
return data;
}
// MozUrlType (_NETSCAPE_URL) MIME is not registered as URI MIME byt Gtk
// is it exports it as plain data. We convert it to "text/x-moz-url"
// which is UTF16 with line breaks.
if (mDataFlavor == nsDragSession::sMozUrlTypeAtom) {
MOZ_ASSERT(mDragData);
LOGDRAGSTATIC("ConvertToMozURL(): _NETSCAPE_URL => text/x-moz-url");
RefPtr<DragData> data = new DragData(nsDragSession::sURLMimeAtom);
data->mAsURIData = true;
data->mUris.AppendElement(
UTF8ToNewString((const char*)mDragData.get(), mDragDataLen));
return data;
}
LOGDRAGSTATIC("ConvertToMozURL(): failed, wrong MIME %s to convert!",
GUniquePtr<gchar>(gdk_atom_name(mDataFlavor)).get());
return nullptr;
}
RefPtr<DragData> DragData::ConvertToFile() const {
// "text/uri-list" is exported as URI mime type by Gtk, perhaps in UTF8.
// We convert it to application/x-moz-file.
if (mDataFlavor != nsDragSession::sTextUriListTypeAtom) {
return nullptr;
}
MOZ_ASSERT(mAsURIData && mDragUris);
// We can use text/uri-list directly as application/x-moz-file
return new DragData(nsDragSession::sFileMimeAtom,
GUniquePtr<char*>(g_strdupv(mDragUris.get())));
}
static int CopyURI(const nsAString& aSourceURL, nsAString& aTargetURL,
int aOffset, bool aRequestNewLine) {
int32_t uriEnd = aSourceURL.FindChar(u'\n', aOffset);
if (uriEnd == aOffset) {
return aOffset + 1;
}
if (uriEnd < 0) {
if (aRequestNewLine) {
return uriEnd;
}
// We may miss newline ending on URL title which is correct
uriEnd = aSourceURL.Length();
}
int32_t newOffset = uriEnd + 1;
if (aSourceURL[uriEnd - 1] == u'\r') {
uriEnd--;
}
nsDependentSubstring url(aSourceURL, aOffset, uriEnd - aOffset);
if (aRequestNewLine) {
url.AppendLiteral("\n");
}
aTargetURL.Append(url);
return newOffset;
}
// It holds the URLs of links followed by their titles,
// separated by a linebreak.
void DragData::ConvertToMozURIList() {
if (mDataFlavor != nsDragSession::sURLMimeAtom) {
return;
}
mAsURIData = true;
const nsDependentSubstring uris((char16_t*)mDragData.get(), mDragDataLen / 2);
LOGDRAGSTATIC("DragData::ConvertToMozURIList(), data %s",
NS_ConvertUTF16toUTF8(uris).get());
int32_t uriBegin = 0;
do {
nsAutoString uri;
// First line contains URL and is terminated by newline
if ((uriBegin = CopyURI(uris, uri, uriBegin, /* aRequestNewLine */ true)) <
0) {
break;
}
// Second line is URL title and may be terminated by newline
if ((uriBegin = CopyURI(uris, uri, uriBegin, /* aRequestNewLine */ false)) <
0) {
break;
}
LOGDRAGSTATIC(" URI: %s", NS_ConvertUTF16toUTF8(uri).get());
mUris.AppendElement(uri);
} while (uriBegin < (int32_t)uris.Length());
mDragData = nullptr;
mDragDataLen = 0;
}
DragData::DragData(GdkAtom aDataFlavor, GUniquePtr<char*> aDragUris)
: mDataFlavor(aDataFlavor),
mAsURIData(true),
mDragUris(std::move(aDragUris)) {}
bool DragData::IsDataValid() const {
if (mDragData) {
return mDragData.get() && mDragDataLen;
} else if (mDragUris) {
return !!(mDragUris.get()[0]);
} else {
return mUris.Length();
}
}
#ifdef MOZ_LOGGING
void DragData::Print() const {
if (mDragData) {
if (IsTextFlavor()) {
nsCString text((char*)mDragData.get(), mDragDataLen);
LOGDRAGSTATIC("DragData() plain data MIME: %s : %s",
GUniquePtr<gchar>(gdk_atom_name(mDataFlavor)).get(),
(char*)text.get());
}
if (IsURIFlavor()) {
nsString text((char16_t*)mDragData.get(), mDragDataLen / 2);
LOGDRAGSTATIC("DragData() plain data MIME: %s : %s",
GUniquePtr<gchar>(gdk_atom_name(mDataFlavor)).get(),
NS_ConvertUTF16toUTF8(text).get());
}
} else if (mDragUris) {
LOGDRAGSTATIC("DragData() URI MIME %s",
GUniquePtr<gchar>(gdk_atom_name(mDataFlavor)).get());
if (MOZ_LOG_TEST(gWidgetDragLog, mozilla::LogLevel::Debug)) {
int i = 0;
for (gchar** uri = mDragUris.get(); uri && *uri; uri++, i++) {
LOGDRAGSTATIC("%d URI %s", i, *uri);
}
}
} else if (mUris.Length()) {
LOGDRAGSTATIC("DragData() URI MIME: %s len %d",
GUniquePtr<gchar>(gdk_atom_name(mDataFlavor)).get(),
(int)mUris.Length());
for (size_t i = 0; i < mUris.Length(); i++) {
LOGDRAGSTATIC("%d URI:\n%s", (int)i,
NS_ConvertUTF16toUTF8(mUris[i]).get());
}
} else {
LOGDRAGSTATIC("DragData() MIME %s is missing data",
GUniquePtr<gchar>(gdk_atom_name(mDataFlavor)).get());
}
}
#endif
/* static */ int nsDragSession::sEventLoopDepth = 0;
nsDragSession::nsDragSession() {
LOGDRAG("nsDragSession::nsDragSession()");
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
sJPEGImageMimeAtom = gdk_atom_intern(kJPEGImageMime, FALSE);
sJPGImageMimeAtom = gdk_atom_intern(kJPGImageMime, FALSE);
sPNGImageMimeAtom = gdk_atom_intern(kPNGImageMime, FALSE);
sGIFImageMimeAtom = gdk_atom_intern(kGIFImageMime, FALSE);
sCustomTypesMimeAtom = gdk_atom_intern(kCustomTypesMime, FALSE);
sURLMimeAtom = gdk_atom_intern(kURLMime, FALSE);
sRTFMimeAtom = gdk_atom_intern(kRTFMime, FALSE);
sTextMimeAtom = gdk_atom_intern(kTextMime, FALSE);
sMozUrlTypeAtom = gdk_atom_intern(gMozUrlType, FALSE);
sMimeListTypeAtom = gdk_atom_intern(gMimeListType, FALSE);
sTextUriListTypeAtom = gdk_atom_intern(gTextUriListType, FALSE);
sTextPlainUTF8TypeAtom = gdk_atom_intern(gTextPlainUTF8Type, FALSE);
sXdndDirectSaveTypeAtom = gdk_atom_intern(gXdndDirectSaveType, FALSE);
sTabDropTypeAtom = gdk_atom_intern(gTabDropType, FALSE);
sFileMimeAtom = gdk_atom_intern(kFileMime, FALSE);
sPortalFileAtom = gdk_atom_intern(gPortalFile, FALSE);
sPortalFileTransferAtom = gdk_atom_intern(gPortalFileTransfer, FALSE);
sFilePromiseURLMimeAtom = gdk_atom_intern(kFilePromiseURLMime, FALSE);
sFilePromiseMimeAtom = gdk_atom_intern(kFilePromiseMime, FALSE);
sNativeImageMimeAtom = gdk_atom_intern(kNativeImageMime, FALSE);
sUTF8STRINGMimeAtom = gdk_atom_intern(gUTF8STRINGType, FALSE);
sSTRINGMimeAtom = gdk_atom_intern(gSTRINGType, FALSE);
});
}
nsDragSession::~nsDragSession() {
LOGDRAG("nsDragSession::~nsDragSession");
if (mTaskSource) g_source_remove(mTaskSource);
}
NS_IMPL_ISUPPORTS_INHERITED0(nsDragSession, nsBaseDragSession)
mozilla::StaticRefPtr<nsDragService> sDragServiceInstance;
/* static */
already_AddRefed<nsDragService> nsDragService::GetInstance() {
if (gfxPlatform::IsHeadless()) {
return nullptr;
}
if (!sDragServiceInstance) {
sDragServiceInstance = MakeRefPtr<nsDragService>();
ClearOnShutdown(&sDragServiceInstance);
}
RefPtr<nsDragService> service = sDragServiceInstance.get();
return service.forget();
}
nsDragService::nsDragService() = default;
already_AddRefed<nsIDragSession> nsDragService::CreateDragSession() {
#ifdef MOZ_WAYLAND
if (StaticPrefs::widget_wayland_native_data_session_AtStartup() &&
widget::GdkIsWaylandDisplay()) {
return MakeAndAddRef<nsDragSessionWayland>();
}
#endif
return MakeAndAddRef<nsDragSessionGtk>();
}
nsIDragSession* nsDragService::StartDragSession(nsISupports* aWidgetProvider) {
return nsBaseDragService::StartDragSession(aWidgetProvider);
}
nsresult nsDragSession::EndDragSessionImpl(bool aDoneDrag,
uint32_t aKeyModifiers) {
LOGDRAG("nsDragSession::EndDragSessionImpl() %d", aDoneDrag);
SetDragAction(nsIDragService::DRAGDROP_ACTION_NONE);
mCachedDragContextID = 0;
mNextScheduledTask = nullptr;
mRecentTask->Reset();
EndDragSessionImplBackend();
return nsBaseDragSession::EndDragSessionImpl(aDoneDrag, aKeyModifiers);
}
// nsIDragSession
NS_IMETHODIMP
nsDragSession::SetCanDrop(bool aCanDrop) {
LOGDRAG("nsDragSession::SetCanDrop %d", aCanDrop);
mCanDrop = aCanDrop;
return NS_OK;
}
NS_IMETHODIMP
nsDragSession::GetCanDrop(bool* aCanDrop) {
LOGDRAG("nsDragSession::GetCanDrop");
*aCanDrop = mCanDrop;
return NS_OK;
}
// Spins event loop, called from JS.
// Can lead to another round of drag_motion events.
NS_IMETHODIMP
nsDragSession::GetNumDropItems(uint32_t* aNumItems) {
LOGDRAG("nsDragSession::GetNumDropItems");
GtkWidget* widget =
mRecentTask->mWindow ? mRecentTask->mWindow->GetGtkWidget() : nullptr;
if (!widget) {
LOGDRAG(
"*** warning: GetNumDropItems \
called without a valid target widget!\n");
*aNumItems = 0;
return NS_OK;
}
if (IsTargetContextList()) {
if (!mSourceDataItems) {
*aNumItems = 0;
return NS_OK;
}
mSourceDataItems->GetLength(aNumItems);
LOGDRAG("GetNumDropItems(): TargetContextList items %d", *aNumItems);
return NS_OK;
}
// Put text/uri-list first, text/x-moz-url tends to be poorly supported
// by third party apps, we got only one file instead of file list
// for instance (Bug 1908196).
//
// We're getting the data to only get number of items here,
// actual data will be received at nsDragSession::GetData().
const GdkAtom fileListFlavors[] = {sTextUriListTypeAtom, // text/uri-list
sPortalFileAtom, sPortalFileTransferAtom,
sURLMimeAtom}; // text/x-moz-url
for (auto fileFlavour : fileListFlavors) {
RefPtr<DragData> data = GetDragData(fileFlavour);
if (data) {
*aNumItems = data->GetURIsNum();
LOGDRAG("GetNumDropItems(): Found MIME %s items %d",
GUniquePtr<gchar>(gdk_atom_name(fileFlavour)).get(), *aNumItems);
return NS_OK;
}
}
// We're missing any file list MIME, return only one item.
*aNumItems = 1;
LOGDRAG("GetNumDropItems(): no list available");
return NS_OK;
}
// Spins event loop, called from JS.
// Can lead to another round of drag_motion events.
NS_IMETHODIMP
nsDragSession::GetData(nsITransferable* aTransferable, uint32_t aItemIndex) {
LOGDRAG("nsDragSession::GetData(), index %d", aItemIndex);
// make sure that we have a transferable
if (!aTransferable) {
return NS_ERROR_INVALID_ARG;
}
GtkWidget* widget =
mRecentTask->mWindow ? mRecentTask->mWindow->GetGtkWidget() : nullptr;
if (!widget) {
LOGDRAG("*** failed: GetData called without a valid target widget!\n");
return NS_ERROR_FAILURE;
}
// get flavor list that includes all acceptable flavors (including
// ones obtained through conversion).
nsTArray<nsCString> flavors;
nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
if (NS_FAILED(rv)) {
LOGDRAG(" failed to get flavors, quit.");
return rv;
}
// check to see if this is an internal list
if (IsTargetContextList()) {
LOGDRAG(" Process as a list...");
// find a matching flavor
for (uint32_t i = 0; i < flavors.Length(); ++i) {
nsCString& flavorStr = flavors[i];
LOGDRAG(" [%d] flavor is %s\n", i, flavorStr.get());
// get the item with the right index
nsCOMPtr<nsITransferable> item =
do_QueryElementAt(mSourceDataItems, aItemIndex);
if (!item) continue;
nsCOMPtr<nsISupports> data;
LOGDRAG(" trying to get transfer data for %s\n", flavorStr.get());
rv = item->GetTransferData(flavorStr.get(), getter_AddRefs(data));
if (NS_FAILED(rv)) {
LOGDRAG(" failed.\n");
continue;
}
rv = aTransferable->SetTransferData(flavorStr.get(), data);
if (NS_FAILED(rv)) {
LOGDRAG(" fail to set transfer data into transferable!\n");
continue;
}
LOGDRAG(" succeeded\n");
// ok, we got the data
return NS_OK;
}
// if we got this far, we failed
LOGDRAG(" failed to match flavors\n");
return NS_ERROR_FAILURE;
}
// Now walk down the list of flavors. When we find one that is
// actually present, copy out the data into the transferable in that
// format. SetTransferData() implicitly handles conversions.
for (uint32_t i = 0; i < flavors.Length(); ++i) {
nsCString& flavorStr = flavors[i];
GdkAtom requestedFlavor = gdk_atom_intern(flavorStr.get(), FALSE);
if (!requestedFlavor) {
continue;
}
LOGDRAG(" we're getting data %s\n", flavorStr.get());
RefPtr<DragData> dragData;
// Let's do conversions first. We may be asked for some kind of MIME
// type data but we rather try to get something different and
// convert to desider MIME type.
// We're asked to get text data. Try to get UTF-8 variant first.
if (requestedFlavor == sTextMimeAtom) {
dragData = GetDragData(sTextPlainUTF8TypeAtom);
}
// Try portals first since text/uri-list URIs in sandboxed environments
// (Flatpak/Snap) may point to inaccessible file paths.
if (requestedFlavor == sURLMimeAtom || requestedFlavor == sFileMimeAtom) {
LOGDRAG(" try portals first\n");
dragData = GetDragData(sPortalFileAtom);
if (!dragData) {
dragData = GetDragData(sPortalFileTransferAtom);
}
}
// We are looking for text/x-moz-url. That format may be poorly supported,
// try first with text/uri-list, and then _NETSCAPE_URL
if (!dragData && requestedFlavor == sURLMimeAtom) {
LOGDRAG(" conversion %s => %s", gTextUriListType, kURLMime);
dragData = GetDragData(sTextUriListTypeAtom);
if (dragData) {
dragData = dragData->ConvertToMozURL();
mCachedDragData.InsertOrUpdate(dragData->GetFlavor(), dragData);
}
if (!dragData) {
LOGDRAG(" conversion %s => %s", gMozUrlType, kURLMime);
dragData = GetDragData(sMozUrlTypeAtom);
if (dragData) {
dragData = dragData->ConvertToMozURL();
if (dragData) {
mCachedDragData.InsertOrUpdate(dragData->GetFlavor(), dragData);
}
}
}
}
// Try to get requested MIME directly
if (!dragData) {
dragData = GetDragData(requestedFlavor);
}
// We're asked to get file mime type but we failed.
// Try text/uri-list conversion.
if (!dragData && requestedFlavor == sFileMimeAtom) {
LOGDRAG(" file not found, proceed with conversion %s => %s flavor\n",
gTextUriListType, kFileMime);
// Conversion text/uri-list => application/x-moz-file
dragData = GetDragData(sTextUriListTypeAtom);
if (dragData) {
dragData = dragData->ConvertToFile();
if (dragData) {
mCachedDragData.InsertOrUpdate(dragData->GetFlavor(), dragData);
}
}
}
if (dragData && dragData->Export(aTransferable, aItemIndex)) {
// We usually want to also get URL for images so continue
if (dragData->IsImageFlavor()) {
continue;
}
return NS_OK;
}
}
return NS_ERROR_FAILURE;
}
NS_IMETHODIMP
nsDragSession::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) {
LOGDRAG("nsDragSession::IsDataFlavorSupported() %s", aDataFlavor);
if (!_retval) {
return NS_ERROR_INVALID_ARG;
}
// set this to no by default
*_retval = false;
// check to make sure that we have a drag object set, here
GtkWidget* widget =
mRecentTask->mWindow ? mRecentTask->mWindow->GetGtkWidget() : nullptr;
if (!widget) {
LOGDRAG(
"*** warning: IsDataFlavorSupported called without a valid target "
"widget!\n");
return NS_OK;
}
// check to see if the target context is a list.
// if it is, just look in the internal data since we are the source
// for it.
if (IsTargetContextList()) {
LOGDRAG(" It's a list");
uint32_t numDragItems = 0;
// if we don't have mDataItems we didn't start this drag so it's
// an external client trying to fool us.
if (!mSourceDataItems) {
LOGDRAG(" quit");
return NS_OK;
}
mSourceDataItems->GetLength(&numDragItems);
LOGDRAG(" drag items %d", numDragItems);
for (uint32_t itemIndex = 0; itemIndex < numDragItems; ++itemIndex) {
nsCOMPtr<nsITransferable> currItem =
do_QueryElementAt(mSourceDataItems, itemIndex);
if (currItem) {
nsTArray<nsCString> flavors;
currItem->FlavorsTransferableCanExport(flavors);
for (uint32_t i = 0; i < flavors.Length(); ++i) {
LOGDRAG(" checking %s against %s\n", flavors[i].get(), aDataFlavor);
if (flavors[i].Equals(aDataFlavor)) {
LOGDRAG(" found.\n");
*_retval = true;
}
}
}
}
return NS_OK;
}
GdkAtom requestedFlavor = gdk_atom_intern(aDataFlavor, FALSE);
if (IsDragFlavorAvailable(requestedFlavor)) {
LOGDRAG(" %s is supported", aDataFlavor);
*_retval = true;
return NS_OK;
}
// GetData can convert text/plain;charset=utf-8 to text/plain, so report it
// as supported here too.
if (requestedFlavor == sTextMimeAtom &&
IsDragFlavorAvailable(sTextPlainUTF8TypeAtom)) {
LOGDRAG(" %s supported with conversion from %s", aDataFlavor,
gTextPlainUTF8Type);
*_retval = true;
return NS_OK;
}
// Check for file/url conversion from uri list
if ((requestedFlavor == sURLMimeAtom || requestedFlavor == sFileMimeAtom) &&
IsDragFlavorAvailable(sTextUriListTypeAtom)) {
LOGDRAG(" %s supported with conversion from %s", aDataFlavor,
gTextUriListType);
*_retval = true;
return NS_OK;
}
// check for automatic _NETSCAPE_URL -> text/x-moz-url mapping
if (requestedFlavor == sURLMimeAtom &&
IsDragFlavorAvailable(sMozUrlTypeAtom)) {
LOGDRAG(" %s supported with conversion from %s", aDataFlavor, gMozUrlType);
*_retval = true;
return NS_OK;
}
// If we're asked for kURLMime/kFileMime we can get it from PortalFile
// or PortalFileTransfer flavors.
if ((requestedFlavor == sURLMimeAtom || requestedFlavor == sFileMimeAtom) &&
(IsDragFlavorAvailable(sPortalFileAtom) ||
IsDragFlavorAvailable(sPortalFileTransferAtom))) {
LOGDRAG(" %s supported with conversion from %s/%s", aDataFlavor,
gPortalFile, gPortalFileTransfer);
*_retval = true;
return NS_OK;
}
LOGDRAG(" %s is not supported", aDataFlavor);
return NS_OK;
}
void nsDragSession::SetCachedDragContext(uintptr_t aDragContextID) {
LOGDRAG("nsDragSession::SetCachedDragContext(): [drag %p / cached %p]",
(void*)aDragContextID, (void*)mCachedDragContextID);
// Clear cache data if we're going to D&D with different drag context.
if (aDragContextID && aDragContextID != mCachedDragContextID) {
LOGDRAG(" cache clear, new context %p", (void*)aDragContextID);
mCachedDragContextID = aDragContextID;
mCachedDragData.Clear();
mCachedDragFlavors.Clear();
}
}
// Spins event loop, called from eDragTaskMotion handler by
// DispatchMotionEvents().
// Can lead to another round of drag_motion events.
RefPtr<DragData> nsDragSession::GetDragData(GdkAtom aRequestedFlavor) {
LOGDRAG("nsDragSession::GetDragData() requested '%s'\n",
GUniquePtr<gchar>(gdk_atom_name(aRequestedFlavor)).get());
// Return early when requested MIME is not offered by D&D.
if (!IsDragFlavorAvailable(aRequestedFlavor)) {
LOGDRAG(" %s is missing",
GUniquePtr<gchar>(gdk_atom_name(aRequestedFlavor)).get());
return nullptr;
}
{
auto data = mCachedDragData.MaybeGet(GDK_ATOM_TO_POINTER(aRequestedFlavor));
if (data) {
LOGDRAG(" MIME %s found in cache, %s",
GUniquePtr<gchar>(gdk_atom_name(aRequestedFlavor)).get(),
*data ? "got correctly" : "failed to get");
return *data;
}
}
if (!GetDragDataImpl(aRequestedFlavor)) {
LOGDRAG(" %s failed to get from system",
GUniquePtr<gchar>(gdk_atom_name(aRequestedFlavor)).get());
return nullptr;
}
RefPtr<DragData> data =
mCachedDragData.Get(GDK_ATOM_TO_POINTER(aRequestedFlavor));
if (!data) {
NS_WARNING(nsPrintfCString(
"nsDragSession::GetDragData() %s failed to get from cache",
GUniquePtr<gchar>(gdk_atom_name(aRequestedFlavor)).get())
.get());
return nullptr;
}
LOGDRAG(" %s received",
GUniquePtr<gchar>(gdk_atom_name(aRequestedFlavor)).get());
return data;
}
#ifdef MOZ_LOGGING
const char* nsDragSession::GetDragServiceTaskName(DragTaskType aTask) {
static const char* taskNames[] = {"eDragTaskNone", "eDragTaskMotion",
"eDragTaskLeave", "eDragTaskDrop"};
MOZ_ASSERT(size_t(aTask) < std::size(taskNames));
return taskNames[aTask];
}
#endif
void nsDragSession::DispatchMotionEvents() {
if (mSourceWindow) {
FireDragEventAtSource(eDrag, GetCurrentModifiers());
}
if (mRecentTask->mWindow) {
mRecentTask->mWindow->DispatchDragEvent(
eDragOver, mRecentTask->mWindowPoint, mRecentTask->mTime);
}
}
void nsDragSession::DispatchDropEvent() {
// We need to check IsDestroyed here because the nsRefPtr
// only protects this from being deleted, it does NOT protect
// against nsView::~nsView() calling Destroy() on it, bug 378273.
if (!mRecentTask->mWindow || mRecentTask->mWindow->IsDestroyed()) {
return;
}
EventMessage msg = mCanDrop ? eDrop : eDragExit;
mRecentTask->mWindow->DispatchDragEvent(msg, mRecentTask->mWindowPoint,
mRecentTask->mTime);
// Deliver message to OS
DropFinish(mCanDrop);
}
int nsDragSession::RunScheduledTaskCallback(void* aData) {
RefPtr<nsDragSession> dragSession = static_cast<nsDragSession*>(aData);
nsDragSession::AutoEventLoop loop(dragSession);
return dragSession->RunScheduledTask();
}
gboolean nsDragSession::Schedule(UniquePtr<DragTask> aTask) {
LOGDRAG("nsDragSession::Schedule()");
if (mDragTaskSourceFinished) {
LOGDRAG(" already finished, quit.");
return FALSE;
}
// If there is an existing leave or motion task scheduled, then that
// will be replaced. When the new task is run, it will dispatch
// any necessary leave or motion events.
// We don't replace eDragTaskDrop as it's final D&D event.
if (mNextScheduledTask && mNextScheduledTask->mType == eDragTaskDrop) {
LOGDRAG(" eDragTaskDrop is a final one, can't be replaced by %s",
GetDragServiceTaskName(aTask->mType));
return FALSE;
}
mNextScheduledTask = std::move(aTask);
if (!mTaskSource) {
// High priority is used here because we want to process motion events
// right after drag_motion event handler which is called by Gtk.
// An ideal scenario is to call RunScheduledTask() directly here
// but we can't do that. RunScheduledTask() spins gtk event loop
// while nsDragSession::Schedule() is already called from event loop
// (by drag_motion* gtk_widget events) so that direct call will cause
// nested recursion.
// RunScheduledTask returns false to stop getting callbacks
// from mTaskSource.
mTaskSource = g_timeout_add_full(G_PRIORITY_HIGH, 0,
RunScheduledTaskCallback, this, nullptr);
}
// We need to reply to drag_motion event on Wayland immediately,
// see Bug 1730203.
if (widget::GdkIsWaylandDisplay() &&
mNextScheduledTask->mType == eDragTaskMotion) {
UpdateDragAction();
ReplyToDragMotion();
}
return TRUE;
}
gboolean nsDragSession::RunScheduledTask() {
// No task is present - remove the timer.
if (!mNextScheduledTask || mDragTaskSourceFinished) {
LOGDRAG(
"nsDragSession::RunScheduledTask(): no task is scheduled or it's "
"finished [%d], quit.",
mDragTaskSourceFinished);
mTaskSource = 0;
return false;
}
// Don't run RunScheduledTask() twice. As we use it in main thread only
// we don't need to be thread safe here. But don't remove mTaskSource -
// call the RunScheduledTask() again later when recent task is finished.
if (mScheduledTaskIsRunning) {
return true;
}
AutoRestore<bool> guard(mScheduledTaskIsRunning);
mScheduledTaskIsRunning = true;
LOGDRAG("nsDragSession::RunScheduledTask() begin");
RunScheduledTask(std::move(mNextScheduledTask));
LOGDRAG("nsDragSession::RunScheduledTask() end");
// We have pending task (may be added by nested loops
// at RunScheduledTask()) - ask for next timer callback.
if (mNextScheduledTask) {
return true;
}
// We don't have any additional task in queue so terminate the timer.
mTaskSource = 0;
return false;
}
void nsDragSession::RunScheduledTask(
mozilla::UniquePtr<DragTask> aScheduledTask) {
MOZ_DIAGNOSTIC_ASSERT(
mScheduledTaskIsRunning,
"Running outside of nsDragSession::RunScheduledTask()?");
LOGDRAG(
"nsDragSession::RunScheduledTask() task %s recent Window %p scheduled "
"Window %p\n",
GetDragServiceTaskName(aScheduledTask->mType), mRecentTask->mWindow.get(),
aScheduledTask->mWindow.get());
if (mRecentTask->mWindow && mRecentTask->mWindow != aScheduledTask->mWindow) {
LOGDRAG(" window changed, dispatch eDragExit to leaved window (%p)\n",
mRecentTask->mWindow.get());
mRecentTask->mWindow->DispatchDragEvent(eDragExit,
aScheduledTask->mWindowPoint, 0);
if (!mSourceNode) {
// The drag that was initiated in a different app. End the drag
// session, since we're done with it for now (until the user drags
// back into this app).
EndDragSession(false, GetCurrentModifiers());
}
}
if (aScheduledTask->mType == eDragTaskLeave) {
LOGDRAG(" quit, selected task %s\n",
GetDragServiceTaskName(aScheduledTask->mType));
// Nothing more to do
// Returning false removes the task source from the event loop.
mRecentTask->Reset();
return;
}
// We process eDragTaskMotion or eDragTaskDrop from now.
// It is possible that the pending state has been updated during dispatch
// of the leave event. That's fine.
// Now we collect the pending state because, from this point on, we want
// to use the same state for all events dispatched. All state is updated
// so that when other tasks are scheduled during dispatch here, this
// task is considered to have already been run.
bool positionHasChanged =
aScheduledTask->mWindow != mRecentTask->mWindow ||
aScheduledTask->mWindowPoint != mRecentTask->mWindowPoint;
mRecentTask = std::move(aScheduledTask);
LOGDRAG(
" start drag session Window %p GtkWidget %p", mRecentTask->mWindow.get(),
mRecentTask->mWindow ? mRecentTask->mWindow->GetGtkWidget() : nullptr);
SetCachedDragContext(mRecentTask->GetContextID());
// http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model
// (as at 27 December 2010) indicates that a "drop" event should only be
// fired (at the current target element) if the current drag operation is
// not none. The current drag operation will only be set to a non-none
// value during a "dragover" event.
//
// If the user has ended the drag before any dragover events have been
// sent, then the spec recommends skipping the drop (because the current
// drag operation is none). However, here we assume that, by releasing
// the mouse button, the user has indicated that they want to drop, so we
// proceed with the drop where possible.
//
// In order to make the events appear to content in the same way as if the
// spec is being followed we make sure to dispatch a "dragover" event with
// appropriate coordinates and check canDrop before the "drop" event.
//
// When the Xdnd protocol is used for source/destination communication (as
// should be the case with GTK source applications) a dragover event
// should have already been sent during the drag-motion signal, which
// would have already been received because XdndDrop messages do not
// contain a position. However, we can't assume the same when the Motif
// protocol is used.
if (mRecentTask->mType == eDragTaskMotion || positionHasChanged) {
LOGDRAG(" process motion event\n");
UpdateDragAction();
TakeDragEventDispatchedToChildProcess(); // Clear the old value.
DispatchMotionEvents();
if (mRecentTask->mType == eDragTaskMotion) {
if (TakeDragEventDispatchedToChildProcess()) {
SetRemoteContext();
} else {
// Reply to tell the source whether we can drop and what
// action would be taken.
ReplyToDragMotion();
}
}
}
if (mRecentTask->mType == eDragTaskDrop) {
LOGDRAG(" process drop task\n");
DispatchDropEvent();
// Make sure to end the drag session. If this drag started in a
// different app, we won't get a drag_end signal to end it from.
EndDragSession(true, GetCurrentModifiers());
}
}
void nsDragSession::SetDragActionGtk(GdkDragAction aGdkAction) {
LOGDRAG("nsDragSession::SetDragActionGtk() action [%d]", aGdkAction);
// default is to do nothing
int action = nsIDragService::DRAGDROP_ACTION_NONE;
// set the default just in case nothing matches below
if (aGdkAction & GDK_ACTION_DEFAULT) {
LOGDRAG("nsDragSession::UpdateDragActionGtk(): set default move");
action = nsIDragService::DRAGDROP_ACTION_MOVE;
}
// first check to see if move is set
if (aGdkAction & GDK_ACTION_MOVE) {
LOGDRAG("nsDragSession::UpdateDragActionGtk(): set explicit move");
action = nsIDragService::DRAGDROP_ACTION_MOVE;
} else if (aGdkAction & GDK_ACTION_LINK) {
// then fall to the others
LOGDRAG("nsDragSession::UpdateDragActionGtk(): set explicit link");
action = nsIDragService::DRAGDROP_ACTION_LINK;
} else if (aGdkAction & GDK_ACTION_COPY) {
// copy is ctrl
LOGDRAG("nsDragSession::UpdateDragActionGtk(): set explicit copy");
action = nsIDragService::DRAGDROP_ACTION_COPY;
}
// update the drag information
SetDragAction(action);
}
GdkDragAction nsDragSession::GetDragActionGtk() {
GdkDragAction action = (GdkDragAction)0;
if (mCanDrop) {
// notify the dragger if we can drop
switch (mDragAction) {
case nsIDragService::DRAGDROP_ACTION_COPY:
LOGDRAG(" set explicit action copy");
action = GDK_ACTION_COPY;
break;
case nsIDragService::DRAGDROP_ACTION_LINK:
LOGDRAG(" set explicit action link");
action = GDK_ACTION_LINK;
break;
// if mCanDrop we switch DRAGDROP_ACTION_NONE to default.
case nsIDragService::DRAGDROP_ACTION_NONE:
LOGDRAG(" set explicit action none");
action = (GdkDragAction)0;
break;
default:
LOGDRAG(" set explicit action move");
action = GDK_ACTION_MOVE;
break;
}
}
LOGDRAG(
"nsDragSession::GetDragActionGtk() can drop %d mDragAction %d GdkAction "
"%d",
mCanDrop, mDragAction, action);
return action;
}
/* static */
uint32_t nsDragSession::GetCurrentModifiers() {
return mozilla::widget::KeymapWrapper::ComputeCurrentKeyModifiers();
}
nsAutoCString nsDragSession::GetDebugTag() const {
nsAutoCString tag;
tag.AppendPrintf("[%p]", this);
return tag;
}
/* static */
bool nsDragSession::IsTextFlavor(GdkAtom aFlavor) {
return aFlavor == nsDragSession::sTextMimeAtom ||
aFlavor == nsDragSession::sTextPlainUTF8TypeAtom ||
aFlavor == nsDragSession::sUTF8STRINGMimeAtom ||
aFlavor == nsDragSession::sSTRINGMimeAtom;
}
nsDragSession::DragTask::DragTask(
DragTaskType aType, nsWindow* aWindow,
const mozilla::LayoutDeviceIntPoint& aWindowPoint, guint aTime)
: mType(aType), mWindow(aWindow), mWindowPoint(aWindowPoint), mTime(aTime) {
};
#undef LOGDRAG
#undef LOGDRAGSTATIC