mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-19 21:11:28 +07:00
388 lines
13 KiB
Kotlin
388 lines
13 KiB
Kotlin
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
|
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE", "ReplaceGetOrSet")
|
|
|
|
package com.intellij.ui
|
|
|
|
import com.intellij.ide.IdeBundle
|
|
import com.intellij.ide.gdpr.Consent
|
|
import com.intellij.ide.gdpr.ConsentOptions
|
|
import com.intellij.ide.gdpr.ConsentSettingsUi
|
|
import com.intellij.ide.plugins.PluginManagerCore
|
|
import com.intellij.internal.statistic.persistence.UsageStatisticsPersistenceComponent
|
|
import com.intellij.openapi.application.ApplicationManager
|
|
import com.intellij.openapi.application.ApplicationNamesInfo
|
|
import com.intellij.openapi.application.PathManager
|
|
import com.intellij.openapi.application.ex.ApplicationInfoEx
|
|
import com.intellij.openapi.application.impl.ApplicationInfoImpl
|
|
import com.intellij.openapi.diagnostic.Logger
|
|
import com.intellij.openapi.diagnostic.logger
|
|
import com.intellij.openapi.project.Project
|
|
import com.intellij.openapi.ui.DialogWrapper
|
|
import com.intellij.openapi.util.Condition
|
|
import com.intellij.openapi.util.IconLoader.setUseDarkIcons
|
|
import com.intellij.openapi.util.SystemInfo
|
|
import com.intellij.openapi.util.SystemInfoRt
|
|
import com.intellij.openapi.wm.IdeFrame
|
|
import com.intellij.ui.AppIcon.MacAppIcon
|
|
import com.intellij.ui.icons.findSvgData
|
|
import com.intellij.ui.scale.DerivedScaleType
|
|
import com.intellij.ui.scale.JBUIScale.scale
|
|
import com.intellij.ui.scale.JBUIScale.sysScale
|
|
import com.intellij.ui.scale.ScaleContext
|
|
import com.intellij.ui.svg.loadWithSizes
|
|
import com.intellij.util.JBHiDPIScaledImage
|
|
import com.intellij.util.PlatformUtils
|
|
import com.intellij.util.io.URLUtil
|
|
import com.intellij.util.ui.ImageUtil
|
|
import com.intellij.util.ui.JBImageIcon
|
|
import sun.awt.AWTAccessor
|
|
import java.awt.*
|
|
import java.awt.event.ActionEvent
|
|
import java.io.File
|
|
import java.lang.reflect.InvocationTargetException
|
|
import java.util.function.Predicate
|
|
import javax.swing.Action
|
|
import javax.swing.Icon
|
|
import javax.swing.JComponent
|
|
import javax.swing.border.Border
|
|
|
|
private const val VENDOR_PREFIX = "jetbrains-"
|
|
private var ourIcons: MutableList<Image?>? = null
|
|
|
|
@Volatile
|
|
private var isMacDocIconSet = false
|
|
|
|
private val LOG: Logger
|
|
get() = logger<AppUIUtil>()
|
|
|
|
fun updateAppWindowIcon(window: Window) {
|
|
if (AppUIUtil.isWindowIconAlreadyExternallySet) {
|
|
return
|
|
}
|
|
|
|
var images = ourIcons
|
|
if (images == null) {
|
|
images = ArrayList(3)
|
|
val appInfo = ApplicationInfoImpl.getShadowInstance()
|
|
val svgIconUrl = appInfo.applicationSvgIconUrl
|
|
val smallSvgIconUrl = appInfo.smallApplicationSvgIconUrl
|
|
val scaleContext = ScaleContext.create(window)
|
|
if (SystemInfoRt.isUnix) {
|
|
loadAppIconImage(svgPath = svgIconUrl, scaleContext = scaleContext, size = 128)?.let {
|
|
images.add(it)
|
|
}
|
|
}
|
|
loadAppIconImage(svgPath = smallSvgIconUrl, scaleContext = scaleContext, size = 32)?.let {
|
|
images.add(it)
|
|
}
|
|
if (SystemInfoRt.isWindows) {
|
|
loadAppIconImage(svgPath = smallSvgIconUrl, scaleContext = scaleContext, size = 16)?.let {
|
|
images.add(it)
|
|
}
|
|
}
|
|
for (i in images.indices) {
|
|
val image = images[i]
|
|
if (image is JBHiDPIScaledImage) {
|
|
images.set(i, image.delegate)
|
|
}
|
|
}
|
|
|
|
ourIcons = images
|
|
}
|
|
|
|
if (!images.isEmpty()) {
|
|
if (!SystemInfoRt.isMac) {
|
|
window.iconImages = images
|
|
}
|
|
else if (!isMacDocIconSet) {
|
|
MacAppIcon.setDockIcon(ImageUtil.toBufferedImage(images.first()!!))
|
|
isMacDocIconSet = true
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Returns a HiDPI-aware image. */
|
|
private fun loadAppIconImage(svgPath: String, scaleContext: ScaleContext, size: Int): Image? {
|
|
val pixScale = scaleContext.getScale(DerivedScaleType.PIX_SCALE).toFloat()
|
|
val svgData = findSvgData(path = svgPath, classLoader = AppUIUtil::class.java.classLoader, pixScale = pixScale)
|
|
if (svgData == null) {
|
|
LOG.warn("Cannot load SVG application icon from $svgPath")
|
|
return null
|
|
}
|
|
return loadWithSizes(sizes = listOf(size), data = svgData, scale = pixScale).first()
|
|
}
|
|
|
|
fun loadSmallApplicationIcon(scaleContext: ScaleContext,
|
|
size: Int = 16,
|
|
isReleaseIcon: Boolean = !ApplicationInfoImpl.getShadowInstance().isEAP): Icon {
|
|
val appInfo = ApplicationInfoImpl.getShadowInstance()
|
|
val smallIconUrl = if (isReleaseIcon && appInfo.isEAP && appInfo is ApplicationInfoImpl) {
|
|
// This is the way to load the release icon in EAP. Needed for some actions.
|
|
appInfo.getSmallApplicationSvgIconUrl(false)
|
|
} else appInfo.smallApplicationSvgIconUrl
|
|
val image = loadAppIconImage(smallIconUrl, scaleContext, size) ?: error("Can't load '${smallIconUrl}'")
|
|
return JBImageIcon(image)
|
|
}
|
|
|
|
fun findAppIcon(): String? {
|
|
val binPath = PathManager.getBinPath()
|
|
val binFiles = File(binPath).list()
|
|
if (binFiles != null) {
|
|
for (child in binFiles) {
|
|
if (child.endsWith(".svg")) {
|
|
return "$binPath/$child"
|
|
}
|
|
}
|
|
}
|
|
val url = ApplicationInfoEx::class.java.getResource(ApplicationInfoImpl.getShadowInstance().applicationSvgIconUrl)
|
|
return if (url != null && URLUtil.FILE_PROTOCOL == url.protocol) URLUtil.urlToFile(url).absolutePath else null
|
|
}
|
|
|
|
object AppUIUtil {
|
|
@Suppress("MemberVisibilityCanBePrivate")
|
|
val isWindowIconAlreadyExternallySet: Boolean
|
|
get() {
|
|
if (SystemInfoRt.isMac) {
|
|
return isMacDocIconSet || !PlatformUtils.isJetBrainsClient() && !PluginManagerCore.isRunningFromSources()
|
|
}
|
|
else {
|
|
return SystemInfoRt.isWindows && java.lang.Boolean.getBoolean("ide.native.launcher") && SystemInfo.isJetBrainsJvm
|
|
}
|
|
}
|
|
|
|
// todo[tav] JBR supports loading icon resource (id=2000) from the exe launcher, remove when OpenJDK supports it as well
|
|
@JvmOverloads
|
|
@JvmStatic
|
|
fun loadSmallApplicationIcon(scaleContext: ScaleContext, size: Int = 16): Icon {
|
|
return loadSmallApplicationIcon(scaleContext = scaleContext,
|
|
size = size,
|
|
isReleaseIcon = !ApplicationInfoImpl.getShadowInstance().isEAP)
|
|
}
|
|
|
|
@JvmStatic
|
|
fun loadApplicationIcon(ctx: ScaleContext, size: Int): Icon? {
|
|
val url = ApplicationInfoImpl.getShadowInstance().applicationSvgIconUrl
|
|
return loadAppIconImage(url, ctx, size)?.let { JBImageIcon(it) }
|
|
}
|
|
|
|
@JvmStatic
|
|
fun invokeLaterIfProjectAlive(project: Project, runnable: Runnable) {
|
|
val application = ApplicationManager.getApplication()
|
|
if (application.isDispatchThread) {
|
|
if (project.isOpen && !project.isDisposed) {
|
|
runnable.run()
|
|
}
|
|
}
|
|
else {
|
|
application.invokeLater(runnable) { !project.isOpen || project.isDisposed }
|
|
}
|
|
}
|
|
|
|
@JvmStatic
|
|
fun invokeOnEdt(runnable: Runnable) {
|
|
@Suppress("DEPRECATION")
|
|
invokeOnEdt(runnable = runnable, expired = null)
|
|
}
|
|
|
|
@JvmStatic
|
|
@Deprecated("Use {@link com.intellij.openapi.application.AppUIExecutor#expireWith(Disposable)}")
|
|
fun invokeOnEdt(runnable: Runnable, expired: Condition<*>?) {
|
|
val application = ApplicationManager.getApplication()
|
|
if (application.isDispatchThread) {
|
|
if (expired == null || !expired.value(null)) {
|
|
runnable.run()
|
|
}
|
|
}
|
|
else if (expired == null) {
|
|
application.invokeLater(runnable)
|
|
}
|
|
else {
|
|
application.invokeLater(runnable, expired)
|
|
}
|
|
}
|
|
|
|
@JvmStatic
|
|
fun getFrameClass(): String {
|
|
val name = ApplicationNamesInfo.getInstance().fullProductNameWithEdition.lowercase()
|
|
.replace(' ', '-')
|
|
.replace("intellij-idea", "idea").replace("android-studio", "studio") // backward compatibility
|
|
.replace("-community-edition", "-ce").replace("-ultimate-edition", "").replace("-professional-edition", "")
|
|
var wmClass = if (name.startsWith(VENDOR_PREFIX)) name else VENDOR_PREFIX + name
|
|
if (PluginManagerCore.isRunningFromSources()) wmClass += "-debug"
|
|
return wmClass
|
|
}
|
|
|
|
@JvmStatic
|
|
fun showConsentsAgreementIfNeeded(log: Logger, filter: Predicate<in Consent?>): Boolean {
|
|
val (first, second) = ConsentOptions.getInstance().getConsents(filter)
|
|
if (!second) {
|
|
return false
|
|
}
|
|
else if (EventQueue.isDispatchThread()) {
|
|
return confirmConsentOptions(first)
|
|
}
|
|
else {
|
|
var result = false
|
|
try {
|
|
EventQueue.invokeAndWait { result = confirmConsentOptions(first) }
|
|
}
|
|
catch (e: InterruptedException) {
|
|
log.warn(e)
|
|
}
|
|
catch (e: InvocationTargetException) {
|
|
log.warn(e)
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
@JvmStatic
|
|
fun updateForDarcula(isDarcula: Boolean) {
|
|
JBColor.setDark(isDarcula)
|
|
setUseDarkIcons(isDarcula)
|
|
}
|
|
|
|
@JvmStatic
|
|
fun confirmConsentOptions(consents: List<Consent>): Boolean {
|
|
if (consents.isEmpty()) {
|
|
return false
|
|
}
|
|
|
|
val ui = ConsentSettingsUi(false)
|
|
val dialog = object : DialogWrapper(true) {
|
|
override fun createContentPaneBorder(): Border? = null
|
|
|
|
override fun createSouthPanel(): JComponent? {
|
|
val southPanel = super.createSouthPanel()
|
|
if (southPanel != null) {
|
|
southPanel.border = createDefaultBorder()
|
|
}
|
|
return southPanel
|
|
}
|
|
|
|
override fun createCenterPanel() = ui.component
|
|
|
|
override fun createActions(): Array<Action> {
|
|
if (consents.size > 1) {
|
|
val actions = super.createActions()
|
|
setOKButtonText(IdeBundle.message("button.save"))
|
|
setCancelButtonText(IdeBundle.message("button.skip"))
|
|
return actions
|
|
}
|
|
setOKButtonText(consents.iterator().next().name)
|
|
return arrayOf(okAction, object : DialogWrapperAction(IdeBundle.message("button.do.not.send")) {
|
|
override fun doAction(e: ActionEvent) {
|
|
close(NEXT_USER_EXIT_CODE)
|
|
}
|
|
})
|
|
}
|
|
|
|
override fun createDefaultActions() {
|
|
super.createDefaultActions()
|
|
init()
|
|
isAutoAdjustable = false
|
|
}
|
|
}
|
|
ui.reset(consents)
|
|
dialog.isModal = true
|
|
dialog.title = IdeBundle.message("dialog.title.data.sharing")
|
|
dialog.pack()
|
|
if (consents.size < 2) {
|
|
dialog.setSize(dialog.window.width, dialog.window.height + scale(75))
|
|
}
|
|
dialog.show()
|
|
val exitCode = dialog.exitCode
|
|
if (exitCode == DialogWrapper.CANCEL_EXIT_CODE) {
|
|
return false // don't save any changes in this case: a user hasn't made a choice
|
|
}
|
|
val result: List<Consent>
|
|
if (consents.size == 1) {
|
|
result = listOf(consents.iterator().next().derive(exitCode == DialogWrapper.OK_EXIT_CODE))
|
|
}
|
|
else {
|
|
result = ArrayList()
|
|
ui.apply(result)
|
|
}
|
|
saveConsents(result)
|
|
return true
|
|
}
|
|
|
|
@JvmStatic
|
|
fun loadConsentsForEditing(): List<Consent> {
|
|
val options = ConsentOptions.getInstance()
|
|
var result = options.consents.first
|
|
if (options.isEAP) {
|
|
val statConsent = options.defaultUsageStatsConsent
|
|
if (statConsent != null) {
|
|
// init stats consent for EAP from the dedicated location
|
|
val consents = result
|
|
result = ArrayList()
|
|
result.add(statConsent.derive(UsageStatisticsPersistenceComponent.getInstance().isAllowed))
|
|
result.addAll(consents)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
@JvmStatic
|
|
fun saveConsents(consents: List<Consent>) {
|
|
if (consents.isEmpty()) {
|
|
return
|
|
}
|
|
|
|
val options = ConsentOptions.getInstance()
|
|
if (ApplicationManager.getApplication() != null && options.isEAP) {
|
|
val isUsageStats = ConsentOptions.condUsageStatsConsent()
|
|
var saved = 0
|
|
for (consent in consents) {
|
|
if (isUsageStats.test(consent)) {
|
|
UsageStatisticsPersistenceComponent.getInstance().isAllowed = consent.isAccepted
|
|
saved++
|
|
}
|
|
}
|
|
if (consents.size - saved > 0) {
|
|
options.setConsents(consents.filter { !isUsageStats.test(it) })
|
|
}
|
|
}
|
|
else {
|
|
options.setConsents(consents)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Targets the component to a (screen) device before showing.
|
|
* In case the component is already a part of the UI hierarchy (and is thus bound to a device), the method does nothing.
|
|
*
|
|
* The prior targeting to a device is required when there's a need to calculate the preferred size of a compound component
|
|
* (such as `JEditorPane`, for instance) which is not yet added to a hierarchy.
|
|
* The calculation in that case may involve device-dependent metrics (such as font metrics)
|
|
* and thus should refer to a particular device in multi-monitor env.
|
|
*
|
|
* Note that if after calling this method the component is added to another hierarchy bound to a different device,
|
|
* AWT will throw `IllegalArgumentException`.
|
|
* To avoid that, the device should be reset by calling `targetToDevice(comp, null)`.
|
|
*
|
|
* @param target the component representing the UI hierarchy and the target device
|
|
* @param comp the component to target
|
|
*/
|
|
@JvmStatic
|
|
fun targetToDevice(comp: Component, target: Component?) {
|
|
if (comp.isShowing) {
|
|
return
|
|
}
|
|
val gc = target?.graphicsConfiguration
|
|
AWTAccessor.getComponentAccessor().setGraphicsConfiguration(comp, gc)
|
|
}
|
|
|
|
@JvmStatic
|
|
fun isInFullScreen(window: Window?): Boolean = window is IdeFrame && (window as IdeFrame).isInFullScreen
|
|
|
|
fun adjustFractionalMetrics(defaultValue: Any): Any {
|
|
if (!SystemInfoRt.isMac || GraphicsEnvironment.isHeadless()) {
|
|
return defaultValue
|
|
}
|
|
val gc = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration
|
|
return if (sysScale(gc) == 1.0f) RenderingHints.VALUE_FRACTIONALMETRICS_OFF else defaultValue
|
|
}
|
|
}
|