aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--meson.build4
-rw-r--r--src/meson.build1
-rw-r--r--src/wallet/wallet.cpp41
-rw-r--r--src/webengine/webview.cpp219
-rw-r--r--src/webengine/webview.h4
-rw-r--r--src/webengine/webviewcontextmenu.cpp227
-rw-r--r--src/webengine/webviewcontextmenu.h21
7 files changed, 272 insertions, 245 deletions
diff --git a/meson.build b/meson.build
index 49f3990..c033cac 100644
--- a/meson.build
+++ b/meson.build
@@ -39,10 +39,6 @@ add_project_arguments(cxx.get_supported_arguments([
'-Wimplicit-fallthrough',
]), language: 'cpp')
-add_project_link_arguments(cxx.get_supported_link_arguments([
- '-fuse-ld=gold'
-]), language: 'cpp')
-
mod_qt5 = import('qt5')
dep_qt5 = dependency('qt5',
modules: ['Core', 'Network', 'Widgets', 'WebEngineWidgets', 'Concurrent'],
diff --git a/src/meson.build b/src/meson.build
index 956c41b..6d6a1aa 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -45,6 +45,7 @@ poi_sourceset.add(files(
'webengine/urlinterceptor.cpp',
'webengine/webpage.cpp',
'webengine/webview.cpp',
+ 'webengine/webviewcontextmenu.cpp',
'webengine/webprofile.cpp',
'webengine/webprofilemanager.cpp',
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index c795cf2..5023ea4 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -8,37 +8,28 @@
#include "wallet.h"
#include <QWebEngineView>
-
-#ifdef PLASMA
-#include <kwallet.h>
-#endif
+#include <QWebEngineScript>
void Wallet::autocompleteForm(QWebEngineView *view)
{
-#ifdef PLASMA
- const auto findFormFunction = QLatin1Literal("index = undefined; for(var i = 0; i < document.forms.length; ++i) { if(document.forms[i].autocomplete) { index = i } }; index");
- view->page()->runJavaScript(findFormFunction, [view](const QVariant &v) {
+ const auto findFormFunction = QLatin1String("index = undefined; for(var i = 0; i < document.forms.length; ++i) { if(document.forms[i].autocomplete) { index = i } }; index");
+
+ view->page()->runJavaScript(findFormFunction, QWebEngineScript::ApplicationWorld, [view](const QVariant &v) {
if(!v.isNull()) {
+ const QString username = "test-username";
+ const QString password = "test-password";
+
auto autofillFunction = QString("inputs = document.forms[%1].getElementsByTagName('input');"
"for(var i = 0; i < inputs.length; ++i) {"
- " if(inputs[i].type == 'email') { inputs[i].value='%2' }"
- " if(inputs[i].type == 'password') { inputs[i].value='%3' }"
- "}");
-
- auto *wallet = KWallet::Wallet::openWallet(KWallet::Wallet::LocalWallet(), view->window()->winId());
- if(wallet) {
- wallet->setFolder("smolbote");
- QMap<QString, QString> map;
- wallet->readMap(view->url().host(), map);
- qDebug() << map;
-
- const auto username = map.firstKey();
- QString password;
- wallet->readPassword(map.value(username), password);
- view->page()->runJavaScript(autofillFunction.arg(v.toString(), username, password));
- }
- delete wallet;
+ " if(inputs[i].type == 'username') { inputs[i].value='username' }"
+ " else if(inputs[i].type == 'email') { inputs[i].value='%2' }"
+ " else if(inputs[i].type == 'password') { inputs[i].value='%3' }"
+ " else { inputs[i].value=inputs[i].type }"
+ "}")
+ .arg(v.toString(), username, password);
+ // TODO
+ // for page->url() get list of pairs type = value
+ view->page()->runJavaScript(autofillFunction, QWebEngineScript::ApplicationWorld);
}
});
-#endif
}
diff --git a/src/webengine/webview.cpp b/src/webengine/webview.cpp
index 5b2b6fd..d42bad5 100644
--- a/src/webengine/webview.cpp
+++ b/src/webengine/webview.cpp
@@ -8,35 +8,12 @@
#include "webview.h"
#include "subwindow/subwindow.h"
-#include "wallet/wallet.h"
#include "webpage.h"
#include "webprofile.h"
#include "webprofilemanager.h"
+#include "webviewcontextmenu.h"
#include <QContextMenuEvent>
-#include <QDialog>
-#include <QMenu>
-#include <QSlider>
-#include <QStatusBar>
-#include <QStyle>
-#include <QToolButton>
-#include <QVBoxLayout>
-#include <QWebEngineContextMenuData>
#include <QWebEngineHistoryItem>
-#include <QWidgetAction>
-
-inline QAction *historyAction(QWebEngineView *view, const QWebEngineHistoryItem &item)
-{
- QAction *action = new QAction(view);
- if(item.title().isEmpty())
- action->setText(item.url().toString());
- else
- action->setText(QObject::tr("%1 (%2)").arg(item.title(), item.url().toString()));
-
- QObject::connect(action, &QAction::triggered, view, [view, item]() {
- view->history()->goToItem(item);
- });
- return action;
-}
WebView::WebView(WebProfile *profile, QWidget *parent)
: QWebEngineView(parent)
@@ -113,197 +90,7 @@ WebView *WebView::createWindow(QWebEnginePage::WebWindowType type)
void WebView::contextMenuEvent(QContextMenuEvent *event)
{
- QMenu *menu = new QMenu(this);
- const auto ctxdata = page()->contextMenuData();
-
- // back, forward, reload, mute buttons, added to all variants of the context menu
- {
- auto *navButtons = new QWidgetAction(this);
-
- auto *buttons = new QWidget(this);
- auto *buttonsLayout = new QHBoxLayout();
- buttonsLayout->setContentsMargins(8, 0, 8, 0);
- buttonsLayout->setSpacing(2);
-
- auto *backButton = new QToolButton(this);
- backButton->setEnabled(history()->canGoBack());
- backButton->setIcon(style()->standardIcon(QStyle::SP_ArrowBack));
- connect(backButton, &QToolButton::clicked, this, [this, menu]() {
- this->back();
- menu->close();
- });
- buttonsLayout->addWidget(backButton);
-
- auto *forwardButton = new QToolButton(this);
- forwardButton->setEnabled(history()->canGoForward());
- forwardButton->setIcon(style()->standardIcon(QStyle::SP_ArrowForward));
- connect(forwardButton, &QToolButton::clicked, this, [this, menu]() {
- this->forward();
- menu->close();
- });
- buttonsLayout->addWidget(forwardButton);
-
- auto *refreshButton = new QToolButton(this);
- refreshButton->setIcon(style()->standardIcon(QStyle::SP_BrowserReload));
- connect(refreshButton, &QToolButton::clicked, this, [this, menu]() {
- this->reload();
- menu->close();
- });
- buttonsLayout->addWidget(refreshButton);
-
- buttonsLayout->addStretch();
-
- auto *muteButton = new QToolButton(this);
- muteButton->setCheckable(true);
- muteButton->setChecked(this->page()->isAudioMuted());
- QIcon muteIcon;
- muteIcon.addPixmap(style()->standardPixmap(QStyle::SP_MediaVolume), QIcon::Normal, QIcon::Off);
- muteIcon.addPixmap(style()->standardPixmap(QStyle::SP_MediaVolumeMuted), QIcon::Normal, QIcon::On);
- muteButton->setIcon(muteIcon);
- connect(muteButton, &QToolButton::clicked, this, [this, menu](bool checked) {
- this->page()->setAudioMuted(checked);
- menu->close();
- });
- buttonsLayout->addWidget(muteButton);
-
- buttons->setLayout(buttonsLayout);
- navButtons->setDefaultWidget(buttons);
-
- menu->addAction(navButtons);
- menu->addSeparator();
- }
-
- if(ctxdata.mediaType() == QWebEngineContextMenuData::MediaTypeNone) {
- auto *backMenu = new QMenu(tr("Back"), this);
- if(!history()->canGoBack()) {
- backMenu->setEnabled(false);
- } else {
- connect(backMenu, &QMenu::aboutToShow, this, [this, backMenu]() {
- backMenu->clear();
- const auto backItems = history()->backItems(10);
- for(const QWebEngineHistoryItem &item : backItems) {
- backMenu->addAction(historyAction(this, item));
- }
- });
- }
- menu->addMenu(backMenu);
-
- auto *forwardMenu = new QMenu(tr("Forward"), this);
- if(!history()->canGoForward()) {
- forwardMenu->setEnabled(false);
- } else {
- connect(forwardMenu, &QMenu::aboutToShow, this, [this, forwardMenu]() {
- forwardMenu->clear();
- const auto forwardItems = history()->forwardItems(10);
- for(const QWebEngineHistoryItem &item : forwardItems) {
- forwardMenu->addAction(historyAction(this, item));
- }
- });
- }
- menu->addMenu(forwardMenu);
-
- connect(menu->addAction(tr("Reload")), &QAction::triggered, this, [this]() {
- page()->triggerAction(QWebEnginePage::Reload);
- });
- connect(menu->addAction(tr("Reload and bypass Cache")), &QAction::triggered, this, [this]() {
- page()->triggerAction(QWebEnginePage::ReloadAndBypassCache);
- });
-
- menu->addSeparator();
-
- connect(menu->addAction(tr("Select All")), &QAction::triggered, this, [this]() {
- page()->triggerAction(QWebEnginePage::SelectAll);
- });
- connect(menu->addAction(tr("Clear Selection")), &QAction::triggered, this, [this]() {
- page()->triggerAction(QWebEnginePage::Unselect);
- });
- connect(menu->addAction(tr("Copy to clipboard")), &QAction::triggered, this, [this]() {
- page()->triggerAction(QWebEnginePage::Copy);
- });
-
- } else if(ctxdata.mediaType() == QWebEngineContextMenuData::MediaTypeImage) {
- connect(menu->addAction(tr("Copy image to clipboard")), &QAction::triggered, this, [this]() {
- page()->triggerAction(QWebEnginePage::CopyImageToClipboard);
- });
- connect(menu->addAction(tr("Copy image URL to clipboard")), &QAction::triggered, this, [this]() {
- page()->triggerAction(QWebEnginePage::CopyImageUrlToClipboard);
- });
- if(!ctxdata.mediaUrl().isEmpty()) {
- if(this->url() != ctxdata.mediaUrl()) {
- connect(menu->addAction(tr("Open image")), &QAction::triggered, this, [this, ctxdata]() {
- load(ctxdata.mediaUrl());
- });
- connect(menu->addAction(tr("Open image in new tab")), &QAction::triggered, this, [this, ctxdata]() {
- createWindow(QWebEnginePage::WebBrowserTab)->load(ctxdata.mediaUrl());
- });
- }
- connect(menu->addAction(tr("Save image")), &QAction::triggered, this, [this, ctxdata]() {
- page()->download(ctxdata.mediaUrl());
- });
- }
-
- } else {
- delete menu;
- menu = page()->createStandardContextMenu();
- }
-
- if(!ctxdata.linkUrl().isEmpty()) {
- menu->addSeparator();
- connect(menu->addAction(tr("Open link in new tab")), &QAction::triggered, this, [this, ctxdata]() {
- createWindow(QWebEnginePage::WebBrowserTab)->load(ctxdata.linkUrl());
- });
-
- auto *newTabMenu = menu->addMenu(tr("Open link in new tab with profile"));
- profileMenu(newTabMenu, [this, ctxdata](WebProfile *profile) {
- auto *view = this->createWindow(QWebEnginePage::WebBrowserTab);
- view->setProfile(profile);
- view->load(ctxdata.linkUrl());
- });
-
- connect(menu->addAction(tr("Open link in new window")), &QAction::triggered, this, [this, ctxdata]() {
- createWindow(QWebEnginePage::WebBrowserWindow)->load(ctxdata.linkUrl());
- });
-
- connect(menu->addAction(tr("Copy link address")), &QAction::triggered, this, [this]() {
- page()->triggerAction(QWebEnginePage::CopyLinkToClipboard);
- });
- }
-
- // zoom widget
- {
- menu->addSeparator();
-
- auto *zoomSlider = new QSlider(Qt::Horizontal);
- zoomSlider->setMinimum(5);
- zoomSlider->setMaximum(50);
- zoomSlider->setValue(zoomFactor() * 10);
-
- auto *zoomAction = menu->addAction(tr("Zoom: %1x").arg(zoomFactor()));
- connect(zoomAction, &QAction::triggered, this, [zoomSlider]() {
- zoomSlider->setValue(10);
- });
-
- connect(zoomSlider, &QSlider::valueChanged, this, [this, zoomAction](int value) {
- zoomAction->setText(tr("Zoom: %1x").arg(static_cast<qreal>(value) / 10));
- setZoomFactor(static_cast<qreal>(value) / 10);
- });
-
- auto *zoomWidgetAction = new QWidgetAction(this);
- zoomWidgetAction->setDefaultWidget(zoomSlider);
-
- menu->addAction(zoomWidgetAction);
- }
-
-#ifdef QT_DEBUG
- {
- menu->addSeparator();
- auto *autofillAction = menu->addAction(tr("Autofill form"));
- connect(autofillAction, &QAction::triggered, this, [this]() {
- Wallet::autocompleteForm(this);
- });
- };
-#endif
-
- menu->setMinimumWidth(250);
+ auto *menu = new WebViewContextMenu(this);
+ //const auto ctxdata = page()->contextMenuData();
menu->exec(event->globalPos());
}
diff --git a/src/webengine/webview.h b/src/webengine/webview.h
index 6fcbe3a..e5cd7fb 100644
--- a/src/webengine/webview.h
+++ b/src/webengine/webview.h
@@ -14,9 +14,13 @@
class WebProfile;
class SubWindow;
+class WebViewContextMenu;
class WebView : public QWebEngineView
{
+ friend class WebViewContextMenu;
+
Q_OBJECT
+
public:
explicit WebView(WebProfile *profile = nullptr, QWidget *parent = nullptr);
~WebView() = default;
diff --git a/src/webengine/webviewcontextmenu.cpp b/src/webengine/webviewcontextmenu.cpp
new file mode 100644
index 0000000..856ff9c
--- /dev/null
+++ b/src/webengine/webviewcontextmenu.cpp
@@ -0,0 +1,227 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#include "webviewcontextmenu.h"
+#include "wallet/wallet.h"
+#include "webprofilemanager.h"
+#include "webview.h"
+#include <QContextMenuEvent>
+#include <QDialog>
+#include <QMenu>
+#include <QSlider>
+#include <QStatusBar>
+#include <QStyle>
+#include <QToolButton>
+#include <QVBoxLayout>
+#include <QWebEngineContextMenuData>
+#include <QWebEngineHistory>
+#include <QWidgetAction>
+
+inline QAction *historyAction(QWebEngineView *view, const QWebEngineHistoryItem &item)
+{
+ QAction *action = new QAction(view);
+ if(item.title().isEmpty())
+ action->setText(item.url().toString());
+ else
+ action->setText(QObject::tr("%1 (%2)").arg(item.title(), item.url().toString()));
+
+ QObject::connect(action, &QAction::triggered, view, [view, item]() {
+ view->history()->goToItem(item);
+ });
+ return action;
+}
+
+WebViewContextMenu::WebViewContextMenu(WebView *view)
+ : QMenu(view)
+{
+ setMinimumWidth(250);
+
+ auto *navButtons = new QWidgetAction(this);
+
+ auto *buttons = new QWidget(this);
+ auto *buttonsLayout = new QHBoxLayout;
+ buttonsLayout->setContentsMargins(8, 0, 8, 0);
+ buttonsLayout->setSpacing(2);
+
+ auto *backButton = new QToolButton(this);
+ backButton->setEnabled(view->history()->canGoBack());
+ backButton->setIcon(style()->standardIcon(QStyle::SP_ArrowBack));
+ connect(backButton, &QToolButton::clicked, view, [this, view]() {
+ view->back();
+ this->close();
+ });
+ buttonsLayout->addWidget(backButton);
+
+ auto *forwardButton = new QToolButton(this);
+ forwardButton->setEnabled(view->history()->canGoForward());
+ forwardButton->setIcon(style()->standardIcon(QStyle::SP_ArrowForward));
+ connect(forwardButton, &QToolButton::clicked, view, [this, view]() {
+ view->forward();
+ this->close();
+ });
+ buttonsLayout->addWidget(forwardButton);
+
+ auto *refreshButton = new QToolButton(this);
+ refreshButton->setIcon(style()->standardIcon(QStyle::SP_BrowserReload));
+ connect(refreshButton, &QToolButton::clicked, view, [view, this]() {
+ view->reload();
+ this->close();
+ });
+ buttonsLayout->addWidget(refreshButton);
+
+ buttonsLayout->addStretch();
+
+ auto *muteButton = new QToolButton(this);
+ muteButton->setCheckable(true);
+ muteButton->setChecked(view->page()->isAudioMuted());
+ QIcon muteIcon;
+ muteIcon.addPixmap(style()->standardPixmap(QStyle::SP_MediaVolume), QIcon::Normal, QIcon::Off);
+ muteIcon.addPixmap(style()->standardPixmap(QStyle::SP_MediaVolumeMuted), QIcon::Normal, QIcon::On);
+ muteButton->setIcon(muteIcon);
+ connect(muteButton, &QToolButton::clicked, view, [view, this](bool checked) {
+ view->page()->setAudioMuted(checked);
+ this->close();
+ });
+ buttonsLayout->addWidget(muteButton);
+
+ buttons->setLayout(buttonsLayout);
+ navButtons->setDefaultWidget(buttons);
+
+ this->addAction(navButtons);
+ this->addSeparator();
+
+ const auto ctxdata = view->page()->contextMenuData();
+
+ if(ctxdata.mediaType() == QWebEngineContextMenuData::MediaTypeNone) {
+ auto *backMenu = this->addMenu(tr("Back"));
+ if(!view->history()->canGoBack()) {
+ backMenu->setEnabled(false);
+ } else {
+ connect(backMenu, &QMenu::aboutToShow, view, [view, backMenu]() {
+ backMenu->clear();
+ const auto backItems = view->history()->backItems(10);
+ for(const QWebEngineHistoryItem &item : backItems) {
+ backMenu->addAction(historyAction(view, item));
+ }
+ });
+ }
+
+ auto *forwardMenu = this->addMenu(tr("Forward"));
+ if(!view->history()->canGoForward()) {
+ forwardMenu->setEnabled(false);
+ } else {
+ connect(forwardMenu, &QMenu::aboutToShow, view, [view, forwardMenu]() {
+ forwardMenu->clear();
+ const auto forwardItems = view->history()->forwardItems(10);
+ for(const QWebEngineHistoryItem &item : forwardItems) {
+ forwardMenu->addAction(historyAction(view, item));
+ }
+ });
+ }
+
+ connect(this->addAction(tr("Reload")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::Reload);
+ });
+ connect(this->addAction(tr("Reload and bypass Cache")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::ReloadAndBypassCache);
+ });
+
+ this->addSeparator();
+
+ connect(this->addAction(tr("Select All")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::SelectAll);
+ });
+ connect(this->addAction(tr("Clear Selection")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::Unselect);
+ });
+ connect(this->addAction(tr("Copy to clipboard")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::Copy);
+ });
+
+ } else if(ctxdata.mediaType() == QWebEngineContextMenuData::MediaTypeImage) {
+ connect(this->addAction(tr("Copy image to clipboard")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::CopyImageToClipboard);
+ });
+ connect(this->addAction(tr("Copy image URL to clipboard")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::CopyImageUrlToClipboard);
+ });
+ if(!ctxdata.mediaUrl().isEmpty()) {
+ if(view->url() != ctxdata.mediaUrl()) {
+ connect(this->addAction(tr("Open image")), &QAction::triggered, view, [view, ctxdata]() {
+ view->load(ctxdata.mediaUrl());
+ });
+ connect(this->addAction(tr("Open image in new tab")), &QAction::triggered, view, [view, ctxdata]() {
+ view->createWindow(QWebEnginePage::WebBrowserTab)->load(ctxdata.mediaUrl());
+ });
+ }
+ connect(this->addAction(tr("Save image")), &QAction::triggered, view, [view, ctxdata]() {
+ view->page()->download(ctxdata.mediaUrl());
+ });
+ }
+
+ } else {
+ addMenu(view->page()->createStandardContextMenu());
+ }
+
+ if(!ctxdata.linkUrl().isEmpty()) {
+ this->addSeparator();
+ connect(this->addAction(tr("Open link in new tab")), &QAction::triggered, view, [view, ctxdata]() {
+ view->createWindow(QWebEnginePage::WebBrowserTab)->load(ctxdata.linkUrl());
+ });
+
+ auto *newTabMenu = this->addMenu(tr("Open link in new tab with profile"));
+ profileMenu(newTabMenu, [view, ctxdata](WebProfile *profile) {
+ auto *v = view->createWindow(QWebEnginePage::WebBrowserTab);
+ v->setProfile(profile);
+ v->load(ctxdata.linkUrl());
+ });
+
+ connect(this->addAction(tr("Open link in new window")), &QAction::triggered, view, [view, ctxdata]() {
+ view->createWindow(QWebEnginePage::WebBrowserWindow)->load(ctxdata.linkUrl());
+ });
+
+ connect(this->addAction(tr("Copy link address")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::CopyLinkToClipboard);
+ });
+ }
+
+ // zoom widget
+ {
+ this->addSeparator();
+
+ auto *zoomSlider = new QSlider(Qt::Horizontal);
+ zoomSlider->setMinimum(5);
+ zoomSlider->setMaximum(50);
+ zoomSlider->setValue(view->zoomFactor() * 10);
+
+ auto *zoomAction = this->addAction(tr("Zoom: %1x").arg(view->zoomFactor()));
+ connect(zoomAction, &QAction::triggered, view, [zoomSlider]() {
+ zoomSlider->setValue(10);
+ });
+
+ connect(zoomSlider, &QSlider::valueChanged, view, [view, zoomAction](int value) {
+ zoomAction->setText(tr("Zoom: %1x").arg(static_cast<qreal>(value) / 10));
+ view->setZoomFactor(static_cast<qreal>(value) / 10);
+ });
+
+ auto *zoomWidgetAction = new QWidgetAction(this);
+ zoomWidgetAction->setDefaultWidget(zoomSlider);
+
+ this->addAction(zoomWidgetAction);
+ }
+
+#ifndef NDEBUG
+ {
+ this->addSeparator();
+ auto *autofillAction = this->addAction(tr("Autofill form"));
+ connect(autofillAction, &QAction::triggered, view, [view]() {
+ Wallet::autocompleteForm(view);
+ });
+ };
+#endif
+}
diff --git a/src/webengine/webviewcontextmenu.h b/src/webengine/webviewcontextmenu.h
new file mode 100644
index 0000000..881670a
--- /dev/null
+++ b/src/webengine/webviewcontextmenu.h
@@ -0,0 +1,21 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#ifndef SMOLBOTE_WEBVIEWCONTEXTMENU_H
+#define SMOLBOTE_WEBVIEWCONTEXTMENU_H
+
+#include <QMenu>
+
+class WebView;
+class WebViewContextMenu : public QMenu
+{
+public:
+ explicit WebViewContextMenu(WebView *view);
+};
+
+#endif // SMOLBOTE_WEBVIEWCONTEXTMENU_H