Skip to content

Commit

Permalink
#19 #34 Code refactoring (preparation for ascii mode detection)
Browse files Browse the repository at this point in the history
  • Loading branch information
siropkin committed Nov 18, 2024
1 parent f7507d6 commit 426002b
Show file tree
Hide file tree
Showing 14 changed files with 158 additions and 116 deletions.
39 changes: 18 additions & 21 deletions src/main/kotlin/com/github/siropkin/kursor/Kursor.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.github.siropkin.kursor

import com.github.siropkin.kursor.keyboardlayout.KeyboardLayout
import com.github.siropkin.kursor.settings.KursorSettings
import com.github.siropkin.kursor.keyboard.Keyboard
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.CaretVisualAttributes
import com.intellij.openapi.editor.Editor
Expand All @@ -16,14 +15,8 @@ import java.awt.event.KeyEvent
import javax.swing.JComponent


object IndicatorPosition {
const val TOP = "top"
const val MIDDLE = "middle"
const val BOTTOM = "bottom"
}

class Kursor(private var editor: Editor): JComponent(), ComponentListener, CaretListener {
private val keyboardLayout = KeyboardLayout()
private val keyboard = Keyboard()

init {
editor.contentComponent.add(this)
Expand Down Expand Up @@ -117,14 +110,14 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret
return
}

val keyboardLayoutString = keyboardLayout.getLayoutInfo().toString()
if (keyboardLayoutString.isEmpty()) {
val keyboardLayout = keyboard.getLayout()
if (keyboardLayout.isEmpty()) {
return
}

val settings = getSettings()
val caret = getPrimaryCaret()
val caretColor = if (settings.changeColorOnNonDefaultLanguage && keyboardLayoutString.lowercase() != settings.defaultLanguage.lowercase()) {
val caretColor = if (settings.changeColorOnNonDefaultLanguage && keyboardLayout.toString().lowercase() != settings.defaultLanguage.lowercase()) {
settings.colorOnNonDefaultLanguage
} else {
null
Expand All @@ -134,31 +127,35 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret
setCaretColor(caret, caretColor)
}

if (!settings.showTextIndicator) {
return
}

val isCapsLockOn = settings.indicateCapsLock && getIsCapsLockOn()
val showTextIndicator = settings.showTextIndicator && (settings.indicateDefaultLanguage || isCapsLockOn || keyboardLayoutString.lowercase() != settings.defaultLanguage.lowercase())
if (!showTextIndicator) {
val isDefaultLanguage = keyboardLayout.toString().lowercase() == settings.defaultLanguage.lowercase()
if (!isCapsLockOn && isDefaultLanguage && !settings.indicateDefaultLanguage) {
return
}

val displayText = if (isCapsLockOn) {
keyboardLayoutString.uppercase()
val textIndicatorString = if (isCapsLockOn) {
keyboardLayout.toString().uppercase()
} else {
keyboardLayoutString.lowercase()
keyboardLayout.toString().lowercase()
}
val caretWidth = getCaretWidth(caret)
val caretHeight = getCaretHeight(caret)
val caretPosition = getCaretPosition(caret)

val indicatorOffsetX = caretWidth + settings.textIndicatorHorizontalOffset
val indicatorOffsetY = when (settings.textIndicatorVerticalPosition) {
IndicatorPosition.TOP -> (if (caret.visualPosition.line == 0) settings.textIndicatorFontSize else settings.textIndicatorFontSize / 2) - 1
IndicatorPosition.MIDDLE -> caretHeight / 2 + settings.textIndicatorFontSize / 2 - 1
IndicatorPosition.BOTTOM -> caretHeight + 3
TextIndicatorVerticalPositions.TOP -> (if (caret.visualPosition.line == 0) settings.textIndicatorFontSize else settings.textIndicatorFontSize / 2) - 1
TextIndicatorVerticalPositions.MIDDLE -> caretHeight / 2 + settings.textIndicatorFontSize / 2 - 1
TextIndicatorVerticalPositions.BOTTOM -> caretHeight + 3
else -> 0
}

g.font = Font(settings.textIndicatorFontName, settings.textIndicatorFontStyle, settings.textIndicatorFontSize)
g.color = getColorWithAlpha(caretColor ?: getDefaultCaretColor()!!, settings.textIndicatorFontAlpha)
g.drawString(displayText, caretPosition.x + indicatorOffsetX, caretPosition.y + indicatorOffsetY)
g.drawString(textIndicatorString, caretPosition.x + indicatorOffsetX, caretPosition.y + indicatorOffsetY)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.siropkin.kursor.settings
package com.github.siropkin.kursor

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 Down Expand Up @@ -31,7 +30,7 @@ class KursorSettings : PersistentStateComponent<KursorSettings> {
var textIndicatorFontSize: Int = 11
var textIndicatorFontAlpha: Int = 180

var textIndicatorVerticalPosition: String = IndicatorPosition.TOP
var textIndicatorVerticalPosition: String = TextIndicatorVerticalPositions.TOP
var textIndicatorHorizontalOffset: Int = 4

var indicateCapsLock: Boolean = true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.github.siropkin.kursor.settings
package com.github.siropkin.kursor

import com.github.siropkin.kursor.IndicatorPosition
import com.github.siropkin.kursor.keyboardlayout.KeyboardLayout
import com.github.siropkin.kursor.keyboard.Keyboard
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.ColorPanel
import com.intellij.ui.components.JBCheckBox
Expand All @@ -20,7 +19,7 @@ private const val LABEL_SPACING = 10
private const val COMPONENT_SPACING = 35

class KursorSettingsComponent {
private val keyboardLayout = KeyboardLayout()
private val keyboardLayout = Keyboard()

private val defaultLanguageComponent = JBTextField("", 5)
private val detectKeyboardLayoutButton = JButton("Detect Keyboard Layout")
Expand All @@ -37,7 +36,7 @@ class KursorSettingsComponent {
private val textIndicatorFontSizeComponent = JBTextField()
private val textIndicatorFontAlphaComponent = JBTextField()

private val textIndicatorVerticalPositionComponent = ComboBox(arrayOf(IndicatorPosition.TOP, IndicatorPosition.MIDDLE, IndicatorPosition.BOTTOM))
private val textIndicatorVerticalPositionComponent = ComboBox(arrayOf(TextIndicatorVerticalPositions.TOP, TextIndicatorVerticalPositions.MIDDLE, TextIndicatorVerticalPositions.BOTTOM))
private val textIndicatorHorizontalOffsetComponent = JBTextField()

var panel: JPanel = FormBuilder.createFormBuilder()
Expand Down Expand Up @@ -148,7 +147,7 @@ class KursorSettingsComponent {
languagePanel.add(detectKeyboardLayoutButton, createRbc(2, 0, 1.0, COMPONENT_SPACING))

detectKeyboardLayoutButton.addActionListener {
defaultLanguageComponent.text = keyboardLayout.getLayoutInfo().toString().lowercase()
defaultLanguageComponent.text = keyboardLayout.getLayout().toString().lowercase()
}

return languagePanel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.siropkin.kursor.settings
package com.github.siropkin.kursor

import com.intellij.openapi.options.Configurable
import javax.swing.JComponent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.siropkin.kursor

object TextIndicatorVerticalPositions {
const val TOP = "top"
const val MIDDLE = "middle"
const val BOTTOM = "bottom"
}
Original file line number Diff line number Diff line change
@@ -1,70 +1,72 @@
package com.github.siropkin.kursor.keyboardlayout
package com.github.siropkin.kursor.keyboard

import com.sun.jna.Platform
import com.sun.jna.platform.win32.User32
import com.sun.jna.platform.win32.WinDef
import com.sun.jna.platform.win32.WinDef.HKL
import java.awt.im.InputContext
import java.io.BufferedReader
import java.io.IOException


class KeyboardLayout {
private val unknown = "UNK"
private var linuxDistribution: String = System.getenv("DESKTOP_SESSION")?.lowercase() ?: ""
private var linuxDesktopGroup: String = System.getenv("XDG_SESSION_TYPE")?.lowercase() ?: ""
private var linuxKeyboardLayoutsCache: List<String> = emptyList()
private const val UNKNOWN = "UNK"

fun getLayoutInfo(): KeyboardLayoutInfo {
class Keyboard {
private var linuxConfig: LinuxConfig? = null

fun getLayout(): KeyboardLayout {
if (Platform.isLinux() && linuxConfig == null) {
linuxConfig = LinuxConfig()
}
return when {
Platform.isLinux() -> getLinuxLayoutInfo()
Platform.isMac() -> getMacLayoutInfo()
Platform.isWindows() -> getWindowsLayoutInfo()
else -> getUnknownLayoutInfo()
Platform.isLinux() -> getLinuxLayout()
Platform.isMac() -> getMacLayout()
Platform.isWindows() -> getWindowsLayout()
else -> getUnknownLayout()
}
}

private fun getUnknownLayoutInfo(): KeyboardLayoutInfo {
return KeyboardLayoutInfo(unknown, unknown, unknown)
private fun getUnknownLayout(): KeyboardLayout {
return KeyboardLayout(UNKNOWN, UNKNOWN, UNKNOWN)
}

private fun getLinuxLayoutInfo(): KeyboardLayoutInfo {
private fun getLinuxLayout(): KeyboardLayout {
// InputContext.getInstance().locale is not working on Linux: it always returns "en_US"
// 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.
// For Linux we know only keyboard layout and do not know keyboard language
val config = linuxConfig ?: return getUnknownLayout()
return when {
linuxDistribution == "ubuntu" -> getUbuntuLayoutInfo()
linuxDesktopGroup == "wayland" -> getWaylandLayoutInfo()
else -> getOtherLinuxLayoutInfo()
config.distribution == "ubuntu" -> getUbuntuLayout()
config.desktopGroup == "wayland" -> getWaylandLayout()
else -> getOtherLinuxLayout()
}
}

private fun getUbuntuLayoutInfo(): KeyboardLayoutInfo {
private fun getUbuntuLayout(): KeyboardLayout {
// Output example: [('xkb', 'us'), ('xkb', 'ru'), ('xkb', 'ca+eng')]
val split = executeNativeCommand(arrayOf("gsettings", "get", "org.gnome.desktop.input-sources", "mru-sources"))
val split = Utils.executeNativeCommand(arrayOf("gsettings", "get", "org.gnome.desktop.input-sources", "mru-sources"))
.substringAfter("('xkb', '")
.substringBefore("')")
.split("+")
val language = if (split.size > 1) split[1] else ""
val country = split[0]
return KeyboardLayoutInfo(language, country, "")
return KeyboardLayout(language, country, "")
}

private fun getWaylandLayoutInfo(): KeyboardLayoutInfo {
private fun getWaylandLayout(): KeyboardLayout {
// FIXME: Other Linux distribution commands not working "Wayland",
// see: https://github.com/siropkin/kursor/issues/3
return getUnknownLayoutInfo()
return getUnknownLayout()
}

private fun getOtherLinuxLayoutInfo(): KeyboardLayoutInfo {
if (linuxKeyboardLayoutsCache.isEmpty()) {
private fun getOtherLinuxLayout(): KeyboardLayout {
val config = linuxConfig ?: return getUnknownLayout()
if (config.availableKeyboardLayouts.isEmpty()) {
// Output example: rules: evdev
//model: pc105
//layout: us
//options: grp:win_space_toggle,terminate:ctrl_alt_bksp
linuxKeyboardLayoutsCache = executeNativeCommand(arrayOf("setxkbmap", "-query"))
config.availableKeyboardLayouts = Utils.executeNativeCommand(arrayOf("setxkbmap", "-query"))
.substringAfter("layout:")
.substringBefore("\n")
.trim()
Expand Down Expand Up @@ -98,46 +100,45 @@ class KeyboardLayout {
// Standby: 0 Suspend: 0 Off: 0
// DPMS is Enabled
// Monitor is On
val linuxCurrentKeyboardLayoutIndex = executeNativeCommand(arrayOf("xset", "-q"))
val linuxCurrentKeyboardLayoutIndex = Utils.executeNativeCommand(arrayOf("xset", "-q"))
.substringAfter("LED mask:")
.substringBefore("\n")
.trim()
.substring(4, 5)
.toInt(16)

// Additional check to avoid out-of-bounds exception
if (linuxCurrentKeyboardLayoutIndex >= linuxKeyboardLayoutsCache.size) {
return getUnknownLayoutInfo()
if (linuxCurrentKeyboardLayoutIndex >= config.availableKeyboardLayouts.size) {
return getUnknownLayout()
}

// 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
if (linuxKeyboardLayoutsCache.size > 2 && linuxCurrentKeyboardLayoutIndex > 0) {
return getUnknownLayoutInfo()
if (config.availableKeyboardLayouts.size > 2 && linuxCurrentKeyboardLayoutIndex > 0) {
return getUnknownLayout()
}

val country = linuxKeyboardLayoutsCache[linuxCurrentKeyboardLayoutIndex]
return KeyboardLayoutInfo("", country, "")
val country = config.availableKeyboardLayouts[linuxCurrentKeyboardLayoutIndex]
return KeyboardLayout("", country, "")
}

private fun getMacLayoutInfo(): KeyboardLayoutInfo {
private fun getMacLayout(): KeyboardLayout {
val locale = InputContext.getInstance().locale
// Variant example for US: UserDefined_252
val variant = MacKeyboardVariants[locale.variant] ?: ""
return KeyboardLayoutInfo(locale.language, locale.country, variant)
val localeVariant = locale.variant.removePrefix("UserDefined_")
val variant = MacStandardKeyboardVariants[localeVariant]
?: MacSogouPinyinVariants[localeVariant]
?: MacRimeSquirrelVariants[localeVariant]
?: ""
return KeyboardLayout(locale.language, locale.country, variant)
}

private fun getWindowsLayoutInfo(): KeyboardLayoutInfo {
private fun getWindowsLayout(): KeyboardLayout {
val locale = InputContext.getInstance().locale
// Standard locale object does not return correct info in case user set different keyboard inputs for one language
// see: https://github.com/siropkin/kursor/issues/4
val user32 = User32.INSTANCE
val fgWindow: WinDef.HWND? = user32.GetForegroundWindow() // Get the handle of the foreground window

if (fgWindow == null) {
return KeyboardLayoutInfo(locale.language, locale.country, "")
}

val fgWindow: WinDef.HWND = user32.GetForegroundWindow()
?: return KeyboardLayout(locale.language, locale.country, "") // Get the handle of the foreground window
val threadId = user32.GetWindowThreadProcessId(fgWindow, null) // Get the thread ID of the foreground window
val hkl: HKL = user32.GetKeyboardLayout(threadId) // Get the keyboard layout for the thread
// FIXME: It should be a better way how to convert pointer to string
Expand All @@ -151,18 +152,7 @@ class KeyboardLayout {
else -> layoutId.substring(2).padStart(8, '0')
}
val variant = WindowsKeyboardVariants[layoutId.uppercase()] ?: ""
return KeyboardLayoutInfo(locale.language, locale.country, variant)
}

private fun executeNativeCommand(command: Array<String>): String {
return try {
val process = Runtime.getRuntime().exec(command)
process.waitFor()
process.inputStream.bufferedReader().use(BufferedReader::readText)
} catch (e: IOException) {
e.printStackTrace()
""
}
return KeyboardLayout(locale.language, locale.country, variant)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.github.siropkin.kursor.keyboard


data class KeyboardLayout(val language: String, val country: String, val variant: String, val asciiMode: Boolean = false) {
fun isEmpty(): Boolean = language.isEmpty() && country.isEmpty() && variant.isEmpty()

override fun toString(): String = listOf(variant, country, language).firstOrNull { it.isNotEmpty() } ?: ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.siropkin.kursor.keyboard

class LinuxConfig {
var distribution: String = System.getenv("DESKTOP_SESSION")?.lowercase() ?: ""
var desktopGroup: String = System.getenv("XDG_SESSION_TYPE")?.lowercase() ?: ""
var availableKeyboardLayouts: List<String> = emptyList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.github.siropkin.kursor.keyboard


// Standard Layouts
// https://github.com/acidanthera/OpenCorePkg/blob/master/Utilities/AppleKeyboardLayouts/AppleKeyboardLayouts.txt
val MacStandardKeyboardVariants = mapOf(
"19458" to "RU", // Russian - PC
"-23205" to "UK", // Ukrainian-QWERTY
"-2354" to "UK", // Ukrainian
)

// Sogou Pinyin Layouts https://pinyin.sogou.com/mac
val MacSogouPinyinVariants = mapOf(
"com.sogou.inputmethod.pinyin" to "ZH"
)

// Rime Squirrel Layouts https://rime.im
val MacRimeSquirrelVariants = mapOf(
"im.rime.inputmethod.Squirrel.Hans" to "ZH", // Simplified
"im.rime.inputmethod.Squirrel.Hant" to "ZH" // Traditional
)
Loading

0 comments on commit 426002b

Please sign in to comment.