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

IDEA-324382 Window frame respect GTK Theme #2565

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class ReviewListCellRenderer<T>(private val presenter: (T) -> ReviewListItemPres
val color = tag.color
if (color != null) {
//TODO: need a separate untinted icon to color properly
label.icon = IconUtil.colorize(DvcsImplIcons.BranchLabel, color)
label.icon = IconUtil.colorizeTint(DvcsImplIcons.BranchLabel, color)
}
else {
label.icon = DvcsImplIcons.BranchLabel
Expand Down
38 changes: 32 additions & 6 deletions platform/core-ui/src/util/IconUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -440,16 +440,28 @@ object IconUtil {

@JvmOverloads
@JvmStatic
fun colorize(source: Icon, color: Color, keepGray: Boolean = false): Icon {
fun colorizeTint(source: Icon, color: Color, keepGray: Boolean = false): Icon {
return filterIcon(icon = source, filterSupplier = object : RgbImageFilterSupplier {
override fun getFilter() = ColorFilter(color = color, keepGray = keepGray)
override fun getFilter() = ColorMultiplyFilter(color = color, keepGray = keepGray)
})
}

@JvmOverloads
@JvmStatic
fun colorize(g: Graphics2D?, source: Icon, color: Color, keepGray: Boolean = false): Icon {
return filterIcon(g = g, source = source, filter = ColorFilter(color = color, keepGray = keepGray))
fun colorizeTint(g: Graphics2D?, source: Icon, color: Color, keepGray: Boolean = false): Icon {
return filterIcon(g = g, source = source, filter = ColorMultiplyFilter(color = color, keepGray = keepGray))
}

@JvmStatic
fun colorizeReplace(source: Icon, color: Color): Icon {
return filterIcon(icon = source, filterSupplier = object : RgbImageFilterSupplier {
override fun getFilter() = ColorReplaceFilter(color = color)
})
}

@JvmStatic
fun colorizeReplace(g: Graphics2D?, source: Icon, color: Color): Icon {
return filterIcon(g = g, source = source, filter = ColorReplaceFilter(color = color))
}

@JvmStatic
Expand Down Expand Up @@ -641,7 +653,7 @@ class CropIcon internal constructor(val sourceIcon: Icon, val crop: Rectangle) :
override fun hashCode(): Int = Objects.hash(sourceIcon, crop)
}

private class ColorFilter(color: Color, private val keepGray: Boolean) : RGBImageFilter() {
private class ColorMultiplyFilter(color: Color, private val keepGray: Boolean) : RGBImageFilter() {
private val base = Color.RGBtoHSB(color.red, color.green, color.blue, null)

override fun filterRGB(x: Int, y: Int, rgba: Int): Int {
Expand All @@ -655,6 +667,20 @@ private class ColorFilter(color: Color, private val keepGray: Boolean) : RGBImag
}
}

private class ColorReplaceFilter(color: Color) : RGBImageFilter() {
private val base = Color.RGBtoHSB(color.red, color.green, color.blue, null)

override fun filterRGB(x: Int, y: Int, rgba: Int): Int {
val r = rgba shr 16 and 0xff
val g = rgba shr 8 and 0xff
val b = rgba and 0xff
val hsb = FloatArray(3)
Color.RGBtoHSB(r, g, b, hsb)
val rgb = Color.HSBtoRGB(base[0], base[1], base[2])
return rgba and -0x1000000 or (rgb and 0xffffff)
}
}

private class DesaturationFilter : RGBImageFilter() {
override fun filterRGB(x: Int, y: Int, rgba: Int): Int {
val r = rgba shr 16 and 0xff
Expand Down Expand Up @@ -762,7 +788,7 @@ private fun scaleByIcon(icon: Icon?, ancestor: Component?, defaultIcon: Icon, si
}
}

private fun filterIcon(g: Graphics2D?, source: Icon, filter: ColorFilter): Icon {
private fun filterIcon(g: Graphics2D?, source: Icon, filter: RGBImageFilter): Icon {
val src = if (g == null) {
ImageUtil.createImage(source.iconWidth, source.iconHeight, BufferedImage.TYPE_INT_ARGB)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,55 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.wm.impl.customFrameDecorations

import com.intellij.icons.AllIcons
import com.intellij.ide.ui.UISettings
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.wm.impl.X11UiUtil
import com.intellij.openapi.wm.impl.customFrameDecorations.frameTitleButtons.FrameTitleButtons
import com.intellij.openapi.wm.impl.customFrameDecorations.frameTitleButtons.LinuxFrameTitleButtons
import com.intellij.openapi.wm.impl.customFrameDecorations.frameTitleButtons.WindowsFrameTitleButtons
import com.intellij.openapi.wm.impl.customFrameDecorations.style.ComponentStyle
import com.intellij.openapi.wm.impl.customFrameDecorations.style.ComponentStyleState
import com.intellij.openapi.wm.impl.customFrameDecorations.style.HOVER_KEY
import com.intellij.openapi.wm.impl.customFrameDecorations.style.StyleManager
import com.intellij.ui.scale.JBUIScale
import com.intellij.util.ui.JBUI.Borders
import com.intellij.util.ui.JBUI.CurrentTheme
import java.awt.Color
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.Graphics
import javax.accessibility.AccessibleContext
import javax.swing.*
import javax.swing.plaf.ButtonUI
import javax.swing.plaf.basic.BasicButtonUI
import javax.swing.Action
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JPanel

internal open class CustomFrameTitleButtons(myCloseAction: Action) {

internal open class CustomFrameTitleButtons(private val myCloseAction: Action,
private val myRestoreAction: Action? = null,
private val myIconifyAction: Action? = null,
private val myMaximizeAction: Action? = null
) {
companion object {
fun create(closeAction: Action): CustomFrameTitleButtons {
val darculaTitleButtons = CustomFrameTitleButtons(closeAction)
fun create(myCloseAction: Action,
myRestoreAction: Action? = null,
myIconifyAction: Action? = null,
myMaximizeAction: Action? = null): CustomFrameTitleButtons {
val darculaTitleButtons = CustomFrameTitleButtons(myCloseAction, myRestoreAction, myIconifyAction, myMaximizeAction)
darculaTitleButtons.createChildren()
return darculaTitleButtons
}
}

private val isLinuxThemingEnabled = Registry.`is`("ide.linux.mimic.system.theme", false)

private val buttons: FrameTitleButtons = if (
SystemInfo.isLinux &&
(SystemInfo.isGNOME || SystemInfo.isKDE) &&
isLinuxThemingEnabled &&
!X11UiUtil.isWSL()
)
LinuxFrameTitleButtons(myCloseAction, myRestoreAction, myIconifyAction, myMaximizeAction)
else
WindowsFrameTitleButtons(myCloseAction, myRestoreAction, myIconifyAction, myMaximizeAction)

private val baseStyle = ComponentStyle.ComponentStyleBuilder<JComponent> {
isOpaque = false
border = Borders.empty()
Expand All @@ -41,32 +63,11 @@ internal open class CustomFrameTitleButtons(myCloseAction: Action) {
}
}

private val closeStyleBuilder: ComponentStyle.ComponentStyleBuilder<JButton> = ComponentStyle.ComponentStyleBuilder<JButton> {
isOpaque = false
border = Borders.empty()
icon = AllIcons.Windows.CloseActive
}.apply {
style(ComponentStyleState.HOVERED) {
isOpaque = true
background = Color(0xe81123)
icon = AllIcons.Windows.CloseHover
}
style(ComponentStyleState.PRESSED) {
isOpaque = true
background = Color(0xf1707a)
icon = AllIcons.Windows.CloseHover
}
}
private val activeCloseStyle = closeStyleBuilder.build()

private val inactiveCloseStyle = closeStyleBuilder
.updateDefault {
icon = AllIcons.Windows.CloseInactive
}.build()
private val panel = TitleButtonsPanel(buttons)

private val panel = TitleButtonsPanel()
fun getView(): JComponent = panel

val closeButton: JButton = createButton("Close", myCloseAction)

internal var isCompactMode: Boolean
set(value) {
Expand All @@ -78,40 +79,62 @@ internal open class CustomFrameTitleButtons(myCloseAction: Action) {

var isSelected: Boolean = false
set(value) {
if(field != value) {
if (field != value) {
field = value
updateStyles()
}
}

protected open fun updateStyles() {
StyleManager.applyStyle(closeButton, if(isSelected) activeCloseStyle else inactiveCloseStyle)
StyleManager.applyStyle(
buttons.closeButton,
getStyle(
if (isSelected) buttons.closeIcon else buttons.closeInactiveIcon,
buttons.closeHoverIcon
)
)
buttons.restoreButton?.let {
StyleManager.applyStyle(
it,
getStyle(
if (isSelected) buttons.restoreIcon else buttons.restoreInactiveIcon,
buttons.restoreIcon
)
)
}
buttons.maximizeButton?.let {
StyleManager.applyStyle(
it,
getStyle(
if (isSelected) buttons.maximizeIcon else buttons.maximizeInactiveIcon,
buttons.maximizeIcon
)
)
}
buttons.minimizeButton?.let {
StyleManager.applyStyle(
it,
getStyle(
if (isSelected) buttons.minimizeIcon else buttons.minimizeInactiveIcon,
buttons.minimizeIcon
)
)
}
}

protected fun createChildren() {
fillButtonPane()
addCloseButton()
buttons.fillButtonPane(panel)
updateVisibility()
updateStyles()
}

fun getView(): JComponent = panel

protected open fun fillButtonPane() {
}

open fun updateVisibility() {
buttons.minimizeButton?.isVisible = myIconifyAction?.isEnabled ?: false
buttons.restoreButton?.isVisible = myRestoreAction?.isEnabled ?: false
buttons.maximizeButton?.isVisible = myMaximizeAction?.isEnabled ?: false
}

private fun addCloseButton() {
addComponent(closeButton)
}

protected fun addComponent(component: JComponent) {
panel.addComponent(component)
}

protected fun getStyle(icon: Icon, hoverIcon : Icon): ComponentStyle<JComponent> {
protected fun getStyle(icon: Icon, hoverIcon: Icon): ComponentStyle<JComponent> {
val clone = baseStyle.clone()
clone.updateDefault {
this.icon = icon
Expand All @@ -127,35 +150,9 @@ internal open class CustomFrameTitleButtons(myCloseAction: Action) {
return clone.build()
}

protected fun createButton(accessibleName: String, action: Action): JButton {
val button = object : JButton(){
init {
super.setUI(HoveredButtonUI())
}

override fun setUI(ui: ButtonUI?) {
}
}
button.action = action
button.isFocusable = false
button.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, accessibleName)
button.text = null
return button
}
}
private class HoveredButtonUI : BasicButtonUI() {
override fun paint(g: Graphics, c: JComponent) {
getHoverColor(c)?.let {
g.color = it
g.fillRect(0, 0, c.width, c.height)
}
super.paint(g, c)
}

private fun getHoverColor(c: JComponent): Color? = c.getClientProperty(HOVER_KEY) as? Color
}

private class TitleButtonsPanel : JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)) {
class TitleButtonsPanel(val buttons: FrameTitleButtons) : JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)) {
var isCompactMode = false
set(value) {
field = value
Expand All @@ -177,10 +174,11 @@ private class TitleButtonsPanel : JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)) {

private fun JComponent.setScaledPreferredSize() {
val size = CurrentTheme.TitlePane.buttonPreferredSize(UISettings.defFontScale).clone() as Dimension
// TODO isCompactMode is always false
if (isCompactMode) {
size.height = JBUIScale.scale(30)
}
preferredSize = Dimension(size.width, size.height)
preferredSize = buttons.setScaledPreferredSize(size)
}

override fun updateUI() {
Expand Down
Loading