From 1b80ef3ab78dbb8b4c8d19fd69084a4a1c9a59b0 Mon Sep 17 00:00:00 2001 From: Ivan Seredkin Date: Fri, 19 Jul 2024 13:18:53 -0400 Subject: [PATCH] #4 Add support for keyboard layout (ubuntu and mac is not tested) --- .../com/github/siropkin/kursor/Kursor.kt | 174 ++++++++++-------- .../kursor/settings/KursorSettings.kt | 6 +- .../settings/KursorSettingsComponent.kt | 34 ++-- .../settings/KursorSettingsConfigurable.kt | 3 + 4 files changed, 123 insertions(+), 94 deletions(-) diff --git a/src/main/kotlin/com/github/siropkin/kursor/Kursor.kt b/src/main/kotlin/com/github/siropkin/kursor/Kursor.kt index 9271ba0..7fad2b9 100644 --- a/src/main/kotlin/com/github/siropkin/kursor/Kursor.kt +++ b/src/main/kotlin/com/github/siropkin/kursor/Kursor.kt @@ -11,21 +11,26 @@ import java.awt.event.ComponentListener import java.awt.event.KeyEvent import java.awt.im.InputContext import java.io.* -import java.util.* import javax.swing.JComponent +const val unknownCountry = "unk" -object Position { +object IndicatorPosition { const val TOP = "top" const val MIDDLE = "middle" const val BOTTOM = "bottom" } +interface KeyboardLocale { + val language: String + val country: String + fun getIndicatorText(useLayout: Boolean): String = if (useLayout) country.lowercase() else language.lowercase() +} class Kursor(private var editor: Editor): JComponent(), ComponentListener, CaretListener { private val os = System.getProperty("os.name").lowercase() private var linuxDistribution = System.getenv("DESKTOP_SESSION")?.lowercase() ?: "" - private var linuxKeyboardLayouts: List = listOf() + private var linuxNonUbuntuKeyboardCountries: List = emptyList() init { editor.contentComponent.add(this) @@ -35,24 +40,18 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret editor.component.addComponentListener(this) } - override fun componentResized(e: ComponentEvent?) { - bounds = getEditorBounds() - repaint() - } + override fun componentShown(e: ComponentEvent?) {} - override fun componentMoved(e: ComponentEvent?) { - bounds = getEditorBounds() - repaint() - } + override fun componentHidden(e: ComponentEvent?) {} - override fun componentShown(e: ComponentEvent?) { - } + override fun componentResized(e: ComponentEvent?) = repaintComponent() - override fun componentHidden(e: ComponentEvent?) { - } + override fun componentMoved(e: ComponentEvent?) = repaintComponent() + + override fun caretPositionChanged(e: CaretEvent) = repaintComponent() - override fun caretPositionChanged(e: CaretEvent) { - bounds = getEditorBounds() + private fun repaintComponent() { + bounds = editor.scrollingModel.visibleArea repaint() } @@ -60,7 +59,7 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret return KursorSettings.getInstance() } - private fun executeNativeCommand(command: String): String { + private fun executeNativeCommand(command: Array): String { return try { val process = Runtime.getRuntime().exec(command) process.waitFor() @@ -71,69 +70,83 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret } } - private fun getLinuxUbuntuKeyboardLayout(): String { - val commandOutput = executeNativeCommand("gsettings get org.gnome.desktop.input-sources mru-sources") - return commandOutput - .substringAfter("('xkb', '") - .substringBefore("')") - .substring(0, 2) - } + private fun getLinuxKeyboardLocale(): KeyboardLocale { + if (linuxDistribution == "ubuntu") { + val country = executeNativeCommand(arrayOf("gsettings", "get", "org.gnome.desktop.input-sources", "mru-sources")) + .substringAfter("('xkb', '") + .substringBefore("')") + .substring(0, 2) + return object : KeyboardLocale { + override val language: String = country + override val country: String = country + } + } - private fun getLinuxNonUbuntuKeyboardLayouts(): List { - if (linuxKeyboardLayouts.isNotEmpty()) { - return linuxKeyboardLayouts + // For non Ubuntu OS we know only keyboard layout and do not know keyboard language + if (linuxNonUbuntuKeyboardCountries.isEmpty()) { + linuxNonUbuntuKeyboardCountries = executeNativeCommand(arrayOf("setxkbmap", "-query")) + .substringAfter("layout:") + .substringBefore("\n") + .trim() + .split(",") } - linuxKeyboardLayouts = executeNativeCommand("setxkbmap -query") - .substringAfter("layout:") - .substringBefore("\n") - .trim() - .split(",") - return linuxKeyboardLayouts - } - private fun getLinuxNonUbuntuKeyboardLayoutIndex(): Int { - return executeNativeCommand("xset -q") + // This is a bad solution because it returns 0 if it's a default layout and 1 in other cases, + // and if user has more than two layouts, we do not know which one is really on + val linuxCurrentKeyboardCountryIndex = executeNativeCommand(arrayOf("xset", "-q")) .substringAfter("LED mask:") .substringBefore("\n") .trim() .substring(4, 5) .toInt(16) + val country = if (linuxNonUbuntuKeyboardCountries.size > 2 && linuxCurrentKeyboardCountryIndex > 0) unknownCountry else linuxNonUbuntuKeyboardCountries[linuxCurrentKeyboardCountryIndex] + return object : KeyboardLocale { + override val language: String = country + override val country: String = country + } } - private fun getLinuxNonUbuntuKeyboardLayout(): String { - val linuxKeyboardLayouts = getLinuxNonUbuntuKeyboardLayouts() - val linuxCurrentKeyboardLayoutIndex = getLinuxNonUbuntuKeyboardLayoutIndex() - return linuxKeyboardLayouts[linuxCurrentKeyboardLayoutIndex] - } - - private fun getOtherOsKeyboardLayout(): String { + private fun getMacKeyboardLocale(): KeyboardLocale { // if locale in format _US_UserDefined_252 then we need to take the first two letters without _ symbol // otherwise we are expecting the locale in format en_US and taking the first two letters val locale = InputContext.getInstance().locale.toString() - if (locale.startsWith("_")) { - return locale.substring(1, 3) + val language = if (locale.startsWith("_")) { + locale.substring(1, 3) + } else { + locale.substring(0, 2) + } + val layout = if (locale.startsWith("_")) { + "" + } else { + locale.substring(3, 5) + } + return object : KeyboardLocale { + override val language: String = language + override val country: String = layout + } + } + + private fun getWindowsKeyboardLocale(): KeyboardLocale { + val locale = InputContext.getInstance().locale + return object : KeyboardLocale { + override val language: String = locale.language + override val country: String = locale.country } - return locale.substring(0, 2) } - private fun getKeyboardLayout(): String { + private fun getKeyboardLocale(): KeyboardLocale { // This is not the ideal solution because it involves executing a shell command to know the current keyboard layout // which might affect the performance. And we have different commands for different Linux distributions. // But it is the only solution I found that works on Linux. - var language = when (os) { - "linux" -> when (linuxDistribution) { - "ubuntu" -> getLinuxUbuntuKeyboardLayout() - else -> getLinuxNonUbuntuKeyboardLayout() + return if (os == "linux") { + getLinuxKeyboardLocale() + } else { + if (os.startsWith("win")) { + getWindowsKeyboardLocale() + } else { + getMacKeyboardLocale() } - else -> getOtherOsKeyboardLayout() - }.lowercase() - if (language == "us") { - language = "en" - } - if (language.isEmpty()) { - language = getSettings().defaultLanguage } - return language } private fun isCapsLockOn(): Boolean { @@ -154,10 +167,6 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret return editor.contentComponent.isFocusOwner } - private fun getEditorBounds(): Rectangle { - return editor.scrollingModel.visibleArea - } - private fun getPrimaryCaret(): Caret { return editor.caretModel.primaryCaret } @@ -206,25 +215,38 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret } val settings = getSettings() - val keyboardLayout = getKeyboardLayout() - val isCapsLockOn = isCapsLockOn() + + val keyboardLocale = getKeyboardLocale() + val isCapsLock = settings.indicateCapsLock && isCapsLockOn() + var indicatorText = keyboardLocale.getIndicatorText(settings.useKeyboardLayout) + if (indicatorText.isEmpty()) { + indicatorText = settings.defaultLanguage + } val caret = getPrimaryCaret() - val caretColor = if (settings.changeColorOnNonDefaultLanguage && keyboardLayout != settings.defaultLanguage) { - settings.colorOnNonDefaultLanguage - } else { - null + var caretColor: Color? = null + if (settings.changeColorOnNonDefaultLanguage) { + if (indicatorText != settings.defaultLanguage) { + caretColor = settings.colorOnNonDefaultLanguage + } } + if (caret.visualAttributes.color != caretColor) { setCaretColor(caret, caretColor) } - val isIndicatorVisible = settings.showIndicator && (settings.indicateDefaultLanguage || keyboardLayout != settings.defaultLanguage || settings.indicateCapsLock && isCapsLockOn) - if (!isIndicatorVisible) { + if (!settings.showIndicator) { return } - val indicatorText = if (settings.indicateCapsLock && isCapsLockOn) keyboardLayout.uppercase(Locale.getDefault()) else keyboardLayout + val showIndicator = settings.indicateDefaultLanguage || isCapsLock || indicatorText.lowercase() != settings.defaultLanguage.lowercase() + if (!showIndicator) { + return + } + + if (isCapsLock) { + indicatorText = indicatorText.uppercase() + } val caretWidth = getCaretWidth(caret) val caretHeight = getCaretHeight(caret) @@ -232,9 +254,9 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret val indicatorOffsetX = caretWidth + settings.indicatorHorizontalOffset val indicatorOffsetY = when (settings.indicatorVerticalPosition) { - Position.TOP -> (if (caret.visualPosition.line == 0) settings.indicatorFontSize else settings.indicatorFontSize / 2) - 1 - Position.MIDDLE -> caretHeight / 2 + settings.indicatorFontSize / 2 - 1 - Position.BOTTOM -> caretHeight + 3 + IndicatorPosition.TOP -> (if (caret.visualPosition.line == 0) settings.indicatorFontSize else settings.indicatorFontSize / 2) - 1 + IndicatorPosition.MIDDLE -> caretHeight / 2 + settings.indicatorFontSize / 2 - 1 + IndicatorPosition.BOTTOM -> caretHeight + 3 else -> 0 } diff --git a/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettings.kt b/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettings.kt index 8754944..c6f7026 100644 --- a/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettings.kt +++ b/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettings.kt @@ -1,6 +1,6 @@ package com.github.siropkin.kursor.settings -import com.github.siropkin.kursor.Position +import com.github.siropkin.kursor.IndicatorPosition import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State @@ -22,15 +22,17 @@ class KursorSettings : PersistentStateComponent { var colorOnNonDefaultLanguage: Color = Color(255, 140, 0) var showIndicator: Boolean = true + var indicateCapsLock: Boolean = true var indicateDefaultLanguage: Boolean = false + var useKeyboardLayout: Boolean = false var indicatorFontName: String = EditorColorsManager.getInstance().globalScheme.editorFontName var indicatorFontStyle: Int = Font.PLAIN var indicatorFontSize: Int = 11 var indicatorFontAlpha: Int = 180 - var indicatorVerticalPosition: String = Position.TOP + var indicatorVerticalPosition: String = IndicatorPosition.TOP var indicatorHorizontalOffset: Int = 4 override fun getState(): KursorSettings { diff --git a/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettingsComponent.kt b/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettingsComponent.kt index 16ba99f..27b852f 100644 --- a/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettingsComponent.kt +++ b/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettingsComponent.kt @@ -1,41 +1,35 @@ package com.github.siropkin.kursor.settings -import com.github.siropkin.kursor.Position +import com.github.siropkin.kursor.IndicatorPosition import com.intellij.ui.ColorPanel import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBTextField import com.intellij.util.ui.FormBuilder +import com.intellij.openapi.ui.ComboBox import java.awt.* -import java.util.* import javax.swing.* - private const val LABEL_SPACING = 10 private const val COMPONENT_SPACING = 35 class KursorSettingsComponent { - private val availableLanguages = Locale.getAvailableLocales() - .map { it.language } - .distinct() - .filter { it.isNotEmpty() } - .sorted() - .toTypedArray() - private val defaultLanguageComponent = JComboBox(availableLanguages) + private val defaultLanguageComponent = JBTextField("", 10) private val changeColorOnNonDefaultLanguageComponent = JBCheckBox("Change color on non-default language") private val colorOnNonDefaultLanguageComponent = ColorPanel() private val showIndicatorComponent = JBCheckBox("Show text indicator") - private val indicateCapsLockComponent = JBCheckBox("Indicate Caps Lock") + private val indicateCapsLockComponent = JBCheckBox("Indicate 'Caps Lock'") private val indicateDefaultLanguageComponent = JBCheckBox("Show default language") + private val useKeyboardLayoutComponent = JBCheckBox("Use keyboard layout") - private val indicatorFontNameComponent = JComboBox(GraphicsEnvironment.getLocalGraphicsEnvironment().availableFontFamilyNames) - private val indicatorFontStyleComponent = JComboBox(arrayOf(Font.PLAIN.toString(), Font.BOLD.toString(), Font.ITALIC.toString())) + private val indicatorFontNameComponent = ComboBox(GraphicsEnvironment.getLocalGraphicsEnvironment().availableFontFamilyNames) + private val indicatorFontStyleComponent = ComboBox(arrayOf(Font.PLAIN.toString(), Font.BOLD.toString(), Font.ITALIC.toString())) private val indicatorFontSizeComponent = JBTextField() private val indicatorFontAlphaComponent = JBTextField() - private val indicatorVerticalPositionComponent = JComboBox(arrayOf(Position.TOP, Position.MIDDLE, Position.BOTTOM)) + private val indicatorVerticalPositionComponent = ComboBox(arrayOf(IndicatorPosition.TOP, IndicatorPosition.MIDDLE, IndicatorPosition.BOTTOM)) private val indicatorHorizontalOffsetComponent = JBTextField() var panel: JPanel = FormBuilder.createFormBuilder() @@ -50,9 +44,9 @@ class KursorSettingsComponent { get() = defaultLanguageComponent var defaultLanguage: String - get() = defaultLanguageComponent.selectedItem as String + get() = defaultLanguageComponent.text set(value) { - defaultLanguageComponent.selectedItem = value + defaultLanguageComponent.text = value } var changeColorOnNonDefaultLanguage: Boolean @@ -87,6 +81,12 @@ class KursorSettingsComponent { indicateDefaultLanguageComponent.isSelected = value } + var useKeyboardLayout: Boolean + get() = useKeyboardLayoutComponent.isSelected + set(value) { + useKeyboardLayoutComponent.isSelected = value + } + var indicatorFontName: String get() = indicatorFontNameComponent.selectedItem as String set(value) { @@ -156,6 +156,7 @@ class KursorSettingsComponent { checkBoxPanel.layout = GridBagLayout() checkBoxPanel.add(showIndicatorComponent, createRbc(0, 0, 0.0)) checkBoxPanel.add(indicateDefaultLanguageComponent, createRbc(1, 0, 0.0, COMPONENT_SPACING)) + checkBoxPanel.add(useKeyboardLayoutComponent, createRbc(2, 0, 0.0, COMPONENT_SPACING)) checkBoxPanel.add(indicateCapsLockComponent, createRbc(2, 0, 1.0, COMPONENT_SPACING)) val fontPanel = JPanel() @@ -169,6 +170,7 @@ class KursorSettingsComponent { showIndicatorComponent.addChangeListener { indicateDefaultLanguageComponent.isEnabled = showIndicator + useKeyboardLayoutComponent.isEnabled = showIndicator indicateCapsLockComponent.isEnabled = showIndicator indicatorFontNameComponent.isEnabled = showIndicator indicatorFontStyleComponent.isEnabled = showIndicator diff --git a/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettingsConfigurable.kt b/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettingsConfigurable.kt index 546a37a..defa909 100644 --- a/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/siropkin/kursor/settings/KursorSettingsConfigurable.kt @@ -29,6 +29,7 @@ class KursorSettingsConfigurable: Configurable { || it.showIndicator != settings.showIndicator || it.indicateCapsLock != settings.indicateCapsLock || it.indicateDefaultLanguage != settings.indicateDefaultLanguage + || it.useKeyboardLayout != settings.useKeyboardLayout || it.indicatorFontName != settings.indicatorFontName || it.indicatorFontStyle != settings.indicatorFontStyle || it.indicatorFontSize != settings.indicatorFontSize @@ -47,6 +48,7 @@ class KursorSettingsConfigurable: Configurable { settings.showIndicator = settingsComponent!!.showIndicator settings.indicateCapsLock = settingsComponent!!.indicateCapsLock settings.indicateDefaultLanguage = settingsComponent!!.indicateDefaultLanguage + settings.useKeyboardLayout = settingsComponent!!.useKeyboardLayout settings.indicatorFontName = settingsComponent!!.indicatorFontName settings.indicatorFontStyle = settingsComponent!!.indicatorFontStyle settings.indicatorFontSize = settingsComponent!!.indicatorFontSize @@ -64,6 +66,7 @@ class KursorSettingsConfigurable: Configurable { it.showIndicator = settings.showIndicator it.indicateCapsLock = settings.indicateCapsLock it.indicateDefaultLanguage = settings.indicateDefaultLanguage + it.useKeyboardLayout = settings.useKeyboardLayout it.indicatorFontName = settings.indicatorFontName it.indicatorFontStyle = settings.indicatorFontStyle it.indicatorFontSize = settings.indicatorFontSize