mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 864382 - Send a click event after a contextmenu event if the later has not been caught. f=amarchesini,r=jlebar,a=tef
This commit is contained in:
parent
97ccd34a21
commit
29284d0091
@ -41,6 +41,20 @@ function sendAsyncMsg(msg, data) {
|
||||
sendAsyncMessage('browser-element-api:call', data);
|
||||
}
|
||||
|
||||
function sendSyncMsg(msg, data) {
|
||||
// Ensure that we don't send any messages before BrowserElementChild.js
|
||||
// finishes loading.
|
||||
if (!BrowserElementIsReady)
|
||||
return;
|
||||
|
||||
if (!data) {
|
||||
data = { };
|
||||
}
|
||||
|
||||
data.msg_name = msg;
|
||||
return sendSyncMessage('browser-element-api:call', data);
|
||||
}
|
||||
|
||||
let CERTIFICATE_ERROR_PAGE_PREF = 'security.alternate_certificate_error_page';
|
||||
|
||||
let NS_ERROR_MODULE_BASE_OFFSET = 0x45;
|
||||
@ -524,8 +538,6 @@ BrowserElementChild.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this._ctxCounter++;
|
||||
this._ctxHandlers = {};
|
||||
|
||||
@ -554,7 +566,18 @@ BrowserElementChild.prototype = {
|
||||
menuData.contextmenu = this._buildMenuObj(menu, '');
|
||||
}
|
||||
}
|
||||
sendAsyncMsg('contextmenu', menuData);
|
||||
|
||||
// The value returned by the contextmenu sync call is true iff the embedder
|
||||
// called preventDefault() on its contextmenu event.
|
||||
//
|
||||
// We call preventDefault() on our contextmenu event iff the embedder called
|
||||
// preventDefault() on /its/ contextmenu event. This way, if the embedder
|
||||
// ignored the contextmenu event, TabChild will fire a click.
|
||||
if (sendSyncMsg('contextmenu', menuData)[0]) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
this._ctxHandlers = {};
|
||||
}
|
||||
},
|
||||
|
||||
_getSystemCtxMenuData: function(elem) {
|
||||
|
@ -294,7 +294,7 @@ BrowserElementParent.prototype = {
|
||||
let evtName = detail.msg_name;
|
||||
|
||||
debug('fireCtxMenuEventFromMsg: ' + evtName + ' ' + detail);
|
||||
let evt = this._createEvent(evtName, detail);
|
||||
let evt = this._createEvent(evtName, detail, /* cancellable */ true);
|
||||
|
||||
if (detail.contextmenu) {
|
||||
var self = this;
|
||||
@ -302,10 +302,11 @@ BrowserElementParent.prototype = {
|
||||
self._sendAsyncMsg('fire-ctx-callback', {menuitem: id});
|
||||
});
|
||||
}
|
||||
|
||||
// The embedder may have default actions on context menu events, so
|
||||
// we fire a context menu event even if the child didn't define a
|
||||
// custom context menu
|
||||
this._frameElement.dispatchEvent(evt);
|
||||
return !this._frameElement.dispatchEvent(evt);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -28,7 +28,6 @@ MOCHITEST_FILES = \
|
||||
browserElement_DataURI.js \
|
||||
test_browserElement_inproc_DataURI.html \
|
||||
browserElement_ErrorSecurity.js \
|
||||
test_browserElement_inproc_ErrorSecurity.html \
|
||||
browserElement_Titlechange.js \
|
||||
test_browserElement_inproc_Titlechange.html \
|
||||
browserElement_TopBarrier.js \
|
||||
@ -169,6 +168,9 @@ MOCHITEST_FILES = \
|
||||
# Disabled due to focus issues (no bug that I'm aware of)
|
||||
# test_browserElement_oop_KeyEvents.html \
|
||||
|
||||
# Disable due to certificate issue (no bug that I'm aware of)
|
||||
# test_browserElement_inproc_ErrorSecurity.html \
|
||||
|
||||
# OOP tests don't work on native-fennec (bug 774939).
|
||||
#
|
||||
# Both the "inproc" and "oop" versions of OpenMixedProcess open remote frames,
|
||||
@ -179,6 +181,7 @@ MOCHITEST_FILES += \
|
||||
browserElement_OpenMixedProcess.js \
|
||||
file_browserElement_OpenMixedProcess.html \
|
||||
test_browserElement_inproc_OpenMixedProcess.html \
|
||||
test_browserElement_inproc_ErrorSecurity.html \
|
||||
test_browserElement_oop_OpenMixedProcess.html \
|
||||
test_browserElement_oop_LoadEvents.html \
|
||||
test_browserElement_oop_DataURI.html \
|
||||
|
@ -1,142 +1,222 @@
|
||||
"use strict";
|
||||
'use strict';
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
browserElementTestHelpers.setEnabledPref(true);
|
||||
browserElementTestHelpers.addPermission();
|
||||
|
||||
var iframeScript = function() {
|
||||
|
||||
content.fireContextMenu = function(element) {
|
||||
var ev = content.document.createEvent('HTMLEvents');
|
||||
ev.initEvent('contextmenu', true, false);
|
||||
element.dispatchEvent(ev);
|
||||
};
|
||||
|
||||
XPCNativeWrapper.unwrap(content).ctxCallbackFired = function(data) {
|
||||
sendAsyncMessage('test:callbackfired', {data: data});
|
||||
};
|
||||
|
||||
XPCNativeWrapper.unwrap(content).onerror = function(e) {
|
||||
sendAsyncMessage('test:errorTriggered', {data: e});
|
||||
};
|
||||
|
||||
content.fireContextMenu(content.document.body);
|
||||
content.fireContextMenu(content.document.getElementById('menu1-trigger'));
|
||||
content.fireContextMenu(content.document.getElementById('inner-link').childNodes[0]);
|
||||
content.fireContextMenu(content.document.getElementById('menu2-trigger'));
|
||||
function runTests() {
|
||||
createIframe(function onIframeLoaded() {
|
||||
checkEmptyContextMenu();
|
||||
});
|
||||
}
|
||||
|
||||
var trigger1 = function() {
|
||||
content.fireContextMenu(content.document.getElementById('menu1-trigger'));
|
||||
function checkEmptyContextMenu() {
|
||||
sendContextMenuTo('body', function onContextMenu(detail) {
|
||||
is(detail.contextmenu, null, 'Body context clicks have no context menu');
|
||||
|
||||
checkInnerContextMenu();
|
||||
});
|
||||
}
|
||||
|
||||
function checkInnerContextMenu() {
|
||||
sendContextMenuTo('#inner-link', function onContextMenu(detail) {
|
||||
is(detail.systemTargets.length, 1, 'Includes anchor data');
|
||||
is(detail.contextmenu.items.length, 2, 'Inner clicks trigger correct menu');
|
||||
|
||||
checkCustomContextMenu();
|
||||
});
|
||||
}
|
||||
|
||||
function checkCustomContextMenu() {
|
||||
sendContextMenuTo('#menu1-trigger', function onContextMenu(detail) {
|
||||
is(detail.contextmenu.items.length, 2, 'trigger custom contextmenu');
|
||||
|
||||
checkNestedContextMenu();
|
||||
});
|
||||
}
|
||||
|
||||
function checkNestedContextMenu() {
|
||||
sendContextMenuTo('#menu2-trigger', function onContextMenu(detail) {
|
||||
var innerMenu = detail.contextmenu.items.filter(function(x) {
|
||||
return x.type === 'menu';
|
||||
});
|
||||
is(detail.systemTargets.length, 2, 'Includes anchor and img data');
|
||||
ok(innerMenu.length > 0, 'Menu contains a nested menu');
|
||||
|
||||
checkPreviousContextMenuHandler();
|
||||
});
|
||||
}
|
||||
|
||||
// Finished testing the data passed to the contextmenu handler,
|
||||
// now we start selecting contextmenu items
|
||||
function checkPreviousContextMenuHandler() {
|
||||
// This is previously triggered contextmenu data, since we have
|
||||
// fired subsequent contextmenus this should not be mistaken
|
||||
// for a current menuitem
|
||||
var detail = previousContextMenuDetail;
|
||||
var previousId = detail.contextmenu.items[0].id;
|
||||
checkContextMenuCallbackForId(detail, previousId, function onCallbackFired(label) {
|
||||
is(label, null, 'Callback label should be empty since this handler is old');
|
||||
|
||||
checkCurrentContextMenuHandler();
|
||||
});
|
||||
}
|
||||
|
||||
function checkCurrentContextMenuHandler() {
|
||||
// This triggers a current menuitem
|
||||
var detail = currentContextMenuDetail;
|
||||
|
||||
var innerMenu = detail.contextmenu.items.filter(function(x) {
|
||||
return x.type === 'menu';
|
||||
});
|
||||
|
||||
var currentId = innerMenu[0].items[1].id;
|
||||
checkContextMenuCallbackForId(detail, currentId, function onCallbackFired(label) {
|
||||
is(label, 'inner 2', 'Callback label should be set correctly');
|
||||
|
||||
checkAgainCurrentContextMenuHandler();
|
||||
});
|
||||
}
|
||||
|
||||
function checkAgainCurrentContextMenuHandler() {
|
||||
// Once an item it selected, subsequent selections are ignored
|
||||
var detail = currentContextMenuDetail;
|
||||
|
||||
var innerMenu = detail.contextmenu.items.filter(function(x) {
|
||||
return x.type === 'menu';
|
||||
});
|
||||
|
||||
var currentId = innerMenu[0].items[1].id;
|
||||
checkContextMenuCallbackForId(detail, currentId, function onCallbackFired(label) {
|
||||
is(label, null, 'Callback label should be empty since this handler has already been used');
|
||||
|
||||
checkCallbackWithPreventDefault();
|
||||
});
|
||||
};
|
||||
|
||||
function runTest() {
|
||||
var iframe1 = document.createElement('iframe');
|
||||
SpecialPowers.wrap(iframe1).mozbrowser = true;
|
||||
document.body.appendChild(iframe1);
|
||||
iframe1.src = 'data:text/html,<html>' +
|
||||
// Finished testing callbacks if the embedder calls preventDefault() on the
|
||||
// mozbrowsercontextmenu event, now we start checking for some cases where the embedder
|
||||
// does not want to call preventDefault() for some reasons.
|
||||
function checkCallbackWithPreventDefault() {
|
||||
sendContextMenuTo('#menu1-trigger', function onContextMenu(detail) {
|
||||
var id = detail.contextmenu.items[0].id;
|
||||
checkContextMenuCallbackForId(detail, id, function onCallbackFired(label) {
|
||||
is(label, 'foo', 'Callback label should be set correctly');
|
||||
|
||||
checkCallbackWithoutPreventDefault();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkCallbackWithoutPreventDefault() {
|
||||
sendContextMenuTo('#menu1-trigger', function onContextMenu(detail) {
|
||||
var id = detail.contextmenu.items[0].id;
|
||||
checkContextMenuCallbackForId(detail, id, function onCallbackFired(label) {
|
||||
is(label, null, 'Callback label should be null');
|
||||
|
||||
SimpleTest.finish();
|
||||
});
|
||||
}, /* ignorePreventDefault */ true);
|
||||
}
|
||||
|
||||
|
||||
/* Helpers */
|
||||
var mm = null;
|
||||
var previousContextMenuDetail = null;
|
||||
var currentContextMenuDetail = null;
|
||||
|
||||
function sendContextMenuTo(selector, callback, ignorePreventDefault) {
|
||||
iframe.addEventListener('mozbrowsercontextmenu', function oncontextmenu(e) {
|
||||
iframe.removeEventListener(e.type, oncontextmenu);
|
||||
|
||||
// The embedder should call preventDefault() on the event if it will handle
|
||||
// it. Not calling preventDefault() means it won't handle the event and
|
||||
// should not be able to deal with context menu callbacks.
|
||||
if (ignorePreventDefault !== true) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Keep a reference to previous/current contextmenu event details.
|
||||
previousContextMenuDetail = currentContextMenuDetail;
|
||||
currentContextMenuDetail = e.detail;
|
||||
|
||||
setTimeout(function() { callback(e.detail); });
|
||||
});
|
||||
|
||||
mm.sendAsyncMessage('contextmenu', { 'selector': selector });
|
||||
}
|
||||
|
||||
function checkContextMenuCallbackForId(detail, id, callback) {
|
||||
mm.addMessageListener('test:callbackfired', function onCallbackFired(msg) {
|
||||
mm.removeMessageListener('test:callbackfired', onCallbackFired);
|
||||
|
||||
msg = SpecialPowers.wrap(msg);
|
||||
setTimeout(function() { callback(msg.data.label); });
|
||||
});
|
||||
|
||||
detail.contextMenuItemSelected(id);
|
||||
}
|
||||
|
||||
|
||||
var iframe = null;
|
||||
function createIframe(callback) {
|
||||
iframe = document.createElement('iframe');
|
||||
SpecialPowers.wrap(iframe).mozbrowser = true;
|
||||
|
||||
iframe.src = 'data:text/html,<html>' +
|
||||
'<body>' +
|
||||
'<menu type="context" id="menu1" label="firstmenu">' +
|
||||
'<menuitem label="foo" onclick="window.ctxCallbackFired(\'foo\')"></menuitem>' +
|
||||
'<menuitem label="bar" onclick="throw(\'anerror\')"></menuitem>' +
|
||||
'<menuitem label="foo" onclick="window.onContextMenuCallbackFired(event)"></menuitem>' +
|
||||
'<menuitem label="bar" onclick="window.onContextMenuCallbackFired(event)"></menuitem>' +
|
||||
'</menu>' +
|
||||
'<menu type="context" id="menu2" label="secondmenu">' +
|
||||
'<menuitem label="outer" onclick="window.ctxCallbackFired(\'err\')"></menuitem>' +
|
||||
'<menuitem label="outer" onclick="window.onContextMenuCallbackFired(event)"></menuitem>' +
|
||||
'<menu>' +
|
||||
'<menuitem label="inner 1"></menuitem>' +
|
||||
'<menuitem label="inner 2" onclick="window.ctxCallbackFired(\'inner2\')"></menuitem>' +
|
||||
'<menuitem label="inner 2" onclick="window.onContextMenuCallbackFired(event)"></menuitem>' +
|
||||
'</menu>' +
|
||||
'</menu>' +
|
||||
'<div id="menu1-trigger" contextmenu="menu1"><a id="inner-link" href="foo.html">Menu 1</a></div>' +
|
||||
'<a href="bar.html" contextmenu="menu2"><img id="menu2-trigger" src="example.png" /></a>' +
|
||||
'</body></html>';
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
var mm;
|
||||
var numIframeLoaded = 0;
|
||||
var ctxMenuEvents = 0;
|
||||
var ctxCallbackEvents = 0;
|
||||
// The following code will be included in the child
|
||||
// =========================================================================
|
||||
function iframeScript() {
|
||||
addMessageListener('contextmenu', function onContextMenu(msg) {
|
||||
var document = content.document;
|
||||
var evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent('contextmenu', true, true);
|
||||
document.querySelector(msg.data.selector).dispatchEvent(evt);
|
||||
});
|
||||
|
||||
var cachedCtxDetail = null;
|
||||
addMessageListener('browser-element-api:call', function onCallback(msg) {
|
||||
if (msg.data.msg_name != 'fire-ctx-callback')
|
||||
return;
|
||||
|
||||
// We fire off various contextmenu events to check the data that gets
|
||||
// passed to the handler
|
||||
function iframeContextmenuHandler(e) {
|
||||
var detail = e.detail;
|
||||
ctxMenuEvents++;
|
||||
if (ctxMenuEvents === 1) {
|
||||
ok(detail.contextmenu === null, 'body context clicks have no context menu');
|
||||
} else if (ctxMenuEvents === 2) {
|
||||
cachedCtxDetail = detail;
|
||||
ok(detail.contextmenu.items.length === 2, 'trigger custom contextmenu');
|
||||
} else if (ctxMenuEvents === 3) {
|
||||
ok(detail.systemTargets.length === 1, 'Includes anchor data');
|
||||
ok(detail.contextmenu.items.length === 2, 'Inner clicks trigger correct menu');
|
||||
} else if (ctxMenuEvents === 4) {
|
||||
var innerMenu = detail.contextmenu.items.filter(function(x) {
|
||||
return x.type === 'menu';
|
||||
/* Use setTimeout in order to react *after* the platform */
|
||||
content.setTimeout(function() {
|
||||
sendAsyncMessage('test:callbackfired', { label: label });
|
||||
label = null;
|
||||
});
|
||||
ok(detail.systemTargets.length === 2, 'Includes anchor and img data');
|
||||
ok(innerMenu.length > 0, 'Menu contains a nested menu');
|
||||
ok(true, 'Got correct number of contextmenu events');
|
||||
// Finished testing the data passed to the contextmenu handler,
|
||||
// now we start selecting contextmenu items
|
||||
});
|
||||
|
||||
// This is previously triggered contextmenu data, since we have
|
||||
// fired subsequent contextmenus this should not be mistaken
|
||||
// for a current menuitem
|
||||
var prevId = cachedCtxDetail.contextmenu.items[0].id;
|
||||
cachedCtxDetail.contextMenuItemSelected(prevId);
|
||||
// This triggers a current menuitem
|
||||
detail.contextMenuItemSelected(innerMenu[0].items[1].id);
|
||||
// Once an item it selected, subsequent selections are ignored
|
||||
detail.contextMenuItemSelected(innerMenu[0].items[0].id);
|
||||
} else if (ctxMenuEvents === 5) {
|
||||
ok(detail.contextmenu.label === 'firstmenu', 'Correct menu enabled');
|
||||
detail.contextMenuItemSelected(detail.contextmenu.items[0].id);
|
||||
} else if (ctxMenuEvents === 6) {
|
||||
detail.contextMenuItemSelected(detail.contextmenu.items[1].id);
|
||||
} else if (ctxMenuEvents > 6) {
|
||||
ok(false, 'Too many events');
|
||||
}
|
||||
var label = null;
|
||||
XPCNativeWrapper.unwrap(content).onContextMenuCallbackFired = function(e) {
|
||||
label = e.target.getAttribute('label');
|
||||
};
|
||||
}
|
||||
// =========================================================================
|
||||
|
||||
function ctxCallbackRecieved(msg) {
|
||||
msg = SpecialPowers.wrap(msg);
|
||||
ctxCallbackEvents++;
|
||||
if (ctxCallbackEvents === 1) {
|
||||
ok(msg.json.data === 'inner2', 'Callback function got fired correctly');
|
||||
mm.loadFrameScript('data:,(' + trigger1.toString() + ')();', false);
|
||||
} else if (ctxCallbackEvents === 2) {
|
||||
ok(msg.json.data === 'foo', 'Callback function got fired correctly');
|
||||
mm.loadFrameScript('data:,(' + trigger1.toString() + ')();', false);
|
||||
} else if (ctxCallbackEvents > 2) {
|
||||
ok(false, 'Too many callback events');
|
||||
}
|
||||
}
|
||||
iframe.addEventListener('mozbrowserloadend', function onload(e) {
|
||||
iframe.removeEventListener(e.type, onload);
|
||||
mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
|
||||
mm.loadFrameScript('data:,(' + iframeScript.toString() + ')();', false);
|
||||
|
||||
var gotError = false;
|
||||
function errorTriggered(msg) {
|
||||
if (gotError) {
|
||||
return;
|
||||
}
|
||||
|
||||
gotError = true;
|
||||
ok(true, 'An error in the callback triggers window.onerror');
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
function iframeLoadedHandler() {
|
||||
numIframeLoaded++;
|
||||
if (numIframeLoaded === 2) {
|
||||
mm = SpecialPowers.getBrowserFrameMessageManager(iframe1);
|
||||
mm.addMessageListener('test:callbackfired', ctxCallbackRecieved);
|
||||
mm.addMessageListener('test:errorTriggered', errorTriggered);
|
||||
mm.loadFrameScript('data:,(' + iframeScript.toString() + ')();', false);
|
||||
}
|
||||
}
|
||||
|
||||
iframe1.addEventListener('mozbrowsercontextmenu', iframeContextmenuHandler);
|
||||
iframe1.addEventListener('mozbrowserloadend', iframeLoadedHandler);
|
||||
// Now we're ready, let's start testing.
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
addEventListener('testready', runTest);
|
||||
addEventListener('testready', runTests);
|
||||
|
@ -1594,11 +1594,8 @@ TabChild::RecvMouseEvent(const nsString& aType,
|
||||
const int32_t& aModifiers,
|
||||
const bool& aIgnoreRootScrollFrame)
|
||||
{
|
||||
nsCOMPtr<nsIDOMWindowUtils> utils(GetDOMWindowUtils());
|
||||
NS_ENSURE_TRUE(utils, true);
|
||||
bool ignored = false;
|
||||
utils->SendMouseEvent(aType, aX, aY, aButton, aClickCount, aModifiers,
|
||||
aIgnoreRootScrollFrame, 0, 0, &ignored);
|
||||
DispatchMouseEvent(aType, aX, aY, aButton, aClickCount, aModifiers,
|
||||
aIgnoreRootScrollFrame);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1735,8 +1732,21 @@ void
|
||||
TabChild::FireContextMenuEvent()
|
||||
{
|
||||
MOZ_ASSERT(mTapHoldTimer && mActivePointerId >= 0);
|
||||
RecvHandleLongTap(mGestureDownPoint);
|
||||
CancelTapTracking();
|
||||
bool defaultPrevented = DispatchMouseEvent(NS_LITERAL_STRING("contextmenu"),
|
||||
mGestureDownPoint.x, mGestureDownPoint.y,
|
||||
2 /* Right button */,
|
||||
1 /* Click count */,
|
||||
0 /* Modifiers */,
|
||||
false /* Ignore root scroll frame */);
|
||||
|
||||
// Fire a click event if someone didn't call preventDefault() on the context
|
||||
// menu event.
|
||||
if (defaultPrevented) {
|
||||
CancelTapTracking();
|
||||
} else if (mTapHoldTimer) {
|
||||
mTapHoldTimer->Cancel();
|
||||
mTapHoldTimer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@ -2208,6 +2218,24 @@ TabChild::IsAsyncPanZoomEnabled()
|
||||
return mScrolling == ASYNC_PAN_ZOOM;
|
||||
}
|
||||
|
||||
bool
|
||||
TabChild::DispatchMouseEvent(const nsString& aType,
|
||||
const float& aX,
|
||||
const float& aY,
|
||||
const int32_t& aButton,
|
||||
const int32_t& aClickCount,
|
||||
const int32_t& aModifiers,
|
||||
const bool& aIgnoreRootScrollFrame)
|
||||
{
|
||||
nsCOMPtr<nsIDOMWindowUtils> utils(GetDOMWindowUtils());
|
||||
NS_ENSURE_TRUE(utils, true);
|
||||
|
||||
bool defaultPrevented = false;
|
||||
utils->SendMouseEvent(aType, aX, aY, aButton, aClickCount, aModifiers,
|
||||
aIgnoreRootScrollFrame, 0, 0, &defaultPrevented);
|
||||
return defaultPrevented;
|
||||
}
|
||||
|
||||
void
|
||||
TabChild::MakeVisible()
|
||||
{
|
||||
|
@ -306,6 +306,17 @@ public:
|
||||
|
||||
bool IsAsyncPanZoomEnabled();
|
||||
|
||||
/** Return a boolean indicating if the page has called preventDefault on
|
||||
* the event.
|
||||
*/
|
||||
bool DispatchMouseEvent(const nsString& aType,
|
||||
const float& aX,
|
||||
const float& aY,
|
||||
const int32_t& aButton,
|
||||
const int32_t& aClickCount,
|
||||
const int32_t& aModifiers,
|
||||
const bool& aIgnoreRootScrollFrame);
|
||||
|
||||
/**
|
||||
* Signal to this TabChild that it should be made visible:
|
||||
* activated widget, retained layer tree, etc. (Respectively,
|
||||
|
Loading…
Reference in New Issue
Block a user