diff --git a/src/coreservices.cpp b/src/coreservices.cpp index e1b57089f90a..fd066ba078fd 100644 --- a/src/coreservices.cpp +++ b/src/coreservices.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -45,10 +47,12 @@ #include "util/sandbox.h" #endif +#ifdef Q_OS_LINUX +#include +#endif + #if defined(Q_OS_LINUX) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -#include #include - #include #include "engine/channelhandle.h" @@ -98,11 +102,229 @@ Bool __xErrorHandler(Display* display, XErrorEvent* event, xError* error) { #endif +#if defined(Q_OS_LINUX) +QLocale localeFromXkbSymbol(const QString& xkbLayout) { + // This maps XKB layouts to locales of keyboard mappings that are shipped with Mixxx + static const QMap xkbToLocaleMap = { + {"cz", QLocale(QLocale::Czech, QLocale::CzechRepublic)}, // cs_CZ.kbd.cfg + {"de", QLocale(QLocale::German, QLocale::Germany)}, // de_DE.kbd.cfg + {"de+nodeadkeys", QLocale(QLocale::German, QLocale::Germany)}, // de_DE.kbd.cfg + {"es", QLocale(QLocale::Spanish, QLocale::Spain)}, // es_ES.kbd.cfg + {"es+nodeadkeys", QLocale(QLocale::Spanish, QLocale::Spain)}, // es_ES.kbd.cfg + {"fr", QLocale(QLocale::French, QLocale::France)}, // fr_FR.kbd.cfg + {"fr+nodeadkeys", QLocale(QLocale::French, QLocale::France)}, // fr_FR.kbd.cfg + {"dk", QLocale(QLocale::Danish, QLocale::Denmark)}, // da_DK.kbd.cfg + {"dk+nodeadkeys", QLocale(QLocale::Danish, QLocale::Denmark)}, // da_DK.kbd.cfg + {"gr", QLocale(QLocale::Greek, QLocale::Greece)}, // el_GR.kbd.cfg + {"gr+nodeadkeys", QLocale(QLocale::Greek, QLocale::Greece)}, // el_GR.kbd.cfg + {"fi", QLocale(QLocale::Finnish, QLocale::Finland)}, // fi_FI.kbd.cfg + {"it", QLocale(QLocale::Italian, QLocale::Italy)}, // it_IT.kbd.cfg + {"it+nodeadkeys", QLocale(QLocale::Italian, QLocale::Italy)}, // it_IT.kbd.cfg + {"us", QLocale(QLocale::English, QLocale::UnitedStates)}, // en_US.kbd.cfg + {"ru", QLocale(QLocale::Russian, QLocale::Russia)}, // ru_RU.kbd.cfg + {"ch", QLocale(QLocale::German, QLocale::Switzerland)}, // de_CH.kbd.cfg + {"ch+de_nodeadkeys", QLocale(QLocale::German, QLocale::Switzerland)}, // de_CH.kbd.cfg + {"ch+fr", QLocale(QLocale::French, QLocale::Switzerland)}, // fr_CH.kbd.cfg + {"ch+fr_nodeadkeys", QLocale(QLocale::French, QLocale::Switzerland)} // fr_CH.kbd.cfg + }; + return xkbToLocaleMap.value(xkbLayout, QLocale(QLocale::English, QLocale::UnitedStates)); +} + +QLocale localeFromXkbName(const QString& xkbLayout) { + // This maps XKB layouts to locales of keyboard mappings that are shipped with Mixxx + static const QMap xkbToLocaleMap = { + {"Czech", + QLocale(QLocale::Czech, + QLocale::CzechRepublic)}, // cs_CZ.kbd.cfg + {"German", + QLocale(QLocale::German, + QLocale::Germany)}, // de_DE.kbd.cfg + {"German (no dead keys)", + QLocale(QLocale::German, + QLocale::Germany)}, // de_DE.kbd.cfg + {"Spanish", + QLocale(QLocale::Spanish, QLocale::Spain)}, // es_ES.kbd.cfg + {"Spanish (no dead keys)", + QLocale(QLocale::Spanish, QLocale::Spain)}, // es_ES.kbd.cfg + {"French", + QLocale(QLocale::French, QLocale::France)}, // fr_FR.kbd.cfg + {"French (no dead keys)", + QLocale(QLocale::French, QLocale::France)}, // fr_FR.kbd.cfg + {"Danish", + QLocale(QLocale::Danish, + QLocale::Denmark)}, // da_DK.kbd.cfg + {"Danish (no dead keys)", + QLocale(QLocale::Danish, + QLocale::Denmark)}, // da_DK.kbd.cfg + {"Greek", + QLocale(QLocale::Greek, QLocale::Greece)}, // el_GR.kbd.cfg + {"Greek (no dead keys)", + QLocale(QLocale::Greek, QLocale::Greece)}, // el_GR.kbd.cfg + {"Finnish", + QLocale(QLocale::Finnish, + QLocale::Finland)}, // fi_FI.kbd.cfg + {"Italian", + QLocale(QLocale::Italian, QLocale::Italy)}, // it_IT.kbd.cfg + {"Italian (no dead keys)", + QLocale(QLocale::Italian, QLocale::Italy)}, // it_IT.kbd.cfg + {"English (US)", + QLocale(QLocale::English, + QLocale::UnitedStates)}, // en_US.kbd.cfg + {"Russian", + QLocale(QLocale::Russian, + QLocale::Russia)}, // ru_RU.kbd.cfg + {"German (Switzerland)", + QLocale(QLocale::German, + QLocale::Switzerland)}, // de_CH.kbd.cfg + {"German (Switzerland, no dead keys)", + QLocale(QLocale::German, + QLocale::Switzerland)}, // de_CH.kbd.cfg + {"French (Switzerland)", + QLocale(QLocale::French, + QLocale::Switzerland)}, // fr_CH.kbd.cfg + {"French (Switzerland, no dead keys)", + QLocale(QLocale::French, + QLocale::Switzerland)} // fr_CH.kbd.cfg + }; + return xkbToLocaleMap.value(xkbLayout, QLocale(QLocale::English, QLocale::UnitedStates)); +} + +inline bool isGnomeSession() { + const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + QString desktop = env.value("XDG_CURRENT_DESKTOP").toLower(); + return desktop.contains("gnome"); +} + +inline bool isXfceSession() { + const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + QString desktop = env.value("XDG_CURRENT_DESKTOP").toLower(); + return desktop.contains("xfce"); +} + +QString getCurrentXkbLayoutName() { + XkbIgnoreExtension(False); + Display* pDisplay = XkbOpenDisplay(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + if (!pDisplay) { + // No X11 / XWayland running or no Xkb in use + return {}; + } + + XkbStateRec state; + if (XkbGetState(pDisplay, XkbUseCoreKbd, &state) != Success) { + qWarning() << "XkbGetState failed"; + return {}; + } + + XkbDescPtr pDesc = XkbGetMap(pDisplay, 0, XkbUseCoreKbd); + if (!pDesc) { + qWarning() << "XkbGetMap failed"; + return {}; + } + + XkbGetNames(pDisplay, XkbGroupNamesMask, pDesc); + if (!pDesc->names) { + qWarning() << "XkbGetNames failed"; + return {}; + } + char* pGroupName = XGetAtomName(pDisplay, pDesc->names->groups[state.group]); + if (!pGroupName) { + qWarning() << "XGetAtomName failed"; + XkbFreeNames(pDesc, XkbGroupNamesMask, True); + return {}; + } + QString layoutName = QString(pGroupName); + XkbFreeNames(pDesc, XkbKeyNamesMask, True); + XFree(pGroupName); + return layoutName; +} +#endif + +// Returns the locale of the current keyboard layout +// On macOS and Windows QGuiApplication::inputMethod() is used straight away. +// On Linux it first tries to via X11/XWayland. That works even if Mixxx itself +// is running with Wayland. If XWayland is not installed it falls back to +// dconf/xfconf-query and than QGuiApplication::inputMethod() which is equivalent +// to "ibus engine". QGuiApplication::inputMethod() does not work with GNOME and XFCE +// https://bugreports.qt.io/browse/QTBUG-137302 inline QLocale inputLocale() { - // Use the default config for local keyboard +#if defined(Q_OS_LINUX) + QString layoutName = getCurrentXkbLayoutName(); + if (!layoutName.isEmpty()) { + qDebug() << "Keyboard Layout from XKB:" << layoutName; + return localeFromXkbName(layoutName); + } + if (isGnomeSession()) { + // In a Gnome session QGuiApplication::inputMethod() is not necessarily correct + // https://github.com/mixxxdj/mixxx/issues/14838 + // If this auto detection still fails the user may use a Custom.kb.cfg + QProcess sourcesProc; + sourcesProc.start("dconf", + {"read", "/org/gnome/desktop/input-sources/mru-sources"}); + if (sourcesProc.waitForFinished(100)) { + const QString sourcesStr = sourcesProc.readAllStandardOutput().trimmed(); + // Expecting something like this: [('xkb', 'de'), ('xkb', 'us')] + // The first match is the current layout. + // This matches entries like ('xkb', 'us') and extracts the layout + // code (e.g. 'us', 'de') + static const QRegularExpression re(QStringLiteral("\\('xkb',\\s*'([^']+)'\\)")); + QRegularExpressionMatch match = re.match(sourcesStr); + if (match.hasMatch()) { + const QString layout = match.captured(1); + ; + qDebug() << "Keyboard Layout from GNOME dconf:" << layout; + return localeFromXkbSymbol(layout); + } else { + // mru-sources (most recently used source) is empty when user + // has only one keyboard layout enabled. Use it from sources. + sourcesProc.start("dconf", + {"read", "/org/gnome/desktop/input-sources/sources"}); + if (sourcesProc.waitForFinished(100)) { + const QString sourcesStr = sourcesProc.readAllStandardOutput().trimmed(); + // Expecting something like this: [('xkb', 'de')] + QRegularExpressionMatch match = re.match(sourcesStr); + if (match.hasMatch()) { + const QString layout = match.captured(1); + qDebug() << "Keyboard Layout from GNOME dconf:" << layout; + return localeFromXkbSymbol(layout); + } else { + qDebug() << "No valid keyboard layout found in dconf:" << sourcesStr; + } + } else { + qDebug() << "Failed to read Keyboard Layout from dconf."; + } + } + } else { + qDebug() << "Failed to read Keyboard Layout from dconf."; + } + } else if (isXfceSession()) { + // In a Xfce session QGuiApplication::inputMethod() is not necessarily correct + // https://github.com/mixxxdj/mixxx/issues/14838 + // If this auto detection still fails the user may use a Custom.kb.cfg + const QStringList args{"-c", "keyboard-layout", "-p", "/Default/XkbLayout"}; + QProcess sourcesProc; + sourcesProc.start("xfconf-query", args); + if (sourcesProc.waitForFinished(100)) { + QString sourcesStr = sourcesProc.readAllStandardOutput().trimmed(); + // Expecting comma-separated layouts: de,gr,cz + // The first is the current layout. + if (sourcesStr.length() >= 2) { + const QStringList allLayouts = sourcesStr.split(','); + const QString currLayout = allLayouts[0]; + qDebug() << "Keyboard Layout from XFCE xfconf:" << currLayout; + return localeFromXkbSymbol(currLayout); + } else { + qDebug() << "No valid keyboard layout found in xfconf:" << sourcesStr; + } + } else { + qDebug() << "Failed to read Keyboard Layout from xfconf."; + } + } +#endif + QInputMethod* pInputMethod = QGuiApplication::inputMethod(); return pInputMethod ? pInputMethod->locale() : QLocale(QLocale::English); } + } // anonymous namespace namespace mixxx {