diff --git a/src/modules/keyboard/Config.cpp b/src/modules/keyboard/Config.cpp index a7c816029a..347917049d 100644 --- a/src/modules/keyboard/Config.cpp +++ b/src/modules/keyboard/Config.cpp @@ -22,11 +22,14 @@ #include "utils/Variant.h" #include +#include #include +#include #include #include #include #include +#include #include #include @@ -119,6 +122,30 @@ xkbmap_query_grp_option() return outputLine.mid( index, lastIndex - index ); } +static void +prepareGroupSwitcher( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra ) +{ + if ( extra.additionalLayout.isEmpty() ) + { + extra.groupSwitcher.clear(); + return; + } + + if ( !settings.selectedGroup.isEmpty() ) + { + extra.groupSwitcher = "grp:" + settings.selectedGroup; + } + + if ( extra.groupSwitcher.isEmpty() ) + { + extra.groupSwitcher = xkbmap_query_grp_option(); + } + if ( extra.groupSwitcher.isEmpty() ) + { + extra.groupSwitcher = "grp:alt_shift_toggle"; + } +} + AdditionalLayoutInfo Config::getAdditionalLayoutInfo( const QString& layout ) { @@ -230,20 +257,7 @@ applyXkb( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra ) QStringList basicArguments = xkbmap_model_args( settings.selectedModel ); if ( !extra.additionalLayout.isEmpty() ) { - if ( !settings.selectedGroup.isEmpty() ) - { - extra.groupSwitcher = "grp:" + settings.selectedGroup; - } - - if ( extra.groupSwitcher.isEmpty() ) - { - extra.groupSwitcher = xkbmap_query_grp_option(); - } - if ( extra.groupSwitcher.isEmpty() ) - { - extra.groupSwitcher = "grp:alt_shift_toggle"; - } - + prepareGroupSwitcher( settings, extra ); basicArguments.append( xkbmap_layout_args_with_group_switch( { extra.additionalLayout, settings.selectedLayout }, { extra.additionalVariant, settings.selectedVariant }, @@ -262,7 +276,7 @@ applyXkb( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra ) } } -static void +static bool applyLocale1( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra ) { QString layout = settings.selectedLayout; @@ -271,6 +285,7 @@ applyLocale1( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra ) if ( !extra.additionalLayout.isEmpty() ) { + prepareGroupSwitcher( settings, extra ); layout = extra.additionalLayout + "," + layout; variant = extra.additionalVariant + "," + variant; option = extra.groupSwitcher; @@ -283,7 +298,7 @@ applyLocale1( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra ) if ( !locale1.isValid() ) { cWarning() << "Interface" << locale1.interface() << "is not valid."; - return; + return false; } // Using convert=true, this also updates the VConsole config @@ -293,51 +308,97 @@ applyLocale1( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra ) if ( !r.isValid() ) { cWarning() << "Could not set keyboard config through org.freedesktop.locale1.X11Keyboard." << r.error(); + return false; } } + + return true; } -// In a config-file's list of lines, replace lines = by = +// In kxkbrc content, set a key inside the [Layout] group and create the group if needed. static void -replaceKey( QStringList& content, const QString& key, const QString& value ) +setLayoutKey( QStringList& content, const QString& key, const QString& value ) { + int groupStart = -1; + int groupEnd = content.length(); for ( int i = 0; i < content.length(); ++i ) + { + const QString line = content.at( i ).trimmed(); + if ( line == QStringLiteral( "[Layout]" ) ) + { + groupStart = i; + continue; + } + if ( groupStart >= 0 && i > groupStart && line.startsWith( '[' ) ) + { + groupEnd = i; + break; + } + } + + if ( groupStart < 0 ) + { + if ( !content.isEmpty() && !content.constLast().isEmpty() ) + { + content.append( QString() ); + } + content.append( QStringLiteral( "[Layout]" ) ); + groupStart = content.length() - 1; + groupEnd = content.length(); + } + + for ( int i = groupStart + 1; i < groupEnd; ++i ) { if ( content.at( i ).startsWith( key ) ) { content[ i ] = key + value; + return; } } + + content.insert( groupEnd, key + value ); } static bool -rewriteKWin( const QString& path, const QString& model, const QString& layouts, const QString& variants ) +rewriteKWin( const QString& path, + const QString& model, + const QString& layouts, + const QString& variants, + const QString& options ) { - if ( !QFile::exists( path ) ) - { - return false; - } - QFile config( path ); - if ( !config.open( QIODevice::ReadOnly ) ) + QStringList content; + if ( config.exists() ) { - return false; + if ( !config.open( QIODevice::ReadOnly ) ) + { + return false; + } + content = []( QFile& f ) + { + QTextStream s( &f ); + return s.readAll().split( '\n' ); + }( config ); + config.close(); } - QStringList content = []( QFile& f ) + else { - QTextStream s( &f ); - return s.readAll().split( '\n' ); - }( config ); - config.close(); + QDir().mkpath( QFileInfo( path ).path() ); + } if ( !config.open( QIODevice::WriteOnly ) ) { return false; } - replaceKey( content, QStringLiteral( "Model=" ), model ); - replaceKey( content, QStringLiteral( "LayoutList=" ), layouts ); - replaceKey( content, QStringLiteral( "VariantList=" ), variants ); + setLayoutKey( content, QStringLiteral( "Model=" ), model ); + setLayoutKey( content, QStringLiteral( "LayoutList=" ), layouts ); + setLayoutKey( content, QStringLiteral( "VariantList=" ), variants ); + if ( !options.isEmpty() ) + { + setLayoutKey( content, QStringLiteral( "Options=" ), options ); + } + setLayoutKey( content, QStringLiteral( "Use=" ), QStringLiteral( "true" ) ); config.write( content.join( '\n' ).toUtf8() ); config.close(); @@ -349,18 +410,20 @@ void applyKWin( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra ) { const auto paths = QStandardPaths::standardLocations( QStandardPaths::ConfigLocation ); + prepareGroupSwitcher( settings, extra ); - auto join = [ &additional = extra.additionalLayout ]( const QString& s1, const QString& s2 ) - { return additional.isEmpty() ? s1 : QStringLiteral( "%1,%2" ).arg( s1, s2 ); }; + auto join = [ &additional = extra.additionalLayout ]( const QString& additionalValue, const QString& selectedValue ) + { return additional.isEmpty() ? selectedValue : QStringLiteral( "%1,%2" ).arg( additionalValue, selectedValue ); }; - const QString layouts = join( settings.selectedLayout, extra.additionalLayout ); - const QString variants = join( settings.selectedVariant, extra.additionalVariant ); + const QString layouts = join( extra.additionalLayout, settings.selectedLayout ); + const QString variants = join( extra.additionalVariant, settings.selectedVariant ); + const QString options = extra.groupSwitcher; bool updated = false; for ( const auto& path : paths ) { const QString candidate = path + QStringLiteral( "/kxkbrc" ); - if ( rewriteKWin( candidate, settings.selectedModel, layouts, variants ) ) + if ( rewriteKWin( candidate, settings.selectedModel, layouts, variants, options ) ) { updated = true; break; @@ -415,19 +478,7 @@ applyGnome( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra ) // gsettings set org.gnome.desktop.input-sources xkb-options "['grp:lalt_lshift_toggle']" if ( !extra.additionalLayout.isEmpty() ) { - // Get a reasonable value for the group switcher, defaulting to alt_shift_toggle if nothing else is set - if ( !settings.selectedGroup.isEmpty() ) - { - extra.groupSwitcher = "grp:" + settings.selectedGroup; - } - if ( extra.groupSwitcher.isEmpty() ) - { - extra.groupSwitcher = xkbmap_query_grp_option(); - } - if ( extra.groupSwitcher.isEmpty() ) - { - extra.groupSwitcher = "grp:alt_shift_toggle"; - } + prepareGroupSwitcher( settings, extra ); const QString xkbOptionsValue = QStringLiteral( "['%1']" ).arg( extra.groupSwitcher ); const QStringList xkbOptionsCommand = QStringList( sudoArguments ) << "xkb-options" << xkbOptionsValue; @@ -448,13 +499,14 @@ void Config::apply() { m_additionalLayoutInfo = getAdditionalLayoutInfo( m_current.selectedLayout ); - if ( m_configureXkb ) + bool locale1Applied = false; + if ( m_configureLocale1 ) { - applyXkb( m_current, m_additionalLayoutInfo ); + locale1Applied = applyLocale1( m_current, m_additionalLayoutInfo ); } - if ( m_configureLocale1 ) + if ( m_configureXkb && !locale1Applied ) { - applyLocale1( m_current, m_additionalLayoutInfo ); + applyXkb( m_current, m_additionalLayoutInfo ); } if ( m_configureKWin ) { @@ -651,22 +703,23 @@ Config::detectCurrentKeyboardLayout() void Config::cancel() { - const auto extra = getAdditionalLayoutInfo( m_original.selectedLayout ); - if ( m_configureXkb ) + auto extra = getAdditionalLayoutInfo( m_original.selectedLayout ); + bool locale1Applied = false; + if ( m_configureLocale1 ) { - applyXkb( m_original, m_additionalLayoutInfo ); + locale1Applied = applyLocale1( m_original, extra ); } - if ( m_configureLocale1 ) + if ( m_configureXkb && !locale1Applied ) { - applyLocale1( m_original, m_additionalLayoutInfo ); + applyXkb( m_original, extra ); } if ( m_configureKWin ) { - applyKWin( m_original, m_additionalLayoutInfo ); + applyKWin( m_original, extra ); } if ( m_configureGnome ) { - applyGnome( m_original, m_additionalLayoutInfo ); + applyGnome( m_original, extra ); } } @@ -892,6 +945,7 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) m_convertedKeymapPath = getString( configurationMap, "convertedKeymapPath" ); m_configureEtcDefaultKeyboard = getBool( configurationMap, "writeEtcDefaultKeyboard", true ); m_configureLocale1 = getBool( configurationMap, "useLocale1", !isX11 ); + m_configureXkb = isX11; bool bogus = false; const auto configureItems = getSubMap( configurationMap, "configure", bogus ); diff --git a/src/modules/keyboard/SetKeyboardLayoutJob.cpp b/src/modules/keyboard/SetKeyboardLayoutJob.cpp index 81eba130b7..2942cd48a2 100644 --- a/src/modules/keyboard/SetKeyboardLayoutJob.cpp +++ b/src/modules/keyboard/SetKeyboardLayoutJob.cpp @@ -37,8 +37,25 @@ removeEmpty( QStringList&& list ) list.removeAll( QString() ); return list; } + } // namespace +STATICTEST QStringList +variantList( const AdditionalLayoutInfo& additionalLayoutInfo, const QString& variant ) +{ + if ( additionalLayoutInfo.additionalLayout.isEmpty() ) + { + return removeEmpty( { variant } ); + } + + QStringList variants { additionalLayoutInfo.additionalVariant, variant }; + if ( variants.join( QString() ).isEmpty() ) + { + variants.clear(); + } + return variants; +} + SetKeyboardLayoutJob::SetKeyboardLayoutJob( const QString& model, const QString& layout, const QString& variant, @@ -280,7 +297,7 @@ SetKeyboardLayoutJob::writeX11Data( const QString& keyboardConfPath ) const const QStringList layouts = removeEmpty( { m_additionalLayoutInfo.additionalLayout, m_layout } ); - const QStringList variants = removeEmpty( { m_additionalLayoutInfo.additionalVariant, m_variant } ); + const QStringList variants = variantList( m_additionalLayoutInfo, m_variant ); stream << " Option \"XkbLayout\" \"" << layouts.join( "," ) << "\"\n"; stream << " Option \"XkbVariant\" \"" << variants.join( "," ) << "\"\n"; if ( !m_additionalLayoutInfo.additionalLayout.isEmpty() ) @@ -314,7 +331,7 @@ SetKeyboardLayoutJob::writeDefaultKeyboardData( const QString& defaultKeyboardPa QTextStream stream( &file ); const QStringList layouts = removeEmpty( { m_additionalLayoutInfo.additionalLayout, m_layout } ); - const QStringList variants = removeEmpty( { m_additionalLayoutInfo.additionalVariant, m_variant } ); + const QStringList variants = variantList( m_additionalLayoutInfo, m_variant ); stream << "# KEYBOARD CONFIGURATION FILE\n\n" "# Consult the keyboard(5) manual page.\n\n"; diff --git a/src/modules/keyboard/Tests.cpp b/src/modules/keyboard/Tests.cpp index 2cdb589359..c9659d8927 100644 --- a/src/modules/keyboard/Tests.cpp +++ b/src/modules/keyboard/Tests.cpp @@ -6,12 +6,14 @@ * Calamares is Free Software: see the License-Identifier above. * */ +#include "AdditionalLayoutInfo.h" #include "utils/Logger.h" #include // Internals of SetKeyboardLayoutJob.cpp extern QString findLegacyKeymap( const QString& layout, const QString& model, const QString& variant ); +extern QStringList variantList( const AdditionalLayoutInfo& additionalLayoutInfo, const QString& variant ); class KeyboardLayoutTests : public QObject { @@ -25,6 +27,8 @@ private Q_SLOTS: void testSimpleLayoutLookup_data(); void testSimpleLayoutLookup(); + void testVariantList_data(); + void testVariantList(); }; void @@ -60,6 +64,45 @@ KeyboardLayoutTests::testSimpleLayoutLookup() QCOMPARE( findLegacyKeymap( layout, model, variant ), vconsole ); } +void +KeyboardLayoutTests::testVariantList_data() +{ + QTest::addColumn< QString >( "additionalLayout" ); + QTest::addColumn< QString >( "additionalVariant" ); + QTest::addColumn< QString >( "variant" ); + QTest::addColumn< QStringList >( "expected" ); + + QTest::newRow( "selected variant only" ) << QString() << QString() << QStringLiteral( "dvorak" ) + << QStringList { QStringLiteral( "dvorak" ) }; + QTest::newRow( "no layouts or variants" ) << QString() << QString() << QString() << QStringList {}; + QTest::newRow( "ignore additional variant without layout" ) << QString() << QStringLiteral( "intl" ) + << QStringLiteral( "dvorak" ) + << QStringList { QStringLiteral( "dvorak" ) }; + QTest::newRow( "additional layout with selected variant" ) << QStringLiteral( "us" ) << QString() + << QStringLiteral( "phonetic" ) + << QStringList { QString(), QStringLiteral( "phonetic" ) }; + QTest::newRow( "additional layout with additional variant" ) << QStringLiteral( "us" ) << QStringLiteral( "intl" ) + << QString() + << QStringList { QStringLiteral( "intl" ), QString() }; + QTest::newRow( "additional layout with no variants" ) << QStringLiteral( "us" ) << QString() << QString() + << QStringList {}; +} + +void +KeyboardLayoutTests::testVariantList() +{ + QFETCH( QString, additionalLayout ); + QFETCH( QString, additionalVariant ); + QFETCH( QString, variant ); + QFETCH( QStringList, expected ); + + AdditionalLayoutInfo additionalLayoutInfo; + additionalLayoutInfo.additionalLayout = additionalLayout; + additionalLayoutInfo.additionalVariant = additionalVariant; + + QCOMPARE( variantList( additionalLayoutInfo, variant ), expected ); +} + QTEST_GUILESS_MAIN( KeyboardLayoutTests ) diff --git a/src/modules/keyboard/keyboard.conf b/src/modules/keyboard/keyboard.conf index 417945ba66..65c96cbb1a 100644 --- a/src/modules/keyboard/keyboard.conf +++ b/src/modules/keyboard/keyboard.conf @@ -26,7 +26,7 @@ convertedKeymapPath: "/lib/kbd/keymaps/xkb" # This is the modern mechanism for configuring the systemwide keyboard layout, # and works on Wayland compositors to set the current layout. # Defaults to false on X11 and true otherwise. -#useLocale1: true +useLocale1: true # Guess the default layout from the user locale. If false, keeps the current # OS keyboard layout as the default (useful if the layout is pre-configured). @@ -42,6 +42,6 @@ configure: # Systems that use KDE Plasma Wayland and locale1 can instead start the # compositor KWin with command-line argument `--locale1`. That # argument makes this configuration option unnecessary. - kwin: true + kwin: false # Configure keyboard when using Wayland with Gnome on Ubuntu 24.10+ gnome: false diff --git a/src/modules/keyboard/keyboard.schema.yaml b/src/modules/keyboard/keyboard.schema.yaml index 0fb1fdcf36..3a826ff1ba 100644 --- a/src/modules/keyboard/keyboard.schema.yaml +++ b/src/modules/keyboard/keyboard.schema.yaml @@ -16,4 +16,5 @@ properties: type: object properties: kwin: { type: boolean, default: false } + gnome: { type: boolean, default: false } required: [ xOrgConfFileName, convertedKeymapPath ]