Skip to content

Commit

Permalink
Refactor maximum width calculation
Browse files Browse the repository at this point in the history
Refactor the FormatConfig object to use JLabel HTML formatting instead
of separate JLabels. This will ensure that the calculated width is
exactly the same as in the popup instead of being just an
approximation.

By refactor this we can also remove a lot of special "calculation
functions" that we had implemented before. This way we have no width
related code in the FormatConfig object.
  • Loading branch information
TheBlob42 committed Feb 27, 2021
1 parent c1bc66a commit 55d9862
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 85 deletions.
100 changes: 22 additions & 78 deletions src/main/kotlin/eu/theblob42/idea/whichkey/config/FormatConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import com.intellij.openapi.editor.colors.TextAttributesKey
import com.maddyhome.idea.vim.ex.vimscript.VimScriptGlobalEnvironment
import eu.theblob42.idea.whichkey.model.Mapping
import java.awt.Color
import java.awt.Font
import javax.swing.JLabel
import javax.swing.KeyStroke

Expand Down Expand Up @@ -101,77 +100,32 @@ object FormatConfig {
}

/**
* Format all given mappings with the appropriate HTML tags and font colors
* @param mappings The mappings which should be formatted
* @return A [List] of the [String]s representing the formatted mappings
*/
fun formatMappings(mappings: List<Pair<String, Mapping>>): List<String> {
return mappings
.map { (key, mapping) ->
val formattedKey = String.format(
buildHtmlFormatString(keyStyle, keyColor),
escapeForHtml(key))
val formattedDivider = String.format(
buildHtmlFormatString("span", defaultForegroundColor),
divider)
val formattedDescription = String.format(
buildHtmlFormatString(
if (mapping.prefix) descPrefixStyle else descCommandStyle,
if (mapping.prefix) descPrefixColor else descCommandColor
),
escapeForHtml(mapping.description))

"$formattedKey$formattedDivider$formattedDescription"
}
}

