Advance Wayland and KDE package bring-up

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-14 10:51:06 +01:00
parent 51f3c21121
commit cf12defd28
15214 changed files with 20594243 additions and 269 deletions
@@ -0,0 +1,162 @@
add_library(KF6ConfigWidgets)
add_library(KF6::ConfigWidgets ALIAS KF6ConfigWidgets)
set_target_properties(KF6ConfigWidgets PROPERTIES
VERSION ${KCONFIGWIDGETS_VERSION}
SOVERSION ${KCONFIGWIDGETS_SOVERSION}
EXPORT_NAME ConfigWidgets
)
target_sources(KF6ConfigWidgets PRIVATE
kcodecaction.cpp
kcolorschememenu.cpp
kcommandbar.cpp
kcommandbarmodel_p.cpp
kconfigdialog.cpp
kconfigviewstatesaver.cpp
kconfigdialogmanager.cpp
khelpclient.cpp
khamburgermenu.cpp
khamburgermenuhelpers.cpp
klanguagebutton.cpp
klanguagename.cpp
kopenaction.cpp
krecentfilesaction.cpp
kstandardaction.cpp
kstylemanager.cpp
kcodecaction.h
kcommandbar.h
kcommandbarmodel_p.h
kconfigdialog.h
kconfigviewstatesaver.h
kconfigdialogmanager.h
khelpclient.h
khamburgermenu.h
khamburgermenuhelpers_p.h
klanguagebutton.h
klanguagename.h
kopenaction_p.h
krecentfilesaction.h
kstandardaction.h
kstylemanager.h
)
ecm_qt_declare_logging_category(KF6ConfigWidgets
HEADER kconfigwidgets_debug.h
IDENTIFIER KCONFIG_WIDGETS_LOG
CATEGORY_NAME kf.configwidgets
OLD_CATEGORY_NAMES kf5.kconfigwidgets
DESCRIPTION "KConfigWidgets"
EXPORT KCONFIGWIDGETS
)
ecm_generate_export_header(KF6ConfigWidgets
BASE_NAME KConfigWidgets
GROUP_BASE_NAME KF
VERSION ${KF_VERSION}
USE_VERSION_HEADER
DEPRECATED_BASE_VERSION 0
DEPRECATION_VERSIONS
EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT}
)
target_include_directories(KF6ConfigWidgets INTERFACE "$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KConfigWidgets>")
target_link_libraries(KF6ConfigWidgets
PUBLIC
KF6::WidgetsAddons # For K*Action, KPage*, KViewStateSerializer, KAcceleratorManager, K*GuiItem
KF6::ConfigGui # KStandardAction uses KStandardShortcut
KF6::ColorScheme
PRIVATE
Qt6::GuiPrivate # KStyleManager::initStyle
KF6::CoreAddons # KAboutData, KFuzzymatcher
KF6::GuiAddons # KColorScheme uses KColorUtils
KF6::I18n # For action and widget texts
KF6::Codecs # KCodecActions uses KCharsets, KEncodingProber
)
if (HAVE_DBUS)
target_link_libraries(KF6ConfigWidgets
PRIVATE
Qt::DBus # KRecentFilesAction to send call to ActivityManager
)
target_compile_definitions(KF6ConfigWidgets PRIVATE -DHAVE_QTDBUS=1)
else()
target_compile_definitions(KF6ConfigWidgets PRIVATE -DHAVE_QTDBUS=0)
endif()
ecm_generate_headers(KConfigWidgets_HEADERS
HEADER_NAMES
KCodecAction
KColorSchemeMenu
KCommandBar
KConfigDialog
KConfigViewStateSaver
KConfigDialogManager
KHamburgerMenu
KHelpClient
KLanguageButton
KLanguageName
KRecentFilesAction
KViewStateMaintainer
KStandardAction
KStyleManager
REQUIRED_HEADERS KConfigWidgets_HEADERS
)
install(TARGETS KF6ConfigWidgets EXPORT KF6ConfigWidgetsTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/kconfigwidgets_export.h
${KConfigWidgets_HEADERS}
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KConfigWidgets COMPONENT Devel
)
install( FILES entry.desktop DESTINATION ${KDE_INSTALL_LOCALEDIR}/en_US RENAME kf6_entry.desktop )
ecm_qt_install_logging_categories(
EXPORT KCONFIGWIDGETS
FILE kconfigwidgets.categories
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
)
if(BUILD_DESIGNERPLUGIN)
add_subdirectory(designer)
endif()
if(BUILD_QCH)
ecm_add_qch(
KF6ConfigWidgets_QCH
NAME KConfigWidgets
BASE_NAME KF6ConfigWidgets
VERSION ${KF_VERSION}
ORG_DOMAIN org.kde
SOURCES # using only public headers, to cover only public API
${KConfigWidgets_HEADERS}
MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md"
IMAGE_DIRS "${CMAKE_SOURCE_DIR}/docs/pics"
LINK_QCHS
KF6Codecs_QCH
KF6WidgetsAddons_QCH
KF6Config_QCH
KF6Auth_QCH
INCLUDE_DIRS
${CMAKE_CURRENT_BINARY_DIR}
BLANK_MACROS
KCONFIGWIDGETS_EXPORT
KCONFIGWIDGETS_DEPRECATED
KCONFIGWIDGETS_DEPRECATED_EXPORT
"KCONFIGWIDGETS_DEPRECATED_VERSION(x, y, t)"
"KCONFIGWIDGETS_DEPRECATED_VERSION_BELATED(x, y, xt, yt, t)"
"KCONFIGWIDGETS_ENUMERATOR_DEPRECATED_VERSION(x, y, t)"
"KCONFIGWIDGETS_ENUMERATOR_DEPRECATED_VERSION_BELATED(x, y, xt, yt, t)"
PREDEFINED_MACROS
"KCONFIGWIDGETS_ENABLE_DEPRECATED_SINCE(x, y)=1"
"KCONFIGWIDGETS_BUILD_DEPRECATED_SINCE(x, y)=1"
TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
COMPONENT Devel
)
endif()
@@ -0,0 +1,13 @@
#!/bin/sh
# Invoke the extractrc script on all .ui, .rc, and .kcfg files in the sources.
# The results are stored in a pseudo .cpp file to be picked up by xgettext.
lst=`find . -name \*.rc -o -name \*.ui -o -name \*.kcfg`
if [ -n "$lst" ] ; then
$EXTRACTRC $lst >> rc.cpp
fi
# Extract strings from all source files.
# If your framework depends on KI18n, use $XGETTEXT. If it uses Qt translation
# system, use $EXTRACT_TR_STRINGS.
$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/kconfigwidgets6.pot
@@ -0,0 +1,18 @@
include(ECMAddQtDesignerPlugin)
ecm_qtdesignerplugin_widget(KLanguageButton
TOOLTIP "Language Button (KF6)"
WHATSTHIS "A pushbutton for language selection from a popup list."
GROUP "Buttons (KF6)"
)
ecm_add_qtdesignerplugin(kconfigwidgetswidgets
NAME KPlottingWidgets
OUTPUT_NAME kconfigwidgets6widgets
WIDGETS
KLanguageButton
LINK_LIBRARIES
KF6::ConfigWidgets
INSTALL_DESTINATION "${KDE_INSTALL_QTPLUGINDIR}/designer"
COMPONENT Devel
)
@@ -0,0 +1,59 @@
[KCM Locale]
Name=US English
Name[ar]=الإنجليزية الأمريكية
Name[az]=US İngiliscə
Name[be]=Англійская (ЗША)
Name[bg]=Английски US
Name[bs]=Američki engleski
Name[ca]=Anglès US
Name[ca@valencia]=Anglés US
Name[cs]=US Angličtina
Name[da]=US Engelsk
Name[de]=US-Englisch
Name[el]=US English
Name[en_GB]=US English
Name[eo]=Usona Angla
Name[es]=Inglés de EE. UU.
Name[et]=USA inglise
Name[eu]=AEBtako ingelesa
Name[fi]=Amerikanenglanti
Name[fr]=Anglais (États-Unis)
Name[gd]=Beurla (SA)
Name[gl]=Inglés americano
Name[he]=אנגלית ארה"ב
Name[hi]=यूएस अंग्रेज़ी
Name[hu]=Angol (amerikai)
Name[ia]=Anglese de S.U.A.
Name[id]=Inggris US
Name[is]=Bandarísk enska
Name[it]=Inglese US
Name[ka]=ინგლისური (აშშ)
Name[ko]=미국 영어
Name[lt]=JAV anglų
Name[lv]=ASV angļu
Name[nb]=Engelsk (USA)
Name[nl]=VS Engels
Name[nn]=Engelsk (USA)
Name[pa]=ਅਮਰੀਕੀ ਅੰਗਰੇਜ਼ੀ
Name[pl]=Angielski amerykański
Name[pt]=Inglês dos EUA
Name[pt_BR]=Inglês dos EUA
Name[ro]=Engleză SUA
Name[ru]=Английский (США)
Name[sa]=अमेरिकी आङ्ग्लभाषा
Name[sk]=Americká angličtina
Name[sl]=ameriško angleško
Name[sr]=амерички енглески
Name[sr@ijekavian]=амерички енглески
Name[sr@ijekavianlatin]=američki engleski
Name[sr@latin]=američki engleski
Name[sv]=Amerikansk engelska
Name[ta]=US ஆங்கிலம்
Name[tg]=Англисии ИМА
Name[tok]=toki Inli pi ma Mewika
Name[tr]=Amerikan İngilizcesi
Name[uk]=англійська (США)
Name[vi]=Tiếng Anh Mĩ
Name[x-test]=xxUS Englishxx
Name[zh_CN]=美式英语
Name[zh_TW]=美式英文
@@ -0,0 +1,133 @@
/*
SPDX-FileCopyrightText: 2003 Jason Keirstead <jason@keirstead.org>
SPDX-FileCopyrightText: 2006 Michel Hermier <michel.hermier@gmail.com>
SPDX-FileCopyrightText: 2007 Nick Shaforostoff <shafff@ukr.net>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kcodecaction.h"
#include "kconfigwidgets_debug.h"
#include <KCharsets>
#include <KEncodingProber>
#include <KLocalizedString>
#include <QMenu>
#include <QVariant>
class KCodecActionPrivate
{
public:
KCodecActionPrivate(KCodecAction *parent)
: q(parent)
{
}
void init(bool);
void subActionTriggered(QAction *);
KCodecAction *const q;
QAction *defaultAction = nullptr;
QAction *currentSubAction = nullptr;
};
KCodecAction::KCodecAction(QObject *parent, bool showAutoOptions)
: KSelectAction(parent)
, d(new KCodecActionPrivate(this))
{
d->init(showAutoOptions);
}
KCodecAction::KCodecAction(const QString &text, QObject *parent, bool showAutoOptions)
: KSelectAction(text, parent)
, d(new KCodecActionPrivate(this))
{
d->init(showAutoOptions);
}
KCodecAction::KCodecAction(const QIcon &icon, const QString &text, QObject *parent, bool showAutoOptions)
: KSelectAction(icon, text, parent)
, d(new KCodecActionPrivate(this))
{
d->init(showAutoOptions);
}
KCodecAction::~KCodecAction() = default;
void KCodecActionPrivate::init(bool showAutoOptions)
{
q->setToolBarMode(KSelectAction::MenuMode);
defaultAction = q->addAction(i18nc("Encodings menu", "Default"));
const auto lstEncodings = KCharsets::charsets()->encodingsByScript();
for (const QStringList &encodingsForScript : lstEncodings) {
KSelectAction *tmp = new KSelectAction(encodingsForScript.at(0), q);
if (showAutoOptions) {
KEncodingProber::ProberType scri = KEncodingProber::proberTypeForName(encodingsForScript.at(0));
if (scri != KEncodingProber::None) {
tmp->addAction(i18nc("Encodings menu", "Autodetect"))->setData(QVariant((uint)scri));
tmp->menu()->addSeparator();
}
}
for (int i = 1; i < encodingsForScript.size(); ++i) {
tmp->addAction(encodingsForScript.at(i));
}
q->connect(tmp, &KSelectAction::actionTriggered, q, [this](QAction *action) {
subActionTriggered(action);
});
tmp->setCheckable(true);
q->addAction(tmp);
}
q->setCurrentItem(0);
}
void KCodecAction::slotActionTriggered(QAction *action)
{
// we don't want to emit any signals from top-level items
// except for the default one
if (action == d->defaultAction) {
Q_EMIT defaultItemTriggered();
}
}
void KCodecActionPrivate::subActionTriggered(QAction *action)
{
if (currentSubAction == action) {
return;
}
currentSubAction = action;
Q_EMIT q->textTriggered(action->text());
Q_EMIT q->codecNameTriggered(action->text().toUtf8());
}
QString KCodecAction::currentCodecName() const
{
return d->currentSubAction->text();
}
bool KCodecAction::setCurrentCodec(const QString &codecName)
{
if (codecName.isEmpty()) {
return false;
}
for (int i = 0; i < actions().size(); ++i) {
if (actions().at(i)->menu()) {
for (int j = 0; j < actions().at(i)->menu()->actions().size(); ++j) {
if (!j && !actions().at(i)->menu()->actions().at(j)->data().isNull()) {
continue;
}
if (codecName == actions().at(i)->menu()->actions().at(j)->text()) {
d->currentSubAction = actions().at(i)->menu()->actions().at(j);
d->currentSubAction->trigger();
return true;
}
}
}
}
return false;
}
#include "moc_kcodecaction.cpp"
@@ -0,0 +1,70 @@
/*
SPDX-FileCopyrightText: 2003 Jason Keirstead <jason@keirstead.org>
SPDX-FileCopyrightText: 2003-2006 Michel Hermier <michel.hermier@gmail.com>
SPDX-FileCopyrightText: 2007 Nick Shaforostoff <shafff@ukr.net>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KCODECACTION_H
#define KCODECACTION_H
#include <kconfigwidgets_export.h>
#include <KSelectAction>
#include <memory>
/**
* @class KCodecAction kcodecaction.h KCodecAction
*
* @short Action for selecting one of several text codecs..
*
* This action shows up a submenu with a list of the available codecs on the system.
*/
class KCONFIGWIDGETS_EXPORT KCodecAction : public KSelectAction
{
Q_OBJECT
Q_PROPERTY(QString codecName READ currentCodecName WRITE setCurrentCodec)
public:
explicit KCodecAction(QObject *parent, bool showAutoOptions = false);
KCodecAction(const QString &text, QObject *parent, bool showAutoOptions = false);
KCodecAction(const QIcon &icon, const QString &text, QObject *parent, bool showAutoOptions = false);
~KCodecAction() override;
public:
QString currentCodecName() const;
bool setCurrentCodec(const QString &codecName);
Q_SIGNALS:
/**
* Emitted when a codec was selected
*
* @param name the name of the selected encoding.
*
* Note that textTriggered(const QString &) is emitted too (as defined in KSelectAction).
*
* @since 5.103
*/
void codecNameTriggered(const QByteArray &name);
/**
* Emitted when the 'Default' codec action is triggered.
*/
void defaultItemTriggered();
protected Q_SLOTS:
void slotActionTriggered(QAction *) override;
protected:
using KSelectAction::actionTriggered;
private:
friend class KCodecActionPrivate;
std::unique_ptr<class KCodecActionPrivate> const d;
};
#endif
@@ -0,0 +1,66 @@
/*
SPDX-FileCopyrightText: 2013 Martin Gräßlin <mgraesslin@kde.org>
SPDX-FileCopyrightText: 2023 David Redondo <kde@david-redondo.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kcolorschememenu.h"
#include <KActionMenu>
#include <KColorSchemeManager>
#include <KColorSchemeModel>
#include <KLocalizedString>
#include <QActionGroup>
#include <QIcon>
#include <QMenu>
constexpr int defaultSchemeRow = 0;
KActionMenu *KColorSchemeMenu::createMenu(KColorSchemeManager *manager, QObject *parent)
{
// Be careful here when connecting to signals. The menu can outlive the manager
KActionMenu *menu = new KActionMenu(QIcon::fromTheme(QStringLiteral("preferences-desktop-color")), i18n("Color Scheme"), parent);
QActionGroup *group = new QActionGroup(menu);
QObject::connect(group, &QActionGroup::triggered, manager, [manager](QAction *action) {
const QString schemePath = action->data().toString();
if (schemePath.isEmpty()) {
// Reset to default
manager->activateScheme(QModelIndex());
} else {
// Software linking KXmlGui gets its static KCheckAccelerators object deploy
// KAcceleratorManager on the whole UI automatically, which adds accelerators
// also to the texts of this menu's actions.
// So they have to be remove here in case
// (hoping that no original names have them also in case no accelerators were added)
// See also KColorSchemeManager::saveSchemeToConfigFile(const QString &schemeName) const.
const QString schemeName = KLocalizedString::removeAcceleratorMarker(action->text());
manager->activateScheme(manager->indexForScheme(schemeName));
}
});
const auto model = manager->model();
for (int i = 0; i < model->rowCount(); ++i) {
QModelIndex index = model->index(i, 0);
QAction *action = new QAction(index.data(KColorSchemeModel::NameRole).toString(), menu);
action->setData(index.data(KColorSchemeModel::PathRole));
action->setActionGroup(group);
action->setCheckable(true);
if (index.data(KColorSchemeModel::IdRole).toString() == manager->activeSchemeId()) {
action->setChecked(true);
}
menu->addAction(action);
QObject::connect(menu->menu(), &QMenu::aboutToShow, model, [action, index] {
if (action->icon().isNull()) {
action->setIcon(index.data(KColorSchemeModel::IconRole).value<QIcon>());
}
});
}
const auto groupActions = group->actions();
if (!group->checkedAction()) {
// If no (valid) color scheme has been selected we select the default one
groupActions[defaultSchemeRow]->setChecked(true);
}
return menu;
}
@@ -0,0 +1,30 @@
/*
SPDX-FileCopyrightText: 2023 David Redondo <kde@david-redondo.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KCOLORSCHEMEMENU_H
#define KCOLORSCHEMEMENU_H
class KActionMenu;
class KColorSchemeManager;
class QObject;
#include <kconfigwidgets_export.h>
/**
* A menu for switching color schemes
*/
namespace KColorSchemeMenu
{
/**
* Creates a KActionMenu populated with all the available color schemes.
* All actions are in an action group and when one of the actions is triggered the scheme
* referenced by this action is activated.
*
* @since 5.107
*/
KCONFIGWIDGETS_EXPORT KActionMenu *createMenu(KColorSchemeManager *manager, QObject *parent = nullptr);
}
#endif
@@ -0,0 +1,760 @@
/*
SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kcommandbar.h"
#include "kcommandbarmodel_p.h"
#include "kconfigwidgets_debug.h"
#include <QAction>
#include <QCoreApplication>
#include <QGraphicsOpacityEffect>
#include <QHeaderView>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QMainWindow>
#include <QMenu>
#include <QPainter>
#include <QPointer>
#include <QScreen>
#include <QSortFilterProxyModel>
#include <QStatusBar>
#include <QStyledItemDelegate>
#include <QTextLayout>
#include <QToolBar>
#include <QTreeView>
#include <QVBoxLayout>
#include <KConfigGroup>
#include <KFuzzyMatcher>
#include <KLocalizedString>
#include <KSharedConfig>
static QRect getCommandBarBoundingRect(KCommandBar *commandBar)
{
QWidget *parentWidget = commandBar->parentWidget();
Q_ASSERT(parentWidget);
const QMainWindow *mainWindow = qobject_cast<const QMainWindow *>(parentWidget);
if (!mainWindow) {
return parentWidget->geometry();
}
QRect boundingRect = mainWindow->contentsRect();
// exclude the menu bar from the bounding rect
if (const QWidget *menuWidget = mainWindow->menuWidget()) {
if (!menuWidget->isHidden()) {
boundingRect.setTop(boundingRect.top() + menuWidget->height());
}
}
// exclude the status bar from the bounding rect
if (const QStatusBar *statusBar = mainWindow->findChild<QStatusBar *>()) {
if (!statusBar->isHidden()) {
boundingRect.setBottom(boundingRect.bottom() - statusBar->height());
}
}
// exclude any undocked toolbar from the bounding rect
const QList<QToolBar *> toolBars = mainWindow->findChildren<QToolBar *>();
for (QToolBar *toolBar : toolBars) {
if (toolBar->isHidden() || toolBar->isFloating()) {
continue;
}
switch (mainWindow->toolBarArea(toolBar)) {
case Qt::TopToolBarArea:
boundingRect.setTop(std::max(boundingRect.top(), toolBar->geometry().bottom()));
break;
case Qt::RightToolBarArea:
boundingRect.setRight(std::min(boundingRect.right(), toolBar->geometry().left()));
break;
case Qt::BottomToolBarArea:
boundingRect.setBottom(std::min(boundingRect.bottom(), toolBar->geometry().top()));
break;
case Qt::LeftToolBarArea:
boundingRect.setLeft(std::max(boundingRect.left(), toolBar->geometry().right()));
break;
default:
break;
}
}
return boundingRect;
}
// BEGIN CommandBarFilterModel
class CommandBarFilterModel final : public QSortFilterProxyModel
{
public:
CommandBarFilterModel(QObject *parent = nullptr)
: QSortFilterProxyModel(parent)
{
connect(this, &CommandBarFilterModel::modelAboutToBeReset, this, [this]() {
m_hasActionsWithIcons = false;
});
}
bool hasActionsWithIcons() const
{
return m_hasActionsWithIcons;
}
Q_SLOT void setFilterString(const QString &string)
{
// MUST reset the model here, we want to repopulate
// invalidateFilter() will not work here
beginResetModel();
m_pattern = string;
endResetModel();
}
protected:
bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
{
const int scoreLeft = sourceLeft.data(KCommandBarModel::Score).toInt();
const int scoreRight = sourceRight.data(KCommandBarModel::Score).toInt();
if (scoreLeft == scoreRight) {
const QString textLeft = sourceLeft.data().toString();
const QString textRight = sourceRight.data().toString();
return textRight.localeAwareCompare(textLeft) < 0;
}
return scoreLeft < scoreRight;
}
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
bool accept = false;
if (m_pattern.isEmpty()) {
accept = true;
} else {
const QString row = index.data(Qt::DisplayRole).toString();
KFuzzyMatcher::Result resAction = KFuzzyMatcher::match(m_pattern, row);
sourceModel()->setData(index, resAction.score, KCommandBarModel::Score);
accept = resAction.matched;
}
if (accept && !m_hasActionsWithIcons) {
m_hasActionsWithIcons |= !index.data(Qt::DecorationRole).isNull();
}
return accept;
}
private:
QString m_pattern;
mutable bool m_hasActionsWithIcons = false;
};
// END CommandBarFilterModel
class CommandBarStyleDelegate final : public QStyledItemDelegate
{
public:
CommandBarStyleDelegate(QObject *parent = nullptr)
: QStyledItemDelegate(parent)
{
}
/**
* Paints a single item's text
*/
static void
paintItemText(QPainter *p, const QString &textt, const QRect &rect, const QStyleOptionViewItem &options, QList<QTextLayout::FormatRange> formats)
{
QString text = options.fontMetrics.elidedText(textt, Qt::ElideRight, rect.width());
// set formats and font
QTextLayout textLayout(text, options.font);
formats.append(textLayout.formats());
textLayout.setFormats(formats);
// set alignment, rtls etc
QTextOption textOption;
textOption.setTextDirection(options.direction);
textOption.setAlignment(QStyle::visualAlignment(options.direction, options.displayAlignment));
textLayout.setTextOption(textOption);
// layout the text
textLayout.beginLayout();
QTextLine line = textLayout.createLine();
if (!line.isValid()) {
return;
}
const int lineWidth = rect.width();
line.setLineWidth(lineWidth);
line.setPosition(QPointF(0, 0));
textLayout.endLayout();
/**
* get "Y" so that we can properly V-Center align the text in row
*/
const int y = QStyle::alignedRect(Qt::LeftToRight, Qt::AlignVCenter, textLayout.boundingRect().size().toSize(), rect).y();
// draw the text
const QPointF pos(rect.x(), y);
textLayout.draw(p, pos);
}
void paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const override
{
painter->save();
/**
* Draw everything, (widget, icon etc) except the text
*/
QStyleOptionViewItem option = opt;
initStyleOption(&option, index);
option.text.clear(); // clear old text
QStyle *style = option.widget->style();
style->drawControl(QStyle::CE_ItemViewItem, &option, painter, option.widget);
const int hMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &option, option.widget);
QRect textRect = option.rect;
const CommandBarFilterModel *model = static_cast<const CommandBarFilterModel *>(index.model());
if (model->hasActionsWithIcons()) {
const int iconWidth = option.decorationSize.width() + (hMargin * 2);
if (option.direction == Qt::RightToLeft) {
textRect.adjust(0, 0, -iconWidth, 0);
} else {
textRect.adjust(iconWidth, 0, 0, 0);
}
}
const QString original = index.data().toString();
QStringView str = original;
int componentIdx = original.indexOf(QLatin1Char(':'));
int actionNameStart = 0;
if (componentIdx > 0) {
actionNameStart = componentIdx + 2;
// + 2 because there is a space after colon
str = str.mid(actionNameStart);
}
QList<QTextLayout::FormatRange> formats;
if (componentIdx > 0) {
QTextCharFormat gray;
gray.setForeground(option.palette.placeholderText());
formats.append({0, componentIdx, gray});
}
QTextCharFormat fmt;
fmt.setForeground(option.palette.link());
fmt.setFontWeight(QFont::Bold);
/**
* Highlight matches from fuzzy matcher
*/
const auto fmtRanges = KFuzzyMatcher::matchedRanges(m_filterString, str);
QTextCharFormat f;
f.setForeground(option.palette.link());
formats.reserve(formats.size() + fmtRanges.size());
std::transform(fmtRanges.begin(), fmtRanges.end(), std::back_inserter(formats), [f, actionNameStart](const KFuzzyMatcher::Range &fr) {
return QTextLayout::FormatRange{fr.start + actionNameStart, fr.length, f};
});
textRect.adjust(hMargin, 0, -hMargin, 0);
paintItemText(painter, original, textRect, option, std::move(formats));
painter->restore();
}
public Q_SLOTS:
void setFilterString(const QString &text)
{
m_filterString = text;
}
private:
QString m_filterString;
};
class ShortcutStyleDelegate final : public QStyledItemDelegate
{
public:
ShortcutStyleDelegate(QObject *parent = nullptr)
: QStyledItemDelegate(parent)
{
}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
// draw background
option.widget->style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter);
const QString shortcutString = index.data().toString();
if (shortcutString.isEmpty()) {
return;
}
const ShortcutSegments shortcutSegments = splitShortcut(shortcutString);
if (shortcutSegments.isEmpty()) {
return;
}
struct Button {
int textWidth;
QString text;
};
// compute the width of each shortcut segment
QList<Button> btns;
btns.reserve(shortcutSegments.count());
const int hMargin = horizontalMargin(option);
for (const QString &text : shortcutSegments) {
int textWidth = option.fontMetrics.horizontalAdvance(text);
textWidth += 2 * hMargin;
btns.append({textWidth, text});
}
int textHeight = option.fontMetrics.lineSpacing();
// this happens on gnome so we manually decrease the height a bit
if (textHeight == option.rect.height()) {
textHeight -= 4;
}
const int y = option.rect.y() + (option.rect.height() - textHeight) / 2;
int x;
if (option.direction == Qt::RightToLeft) {
x = option.rect.x() + hMargin;
} else {
x = option.rect.right() - shortcutDrawingWidth(option, shortcutSegments, hMargin) - hMargin;
}
painter->save();
painter->setPen(option.palette.buttonText().color());
painter->setRenderHint(QPainter::Antialiasing);
for (int i = 0, n = btns.count(); i < n; ++i) {
const Button &button = btns.at(i);
QRect outputRect(x, y, button.textWidth, textHeight);
// an even element indicates that it is a key
if (i % 2 == 0) {
painter->save();
painter->setPen(Qt::NoPen);
// draw rounded rect shadow
auto shadowRect = outputRect.translated(0, 1);
painter->setBrush(option.palette.shadow());
painter->drawRoundedRect(shadowRect, 3.0, 3.0);
// draw rounded rect itself
painter->setBrush(option.palette.window());
painter->drawRoundedRect(outputRect, 3.0, 3.0);
painter->restore();
}
// draw shortcut segment
painter->drawText(outputRect, Qt::AlignCenter, button.text);
x += outputRect.width();
}
painter->restore();
}
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
if (index.isValid() && index.column() == KCommandBarModel::Column_Shortcut) {
const QString shortcut = index.data().toString();
if (!shortcut.isEmpty()) {
const ShortcutSegments shortcutSegments = splitShortcut(shortcut);
if (!shortcutSegments.isEmpty()) {
const int hMargin = horizontalMargin(option);
int width = shortcutDrawingWidth(option, shortcutSegments, hMargin);
// add left and right margins
width += 2 * hMargin;
return QSize(width, 0);
}
}
}
return QStyledItemDelegate::sizeHint(option, index);
}
private:
using ShortcutSegments = QStringList;
// split shortcut into segments i.e. will return
// ["Ctrl", "+", "A", ", ", "Ctrl", "+", "K"] for "Ctrl+A, Ctrl+K"
// twice as fast as using regular expressions
static ShortcutSegments splitShortcut(const QString &shortcut)
{
ShortcutSegments segments;
if (!shortcut.isEmpty()) {
const int shortcutLength = shortcut.length();
int start = 0;
for (int i = 0; i < shortcutLength; ++i) {
const QChar c = shortcut.at(i);
if (c == QLatin1Char('+')) {
if (i > start) {
segments << shortcut.mid(start, i - start);
}
segments << shortcut.at(i);
start = i + 1;
} else if (c == QLatin1Char(',')) {
if (i > start) {
segments << shortcut.mid(start, i - start);
start = i;
}
const int j = i + 1;
if (j < shortcutLength && shortcut.at(j) == QLatin1Char(' ')) {
segments << shortcut.mid(start, j - start + 1);
i = j;
} else {
segments << shortcut.at(i);
}
start = i + 1;
}
}
if (start < shortcutLength) {
segments << shortcut.mid(start);
}
// check we have successfully parsed the shortcut
if (segments.isEmpty()) {
qCWarning(KCONFIG_WIDGETS_LOG) << "Splitting shortcut failed" << shortcut;
}
}
return segments;
}
// returns the width needed to draw the shortcut
static int shortcutDrawingWidth(const QStyleOptionViewItem &option, const ShortcutSegments &shortcutSegments, int hMargin)
{
int width = 0;
if (!shortcutSegments.isEmpty()) {
width = option.fontMetrics.horizontalAdvance(shortcutSegments.join(QString()));
// add left and right margins for each segment
width += shortcutSegments.count() * 2 * hMargin;
}
return width;
}
int horizontalMargin(const QStyleOptionViewItem &option) const
{
return option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, &option) + 2;
}
};
// BEGIN KCommandBarPrivate
class KCommandBarPrivate
{
public:
QTreeView m_treeView;
QLineEdit m_lineEdit;
KCommandBarModel m_model;
CommandBarFilterModel m_proxyModel;
/**
* selects first item in treeview
*/
void reselectFirst()
{
const QModelIndex index = m_proxyModel.index(0, 0);
m_treeView.setCurrentIndex(index);
}
/**
* blocks signals before clearing line edit to ensure
* we don't trigger filtering / sorting
*/
void clearLineEdit()
{
const QSignalBlocker blocker(m_lineEdit);
m_lineEdit.clear();
}
void slotReturnPressed(KCommandBar *q);
void setLastUsedActions();
QStringList lastUsedActions() const;
};
void KCommandBarPrivate::slotReturnPressed(KCommandBar *q)
{
auto act = m_proxyModel.data(m_treeView.currentIndex(), Qt::UserRole).value<QAction *>();
if (act) {
// if the action is a menu, we take all its actions
// and reload our dialog with these instead.
if (auto menu = act->menu()) {
auto menuActions = menu->actions();
KCommandBar::ActionGroup ag;
// if there are no actions, trigger load actions
// this happens with some menus that are loaded on demand
if (menuActions.size() == 0) {
Q_EMIT menu->aboutToShow();
ag.actions = menu->actions();
}
QString groupName = KLocalizedString::removeAcceleratorMarker(act->text());
ag.name = groupName;
m_model.refresh({ag});
reselectFirst();
/**
* We want the "textChanged" signal here
* so that proxy model triggers filtering again
* so don't use d->clearLineEdit()
*/
m_lineEdit.clear();
return;
} else {
m_model.actionTriggered(act->text());
q->hide();
act->trigger();
}
}
clearLineEdit();
q->hide();
q->deleteLater();
}
void KCommandBarPrivate::setLastUsedActions()
{
auto cfg = KSharedConfig::openStateConfig();
KConfigGroup cg(cfg, QStringLiteral("General"));
QStringList actionNames = cg.readEntry(QStringLiteral("CommandBarLastUsedActions"), QStringList());
return m_model.setLastUsedActions(actionNames);
}
QStringList KCommandBarPrivate::lastUsedActions() const
{
return m_model.lastUsedActions();
}
// END KCommandBarPrivate
// BEGIN KCommandBar
KCommandBar::KCommandBar(QWidget *parent)
: QFrame(parent)
, d(new KCommandBarPrivate)
{
setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
setProperty("_breeze_force_frame", true);
QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect(this);
e->setColor(palette().color(QPalette::Dark));
e->setOffset(0, 4);
e->setBlurRadius(48);
setGraphicsEffect(e);
QVBoxLayout *layout = new QVBoxLayout();
layout->setSpacing(0);
layout->setContentsMargins(QMargins());
setLayout(layout);
layout->addWidget(&d->m_lineEdit);
d->m_lineEdit.setClearButtonEnabled(true);
d->m_lineEdit.addAction(QIcon::fromTheme(QStringLiteral("search")), QLineEdit::LeadingPosition);
d->m_lineEdit.setFrame(false);
d->m_lineEdit.setTextMargins(QMargins() + style()->pixelMetric(QStyle::PM_ButtonMargin));
setFocusProxy(&d->m_lineEdit);
layout->addWidget(&d->m_treeView);
d->m_treeView.setTextElideMode(Qt::ElideLeft);
d->m_treeView.setUniformRowHeights(true);
d->m_treeView.setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags(Qt::TopEdge)));
CommandBarStyleDelegate *delegate = new CommandBarStyleDelegate(this);
ShortcutStyleDelegate *del = new ShortcutStyleDelegate(this);
d->m_treeView.setItemDelegateForColumn(KCommandBarModel::Column_Command, delegate);
d->m_treeView.setItemDelegateForColumn(KCommandBarModel::Column_Shortcut, del);
connect(&d->m_lineEdit, &QLineEdit::returnPressed, this, [this]() {
d->slotReturnPressed(this);
});
connect(&d->m_lineEdit, &QLineEdit::textChanged, &d->m_proxyModel, &CommandBarFilterModel::setFilterString);
connect(&d->m_lineEdit, &QLineEdit::textChanged, delegate, &CommandBarStyleDelegate::setFilterString);
connect(&d->m_lineEdit, &QLineEdit::textChanged, this, [this]() {
d->m_treeView.viewport()->update();
d->reselectFirst();
});
connect(&d->m_treeView, &QTreeView::clicked, this, [this]() {
d->slotReturnPressed(this);
});
d->m_proxyModel.setSourceModel(&d->m_model);
d->m_treeView.setSortingEnabled(true);
d->m_treeView.setModel(&d->m_proxyModel);
d->m_treeView.header()->setMinimumSectionSize(0);
d->m_treeView.header()->setStretchLastSection(false);
d->m_treeView.header()->setSectionResizeMode(KCommandBarModel::Column_Command, QHeaderView::Stretch);
d->m_treeView.header()->setSectionResizeMode(KCommandBarModel::Column_Shortcut, QHeaderView::ResizeToContents);
parent->installEventFilter(this);
d->m_treeView.installEventFilter(this);
d->m_lineEdit.installEventFilter(this);
d->m_treeView.setHeaderHidden(true);
d->m_treeView.setRootIsDecorated(false);
d->m_treeView.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
d->m_treeView.setSelectionMode(QTreeView::SingleSelection);
QLabel *placeholderLabel = new QLabel;
placeholderLabel->setAlignment(Qt::AlignCenter);
placeholderLabel->setTextInteractionFlags(Qt::NoTextInteraction);
placeholderLabel->setWordWrap(true);
// To match the size of a level 2 Heading/KTitleWidget
QFont placeholderLabelFont = placeholderLabel->font();
placeholderLabelFont.setPointSize(qRound(placeholderLabelFont.pointSize() * 1.3));
placeholderLabel->setFont(placeholderLabelFont);
// Match opacity of QML placeholder label component
QGraphicsOpacityEffect *opacityEffect = new QGraphicsOpacityEffect(placeholderLabel);
opacityEffect->setOpacity(0.5);
placeholderLabel->setGraphicsEffect(opacityEffect);
QHBoxLayout *placeholderLayout = new QHBoxLayout;
placeholderLayout->addWidget(placeholderLabel);
d->m_treeView.setLayout(placeholderLayout);
connect(&d->m_proxyModel, &CommandBarFilterModel::modelReset, this, [this, placeholderLabel]() {
if (d->m_proxyModel.rowCount() > 0) {
placeholderLabel->hide();
} else {
if (d->m_model.rowCount() == 0) {
placeholderLabel->setText(i18n("No commands to display"));
} else {
placeholderLabel->setText(i18n("No commands matching the filter"));
}
placeholderLabel->show();
}
});
setHidden(true);
// Migrate last used action config to new location
KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General"));
if (cg.hasKey("CommandBarLastUsedActions")) {
const QStringList actionNames = cg.readEntry("CommandBarLastUsedActions", QStringList());
KConfigGroup stateCg(KSharedConfig::openStateConfig(), QStringLiteral("General"));
stateCg.writeEntry(QStringLiteral("CommandBarLastUsedActions"), actionNames);
cg.deleteEntry(QStringLiteral("CommandBarLastUsedActions"));
}
}
/**
* Destructor defined here to make unique_ptr work
*/
KCommandBar::~KCommandBar()
{
auto lastUsedActions = d->lastUsedActions();
auto cfg = KSharedConfig::openStateConfig();
KConfigGroup cg(cfg, QStringLiteral("General"));
cg.writeEntry("CommandBarLastUsedActions", lastUsedActions);
// Explicitly remove installed event filters of children of d-pointer
// class, otherwise while KCommandBar is being torn down, an event could
// fire and the eventFilter() accesses d, which would cause a crash
// bug 452527
d->m_treeView.removeEventFilter(this);
d->m_lineEdit.removeEventFilter(this);
}
void KCommandBar::setActions(const QList<ActionGroup> &actions)
{
// First set last used actions in the model
d->setLastUsedActions();
d->m_model.refresh(actions);
d->reselectFirst();
show();
setFocus();
}
void KCommandBar::show()
{
const QRect boundingRect = getCommandBarBoundingRect(this);
static constexpr int minWidth = 500;
const int maxWidth = boundingRect.width();
const int preferredWidth = maxWidth / 2.4;
static constexpr int minHeight = 250;
const int maxHeight = boundingRect.height();
const int preferredHeight = maxHeight / 2;
const QSize size{std::min(maxWidth, std::max(preferredWidth, minWidth)), std::min(maxHeight, std::max(preferredHeight, minHeight))};
setFixedSize(size);
// set the position to the top-center of the parent
// just below the menubar/toolbar (if any)
const QPoint position{boundingRect.center().x() - size.width() / 2, boundingRect.y() + 6};
move(position);
QWidget::show();
}
bool KCommandBar::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (obj == &d->m_lineEdit) {
const int key = keyEvent->key();
const bool forward2list = (key == Qt::Key_Up) || (key == Qt::Key_Down) || (key == Qt::Key_PageUp) || (key == Qt::Key_PageDown);
if (forward2list) {
QCoreApplication::sendEvent(&d->m_treeView, event);
return true;
}
} else if (obj == &d->m_treeView) {
const int key = keyEvent->key();
const bool forward2input = (key != Qt::Key_Up) && (key != Qt::Key_Down) && (key != Qt::Key_PageUp) && (key != Qt::Key_PageDown)
&& (key != Qt::Key_Tab) && (key != Qt::Key_Backtab);
if (forward2input) {
QCoreApplication::sendEvent(&d->m_lineEdit, event);
return true;
}
}
if (keyEvent->key() == Qt::Key_Escape) {
hide();
deleteLater();
return true;
}
}
// hide on focus out, if neither input field nor list have focus!
else if (event->type() == QEvent::FocusOut && isVisible() && !(d->m_lineEdit.hasFocus() || d->m_treeView.hasFocus())) {
d->clearLineEdit();
deleteLater();
hide();
return true;
}
// handle resizing
if (parent() == obj && event->type() == QEvent::Resize) {
show();
}
return QWidget::eventFilter(obj, event);
}
// END KCommandBar
#include "moc_kcommandbar.cpp"
@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KCOMMANDBAR_H
#define KCOMMANDBAR_H
#include "kconfigwidgets_export.h"
#include <QFrame>
#include <memory>
/**
* @class KCommandBar kcommandbar.h KCommandBar
*
* @short A hud style menu which allows listing and executing actions
*
* KCommandBar takes as input a list of QActions and then shows them
* in a "list-like-view" with a filter line edit. This allows quickly
* executing an action.
*
* Usage is simple. Just create a KCommandBar instance when you need it
* and throw it away once the user is done with it. You can store it as
* a class member as well but there is little benefit in that. Example:
*
* @code
* void slotOpenCommandBar()
* {
* // `this` is important, you must pass a parent widget
* // here. Ideally it will be your mainWindow
* KCommandBar *bar = new KCommandBar(this);
*
* // Load actions into the command bar
* // These actions can be from your menus / toolbars etc
* QList<ActionGroup> actionGroups = ...;
* bar->setActions(actionGroups);
*
* // Show
* bar->show();
* }
* @endcode
*
* @since 5.83
* @author Waqar Ahmed <waqar.17a@gmail.com>
*/
class KCONFIGWIDGETS_EXPORT KCommandBar : public QFrame
{
Q_OBJECT
public:
/**
* Represents a list of action that belong to the same group.
* For example:
* - All actions under the menu "File" or "Tool"
*/
struct ActionGroup {
QString name;
QList<QAction *> actions;
};
/**
* constructor
*
* @p parent is used to determine position and size of the
* command bar. It *must* not be @c nullptr.
*/
explicit KCommandBar(QWidget *parent);
~KCommandBar() override;
/**
* @p actions is a list of {GroupName, QAction}. Group name can be the name
* of the component/menu where a QAction lives, for example in a menu "File -> Open File",
* "File" should be the GroupName.
*/
void setActions(const QList<ActionGroup> &actions);
public Q_SLOTS:
void show();
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
private:
std::unique_ptr<class KCommandBarPrivate> const d;
};
Q_DECLARE_TYPEINFO(KCommandBar::ActionGroup, Q_RELOCATABLE_TYPE);
#endif
@@ -0,0 +1,176 @@
/*
SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kcommandbarmodel_p.h"
#include "kcommandbar.h" // For ActionGroup
#include "kconfigwidgets_debug.h"
#include <KLocalizedString>
#include <QAction>
#include <QMenu>
#include <unordered_set>
QString KCommandBarModel::Item::displayName() const
{
const QString group = KLocalizedString::removeAcceleratorMarker(groupName);
const QString command = KLocalizedString::removeAcceleratorMarker(action->text());
return group + QStringLiteral(": ") + command;
}
KCommandBarModel::KCommandBarModel(QObject *parent)
: QAbstractTableModel(parent)
{
m_clearHistoryAction = new QAction(tr("Clear History"), this);
m_clearHistoryAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history")));
connect(m_clearHistoryAction, &QAction::triggered, this, [this]() {
m_lastTriggered.clear();
});
}
void fillRows(QList<KCommandBarModel::Item> &rows, const QString &title, const QList<QAction *> &actions, std::unordered_set<QAction *> &uniqueActions)
{
for (const auto &action : actions) {
// We don't want diabled actions
if (!action->isEnabled()) {
continue;
}
// Is this action actually a menu?
if (auto menu = action->menu()) {
auto menuActionList = menu->actions();
// Empty? => Maybe the menu loads action on aboutToShow()?
if (menuActionList.isEmpty()) {
Q_EMIT menu->aboutToShow();
menuActionList = menu->actions();
}
const QString menuTitle = menu->title();
fillRows(rows, menuTitle, menuActionList, uniqueActions);
continue;
}
if (action->text().isEmpty() && !action->isSeparator()) {
qCWarning(KCONFIG_WIDGETS_LOG) << "Action" << action << "in group" << title << "has no text";
continue;
}
if (uniqueActions.insert(action).second) {
rows.push_back(KCommandBarModel::Item{title, action, -1});
}
}
}
void KCommandBarModel::refresh(const QList<KCommandBar::ActionGroup> &actionGroups)
{
int totalActions = std::accumulate(actionGroups.begin(), actionGroups.end(), 0, [](int a, const KCommandBar::ActionGroup &ag) {
return a + ag.actions.count();
});
++totalActions; // for m_clearHistoryAction
QList<Item> temp_rows;
std::unordered_set<QAction *> uniqueActions;
temp_rows.reserve(totalActions);
for (const auto &ag : actionGroups) {
const auto &agActions = ag.actions;
fillRows(temp_rows, ag.name, agActions, uniqueActions);
}
temp_rows.push_back({tr("Command Bar"), m_clearHistoryAction, -1});
/**
* For each action in last triggered actions,
* - Find it in the actions
* - Use the score variable to set its score
*
* Items in m_lastTriggered are stored in descending order
* by their usage i.e., the first item in the vector is the most
* recently invoked action.
*
* Here we traverse them in reverse order, i.e., from least recent to
* most recent and then assign a score to them in a way that most recent
* ends up having the highest score. Thus when proxy model does the sorting
* later, most recent item will end up on the top
*/
int score = 0;
std::for_each(m_lastTriggered.crbegin(), m_lastTriggered.crend(), [&score, &temp_rows](const QString &act) {
auto it = std::find_if(temp_rows.begin(), temp_rows.end(), [act](const KCommandBarModel::Item &i) {
return i.action->text() == act;
});
if (it != temp_rows.end()) {
it->score = score++;
}
});
beginResetModel();
m_rows = std::move(temp_rows);
endResetModel();
}
QVariant KCommandBarModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
const auto &entry = m_rows[index.row()];
const int col = index.column();
switch (role) {
case Qt::DisplayRole:
if (col == Column_Command) {
return entry.displayName();
}
Q_ASSERT(col == Column_Shortcut);
return entry.action->shortcut().toString();
case Qt::DecorationRole:
if (col == Column_Command) {
return entry.action->icon();
}
break;
case Qt::ToolTipRole: {
QString toolTip = entry.displayName();
if (!entry.action->shortcut().isEmpty()) {
toolTip += QLatin1Char('\n');
toolTip += entry.action->shortcut().toString();
}
return toolTip;
}
case Qt::UserRole: {
return QVariant::fromValue(entry.action);
}
case Role::Score:
return entry.score;
}
return {};
}
void KCommandBarModel::actionTriggered(const QString &name)
{
if (m_lastTriggered.size() == 6) {
m_lastTriggered.pop_back();
}
m_lastTriggered.push_front(name);
}
QStringList KCommandBarModel::lastUsedActions() const
{
return m_lastTriggered;
}
void KCommandBarModel::setLastUsedActions(const QStringList &actionNames)
{
m_lastTriggered = actionNames;
while (m_lastTriggered.size() > 6) {
m_lastTriggered.pop_back();
}
}
#include "moc_kcommandbarmodel_p.cpp"
@@ -0,0 +1,106 @@
/*
SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KCOMMANDBARMODEL_H
#define KCOMMANDBARMODEL_H
#include "kcommandbar.h"
#include <QAbstractTableModel>
#include <QList>
class QAction;
class KCommandBarModel final : public QAbstractTableModel
{
Q_OBJECT
public:
struct Item {
QString displayName() const;
QString groupName;
QAction *action;
int score;
};
KCommandBarModel(QObject *parent = nullptr);
enum Role { Score = Qt::UserRole + 1 };
enum Column { Column_Command, Column_Shortcut, Column_Count };
/**
* Resets the model
*
* If you are using last Used actions functionality, make sure
* to set the last used actions before calling this function
*/
void refresh(const QList<KCommandBar::ActionGroup> &actionGroups);
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
if (parent.isValid()) {
return 0;
}
return m_rows.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
Q_UNUSED(parent);
return Column_Count;
}
/**
* reimplemented function to update score that is calculated by KFuzzyMatcher
*/
bool setData(const QModelIndex &index, const QVariant &value, int role) override
{
if (!index.isValid())
return false;
if (role == Role::Score) {
auto row = index.row();
m_rows[row].score = value.toInt();
}
return QAbstractTableModel::setData(index, value, role);
}
QVariant data(const QModelIndex &index, int role) const override;
/**
* action with name @p name was triggered, store it in m_lastTriggered
*/
void actionTriggered(const QString &name);
/**
* last used actions
* max = 6;
*/
QStringList lastUsedActions() const;
/**
* incoming lastUsedActions
*
* should be set before calling refresh()
*/
void setLastUsedActions(const QStringList &actionNames);
private:
QList<Item> m_rows;
/**
* Last triggered actions by user
*
* Ordered descending i.e., least recently invoked
* action is at the end
*/
QStringList m_lastTriggered;
QAction *m_clearHistoryAction;
};
Q_DECLARE_TYPEINFO(KCommandBarModel::Item, Q_RELOCATABLE_TYPE);
#endif // KCOMMANDBARMODEL_H
@@ -0,0 +1,358 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2003 Benjamin C Meyer <ben+kdelibs at meyerhome dot net>
SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
SPDX-FileCopyrightText: 2004 Michael Brade <brade@kde.org>
SPDX-FileCopyrightText: 2021 Ahmad Samir <a.samirh78@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kconfigdialog.h"
#include <KCoreConfigSkeleton>
#include <KLocalizedString>
#include <KPageWidgetModel>
#include <kconfigdialogmanager.h>
#include <khelpclient.h>
#include <QDialogButtonBox>
#include <QIcon>
#include <QPushButton>
#include <QScrollArea>
#include <QScrollBar>
#include <QVBoxLayout>
#include <vector>
class KConfigDialogPrivate
{
public:
KConfigDialogPrivate(const QString &name, KCoreConfigSkeleton *config, KConfigDialog *qq)
: q(qq)
{
const QString dialogName = !name.isEmpty() ? name : QString::asprintf("SettingsDialog-%p", static_cast<void *>(q));
q->setObjectName(dialogName);
q->setWindowTitle(i18nc("@title:window", "Configure"));
q->setFaceType(KPageDialog::List);
s_openDialogs.push_back({dialogName, q});
QDialogButtonBox *buttonBox = q->buttonBox();
buttonBox->setStandardButtons(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel
| QDialogButtonBox::Help);
QObject::connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, q, &KConfigDialog::updateSettings);
QObject::connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, q, &KConfigDialog::updateSettings);
QObject::connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, q, [this]() {
updateButtons();
});
QObject::connect(buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, q, &KConfigDialog::updateWidgets);
QObject::connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, q, &KConfigDialog::updateWidgetsDefault);
QObject::connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, q, [this]() {
updateButtons();
});
QObject::connect(buttonBox->button(QDialogButtonBox::Help), &QAbstractButton::clicked, q, &KConfigDialog::showHelp);
QObject::connect(q, &KPageDialog::pageRemoved, q, &KConfigDialog::onPageRemoved);
manager = new KConfigDialogManager(q, config);
setupManagerConnections(manager);
if (QPushButton *applyButton = q->buttonBox()->button(QDialogButtonBox::Apply)) {
applyButton->setEnabled(false);
};
}
KPageWidgetItem *addPageInternal(QWidget *page, const QString &itemName, const QString &pixmapName, const QString &header);
void setupManagerConnections(KConfigDialogManager *manager);
void updateApplyButton();
void updateDefaultsButton();
void updateButtons();
void settingsChangedSlot();
KConfigDialog *const q;
QString mAnchor;
QString mHelpApp;
bool shown = false;
KConfigDialogManager *manager = nullptr;
struct WidgetManager {
QWidget *widget = nullptr;
KConfigDialogManager *manager = nullptr;
};
std::vector<WidgetManager> m_managerForPage;
/**
* The list of existing dialogs.
*/
struct OpenDialogInfo {
QString dialogName;
KConfigDialog *dialog = nullptr;
};
static std::vector<OpenDialogInfo> s_openDialogs;
};
std::vector<KConfigDialogPrivate::OpenDialogInfo> KConfigDialogPrivate::s_openDialogs;
KConfigDialog::KConfigDialog(QWidget *parent, const QString &name, KCoreConfigSkeleton *config)
: KPageDialog(parent)
, d(new KConfigDialogPrivate(name, config, this))
{
}
KConfigDialog::~KConfigDialog()
{
auto &openDlgs = KConfigDialogPrivate::s_openDialogs;
const QString currentObjectName = objectName();
auto it = std::find_if(openDlgs.cbegin(), openDlgs.cend(), [=](const KConfigDialogPrivate::OpenDialogInfo &info) {
return currentObjectName == info.dialogName;
});
if (it != openDlgs.cend()) {
openDlgs.erase(it);
}
}
KPageWidgetItem *KConfigDialog::addPage(QWidget *page, const QString &itemName, const QString &pixmapName, const QString &header, bool manage)
{
Q_ASSERT(page);
if (!page) {
return nullptr;
}
KPageWidgetItem *item = d->addPageInternal(page, itemName, pixmapName, header);
if (manage) {
d->manager->addWidget(page);
}
if (d->shown && manage) {
// update the default button if the dialog is shown
QPushButton *defaultButton = buttonBox()->button(QDialogButtonBox::RestoreDefaults);
if (defaultButton) {
bool is_default = defaultButton->isEnabled() && d->manager->isDefault();
defaultButton->setEnabled(!is_default);
}
}
return item;
}
KPageWidgetItem *KConfigDialog::addPage(QWidget *page, KCoreConfigSkeleton *config, const QString &itemName, const QString &pixmapName, const QString &header)
{
Q_ASSERT(page);
if (!page) {
return nullptr;
}
KPageWidgetItem *item = d->addPageInternal(page, itemName, pixmapName, header);
auto *manager = new KConfigDialogManager(page, config);
d->m_managerForPage.push_back({page, manager});
d->setupManagerConnections(manager);
if (d->shown) {
// update the default button if the dialog is shown
QPushButton *defaultButton = buttonBox()->button(QDialogButtonBox::RestoreDefaults);
if (defaultButton) {
const bool is_default = defaultButton->isEnabled() && manager->isDefault();
defaultButton->setEnabled(!is_default);
}
}
return item;
}
KPageWidgetItem *KConfigDialogPrivate::addPageInternal(QWidget *page, const QString &itemName, const QString &pixmapName, const QString &header)
{
QWidget *frame = new QWidget(q);
QVBoxLayout *boxLayout = new QVBoxLayout(frame);
boxLayout->setContentsMargins(0, 0, 0, 0);
boxLayout->setContentsMargins(0, 0, 0, 0);
QScrollArea *scroll = new QScrollArea(q);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scroll->setWidget(page);
scroll->setWidgetResizable(true);
scroll->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
if (page->minimumSizeHint().height() > scroll->sizeHint().height() - 2) {
if (page->sizeHint().width() < scroll->sizeHint().width() + 2) {
// QScrollArea is planning only a vertical scroll bar,
// try to avoid the horizontal one by reserving space for the vertical one.
// Currently KPageViewPrivate::_k_modelChanged() queries the minimumSizeHint().
// We can only set the minimumSize(), so this approach relies on QStackedWidget size calculation.
scroll->setMinimumWidth(scroll->sizeHint().width() + qBound(0, scroll->verticalScrollBar()->sizeHint().width(), 200) + 4);
}
}
boxLayout->addWidget(scroll);
KPageWidgetItem *item = new KPageWidgetItem(frame, itemName);
item->setHeader(header);
if (!pixmapName.isEmpty()) {
item->setIcon(QIcon::fromTheme(pixmapName));
}
q->KPageDialog::addPage(item);
return item;
}
void KConfigDialogPrivate::setupManagerConnections(KConfigDialogManager *manager)
{
q->connect(manager, qOverload<>(&KConfigDialogManager::settingsChanged), q, [this]() {
settingsChangedSlot();
});
q->connect(manager, &KConfigDialogManager::widgetModified, q, [this]() {
updateButtons();
});
QDialogButtonBox *buttonBox = q->buttonBox();
q->connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, manager, &KConfigDialogManager::updateSettings);
q->connect(buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, manager, &KConfigDialogManager::updateSettings);
q->connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, manager, &KConfigDialogManager::updateWidgets);
q->connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, manager, &KConfigDialogManager::updateWidgetsDefault);
}
void KConfigDialogPrivate::updateApplyButton()
{
QPushButton *applyButton = q->buttonBox()->button(QDialogButtonBox::Apply);
if (!applyButton) {
return;
}
const bool hasManagerChanged = std::any_of(m_managerForPage.cbegin(), m_managerForPage.cend(), [](const WidgetManager &widgetManager) {
return widgetManager.manager->hasChanged();
});
applyButton->setEnabled(manager->hasChanged() || q->hasChanged() || hasManagerChanged);
}
void KConfigDialogPrivate::updateDefaultsButton()
{
QPushButton *restoreDefaultsButton = q->buttonBox()->button(QDialogButtonBox::RestoreDefaults);
if (!restoreDefaultsButton) {
return;
}
const bool isManagerDefaulted = std::all_of(m_managerForPage.cbegin(), m_managerForPage.cend(), [](const WidgetManager &widgetManager) {
return widgetManager.manager->isDefault();
});
restoreDefaultsButton->setDisabled(manager->isDefault() && q->isDefault() && isManagerDefaulted);
}
void KConfigDialog::onPageRemoved(KPageWidgetItem *item)
{
auto it = std::find_if(d->m_managerForPage.cbegin(), d->m_managerForPage.cend(), [item](const KConfigDialogPrivate::WidgetManager &wm) {
return item->widget()->isAncestorOf(wm.widget);
});
if (it != d->m_managerForPage.cend()) { // There is a manager for this page, so remove it
delete it->manager;
d->m_managerForPage.erase(it);
d->updateButtons();
}
}
KConfigDialog *KConfigDialog::exists(const QString &name)
{
auto &openDlgs = KConfigDialogPrivate::s_openDialogs;
auto it = std::find_if(openDlgs.cbegin(), openDlgs.cend(), [name](const KConfigDialogPrivate::OpenDialogInfo &info) {
return name == info.dialogName;
});
return it != openDlgs.cend() ? it->dialog : nullptr;
}
bool KConfigDialog::showDialog(const QString &name)
{
KConfigDialog *dialog = exists(name);
if (dialog) {
dialog->show();
}
return (dialog != nullptr);
}
void KConfigDialogPrivate::updateButtons()
{
static bool only_once = false;
if (only_once) {
return;
}
only_once = true;
updateApplyButton();
updateDefaultsButton();
Q_EMIT q->widgetModified();
only_once = false;
}
void KConfigDialogPrivate::settingsChangedSlot()
{
// Update the buttons
updateButtons();
Q_EMIT q->settingsChanged(q->objectName());
}
void KConfigDialog::showEvent(QShowEvent *e)
{
if (!d->shown) {
updateWidgets();
d->manager->updateWidgets();
for (auto [widget, manager] : d->m_managerForPage) {
manager->updateWidgets();
}
d->updateApplyButton();
d->updateDefaultsButton();
d->shown = true;
}
KPageDialog::showEvent(e);
}
void KConfigDialog::updateSettings()
{
}
void KConfigDialog::updateWidgets()
{
}
void KConfigDialog::updateWidgetsDefault()
{
}
bool KConfigDialog::hasChanged()
{
return false;
}
bool KConfigDialog::isDefault()
{
return true;
}
void KConfigDialog::updateButtons()
{
d->updateButtons();
}
void KConfigDialog::settingsChangedSlot()
{
d->settingsChangedSlot();
}
void KConfigDialog::setHelp(const QString &anchor, const QString &appname)
{
d->mAnchor = anchor;
d->mHelpApp = appname;
}
void KConfigDialog::showHelp()
{
KHelpClient::invokeHelp(d->mAnchor, d->mHelpApp);
}
#include "moc_kconfigdialog.cpp"
@@ -0,0 +1,254 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2003 Benjamin C Meyer <ben+kdelibs at meyerhome dot net>
SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KCONFIGDIALOG_H
#define KCONFIGDIALOG_H
#include <KPageDialog>
#include <memory>
#include "kconfigwidgets_export.h"
class KConfig;
class KCoreConfigSkeleton;
class KConfigDialogManager;
/**
* @class KConfigDialog kconfigdialog.h KConfigDialog
*
* \short Standard %KDE configuration dialog class
*
* The KConfigDialog class provides an easy and uniform means of displaying
* a settings dialog using KPageDialog, KConfigDialogManager and a
* KConfigSkeleton derived settings class.
*
* KConfigDialog handles the enabling and disabling of buttons, creation
* of the dialog, and deletion of the widgets. Because of
* KConfigDialogManager, this class also manages: restoring
* the settings, resetting them to the default values, and saving them. This
* requires that the names of the widgets corresponding to configuration entries
* have to have the same name plus an additional "kcfg_" prefix. For example the
* widget named "kcfg_MyOption" would be associated with the configuration entry
* "MyOption".
*
* Here is an example usage of KConfigDialog:
*
* @code
* void KCoolApp::showSettings(){
* if (KConfigDialog::showDialog(QStringLiteral("settings"))) {
* return;
* }
* KConfigDialog *dialog = new KConfigDialog(this, QStringLiteral("settings"), MySettings::self());
* dialog->setFaceType(KPageDialog::List);
* dialog->addPage(new General(0, "General"), i18n("General"));
* dialog->addPage(new Appearance(0, "Style"), i18n("Appearance"));
* connect(dialog, &KConfigDialog::settingsChanged, mainWidget, &Bar::loadSettings);
* connect(dialog, &KConfigDialog::settingsChanged, this, &Foo::loadSettings);
* dialog->show();
* }
* @endcode
*
* Other than the above code, each class that has settings in the dialog should
* have a loadSettings() type slot to read settings and perform any
* necessary changes.
*
* For dialog appearance options (like buttons, default button, ...) please see
* @ref KPageDialog.
*
* @see KConfigSkeleton
* @author Waldo Bastian <bastian@kde.org>
*/
class KCONFIGWIDGETS_EXPORT KConfigDialog : public KPageDialog
{
Q_OBJECT
Q_SIGNALS:
/**
* A widget in the dialog was modified.
*/
void widgetModified();
/**
* One or more of the settings have been permanently changed such as if
* the user clicked on the Apply or Ok button.
* @param dialogName the name of the dialog.
*/
void settingsChanged(const QString &dialogName);
public:
/**
* @param parent - The parent of this object. Even though the class
* deletes itself the parent should be set so the dialog can be centered
* with the application on the screen.
*
* @param name - The name of this object. The name is used in determining if
* there can be more than one dialog at a time. Use names such as:
* "Font Settings" or "Color Settings" and not just "Settings" in
* applications where there is more than one dialog.
*
* @param config - Config object containing settings.
*/
KConfigDialog(QWidget *parent, const QString &name, KCoreConfigSkeleton *config);
/**
* Deconstructor, removes name from the list of open dialogs.
* Deletes private class.
* @see exists()
*/
~KConfigDialog() override;
/**
* Adds page to the dialog and to KConfigDialogManager. When an
* application is done adding pages show() should be called to
* display the dialog.
* @param page - Pointer to the page that is to be added to the dialog.
* This object is reparented.
* @param itemName - Name of the page.
* @param pixmapName - Name of the icon that should be used, if needed, when
* displaying the page. The string may either be the name of a themed
* icon (e.g. "document-save"), which the internal icon loader will be
* used to retrieve, or an absolute path to the pixmap on disk.
* @param header - Header text use in the list modes. Ignored in Tabbed
* mode. If empty, the itemName text is used when needed.
* @param manage - Whether KConfigDialogManager should manage the page or not.
* @returns The KPageWidgetItem associated with the page.
*/
KPageWidgetItem *
addPage(QWidget *page, const QString &itemName, const QString &pixmapName = QString(), const QString &header = QString(), bool manage = true);
/**
* Adds page to the dialog that is managed by a custom KConfigDialogManager.
* This is useful for dialogs that contain settings spread over more than
* one configuration file and thus have/need more than one KConfigSkeleton.
* When an application is done adding pages show() should be called to
* display the dialog.
* @param page - Pointer to the page that is to be added to the dialog.
* This object is reparented.
* @param config - Config object containing corresponding settings.
* @param itemName - Name of the page.
* @param pixmapName - Name of the icon that should be used, if needed, when
* displaying the page. The string may either be the name of a themed
* icon (e.g. "document-save"), which the internal icon loader will be
* used to retrieve, or an absolute path to the pixmap on disk.
* @param header - Header text use in the list modes. Ignored in Tabbed
* mode. If empty, the itemName text is used when needed.
* @returns The KPageWidgetItem associated with the page.
*/
KPageWidgetItem *
addPage(QWidget *page, KCoreConfigSkeleton *config, const QString &itemName, const QString &pixmapName = QString(), const QString &header = QString());
/**
* See if a dialog with the name 'name' already exists.
* @see showDialog()
* @param name - Dialog name to look for.
* @return Pointer to widget or NULL if it does not exist.
*/
static KConfigDialog *exists(const QString &name);
/**
* Attempts to show the dialog with the name 'name'.
* @see exists()
* @param name - The name of the dialog to show.
* @return True if the dialog 'name' exists and was shown.
*/
static bool showDialog(const QString &name);
protected Q_SLOTS:
/**
* Update the settings from the dialog.
* Virtual function for custom additions.
*
* Example use: User clicks Ok or Apply button in a configure dialog.
*/
virtual void updateSettings();
/**
* Update the dialog based on the settings.
* Virtual function for custom additions.
*
* Example use: Initialisation of dialog.
* Example use: User clicks Reset button in a configure dialog.
*/
virtual void updateWidgets();
/**
* Update the dialog based on the default settings.
* Virtual function for custom additions.
*
* Example use: User clicks Defaults button in a configure dialog.
*/
virtual void updateWidgetsDefault();
/**
* Updates the Apply and Default buttons.
* Connect to this slot if you implement your own hasChanged()
* or isDefault() methods for widgets not managed by KConfig.
* @since 4.3
*/
void updateButtons();
/**
* Some setting was changed. Emit the signal with the dialogs name.
* Connect to this slot if there are widgets not managed by KConfig.
* @since 4.3
*/
void settingsChangedSlot();
/**
* Sets the help path and topic.
*
* The HTML file will be found using the X-DocPath entry in the application's desktop file.
* It can be either a relative path, or a website URL.
*
* @param anchor This has to be a defined anchor in your
* docbook sources or website. If empty the main index
* is loaded.
* @param appname This allows you to specify the .desktop file to get the help path from.
* If empty the QCoreApplication::applicationName() is used.
*/
void setHelp(const QString &anchor, const QString &appname = QString());
/**
* Displays help for this config dialog.
* @since 5.0
*/
virtual void showHelp();
protected:
/**
* Returns whether the current state of the dialog is
* different from the current configuration.
* Virtual function for custom additions.
*/
virtual bool hasChanged();
/**
* Returns whether the current state of the dialog is
* the same as the default configuration.
*/
virtual bool isDefault();
/**
* @internal
*/
void showEvent(QShowEvent *e) override;
private Q_SLOTS:
/**
* Slot which cleans up the KConfigDialogManager of the page.
* */
KCONFIGWIDGETS_NO_EXPORT void onPageRemoved(KPageWidgetItem *item);
private:
friend class KConfigDialogPrivate;
std::unique_ptr<class KConfigDialogPrivate> const d;
Q_DISABLE_COPY(KConfigDialog)
};
#endif // KCONFIGDIALOG_H
@@ -0,0 +1,651 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2003 Benjamin C Meyer <ben+kdelibs at meyerhome dot net>
SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau <kossebau@kde.org>
SPDX-FileCopyrightText: 2020 Kevin Ottens <kevin.ottens@enioka.com>
SPDX-FileCopyrightText: 2020 Cyril Rossi <cyril.rossi@enioka.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kconfigdialogmanager.h"
#include "kconfigdialogmanager_p.h"
#include "kconfigwidgets_debug.h"
#include <QComboBox>
#include <QGroupBox>
#include <QLabel>
#include <QLayout>
#include <QMetaObject>
#include <QMetaProperty>
#include <QRadioButton>
#include <QTimer>
#include <KConfigSkeleton>
typedef QHash<QString, QByteArray> MyHash;
Q_GLOBAL_STATIC(MyHash, s_propertyMap)
Q_GLOBAL_STATIC(MyHash, s_changedMap)
KConfigDialogManager::KConfigDialogManager(QWidget *parent, KCoreConfigSkeleton *conf)
: QObject(parent)
, d(new KConfigDialogManagerPrivate(this))
{
d->m_conf = conf;
d->m_dialog = parent;
init(true);
}
KConfigDialogManager::~KConfigDialogManager() = default;
// KF6: Drop this and get signals only from metaObject and/or widget's dynamic properties kcfg_property/kcfg_propertyNotify
void KConfigDialogManager::initMaps()
{
if (s_propertyMap()->isEmpty()) {
s_propertyMap()->insert(QStringLiteral("KButtonGroup"), "current");
s_propertyMap()->insert(QStringLiteral("KColorButton"), "color");
s_propertyMap()->insert(QStringLiteral("KColorCombo"), "color");
s_propertyMap()->insert(QStringLiteral("KKeySequenceWidget"), "keySequence");
}
if (s_changedMap()->isEmpty()) {
// Qt
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
s_changedMap()->insert(QStringLiteral("QCheckBox"), SIGNAL(checkStateChanged(Qt::CheckState)));
#else
s_changedMap()->insert(QStringLiteral("QCheckBox"), SIGNAL(stateChanged(int)));
#endif
s_changedMap()->insert(QStringLiteral("QPushButton"), SIGNAL(clicked(bool)));
s_changedMap()->insert(QStringLiteral("QRadioButton"), SIGNAL(toggled(bool)));
s_changedMap()->insert(QStringLiteral("QGroupBox"), SIGNAL(toggled(bool)));
s_changedMap()->insert(QStringLiteral("QComboBox"), SIGNAL(activated(int)));
s_changedMap()->insert(QStringLiteral("QDateEdit"), SIGNAL(dateChanged(QDate)));
s_changedMap()->insert(QStringLiteral("QTimeEdit"), SIGNAL(timeChanged(QTime)));
s_changedMap()->insert(QStringLiteral("QDateTimeEdit"), SIGNAL(dateTimeChanged(QDateTime)));
s_changedMap()->insert(QStringLiteral("QDial"), SIGNAL(valueChanged(int)));
s_changedMap()->insert(QStringLiteral("QDoubleSpinBox"), SIGNAL(valueChanged(double)));
s_changedMap()->insert(QStringLiteral("QLineEdit"), SIGNAL(textChanged(QString)));
s_changedMap()->insert(QStringLiteral("QSlider"), SIGNAL(valueChanged(int)));
s_changedMap()->insert(QStringLiteral("QSpinBox"), SIGNAL(valueChanged(int)));
s_changedMap()->insert(QStringLiteral("QTextEdit"), SIGNAL(textChanged()));
s_changedMap()->insert(QStringLiteral("QTextBrowser"), SIGNAL(sourceChanged(QString)));
s_changedMap()->insert(QStringLiteral("QPlainTextEdit"), SIGNAL(textChanged()));
s_changedMap()->insert(QStringLiteral("QTabWidget"), SIGNAL(currentChanged(int)));
// KDE
s_changedMap()->insert(QStringLiteral("KComboBox"), SIGNAL(activated(int)));
s_changedMap()->insert(QStringLiteral("KFontComboBox"), SIGNAL(activated(int)));
s_changedMap()->insert(QStringLiteral("KFontRequester"), SIGNAL(fontSelected(QFont)));
s_changedMap()->insert(QStringLiteral("KFontChooser"), SIGNAL(fontSelected(QFont)));
s_changedMap()->insert(QStringLiteral("KColorCombo"), SIGNAL(activated(QColor)));
s_changedMap()->insert(QStringLiteral("KColorButton"), SIGNAL(changed(QColor)));
s_changedMap()->insert(QStringLiteral("KDatePicker"), SIGNAL(dateSelected(QDate)));
s_changedMap()->insert(QStringLiteral("KDateWidget"), SIGNAL(changed(QDate)));
s_changedMap()->insert(QStringLiteral("KDateTimeWidget"), SIGNAL(valueChanged(QDateTime)));
s_changedMap()->insert(QStringLiteral("KEditListWidget"), SIGNAL(changed()));
s_changedMap()->insert(QStringLiteral("KListWidget"), SIGNAL(itemSelectionChanged()));
s_changedMap()->insert(QStringLiteral("KLineEdit"), SIGNAL(textChanged(QString)));
s_changedMap()->insert(QStringLiteral("KRestrictedLine"), SIGNAL(textChanged(QString)));
s_changedMap()->insert(QStringLiteral("KTextEdit"), SIGNAL(textChanged()));
s_changedMap()->insert(QStringLiteral("KUrlRequester"), SIGNAL(textChanged(QString)));
s_changedMap()->insert(QStringLiteral("KUrlComboRequester"), SIGNAL(textChanged(QString)));
s_changedMap()->insert(QStringLiteral("KUrlComboBox"), SIGNAL(urlActivated(QUrl)));
s_changedMap()->insert(QStringLiteral("KButtonGroup"), SIGNAL(changed(int)));
}
}
QHash<QString, QByteArray> *KConfigDialogManager::propertyMap()
{
initMaps();
return s_propertyMap();
}
void KConfigDialogManager::init(bool trackChanges)
{
initMaps();
d->trackChanges = trackChanges;
// Go through all of the children of the widgets and find all known widgets
(void)parseChildren(d->m_dialog, trackChanges);
}
void KConfigDialogManager::addWidget(QWidget *widget)
{
(void)parseChildren(widget, true);
}
void KConfigDialogManager::setupWidget(QWidget *widget, KConfigSkeletonItem *item)
{
QVariant minValue = item->minValue();
if (minValue.isValid()) {
// KSelector is using this property
if (widget->metaObject()->indexOfProperty("minValue") != -1) {
widget->setProperty("minValue", minValue);
}
if (widget->metaObject()->indexOfProperty("minimum") != -1) {
widget->setProperty("minimum", minValue);
}
}
QVariant maxValue = item->maxValue();
if (maxValue.isValid()) {
// KSelector is using this property
if (widget->metaObject()->indexOfProperty("maxValue") != -1) {
widget->setProperty("maxValue", maxValue);
}
if (widget->metaObject()->indexOfProperty("maximum") != -1) {
widget->setProperty("maximum", maxValue);
}
}
if (widget->whatsThis().isEmpty()) {
QString whatsThis = item->whatsThis();
if (!whatsThis.isEmpty()) {
widget->setWhatsThis(whatsThis);
}
}
if (widget->toolTip().isEmpty()) {
QString toolTip = item->toolTip();
if (!toolTip.isEmpty()) {
widget->setToolTip(toolTip);
}
}
// If it is a QGroupBox with only autoExclusive buttons
// and has no custom property and the config item type
// is an integer, assume we want to save the index like we did with
// KButtonGroup instead of if it is checked or not
QGroupBox *gb = qobject_cast<QGroupBox *>(widget);
if (gb && getCustomProperty(gb).isEmpty()) {
const KConfigSkeletonItem *item = d->m_conf->findItem(widget->objectName().mid(5));
if (item->property().userType() == QMetaType::Int) {
QObjectList children = gb->children();
children.removeAll(gb->layout());
const QList<QAbstractButton *> buttons = gb->findChildren<QAbstractButton *>();
bool allAutoExclusiveDirectChildren = true;
for (QAbstractButton *button : buttons) {
allAutoExclusiveDirectChildren = allAutoExclusiveDirectChildren && button->autoExclusive() && button->parent() == gb;
}
if (allAutoExclusiveDirectChildren) {
d->allExclusiveGroupBoxes << widget;
}
}
}
if (!item->isEqual(property(widget))) {
setProperty(widget, item->property());
}
d->updateWidgetIndicator(item->name(), widget);
}
bool KConfigDialogManager::parseChildren(const QWidget *widget, bool trackChanges)
{
bool valueChanged = false;
const QList<QObject *> listOfChildren = widget->children();
if (listOfChildren.isEmpty()) { //?? XXX
return valueChanged;
}
const QMetaMethod onWidgetModifiedSlot = metaObject()->method(metaObject()->indexOfSlot("onWidgetModified()"));
Q_ASSERT(onWidgetModifiedSlot.isValid() && metaObject()->indexOfSlot("onWidgetModified()") >= 0);
for (QObject *object : listOfChildren) {
if (!object->isWidgetType()) {
continue; // Skip non-widgets
}
QWidget *childWidget = static_cast<QWidget *>(object);
QString widgetName = childWidget->objectName();
bool bParseChildren = true;
bool bSaveInsideGroupBox = d->insideGroupBox;
if (widgetName.startsWith(QLatin1String("kcfg_"))) {
// This is one of our widgets!
QString configId = widgetName.mid(5);
KConfigSkeletonItem *item = d->m_conf->findItem(configId);
if (item) {
d->knownWidget.insert(configId, childWidget);
setupWidget(childWidget, item);
if (trackChanges) {
bool changeSignalFound = false;
if (d->allExclusiveGroupBoxes.contains(childWidget)) {
const QList<QAbstractButton *> buttons = childWidget->findChildren<QAbstractButton *>();
for (QAbstractButton *button : buttons) {
connect(button, &QAbstractButton::toggled, this, [this] {
d->onWidgetModified();
});
}
}
QByteArray propertyChangeSignal = getCustomPropertyChangedSignal(childWidget);
if (propertyChangeSignal.isEmpty()) {
propertyChangeSignal = getUserPropertyChangedSignal(childWidget);
}
if (propertyChangeSignal.isEmpty()) {
// get the change signal from the meta object
const QMetaObject *metaObject = childWidget->metaObject();
QByteArray userproperty = getCustomProperty(childWidget);
if (userproperty.isEmpty()) {
userproperty = getUserProperty(childWidget);
}
if (!userproperty.isEmpty()) {
const int indexOfProperty = metaObject->indexOfProperty(userproperty.constData());
if (indexOfProperty != -1) {
const QMetaProperty property = metaObject->property(indexOfProperty);
const QMetaMethod notifySignal = property.notifySignal();
if (notifySignal.isValid()) {
connect(childWidget, notifySignal, this, onWidgetModifiedSlot);
changeSignalFound = true;
}
}
} else {
qCWarning(KCONFIG_WIDGETS_LOG) << "Don't know how to monitor widget" << childWidget->metaObject()->className() << "for changes!";
}
} else {
connect(childWidget, propertyChangeSignal.constData(), this, SLOT(onWidgetModified()));
changeSignalFound = true;
}
if (changeSignalFound) {
QComboBox *cb = qobject_cast<QComboBox *>(childWidget);
if (cb && cb->isEditable()) {
connect(cb, &QComboBox::editTextChanged, this, &KConfigDialogManager::widgetModified);
}
}
}
QGroupBox *gb = qobject_cast<QGroupBox *>(childWidget);
if (!gb) {
bParseChildren = false;
} else {
d->insideGroupBox = true;
}
} else {
qCWarning(KCONFIG_WIDGETS_LOG) << "A widget named" << widgetName << "was found but there is no setting named" << configId;
}
} else if (QLabel *label = qobject_cast<QLabel *>(childWidget)) {
QWidget *buddy = label->buddy();
if (!buddy) {
continue;
}
QString buddyName = buddy->objectName();
if (buddyName.startsWith(QLatin1String("kcfg_"))) {
// This is one of our widgets!
QString configId = buddyName.mid(5);
d->buddyWidget.insert(configId, childWidget);
}
}
// kf5: commented out to reduce debug output
// #ifndef NDEBUG
// else if (!widgetName.isEmpty() && trackChanges)
// {
// QHash<QString, QByteArray>::const_iterator changedIt = s_changedMap()->constFind(childWidget->metaObject()->className());
// if (changedIt != s_changedMap()->constEnd())
// {
// if ((!d->insideGroupBox || !qobject_cast<QRadioButton*>(childWidget)) &&
// !qobject_cast<QGroupBox*>(childWidget) &&!qobject_cast<QTabWidget*>(childWidget) )
// qCDebug(KCONFIG_WIDGETS_LOG) << "Widget '" << widgetName << "' (" << childWidget->metaObject()->className() << ") remains unmanaged.";
// }
// }
// #endif
if (bParseChildren) {
// this widget is not known as something we can store.
// Maybe we can store one of its children.
valueChanged |= parseChildren(childWidget, trackChanges);
}
d->insideGroupBox = bSaveInsideGroupBox;
}
return valueChanged;
}
void KConfigDialogManager::updateWidgets()
{
bool changed = false;
bool bSignalsBlocked = signalsBlocked();
blockSignals(true);
QWidget *widget;
QHashIterator<QString, QWidget *> it(d->knownWidget);
while (it.hasNext()) {
it.next();
widget = it.value();
KConfigSkeletonItem *item = d->m_conf->findItem(it.key());
if (!item) {
qCWarning(KCONFIG_WIDGETS_LOG) << "The setting" << it.key() << "has disappeared!";
continue;
}
if (!item->isEqual(property(widget))) {
setProperty(widget, item->property());
// qCDebug(KCONFIG_WIDGETS_LOG) << "The setting" << it.key() << "[" << widget->className() << "] has changed";
changed = true;
}
if (item->isImmutable()) {
widget->setEnabled(false);
QWidget *buddy = d->buddyWidget.value(it.key(), nullptr);
if (buddy) {
buddy->setEnabled(false);
}
}
}
blockSignals(bSignalsBlocked);
if (changed) {
QTimer::singleShot(0, this, &KConfigDialogManager::widgetModified);
d->updateAllWidgetIndicators();
}
}
void KConfigDialogManager::updateWidgetsDefault()
{
bool bUseDefaults = d->m_conf->useDefaults(true);
updateWidgets();
d->m_conf->useDefaults(bUseDefaults);
d->updateAllWidgetIndicators();
}
void KConfigDialogManager::setDefaultsIndicatorsVisible(bool enabled)
{
d->setDefaultsIndicatorsVisible(enabled);
}
void KConfigDialogManager::updateSettings()
{
bool changed = false;
QWidget *widget;
QHashIterator<QString, QWidget *> it(d->knownWidget);
while (it.hasNext()) {
it.next();
widget = it.value();
KConfigSkeletonItem *item = d->m_conf->findItem(it.key());
if (!item) {
qCWarning(KCONFIG_WIDGETS_LOG) << "The setting" << it.key() << "has disappeared!";
continue;
}
QVariant fromWidget = property(widget);
if (!item->isEqual(fromWidget)) {
item->setProperty(fromWidget);
changed = true;
}
}
if (changed) {
d->m_conf->save();
Q_EMIT settingsChanged();
d->updateAllWidgetIndicators();
}
}
QByteArray KConfigDialogManager::getUserProperty(const QWidget *widget) const
{
MyHash *map = s_propertyMap();
const QMetaObject *metaObject = widget->metaObject();
const QString className(QLatin1String(metaObject->className()));
auto it = map->find(className);
if (it == map->end()) {
const QMetaProperty userProp = metaObject->userProperty();
if (userProp.isValid()) {
it = map->insert(className, userProp.name());
// qCDebug(KCONFIG_WIDGETS_LOG) << "class name: '" << className
//<< " 's USER property: " << metaProperty.name() << endl;
} else {
return QByteArray(); // no USER property
}
}
const QComboBox *cb = qobject_cast<const QComboBox *>(widget);
if (cb) {
const char *qcomboUserPropertyName = cb->QComboBox::metaObject()->userProperty().name();
const int qcomboUserPropertyIndex = qcomboUserPropertyName ? cb->QComboBox::metaObject()->indexOfProperty(qcomboUserPropertyName) : -1;
const char *widgetUserPropertyName = metaObject->userProperty().name();
const int widgetUserPropertyIndex = widgetUserPropertyName ? cb->metaObject()->indexOfProperty(widgetUserPropertyName) : -1;
// no custom user property set on subclass of QComboBox?
if (qcomboUserPropertyIndex == widgetUserPropertyIndex) {
return QByteArray(); // use the q/kcombobox special code
}
}
return it != map->end() ? it.value() : QByteArray{};
}
QByteArray KConfigDialogManager::getCustomProperty(const QWidget *widget) const
{
QVariant prop(widget->property("kcfg_property"));
if (prop.isValid()) {
if (!prop.canConvert<QByteArray>()) {
qCWarning(KCONFIG_WIDGETS_LOG) << "kcfg_property on" << widget->metaObject()->className() << "is not of type ByteArray";
} else {
return prop.toByteArray();
}
}
return QByteArray();
}
QByteArray KConfigDialogManager::getUserPropertyChangedSignal(const QWidget *widget) const
{
const QString className = QLatin1String(widget->metaObject()->className());
auto changedIt = s_changedMap()->constFind(className);
if (changedIt == s_changedMap()->constEnd()) {
// If the class name of the widget wasn't in the monitored widgets map, then look for
// it again using the super class name. This fixes a problem with using QtRuby/Korundum
// widgets with KConfigXT where 'Qt::Widget' wasn't being seen a the real deal, even
// though it was a 'QWidget'.
if (widget->metaObject()->superClass()) {
const QString parentClassName = QLatin1String(widget->metaObject()->superClass()->className());
changedIt = s_changedMap()->constFind(parentClassName);
}
}
return (changedIt == s_changedMap()->constEnd()) ? QByteArray() : *changedIt;
}
QByteArray KConfigDialogManager::getCustomPropertyChangedSignal(const QWidget *widget) const
{
QVariant prop(widget->property("kcfg_propertyNotify"));
if (prop.isValid()) {
if (!prop.canConvert<QByteArray>()) {
qCWarning(KCONFIG_WIDGETS_LOG) << "kcfg_propertyNotify on" << widget->metaObject()->className() << "is not of type ByteArray";
} else {
return prop.toByteArray();
}
}
return QByteArray();
}
void KConfigDialogManager::setProperty(QWidget *w, const QVariant &v)
{
if (d->allExclusiveGroupBoxes.contains(w)) {
const QList<QAbstractButton *> buttons = w->findChildren<QAbstractButton *>();
if (v.toInt() < buttons.count()) {
buttons[v.toInt()]->setChecked(true);
}
return;
}
QByteArray userproperty = getCustomProperty(w);
if (userproperty.isEmpty()) {
userproperty = getUserProperty(w);
}
if (userproperty.isEmpty()) {
QComboBox *cb = qobject_cast<QComboBox *>(w);
if (cb) {
if (cb->isEditable()) {
int i = cb->findText(v.toString());
if (i != -1) {
cb->setCurrentIndex(i);
} else {
cb->setEditText(v.toString());
}
} else {
cb->setCurrentIndex(v.toInt());
}
return;
}
}
if (userproperty.isEmpty()) {
qCWarning(KCONFIG_WIDGETS_LOG) << w->metaObject()->className() << "widget not handled!";
return;
}
w->setProperty(userproperty.constData(), v);
}
QVariant KConfigDialogManager::property(QWidget *w) const
{
if (d->allExclusiveGroupBoxes.contains(w)) {
const QList<QAbstractButton *> buttons = w->findChildren<QAbstractButton *>();
for (int i = 0; i < buttons.count(); ++i) {
if (buttons[i]->isChecked()) {
return i;
}
}
return -1;
}
QByteArray userproperty = getCustomProperty(w);
if (userproperty.isEmpty()) {
userproperty = getUserProperty(w);
}
if (userproperty.isEmpty()) {
QComboBox *cb = qobject_cast<QComboBox *>(w);
if (cb) {
if (cb->isEditable()) {
return QVariant(cb->currentText());
} else {
return QVariant(cb->currentIndex());
}
}
}
if (userproperty.isEmpty()) {
qCWarning(KCONFIG_WIDGETS_LOG) << w->metaObject()->className() << "widget not handled!";
return QVariant();
}
return w->property(userproperty.constData());
}
bool KConfigDialogManager::hasChanged() const
{
QWidget *widget;
QHashIterator<QString, QWidget *> it(d->knownWidget);
while (it.hasNext()) {
it.next();
widget = it.value();
KConfigSkeletonItem *item = d->m_conf->findItem(it.key());
if (!item) {
qCWarning(KCONFIG_WIDGETS_LOG) << "The setting" << it.key() << "has disappeared!";
continue;
}
if (!item->isEqual(property(widget))) {
// qCDebug(KCONFIG_WIDGETS_LOG) << "Widget for '" << it.key() << "' has changed.";
return true;
}
}
return false;
}
bool KConfigDialogManager::isDefault() const
{
QWidget *widget;
QHashIterator<QString, QWidget *> it(d->knownWidget);
while (it.hasNext()) {
it.next();
widget = it.value();
KConfigSkeletonItem *item = d->m_conf->findItem(it.key());
if (!item) {
qCWarning(KCONFIG_WIDGETS_LOG) << "The setting" << it.key() << "has disappeared!";
continue;
}
if (property(widget) != item->getDefault()) {
return false;
}
}
return true;
}
KConfigDialogManagerPrivate::KConfigDialogManagerPrivate(KConfigDialogManager *qq)
: q(qq)
, insideGroupBox(false)
, defaultsIndicatorsVisible(false)
{
}
void KConfigDialogManagerPrivate::setDefaultsIndicatorsVisible(bool enabled)
{
if (defaultsIndicatorsVisible != enabled) {
defaultsIndicatorsVisible = enabled;
updateAllWidgetIndicators();
}
}
void KConfigDialogManagerPrivate::onWidgetModified()
{
const auto widget = qobject_cast<QWidget *>(q->sender());
Q_ASSERT(widget);
const QLatin1String prefix("kcfg_");
QString configId = widget->objectName();
if (configId.startsWith(prefix)) {
configId.remove(0, prefix.size());
updateWidgetIndicator(configId, widget);
} else {
auto *parent = qobject_cast<QWidget *>(widget->parent());
Q_ASSERT(parent);
configId = parent->objectName();
Q_ASSERT(configId.startsWith(prefix));
configId.remove(0, prefix.size());
updateWidgetIndicator(configId, parent);
}
Q_EMIT q->widgetModified();
}
void KConfigDialogManagerPrivate::updateWidgetIndicator(const QString &configId, QWidget *widget)
{
const auto item = m_conf->findItem(configId);
Q_ASSERT(item);
const auto widgetValue = q->property(widget);
const auto defaultValue = item->getDefault();
const auto defaulted = widgetValue == defaultValue;
if (allExclusiveGroupBoxes.contains(widget)) {
const QList<QAbstractButton *> buttons = widget->findChildren<QAbstractButton *>();
for (int i = 0; i < buttons.count(); i++) {
const auto value = widgetValue.toInt() == i && !defaulted && defaultsIndicatorsVisible;
buttons.at(i)->setProperty("_kde_highlight_neutral", value);
buttons.at(i)->update();
}
} else {
widget->setProperty("_kde_highlight_neutral", !defaulted && defaultsIndicatorsVisible);
widget->update();
}
}
void KConfigDialogManagerPrivate::updateAllWidgetIndicators()
{
QHashIterator<QString, QWidget *> it(knownWidget);
while (it.hasNext()) {
it.next();
updateWidgetIndicator(it.key(), it.value());
}
}
#include "moc_kconfigdialogmanager.cpp"
@@ -0,0 +1,303 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2003 Benjamin C Meyer <ben+kdelibs at meyerhome dot net>
SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KCONFIGDIALOGMANAGER_H
#define KCONFIGDIALOGMANAGER_H
#include <kconfigwidgets_export.h>
#include <QHash>
#include <QObject>
#include <memory>
class KConfigDialogManagerPrivate;
class KCoreConfigSkeleton;
class KConfigSkeleton;
class KConfigSkeletonItem;
class QWidget;
/**
* @class KConfigDialogManager kconfigdialogmanager.h KConfigDialogManager
*
* @short Provides a means of automatically retrieving,
* saving and resetting KConfigSkeleton based settings in a dialog.
*
* The KConfigDialogManager class provides a means of automatically
* retrieving, saving and resetting basic settings.
* It also can emit signals when settings have been changed
* (settings were saved) or modified (the user changes a checkbox
* from on to off).
*
* The object names of the widgets to be managed have to correspond to the names of the
* configuration entries in the KConfigSkeleton object plus an additional
* "kcfg_" prefix. For example a widget with the object name "kcfg_MyOption"
* would be associated to the configuration entry "MyOption".
*
* The widget classes of Qt and KDE Frameworks are supported out of the box,
* for other widgets see below:
*
* @par Using Custom Widgets
* @parblock
* Custom widget classes are supported if they have a Q_PROPERTY defined for the
* property representing the value edited by the widget. By default the property
* is used for which "USER true" is set. For using another property, see below.
*
* Example:
*
* A class ColorEditWidget is used in the settings UI to select a color. The
* color value is set and read as type QColor. For that it has a definition of
* the value property similar to this:
* \code
* Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged USER true)
* \endcode
* And of course it has the definition and implementation of the respective
* read & write methods and the notify signal.
* This class then can be used directly with KConfigDialogManager and does not need
* further setup.
* @endparblock
*
* @par Using Other Properties than The USER Property
* @parblock
* To use a widget's property that is not the USER property, the property to use
* can be selected by setting onto the widget instance a property with the key
* "kcfg_property" and as the value the name of the property:
* \code
* ColorEditWidget *myWidget = new ColorEditWidget;
* myWidget->setProperty("kcfg_property", QByteArray("redColorPart"));
* \endcode
* This selection of the property to use is just valid for this widget instance.
* When using a UI file, the "kcfg_property" property can also be set using Qt Designer.
* @endparblock
*
* @par Configuring Classes to use Other Properties Globally
* @parblock
* Alternatively a non-USER property can be defined for a widget class globally
* by registering it for the class in the KConfigDialogManager::propertyMap().
* This global registration has lower priority than any "kcfg_property" property
* set on a class instance though, so the latter overrules this global setting.
* Note: setting the property in the propertyMap affects any instances of that
* widget class in the current application, so use only when needed and prefer
* instead the "kcfg_property" property. Especially with software with many
* libraries and 3rd-party plugins in one process there is a chance of
* conflicting settings.
*
* Example:
*
* If the ColorEditWidget has another property redColor defined by
* \code
* Q_PROPERTY(int redColorPart READ redColorPart WRITE setRedColorPart NOTIFY redColorPartChanged)
* \endcode
* and this one should be used in the settings, call somewhere in the code before
* using the settings:
* \code
* KConfigDialogManager::propertyMap()->insert("ColorEditWidget", QByteArray("redColorPart"));
* \endcode
* @endparblock
*
* @par Using Different Signals than The NOTIFY Signal
* @parblock
* If some non-default signal should be used, e.g. because the property to use does not
* have a NOTIFY setting, for a given widget instance the signal to use can be set
* by a property with the key "kcfg_propertyNotify" and as the value the signal signature.
* This will take priority over the signal noted by NOTIFY for the chosen property
* as well as the content of KConfigDialogManager::changedMap(). Since 5.32.
*
* Example:
*
* If for a class OtherColorEditWidget there was no NOTIFY set on the USER property,
* but some signal colorSelected(QColor) defined which would be good enough to reflect
* the settings change, defined by
* \code
* Q_PROPERTY(QColor color READ color WRITE setColor USER true)
* Q_SIGNALS:
* void colorSelected(const QColor &color);
* \endcode
* the signal to use would be defined by this:
* \code
* OtherColorEditWidget *myWidget = new OtherColorEditWidget;
* myWidget->setProperty("kcfg_propertyNotify", QByteArray(SIGNAL(colorSelected(QColor))));
* \endcode
* @endparblock
*
* @author Benjamin C Meyer <ben+kdelibs at meyerhome dot net>
* @author Waldo Bastian <bastian@kde.org>
*/
class KCONFIGWIDGETS_EXPORT KConfigDialogManager : public QObject
{
Q_OBJECT
Q_SIGNALS:
/**
* One or more of the settings have been saved (such as when the user
* clicks on the Apply button). This is only emitted by updateSettings()
* whenever one or more setting were changed and consequently saved.
*/
void settingsChanged(); // clazy:exclude=overloaded-signal
/**
* If retrieveSettings() was told to track changes then if
* any known setting was changed this signal will be emitted. Note
* that a settings can be modified several times and might go back to the
* original saved state. hasChanged() will tell you if anything has
* actually changed from the saved values.
*/
void widgetModified();
public:
/**
* Constructor.
* @param parent Dialog widget to manage
* @param conf Object that contains settings
*/
KConfigDialogManager(QWidget *parent, KCoreConfigSkeleton *conf);
/**
* Destructor.
*/
~KConfigDialogManager() override;
/**
* Add additional widgets to manage
* @param widget Additional widget to manage, including all its children
*/
void addWidget(QWidget *widget);
/**
* Returns whether the current state of the known widgets are
* different from the state in the config object.
*/
bool hasChanged() const;
/**
* Returns whether the current state of the known widgets are
* the same as the default state in the config object.
*/
bool isDefault() const;
/**
* Retrieve the map between widgets class names and the
* USER properties used for the configuration values.
*/
static QHash<QString, QByteArray> *propertyMap();
public Q_SLOTS:
/**
* Traverse the specified widgets, saving the settings of all known
* widgets in the settings object.
*
* Example use: User clicks Ok or Apply button in a configure dialog.
*/
void updateSettings();
/**
* Traverse the specified widgets, sets the state of all known
* widgets according to the state in the settings object.
*
* Example use: Initialisation of dialog.
* Example use: User clicks Reset button in a configure dialog.
*/
void updateWidgets();
/**
* Traverse the specified widgets, sets the state of all known
* widgets according to the default state in the settings object.
*
* Example use: User clicks Defaults button in a configure dialog.
*/
void updateWidgetsDefault();
/**
* Show or hide an indicator when settings have changed from their default value.
* Update all widgets to show or hide the indicator accordingly.
*
* @since 5.73
*/
void setDefaultsIndicatorsVisible(bool enabled);
protected:
/**
* @param trackChanges - If any changes by the widgets should be tracked
* set true. This causes the emitting the modified() signal when
* something changes.
* TODO: @return bool - True if any setting was changed from the default.
*/
void init(bool trackChanges);
/**
* Recursive function that finds all known children.
* Goes through the children of widget and if any are known and not being
* ignored, stores them in currentGroup. Also checks if the widget
* should be disabled because it is set immutable.
* @param widget - Parent of the children to look at.
* @param trackChanges - If true then tracks any changes to the children of
* widget that are known.
* @return bool - If a widget was set to something other than its default.
*/
bool parseChildren(const QWidget *widget, bool trackChanges);
/**
* Finds the USER property name using Qt's MetaProperty system, and caches
* it in the property map (the cache could be retrieved by propertyMap() ).
*/
QByteArray getUserProperty(const QWidget *widget) const;
/**
* Find the property to use for a widget by querying the "kcfg_property"
* property of the widget. Like a widget can use a property other than the
* USER property.
* @since 4.3
*/
QByteArray getCustomProperty(const QWidget *widget) const;
/**
* Finds the changed signal of the USER property using Qt's MetaProperty system.
* @since 5.32
*/
QByteArray getUserPropertyChangedSignal(const QWidget *widget) const;
/**
* Find the changed signal of the property to use for a widget by querying
* the "kcfg_propertyNotify" property of the widget. Like a widget can use a
* property change signal other than the one for USER property, if there even is one.
* @since 5.32
*/
QByteArray getCustomPropertyChangedSignal(const QWidget *widget) const;
/**
* Set a property
*/
void setProperty(QWidget *w, const QVariant &v);
/**
* Retrieve a property
*/
QVariant property(QWidget *w) const;
/**
* Setup secondary widget properties
*/
void setupWidget(QWidget *widget, KConfigSkeletonItem *item);
/**
* Initializes the property maps
*/
static void initMaps();
private:
/**
* KConfigDialogManager KConfigDialogManagerPrivate class.
*/
std::unique_ptr<KConfigDialogManagerPrivate> const d;
friend class KConfigDialogManagerPrivate;
Q_DISABLE_COPY(KConfigDialogManager)
Q_PRIVATE_SLOT(d, void onWidgetModified())
};
#endif // KCONFIGDIALOGMANAGER_H
@@ -0,0 +1,54 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2003 Benjamin C Meyer <ben+kdelibs at meyerhome dot net>
SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau <kossebau@kde.org>
SPDX-FileCopyrightText: 2020 Kevin Ottens <kevin.ottens@enioka.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KCONFIGDIALOGMANAGER_P_H
#define KCONFIGDIALOGMANAGER_P_H
#include <QHash>
#include <QSet>
#include <QString>
class QWidget;
class KConfigDialogManager;
class KCoreConfigSkeleton;
class KConfigDialogManagerPrivate
{
public:
KConfigDialogManagerPrivate(KConfigDialogManager *qq);
void setDefaultsIndicatorsVisible(bool enabled);
void onWidgetModified();
void updateWidgetIndicator(const QString &configId, QWidget *widget);
void updateAllWidgetIndicators();
public:
KConfigDialogManager *const q;
/**
* KConfigSkeleton object used to store settings
*/
KCoreConfigSkeleton *m_conf = nullptr;
/**
* Dialog being managed
*/
QWidget *m_dialog = nullptr;
QHash<QString, QWidget *> knownWidget;
QHash<QString, QWidget *> buddyWidget;
QSet<QWidget *> allExclusiveGroupBoxes;
bool insideGroupBox : 1;
bool trackChanges : 1;
bool defaultsIndicatorsVisible : 1;
};
#endif // KCONFIGDIALOGMANAGER_P_H
@@ -0,0 +1,47 @@
#include "kconfigviewstatesaver.h"
#include <KConfigGroup>
static const char selectionKey[] = "Selection";
static const char expansionKey[] = "Expansion";
static const char currentKey[] = "Current";
static const char scrollStateHorizontalKey[] = "HorizontalScroll";
static const char scrollStateVerticalKey[] = "VerticalScroll";
KConfigViewStateSaver::KConfigViewStateSaver(QObject *parent)
: KViewStateSerializer(parent)
{
}
void KConfigViewStateSaver::restoreState(const KConfigGroup &configGroup)
{
restoreSelection(configGroup.readEntry(selectionKey, QStringList()));
restoreCurrentItem(configGroup.readEntry(currentKey, QString()));
restoreExpanded(configGroup.readEntry(expansionKey, QStringList()));
restoreScrollState(configGroup.readEntry(scrollStateVerticalKey, -1), configGroup.readEntry(scrollStateHorizontalKey, -1));
KViewStateSerializer::restoreState();
}
void KConfigViewStateSaver::saveState(KConfigGroup &configGroup)
{
if (selectionModel()) {
configGroup.writeEntry(selectionKey, selectionKeys());
configGroup.writeEntry(currentKey, currentIndexKey());
}
if (view()) {
QStringList expansion = expansionKeys();
configGroup.writeEntry(expansionKey, expansion);
}
if (view()) {
QPair<int, int> _scrollState = scrollState();
configGroup.writeEntry(scrollStateVerticalKey, _scrollState.first);
configGroup.writeEntry(scrollStateHorizontalKey, _scrollState.second);
}
}
#include "moc_kconfigviewstatesaver.cpp"
@@ -0,0 +1,33 @@
#ifndef KCONFIGVIEWSTATESAVER_H
#define KCONFIGVIEWSTATESAVER_H
#include <KViewStateSerializer>
#include "kconfigwidgets_export.h"
class KConfigGroup;
/**
* @class KConfigViewStateSaver kconfigviewstatesaver.h KConfigViewStateSaver
*
* @brief Base class for saving and restoring state in QTreeViews and QItemSelectionModels using KConfig as storage
*/
class KCONFIGWIDGETS_EXPORT KConfigViewStateSaver : public KViewStateSerializer
{
Q_OBJECT
public:
explicit KConfigViewStateSaver(QObject *parent = nullptr);
/**
Saves the state to the @p configGroup
*/
void saveState(KConfigGroup &configGroup);
/**
Restores the state from the @p configGroup
*/
void restoreState(const KConfigGroup &configGroup);
};
#endif
@@ -0,0 +1,473 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2021 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "khamburgermenu.h"
#include "khamburgermenu_p.h"
#include "khamburgermenuhelpers_p.h"
#include <KLocalizedString>
#include <QMenu>
#include <QMenuBar>
#include <QStyle>
#include <QToolBar>
#include <QToolButton>
#include <algorithm>
#include <forward_list>
#include <unordered_set>
KHamburgerMenu::KHamburgerMenu(QObject *parent)
: QWidgetAction{parent}
, d_ptr{new KHamburgerMenuPrivate(this)}
{
}
KHamburgerMenuPrivate::KHamburgerMenuPrivate(KHamburgerMenu *qq)
: q_ptr{qq}
, m_listeners{new ListenerContainer(this)}
{
q_ptr->setPriority(QAction::LowPriority);
connect(q_ptr, &QAction::changed, this, &KHamburgerMenuPrivate::slotActionChanged);
connect(q_ptr, &QAction::triggered, this, &KHamburgerMenuPrivate::slotActionTriggered);
}
KHamburgerMenu::~KHamburgerMenu() = default;
KHamburgerMenuPrivate::~KHamburgerMenuPrivate() = default;
void KHamburgerMenu::setMenuBar(QMenuBar *menuBar)
{
Q_D(KHamburgerMenu);
d->setMenuBar(menuBar);
}
void KHamburgerMenuPrivate::setMenuBar(QMenuBar *menuBar)
{
if (m_menuBar) {
m_menuBar->removeEventFilter(m_listeners->get<VisibilityChangesListener>());
m_menuBar->removeEventFilter(m_listeners->get<AddOrRemoveActionListener>());
}
m_menuBar = menuBar;
updateVisibility();
if (m_menuBar) {
m_menuBar->installEventFilter(m_listeners->get<VisibilityChangesListener>());
m_menuBar->installEventFilter(m_listeners->get<AddOrRemoveActionListener>());
}
}
QMenuBar *KHamburgerMenu::menuBar() const
{
Q_D(const KHamburgerMenu);
return d->menuBar();
}
QMenuBar *KHamburgerMenuPrivate::menuBar() const
{
return m_menuBar;
}
void KHamburgerMenu::setMenuBarAdvertised(bool advertise)
{
Q_D(KHamburgerMenu);
d->setMenuBarAdvertised(advertise);
}
void KHamburgerMenuPrivate::setMenuBarAdvertised(bool advertise)
{
m_advertiseMenuBar = advertise;
}
bool KHamburgerMenu::menuBarAdvertised() const
{
Q_D(const KHamburgerMenu);
return d->menuBarAdvertised();
}
bool KHamburgerMenuPrivate::menuBarAdvertised() const
{
return m_advertiseMenuBar;
}
void KHamburgerMenu::setShowMenuBarAction(QAction *showMenuBarAction)
{
Q_D(KHamburgerMenu);
d->setShowMenuBarAction(showMenuBarAction);
}
void KHamburgerMenuPrivate::setShowMenuBarAction(QAction *showMenuBarAction)
{
m_showMenuBarAction = showMenuBarAction;
}
void KHamburgerMenu::addToMenu(QMenu *menu)
{
Q_D(KHamburgerMenu);
d->insertIntoMenuBefore(menu, nullptr);
}
void KHamburgerMenu::insertIntoMenuBefore(QMenu *menu, QAction *before)
{
Q_D(KHamburgerMenu);
d->insertIntoMenuBefore(menu, before);
}
void KHamburgerMenuPrivate::insertIntoMenuBefore(QMenu *menu, QAction *before)
{
Q_CHECK_PTR(menu);
Q_Q(KHamburgerMenu);
if (!m_menuAction) {
m_menuAction = new QAction(this);
m_menuAction->setText(i18nc("@action:inmenu General purpose menu", "&Menu"));
m_menuAction->setIcon(q->icon());
m_menuAction->setMenu(m_actualMenu.get());
}
updateVisibility(); // Sets the appropriate visibility of m_menuAction.
menu->insertAction(before, m_menuAction);
connect(menu, &QMenu::aboutToShow, this, [this, menu, q]() {
if (m_menuAction->isVisible()) {
Q_EMIT q->aboutToShowMenu();
hideActionsOf(menu);
resetMenu();
}
});
}
void KHamburgerMenu::hideActionsOf(QWidget *widget)
{
Q_D(KHamburgerMenu);
d->hideActionsOf(widget);
}
void KHamburgerMenuPrivate::hideActionsOf(QWidget *widget)
{
Q_CHECK_PTR(widget);
m_widgetsWithActionsToBeHidden.remove(nullptr);
if (listContainsWidget(m_widgetsWithActionsToBeHidden, widget)) {
return;
}
m_widgetsWithActionsToBeHidden.emplace_front(QPointer<const QWidget>(widget));
if (QMenu *menu = qobject_cast<QMenu *>(widget)) {
// QMenus are normally hidden. This will avoid redundancy with their actions anyways.
menu->installEventFilter(m_listeners->get<AddOrRemoveActionListener>());
notifyMenuResetNeeded();
} else {
// Only avoid redundancy when the widget is visible.
widget->installEventFilter(m_listeners->get<VisibleActionsChangeListener>());
if (widget->isVisible()) {
notifyMenuResetNeeded();
}
}
}
void KHamburgerMenu::showActionsOf(QWidget *widget)
{
Q_D(KHamburgerMenu);
d->showActionsOf(widget);
}
void KHamburgerMenuPrivate::showActionsOf(QWidget *widget)
{
Q_CHECK_PTR(widget);
m_widgetsWithActionsToBeHidden.remove(widget);
widget->removeEventFilter(m_listeners->get<AddOrRemoveActionListener>());
widget->removeEventFilter(m_listeners->get<VisibleActionsChangeListener>());
if (isWidgetActuallyVisible(widget)) {
notifyMenuResetNeeded();
}
}
QWidget *KHamburgerMenu::createWidget(QWidget *parent)
{
Q_D(KHamburgerMenu);
return d->createWidget(parent);
}
QWidget *KHamburgerMenuPrivate::createWidget(QWidget *parent)
{
if (qobject_cast<QMenu *>(parent)) {
qDebug(
"Adding a KHamburgerMenu directly to a QMenu. "
"This will look odd. Use addToMenu() instead.");
}
Q_Q(KHamburgerMenu);
auto toolButton = new QToolButton(parent);
// Set appearance
toolButton->setDefaultAction(q);
toolButton->setMenu(m_actualMenu.get());
toolButton->setAttribute(Qt::WidgetAttribute::WA_CustomWhatsThis);
toolButton->setPopupMode(QToolButton::InstantPopup);
updateButtonStyle(toolButton);
if (const QToolBar *toolbar = qobject_cast<QToolBar *>(parent)) {
connect(toolbar, &QToolBar::toolButtonStyleChanged, toolButton, &QToolButton::setToolButtonStyle);
}
setToolButtonVisible(toolButton, !isMenuBarVisible(m_menuBar));
// Make sure the menu will be ready in time
toolButton->installEventFilter(m_listeners->get<ButtonPressListener>());
hideActionsOf(parent);
return toolButton;
}
QAction *KHamburgerMenuPrivate::actionWithExclusivesFrom(QAction *from, QWidget *parent, std::unordered_set<const QAction *> &nonExclusives) const
{
Q_CHECK_PTR(from);
if (nonExclusives.count(from) > 0) {
return nullptr; // The action is non-exclusive/already visible elsewhere.
}
if (!from->menu() || from->menu()->isEmpty()) {
return from; // The action is exclusive and doesn't have a menu.
}
std::unique_ptr<QAction> menuActionWithExclusives(new QAction(from->icon(), from->text(), parent));
std::unique_ptr<QMenu> menuWithExclusives(new QMenu(parent));
const auto fromMenuActions = from->menu()->actions();
for (QAction *action : fromMenuActions) {
QAction *actionWithExclusives = actionWithExclusivesFrom(action, menuWithExclusives.get(), nonExclusives);
if (actionWithExclusives) {
menuWithExclusives->addAction(actionWithExclusives);
}
}
if (menuWithExclusives->isEmpty()) {
return nullptr; // "from" has a menu that contains zero exclusive actions.
// There is a chance that "from" is an exclusive action itself and should
// therefore be returned instead but that is unlikely for an action that has a menu().
// This fringe case is the only one that can't be correctly covered because we can
// not know or assume that activating the action does something or if it is nothing
// but a container for a menu.
}
menuActionWithExclusives->setMenu(menuWithExclusives.release());
return menuActionWithExclusives.release();
}
std::unique_ptr<QMenu> KHamburgerMenuPrivate::newMenu()
{
std::unique_ptr<QMenu> menu(new QMenu());
Q_Q(const KHamburgerMenu);
// Make sure we notice if the q->menu() is changed or replaced in the future.
if (q->menu() != m_lastUsedMenu) {
q->menu()->installEventFilter(m_listeners->get<AddOrRemoveActionListener>());
if (m_lastUsedMenu && !listContainsWidget(m_widgetsWithActionsToBeHidden, m_lastUsedMenu)) {
m_lastUsedMenu->removeEventFilter(m_listeners->get<AddOrRemoveActionListener>());
}
m_lastUsedMenu = q->menu();
}
if (!q->menu() && !m_menuBar) {
return menu; // empty menu
}
if (!q->menu()) {
// We have nothing else to work with so let's just add the menuBar contents.
const auto menuBarActions = m_menuBar->actions();
for (QAction *menuAction : menuBarActions) {
menu->addAction(menuAction);
}
return menu;
}
// Collect actions which shouldn't be added to the menu
std::unordered_set<const QAction *> visibleActions;
m_widgetsWithActionsToBeHidden.remove(nullptr);
for (const QWidget *widget : m_widgetsWithActionsToBeHidden) {
if (qobject_cast<const QMenu *>(widget) || isWidgetActuallyVisible(widget)) {
// avoid redundancy with menus even when they are not actually visible.
visibleActions.reserve(visibleActions.size() + widget->actions().size());
const auto widgetActions = widget->actions();
for (QAction *action : widgetActions) {
visibleActions.insert(action);
}
}
}
// Populate the menu
const auto menuActions = q->menu()->actions();
for (QAction *action : menuActions) {
if (visibleActions.count(action) == 0) {
menu->addAction(action);
visibleActions.insert(action);
}
}
// Add the last two menu actions
if (m_menuBar) {
connect(menu.get(), &QMenu::aboutToShow, this, [this]() {
if (m_menuBar->actions().last()->icon().isNull()) {
m_helpIconIsSet = false;
m_menuBar->actions().last()->setIcon(QIcon::fromTheme(QStringLiteral("help-contents"))); // set "Help" menu icon
} else {
m_helpIconIsSet = true; // if the "Help" icon was set by the application, we want to leave it untouched
}
});
connect(menu.get(), &QMenu::aboutToHide, this, [this]() {
if (m_menuBar->actions().last()->icon().name() == QStringLiteral("help-contents") && !m_helpIconIsSet) {
m_menuBar->actions().last()->setIcon(QIcon());
}
});
menu->addAction(m_menuBar->actions().last()); // add "Help" menu
visibleActions.insert(m_menuBar->actions().last());
if (m_advertiseMenuBar) {
menu->addSeparator();
m_menuBarAdvertisementMenu = newMenuBarAdvertisementMenu(visibleActions);
menu->addAction(m_menuBarAdvertisementMenu->menuAction());
}
}
return menu;
}
std::unique_ptr<QMenu> KHamburgerMenuPrivate::newMenuBarAdvertisementMenu(std::unordered_set<const QAction *> &visibleActions)
{
std::unique_ptr<QMenu> advertiseMenuBarMenu(new QMenu());
m_showMenuBarWithAllActionsText = i18nc("@action:inmenu A menu item that advertises and enables the menubar", "Show &Menubar with All Actions");
connect(advertiseMenuBarMenu.get(), &QMenu::aboutToShow, this, [this]() {
if (m_showMenuBarAction) {
m_showMenuBarText = m_showMenuBarAction->text();
m_showMenuBarAction->setText(m_showMenuBarWithAllActionsText);
}
});
connect(advertiseMenuBarMenu.get(), &QMenu::aboutToHide, this, [this]() {
if (m_showMenuBarAction && m_showMenuBarAction->text() == m_showMenuBarWithAllActionsText) {
m_showMenuBarAction->setText(m_showMenuBarText);
}
});
if (m_showMenuBarAction) {
advertiseMenuBarMenu->addAction(m_showMenuBarAction);
visibleActions.insert(m_showMenuBarAction);
}
QAction *section = advertiseMenuBarMenu->addSeparator();
const auto menuBarActions = m_menuBar->actions();
for (QAction *menuAction : menuBarActions) {
QAction *menuActionWithExclusives = actionWithExclusivesFrom(menuAction, advertiseMenuBarMenu.get(), visibleActions);
if (menuActionWithExclusives) {
advertiseMenuBarMenu->addAction(menuActionWithExclusives);
}
}
advertiseMenuBarMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic")));
advertiseMenuBarMenu->setTitle(i18nc("@action:inmenu A menu text advertising its contents (more features).", "More"));
section->setText(i18nc("@action:inmenu A section heading advertising the contents of the menu bar", "More Actions"));
return advertiseMenuBarMenu;
}
void KHamburgerMenuPrivate::resetMenu()
{
Q_Q(KHamburgerMenu);
if (!m_menuResetNeeded && m_actualMenu && m_lastUsedMenu == q->menu()) {
return;
}
m_menuResetNeeded = false;
m_actualMenu = newMenu();
const auto createdWidgets = q->createdWidgets();
for (auto widget : createdWidgets) {
static_cast<QToolButton *>(widget)->setMenu(m_actualMenu.get());
}
if (m_menuAction) {
m_menuAction->setMenu(m_actualMenu.get());
}
}
void KHamburgerMenuPrivate::updateVisibility()
{
Q_Q(KHamburgerMenu);
/** The visibility of KHamburgerMenu should be opposite to the visibility of m_menuBar.
* Exception: We only consider a visible m_menuBar as actually visible if it is not a native
* menu bar because native menu bars can come in many shapes and sizes which don't necessarily
* have the same usability benefits as a traditional in-window menu bar.
* KDE applications normally allow the user to remove any actions from their toolbar(s) anyway. */
const bool menuBarVisible = isMenuBarVisible(m_menuBar);
const auto createdWidgets = q->createdWidgets();
for (auto widget : createdWidgets) {
setToolButtonVisible(widget, !menuBarVisible);
}
if (!m_menuAction) {
if (menuBarVisible && m_actualMenu) {
m_actualMenu.release()->deleteLater(); // might as well free up some memory
}
return;
}
// The m_menuAction acts as a fallback if both the m_menuBar and all createdWidgets() on the UI
// are currently hidden. Only then should the m_menuAction ever be visible in a QMenu.
if (menuBarVisible || (m_menuBar && m_menuBar->isNativeMenuBar()) // See [1] below.
|| std::any_of(createdWidgets.cbegin(), createdWidgets.cend(), isWidgetActuallyVisible)) {
m_menuAction->setVisible(false);
return;
}
m_menuAction->setVisible(true);
// [1] While the m_menuAction can be used as a normal menu by users that don't mind invoking a
// QMenu to access any menu actions, its primary use really is that of a fallback.
// Therefore the existence of a native menu bar (no matter what shape or size it might have)
// is enough reason for us to hide m_menuAction.
}
void KHamburgerMenuPrivate::slotActionChanged()
{
Q_Q(KHamburgerMenu);
const auto createdWidgets = q->createdWidgets();
for (auto widget : createdWidgets) {
auto toolButton = static_cast<QToolButton *>(widget);
updateButtonStyle(toolButton);
}
}
void KHamburgerMenuPrivate::slotActionTriggered()
{
if (isMenuBarVisible(m_menuBar)) {
const auto menuBarActions = m_menuBar->actions();
for (const auto action : menuBarActions) {
if (action->isEnabled() && !action->isSeparator()) {
m_menuBar->setActiveAction(m_menuBar->actions().constFirst());
return;
}
}
}
Q_Q(KHamburgerMenu);
const auto createdWidgets = q->createdWidgets();
for (auto widget : createdWidgets) {
if (isWidgetActuallyVisible(widget) && widget->isActiveWindow()) {
auto toolButton = static_cast<QToolButton *>(widget);
m_listeners->get<ButtonPressListener>()->prepareHamburgerButtonForPress(toolButton);
toolButton->pressed();
return;
}
}
Q_EMIT q->aboutToShowMenu();
resetMenu();
prepareParentlessMenuForShowing(m_actualMenu.get(), nullptr);
m_actualMenu->popup(QCursor::pos());
}
void KHamburgerMenuPrivate::updateButtonStyle(QToolButton *hamburgerMenuButton) const
{
Q_Q(const KHamburgerMenu);
Qt::ToolButtonStyle buttonStyle = Qt::ToolButtonFollowStyle;
if (QToolBar *toolbar = qobject_cast<QToolBar *>(hamburgerMenuButton->parent())) {
buttonStyle = toolbar->toolButtonStyle();
}
if (buttonStyle == Qt::ToolButtonFollowStyle) {
buttonStyle = static_cast<Qt::ToolButtonStyle>(hamburgerMenuButton->style()->styleHint(QStyle::SH_ToolButtonStyle));
}
if (buttonStyle == Qt::ToolButtonTextBesideIcon && q->priority() < QAction::NormalPriority) {
hamburgerMenuButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
} else {
hamburgerMenuButton->setToolButtonStyle(buttonStyle);
}
}
#include "moc_khamburgermenu.cpp"
#include "moc_khamburgermenu_p.cpp"
@@ -0,0 +1,212 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2021 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KHamburgerMenu_H
#define KHamburgerMenu_H
#include <kconfigwidgets_export.h>
#include <QWidgetAction>
#include <memory>
class KHamburgerMenuPrivate;
class QMenuBar;
/**
* @class KHamburgerMenu khamburgermenu.h KHamburgerMenu
*
* @short A menu that substitutes a menu bar when necessary
*
* Allowing users to toggle the visibility of the menu bar and/or toolbars,
* while pretty/"simple by default", can lead to various grave usability issues.
* This class makes it easy to prevent all of them.
*
* Simply add a KHamburgerMenu to your UI (typically to a QToolBar) and make
* it aware of a QMenuBar like this:
*
* \code
* auto hamburgerMenu = KStandardAction::hamburgerMenu(nullptr, nullptr, actionCollection());
* toolBar()->addAction(hamburgerMenu);
* hamburgerMenu->hideActionsOf(toolBar());
* // after the QMenuBar has been initialised
* hamburgerMenu->setMenuBar(menuBar()); // line not needed if there is no QMenuBar
* \endcode
*
* The added menu button will only be visible when the QMenuBar is hidden.
* With this minimal initialisation it will contain the contents of the menu bar.
* If a user (also) hides the container the KHamburgerMenu was added to they
* might find themselves without a way to get a menu back. To prevent this, it is
* recommended to add the hamburgerMenu to prominent context menus like the one
* of your central widget preferably at the first position. Simply write:
*
* \code
* hamburgerMenu->addActionToMenu(contextMenu);
* \endcode
*
* The added menu will only be visible if the QMenuBar is hidden and the
* hamburgerMenu->createdWidgets() are all invisible to the user.
*
* **Populating the KHamburgerMenu**
*
* This is easy:
*
* \code
* auto menu = new QMenu(this);
* menu->addAction(action);
* // Add actions, separators, etc. like usual.
* hamburgerMenu->setMenu(menu);
* \endcode
*
* You probably do not want this to happen on startup. Therefore KHamburgerMenu
* provides the signal aboutToShowMenu that you can connect to a function containing
* the previous statements.
*
* \code
* connect(hamburgerMenu, &KHamburgerMenu::aboutToShowMenu,
* this, &MainWindow::updateHamburgerMenu);
* // You might want to disconnect the signal after initial creation if the contents never change.
* \endcode
*
* **Deciding what to put on the hamburger menu**
*
* 1. Be sure to add all of the most important actions. Actions which are already
* visible on QToolBars, etc. will not show up in the hamburgerMenu. To manage
* which containers KHamburgerMenu should watch for redundancy use
* hideActionsOf(QWidget *) and showActionsOf(QWidget *).
* When a KHamburgerMenu is added to a widget, hideActionsOf(that widget)
* will automatically be called.
* 2. Do not worry about adding all actions the application has to offer.
* The KHamburgerMenu will automatically have a section advertising excluded
* actions which can be found in the QMenuBar. There will also be the
* showMenuBarAction if you set it with setShowMenuBarAction().
* 3. Do not worry about the help menu. KHamburgerMenu will automatically contain
* a help menu as the second to last item (if you set a QMenuBar which is
* expected to have the help menu as the last action).
*
* **Open menu by shortcut**
*
* For visually impaired users it is important to have a consistent way to open a general-purpose
* menu. Triggering the keyboard shortcut bound to KHamburgerMenu will always open a menu.
* - If setMenuBar() was called and that menu bar is visible, the shortcut will open the first menu
* of that menu bar.
* - Otherwise, if there is a visible KHamburgerMenu button in the user interface, that menu will
* open.
* - Otherwise, KHamburgerMenu's menu will open at the mouse cursor position.
*
* @since 5.81
*/
class KCONFIGWIDGETS_EXPORT KHamburgerMenu : public QWidgetAction
{
Q_OBJECT
Q_DECLARE_PRIVATE(KHamburgerMenu)
public:
explicit KHamburgerMenu(QObject *parent);
~KHamburgerMenu() override;
/**
* Associates this KHamburgerMenu with @p menuBar. The KHamburgerMenu will from now
* on only be visible when @p menuBar is hidden.
* (Menu bars with QMenuBar::isNativeMenuBar() == true are considered hidden.)
*
* Furthermore the KHamburgerMenu will have the help menu from the @p menuBar added
* at the end. There will also be a special sub-menu advertising actions which are
* only available in the menu bar unless advertiseMenuBar(false) was called.
*
* @param menuBar The QMenuBar the KHamburgerMenu should be associated with.
* This can be set to nullptr.
*/
void setMenuBar(QMenuBar *menuBar);
/** @see setMenuBar() */
QMenuBar *menuBar() const;
/**
* By default the KHamburgerMenu contains a special sub-menu that advertises actions
* of the menu bar which would otherwise not be visible or discoverable for the user.
* This method removes or re-adds that sub-menu.
*
* @param advertise sets whether the special sub-menu that advertises menu bar only
* actions should exist.
*/
void setMenuBarAdvertised(bool advertise);
/** @see setMenuBarAdvertised() */
bool menuBarAdvertised() const;
/**
* Adds the @p showMenuBarAction as the first item of the sub-menu which advertises actions
* from the menu bar.
* @see setMenuBarAdvertised()
*/
void setShowMenuBarAction(QAction *showMenuBarAction);
/**
* Adds this KHamburgerMenu to @p menu.
* It will only be visible in the menu if both the menu bar and all of this
* QWidgetAction's createdWidgets() are invisible.
* If it is visible in the menu, then opening the menu emits the aboutToShowMenu
* signal.
*
* @param menu The menu this KHamburgerMenu is supposed to appear in.
*/
void addToMenu(QMenu *menu);
/**
* Inserts this KHamburgerMenu to @p menu's list of actions, before the action @p before.
* It will only be visible in the menu if both the menu bar and all of this
* QWidgetAction's createdWidgets() are invisible.
* If it is visible in the menu, then opening the menu emits the aboutToShowMenu
* signal.
*
* @param before The action before which KHamburgerMenu should be inserted.
* @param menu The menu this KHamburgerMenu is supposed to appear in.
*
* @see QWidget::insertAction(), QMenu::insertMenu()
*
* @since 5.99
*/
void insertIntoMenuBefore(QMenu *menu, QAction *before);
/**
* Adds @p widget to a list of widgets that should be monitored for their actions().
* If the widget is a QMenu, its actions will be treated as known to the user.
* If the widget isn't a QMenu, its actions will only be treated as known to the user
* when the widget is actually visible.
* @param widget A widget that contains actions which should not show up in the
* KHamburgerMenu redundantly.
*/
void hideActionsOf(QWidget *widget);
/**
* Reverses a hideActionsOf(widget) method call.
* @see hideActionsOf()
*/
void showActionsOf(QWidget *widget);
Q_SIGNALS:
/**
* This signal is emitted when a hamburger menu button is about to be pressed down.
* It is also emitted when a QMenu that contains a visible KHamburgerMenu emits
* QMenu::aboutToShow.
*/
void aboutToShowMenu();
protected:
/**
* @see QWidgetAction::createWidget
*/
QWidget *createWidget(QWidget *parent) override;
private:
std::unique_ptr<KHamburgerMenuPrivate> const d_ptr;
};
#endif // KHamburgerMenu_H
@@ -0,0 +1,174 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2021 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KHamburgerMenu_P_H
#define KHamburgerMenu_P_H
#include "khamburgermenu.h"
#include <QWidgetAction>
#include <QPointer>
#include <QScopedPointer>
#include <forward_list>
#include <unordered_set>
class ListenerContainer;
class QMenu;
class QMenuBar;
class QToolButton;
/**
* The private class of KHamburgerMenu used for the PIMPL idiom.
* \internal
*/
class KHamburgerMenuPrivate : public QObject
{
Q_OBJECT
Q_DECLARE_PUBLIC(KHamburgerMenu)
public:
explicit KHamburgerMenuPrivate(KHamburgerMenu *qq);
~KHamburgerMenuPrivate() override;
/** @see KHamburgerMenu::setMenuBar() */
void setMenuBar(QMenuBar *menuBar);
/** @see KHamburgerMenu::setMenuBar() */
QMenuBar *menuBar() const;
/** @see KHamburgerMenu::setMenuBarAdvertised() */
void setMenuBarAdvertised(bool advertise);
/** @see KHamburgerMenu::setMenuBarAdvertised() */
bool menuBarAdvertised() const;
/** @see KHamburgerMenu::setShowMenuBarAction() */
void setShowMenuBarAction(QAction *showMenuBarAction);
/** @see KHamburgerMenu::insertIntoMenuBefore() */
void insertIntoMenuBefore(QMenu *menu, QAction *before);
/** @see KHamburgerMenu::hideActionsOf() */
void hideActionsOf(QWidget *widget);
/** @see KHamburgerMenu::showActionsOf() */
void showActionsOf(QWidget *widget);
/** @see KHamburgerMenu::createWidget() */
QWidget *createWidget(QWidget *parent);
/**
* This method only returns exclusive actions. The idea is to remove any @p nonExclusives
* from @p from and all of its sub-menus. This means a copy of @p from is created when
* necessary that has a menu() that only contains the exclusive actions from @p from.
* @param from the action this method extracts the exclusive actions from.
* @param parent the widget that is to become the parent if a copy of @p from needs
* to be created.
* @param nonExclusives the actions which will not be anywhere within the returned action.
* @return either nullptr, @p from unchanged or a copy of @p from without the @p nonExclusives.
* In the last case, the caller gets ownership of this new copy with parent @p parent.
*/
QAction *actionWithExclusivesFrom(QAction *from, QWidget *parent, std::unordered_set<const QAction *> &nonExclusives) const;
/**
* @return a new menu with all actions from KHamburgerMenu::menu() which aren't
* exempted from being displayed (@see hideActionsOf()).
* Next adds the help menu.
* At last adds a special sub-menu by calling newMenuBarAdvertisementMenu() if this step
* was not explicitly set to be skipped (@see KHamburgerMenu::setMenuBarAdvertised()).
*/
std::unique_ptr<QMenu> newMenu();
/**
* @return a special sub-menu that advertises actions of the menu bar which would otherwise
* not be visible or discoverable for the user
* @see KHamburgerMenu::setMenuBarAdvertised()
*/
std::unique_ptr<QMenu> newMenuBarAdvertisementMenu(std::unordered_set<const QAction *> &visibleActions);
/** @see resetMenu() */
inline void notifyMenuResetNeeded()
{
m_menuResetNeeded = true;
}
/**
* Does nothing if m_menuResetNeeded is false.
* Otherwise deletes m_actualMenu and creates a newMenu() in its place. This new menu
* is then set to be used whenever the hamburger menu is opened.
* @see newMenu()
*/
void resetMenu();
/**
* Sets the correct visibility for KHamburgerMenu buttons based on the visibility of the
* menu bar (@see setMenuBar()).
* Also sets the correct visibility of the menu item (@see addToMenu()) based on the visibility
* of the menu bar and of the KHamburgerMenu buttons.
*/
void updateVisibility();
protected:
/**
* Makes the KHamburgerMenu buttons change style just like other toolbuttons would
* when their associated action changes.
*/
void slotActionChanged();
/**
* When m_menuBar->isVisible(): Opens the first menu of the menu bar.
* Otherwise it acts the same way as clicking on a visible KHamburgerMenu button.
* If no KHamburgerMenu button is visible, open its menu anyway.
*
* @note The action triggered signal is not emitted when a normal button that contains a menu
* is pressed. So this slot will effectively only be called by keyboard shortcut.
*/
void slotActionTriggered();
/**
* Updates the style of @p hamburgerMenuButton based on its parent's style and q->priority().
*/
void updateButtonStyle(QToolButton *hamburgerMenuButton) const;
public:
KHamburgerMenu *const q_ptr;
protected:
/** @see newMenu(). Do not confuse this menu with QAction::menu(). */
std::unique_ptr<QMenu> m_actualMenu;
/** @see KHamburgerMenu::setMenuBarAdvertised() */
bool m_advertiseMenuBar = true;
/** @see newMenuBarAdvertisementMenu() */
std::unique_ptr<QMenu> m_menuBarAdvertisementMenu;
/** @see KHamburgerMenu::hideActionsOf() */
std::forward_list<QPointer<const QWidget>> m_widgetsWithActionsToBeHidden;
/** The menu that was used as a base when newMenu() was last called. With this we
* make sure to reset the m_actualMenu if the q->menu() has been changed or replaced. */
QPointer<QMenu> m_lastUsedMenu;
/** Makes sure there are no redundant event listeners of the same class. */
std::unique_ptr<ListenerContainer> m_listeners;
/** The action that is put into QMenus to represent the KHamburgerMenu.
* @see KHamburgerMenu::addToMenu() */
QPointer<QAction> m_menuAction;
/** @see KHamburgerMenu::setMenuBar() */
QPointer<QMenuBar> m_menuBar;
/** @see resetMenu() */
bool m_menuResetNeeded = false;
/** @see KHamburgerMenu::setShowMenuBarAction */
QPointer<QAction> m_showMenuBarAction;
/** Keeps track of changes to the "Show Menubar" button text. */
QString m_showMenuBarText;
QString m_showMenuBarWithAllActionsText;
/** Identifies if the application set an icon for "Help" menu. */
bool m_helpIconIsSet = false;
};
#endif // KHamburgerMenu_P_H
@@ -0,0 +1,162 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2021 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "khamburgermenuhelpers_p.h"
#include "khamburgermenu.h"
#include <QEvent>
#include <QGuiApplication>
#include <QMenu>
#include <QMenuBar>
#include <QToolBar>
#include <QToolButton>
#include <QWidget>
#include <QWindow>
ListenerContainer::ListenerContainer(KHamburgerMenuPrivate *hamburgerMenuPrivate)
: QObject{hamburgerMenuPrivate}
, m_listeners{std::vector<std::unique_ptr<QObject>>(4)}
{
}
ListenerContainer::~ListenerContainer()
{
}
bool AddOrRemoveActionListener::eventFilter(QObject * /*watched*/, QEvent *event)
{
if (event->type() == QEvent::ActionAdded || event->type() == QEvent::ActionRemoved) {
static_cast<KHamburgerMenuPrivate *>(parent())->notifyMenuResetNeeded();
}
return false;
}
void ButtonPressListener::prepareHamburgerButtonForPress(QObject *button)
{
Q_ASSERT(qobject_cast<QToolButton *>(button));
auto hamburgerMenuPrivate = static_cast<KHamburgerMenuPrivate *>(parent());
auto q = static_cast<KHamburgerMenu *>(hamburgerMenuPrivate->q_ptr);
Q_EMIT q->aboutToShowMenu();
hamburgerMenuPrivate->resetMenu(); // This menu never has a parent which can be
// problematic because it can lead to situations in which the QMenu itself is
// treated like its own window.
// To avoid this we set a sane transientParent() now even if it already has one
// because the menu might be opened from another window this time.
const auto watchedButton = static_cast<QToolButton *>(button);
auto menu = watchedButton->menu();
if (!menu) {
return;
}
prepareParentlessMenuForShowing(menu, watchedButton);
}
bool ButtonPressListener::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::KeyPress || event->type() == QEvent::MouseButtonPress) {
prepareHamburgerButtonForPress(watched);
}
return false;
}
bool VisibleActionsChangeListener::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::Show || event->type() == QEvent::Hide) {
if (!event->spontaneous()) {
static_cast<KHamburgerMenuPrivate *>(parent())->notifyMenuResetNeeded();
}
} else if (event->type() == QEvent::ActionAdded || event->type() == QEvent::ActionRemoved) {
Q_ASSERT_X(qobject_cast<QWidget *>(watched), "VisibileActionsChangeListener", "The watched QObject is expected to be a QWidget.");
if (static_cast<QWidget *>(watched)->isVisible()) {
static_cast<KHamburgerMenuPrivate *>(parent())->notifyMenuResetNeeded();
}
}
return false;
}
bool VisibilityChangesListener::eventFilter(QObject * /*watched*/, QEvent *event)
{
if (event->type() == QEvent::Show || event->type() == QEvent::Hide) {
if (!event->spontaneous()) {
static_cast<KHamburgerMenuPrivate *>(parent())->updateVisibility();
}
}
return false;
}
bool isMenuBarVisible(const QMenuBar *menuBar)
{
return menuBar && (menuBar->isVisible() && !menuBar->isNativeMenuBar());
}
bool isWidgetActuallyVisible(const QWidget *widget)
{
Q_CHECK_PTR(widget);
if (widget->width() < 1 || widget->height() < 1) {
return false;
}
bool actuallyVisible = widget->isVisible();
const QWidget *ancestorWidget = widget->parentWidget();
while (actuallyVisible && ancestorWidget) {
actuallyVisible = ancestorWidget->isVisible();
ancestorWidget = ancestorWidget->parentWidget();
}
return actuallyVisible;
}
void prepareParentlessMenuForShowing(QMenu *menu, const QWidget *surrogateParent)
{
Q_CHECK_PTR(menu);
// ensure polished so the style can change the surfaceformat of the window which is
// not possible once the window has been created
menu->ensurePolished();
menu->winId(); // trigger being a native widget already, to ensure windowHandle created
// generic code if not known if the available parent widget is a native widget or not
if (surrogateParent) {
auto parentWindowHandle = surrogateParent->windowHandle();
if (!parentWindowHandle) {
parentWindowHandle = surrogateParent->nativeParentWidget()->windowHandle();
}
menu->windowHandle()->setTransientParent(parentWindowHandle);
return;
}
menu->windowHandle()->setTransientParent(qGuiApp->focusWindow());
// Worst case: The menu's transientParent is now still nullptr in which case it might open as
// its own window.
}
void setToolButtonVisible(QWidget *toolButton, bool visible)
{
toolButton->setVisible(visible);
// setVisible() unfortunately has no effect for QWidgetActions on toolbars,
// so we work around this by using setMaximumSize().
if (qobject_cast<QToolBar *>(toolButton->parent())) {
if (visible) {
toolButton->setMaximumSize(QSize(9999999, 9999999));
toolButton->setFocusPolicy(Qt::TabFocus);
} else {
toolButton->setMaximumSize(QSize(0, 0));
toolButton->setFocusPolicy(Qt::NoFocus); // We don't want focus on invisible items.
}
}
}
bool listContainsWidget(const std::forward_list<QPointer<const QWidget>> &list, const QWidget *widget)
{
for (const auto &item : list) {
if (widget == item) {
return true;
}
}
return false;
}
#include "moc_khamburgermenuhelpers_p.cpp"
@@ -0,0 +1,174 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2021 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KHAMBURGERMENUHELPERS_P_H
#define KHAMBURGERMENUHELPERS_P_H
#include "khamburgermenu_p.h"
#include <QObject>
#include <memory>
#include <vector>
class QFont;
class QMenuBar;
class QWidget;
/**
* @brief Makes sure there are no redundant event listeners.
*
* Functionally identical event listeners are needed throughout khamburgermenu.cpp.
* This class makes sure only one of each is created when needed and then reused.
* This also simplifies the removal of event listeners.
* \internal
*/
class ListenerContainer : private QObject
{
public:
explicit ListenerContainer(KHamburgerMenuPrivate *hamburgerMenuPrivate);
~ListenerContainer() override;
/**
* @return an object of class @p Listener with the same parent as ListenerContainer.
*/
template<class Listener>
Listener *get()
{
for (auto &i : m_listeners) {
if (auto existingListener = qobject_cast<Listener *>(i.get())) {
return existingListener;
}
}
KHamburgerMenuPrivate *hamburgerMenuPrivate = static_cast<KHamburgerMenuPrivate *>(parent());
m_listeners.push_back(std::unique_ptr<QObject>(new Listener(hamburgerMenuPrivate)));
return static_cast<Listener *>(m_listeners.back().get());
}
protected:
std::vector<std::unique_ptr<QObject>> m_listeners;
};
/**
* When an action is added or removed, calls KHamburgerMenuPrivate::notifyMenuResetNeeded().
* \internal
*/
class AddOrRemoveActionListener : public QObject
{
Q_OBJECT
protected:
inline AddOrRemoveActionListener(QObject *parent)
: QObject{parent} {};
bool eventFilter(QObject * /*watched*/, QEvent *event) override;
friend class ListenerContainer;
};
/**
* When the button is pressed, emits KHamburgerMenu::aboutToShowMenu(), then calls
* KHamburgerMenuPrivate::resetMenu() (which will only actually reset the menu if
* a menu reset is needed).
* \internal
*/
class ButtonPressListener : public QObject
{
Q_OBJECT
public:
/**
* Makes sure the button can show the expected menu in the expected way when pressed.
* A button that hasn't been prepared yet will have no menu at all because the menu is only
* created when it is needed.
*/
void prepareHamburgerButtonForPress(QObject *button);
protected:
inline ButtonPressListener(QObject *parent)
: QObject{parent} {};
/** Calls prepareButtonForPress() when an event that presses the button is detected. */
bool eventFilter(QObject *watched, QEvent *event) override;
friend class ListenerContainer;
};
/**
* When either
* - the visibility of the widget changes or
* - actions are added or removed from the widget while it isVisible()
* calls KHamburgerMenuPrivate::notifyMenuResetNeeded().
* \internal
*/
class VisibleActionsChangeListener : public QObject
{
Q_OBJECT
protected:
inline VisibleActionsChangeListener(QObject *parent)
: QObject{parent} {};
/**
* Listen for events that potentially lead to a change in user-visible actions.
* Examples: Adding an action or hiding a toolbar.
*/
bool eventFilter(QObject *watched, QEvent *event) override;
friend class ListenerContainer;
};
/**
* When the visibility of the widget changes calls KHamburgerMenuPrivate::updateVisibility().
* \internal
*/
class VisibilityChangesListener : public QObject
{
Q_OBJECT
protected:
inline VisibilityChangesListener(QObject *parent)
: QObject{parent} {};
bool eventFilter(QObject * /*watched*/, QEvent *event) override;
friend class ListenerContainer;
};
/*
* We only consider a visible m_menuBar as actually visible if it is not a native
* menu bar because native menu bars can come in many shapes and sizes which don't necessarily
* have the same usability benefits as a traditional in-window menu bar.
*/
bool isMenuBarVisible(const QMenuBar *menuBar);
/**
* Is the widget and all of its ancestors visible?
*/
bool isWidgetActuallyVisible(const QWidget *widget);
/*
* Call this on menus that don't have a parent or don't want to belong to a singular parent() so
* those menus won't be treated like their own separate windows.
* @param menu Any menu. Though calling this doesn't make sense if the menu has a parent().
* @param surrogateParent The widget that is logically closest to be considered a parent at this
* point in time. Pass nullptr if this function is supposed to guess.
*/
void prepareParentlessMenuForShowing(QMenu *menu, const QWidget *surrogateParent);
/**
* Use this instead of QWidget::isVisible() to work around a peculiarity of QToolBar/QToolButton.
*/
void setToolButtonVisible(QWidget *toolButton, bool visible);
/**
* Does the @p list contain the @p widget?
*/
bool listContainsWidget(const std::forward_list<QPointer<const QWidget>> &list, const QWidget *widget);
#endif // KHAMBURGERMENUHELPERS_P_H
@@ -0,0 +1,65 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2012 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "khelpclient.h"
#include <KDesktopFile>
#include <QCoreApplication>
#include <QDesktopServices>
#include <QDirIterator>
#include <QUrl>
void KHelpClient::invokeHelp(const QString &anchor, const QString &_appname)
{
QString appname;
if (_appname.isEmpty()) {
appname = QCoreApplication::instance()->applicationName();
} else {
appname = _appname;
}
// Look for the .desktop file of the application
// was:
// KService::Ptr service(KService::serviceByDesktopName(appname));
// if (service)
// docPath = service->docPath();
// but we don't want to depend on KService here.
QString docPath;
const QStringList desktopDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
for (const QString &dir : desktopDirs) {
QDirIterator it(dir, QStringList() << appname + QLatin1String(".desktop"), QDir::NoFilter, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
while (it.hasNext()) {
const QString desktopPath(it.next());
KDesktopFile desktopFile(desktopPath);
docPath = desktopFile.readDocPath();
break;
}
}
// docPath could be a path or a full URL, I think.
QUrl url;
if (!docPath.isEmpty()) {
url = QUrl(QStringLiteral("help:/")).resolved(QUrl(docPath));
} else {
if (!anchor.isEmpty()) {
if (anchor.contains(QLatin1Char('#'))) {
url = QUrl(QStringLiteral("help:/%1/%2").arg(appname, anchor));
} else {
url = QUrl(QStringLiteral("help:/%1/%2.html").arg(appname, anchor));
}
} else {
url = QUrl(QStringLiteral("help:/%1/index.html").arg(appname));
}
}
// launch khelpcenter, or a browser for URIs not handled by khelpcenter
QDesktopServices::openUrl(url);
}
@@ -0,0 +1,36 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2012 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KHELPCLIENT_H
#define KHELPCLIENT_H
#include "kconfigwidgets_export.h"
#include <QString>
/**
* @namespace KHelpClient
* Provides utility functions for access to help manuals.
*/
namespace KHelpClient
{
/**
* Invokes the KHelpCenter HTML help viewer from docbook sources.
*
* The HTML file will be found using the X-DocPath entry in the application's desktop file.
* It can be either a relative path, or a website URL.
*
* @param anchor This has to be a defined anchor in your
* docbook sources or website. If empty the main index
* is loaded.
* @param appname This allows you to specify the .desktop file to get the help path from.
* If empty the QCoreApplication::applicationName() is used.
* @since 5.0
*/
KCONFIGWIDGETS_EXPORT void invokeHelp(const QString &anchor = QString(), const QString &appname = QString());
}
#endif /* KHELPCLIENT_H */
@@ -0,0 +1,283 @@
/*
SPDX-FileCopyrightText: 1999-2003 Hans Petter Bieker <bieker@kde.org>
SPDX-FileCopyrightText: 2007 David Jarvie <djarvie@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "klanguagebutton.h"
#include <QDir>
#include <QFile>
#include <QHBoxLayout>
#include <QLocale>
#include <QMenu>
#include <QPushButton>
#include <KConfig>
#include <KConfigGroup>
static void checkInsertPos(QMenu *popup, const QString &str, int &index)
{
if (index != -1) {
return;
}
int a = 0;
const QList<QAction *> actions = popup->actions();
int b = actions.count();
while (a < b) {
int w = (a + b) / 2;
QAction *ac = actions[w];
int j = str.localeAwareCompare(ac->text());
if (j > 0) {
a = w + 1;
} else {
b = w;
}
}
index = a; // it doesn't really matter ... a == b here.
Q_ASSERT(a == b);
}
class KLanguageButtonPrivate
{
public:
explicit KLanguageButtonPrivate(KLanguageButton *parent);
~KLanguageButtonPrivate()
{
delete button;
delete popup;
}
void setCurrentItem(QAction *);
void clear();
QAction *findAction(const QString &data) const;
QPushButton *button = nullptr;
QStringList ids;
QMenu *popup = nullptr;
QString current;
QString locale;
bool staticText : 1;
bool showCodes : 1;
};
KLanguageButton::KLanguageButton(QWidget *parent)
: QWidget(parent)
, d(new KLanguageButtonPrivate(this))
{
}
KLanguageButton::KLanguageButton(const QString &text, QWidget *parent)
: QWidget(parent)
, d(new KLanguageButtonPrivate(this))
{
setText(text);
}
KLanguageButtonPrivate::KLanguageButtonPrivate(KLanguageButton *parent)
: button(new QPushButton(parent))
, popup(new QMenu(parent))
, locale(QLocale::system().name())
, staticText(false)
, showCodes(false)
{
QHBoxLayout *layout = new QHBoxLayout(parent);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(button);
parent->setFocusProxy(button);
parent->setFocusPolicy(button->focusPolicy());
button->setMenu(popup);
QObject::connect(popup, &QMenu::triggered, parent, &KLanguageButton::slotTriggered);
QObject::connect(popup, &QMenu::hovered, parent, &KLanguageButton::slotHovered);
}
KLanguageButton::~KLanguageButton() = default;
void KLanguageButton::setText(const QString &text)
{
d->staticText = true;
d->button->setText(text);
}
void KLanguageButton::setLocale(const QString &locale)
{
d->locale = locale;
}
void KLanguageButton::showLanguageCodes(bool show)
{
d->showCodes = show;
}
static QString nameFromEntryFile(const QString &entryFile)
{
const KConfig entry(entryFile, KConfig::SimpleConfig);
const KConfigGroup group(&entry, QStringLiteral("KCM Locale"));
return group.readEntry("Name", QString());
}
void KLanguageButton::insertLanguage(const QString &languageCode, const QString &name, int index)
{
QString text;
bool showCodes = d->showCodes;
if (name.isEmpty()) {
const QString entryFile =
QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("locale/") + languageCode + QLatin1String("/kf6_entry.desktop"));
if (QFile::exists(entryFile)) {
text = nameFromEntryFile(entryFile);
}
if (text.isEmpty()) {
text = languageCode;
QLocale locale(languageCode);
if (locale != QLocale::c()) {
text = locale.nativeLanguageName();
// For some languages the native name might be empty.
// In this case use the non native language name as fallback.
// See: QTBUG-51323
text = text.isEmpty() ? QLocale::languageToString(locale.language()) : text;
} else {
showCodes = false;
}
}
} else {
text = name;
}
if (showCodes) {
text += QLatin1String(" (") + languageCode + QLatin1Char(')');
}
checkInsertPos(d->popup, text, index);
QAction *a = new QAction(QIcon(), text, this);
a->setData(languageCode);
if (index >= 0 && index < d->popup->actions().count() - 1) {
d->popup->insertAction(d->popup->actions()[index], a);
} else {
d->popup->addAction(a);
}
d->ids.append(languageCode);
}
void KLanguageButton::insertSeparator(int index)
{
if (index >= 0 && index < d->popup->actions().count() - 1) {
d->popup->insertSeparator(d->popup->actions()[index]);
} else {
d->popup->addSeparator();
}
}
void KLanguageButton::loadAllLanguages()
{
const QStringList localeDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("locale"), QStandardPaths::LocateDirectory);
for (const QString &localeDir : localeDirs) {
const QStringList entries = QDir(localeDir).entryList(QDir::Dirs, QDir::Name);
for (const QString &d : entries) {
const QString entryFile = localeDir + QLatin1Char('/') + d + QStringLiteral("/kf6_entry.desktop");
if (QFile::exists(entryFile)) {
insertLanguage(d);
}
}
}
d->ids.removeDuplicates();
setCurrentItem(d->locale);
}
void KLanguageButton::slotTriggered(QAction *a)
{
// qCDebug(KCONFIG_WIDGETS_LOG) << "slotTriggered" << index;
if (!a) {
return;
}
d->setCurrentItem(a);
// Forward event from popup menu as if it was emitted from this widget:
Q_EMIT activated(d->current);
}
void KLanguageButton::slotHovered(QAction *a)
{
// qCDebug(KCONFIG_WIDGETS_LOG) << "slotHovered" << index;
Q_EMIT highlighted(a->data().toString());
}
int KLanguageButton::count() const
{
return d->ids.count();
}
void KLanguageButton::clear()
{
d->clear();
}
void KLanguageButtonPrivate::clear()
{
ids.clear();
popup->clear();
if (!staticText) {
button->setText(QString());
}
}
bool KLanguageButton::contains(const QString &languageCode) const
{
return d->ids.contains(languageCode);
}
QString KLanguageButton::current() const
{
return d->current.isEmpty() ? QStringLiteral("en") : d->current;
}
QAction *KLanguageButtonPrivate::findAction(const QString &data) const
{
const auto listActions = popup->actions();
for (QAction *a : listActions) {
if (!a->data().toString().compare(data)) {
return a;
}
}
return nullptr;
}
void KLanguageButton::setCurrentItem(const QString &languageCode)
{
if (d->ids.isEmpty()) {
return;
}
QAction *a;
if (d->ids.indexOf(languageCode) < 0) {
a = d->findAction(d->ids[0]);
} else {
a = d->findAction(languageCode);
}
if (a) {
d->setCurrentItem(a);
}
}
void KLanguageButtonPrivate::setCurrentItem(QAction *a)
{
if (!a->data().isValid()) {
return;
}
current = a->data().toString();
if (!staticText) {
button->setText(a->text());
}
}
#include "moc_klanguagebutton.cpp"
@@ -0,0 +1,166 @@
/*
klangbutton.h - Button with language selection drop down menu.
Derived from the KLangCombo class by Hans Petter Bieker.
SPDX-FileCopyrightText: 1999-2003 Hans Petter Bieker <bieker@kde.org>
SPDX-FileCopyrightText: 2001 Martijn Klingens <klingens@kde.org>
SPDX-FileCopyrightText: 2007 David Jarvie <djarvie@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KLANGUAGEBUTTON_H
#define KLANGUAGEBUTTON_H
#include "kconfigwidgets_export.h"
#include <QWidget>
#include <memory>
class QAction;
class KLanguageButtonPrivate;
/**
* @class KLanguageButton klanguagebutton.h KLanguageButton
*
* KLanguageButton is a pushbutton which allows a language to be selected from
* a popup list.
*
* Languages are identified by their ISO 639-1 codes, e.g. en, pt_BR.
*
* \image html klanguagebutton.png "KDE Language Selection Widget"
*
* @author Hans Petter Bieker <bieker@kde.org>, Martijn Klingens <klingens@kde.org>,
* David Jarvie <djarvie@kde.org>
*/
class KCONFIGWIDGETS_EXPORT KLanguageButton : public QWidget
{
Q_OBJECT
public:
/**
* Constructs a button whose text is determined by the current language
* in the popup list.
*
* @param parent the parent of the button
*/
explicit KLanguageButton(QWidget *parent = nullptr);
/**
* Constructs a button with static text.
*
* @param text the text of the button
* @param parent the parent of the button
*/
explicit KLanguageButton(const QString &text, QWidget *parent = nullptr);
/**
* Deconstructor
*/
~KLanguageButton() override;
/**
* Sets the locale to display language names. By default, QLocale::system().name() is used.
*
* @param locale locale to use
*/
void setLocale(const QString &locale);
/**
* Sets a static button text.
*
* @param text button text
*/
void setText(const QString &text);
/**
* Specifies whether language codes should be shown alongside language names
* in the popup. Calling this method does not affect any previously
* inserted language texts, so it should normally be called before
* populating the list.
*
* @param show true to show codes, false to hide codes
*/
void showLanguageCodes(bool show);
/**
* Load all known languages into the popup list.
* The current language in the list is set to the default language for the
* current locale (as modified by setLocale()).
*/
void loadAllLanguages();
/**
* Inserts a language into the combo box.
* Normally the display name of the language is obtained automatically, but
* if either the language code does not exist, or there are special display
* requirements, the name of the language can be specified in @p name.
*
* @param languageCode the code for the language
* @param name language name. If empty, the name is obtained automatically.
* @param index the insertion position, or -1 to insert in alphabetical order
*/
void insertLanguage(const QString &languageCode, const QString &name = QString(), int index = -1);
/**
* Inserts a separator item into the combo box. A negative index will append the item.
*
* @param index the insertion position
*/
void insertSeparator(int index = -1);
/**
* Returns the number of items in the combo box.
*/
int count() const;
/**
* Removes all combobox items.
*/
void clear();
/**
* Returns the language code of the combobox's current item.
*
* @return the current item's language code
*/
QString current() const;
/**
* Checks whether the specified language is in the popup list.
*
* @param languageCode the language's code
* @return true if in the list
*/
bool contains(const QString &languageCode) const;
/**
* Sets a given language to be the current item.
*
* @param languageCode the language's code
*/
void setCurrentItem(const QString &languageCode);
Q_SIGNALS:
/**
* This signal is emitted when a new item is activated.
*
* @param languageCode code of the activated language
*/
void activated(const QString &languageCode);
/**
* This signal is emitted when a new item is highlighted.
*
* @param languageCode code of the highlighted language
*/
void highlighted(const QString &languageCode);
private Q_SLOTS:
KCONFIGWIDGETS_NO_EXPORT void slotTriggered(QAction *action);
KCONFIGWIDGETS_NO_EXPORT void slotHovered(QAction *action);
private:
friend class KLanguageButtonPrivate;
std::unique_ptr<KLanguageButtonPrivate> const d;
};
#endif
@@ -0,0 +1,87 @@
/*
SPDX-FileCopyrightText: 1999-2003 Hans Petter Bieker <bieker@kde.org>
SPDX-FileCopyrightText: 2007 David Jarvie <software@astrojar.org.uk>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "klanguagename.h"
#include <KConfig>
#include <KConfigGroup>
#include <QDir>
#include <QLocale>
QString KLanguageName::nameForCode(const QString &code)
{
const QStringList parts = QLocale().name().split(QLatin1Char('_'));
return nameForCodeInLocale(code, parts.at(0));
}
static std::tuple<QString, QString> namesFromEntryFile(const QString &realCode, const QString &realOutputCode)
{
const QString entryFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
QStringLiteral("locale") + QLatin1Char('/') + realCode + QStringLiteral("/kf6_entry.desktop"));
if (!entryFile.isEmpty()) {
KConfig entry(entryFile, KConfig::SimpleConfig);
entry.setLocale(realOutputCode);
const KConfigGroup group(&entry, QStringLiteral("KCM Locale"));
const QString name = group.readEntry("Name");
entry.setLocale(QStringLiteral("en_US"));
const QString englishName = group.readEntry("Name");
return std::make_tuple(name, englishName);
}
return {};
}
QString KLanguageName::nameForCodeInLocale(const QString &code, const QString &outputCode)
{
const QString realCode = code == QLatin1String("en") ? QStringLiteral("en_US") : code;
const QString realOutputCode = outputCode == QLatin1String("en") ? QStringLiteral("en_US") : outputCode;
const std::tuple<QString, QString> nameAndEnglishName = namesFromEntryFile(realCode, realOutputCode);
const QString name = std::get<0>(nameAndEnglishName);
const QString englishName = std::get<1>(nameAndEnglishName);
if (!name.isEmpty()) {
// KConfig doesn't have a way to say it didn't find the entry in
// realOutputCode. When it doesn't find it in the locale you ask for, it just returns the english version
// so we compare the returned name against the english version, if they are different we return it, if they
// are equal we defer to QLocale (with a final fallback to name/englishName if QLocale doesn't know about it)
if (name != englishName || realOutputCode == QLatin1String("en_US")) {
return name;
}
}
const QLocale locale(realCode);
if (locale != QLocale::c()) {
if (realCode == realOutputCode) {
return locale.nativeLanguageName();
}
return QLocale::languageToString(locale.language());
}
// We get here if QLocale doesn't know about realCode (at the time of writing this happens for crh, csb, hne, mai) and name and englishName are the same.
// So what we do here is return name, which can be either empty if the KDE side doesn't know about the code, or otherwise will be the name/englishName
return name;
}
QStringList KLanguageName::allLanguageCodes()
{
QStringList systemLangList;
const QStringList localeDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("locale"), QStandardPaths::LocateDirectory);
for (const QString &localeDir : localeDirs) {
const QStringList entries = QDir(localeDir).entryList(QDir::Dirs);
auto languageExists = [&localeDir](const QString &language) {
return QFile::exists(localeDir + QLatin1Char('/') + language + QLatin1String("/kf6_entry.desktop"));
};
std::copy_if(entries.begin(), entries.end(), std::back_inserter(systemLangList), languageExists);
}
if (localeDirs.count() > 1) {
systemLangList.removeDuplicates();
}
return systemLangList;
}
@@ -0,0 +1,59 @@
/*
SPDX-FileCopyrightText: 1999-2003 Hans Petter Bieker <bieker@kde.org>
SPDX-FileCopyrightText: 2001 Martijn Klingens <klingens@kde.org>
SPDX-FileCopyrightText: 2007 David Jarvie <software@astrojar.org.uk>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KLANGUAGENAME_H
#define KLANGUAGENAME_H
#include "kconfigwidgets_export.h"
#include <QStringList>
class QString;
/**
* @class KLanguageName klanguagename.h KLanguageName
*
* KLanguageName is a helper namespace that returns the name of a given language code.
*
* @since 5.55
*
*/
namespace KLanguageName
{
/**
* Returns the name of the given language code in the current locale.
*
* If it can't be found in the current locale it returns the name in English.
*
* It it can't be found in English either it returns an empty QString.
*
* @param code code (ISO 639-1) of the language whose name is wanted.
*/
KCONFIGWIDGETS_EXPORT QString nameForCode(const QString &code);
/**
* Returns the name of the given language code in the other given locale code.
*
* If it can't be found in the given locale it returns the name in English.
*
* It it can't be found in English either it returns an empty QString.
*
* @param code code (ISO 639-1) of the language whose name is wanted.
* @param outputLocale code (ISO 639-1) of the language in which we want the name in.
*/
KCONFIGWIDGETS_EXPORT QString nameForCodeInLocale(const QString &code, const QString &outputLocale);
/**
* Returns the list of language codes found on the system.
*
* @since 5.74
*/
KCONFIGWIDGETS_EXPORT QStringList allLanguageCodes();
}
#endif
@@ -0,0 +1,100 @@
/*
* SPDX-FileCopyrightText: 2023 Kai Uwe Broulik <kde@broulik.de>
*
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "kopenaction_p.h"
#include <KRecentFilesAction>
#include <KStandardAction>
#include <QMenu>
#include <QPointer>
class KOpenActionPrivate
{
public:
KOpenActionPrivate(KOpenAction *q);
void updatePopupMode();
void onPopupMenuAboutToShow();
KOpenAction *q;
QPointer<KRecentFilesAction> recentFilesAction;
};
KOpenActionPrivate::KOpenActionPrivate(KOpenAction *q)
: q(q)
{
}
KOpenAction::~KOpenAction() = default;
void KOpenActionPrivate::updatePopupMode()
{
if (recentFilesAction && recentFilesAction->isEnabled()) {
q->setPopupMode(KToolBarPopupAction::MenuButtonPopup);
} else {
q->setPopupMode(KToolBarPopupAction::NoPopup);
}
}
void KOpenActionPrivate::onPopupMenuAboutToShow()
{
q->popupMenu()->clear();
if (recentFilesAction) {
// Using the menu explicitly rather than the actions so we also get the "Forget..." entry.
if (auto *recentMenu = recentFilesAction->menu()) {
// Trigger a menu update.
Q_EMIT recentMenu->aboutToShow();
const auto actions = recentMenu->actions();
for (QAction *action : actions) {
q->popupMenu()->addAction(action);
}
}
}
}
KOpenAction::KOpenAction(QObject *parent)
: KOpenAction(QIcon(), QString(), parent)
{
}
KOpenAction::KOpenAction(const QIcon &icon, const QString &text, QObject *parent)
: KToolBarPopupAction(icon, text, parent)
, d(new KOpenActionPrivate(this))
{
setPopupMode(KToolBarPopupAction::NoPopup);
connect(popupMenu(), &QMenu::aboutToShow, this, [this] {
d->onPopupMenuAboutToShow();
});
}
QWidget *KOpenAction::createWidget(QWidget *parentWidget)
{
if (!d->recentFilesAction) {
// Find the accompanying file_open_recent action.
QAction *action = nullptr;
if (parent() && parent()->inherits("KActionCollection")) {
const QString openRecentActionId = KStandardAction::name(KStandardAction::OpenRecent);
QMetaObject::invokeMethod(parent(), "action", Q_RETURN_ARG(QAction *, action), Q_ARG(QString, openRecentActionId));
}
d->recentFilesAction = qobject_cast<KRecentFilesAction *>(action);
if (d->recentFilesAction) {
connect(d->recentFilesAction.data(), &QAction::enabledChanged, this, [this] {
d->updatePopupMode();
});
}
}
d->updatePopupMode();
return KToolBarPopupAction::createWidget(parentWidget);
}
#include "moc_kopenaction_p.cpp"
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2023 Kai Uwe Broulik <kde@broulik.de>
*
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KOPENACTION_H
#define KOPENACTION_H
#include <KToolBarPopupAction>
#include <kconfigwidgets_export.h>
#include <memory>
class KOpenActionPrivate;
// TODO export only for for unittests?
class KCONFIGWIDGETS_EXPORT KOpenAction : public KToolBarPopupAction
{
Q_OBJECT
public:
explicit KOpenAction(QObject *parent = nullptr);
KOpenAction(const QIcon &icon, const QString &text, QObject *parent);
~KOpenAction() override;
protected:
QWidget *createWidget(QWidget *parent) override;
private:
std::unique_ptr<KOpenActionPrivate> const d;
};
#endif // KOPENACTION_H
@@ -0,0 +1,411 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org>
SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org>
SPDX-FileCopyrightText: 2000 Nicolas Hadacek <haadcek@kde.org>
SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org>
SPDX-FileCopyrightText: 2000 Michael Koch <koch@kde.org>
SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@kde.org>
SPDX-FileCopyrightText: 2002 Ellis Whitehead <ellis@kde.org>
SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org>
SPDX-FileCopyrightText: 2003 Andras Mantia <amantia@kde.org>
SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "krecentfilesaction.h"
#include "krecentfilesaction_p.h"
#include <QActionGroup>
#include <QDir>
#include <QGuiApplication>
#include <QMenu>
#include <QMimeDatabase>
#include <QMimeType>
#include <QScreen>
#if HAVE_QTDBUS
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusMessage>
#endif
#include <KConfig>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KShell>
#include <set>
KRecentFilesAction::KRecentFilesAction(QObject *parent)
: KSelectAction(parent)
, d_ptr(new KRecentFilesActionPrivate(this))
{
Q_D(KRecentFilesAction);
d->init();
}
KRecentFilesAction::KRecentFilesAction(const QString &text, QObject *parent)
: KSelectAction(parent)
, d_ptr(new KRecentFilesActionPrivate(this))
{
Q_D(KRecentFilesAction);
d->init();
// Want to keep the ampersands
setText(text);
}
KRecentFilesAction::KRecentFilesAction(const QIcon &icon, const QString &text, QObject *parent)
: KSelectAction(parent)
, d_ptr(new KRecentFilesActionPrivate(this))
{
Q_D(KRecentFilesAction);
d->init();
setIcon(icon);
// Want to keep the ampersands
setText(text);
}
void KRecentFilesActionPrivate::init()
{
Q_Q(KRecentFilesAction);
delete q->menu();
q->setMenu(new QMenu());
q->setToolBarMode(KSelectAction::MenuMode);
m_noEntriesAction = q->menu()->addAction(i18n("No Entries"));
m_noEntriesAction->setObjectName(QStringLiteral("no_entries"));
m_noEntriesAction->setEnabled(false);
clearSeparator = q->menu()->addSeparator();
clearSeparator->setVisible(false);
clearSeparator->setObjectName(QStringLiteral("separator"));
clearAction = q->menu()->addAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), i18n("Clear List"), q, &KRecentFilesAction::clear);
clearAction->setObjectName(QStringLiteral("clear_action"));
clearAction->setVisible(false);
q->setEnabled(false);
q->connect(q, &KSelectAction::actionTriggered, q, [this](QAction *action) {
urlSelected(action);
});
q->connect(q->menu(), &QMenu::aboutToShow, q, [q] {
std::vector<RecentActionInfo> &recentActions = q->d_ptr->m_recentActions;
// Set icons lazily based on the mimetype
for (auto action : recentActions) {
if (action.action->icon().isNull()) {
if (!action.mimeType.isValid()) {
action.mimeType = QMimeDatabase().mimeTypeForFile(action.url.path(), QMimeDatabase::MatchExtension);
}
if (!action.mimeType.isDefault()) {
action.action->setIcon(QIcon::fromTheme(action.mimeType.iconName()));
}
}
}
});
}
KRecentFilesAction::~KRecentFilesAction() = default;
void KRecentFilesActionPrivate::urlSelected(QAction *action)
{
Q_Q(KRecentFilesAction);
auto it = findByAction(action);
Q_ASSERT(it != m_recentActions.cend()); // Should never happen
const QUrl url = it->url; // BUG: 461448; see iterator invalidation rules
Q_EMIT q->urlSelected(url);
}
// TODO: remove this helper function, it will crash if you use it in a loop
void KRecentFilesActionPrivate::removeAction(std::vector<RecentActionInfo>::iterator it)
{
Q_Q(KRecentFilesAction);
delete q->KSelectAction::removeAction(it->action);
m_recentActions.erase(it);
}
int KRecentFilesAction::maxItems() const
{
Q_D(const KRecentFilesAction);
return d->m_maxItems;
}
void KRecentFilesAction::setMaxItems(int maxItems)
{
Q_D(KRecentFilesAction);
// set new maxItems
d->m_maxItems = std::max(maxItems, 0);
// Remove all excess items, oldest (i.e. first added) first
const int difference = static_cast<int>(d->m_recentActions.size()) - d->m_maxItems;
if (difference > 0) {
auto beginIt = d->m_recentActions.begin();
auto endIt = d->m_recentActions.begin() + difference;
for (auto it = beginIt; it < endIt; ++it) {
// Remove the action from the menus, action groups ...etc
delete KSelectAction::removeAction(it->action);
}
d->m_recentActions.erase(beginIt, endIt);
}
}
static QString titleWithSensibleWidth(const QString &nameValue, const QString &value)
{
// Calculate 3/4 of screen geometry, we do not want
// action titles to be bigger than that
// Since we do not know in which screen we are going to show
// we choose the min of all the screens
int maxWidthForTitles = INT_MAX;
const auto screens = QGuiApplication::screens();
for (QScreen *screen : screens) {
maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4);
}
const QFontMetrics fontMetrics = QFontMetrics(QFont());
QString title = nameValue + QLatin1String(" [") + value + QLatin1Char(']');
const int nameWidth = fontMetrics.boundingRect(title).width();
if (nameWidth > maxWidthForTitles) {
// If it does not fit, try to cut only the whole path, though if the
// name is too long (more than 3/4 of the whole text) we cut it a bit too
const int nameValueMaxWidth = maxWidthForTitles * 3 / 4;
QString cutNameValue;
QString cutValue;
if (nameWidth > nameValueMaxWidth) {
cutNameValue = fontMetrics.elidedText(nameValue, Qt::ElideMiddle, nameValueMaxWidth);
cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameValueMaxWidth);
} else {
cutNameValue = nameValue;
cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameWidth);
}
title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']');
}
return title;
}
void KRecentFilesAction::addUrl(const QUrl &url, const QString &name)
{
addUrl(url, name, QString());
}
void KRecentFilesAction::addUrl(const QUrl &url, const QString &name, const QString &mimeTypeStr)
{
Q_D(KRecentFilesAction);
// ensure we never add items if we want none
if (d->m_maxItems == 0) {
return;
}
if (url.isLocalFile() && url.toLocalFile().startsWith(QDir::tempPath())) {
return;
}
// Remove url if it already exists in the list
removeUrl(url);
// Remove oldest item if already maxItems in list
Q_ASSERT(d->m_maxItems > 0);
if (static_cast<int>(d->m_recentActions.size()) == d->m_maxItems) {
d->removeAction(d->m_recentActions.begin());
}
const QString pathOrUrl(url.toDisplayString(QUrl::PreferLocalFile));
const QString tmpName = !name.isEmpty() ? name : url.fileName();
#ifdef Q_OS_WIN
const QString file = url.isLocalFile() ? QDir::toNativeSeparators(pathOrUrl) : pathOrUrl;
#else
const QString file = pathOrUrl;
#endif
d->m_noEntriesAction->setVisible(false);
d->clearSeparator->setVisible(true);
d->clearAction->setVisible(true);
setEnabled(true);
// add file to list
const QString title = titleWithSensibleWidth(tmpName, KShell::tildeCollapse(file));
#if HAVE_QTDBUS
static bool isKdeSession = qgetenv("XDG_CURRENT_DESKTOP") == "KDE";
if (isKdeSession) {
const QDBusConnection bus = QDBusConnection::sessionBus();
if (bus.isConnected()
&& bus.interface()->isServiceRegistered(QStringLiteral("org.kde.ActivityManager"))
// skip files in hidden directories
&& !url.path().contains(QStringLiteral("/."))) {
const static QString activityService = QStringLiteral("org.kde.ActivityManager");
const static QString activityResources = QStringLiteral("/ActivityManager/Resources");
const static QString activityResouceInferface = QStringLiteral("org.kde.ActivityManager.Resources");
QMimeType mimeType;
if (!mimeTypeStr.isEmpty()) {
mimeType = QMimeDatabase().mimeTypeForName(mimeTypeStr);
} else {
mimeType = QMimeDatabase().mimeTypeForFile(url.path(), QMimeDatabase::MatchExtension);
}
const auto urlString = url.toString(QUrl::PreferLocalFile);
QDBusMessage message =
QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceEvent"));
message.setArguments({qApp->desktopFileName(), uint(0) /* WinId */, urlString, uint(0) /* eventType Accessed */});
bus.asyncCall(message);
message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceMimetype"));
message.setArguments({urlString, mimeType.name()});
bus.asyncCall(message);
message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceTitle"));
message.setArguments({urlString, url.fileName()});
bus.asyncCall(message);
}
}
#endif
QAction *action = new QAction(title, selectableActionGroup());
addAction(action, url, tmpName, QMimeType());
}
void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &name, const QMimeType &mimeType)
{
Q_D(KRecentFilesAction);
menu()->insertAction(menu()->actions().value(0), action);
d->m_recentActions.push_back({action, url, name, mimeType});
}
QAction *KRecentFilesAction::removeAction(QAction *action)
{
Q_D(KRecentFilesAction);
auto it = d->findByAction(action);
Q_ASSERT(it != d->m_recentActions.cend());
d->m_recentActions.erase(it);
return KSelectAction::removeAction(action);
}
void KRecentFilesAction::removeUrl(const QUrl &url)
{
Q_D(KRecentFilesAction);
auto it = d->findByUrl(url);
if (it != d->m_recentActions.cend()) {
d->removeAction(it);
};
}
QList<QUrl> KRecentFilesAction::urls() const
{
Q_D(const KRecentFilesAction);
QList<QUrl> list;
list.reserve(d->m_recentActions.size());
using Info = KRecentFilesActionPrivate::RecentActionInfo;
// Reverse order to match how the actions appear in the menu
std::transform(d->m_recentActions.crbegin(), d->m_recentActions.crend(), std::back_inserter(list), [](const Info &info) {
return info.url;
});
return list;
}
void KRecentFilesAction::clear()
{
clearEntries();
Q_EMIT recentListCleared();
}
void KRecentFilesAction::clearEntries()
{
Q_D(KRecentFilesAction);
KSelectAction::clear();
d->m_recentActions.clear();
d->m_noEntriesAction->setVisible(true);
d->clearSeparator->setVisible(false);
d->clearAction->setVisible(false);
setEnabled(false);
}
void KRecentFilesAction::loadEntries(const KConfigGroup &_config)
{
Q_D(KRecentFilesAction);
clearEntries();
QString key;
QString value;
QString nameKey;
QString nameValue;
QString title;
QUrl url;
KConfigGroup cg = _config;
// "<default>" means the group was constructed with an empty name
if (cg.name() == QLatin1String("<default>")) {
cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles"));
}
std::set<QUrl> seenUrls;
bool thereAreEntries = false;
// read file list
for (int i = 1; i <= d->m_maxItems; i++) {
key = QStringLiteral("File%1").arg(i);
value = cg.readPathEntry(key, QString());
if (value.isEmpty()) {
continue;
}
url = QUrl::fromUserInput(value);
auto [it, isNewUrl] = seenUrls.insert(url);
// Don't restore if this url has already been restored (e.g. broken config)
if (!isNewUrl) {
continue;
}
#ifdef Q_OS_WIN
// convert to backslashes
if (url.isLocalFile()) {
value = QDir::toNativeSeparators(value);
}
#endif
nameKey = QStringLiteral("Name%1").arg(i);
nameValue = cg.readPathEntry(nameKey, url.fileName());
title = titleWithSensibleWidth(nameValue, KShell::tildeCollapse(value));
if (!value.isNull()) {
thereAreEntries = true;
addAction(new QAction(title, selectableActionGroup()), url, nameValue);
}
}
if (thereAreEntries) {
d->m_noEntriesAction->setVisible(false);
d->clearSeparator->setVisible(true);
d->clearAction->setVisible(true);
setEnabled(true);
}
}
void KRecentFilesAction::saveEntries(const KConfigGroup &_cg)
{
Q_D(KRecentFilesAction);
KConfigGroup cg = _cg;
// "<default>" means the group was constructed with an empty name
if (cg.name() == QLatin1String("<default>")) {
cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles"));
}
cg.deleteGroup();
// write file list
int i = 1;
for (const auto &[action, url, shortName, _] : d->m_recentActions) {
cg.writePathEntry(QStringLiteral("File%1").arg(i), url.toDisplayString(QUrl::PreferLocalFile));
cg.writePathEntry(QStringLiteral("Name%1").arg(i), shortName);
++i;
}
}
#include "moc_krecentfilesaction.cpp"
@@ -0,0 +1,202 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org>
SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org>
SPDX-FileCopyrightText: 2000 Nicolas Hadacek <haadcek@kde.org>
SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org>
SPDX-FileCopyrightText: 2000 Michael Koch <koch@kde.org>
SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@kde.org>
SPDX-FileCopyrightText: 2002 Ellis Whitehead <ellis@kde.org>
SPDX-FileCopyrightText: 2003 Andras Mantia <amantia@kde.org>
SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KRECENTFILESACTION_H
#define KRECENTFILESACTION_H
#include <KSelectAction>
#include <QUrl>
#include <kconfigwidgets_export.h>
#include <QMimeType>
class KConfigGroup;
class KRecentFilesActionPrivate;
/**
* @class KRecentFilesAction krecentfilesaction.h KRecentFilesAction
*
* @short Recent files action
*
* This class is an action to handle a recent files submenu.
* The best way to create the action is to use KStandardAction::openRecent.
* Then you simply need to call loadEntries on startup, saveEntries
* on shutdown, addURL when your application loads/saves a file.
*
* @author Michael Koch
*/
class KCONFIGWIDGETS_EXPORT KRecentFilesAction : public KSelectAction
{
Q_OBJECT
Q_PROPERTY(int maxItems READ maxItems WRITE setMaxItems)
Q_DECLARE_PRIVATE(KRecentFilesAction)
public:
/**
* Constructs an action with the specified parent.
*
* @param parent The parent of this action.
*/
explicit KRecentFilesAction(QObject *parent);
/**
* Constructs an action with text; a shortcut may be specified by
* the ampersand character (e.g. \"&amp;Option\" creates a shortcut with key \e O )
*
* This is the most common KAction used when you do not have a
* corresponding icon (note that it won't appear in the current version
* of the "Edit ToolBar" dialog, because an action needs an icon to be
* plugged in a toolbar...).
*
* @param text The text that will be displayed.
* @param parent The parent of this action.
*/
KRecentFilesAction(const QString &text, QObject *parent);
/**
* Constructs an action with text and an icon; a shortcut may be specified by
* the ampersand character (e.g. \"&amp;Option\" creates a shortcut with key \e O )
*
* This is the other common KAction used. Use it when you
* \e do have a corresponding icon.
*
* @param icon The icon to display.
* @param text The text that will be displayed.
* @param parent The parent of this action.
*/
KRecentFilesAction(const QIcon &icon, const QString &text, QObject *parent);
/**
* Destructor.
*/
~KRecentFilesAction() override;
/**
* Adds \a action to the list of URLs, with \a url and title \a name.
*
* Do not use addAction(QAction*), as no url will be associated, and
* consequently urlSelected() will not be emitted when \a action is selected.
*/
void addAction(QAction *action, const QUrl &url, const QString &name, const QMimeType &mimeType = QMimeType());
/**
* Reimplemented for internal reasons.
*/
QAction *removeAction(QAction *action) override;
/**
* Returns the maximum of items in the recent files list.
*/
int maxItems() const;
/**
* Sets the maximum of items in the recent files list.
* The default for this value is 10 set in the constructor.
*
* If this value is lesser than the number of items currently
* in the recent files list the last items are deleted until
* the number of items are equal to the new maximum.
*
* Negative values will be normalized to 0.
*/
void setMaxItems(int maxItems);
/**
* Loads the recent files entries from a given KConfigGroup object.
* You can provide the name of the group used to load the entries.
* If the groupname is empty, entries are loaded from a group called 'RecentFiles'.
* Local file entries that do not exist anymore are not restored.
*
*/
void loadEntries(const KConfigGroup &config);
/**
* Saves the current recent files entries to a given KConfigGroup object.
* You can provide the name of the group used to load the entries.
* If the groupname is empty, entries are saved to a group called 'RecentFiles'.
*
*/
void saveEntries(const KConfigGroup &config);
/**
* Add URL to the recent files list. This will enable this action.
*
* @param url The URL of the file
* @param name The user visible pretty name that appears before the URL
*
* @note URLs corresponding to local files in the temporary directory
* (see @ref QDir::tempPath()) are automatically ignored by this method.
*/
void addUrl(const QUrl &url, const QString &name = QString());
/**
* Add URL to the recent files list. This will enable this action.
*
* @param url The URL of the file
* @param name The user visible pretty name that appears before the URL
* @param mimeType Specify the mimeType of the file
*
* @note URLs corresponding to local files in the temporary directory
* (see @ref QDir::tempPath()) are automatically ignored by this method.
*
* @since 6.9
*/
void addUrl(const QUrl &url, const QString &name, const QString &mimeType);
/**
* Remove an URL from the recent files list.
*
* @param url The URL of the file
*/
void removeUrl(const QUrl &url);
/**
* Retrieve a list of all URLs in the recent files list.
*/
QList<QUrl> urls() const;
public Q_SLOTS:
/**
* Clears the recent files list.
* Note that there is also an action shown to the user for clearing the list.
*/
virtual void clear();
Q_SIGNALS:
/**
* This signal gets emitted when the user selects an URL.
*
* @param url The URL that the user selected.
*/
void urlSelected(const QUrl &url);
/**
* This signal gets emitted when the user clear list.
* So when user store url in specific config file it can saveEntry.
* @since 4.3
*/
void recentListCleared();
private:
// Internal
KCONFIGWIDGETS_NO_EXPORT void clearEntries();
// Don't warn about the virtual overload. As the comment of the other
// addAction() says, addAction( QAction* ) should not be used.
using KSelectAction::addAction;
private:
std::unique_ptr<KRecentFilesActionPrivate> const d_ptr;
};
#endif
@@ -0,0 +1,73 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org>
SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org>
SPDX-FileCopyrightText: 2000 Nicolas Hadacek <haadcek@kde.org>
SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org>
SPDX-FileCopyrightText: 2000 Michael Koch <koch@kde.org>
SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@kde.org>
SPDX-FileCopyrightText: 2002 Ellis Whitehead <ellis@kde.org>
SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org>
SPDX-FileCopyrightText: 2003 Andras Mantia <amantia@kde.org>
SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KRECENTFILESACTION_P_H
#define KRECENTFILESACTION_P_H
#include "krecentfilesaction.h"
class KRecentFilesActionPrivate
{
Q_DECLARE_PUBLIC(KRecentFilesAction)
public:
explicit KRecentFilesActionPrivate(KRecentFilesAction *parent)
: q_ptr(parent)
{
}
virtual ~KRecentFilesActionPrivate()
{
}
void init();
void urlSelected(QAction *);
int m_maxItems = 10;
struct RecentActionInfo {
QAction *action = nullptr;
QUrl url;
QString shortName;
QMimeType mimeType;
};
std::vector<RecentActionInfo> m_recentActions;
std::vector<RecentActionInfo>::iterator findByUrl(const QUrl &url)
{
return std::find_if(m_recentActions.begin(), m_recentActions.end(), [&url](const RecentActionInfo &info) {
return info.url == url;
});
}
std::vector<RecentActionInfo>::iterator findByAction(const QAction *action)
{
return std::find_if(m_recentActions.begin(), m_recentActions.end(), [action](const RecentActionInfo &info) {
return info.action == action;
});
}
void removeAction(std::vector<RecentActionInfo>::iterator it);
QAction *m_noEntriesAction = nullptr;
QAction *clearSeparator = nullptr;
QAction *clearAction = nullptr;
KRecentFilesAction *const q_ptr;
};
#endif // KRECENTFILESACTION_P_H
@@ -0,0 +1,731 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 1999, 2000 Kurt Granroth <granroth@kde.org>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "kstandardaction.h"
#include "kopenaction_p.h"
#include "kstandardaction_p.h"
#include "moc_kstandardaction_p.cpp"
#include <KAboutData>
#include <KAcceleratorManager>
#include <KLocalizedString>
#include <KStandardShortcutWatcher>
#include <QGuiApplication>
#include <QLayout>
#include <QMainWindow>
#include <QMenuBar>
namespace KStandardAction
{
AutomaticAction::AutomaticAction(const QIcon &icon,
const QString &text,
KStandardShortcut::StandardShortcut standardShortcut,
const char *slot,
QObject *parent)
: QAction(parent)
{
setText(text);
setIcon(icon);
const QList<QKeySequence> shortcut = KStandardShortcut::shortcut(standardShortcut);
setShortcuts(shortcut);
setProperty("defaultShortcuts", QVariant::fromValue(shortcut));
connect(KStandardShortcut::shortcutWatcher(),
&KStandardShortcut::StandardShortcutWatcher::shortcutChanged,
this,
[standardShortcut, this](KStandardShortcut::StandardShortcut id, const QList<QKeySequence> &newShortcut) {
if (id != standardShortcut) {
return;
}
setShortcuts(newShortcut);
setProperty("defaultShortcuts", QVariant::fromValue(newShortcut));
});
connect(this, SIGNAL(triggered()), this, slot);
}
QStringList stdNames()
{
return internal_stdNames();
}
QList<StandardAction> actionIds()
{
QList<StandardAction> result;
for (uint i = 0; g_rgActionInfo[i].id != ActionNone; i++) {
result.append(g_rgActionInfo[i].id);
}
return result;
}
KStandardShortcut::StandardShortcut shortcutForActionId(StandardAction id)
{
const KStandardActionInfo *pInfo = infoPtr(id);
return (pInfo) ? pInfo->idAccel : KStandardShortcut::AccelNone;
}
class ShowMenubarActionFilter : public QObject
{
public:
ShowMenubarActionFilter(QAction *parent)
: QObject(parent)
, wasNative(false)
, wasChecked(false)
, wasVisible(false)
{
}
bool eventFilter(QObject * /*watched*/, QEvent *e) override
{
if (e->type() == QEvent::Show) {
updateAction();
}
return false;
}
void updateAction()
{
bool allMenuBarsNative = true;
bool hasAnyMenuBar = false;
const auto lstWidget = qApp->topLevelWidgets();
for (QWidget *w : lstWidget) {
QMainWindow *mw = qobject_cast<QMainWindow *>(w);
if (mw) {
mw->installEventFilter(this); // this is just in case a new main window appeared
// if we were filtering it already it is almost a noop
if (mw->layout() && mw->layout()->menuBar()) {
QMenuBar *mb = qobject_cast<QMenuBar *>(mw->layout()->menuBar());
if (mb) {
hasAnyMenuBar = true;
if (!mb->isNativeMenuBar()) {
allMenuBarsNative = false;
}
}
}
}
}
if (!hasAnyMenuBar) {
return;
}
QAction *showMenubarAction = static_cast<QAction *>(parent());
if (allMenuBarsNative && !wasNative) {
wasNative = true;
wasChecked = showMenubarAction->isChecked();
wasVisible = showMenubarAction->isVisible();
showMenubarAction->setChecked(true);
showMenubarAction->setVisible(false);
} else if (!allMenuBarsNative && wasNative) {
showMenubarAction->setChecked(wasChecked);
showMenubarAction->setVisible(wasVisible);
}
}
bool wasNative;
bool wasChecked;
bool wasVisible;
};
QAction *_k_createInternal(StandardAction id, QObject *parent)
{
static bool stdNamesInitialized = false;
if (!stdNamesInitialized) {
KAcceleratorManager::addStandardActionNames(stdNames());
stdNamesInitialized = true;
}
QAction *pAction = nullptr;
const KStandardActionInfo *pInfo = infoPtr(id);
// qCDebug(KCONFIG_WIDGETS_LOG) << "KStandardAction::create( " << id << "=" << (pInfo ? pInfo->psName : (const char*)0) << ", " << parent << " )"; // ellis
if (pInfo) {
QString sLabel;
QString iconName = pInfo->psIconName.toString();
switch (id) {
case Back:
sLabel = i18nc("go back", "&Back");
if (QGuiApplication::isRightToLeft()) {
iconName = QStringLiteral("go-next");
}
break;
case Forward:
sLabel = i18nc("go forward", "&Forward");
if (QGuiApplication::isRightToLeft()) {
iconName = QStringLiteral("go-previous");
}
break;
case Home:
sLabel = i18nc("home page", "&Home");
break;
case Preferences:
case AboutApp:
case HelpContents: {
QString appDisplayName = QGuiApplication::applicationDisplayName();
if (appDisplayName.isEmpty()) {
appDisplayName = QCoreApplication::applicationName();
}
sLabel = pInfo->psLabel.subs(appDisplayName).toString();
} break;
default:
sLabel = pInfo->psLabel.toString();
}
if (QGuiApplication::isRightToLeft()) {
switch (id) {
case Prior:
iconName = QStringLiteral("go-next-view-page");
break;
case Next:
iconName = QStringLiteral("go-previous-view-page");
break;
case FirstPage:
iconName = QStringLiteral("go-last-view-page");
break;
case LastPage:
iconName = QStringLiteral("go-first-view-page");
break;
case DocumentBack:
iconName = QStringLiteral("go-next");
break;
case DocumentForward:
iconName = QStringLiteral("go-previous");
break;
default:
break;
}
}
if (id == Donate) {
const QString currencyCode = QLocale().currencySymbol(QLocale::CurrencyIsoCode).toLower();
if (!currencyCode.isEmpty()) {
iconName = QStringLiteral("help-donate-%1").arg(currencyCode);
}
}
QIcon icon = iconName.isEmpty() ? QIcon() : QIcon::fromTheme(iconName);
switch (id) {
case Open:
pAction = new KOpenAction(parent);
break;
case OpenRecent:
pAction = new KRecentFilesAction(parent);
break;
case ShowMenubar: {
pAction = new KToggleAction(parent);
pAction->setWhatsThis(
i18n("Show Menubar<p>"
"Shows the menubar again after it has been hidden</p>"));
pAction->setChecked(true);
ShowMenubarActionFilter *mf = new ShowMenubarActionFilter(pAction);
const auto lstWidget = qApp->topLevelWidgets();
for (QWidget *w : lstWidget) {
if (qobject_cast<QMainWindow *>(w)) {
w->installEventFilter(mf);
}
}
mf->updateAction();
break;
}
case ShowToolbar:
pAction = new KToggleAction(parent);
pAction->setChecked(true);
break;
case ShowStatusbar:
pAction = new KToggleAction(parent);
pAction->setWhatsThis(
i18n("Show Statusbar<p>"
"Shows the statusbar, which is the bar at the bottom of the window used for status information.</p>"));
pAction->setChecked(true);
break;
case FullScreen:
pAction = new KToggleFullScreenAction(parent);
pAction->setCheckable(true);
break;
// Same as default, but with the app icon
case AboutApp: {
pAction = new QAction(parent);
icon = qApp->windowIcon();
break;
}
case HamburgerMenu: {
pAction = new KHamburgerMenu(parent);
break;
}
default:
pAction = new QAction(parent);
break;
}
// Set the text before setting the MenuRole, as on OS X setText will do some heuristic role guessing.
// This ensures user menu items get the intended role out of the list below.
pAction->setText(sLabel);
switch (id) {
case Quit:
pAction->setMenuRole(QAction::QuitRole);
break;
case Preferences:
pAction->setMenuRole(QAction::PreferencesRole);
break;
case AboutApp:
pAction->setMenuRole(QAction::AboutRole);
break;
default:
pAction->setMenuRole(QAction::NoRole);
break;
}
if (!pInfo->psToolTip.isEmpty()) {
pAction->setToolTip(pInfo->psToolTip.toString());
}
pAction->setIcon(icon);
QList<QKeySequence> cut = KStandardShortcut::shortcut(pInfo->idAccel);
if (!cut.isEmpty()) {
// emulate KActionCollection::setDefaultShortcuts to allow the use of "configure shortcuts"
pAction->setShortcuts(cut);
pAction->setProperty("defaultShortcuts", QVariant::fromValue(cut));
}
pAction->connect(KStandardShortcut::shortcutWatcher(),
&KStandardShortcut::StandardShortcutWatcher::shortcutChanged,
pAction,
[pAction, shortcut = pInfo->idAccel](KStandardShortcut::StandardShortcut id, const QList<QKeySequence> &newShortcut) {
if (id != shortcut) {
return;
}
pAction->setShortcuts(newShortcut);
pAction->setProperty("defaultShortcuts", QVariant::fromValue(newShortcut));
});
pAction->setObjectName(pInfo->psName.toString());
}
if (pAction && parent && parent->inherits("KActionCollection")) {
QMetaObject::invokeMethod(parent, "addAction", Q_ARG(QString, pAction->objectName()), Q_ARG(QAction *, pAction));
}
return pAction;
}
QAction *create(StandardAction id, const QObject *recvr, const char *slot, QObject *parent)
{
QAction *pAction = _k_createInternal(id, parent);
if (recvr && slot) {
if (id == OpenRecent) {
// FIXME QAction port: probably a good idea to find a cleaner way to do this
// Open Recent is a special case - provide the selected URL
QObject::connect(pAction, SIGNAL(urlSelected(QUrl)), recvr, slot);
} else if (id == ConfigureToolbars) { // #200815
QObject::connect(pAction, SIGNAL(triggered(bool)), recvr, slot, Qt::QueuedConnection);
} else {
QObject::connect(pAction, SIGNAL(triggered(bool)), recvr, slot);
}
}
return pAction;
}
QString name(StandardAction id)
{
const KStandardActionInfo *pInfo = infoPtr(id);
return (pInfo) ? pInfo->psName.toString() : QString();
}
QAction *openNew(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(New, recvr, slot, parent);
}
QAction *open(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Open, recvr, slot, parent);
}
KRecentFilesAction *openRecent(const QObject *recvr, const char *slot, QObject *parent)
{
return (KRecentFilesAction *)KStandardAction::create(OpenRecent, recvr, slot, parent);
}
QAction *save(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Save, recvr, slot, parent);
}
QAction *saveAs(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(SaveAs, recvr, slot, parent);
}
QAction *revert(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Revert, recvr, slot, parent);
}
QAction *print(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Print, recvr, slot, parent);
}
QAction *printPreview(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(PrintPreview, recvr, slot, parent);
}
QAction *close(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Close, recvr, slot, parent);
}
QAction *mail(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Mail, recvr, slot, parent);
}
QAction *quit(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Quit, recvr, slot, parent);
}
QAction *undo(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Undo, recvr, slot, parent);
}
QAction *redo(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Redo, recvr, slot, parent);
}
QAction *cut(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Cut, recvr, slot, parent);
}
QAction *copy(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Copy, recvr, slot, parent);
}
QAction *paste(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Paste, recvr, slot, parent);
}
QAction *clear(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Clear, recvr, slot, parent);
}
QAction *selectAll(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(SelectAll, recvr, slot, parent);
}
QAction *deselect(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Deselect, recvr, slot, parent);
}
QAction *find(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Find, recvr, slot, parent);
}
QAction *findNext(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(FindNext, recvr, slot, parent);
}
QAction *findPrev(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(FindPrev, recvr, slot, parent);
}
QAction *replace(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Replace, recvr, slot, parent);
}
QAction *actualSize(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(ActualSize, recvr, slot, parent);
}
QAction *fitToPage(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(FitToPage, recvr, slot, parent);
}
QAction *fitToWidth(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(FitToWidth, recvr, slot, parent);
}
QAction *fitToHeight(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(FitToHeight, recvr, slot, parent);
}
QAction *zoomIn(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(ZoomIn, recvr, slot, parent);
}
QAction *zoomOut(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(ZoomOut, recvr, slot, parent);
}
QAction *zoom(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Zoom, recvr, slot, parent);
}
QAction *redisplay(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Redisplay, recvr, slot, parent);
}
QAction *up(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Up, recvr, slot, parent);
}
QAction *back(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Back, recvr, slot, parent);
}
QAction *forward(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Forward, recvr, slot, parent);
}
QAction *home(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Home, recvr, slot, parent);
}
QAction *prior(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Prior, recvr, slot, parent);
}
QAction *next(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Next, recvr, slot, parent);
}
QAction *goTo(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Goto, recvr, slot, parent);
}
QAction *gotoPage(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(GotoPage, recvr, slot, parent);
}
QAction *gotoLine(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(GotoLine, recvr, slot, parent);
}
QAction *firstPage(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(FirstPage, recvr, slot, parent);
}
QAction *lastPage(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(LastPage, recvr, slot, parent);
}
QAction *documentBack(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(DocumentBack, recvr, slot, parent);
}
QAction *documentForward(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(DocumentForward, recvr, slot, parent);
}
QAction *addBookmark(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(AddBookmark, recvr, slot, parent);
}
QAction *editBookmarks(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(EditBookmarks, recvr, slot, parent);
}
QAction *spelling(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Spelling, recvr, slot, parent);
}
static QAction *buildAutomaticAction(QObject *parent, StandardAction id, const char *slot)
{
const KStandardActionInfo *p = infoPtr(id);
if (!p) {
return nullptr;
}
AutomaticAction *action = new AutomaticAction(QIcon::fromTheme(p->psIconName.toString()), p->psLabel.toString(), p->idAccel, slot, parent);
action->setObjectName(p->psName.toString());
if (!p->psToolTip.isEmpty()) {
action->setToolTip(p->psToolTip.toString());
}
if (parent && parent->inherits("KActionCollection")) {
QMetaObject::invokeMethod(parent, "addAction", Q_ARG(QString, action->objectName()), Q_ARG(QAction *, action));
}
return action;
}
QAction *cut(QObject *parent)
{
return buildAutomaticAction(parent, Cut, SLOT(cut()));
}
QAction *copy(QObject *parent)
{
return buildAutomaticAction(parent, Copy, SLOT(copy()));
}
QAction *paste(QObject *parent)
{
return buildAutomaticAction(parent, Paste, SLOT(paste()));
}
QAction *clear(QObject *parent)
{
return buildAutomaticAction(parent, Clear, SLOT(clear()));
}
QAction *selectAll(QObject *parent)
{
return buildAutomaticAction(parent, SelectAll, SLOT(selectAll()));
}
KToggleAction *showMenubar(const QObject *recvr, const char *slot, QObject *parent)
{
QAction *ret = KStandardAction::create(ShowMenubar, recvr, slot, parent);
Q_ASSERT(qobject_cast<KToggleAction *>(ret));
return static_cast<KToggleAction *>(ret);
}
KToggleAction *showStatusbar(const QObject *recvr, const char *slot, QObject *parent)
{
QAction *ret = KStandardAction::create(ShowStatusbar, recvr, slot, parent);
Q_ASSERT(qobject_cast<KToggleAction *>(ret));
return static_cast<KToggleAction *>(ret);
}
KToggleFullScreenAction *fullScreen(const QObject *recvr, const char *slot, QWidget *window, QObject *parent)
{
KToggleFullScreenAction *ret;
ret = static_cast<KToggleFullScreenAction *>(KStandardAction::create(FullScreen, recvr, slot, parent));
ret->setWindow(window);
return ret;
}
QAction *keyBindings(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(KeyBindings, recvr, slot, parent);
}
QAction *preferences(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Preferences, recvr, slot, parent);
}
QAction *configureToolbars(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(ConfigureToolbars, recvr, slot, parent);
}
QAction *configureNotifications(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(ConfigureNotifications, recvr, slot, parent);
}
QAction *helpContents(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(HelpContents, recvr, slot, parent);
}
QAction *whatsThis(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(WhatsThis, recvr, slot, parent);
}
QAction *reportBug(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(ReportBug, recvr, slot, parent);
}
QAction *switchApplicationLanguage(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(SwitchApplicationLanguage, recvr, slot, parent);
}
QAction *aboutApp(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(AboutApp, recvr, slot, parent);
}
QAction *aboutKDE(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(AboutKDE, recvr, slot, parent);
}
QAction *deleteFile(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(DeleteFile, recvr, slot, parent);
}
QAction *renameFile(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(RenameFile, recvr, slot, parent);
}
QAction *moveToTrash(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(MoveToTrash, recvr, slot, parent);
}
QAction *donate(const QObject *recvr, const char *slot, QObject *parent)
{
return KStandardAction::create(Donate, recvr, slot, parent);
}
KHamburgerMenu *hamburgerMenu(const QObject *recvr, const char *slot, QObject *parent)
{
return static_cast<KHamburgerMenu *>(KStandardAction::create(HamburgerMenu, recvr, slot, parent));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,210 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 1999, 2000 Kurt Granroth <granroth@kde.org>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KSTANDARDACTION_PRIVATE_H
#define KSTANDARDACTION_PRIVATE_H
#include <QAction>
#include <QApplication>
#include <KLazyLocalizedString>
#include <KLocalizedString>
#include <KStandardShortcut>
#include <string>
namespace KStandardAction
{
// Helper class for storing raw data in static tables which can be used for QString instance
// creation at runtime without copying/converting to new memalloc'ed memory, as well as avoiding
// that way storing the strings directly as QStrings resulting in non-constexpr init code on
// library loading
struct RawStringData {
template<std::size_t StringSize>
constexpr inline RawStringData(const char16_t (&_data)[StringSize])
: data(_data)
, size(std::char_traits<char16_t>::length(_data))
{
}
constexpr inline RawStringData(std::nullptr_t)
{
}
constexpr inline RawStringData() = default;
inline QString toString() const
{
if (!data) {
return QString();
}
return Qt::Literals::StringLiterals::operator""_s(data, size);
}
private:
const char16_t *const data = nullptr;
const std::size_t size = 0;
};
struct KStandardActionInfo {
StandardAction id;
KStandardShortcut::StandardShortcut idAccel;
const RawStringData psName;
const KLazyLocalizedString psLabel;
const KLazyLocalizedString psToolTip;
const RawStringData psIconName;
};
// clang-format off
static const KStandardActionInfo g_rgActionInfo[] = {
{ New, KStandardShortcut::New, u"file_new", kli18n("&New"), kli18n("Create new document"), u"document-new" },
{ Open, KStandardShortcut::Open, u"file_open", kli18n("&Open…"), kli18n("Open an existing document"), u"document-open" },
{ OpenRecent, KStandardShortcut::AccelNone, u"file_open_recent", kli18n("Open &Recent"), kli18n("Open a document which was recently opened"), u"document-open-recent" },
{ Save, KStandardShortcut::Save, u"file_save", kli18n("&Save"), kli18n("Save document"), u"document-save" },
{ SaveAs, KStandardShortcut::SaveAs, u"file_save_as", kli18n("Save &As…"), kli18n("Save document under a new name"), u"document-save-as" },
{ Revert, KStandardShortcut::Revert, u"file_revert", kli18n("Re&vert"), kli18n("Revert unsaved changes made to document"), u"document-revert" },
{ Close, KStandardShortcut::Close, u"file_close", kli18n("&Close"), kli18n("Close document"), u"document-close" },
{ Print, KStandardShortcut::Print, u"file_print", kli18n("&Print…"), kli18n("Print document"), u"document-print" },
{ PrintPreview, KStandardShortcut::PrintPreview, u"file_print_preview", kli18n("Print Previe&w"), kli18n("Show a print preview of document"), u"document-print-preview" },
{ Mail, KStandardShortcut::Mail, u"file_mail", kli18n("&Mail…"), kli18n("Send document by mail"), u"mail-send" },
{ Quit, KStandardShortcut::Quit, u"file_quit", kli18n("&Quit"), kli18n("Quit application"), u"application-exit" },
{ Undo, KStandardShortcut::Undo, u"edit_undo", kli18n("&Undo"), kli18n("Undo last action"), u"edit-undo" },
{ Redo, KStandardShortcut::Redo, u"edit_redo", kli18n("Re&do"), kli18n("Redo last undone action"), u"edit-redo" },
{ Cut, KStandardShortcut::Cut, u"edit_cut", kli18n("Cu&t"), kli18n("Cut selection to clipboard"), u"edit-cut" },
{ Copy, KStandardShortcut::Copy, u"edit_copy", kli18n("&Copy"), kli18n("Copy selection to clipboard"), u"edit-copy" },
{ Paste, KStandardShortcut::Paste, u"edit_paste", kli18n("&Paste"), kli18n("Paste clipboard content"), u"edit-paste" },
{ Clear, KStandardShortcut::Clear, u"edit_clear", kli18n("C&lear"), {}, u"edit-clear" },
{ SelectAll, KStandardShortcut::SelectAll, u"edit_select_all", kli18n("Select &All"), {}, u"edit-select-all" },
{ Deselect, KStandardShortcut::Deselect, u"edit_deselect", kli18n("Dese&lect"), {}, u"edit-select-none" },
{ Find, KStandardShortcut::Find, u"edit_find", kli18n("&Find…"), {}, u"edit-find" },
{ FindNext, KStandardShortcut::FindNext, u"edit_find_next", kli18n("Find &Next"), {}, u"go-down-search" },
{ FindPrev, KStandardShortcut::FindPrev, u"edit_find_prev", kli18n("Find Pre&vious"), {}, u"go-up-search" },
{ Replace, KStandardShortcut::Replace, u"edit_replace", kli18n("&Replace…"), {}, u"edit-find-replace" },
{ ActualSize, KStandardShortcut::ActualSize, u"view_actual_size", kli18n("Zoom to &Actual Size"), kli18n("View document at its actual size"), u"zoom-original" },
{ FitToPage, KStandardShortcut::FitToPage, u"view_fit_to_page", kli18n("&Fit to Page"), kli18n("Zoom to fit page in window"), u"zoom-fit-page" },
{ FitToWidth, KStandardShortcut::FitToWidth, u"view_fit_to_width", kli18n("Fit to Page &Width"), kli18n("Zoom to fit page width in window"), u"zoom-fit-width" },
{ FitToHeight, KStandardShortcut::FitToHeight, u"view_fit_to_height", kli18n("Fit to Page &Height"), kli18n("Zoom to fit page height in window"), u"zoom-fit-height" },
{ ZoomIn, KStandardShortcut::ZoomIn, u"view_zoom_in", kli18n("Zoom &In"), {}, u"zoom-in" },
{ ZoomOut, KStandardShortcut::ZoomOut, u"view_zoom_out", kli18n("Zoom &Out"), {}, u"zoom-out" },
{ Zoom, KStandardShortcut::Zoom, u"view_zoom", kli18n("&Zoom…"), kli18n("Select zoom level"), u"zoom" },
{ Redisplay, KStandardShortcut::Reload, u"view_redisplay", kli18n("&Refresh"), kli18n("Refresh document"), u"view-refresh" },
{ Up, KStandardShortcut::Up, u"go_up", kli18n("&Up"), kli18n("Go up"), u"go-up" },
// The following three have special i18n() needs for sLabel
{ Back, KStandardShortcut::Back, u"go_back", {}, {}, u"go-previous" },
{ Forward, KStandardShortcut::Forward, u"go_forward", {}, {}, u"go-next" },
{ Home, KStandardShortcut::Home, u"go_home", {}, {}, u"go-home" },
{ Prior, KStandardShortcut::Prior, u"go_previous", kli18n("&Previous Page"), kli18n("Go to previous page"), u"go-previous-view-page" },
{ Next, KStandardShortcut::Next, u"go_next", kli18n("&Next Page"), kli18n("Go to next page"), u"go-next-view-page" },
{ Goto, KStandardShortcut::Goto, u"go_goto", kli18n("&Go To…"), {}, {} },
{ GotoPage, KStandardShortcut::GotoPage, u"go_goto_page", kli18n("&Go to Page…"), {}, u"go-jump" },
{ GotoLine, KStandardShortcut::GotoLine, u"go_goto_line", kli18n("&Go to Line…"), {}, {} },
{ FirstPage, KStandardShortcut::Begin, u"go_first", kli18n("&First Page"), kli18n("Go to first page"), u"go-first-view-page" },
{ LastPage, KStandardShortcut::End, u"go_last", kli18n("&Last Page"), kli18n("Go to last page"), u"go-last-view-page" },
{ DocumentBack, KStandardShortcut::DocumentBack, u"go_document_back", kli18n("&Back"), kli18n("Go back in document"), u"go-previous" },
{ DocumentForward, KStandardShortcut::DocumentForward, u"go_document_forward", kli18n("&Forward"), kli18n("Go forward in document"), u"go-next" },
{ AddBookmark, KStandardShortcut::AddBookmark, u"bookmark_add", kli18n("&Add Bookmark"), {}, u"bookmark-new" },
{ EditBookmarks, KStandardShortcut::EditBookmarks, u"bookmark_edit", kli18n("&Edit Bookmarks…"), {}, u"bookmarks-organize" },
{ Spelling, KStandardShortcut::Spelling, u"tools_spelling", kli18n("&Spelling…"), kli18n("Check spelling in document"), u"tools-check-spelling" },
{ ShowMenubar, KStandardShortcut::ShowMenubar, u"options_show_menubar", kli18n("Show &Menubar"), kli18n("Show or hide menubar"), u"show-menu" },
{ ShowToolbar, KStandardShortcut::ShowToolbar, u"options_show_toolbar", kli18n("Show &Toolbar"), kli18n("Show or hide toolbar"), {} },
{ ShowStatusbar, KStandardShortcut::ShowStatusbar, u"options_show_statusbar", kli18n("Show St&atusbar"), kli18n("Show or hide statusbar"), {} },
{ FullScreen, KStandardShortcut::FullScreen, u"fullscreen", kli18n("F&ull Screen Mode"), {}, u"view-fullscreen" },
{ KeyBindings, KStandardShortcut::KeyBindings, u"options_configure_keybinding", kli18n("Configure Keyboard S&hortcuts…"), {}, u"configure-shortcuts" },
{ Preferences, KStandardShortcut::Preferences, u"options_configure", kli18n("&Configure %1…"), {}, u"configure" },
{ ConfigureToolbars, KStandardShortcut::ConfigureToolbars, u"options_configure_toolbars", kli18n("Configure Tool&bars…"), {}, u"configure-toolbars" },
{ ConfigureNotifications, KStandardShortcut::ConfigureNotifications, u"options_configure_notifications", kli18n("Configure &Notifications…"), {}, u"preferences-desktop-notification" },
// the idea here is that Contents is used in menus, and Help in dialogs, so both share the same
// shortcut
{ HelpContents, KStandardShortcut::Help, u"help_contents", kli18n("%1 &Handbook"), {}, u"help-contents" },
{ WhatsThis, KStandardShortcut::WhatsThis, u"help_whats_this", kli18n("What's &This?"), {}, u"help-contextual" },
{ ReportBug, KStandardShortcut::ReportBug, u"help_report_bug", kli18n("&Report Bug…"), {}, u"tools-report-bug" },
{ SwitchApplicationLanguage, KStandardShortcut::SwitchApplicationLanguage, u"switch_application_language", kli18n("Configure &Language…"), {}, u"preferences-desktop-locale" },
{ AboutApp, KStandardShortcut::AccelNone, u"help_about_app", kli18n("&About %1"), {}, nullptr },
{ AboutKDE, KStandardShortcut::AccelNone, u"help_about_kde", kli18n("About &KDE"), {}, u"kde" },
{ DeleteFile, KStandardShortcut::DeleteFile, u"deletefile", kli18n("&Delete"), {}, u"edit-delete" },
{ RenameFile, KStandardShortcut::RenameFile, u"renamefile", kli18n("&Rename…"), {}, u"edit-rename" },
{ MoveToTrash, KStandardShortcut::MoveToTrash, u"movetotrash", kli18n("&Move to Trash"), {}, u"trash-empty" },
{ Donate, KStandardShortcut::Donate, u"help_donate", kli18n("&Donate"), {}, u"help-donate"},
{ HamburgerMenu, KStandardShortcut::OpenMainMenu, u"hamburger_menu", kli18n("Open &Menu"), {}, u"application-menu" },
{ ActionNone, KStandardShortcut::AccelNone, nullptr, {}, {}, nullptr }
};
// clang-format on
inline const KStandardActionInfo *infoPtr(StandardAction id)
{
for (uint i = 0; g_rgActionInfo[i].id != ActionNone; i++) {
if (g_rgActionInfo[i].id == id) {
return &g_rgActionInfo[i];
}
}
return nullptr;
}
static inline QStringList internal_stdNames()
{
QStringList result;
for (uint i = 0; g_rgActionInfo[i].id != ActionNone; i++)
if (!g_rgActionInfo[i].psLabel.isEmpty()) {
if (QByteArrayView(g_rgActionInfo[i].psLabel.untranslatedText()).contains("%1"))
// Prevents KLocalizedString::toString() from complaining about unsubstituted placeholder.
{
result.append(g_rgActionInfo[i].psLabel.subs(QString()).toString());
} else {
result.append(g_rgActionInfo[i].psLabel.toString());
}
}
return result;
}
class AutomaticAction : public QAction
{
Q_OBJECT
public:
AutomaticAction(const QIcon &icon, const QString &text, KStandardShortcut::StandardShortcut standardShortcut, const char *slot, QObject *parent);
public Q_SLOTS:
inline void cut()
{
invokeEditSlot("cut");
}
inline void copy()
{
invokeEditSlot("copy");
}
inline void paste()
{
invokeEditSlot("paste");
}
inline void clear()
{
invokeEditSlot("clear");
}
inline void selectAll()
{
invokeEditSlot("selectAll");
}
void invokeEditSlot(const char *slot)
{
if (qApp->focusWidget()) {
QMetaObject::invokeMethod(qApp->focusWidget(), slot);
}
}
};
}
#endif
@@ -0,0 +1,87 @@
/*
SPDX-FileCopyrightText: 2024 Christoph Cullmann <cullmann@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kstylemanager.h"
#include <KActionMenu>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig>
#include <QAction>
#include <QActionGroup>
#include <QApplication>
#include <QStyle>
#include <QStyleFactory>
#include <private/qguiapplication_p.h>
#include <qpa/qplatformtheme.h>
void KStyleManager::initStyle()
{
// do nothing if we have the proper platform theme already
if (QGuiApplicationPrivate::platformTheme() && QGuiApplicationPrivate::platformTheme()->name() == QLatin1String("kde")) {
return;
}
// get config, with fallback to kdeglobals
const auto config = KSharedConfig::openConfig();
// enforce the style configured by the user, with kdeglobals fallback
// if not set or the style is not there, use Breeze
QString styleToUse = KConfigGroup(config, QStringLiteral("KDE")).readEntry("widgetStyle", QString());
if (styleToUse.isEmpty() || !QApplication::setStyle(styleToUse)) {
styleToUse = QStringLiteral("breeze");
QApplication::setStyle(styleToUse);
}
}
QAction *KStyleManager::createConfigureAction(QObject *parent)
{
// if we are running with our application theme, just return a disabled & hidden action
// there we just follow the Plasma theme
if (QGuiApplicationPrivate::platformTheme() && QGuiApplicationPrivate::platformTheme()->name() == QLatin1String("kde")) {
QAction *a = new QAction(parent);
a->setEnabled(false);
a->setVisible(false);
return a;
}
// else: provide style chooser
// get config, without fallback to kdeglobals
const auto config = KSharedConfig::openConfig(QString(), KConfig::NoGlobals);
const QString styleWeUse = KConfigGroup(config, QStringLiteral("KDE")).readEntry("widgetStyle", QString());
// build menu with default to reset setting and all known styles
KActionMenu *menu = new KActionMenu(QIcon::fromTheme(QStringLiteral("preferences-desktop-theme-applications")), i18n("Application Style"), parent);
QActionGroup *group = new QActionGroup(menu);
const QStringList styles = QStringList() << QString() << QStyleFactory::keys();
for (const QString &style : styles) {
QAction *action = new QAction(style.isEmpty() ? i18n("Default") : style, menu);
action->setData(style);
action->setActionGroup(group);
action->setCheckable(true);
if (style.toLower() == styleWeUse.toLower()) {
action->setChecked(true);
}
menu->addAction(action);
}
// toggle style change
QObject::connect(group, &QActionGroup::triggered, group, [](QAction *action) {
const QString styleToUse = action->data().toString();
const auto config = KSharedConfig::openConfig();
if (styleToUse.isEmpty()) {
KConfigGroup(config, QStringLiteral("KDE")).deleteEntry("widgetStyle");
} else {
KConfigGroup(config, QStringLiteral("KDE")).writeEntry("widgetStyle", styleToUse);
}
initStyle();
});
return menu;
}
@@ -0,0 +1,47 @@
/*
SPDX-FileCopyrightText: 2024 Christoph Cullmann <cullmann@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KSTYLEMANAGER_H
#define KSTYLEMANAGER_H
#include <kconfigwidgets_export.h>
class QAction;
class QObject;
/**
* Manager for Qt widget styles
*/
namespace KStyleManager
{
/**
* Enforces the style configured by the user with fallback to the Breeze style.
*
* (following the settings in the application configuration with fallback to kdeglobals)
*
* Must be called after one has constructed the QApplication.
*
* If the application is already using the KDE platform theme, the style will not
* be touched and the platform theme will ensure proper styling.
*
* @since 6.3
*/
KCONFIGWIDGETS_EXPORT void initStyle();
/**
* Creates an action to configure the current used style.
*
* If the application is running using the KDE platform theme, this will
* just return a disabled and hidden action, else it will provide an action to
* show a menu to select the style to use for this applications.
*
* @since 6.3
*/
KCONFIGWIDGETS_EXPORT QAction *createConfigureAction(QObject *parent = nullptr);
}
#endif
@@ -0,0 +1,65 @@
/*
SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
SPDX-FileContributor: Stephen Kelly <stephen@kdab.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KVIEWSTATEMAINTAINER_H
#define KVIEWSTATEMAINTAINER_H
#include <KViewStateMaintainerBase>
#include <QAbstractItemView>
#include <KConfigGroup>
/**
* @class KViewStateMaintainer kviewstatemaintainer.h KViewStateMaintainer
*
* @brief Encapsulates the maintenance of state between resets of QAbstractItemModel
*
* @code
* m_collectionViewStateMaintainer = new KViewStateMaintainer<Akonadi::ETMViewStateSaver>(KSharedConfig::openConfig()->group("collectionView"));
* m_collectionViewStateMaintainer->setView(m_collectionView);
*
* m_collectionCheckStateMaintainer = new KViewStateMaintainer<Akonadi::ETMViewStateSaver>(KSharedConfig::openConfig()->group("collectionCheckState"));
* m_collectionCheckStateMaintainer->setSelectionModel(m_checkableProxy->selectionModel());
* @endcode
*
* @see KConfigViewStateSaver
*/
template<typename StateSaver>
class KViewStateMaintainer : public KViewStateMaintainerBase
{
typedef StateSaver StateRestorer;
public:
KViewStateMaintainer(const KConfigGroup &configGroup, QObject *parent = nullptr)
: KViewStateMaintainerBase(parent)
, m_configGroup(configGroup)
{
}
/* reimp */ void saveState()
{
StateSaver saver;
saver.setView(view());
saver.setSelectionModel(selectionModel());
saver.saveState(m_configGroup);
m_configGroup.sync();
}
/* reimp */ void restoreState()
{
StateRestorer *restorer = new StateRestorer;
restorer->setView(view());
restorer->setSelectionModel(selectionModel());
restorer->restoreState(m_configGroup);
}
private:
KConfigGroup m_configGroup;
};
#endif