mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
20c086f54e
Backed out changeset 620790051502 (bug 1661935) Backed out changeset 7b10b4e274db (bug 1661935) Backed out changeset 594beeb5e424 (bug 1661935)
573 lines
17 KiB
C++
573 lines
17 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* 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 "WidgetUtilsGtk.h"
|
|
|
|
#include "MainThreadUtils.h"
|
|
#include "mozilla/StaticPrefs_widget.h"
|
|
#include "mozilla/UniquePtr.h"
|
|
#include "nsReadableUtils.h"
|
|
#include "nsString.h"
|
|
#include "nsStringFwd.h"
|
|
#include "nsWindow.h"
|
|
#include "nsIGfxInfo.h"
|
|
#include "mozilla/Components.h"
|
|
#include "nsCOMPtr.h"
|
|
#include "nsIProperties.h"
|
|
#include "nsIFile.h"
|
|
#include "nsXULAppAPI.h"
|
|
#include "nsXPCOMCID.h"
|
|
#include "nsDirectoryServiceDefs.h"
|
|
#include "nsString.h"
|
|
#include "nsGtkKeyUtils.h"
|
|
#include "nsGtkUtils.h"
|
|
|
|
#include <gtk/gtk.h>
|
|
#include <dlfcn.h>
|
|
#include <glib.h>
|
|
|
|
#ifdef MOZ_X11
|
|
# include <X11/Xlib.h>
|
|
# include <X11/Xatom.h>
|
|
#endif /* MOZ_X11 */
|
|
|
|
#undef LOGW
|
|
#ifdef MOZ_LOGGING
|
|
# include "mozilla/Logging.h"
|
|
extern mozilla::LazyLogModule gWidgetLog;
|
|
# define LOGW(...) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
|
|
#else
|
|
# define LOGW(...)
|
|
#endif /* MOZ_LOGGING */
|
|
|
|
namespace mozilla::widget {
|
|
|
|
int32_t WidgetUtilsGTK::IsTouchDeviceSupportPresent() {
|
|
int32_t result = 0;
|
|
GdkDisplay* display = gdk_display_get_default();
|
|
if (!display) {
|
|
return 0;
|
|
}
|
|
|
|
GdkDeviceManager* manager = gdk_display_get_device_manager(display);
|
|
if (!manager) {
|
|
return 0;
|
|
}
|
|
|
|
GList* devices =
|
|
gdk_device_manager_list_devices(manager, GDK_DEVICE_TYPE_SLAVE);
|
|
GList* list = devices;
|
|
|
|
while (devices) {
|
|
GdkDevice* device = static_cast<GdkDevice*>(devices->data);
|
|
if (gdk_device_get_source(device) == GDK_SOURCE_TOUCHSCREEN) {
|
|
result = 1;
|
|
break;
|
|
}
|
|
devices = devices->next;
|
|
}
|
|
|
|
if (list) {
|
|
g_list_free(list);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool IsMainWindowTransparent() {
|
|
return nsWindow::IsToplevelWindowTransparent();
|
|
}
|
|
|
|
// We avoid linking gdk_*_display_get_type directly in order to avoid a runtime
|
|
// dependency on GTK built with both backends. Other X11- and Wayland-specific
|
|
// functions get stubbed out by libmozgtk and crash when called, but those
|
|
// should only be called when the matching backend is already in use.
|
|
|
|
bool GdkIsWaylandDisplay(GdkDisplay* display) {
|
|
static auto sGdkWaylandDisplayGetType =
|
|
(GType(*)())dlsym(RTLD_DEFAULT, "gdk_wayland_display_get_type");
|
|
return sGdkWaylandDisplayGetType &&
|
|
G_TYPE_CHECK_INSTANCE_TYPE(display, sGdkWaylandDisplayGetType());
|
|
}
|
|
|
|
bool GdkIsX11Display(GdkDisplay* display) {
|
|
static auto sGdkX11DisplayGetType =
|
|
(GType(*)())dlsym(RTLD_DEFAULT, "gdk_x11_display_get_type");
|
|
return sGdkX11DisplayGetType &&
|
|
G_TYPE_CHECK_INSTANCE_TYPE(display, sGdkX11DisplayGetType());
|
|
}
|
|
|
|
bool IsXWaylandProtocol() {
|
|
static bool isXwayland = [] {
|
|
return !GdkIsWaylandDisplay() && !!getenv("WAYLAND_DISPLAY");
|
|
}();
|
|
return isXwayland;
|
|
}
|
|
|
|
bool GdkIsWaylandDisplay() {
|
|
static bool isWaylandDisplay = gdk_display_get_default() &&
|
|
GdkIsWaylandDisplay(gdk_display_get_default());
|
|
return isWaylandDisplay;
|
|
}
|
|
|
|
bool GdkIsX11Display() {
|
|
static bool isX11Display = gdk_display_get_default()
|
|
? GdkIsX11Display(gdk_display_get_default())
|
|
: false;
|
|
return isX11Display;
|
|
}
|
|
|
|
GdkDevice* GdkGetPointer() {
|
|
GdkDisplay* display = gdk_display_get_default();
|
|
GdkDeviceManager* deviceManager = gdk_display_get_device_manager(display);
|
|
return gdk_device_manager_get_client_pointer(deviceManager);
|
|
}
|
|
|
|
static GdkEvent* sLastMousePressEvent = nullptr;
|
|
GdkEvent* GetLastMousePressEvent() { return sLastMousePressEvent; }
|
|
|
|
void SetLastMousePressEvent(GdkEvent* aEvent) {
|
|
if (sLastMousePressEvent) {
|
|
GUniquePtr<GdkEvent> event(sLastMousePressEvent);
|
|
sLastMousePressEvent = nullptr;
|
|
}
|
|
if (!aEvent) {
|
|
return;
|
|
}
|
|
GUniquePtr<GdkEvent> event(gdk_event_copy(aEvent));
|
|
sLastMousePressEvent = event.release();
|
|
}
|
|
|
|
bool IsRunningUnderSnap() { return !!GetSnapInstanceName(); }
|
|
|
|
bool IsRunningUnderFlatpak() {
|
|
// https://gitlab.gnome.org/GNOME/gtk/-/blob/4300a5c609306ce77cbc8a3580c19201dccd8d13/gdk/gdk.c#L472
|
|
static bool sRunning = [] {
|
|
return g_file_test("/.flatpak-info", G_FILE_TEST_EXISTS);
|
|
}();
|
|
return sRunning;
|
|
}
|
|
|
|
bool IsPackagedAppFileExists() {
|
|
static bool sRunning = [] {
|
|
nsresult rv;
|
|
nsCString path;
|
|
nsCOMPtr<nsIFile> file;
|
|
nsCOMPtr<nsIProperties> directoryService;
|
|
|
|
directoryService = do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
|
|
NS_ENSURE_TRUE(directoryService, FALSE);
|
|
|
|
rv = directoryService->Get(NS_GRE_DIR, NS_GET_IID(nsIFile),
|
|
getter_AddRefs(file));
|
|
NS_ENSURE_SUCCESS(rv, FALSE);
|
|
|
|
rv = file->AppendNative("is-packaged-app"_ns);
|
|
NS_ENSURE_SUCCESS(rv, FALSE);
|
|
|
|
rv = file->GetNativePath(path);
|
|
NS_ENSURE_SUCCESS(rv, FALSE);
|
|
|
|
return g_file_test(path.get(), G_FILE_TEST_EXISTS);
|
|
}();
|
|
return sRunning;
|
|
}
|
|
|
|
const char* GetSnapInstanceName() {
|
|
static const char* sInstanceName = []() -> const char* {
|
|
const char* snapName = g_getenv("SNAP_NAME");
|
|
if (!snapName) {
|
|
return nullptr;
|
|
}
|
|
if (g_strcmp0(snapName, MOZ_APP_NAME) &&
|
|
g_strcmp0(snapName, MOZ_APP_NAME "-devel")) {
|
|
return nullptr;
|
|
}
|
|
// Intentionally leaked, as keeping a pointer to the environment forever
|
|
// is a bit suspicious.
|
|
if (const char* instanceName = g_getenv("SNAP_INSTANCE_NAME")) {
|
|
return g_strdup(instanceName);
|
|
}
|
|
// Instance name didn't exist for snapd <= 2.35:
|
|
return g_strdup(snapName);
|
|
}();
|
|
return sInstanceName;
|
|
}
|
|
|
|
bool ShouldUsePortal(PortalKind aPortalKind) {
|
|
static bool sPortalEnv = [] {
|
|
if (IsRunningUnderFlatpakOrSnap()) {
|
|
return true;
|
|
}
|
|
const char* portalEnvString = g_getenv("GTK_USE_PORTAL");
|
|
return portalEnvString && atoi(portalEnvString) != 0;
|
|
}();
|
|
|
|
bool autoBehavior = sPortalEnv;
|
|
const int32_t pref = [&] {
|
|
switch (aPortalKind) {
|
|
case PortalKind::FilePicker:
|
|
return StaticPrefs::widget_use_xdg_desktop_portal_file_picker();
|
|
case PortalKind::MimeHandler:
|
|
// Mime portal breaks default browser handling, see bug 1516290.
|
|
autoBehavior = IsRunningUnderFlatpakOrSnap();
|
|
return StaticPrefs::widget_use_xdg_desktop_portal_mime_handler();
|
|
case PortalKind::Settings:
|
|
autoBehavior = true;
|
|
return StaticPrefs::widget_use_xdg_desktop_portal_settings();
|
|
case PortalKind::Location:
|
|
return StaticPrefs::widget_use_xdg_desktop_portal_location();
|
|
case PortalKind::OpenUri:
|
|
return StaticPrefs::widget_use_xdg_desktop_portal_open_uri();
|
|
}
|
|
return 2;
|
|
}();
|
|
|
|
switch (pref) {
|
|
case 0:
|
|
return false;
|
|
case 1:
|
|
return true;
|
|
default:
|
|
return autoBehavior;
|
|
}
|
|
}
|
|
|
|
nsTArray<nsCString> ParseTextURIList(const nsACString& aData) {
|
|
UniquePtr<char[]> data(ToNewCString(aData));
|
|
gchar** uris = g_uri_list_extract_uris(data.get());
|
|
|
|
nsTArray<nsCString> result;
|
|
for (size_t i = 0; i < g_strv_length(uris); i++) {
|
|
result.AppendElement(nsCString(uris[i]));
|
|
}
|
|
|
|
g_strfreev(uris);
|
|
return result;
|
|
}
|
|
|
|
#ifdef MOZ_WAYLAND
|
|
static gboolean token_failed(gpointer aData);
|
|
|
|
class XDGTokenRequest {
|
|
public:
|
|
void SetTokenID(const char* aTokenID) {
|
|
LOGW("RequestWaylandFocusPromise() SetTokenID %s", aTokenID);
|
|
mTransferPromise->Resolve(aTokenID, __func__);
|
|
}
|
|
void Cancel() {
|
|
LOGW("RequestWaylandFocusPromise() canceled");
|
|
mTransferPromise->Reject(false, __func__);
|
|
mActivationTimeoutID = 0;
|
|
}
|
|
|
|
XDGTokenRequest(xdg_activation_token_v1* aXdgToken,
|
|
RefPtr<FocusRequestPromise::Private> aTransferPromise)
|
|
: mXdgToken(aXdgToken), mTransferPromise(std::move(aTransferPromise)) {
|
|
mActivationTimeoutID =
|
|
g_timeout_add(sActivationTimeout, token_failed, this);
|
|
}
|
|
~XDGTokenRequest() {
|
|
MozClearPointer(mXdgToken, xdg_activation_token_v1_destroy);
|
|
if (mActivationTimeoutID) {
|
|
g_source_remove(mActivationTimeoutID);
|
|
}
|
|
}
|
|
|
|
private:
|
|
xdg_activation_token_v1* mXdgToken;
|
|
RefPtr<FocusRequestPromise::Private> mTransferPromise;
|
|
guint mActivationTimeoutID;
|
|
// Reject FocusRequestPromise if we don't get XDG token in 0.5 sec.
|
|
static constexpr int sActivationTimeout = 500;
|
|
};
|
|
|
|
// Failed to get token in time
|
|
static gboolean token_failed(gpointer data) {
|
|
UniquePtr<XDGTokenRequest> request(static_cast<XDGTokenRequest*>(data));
|
|
request->Cancel();
|
|
return false;
|
|
}
|
|
|
|
// We've got activation token from Wayland compositor so it's time to use it.
|
|
static void token_done(gpointer data, struct xdg_activation_token_v1* provider,
|
|
const char* tokenID) {
|
|
UniquePtr<XDGTokenRequest> request(static_cast<XDGTokenRequest*>(data));
|
|
request->SetTokenID(tokenID);
|
|
}
|
|
|
|
static const struct xdg_activation_token_v1_listener token_listener = {
|
|
token_done,
|
|
};
|
|
#endif
|
|
|
|
RefPtr<FocusRequestPromise> RequestWaylandFocusPromise() {
|
|
#ifdef MOZ_WAYLAND
|
|
if (!GdkIsWaylandDisplay() || !WaylandDisplayGet()->GetSeat()) {
|
|
LOGW("RequestWaylandFocusPromise() failed.");
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<nsWindow> sourceWindow = nsWindow::GetFocusedWindow();
|
|
if (!sourceWindow || sourceWindow->IsDestroyed()) {
|
|
LOGW("RequestWaylandFocusPromise() missing source window");
|
|
return nullptr;
|
|
}
|
|
|
|
xdg_activation_v1* xdg_activation = WaylandDisplayGet()->GetXdgActivation();
|
|
if (!xdg_activation) {
|
|
LOGW("RequestWaylandFocusPromise() missing xdg_activation");
|
|
return nullptr;
|
|
}
|
|
|
|
wl_surface* focusSurface;
|
|
uint32_t focusSerial;
|
|
KeymapWrapper::GetFocusInfo(&focusSurface, &focusSerial);
|
|
if (!focusSurface) {
|
|
LOGW("RequestWaylandFocusPromise() missing focusSurface");
|
|
return nullptr;
|
|
}
|
|
|
|
GdkWindow* gdkWindow = sourceWindow->GetToplevelGdkWindow();
|
|
if (!gdkWindow) {
|
|
return nullptr;
|
|
}
|
|
wl_surface* surface = gdk_wayland_window_get_wl_surface(gdkWindow);
|
|
if (focusSurface != surface) {
|
|
LOGW("RequestWaylandFocusPromise() missing wl_surface");
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<FocusRequestPromise::Private> transferPromise =
|
|
new FocusRequestPromise::Private(__func__);
|
|
|
|
xdg_activation_token_v1* aXdgToken =
|
|
xdg_activation_v1_get_activation_token(xdg_activation);
|
|
xdg_activation_token_v1_add_listener(
|
|
aXdgToken, &token_listener,
|
|
new XDGTokenRequest(aXdgToken, transferPromise));
|
|
xdg_activation_token_v1_set_serial(aXdgToken, focusSerial,
|
|
WaylandDisplayGet()->GetSeat());
|
|
xdg_activation_token_v1_set_surface(aXdgToken, focusSurface);
|
|
xdg_activation_token_v1_commit(aXdgToken);
|
|
|
|
LOGW("RequestWaylandFocusPromise() XDG Token sent");
|
|
|
|
return transferPromise.forget();
|
|
#else // !defined(MOZ_WAYLAND)
|
|
return nullptr;
|
|
#endif
|
|
}
|
|
|
|
// https://specifications.freedesktop.org/wm-spec/1.3/ar01s05.html
|
|
static nsCString GetWindowManagerName() {
|
|
if (!GdkIsX11Display()) {
|
|
return {};
|
|
}
|
|
|
|
#ifdef MOZ_X11
|
|
Display* xdisplay = gdk_x11_get_default_xdisplay();
|
|
Window root_win =
|
|
GDK_WINDOW_XID(gdk_screen_get_root_window(gdk_screen_get_default()));
|
|
|
|
int actual_format_return;
|
|
Atom actual_type_return;
|
|
unsigned long nitems_return;
|
|
unsigned long bytes_after_return;
|
|
unsigned char* prop_return = nullptr;
|
|
auto releaseXProperty = MakeScopeExit([&] {
|
|
if (prop_return) {
|
|
XFree(prop_return);
|
|
}
|
|
});
|
|
|
|
Atom property = XInternAtom(xdisplay, "_NET_SUPPORTING_WM_CHECK", true);
|
|
Atom req_type = XInternAtom(xdisplay, "WINDOW", true);
|
|
if (!property || !req_type) {
|
|
return {};
|
|
}
|
|
int result =
|
|
XGetWindowProperty(xdisplay, root_win, property,
|
|
0L, // offset
|
|
sizeof(Window) / 4, // length
|
|
false, // delete
|
|
req_type, &actual_type_return, &actual_format_return,
|
|
&nitems_return, &bytes_after_return, &prop_return);
|
|
|
|
if (result != Success || bytes_after_return != 0 || nitems_return != 1) {
|
|
return {};
|
|
}
|
|
|
|
Window wmWindow = reinterpret_cast<Window*>(prop_return)[0];
|
|
if (!wmWindow) {
|
|
return {};
|
|
}
|
|
|
|
XFree(prop_return);
|
|
prop_return = nullptr;
|
|
|
|
property = XInternAtom(xdisplay, "_NET_WM_NAME", true);
|
|
req_type = XInternAtom(xdisplay, "UTF8_STRING", true);
|
|
if (!property || !req_type) {
|
|
return {};
|
|
}
|
|
result =
|
|
XGetWindowProperty(xdisplay, wmWindow, property,
|
|
0L, // offset
|
|
INT32_MAX, // length
|
|
false, // delete
|
|
req_type, &actual_type_return, &actual_format_return,
|
|
&nitems_return, &bytes_after_return, &prop_return);
|
|
if (result != Success || bytes_after_return != 0) {
|
|
return {};
|
|
}
|
|
|
|
return nsCString(reinterpret_cast<const char*>(prop_return));
|
|
#else
|
|
return {};
|
|
#endif
|
|
}
|
|
|
|
// Getting a reliable identifier is quite tricky. We try to use the standard
|
|
// XDG_CURRENT_DESKTOP environment first, _NET_WM_NAME later, and a set of
|
|
// legacy / non-standard environment variables otherwise.
|
|
//
|
|
// Documentation for some of those can be found in:
|
|
//
|
|
// https://wiki.archlinux.org/title/Environment_variables#Examples
|
|
// https://wiki.archlinux.org/title/Xdg-utils#Environment_variables
|
|
const nsCString& GetDesktopEnvironmentIdentifier() {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
static const nsDependentCString sIdentifier = [] {
|
|
nsCString ident = [] {
|
|
auto Env = [](const char* aKey) -> const char* {
|
|
const char* v = getenv(aKey);
|
|
return v && *v ? v : nullptr;
|
|
};
|
|
if (const char* currentDesktop = Env("XDG_CURRENT_DESKTOP")) {
|
|
return nsCString(currentDesktop);
|
|
}
|
|
if (auto wm = GetWindowManagerName(); !wm.IsEmpty()) {
|
|
return wm;
|
|
}
|
|
if (const char* sessionDesktop = Env("XDG_SESSION_DESKTOP")) {
|
|
// This is not really standardized in freedesktop.org, but it is
|
|
// documented here, and should be set in systemd systems.
|
|
// https://www.freedesktop.org/software/systemd/man/pam_systemd.html#%24XDG_SESSION_DESKTOP
|
|
return nsCString(sessionDesktop);
|
|
}
|
|
// We try first the DE-specific variables, then SESSION_DESKTOP, to match
|
|
// the documented order in:
|
|
// https://wiki.archlinux.org/title/Xdg-utils#Environment_variables
|
|
if (getenv("GNOME_DESKTOP_SESSION_ID")) {
|
|
return nsCString("gnome"_ns);
|
|
}
|
|
if (getenv("KDE_FULL_SESSION")) {
|
|
return nsCString("kde"_ns);
|
|
}
|
|
if (getenv("MATE_DESKTOP_SESSION_ID")) {
|
|
return nsCString("mate"_ns);
|
|
}
|
|
if (getenv("LXQT_SESSION_CONFIG")) {
|
|
return nsCString("lxqt"_ns);
|
|
}
|
|
if (const char* desktopSession = Env("DESKTOP_SESSION")) {
|
|
// Try the legacy DESKTOP_SESSION as a last resort.
|
|
return nsCString(desktopSession);
|
|
}
|
|
return nsCString();
|
|
}();
|
|
ToLowerCase(ident);
|
|
// Intentionally put into a ToNewCString copy, rather than just making a
|
|
// static nsCString to avoid leakchecking errors, since we really want to
|
|
// leak this string.
|
|
return nsDependentCString(ToNewCString(ident), ident.Length());
|
|
}();
|
|
return sIdentifier;
|
|
}
|
|
|
|
bool IsGnomeDesktopEnvironment() {
|
|
static bool sIsGnome =
|
|
!!FindInReadable("gnome"_ns, GetDesktopEnvironmentIdentifier());
|
|
return sIsGnome;
|
|
}
|
|
|
|
bool IsKdeDesktopEnvironment() {
|
|
static bool sIsKde = GetDesktopEnvironmentIdentifier().EqualsLiteral("kde");
|
|
return sIsKde;
|
|
}
|
|
|
|
bool IsCancelledGError(GError* aGError) {
|
|
return g_error_matches(aGError, G_IO_ERROR, G_IO_ERROR_CANCELLED);
|
|
}
|
|
|
|
#if defined(MOZ_X11)
|
|
static unsigned long GetWindowUserTime(GdkDisplay* aDisplay,
|
|
uintptr_t aWindow) {
|
|
Atom actualType;
|
|
int actualFormat;
|
|
unsigned long numberOfItems;
|
|
unsigned long bytesAfter;
|
|
unsigned char* property = nullptr;
|
|
unsigned long userTime = 0;
|
|
|
|
Display* xDisplay = GDK_DISPLAY_XDISPLAY(aDisplay);
|
|
Atom atom =
|
|
gdk_x11_get_xatom_by_name_for_display(aDisplay, "_NET_WM_USER_TIME");
|
|
|
|
if (XGetWindowProperty(xDisplay, aWindow, atom, 0, 1, false, XA_CARDINAL,
|
|
&actualType, &actualFormat, &numberOfItems,
|
|
&bytesAfter, &property) == Success &&
|
|
property) {
|
|
if (numberOfItems == 1) {
|
|
userTime = *((unsigned long*)property);
|
|
}
|
|
XFree(property);
|
|
}
|
|
|
|
return userTime;
|
|
}
|
|
|
|
void FindLatestUserTime(GdkDisplay* aDisplay, uintptr_t aWindow,
|
|
unsigned long* aLatestTime) {
|
|
Window rootReturn;
|
|
Window parentReturn;
|
|
Window* children;
|
|
unsigned int numberOfChildren;
|
|
unsigned long userTime;
|
|
|
|
Display* xDisplay = GDK_DISPLAY_XDISPLAY(aDisplay);
|
|
|
|
if (XQueryTree(xDisplay, aWindow, &rootReturn, &parentReturn, &children,
|
|
&numberOfChildren)) {
|
|
for (unsigned int i = 0; i < numberOfChildren; i++) {
|
|
userTime = GetWindowUserTime(aDisplay, children[i]);
|
|
if (userTime > *aLatestTime) {
|
|
*aLatestTime = userTime;
|
|
}
|
|
FindLatestUserTime(aDisplay, children[i], aLatestTime);
|
|
}
|
|
|
|
XFree(children);
|
|
}
|
|
}
|
|
|
|
// Assume we're started from user interaction and infer user time if its missing
|
|
nsCString SynthesizeStartupToken() {
|
|
unsigned long latestUserTime = 0;
|
|
FindLatestUserTime(gdk_display_get_default(),
|
|
GDK_WINDOW_XID(gdk_get_default_root_window()),
|
|
&latestUserTime);
|
|
|
|
if (latestUserTime == 0) {
|
|
return nsCString();
|
|
}
|
|
|
|
return nsPrintfCString("%s_TIME%lu", g_get_host_name(), latestUserTime);
|
|
}
|
|
#endif
|
|
|
|
} // namespace mozilla::widget
|