/**
* Calculate the string width (number of characters) without any HTML tags or style attributes for the given [entry]
* Format the given mapping with the appropriate HTML tags and font attributes
*
* @param entry A [Pair] of the next key to press and the corresponding [Mapping] that should be used for the calculation
* @return The raw string width (number of characters)
* @param entry A [Pair] of the next key to press and the corresponding [Mapping]
* @return An HTML [String] representation of the formatted mapping
*/
fun calcRawMappingWidth(entry: Pair<String, Mapping>): Int {
fun formatMappingEntry(entry: Pair<String, Mapping>): String {
val (key, mapping) = entry
return "${key}$divider${mapping.description}".length
}

/**
* Calculate the pixel width of the given [entry] after formatting it according to the configured
* font-family, font-size and font-style for each individual part (key, divider & description).
*
* The calculated width is not 100% exact but its approximation is very close and from testing only a few pixels of,
* which is good enough for our use case.
*
* @param entry A [Pair] of the next key to press and the corresponding [Mapping] that should be used for the calculation
* @return The approximate width of the formatted entry in pixels
*/
fun calcFormattedMappingWidth(entry: Pair<String, Mapping>): Int {
val (key, mapping) = entry
val keyFont = Font(fontFamily, toFontStyle(keyStyle), fontSize)
val dividerFont = Font(fontFamily, Font.PLAIN, fontSize)
val descriptionFont = Font(
fontFamily,
toFontStyle(if (mapping.prefix) descPrefixStyle else descCommandStyle),
fontSize)

val keyLabel = JLabel(key)
keyLabel.font = keyFont
val keyWidth = keyLabel.preferredSize.width

val dividerLabel = JLabel(divider)
dividerLabel.font = dividerFont
val dividerWidth = dividerLabel.preferredSize.width

val descriptionLabel = JLabel(mapping.description)
descriptionLabel.font = descriptionFont
val descriptionWidth = descriptionLabel.preferredSize.width

return keyWidth + dividerWidth + descriptionWidth
val formattedKey = String.format(
buildHtmlFormatString(keyStyle, keyColor),
escapeForHtml(key))
val formattedDivider = String.format(
buildHtmlFormatString("span", defaultForegroundColor),
divider)
val formattedDescription = String.format(
buildHtmlFormatString(
if (mapping.prefix) descPrefixStyle else descCommandStyle,
if (mapping.prefix) descPrefixColor else descCommandColor
),
escapeForHtml(mapping.description))

return "$formattedKey$formattedDivider$formattedDescription"
}

/**
* Format the typed keys sequence as paragraph with appropriate HTML tags and font colors
*
* @param keyStrokes The key strokes which should be formatted
* @return The formatted sequence as HTML paragraph
*/
Expand All @@ -190,21 +144,9 @@ object FormatConfig {
// ***** UTILITY FUNCTIONS
// *****************************************************************************************************************

/**
* Convert the given HTML font [tag] into an appropriate [Font] style value
* @param tag The HTML tag to convert (e.g. "i", "b")
* @return The appropriate constant (e.g. [Font.ITALIC], [Font.BOLD])
*/
private fun toFontStyle(tag: String): Int {
return when (tag) {
"i" -> Font.ITALIC
"b" -> Font.BOLD
else -> Font.PLAIN
}
}

/**
* Build an HTML string for the usage with [String.format] to add tags and CSS colors
*
* @param tagName The HTML tag to use
* @param color The font color
* @return The built format string
Expand All @@ -215,6 +157,7 @@ object FormatConfig {

/**
* Convert [Color] to hex color code
*
* @param color The AWT [Color] object to convert
* @return The appropriate hex code for the given [Color]
*/
Expand All @@ -224,6 +167,7 @@ object FormatConfig {

/**
* Replace any problematic HTML characters with the appropriate escaped ones
*
* @param text The text to escape for the usage in HTML
* @return The escaped [String]
*/
Expand Down
16 changes: 9 additions & 7 deletions src/main/kotlin/eu/theblob42/idea/whichkey/config/PopupConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.maddyhome.idea.vim.option.OptionsManager
import eu.theblob42.idea.whichkey.model.Mapping
import kotlinx.coroutines.*
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.KeyStroke
import kotlin.math.ceil

Expand Down Expand Up @@ -52,10 +53,10 @@ object PopupConfig {
* it might be erroneous and could change in the future
*/
val frameWidth = (ideFrame.width * 0.65).toInt()
// check for the longest string without HTML tags or styling (we have manually checked that 'nestedMappings' is not empty)
val maxMapping = nestedMappings.maxByOrNull { FormatConfig.calcRawMappingWidth(it) }!!
// calculate the pixel width of the longest mapping string (with styling)
val maxStringWidth = FormatConfig.calcFormattedMappingWidth(maxMapping)
// check for the longest string as this will most probably be the widest mapping
val maxMapping = nestedMappings.maxByOrNull { (key, mapping) -> key.length + mapping.description.length }!! // (we have manually checked that 'nestedMappings' is not empty)
// calculate the pixel width of the longest mapping string (with HTML formatting & styling)
val maxStringWidth = JLabel("<html>${FormatConfig.formatMappingEntry(maxMapping)}</html>").preferredSize.width
val possibleColumns = (frameWidth / maxStringWidth).let {
when {
// ensure a minimum value of 1 to avoid dividing by zero
Expand All @@ -69,10 +70,11 @@ object PopupConfig {
val columnWidth = frameWidth / possibleColumns

val elementsPerColumn = ceil(nestedMappings.size / possibleColumns.toDouble()).toInt()
val windowedMappings = FormatConfig.formatMappings(
val windowedMappings = nestedMappings
// TODO implement other sort options
nestedMappings.sortedBy { it.first }
).windowed(elementsPerColumn, elementsPerColumn, true)
.sortedBy { it.first }
.map(FormatConfig::formatMappingEntry)
.windowed(elementsPerColumn, elementsPerColumn, true)

// to properly align the columns within HTML use a table with fixed with cells
val mappingsStringBuilder = StringBuilder()
Expand Down

0 comments on commit 55d9862

Please sign in to comment.