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.4.2 #31

Merged
merged 6 commits into from
Nov 6, 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
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@
## [Unreleased]


## [1.4.2]
### Changed
- #30 Add support for "Ukrainian" and "Ukrainian-QWERTY" on macOS.

### Fixed
- #28 Fix "Do not call invokeLater when app is not yet fully initialized" error on startup.
- #29 Fix "Migrate com.github.siropkin.kursor.KursorStartupActivity to ProjectActivity" development warning on startup.


## [1.4.1]
### Changed
- #18 Add support for "Squirrel Method" (Chinese) (https://rime.im) on macOS.
- #18 Add support for [Squirrel](https://rime.im) method (Zhuyin) on macOS.
- #20 Add support for "Russian - PC" on macOS.
- #21 Fix color settings save bug; color settings now save correctly.

Expand All @@ -24,7 +33,7 @@ Version skipped due to a mistake in the release process.

## [1.3.0] - 2024-07-31
### Changed
- Add support of Sogou Pinyin Method (Chinese) for macOS.
- Add support of [Sogou Pinyin](https://pinyin.sogou.com/mac) method (Zhuyin) for macOS.

### For Contributors and Developers
- Migrate from Gradle IntelliJ Plugin 1.x to 2.0.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ This feature is particularly beneficial for developers juggling multiple languag
- **🔒 Caps Lock Indicator:** Shows the Caps Lock status on the cursor.
- **🔧 Customization:** Customize the language indicator's font, size, opacity, and position.
- **🖥️ Supported Operating Systems:** Available on Windows, Mac, and Linux.
- **🌐 Supported Languages And Input Methods:** Supports a wide range of languages and input methods, including Chinese Sogou Pinyin and Squirrel Methods on macOS.
- **🌐 Supported Languages And Input Methods:** Supports a wide range of languages and input methods, including [Sogou Pinyin](https://pinyin.sogou.com/mac) and [Squirrel](https://rime.im) Zhuyin methods on macOS.


## Usage
Expand Down
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.4.1
pluginVersion = 1.4.2

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 232
Expand Down
45 changes: 20 additions & 25 deletions src/main/kotlin/com/github/siropkin/kursor/Kursor.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.github.siropkin.kursor

import com.github.siropkin.kursor.keyboardlayout.KeyboardLayout
import com.github.siropkin.kursor.settings.KursorSettings
import com.intellij.openapi.editor.*
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.CaretVisualAttributes
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.VisualPosition
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener
Expand Down Expand Up @@ -113,39 +117,34 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret
return
}

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

val settings = getSettings()
val caret = getPrimaryCaret()
var caretColor: Color? = null
if (settings.changeColorOnNonDefaultLanguage) {
if (keyboardLayoutStringInfo != settings.defaultLanguage) {
caretColor = settings.colorOnNonDefaultLanguage
}
val caretColor = if (settings.changeColorOnNonDefaultLanguage && keyboardLayoutString.lowercase() != settings.defaultLanguage.lowercase()) {
settings.colorOnNonDefaultLanguage
} else {
null
}

if (caret.visualAttributes.color != caretColor) {
setCaretColor(caret, caretColor)
}

if (!settings.showTextIndicator) {
return
}

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

if (isCapsLockOn) {
keyboardLayoutStringInfo = keyboardLayoutStringInfo.uppercase()
val displayText = if (isCapsLockOn) {
keyboardLayoutString.uppercase()
} else {
keyboardLayoutString.lowercase()
}

val caretWidth = getCaretWidth(caret)
val caretHeight = getCaretHeight(caret)
val caretPosition = getCaretPosition(caret)
Expand All @@ -159,11 +158,7 @@ class Kursor(private var editor: Editor): JComponent(), ComponentListener, Caret
}

g.font = Font(settings.textIndicatorFontName, settings.textIndicatorFontStyle, settings.textIndicatorFontSize)
g.color = if (caretColor == null) {
getColorWithAlpha(getDefaultCaretColor()!!, settings.textIndicatorFontAlpha)
} else {
getColorWithAlpha(caretColor, settings.textIndicatorFontAlpha)
}
g.drawString(keyboardLayoutStringInfo, caretPosition.x + indicatorOffsetX, caretPosition.y + indicatorOffsetY)
g.color = getColorWithAlpha(caretColor ?: getDefaultCaretColor()!!, settings.textIndicatorFontAlpha)
g.drawString(displayText, caretPosition.x + indicatorOffsetX, caretPosition.y + indicatorOffsetY)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.event.EditorFactoryEvent
import com.intellij.openapi.editor.event.EditorFactoryListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
import com.intellij.openapi.startup.ProjectActivity
import java.awt.event.KeyEvent


class KursorStartupActivity: StartupActivity {
class KursorStartupActivity: ProjectActivity {
private val kursors = mutableMapOf<Editor, Kursor>()

override fun runActivity(project: Project) {
override suspend fun execute(project: Project) {
// add kursor to all existing editors
val editors: Array<Editor> = EditorFactory.getInstance().allEditors
for (editor in editors) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.siropkin.kursor
package com.github.siropkin.kursor.keyboardlayout

import com.sun.jna.Platform
import com.sun.jna.platform.win32.User32
Expand All @@ -9,140 +9,69 @@
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 windowsKeyboardVariantMap = mapOf(
"00000402" to "BG",
"00000404" to "CH",
"00000405" to "CZ",
"00000406" to "DK",
"00000407" to "DE",
"00000408" to "GK",
"00000409" to "US",
"0000040A" to "SP",
"0000040B" to "SU",
"0000040C" to "FR",
"0000040E" to "HU",
"0000040F" to "IS",
"00000410" to "IT",
"00000411" to "JP",
"00000412" to "KO",
"00000413" to "NL",
"00000414" to "NO",
"00000415" to "PL",
"00000416" to "BR",
"00000418" to "RO",
"00000419" to "RU",
"0000041A" to "YU",
"0000041B" to "SL",
"0000041C" to "US",
"0000041D" to "SV",
"0000041F" to "TR",
"00000422" to "US",
"00000423" to "US",
"00000424" to "YU",
"00000425" to "ET",
"00000426" to "US",
"00000427" to "US",
"00000804" to "CH",
"00000809" to "UK",
"0000080A" to "LA",
"0000080C" to "BE",
"00000813" to "BE",
"00000816" to "PO",
"00000C0C" to "CF",
"00000C1A" to "US",
"00001009" to "CAFR",
"0000100C" to "SF",
"00001809" to "US",
"00010402" to "US",
"00010405" to "CZ",
"00010407" to "DEI",
"00010408" to "GK",
"00010409" to "DV",
"0001040A" to "SP",
"0001040E" to "HU",
"00010410" to "IT",
"00010415" to "PL",
"00010419" to "RUT",
"0001041B" to "SL",
"0001041F" to "TRF",
"00010426" to "US",
"00010C0C" to "CF",
"00010C1A" to "US",
"00020408" to "GK",
"00020409" to "US",
"00030409" to "USL",
"00040409" to "USR",
"00050408" to "GK"
)

private val macKeyboardVariantMap = mapOf(
"UserDefined_19458" to "RU", // Russian
"UserDefined_com.sogou.inputmethod.pinyin" to "ZH", // Sogou Pinyin: https://pinyin.sogou.com/mac
"UserDefined_im.rime.inputmethod.Squirrel.Hans" to "ZH", // Squirrel - Simplified: https://rime.im
"UserDefined_im.rime.inputmethod.Squirrel.Hant" to "ZH" // Squirrel - Traditional: https://rime.im
)

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 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 linuxNonUbuntuKeyboardLayouts: List<String> = emptyList()
private var linuxKeyboardLayoutsCache: List<String> = emptyList()

fun getInfo(): KeyboardLayoutInfo {
fun getLayoutInfo(): KeyboardLayoutInfo {
return when {
Platform.isLinux() -> getLinuxKeyboardLayout()
Platform.isMac() -> getMacKeyboardLayout()
Platform.isWindows() -> getWindowsKeyboardLayout()
else -> KeyboardLayoutInfo(unknown, unknown, unknown)
Platform.isLinux() -> getLinuxLayoutInfo()
Platform.isMac() -> getMacLayoutInfo()
Platform.isWindows() -> getWindowsLayoutInfo()
else -> getUnknownLayoutInfo()
}
}

private fun getLinuxKeyboardLayout(): KeyboardLayoutInfo {
private fun getUnknownLayoutInfo(): KeyboardLayoutInfo {
return KeyboardLayoutInfo(unknown, unknown, unknown)
}

private fun getLinuxLayoutInfo(): 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.
// 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
if (linuxDistribution == "ubuntu") {
// output example: [('xkb', 'us'), ('xkb', 'ru'), ('xkb', 'ca+eng')]
val split = 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 when {
linuxDistribution == "ubuntu" -> getUbuntuLayoutInfo()
linuxDesktopGroup == "wayland" -> getWaylandLayoutInfo()
else -> getOtherLinuxLayoutInfo()
}
}

private fun getUbuntuLayoutInfo(): KeyboardLayoutInfo {
// Output example: [('xkb', 'us'), ('xkb', 'ru'), ('xkb', 'ca+eng')]
val split = 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, "")
}

// FIXME: This command does not work on linuxDesktopGroup = "wayland",
private fun getWaylandLayoutInfo(): KeyboardLayoutInfo {
// FIXME: Other Linux distribution commands not working "Wayland",
// see: https://github.com/siropkin/kursor/issues/3
if (linuxDesktopGroup == "wayland") {
return KeyboardLayoutInfo(unknown, unknown, unknown)
}
return getUnknownLayoutInfo()
}

if (linuxNonUbuntuKeyboardLayouts.isEmpty()) {
// output example: rules: evdev
private fun getOtherLinuxLayoutInfo(): KeyboardLayoutInfo {
if (linuxKeyboardLayoutsCache.isEmpty()) {
// Output example: rules: evdev
//model: pc105
//layout: us
//options: grp:win_space_toggle,terminate:ctrl_alt_bksp
linuxNonUbuntuKeyboardLayouts = executeNativeCommand(arrayOf("setxkbmap", "-query"))
linuxKeyboardLayoutsCache = executeNativeCommand(arrayOf("setxkbmap", "-query"))
.substringAfter("layout:")
.substringBefore("\n")
.trim()
.split(",")
}

// output example: Keyboard Control:
// Output example: Keyboard Control:
// auto repeat: on key click percent: 0 LED mask: 00000000
// XKB indicators:
// 00: Caps Lock: off 01: Num Lock: off 02: Scroll Lock: off
Expand Down Expand Up @@ -177,34 +106,35 @@
.toInt(16)

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

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

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

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

private fun getWindowsKeyboardLayout(): KeyboardLayoutInfo {
private fun getWindowsLayoutInfo(): 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

if (fgWindow == null) {

Check notice on line 137 in src/main/kotlin/com/github/siropkin/kursor/keyboardlayout/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, "")
}

Expand All @@ -215,21 +145,12 @@
val inputMethod = hkl.pointer.toString().split("@")[1]
var layoutId = inputMethod.substring(0, inputMethod.length - 4)
layoutId = when (layoutId) {
"0xfffffffff008" -> {
"00010419"
}
"0xfffffffff014" -> {
"0001041F"
}
"0xfffffffff012" -> {
"00010407"
}
else -> {
layoutId.substring(2).padStart(8, '0')
}
"0xfffffffff008" -> "00010419"
"0xfffffffff014" -> "0001041F"
"0xfffffffff012" -> "00010407"
else -> layoutId.substring(2).padStart(8, '0')
}
layoutId = layoutId.uppercase()
val variant = windowsKeyboardVariantMap[layoutId] ?: ""
val variant = WindowsKeyboardVariants[layoutId.uppercase()] ?: ""
return KeyboardLayoutInfo(locale.language, locale.country, variant)
}

Expand Down
Loading
Loading