Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v14.3 Add support for IntelliJ IDEA 2024.3 #36

Merged
merged 4 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## [Unreleased]

## [1.4.3]

### Changed

- Add support for IntelliJ IDEA 2024.3.

## [1.4.2]

### Changed
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ pluginGroup = com.github.siropkin.kursor
pluginName = Kursor
pluginRepositoryUrl = https://github.com/siropkin/kursor
# SemVer format -> https://semver.org
pluginVersion = 1.4.2
pluginVersion = 1.4.3

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 232
pluginUntilBuild = 242.*
pluginUntilBuild = 243.*

# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType = IC
Expand Down
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()
}
Loading