Skip to content

Commit

Permalink
#4 Add support for keyboard layout (ubuntu and mac is not tested)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivan Seredkin committed Jul 19, 2024
1 parent edee2e6 commit 1b80ef3
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 94 deletions.
174 changes: 98 additions & 76 deletions src/main/kotlin/com/github/siropkin/kursor/Kursor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = listOf()
private var linuxNonUbuntuKeyboardCountries: List<String> = emptyList()

init {
editor.contentComponent.add(this)
Expand All @@ -35,32 +40,26 @@ 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()
}

private fun getSettings(): KursorSettings {
return KursorSettings.getInstance()
}

private fun executeNativeCommand(command: String): String {
private fun executeNativeCommand(command: Array<String>): String {
return try {
val process = Runtime.getRuntime().exec(command)
process.waitFor()
Expand All @@ -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<String> {
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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -206,35 +215,48 @@ 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)
val caretPosition = getCaretPosition(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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,15 +22,17 @@ class KursorSettings : PersistentStateComponent<KursorSettings> {
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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand All @@ -169,6 +170,7 @@ class KursorSettingsComponent {

showIndicatorComponent.addChangeListener {
indicateDefaultLanguageComponent.isEnabled = showIndicator
useKeyboardLayoutComponent.isEnabled = showIndicator
indicateCapsLockComponent.isEnabled = showIndicator
indicatorFontNameComponent.isEnabled = showIndicator
indicatorFontStyleComponent.isEnabled = showIndicator
Expand Down
Loading

0 comments on commit 1b80ef3

Please sign in to comment.