Skip to content

Commit

Permalink
Merge pull request #17 from siropkin/develop
Browse files Browse the repository at this point in the history
v1.3.0 Fix NullPointerException for Windows users
  • Loading branch information
siropkin authored Aug 26, 2024
2 parents ef72a2b + 0f0054d commit 15d96f4
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 339 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
## [Unreleased]


## [1.3.1] - 2024-08-26
### Changed
- Fix NullPointerException for Windows users.


## [1.3.0] - 2024-07-31
### Changed
- Add support of Sogou Pinyin Method (Chinese) for macOS.
Expand Down
9 changes: 1 addition & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ This feature is particularly beneficial for developers juggling multiple languag
- **🌐 Supported Languages And Input Methods:** Supports multiple languages and input methods, including Sogou Pinyin Method (Chinese) for macOS.


## Installation
[Get from marketplace](https://plugins.jetbrains.com/plugin/22072-kursor) or install it directly inside your IDE:

- **For Windows & Linux:** `File` > `Settings` > `Plugins` > `Marketplace` > Search for "Kursor" > `Install Plugin` > Restart IntelliJ IDEA
- **For Mac:** `IntelliJ IDEA` > `Preferences` > `Plugins` > `Marketplace` > Search for "Kursor" > `Install Plugin` > Restart IntelliJ IDEA


## Usage
Once installed, Kursor will automatically run when you open your project in IntelliJ IDEA.

Expand Down Expand Up @@ -79,5 +72,5 @@ Kursor is open-source and available under the [Apache 2.0 license](https://www.a


## Support
[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/ivan.seredkin)
[Buy Me A Coffee](https://www.buymeacoffee.com/ivan.seredkin)
<!-- Plugin description end -->
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginGroup = com.github.siropkin.kursor
pluginName = Kursor
pluginRepositoryUrl = https://github.com/siropkin/kursor
# SemVer format -> https://semver.org
pluginVersion = 1.3.0
pluginVersion = 1.3.1

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 232
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import java.io.IOException
private const val unknown = "unk"

// https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-language-pack-default-values
private val windowsKeyboardLayoutMap = mapOf(
private val windowsKeyboardVariantMap = mapOf(
"00000402" to "BG",
"00000404" to "CH",
"00000405" to "CZ",
Expand Down Expand Up @@ -78,157 +78,33 @@ private val windowsKeyboardLayoutMap = mapOf(
"00050408" to "GK"
)

// https://www.autoitscript.com/autoit3/docs/appendix/OSLangCodes.htm
private val windowsKeyboardCountryCodeMap = mapOf(
"0004" to "zh-CHS",
"0401" to "ar-SA",
"0402" to "bg-BG",
"0403" to "ca-ES",
"0404" to "zh-TW",
"0405" to "cs-CZ",
"0406" to "da-DK",
"0407" to "de-DE",
"0408" to "el-GR",
"0409" to "en-US",
"040A" to "es-ES",
"040B" to "fi-FI",
"040C" to "fr-FR",
"040D" to "he-IL",
"040E" to "hu-HU",
"040F" to "is-IS",
"0410" to "it-IT",
"0411" to "ja-JP",
"0412" to "ko-KR",
"0413" to "nl-NL",
"0414" to "nb-NO",
"0415" to "pl-PL",
"0416" to "pt-BR",
"0417" to "rm-CH",
"0418" to "ro-RO",
"0419" to "ru-RU",
"041A" to "hr-HR",
"041B" to "sk-SK",
"041C" to "sq-AL",
"041D" to "sv-SE",
"041E" to "th-TH",
"041F" to "tr-TR",
"0420" to "ur-PK",
"0421" to "id-ID",
"0422" to "uk-UA",
"0423" to "be-BY",
"0424" to "sl-SI",
"0425" to "et-EE",
"0426" to "lv-LV",
"0427" to "lt-LT",
"0428" to "tg-TJ",
"0429" to "fa-IR",
"042A" to "vi-VN",
"042B" to "hy-AM",
"042C" to "az-AZ",
"042D" to "eu-ES",
"042E" to "hsb-DE",
"042F" to "mk-MK",
"0432" to "tn-ZA",
"0434" to "xh-ZA",
"0435" to "zu-ZA",
"0436" to "af-ZA",
"0437" to "ka-GE",
"0438" to "fo-FO",
"0439" to "hi-IN",
"043A" to "mt-MT",
"043B" to "se-NO",
"043E" to "ms-MY",
"043F" to "kk-KZ",
"0440" to "ky-KG",
"0441" to "sw-KE",
"0442" to "tk-TM",
"0443" to "uz-UZ",
"0444" to "tt-RU",
"0445" to "bn-IN",
"0446" to "pa-IN",
"0447" to "gu-IN",
"0448" to "or-IN",
"0449" to "ta-IN",
"044A" to "te-IN",
"044B" to "kn-IN",
"044C" to "ml-IN",
"044D" to "as-IN",
"044E" to "mr-IN",
"044F" to "sa-IN",
"0450" to "mn-MN",
"0451" to "bo-CN",
"0452" to "cy-GB",
"0453" to "km-KH",
"0454" to "lo-LA",
"0456" to "gl-ES",
"0457" to "kok-IN",
"0459" to "sd-IN",
"045A" to "syr-SY",
"045B" to "si-LK",
"045C" to "chr-US",
"045D" to "iu-CA",
"045E" to "am-ET",
"0461" to "ne-NP",
"0462" to "fy-NL",
"0463" to "ps-AF",
"0464" to "fil-PH",
"0465" to "dv-MV",
"0468" to "ha-NG",
"046A" to "yo-NG",
"046B" to "quz-BO",
"046C" to "nso-ZA",
"046D" to "ba-RU",
"046E" to "lb-LU",
"046F" to "kl-GL",
"0470" to "ig-NG",
"0473" to "ti-ET",
"0475" to "haw-US",
"0478" to "ii-CN",
"047A" to "arn-CL",
"047C" to "moh-CA",
"047E" to "br-FR",
"0480" to "ug-CN",
"0481" to "mi-NZ",
"0482" to "oc-FR",
"0483" to "co-FR",
"0484" to "gsw-FR",
"0485" to "sah-RU",
"0486" to "quc-GT",
"0487" to "rw-RW",
"0488" to "wo-SN",
"048C" to "prs-AF",
"0491" to "gd-GB",
"0492" to "ku-IQ",
"0801" to "ar-IQ"
)

private val macKeyboardVariantMap = mapOf(
"UserDefined_com.sogou.inputmethod.pinyin" to "ZH" // https://pinyin.sogou.com/mac
)

class KeyboardLayout(private val layout: String, private val country: String, private val language: String) {
override fun toString(): String = layout.lowercase().ifEmpty {
class KeyboardLayoutInfo(private val language: String, private val country: String, private val variant: String) {
override fun toString(): String = variant.lowercase().ifEmpty {
country.lowercase().ifEmpty {
language.lowercase()
}
}
}

class KeyboardLayoutInfo {
class KeyboardLayout {
private var linuxDistribution: String = System.getenv("DESKTOP_SESSION")?.lowercase() ?: ""
private var linuxDesktopGroup: String = System.getenv("XDG_SESSION_TYPE")?.lowercase() ?: ""
private var linuxNonUbuntuKeyboardLayouts: List<String> = emptyList()

fun getLayout(): KeyboardLayout {
fun getInfo(): KeyboardLayoutInfo {
return when {
Platform.isLinux() -> getLinuxKeyboardLayout()
Platform.isWindows() -> getWindowsKeyboardLayout()
Platform.isMac() -> getMacKeyboardLayout()
else -> KeyboardLayout(unknown, unknown, unknown)
Platform.isWindows() -> getWindowsKeyboardLayout()
else -> KeyboardLayoutInfo(unknown, unknown, unknown)
}
}

private fun getLinuxKeyboardLayout(): KeyboardLayout {
private fun getLinuxKeyboardLayout(): KeyboardLayoutInfo {
// 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.
Expand All @@ -240,15 +116,15 @@ class KeyboardLayoutInfo {
.substringAfter("('xkb', '")
.substringBefore("')")
.split("+")
val country = split[0]
val language = if (split.size > 1) split[1] else ""
return KeyboardLayout("", country, language)
val country = split[0]
return KeyboardLayoutInfo(language, country, "")
}

// FIXME: This command does not work on linuxDesktopGroup = "wayland",
// see: https://github.com/siropkin/kursor/issues/3
if (linuxDesktopGroup == "wayland") {
return KeyboardLayout(unknown, unknown, unknown)
return KeyboardLayoutInfo(unknown, unknown, unknown)
}

if (linuxNonUbuntuKeyboardLayouts.isEmpty()) {
Expand Down Expand Up @@ -299,34 +175,38 @@ class KeyboardLayoutInfo {

// Additional check to avoid out-of-bounds exception
if (linuxCurrentKeyboardLayoutIndex >= linuxNonUbuntuKeyboardLayouts.size) {
return KeyboardLayout(unknown, unknown, unknown)
return KeyboardLayoutInfo(unknown, unknown, unknown)
}

// 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 (linuxNonUbuntuKeyboardLayouts.size > 2 && linuxCurrentKeyboardLayoutIndex > 0) {
return KeyboardLayout(unknown, unknown, unknown)
return KeyboardLayoutInfo(unknown, unknown, unknown)
}

val country = linuxNonUbuntuKeyboardLayouts[linuxCurrentKeyboardLayoutIndex]
return KeyboardLayout("", country, "")
return KeyboardLayoutInfo("", country, "")
}

private fun getMacKeyboardLayout(): KeyboardLayout {
private fun getMacKeyboardLayout(): KeyboardLayoutInfo {
val locale = InputContext.getInstance().locale
val layout = macKeyboardVariantMap[locale.variant] ?: "" // variant example for US: UserDefined_252
return KeyboardLayout(layout, locale.country, locale.language)
val variant = macKeyboardVariantMap[locale.variant] ?: "" // variant example for US: UserDefined_252
return KeyboardLayoutInfo(locale.language, locale.country, variant)
}

private fun getWindowsKeyboardLayout(): KeyboardLayout {
// Standard "InputContext.getInstance().locale" does not work on Windows
// in case user set different keyboard layout (inputs) for one language
private fun getWindowsKeyboardLayout(): KeyboardLayoutInfo {
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
val fgWindow: WinDef.HWND? = user32.GetForegroundWindow() // Get the handle of the foreground window

if (fgWindow == null) {

Check notice on line 204 in src/main/kotlin/com/github/siropkin/kursor/KeyboardLayout.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

If-Null return/break/... foldable to '?:'

If-Null return/break/... foldable to '?:'
return KeyboardLayoutInfo(locale.language, locale.country, "")
}

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
// hkl.pointer returns native@0x4090409: last 4 digits are language id, the rest is layout id
val inputMethod = hkl.pointer.toString().split("@")[1]
Expand All @@ -346,18 +226,8 @@ class KeyboardLayoutInfo {
}
}
layoutId = layoutId.uppercase()

val languageId = inputMethod.substring(inputMethod.length - 4, inputMethod.length).uppercase()

val layout = windowsKeyboardLayoutMap[layoutId] ?: unknown
val countryCode = windowsKeyboardCountryCodeMap[languageId] ?: unknown
if (layout == unknown && countryCode == unknown) {
return KeyboardLayout(unknown, unknown, unknown)
}
val split = countryCode.split("-")
val language = split[0]
val country = if (split.size > 1) split[1] else ""
return KeyboardLayout(layout, country, language)
val variant = windowsKeyboardVariantMap[layoutId] ?: ""
return KeyboardLayoutInfo(locale.language, locale.country, variant)
}

private fun executeNativeCommand(command: Array<String>): String {
Expand Down
35 changes: 17 additions & 18 deletions src/main/kotlin/com/github/siropkin/kursor/Kursor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ object IndicatorPosition {
const val BOTTOM = "bottom"
}


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

init {
editor.contentComponent.add(this)
Expand Down Expand Up @@ -116,16 +115,16 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret

val settings = getSettings()
val isCapsLockOn = settings.indicateCapsLock && getIsCapsLockOn()
val keyboardLayout = keyboardLayoutInfo.getLayout()
var keyboardLayoutString = keyboardLayout.toString()
if (keyboardLayoutString.isEmpty()) {
val keyboardLayoutInfo = keyboardLayout.getInfo()
var keyboardLayoutStringInfo = keyboardLayoutInfo.toString()
if (keyboardLayoutStringInfo.isEmpty()) {
return
}

val caret = getPrimaryCaret()
var caretColor: Color? = null
if (settings.changeColorOnNonDefaultLanguage) {
if (keyboardLayoutString != settings.defaultLanguage) {
if (keyboardLayoutStringInfo != settings.defaultLanguage) {
caretColor = settings.colorOnNonDefaultLanguage
}
}
Expand All @@ -134,37 +133,37 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret
setCaretColor(caret, caretColor)
}

if (!settings.showIndicator) {
if (!settings.showTextIndicator) {
return
}

val showIndicator = settings.indicateDefaultLanguage || isCapsLockOn || keyboardLayoutString.lowercase() != settings.defaultLanguage.lowercase()
if (!showIndicator) {
val showTextIndicator = settings.indicateDefaultLanguage || isCapsLockOn || keyboardLayoutStringInfo.lowercase() != settings.defaultLanguage.lowercase()
if (!showTextIndicator) {
return
}

if (isCapsLockOn) {
keyboardLayoutString = keyboardLayoutString.uppercase()
keyboardLayoutStringInfo = keyboardLayoutStringInfo.uppercase()
}

val caretWidth = getCaretWidth(caret)
val caretHeight = getCaretHeight(caret)
val caretPosition = getCaretPosition(caret)

val indicatorOffsetX = caretWidth + settings.indicatorHorizontalOffset
val indicatorOffsetY = when (settings.indicatorVerticalPosition) {
IndicatorPosition.TOP -> (if (caret.visualPosition.line == 0) settings.indicatorFontSize else settings.indicatorFontSize / 2) - 1
IndicatorPosition.MIDDLE -> caretHeight / 2 + settings.indicatorFontSize / 2 - 1
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
else -> 0
}

g.font = Font(settings.indicatorFontName, settings.indicatorFontStyle, settings.indicatorFontSize)
g.font = Font(settings.textIndicatorFontName, settings.textIndicatorFontStyle, settings.textIndicatorFontSize)
g.color = if (caretColor == null) {
getColorWithAlpha(getDefaultCaretColor()!!, settings.indicatorFontAlpha)
getColorWithAlpha(getDefaultCaretColor()!!, settings.textIndicatorFontAlpha)
} else {
getColorWithAlpha(caretColor, settings.indicatorFontAlpha)
getColorWithAlpha(caretColor, settings.textIndicatorFontAlpha)
}
g.drawString(keyboardLayoutString, caretPosition.x + indicatorOffsetX, caretPosition.y + indicatorOffsetY)
g.drawString(keyboardLayoutStringInfo, caretPosition.x + indicatorOffsetX, caretPosition.y + indicatorOffsetY)
}
}
Loading

0 comments on commit 15d96f4

Please sign in to comment.