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

v1.3.0 Fix NullPointerException for Windows users #17

Merged
merged 5 commits into from
Aug 26, 2024
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 1 addition & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

@@ -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
@@ -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
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
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",
@@ -78,157 +78,33 @@
"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.
@@ -240,15 +116,15 @@
.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()) {
@@ -299,34 +175,38 @@

// 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

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]
@@ -346,18 +226,8 @@
}
}
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 {
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
@@ -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)
@@ -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
}
}
@@ -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