Skip to content

Commit

Permalink
Merge pull request #31 from siropkin/develop
Browse files Browse the repository at this point in the history
v1.4.2
  • Loading branch information
siropkin authored Nov 6, 2024
2 parents 00786e2 + f69a5b1 commit dd34e63
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 168 deletions.
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.BufferedReader
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,27 +106,28 @@ class KeyboardLayout {
.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
Expand All @@ -215,21 +145,12 @@ class KeyboardLayout {
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

0 comments on commit dd34e63

Please sign in to comment.